import test from "node:test"; import assert from "node:assert/strict"; import { mkdtemp, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { runOverflowQuery } from "../src/query.ts"; import { storeEntry } from "../src/store.ts"; import { readOverflowSettings, type OverflowSettings } from "../settings.ts"; function settings(overrides: Partial = {}): OverflowSettings { return { ...readOverflowSettings("/nonexistent/pi-overflow.json"), ...overrides }; } async function store(text: string, id = "0123456789ab") { const root = await mkdtemp(join(tmpdir(), "pi-overflow-query-")); await storeEntry(root, id, { kind: "text", text }, { tool: "bash", args: { command: "run" } }, 1_000_000); return root; } test("search returns hits with merged context", async () => { const lines = Array.from({ length: 100 }, (_, i) => (i === 9 || i === 11 || i === 50 ? `line ${i + 1} FAIL` : `line ${i + 1}`)); const root = await store(lines.join("\n")); const result = await runOverflowQuery(root, { id: "0123456789ab", search: "FAIL", context: 1 }, settings()); assert.ok(!result.error); assert.match(result.text, /L10: line 10 FAIL/); assert.match(result.text, /L12: line 12 FAIL/); assert.match(result.text, /L51: line 51 FAIL/); // hits at lines 10 and 12 with context 1 merge into one range 9-13 assert.match(result.text, /--- lines 9-13 ---/); assert.match(result.text, /--- lines 50-52 ---/); }); test("line-range retrieval returns numbered lines", async () => { const lines = Array.from({ length: 50 }, (_, i) => `row ${i + 1}`); const root = await store(lines.join("\n")); const result = await runOverflowQuery(root, { id: "0123456789ab", lines: { start: 10, end: 15 } }, settings()); assert.match(result.text, /--- lines 10-15 ---/); assert.match(result.text, /L10: row 10/); assert.match(result.text, /L15: row 15/); assert.ok(!result.text.includes("row 16")); }); test("jsonPath extracts nested values", async () => { const doc = JSON.stringify({ snapshot: { nodes: [{ name: "a" }, { name: "b" }, { name: "c" }] } }); const root = await store(doc); const result = await runOverflowQuery(root, { id: "0123456789ab", jsonPath: "snapshot.nodes[2].name" }, settings()); assert.match(result.text, /"c"/); }); test("invalid id, regex, JSON and path return clear errors without throwing", async () => { const root = await store('{"a": {"b": 1}}'); assert.ok((await runOverflowQuery(root, { id: "../etc/passwd" }, settings())).error); assert.ok((await runOverflowQuery(root, { id: "zzzzzzzzzzzz" }, settings())).error); assert.ok((await runOverflowQuery(root, { id: "0123456789ab", search: "([" }, settings())).error); const goodPath = await runOverflowQuery(root, { id: "0123456789ab", jsonPath: "a.b" }, settings()); assert.ok(!goodPath.error, "valid path on valid JSON must succeed"); const badPath = await runOverflowQuery(root, { id: "0123456789ab", jsonPath: "a.b.c!!" }, settings()); assert.ok(badPath.error); const missing = await runOverflowQuery(root, { id: "ffffffffffff" }, settings()); assert.ok(missing.error); }); test("non-JSON content + jsonPath reports the parse failure", async () => { const root = await store("not json at all"); const result = await runOverflowQuery(root, { id: "0123456789ab", jsonPath: "a" }, settings()); assert.ok(result.error); assert.match(result.text, /not valid JSON/); }); test("output is bounded by thresholdChars and marked truncated", async () => { const lines = Array.from({ length: 5000 }, (_, i) => `line ${i + 1} pad ${"p".repeat(40)}`); const root = await store(lines.join("\n")); const result = await runOverflowQuery( root, { id: "0123456789ab", lines: { start: 1, end: 5000 } }, settings({ thresholdChars: 4000 }), ); assert.ok(result.text.length <= 4000); assert.equal(result.truncated, true); assert.match(result.text, /truncated/); }); test("lines and search output clip each line to maxLineChars", async () => { const long = `row ${"q".repeat(2_000)}`; const root = await store(`first\n${long}\n${long} ERROR\nlast`); const s = settings({ maxLineChars: 60 }); const lines = await runOverflowQuery(root, { id: "0123456789ab", lines: { start: 2, end: 3 } }, s); assert.ok(lines.text.includes("q".repeat(56) + "…")); assert.ok(!lines.text.includes("q".repeat(61))); const search = await runOverflowQuery(root, { id: "0123456789ab", search: "ERROR", context: 0 }, s); assert.ok(search.text.includes("q".repeat(56) + "…")); assert.ok(!search.text.includes("q".repeat(61))); }); test("search only scans the first 10000 chars of each line", async () => { const earlyHit = `hit ${"a".repeat(100)} FAIL`; const lateHit = `${"a".repeat(10_500)} FAIL`; const root = await store(`${earlyHit}\n${lateHit}\nplain`); const result = await runOverflowQuery(root, { id: "0123456789ab", search: "FAIL", context: 0 }, settings()); assert.match(result.text, /L1: hit /); assert.ok(!result.text.includes("L2:"), "match beyond char 10000 must not be reported"); }); test("default query returns metadata and a first-screen view", async () => { const lines = Array.from({ length: 300 }, (_, i) => `row ${i + 1}`); const root = await store(lines.join("\n")); const result = await runOverflowQuery(root, { id: "0123456789ab" }, settings()); assert.ok(!result.error); assert.match(result.text, /\[pi-overflow id=0123456789ab tool=bash \d+ lines \/ \d+ bytes\]/); assert.match(result.text, /--- lines 1-40 ---/); assert.ok(result.text.includes("row 300"), "tail of the stored file must appear"); });