Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/ultra/__tests__/interp.test.ts

Raw
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("<r#0,r#2,r#3,r#4>");
		expect(rendered).toContain("item=performance<r#4>");
	});
});

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);
	});
});