import { describe, expect, it } from "vitest"; import { type InterpContext, resolveOver, resolveOverWithSources, resolveSelector, resolveTemplate, resolveTemplateWithSources, SpecValidationError, } from "../interp.ts"; // One shared null-containing fixture drives the "line-197" property: // the same `null` is RETAINED positionally for plain `{r.results}` but // DROPPED under flatten `{r.results[].findings[]}` and `{... | where real}`. // It also carries a missing-`findings` row and a missing-`real` row so the // "missing FIELD" skip is exercised for both flatten and where. function fixture(): InterpContext { return { args: { topic: "security", count: 3, tags: ["a", "b"] }, item: "performance", results: { r: [ { real: true, findings: ["a1", "a2"] }, // index 0: kept, 2 findings null, // index 1: the retained/dropped null { real: false, findings: ["b1"] }, // index 2: real falsy { real: true }, // index 3: truthy but MISSING findings { findings: ["c1"] }, // index 4: has findings but MISSING real ], }, failures: { r: [ { code: "transport", retryable: true, message: "rate limited" }, { code: "auth", retryable: false, message: "missing key" }, ], }, }; } describe("resolveTemplate", () => { it("substitutes {args.NAME}", () => { expect(resolveTemplate("topic is {args.topic}", fixture())).toBe( "topic is security", ); }); it("renders {item} string verbatim", () => { expect(resolveTemplate("do {item}", fixture())).toBe("do performance"); }); it("renders {item} object as compact JSON", () => { const ctx: InterpContext = { item: { file: "a.ts", line: 3 } }; expect(resolveTemplate("Finding: {item}", ctx)).toBe( 'Finding: {"file":"a.ts","line":3}', ); }); it("renders a non-string arg (number) as JSON", () => { expect(resolveTemplate("n={args.count}", fixture())).toBe("n=3"); }); it("substitutes multiple tokens in one template", () => { expect(resolveTemplate("{args.topic}/{item}", fixture())).toBe( "security/performance", ); }); it("renders prior phase failures", () => { expect(resolveTemplate("repair {r.failures}", fixture())).toContain( '"code":"transport"', ); }); it("throws SpecValidationError on an unknown token", () => { expect(() => resolveTemplate("x {foo.bar | map x} y", fixture())).toThrow( SpecValidationError, ); }); }); describe("source attribution", () => { it("preserves producer positions through flattening and filtering", () => { const flattened = resolveOverWithSources( "{r.results[].findings[]}", fixture(), ); expect(flattened).toEqual([ { value: "a1", sources: [{ phase: "r", index: 0 }] }, { value: "a2", sources: [{ phase: "r", index: 0 }] }, { value: "b1", sources: [{ phase: "r", index: 2 }] }, { value: "c1", sources: [{ phase: "r", index: 4 }] }, ]); const filtered = resolveOverWithSources( "{r.results | where real}", fixture(), ); expect(filtered.map((item) => item.sources)).toEqual([ [{ phase: "r", index: 0 }], [{ phase: "r", index: 3 }], ]); }); it("unions member producers for grouped fanout items", () => { const grouped = resolveOverWithSources("{r.results | groupBy source}", { results: { r: [ { source: "sessions", id: 1 }, { source: "repository", id: 2 }, { source: "sessions", id: 3 }, ], }, }); expect(grouped[0].sources).toEqual([ { phase: "r", index: 0 }, { phase: "r", index: 2 }, ]); expect(grouped[1].sources).toEqual([{ phase: "r", index: 1 }]); }); it("reports sources to prompt rendering without changing ordinary values", () => { const rendered = resolveTemplateWithSources( "topic={args.topic}; prior={r.results}; item={item}", { ...fixture(), itemSources: [{ phase: "r", index: 4 }], }, (content, sources) => `${content}<${sources.map(({ phase, index }) => `${phase}#${index}`).join(",")}>`, ); expect(rendered).toContain("topic=security<>"); expect(rendered).toContain(""); expect(rendered).toContain("item=performance"); }); }); describe("resolveSelector", () => { it("retains nulls positionally for plain {PHASE.results}", () => { const out = resolveSelector("{r.results}", fixture()); expect(Array.isArray(out)).toBe(true); expect(out).toHaveLength(5); expect((out as unknown[])[1]).toBeNull(); expect((out as unknown[])[0]).toEqual({ real: true, findings: ["a1", "a2"], }); }); it("flatten {PHASE.results[].FIELD[]} collects arrays and skips null elements + missing FIELDs", () => { // null (idx1) skipped; idx3 missing findings skipped; rest flattened. const out = resolveSelector("{r.results[].findings[]}", fixture()); expect(out).toEqual(["a1", "a2", "b1", "c1"]); }); it("{EXPR | where FIELD} keeps truthy FIELD, skips null elements + falsy + missing FIELD", () => { // idx0 real:true kept; idx1 null skipped; idx2 real:false skipped; // idx3 real:true kept; idx4 missing real skipped. const out = resolveSelector("{r.results | where real}", fixture()); expect(out).toEqual([ { real: true, findings: ["a1", "a2"] }, { real: true }, ]); }); it("where supports exact string equality", () => { const ctx: InterpContext = { results: { r: [ { source: "sessions", id: 1 }, { source: "repository", id: 2 }, { source: "sessions", id: 3 }, { source: "a|b", id: 4 }, { source: 7, id: 5 }, { id: 6 }, ], }, }; expect( resolveSelector('{r.results | where source == "sessions"}', ctx), ).toEqual([ { source: "sessions", id: 1 }, { source: "sessions", id: 3 }, ]); expect(resolveSelector('{r.results | where source == "a|b"}', ctx)).toEqual( [{ source: "a|b", id: 4 }], ); }); it("groupBy groups string fields in first-seen order", () => { const ctx: InterpContext = { results: { r: [ { source: "sessions", id: 1 }, { source: "repository", id: 2 }, { source: "sessions", id: 3 }, { source: 7, id: 4 }, { id: 5 }, ], }, }; expect(resolveSelector("{r.results | groupBy source}", ctx)).toEqual([ { key: "sessions", items: [ { source: "sessions", id: 1 }, { source: "sessions", id: 3 }, ], }, { key: "repository", items: [{ source: "repository", id: 2 }] }, ]); }); it("where equality is strict, case-sensitive, and supports empty values", () => { const ctx: InterpContext = { results: { r: [ { source: "sessions", id: 1 }, { source: "Sessions", id: 2 }, { source: "", id: 3 }, { source: false, id: 4 }, { source: 0, id: 5 }, { source: null, id: 6 }, { id: 7 }, ], }, }; expect( resolveSelector('{r.results | where source == "Sessions"}', ctx), ).toEqual([{ source: "Sessions", id: 2 }]); expect(resolveSelector('{r.results | where source == ""}', ctx)).toEqual([ { source: "", id: 3 }, ]); expect(resolveSelector('{r.results | where source == "0"}', ctx)).toEqual( [], ); }); it("where equality accepts surrounding whitespace", () => { const ctx: InterpContext = { results: { r: [{ source: "sessions" }, { source: "repository" }] }, }; expect( resolveSelector('{r.results | where source == "sessions"}', ctx), ).toEqual([{ source: "sessions" }]); }); it("where equality supports escaped strings and pipes", () => { const ctx: InterpContext = { results: { r: [ { source: 'a"b', id: 1 }, { source: "a\\b", id: 2 }, { source: "a|b", id: 3 }, ], }, }; expect( resolveSelector(String.raw`{r.results | where source == "a\"b"}`, ctx), ).toEqual([{ source: 'a"b', id: 1 }]); expect( resolveSelector(String.raw`{r.results | where source == "a\\b"}`, ctx), ).toEqual([{ source: "a\\b", id: 2 }]); expect(resolveSelector('{r.results | where source == "a|b"}', ctx)).toEqual( [{ source: "a|b", id: 3 }], ); }); it("where equality rejects non-string and malformed operands", () => { const invalid = [ "{r.results | where source == sessions}", `{r.results | where source != "sessions"}`, `{r.results | where source == 'sessions'}`, String.raw`{r.results | where source == "bad\q"}`, `{r.results | where source == "unterminated}`, ]; for (const selector of invalid) expect(() => resolveSelector(selector, fixture())).toThrow( SpecValidationError, ); }); it("where and groupBy reject non-array sources and unknown phases", () => { for (const selector of [ "{item | where real}", '{item | where real == "yes"}', "{item | groupBy source}", "{missing.results | groupBy source}", ]) expect(() => resolveSelector(selector, fixture())).toThrow( SpecValidationError, ); }); it("groupBy handles empty keys and skips invalid keys", () => { const ctx: InterpContext = { results: { r: [ { source: "", id: 1 }, { source: "__proto__", id: 2 }, { source: "", id: 3 }, { source: "__proto__", id: 4 }, { source: null, id: 5 }, { source: false, id: 6 }, { source: ["sessions"], id: 7 }, { id: 8 }, ], }, }; expect(resolveSelector("{r.results | groupBy source}", ctx)).toEqual([ { key: "", items: [ { source: "", id: 1 }, { source: "", id: 3 }, ], }, { key: "__proto__", items: [ { source: "__proto__", id: 2 }, { source: "__proto__", id: 4 }, ], }, ]); }); it("groupBy returns an empty array for empty or entirely invalid input", () => { expect( resolveSelector("{r.results | groupBy source}", { results: { r: [] } }), ).toEqual([]); expect( resolveSelector("{r.results | groupBy source}", { results: { r: [null, { source: 1 }, { id: 2 }] }, }), ).toEqual([]); }); it("groupBy renders as structured JSON in templates", () => { const output = resolveTemplate("groups={r.results | groupBy source}", { results: { r: [ { source: "sessions", id: 1 }, { source: "web", id: 2 }, ], }, }); expect(output).toBe( 'groups=[{"key":"sessions","items":[{"source":"sessions","id":1}]},{"key":"web","items":[{"source":"web","id":2}]}]', ); }); it("rejects chained and repeated selector operators", () => { for (const selector of [ "{r.results | groupBy source | where key}", "{r.results | where source | groupBy source}", ]) expect(() => resolveSelector(selector, fixture())).toThrow( SpecValidationError, ); }); it("resolves and filters {PHASE.failures}", () => { expect(resolveSelector("{r.failures}", fixture())).toHaveLength(2); expect( resolveSelector("{r.failures | where retryable}", fixture()), ).toEqual([ { code: "transport", retryable: true, message: "rate limited" }, ]); }); it("rejects an unknown phase failure reference", () => { expect(() => resolveSelector("{missing.failures}", fixture())).toThrow( SpecValidationError, ); }); it("resolves {args.NAME}", () => { expect(resolveSelector("{args.tags}", fixture())).toEqual(["a", "b"]); }); it("resolves {item}", () => { expect(resolveSelector("{item}", fixture())).toBe("performance"); }); it("throws SpecValidationError on an unknown token {foo.bar | map x}", () => { expect(() => resolveSelector("{foo.bar | map x}", fixture())).toThrow( SpecValidationError, ); }); it("throws SpecValidationError on an unknown filter", () => { expect(() => resolveSelector("{r.results | filter real}", fixture()), ).toThrow(SpecValidationError); }); it("throws SpecValidationError on a non-brace-wrapped selector", () => { expect(() => resolveSelector("r.results", fixture())).toThrow( SpecValidationError, ); }); it("throws SpecValidationError on a phase reference that is not present", () => { expect(() => resolveSelector("{missing.results}", fixture())).toThrow( SpecValidationError, ); }); it("projects scalar, object, and array fields in result order", () => { const ctx: InterpContext = { results: { r: [ { report: { summary: "first" } }, { report: ["nested", "array"] }, { report: false }, { report: 0 }, { report: "" }, ], }, }; expect(resolveSelector("{r.results[].report}", ctx)).toEqual([ { summary: "first" }, ["nested", "array"], false, 0, "", ]); }); it("skips null, missing, and non-record values under projection", () => { const ctx: InterpContext = { results: { r: [null, "not-a-record", { report: null }, {}, { report: "ok" }], }, }; expect(resolveSelector("{r.results[].report}", ctx)).toEqual(["ok"]); }); it("skips a non-array FIELD under flatten", () => { const ctx: InterpContext = { results: { r: [{ findings: "not-an-array" }, { findings: ["ok"] }] }, }; expect(resolveSelector("{r.results[].findings[]}", ctx)).toEqual(["ok"]); }); }); describe("line-197 shared-fixture property", () => { it("the same null is retained for plain results but dropped under flatten and where", () => { const ctx = fixture(); const plain = resolveSelector("{r.results}", ctx) as unknown[]; const flat = resolveSelector("{r.results[].findings[]}", ctx) as unknown[]; const filtered = resolveSelector( "{r.results | where real}", ctx, ) as unknown[]; // plain: null present positionally expect(plain).toHaveLength(5); expect(plain[1]).toBeNull(); // flatten + where: null never appears expect(flat.some((x) => x === null)).toBe(false); expect(filtered.some((x) => x === null)).toBe(false); expect(flat).toEqual(["a1", "a2", "b1", "c1"]); expect(filtered).toEqual([ { real: true, findings: ["a1", "a2"] }, { real: true }, ]); }); }); describe("resolveOver", () => { it("returns an array literal unchanged", () => { const lit = ["correctness", "security", "performance"]; expect(resolveOver(lit, {})).toEqual(lit); }); it("resolves grouped results as a fanout input", () => { expect( resolveOver("{r.results | groupBy source}", { results: { r: [ { source: "sessions", id: 1 }, { source: "repository", id: 2 }, ], }, }), ).toEqual([ { key: "sessions", items: [{ source: "sessions", id: 1 }] }, { key: "repository", items: [{ source: "repository", id: 2 }] }, ]); }); it("resolves failures as a recovery fanout", () => { expect(resolveOver("{r.failures}", fixture())).toEqual( fixture().failures?.r, ); }); it("resolves a field projection as a fanout input", () => { expect( resolveOver("{r.results[].findings}", { results: { r: [{ findings: ["a"] }, { findings: ["b"] }] }, }), ).toEqual([["a"], ["b"]]); }); it("resolves an interpolation expression to its array", () => { expect(resolveOver("{r.results[].findings[]}", fixture())).toEqual([ "a1", "a2", "b1", "c1", ]); }); it("throws SpecValidationError when an interpolation does not resolve to an array", () => { // {item} resolves to a string, not an array expect(() => resolveOver("{item}", fixture())).toThrow(SpecValidationError); }); it("throws SpecValidationError when over is neither an array nor a string", () => { expect(() => resolveOver(42, fixture())).toThrow(SpecValidationError); }); });