mirror of
https://github.com/morluto/rea.git
synced 2026-10-28 18:35:53 +00:00
test: overhaul deterministic suite architecture
This commit is contained in:
1 parent
7f2348619a
commit
513d9b1555
371 files changed
+30879
-24389
No files matched your search
@@ -0,0 +1,336 @@
|
||||
import { copyFile, mkdir, rm, writeFile } from "node:fs/promises";
|
||||
import { join, resolve } from "node:path";
|
||||
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
import { analyzeJavaScriptApplication } from "../../../src/application/JavaScriptApplicationService.js";
|
||||
import { parseApplicationGraphEvidence } from "../../../src/application/JavaScriptApplicationEvidenceGraph.js";
|
||||
import { compareJavaScriptExportShapes } from "../../../src/domain/javascriptExportShapeComparison.js";
|
||||
import { javaScriptExportShapeComparisonResultSchema } from "../../../src/domain/javascriptExportShapeComparisonSchemas.js";
|
||||
import {
|
||||
createJavaScriptApplicationGraph,
|
||||
createJavaScriptApplicationNode,
|
||||
} from "../../../src/domain/javascriptApplicationGraph.js";
|
||||
import { permissionAuthorityForRoot } from "../../fixtures/permissionAuthority.js";
|
||||
|
||||
const temporary: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
temporary
|
||||
.splice(0)
|
||||
.map(async (path) => rm(path, { recursive: true, force: true })),
|
||||
);
|
||||
});
|
||||
|
||||
describe("JavaScript export return-shape comparison", () => {
|
||||
it("reports exactly the heading depth addition from source-owned parser fixtures", async () => {
|
||||
const root = await temporaryRoot(temporary);
|
||||
const leftRoot = join(root, "left");
|
||||
const rightRoot = join(root, "right");
|
||||
await Promise.all([mkdir(leftRoot), mkdir(rightRoot)]);
|
||||
await Promise.all([
|
||||
copyFile(
|
||||
resolve("tests/fixtures/replay/parser.mjs"),
|
||||
join(leftRoot, "parser.mjs"),
|
||||
),
|
||||
copyFile(
|
||||
resolve("tests/fixtures/replay/parser-v2.mjs"),
|
||||
join(rightRoot, "parser.mjs"),
|
||||
),
|
||||
]);
|
||||
const [left, right] = await Promise.all([
|
||||
analyzeGraph(leftRoot),
|
||||
analyzeGraph(rightRoot),
|
||||
]);
|
||||
|
||||
const first = compare(left, right);
|
||||
const second = compare(left, right);
|
||||
|
||||
expect(first.comparison_id).toBe(second.comparison_id);
|
||||
expect(() =>
|
||||
javaScriptExportShapeComparisonResultSchema.parse(first),
|
||||
).not.toThrow();
|
||||
expect(first.summary).toEqual({
|
||||
added: 1,
|
||||
removed: 0,
|
||||
changed: 0,
|
||||
unknown: 0,
|
||||
});
|
||||
expect(first.changes).toEqual([
|
||||
expect.objectContaining({
|
||||
status: "added",
|
||||
path: "/depth",
|
||||
discriminant: { path: "/type", value: "heading" },
|
||||
left: { availability: "absent" },
|
||||
right: { availability: "literal", value: 1 },
|
||||
}),
|
||||
]);
|
||||
expect(first.changes.some(({ path }) => path === "/text")).toBe(false);
|
||||
expect(first.coverage).toMatchObject({
|
||||
status: "complete-within-inputs",
|
||||
paired_variants: 3,
|
||||
unpaired_left_variants: 0,
|
||||
unpaired_right_variants: 0,
|
||||
});
|
||||
expect(first.runtime_validation).toEqual({
|
||||
recommended_tool: "run_controlled_replay",
|
||||
automatically_started: false,
|
||||
required_for: "runtime-semantics",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps additions unknown when spread coverage is incomplete", async () => {
|
||||
const [left, right] = await analyzeSources(temporary, {
|
||||
left: `
|
||||
const dynamic = getDynamic();
|
||||
export default () => ({ type: "heading", ...dynamic });
|
||||
`,
|
||||
right: `
|
||||
const dynamic = getDynamic();
|
||||
export default () => ({ type: "heading", depth: 1, ...dynamic });
|
||||
`,
|
||||
});
|
||||
const result = compare(left, right);
|
||||
|
||||
expect(result.changes).toEqual([
|
||||
expect.objectContaining({ status: "unknown", path: "/depth" }),
|
||||
]);
|
||||
expect(result.coverage.status).toBe("partial");
|
||||
});
|
||||
|
||||
it("does not pair ambiguous variants or invent behavior for no-return exports", async () => {
|
||||
const ambiguous = await analyzeSources(temporary, {
|
||||
left: `
|
||||
export default function parse(value) {
|
||||
if (value) return { type: "heading", text: render(value) };
|
||||
return { type: "heading", text: render(value) };
|
||||
}
|
||||
`,
|
||||
right: `export default (value) => ({ type: "heading", text: render(value) });`,
|
||||
});
|
||||
const compared = compare(...ambiguous);
|
||||
expect(compared.summary.unknown).toBe(3);
|
||||
expect(compared.coverage).toMatchObject({
|
||||
status: "partial",
|
||||
paired_variants: 0,
|
||||
unpaired_left_variants: 2,
|
||||
unpaired_right_variants: 1,
|
||||
});
|
||||
|
||||
const noReturn = await analyzeSources(temporary, {
|
||||
left: `export default function stop() { throw new Error("stop"); }`,
|
||||
right: `export default function stop() { throw new Error("stop"); }`,
|
||||
});
|
||||
const unknown = compare(...noReturn);
|
||||
expect(unknown.changes).toEqual([
|
||||
expect.objectContaining({ status: "unknown", path: "" }),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("JavaScript export return-shape selection", () => {
|
||||
it("reports selector candidates and enforces candidate, variant, and change limits", async () => {
|
||||
const candidates = await analyzeSources(temporary, {
|
||||
left: `
|
||||
export const first = () => ({ type: "first" });
|
||||
export const second = () => ({ type: "second" });
|
||||
export const third = () => ({ type: "third" });
|
||||
`,
|
||||
right: `export default () => ({ type: "default" });`,
|
||||
});
|
||||
const missing = compare(candidates[0], candidates[1], {
|
||||
leftExportName: "missing",
|
||||
limits: { max_candidate_exports: 1 },
|
||||
});
|
||||
expect(missing.left).toMatchObject({
|
||||
status: "missing",
|
||||
omitted_candidates: 2,
|
||||
candidates: [expect.objectContaining({ module_path: "parser.mjs" })],
|
||||
});
|
||||
expect(missing.coverage.status).toBe("truncated");
|
||||
|
||||
const parsers = await sourceOwnedParsers(temporary);
|
||||
const variantLimited = compare(parsers[0], parsers[1], {
|
||||
limits: { max_return_variants: 1 },
|
||||
});
|
||||
expect(variantLimited.coverage).toMatchObject({
|
||||
status: "truncated",
|
||||
omitted_left_variants: 2,
|
||||
omitted_right_variants: 2,
|
||||
});
|
||||
|
||||
const changes = await analyzeSources(temporary, {
|
||||
left: `export default () => ({ type: "item" });`,
|
||||
right: `export default () => ({ type: "item", depth: 1, level: 2 });`,
|
||||
});
|
||||
const changeLimited = compare(changes[0], changes[1], {
|
||||
limits: { max_changes: 1 },
|
||||
});
|
||||
expect(changeLimited.summary.added).toBe(2);
|
||||
expect(changeLimited.changes).toHaveLength(1);
|
||||
expect(changeLimited.coverage).toMatchObject({
|
||||
status: "truncated",
|
||||
omitted_changes: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("refuses an exact selector that resolves to multiple graph nodes", async () => {
|
||||
const [left, right] = await analyzeSources(temporary, {
|
||||
left: `export default () => ({ type: "item" });`,
|
||||
right: `export default () => ({ type: "item" });`,
|
||||
});
|
||||
const exported = left.graph.nodes.find((node) =>
|
||||
node.observations.some(
|
||||
({ properties }) => properties.semantic_role === "export-binding",
|
||||
),
|
||||
);
|
||||
if (
|
||||
exported === undefined ||
|
||||
exported.identity.strategy !== "artifact-local-key"
|
||||
)
|
||||
throw new Error("Expected one artifact-local export fixture node");
|
||||
const duplicate = createJavaScriptApplicationNode({
|
||||
kind: exported.kind,
|
||||
identity: {
|
||||
...exported.identity,
|
||||
key: `${exported.identity.key}:duplicate`,
|
||||
},
|
||||
observations: exported.observations.map(
|
||||
({ label, properties, evidence }) => ({ label, properties, evidence }),
|
||||
),
|
||||
});
|
||||
const ambiguousGraph = createJavaScriptApplicationGraph({
|
||||
schema: "JavaScriptApplicationGraph",
|
||||
schema_version: 1,
|
||||
root_node_ids: left.graph.root_node_ids,
|
||||
nodes: [...left.graph.nodes, duplicate],
|
||||
edges: left.graph.edges,
|
||||
coverage: left.graph.coverage,
|
||||
limitations: left.graph.limitations,
|
||||
});
|
||||
const result = compare({ ...left, graph: ambiguousGraph }, right);
|
||||
|
||||
expect(result.left).toMatchObject({
|
||||
status: "ambiguous",
|
||||
selected_node_id: null,
|
||||
candidates: [expect.any(Object), expect.any(Object)],
|
||||
});
|
||||
expect(result.changes).toEqual([
|
||||
expect.objectContaining({ status: "unknown", path: "" }),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("JavaScript export return-shape projection", () => {
|
||||
it("marks graph field projection omissions as truncated", async () => {
|
||||
const properties = [
|
||||
'a_type: "item"',
|
||||
...Array.from(
|
||||
{ length: 70 },
|
||||
(_, index) => `field${String(index)}: ${String(index)}`,
|
||||
),
|
||||
].join(",");
|
||||
const [left, right] = await analyzeSources(temporary, {
|
||||
left: `export default () => ({ ${properties} });`,
|
||||
right: `export default () => ({ ${properties} });`,
|
||||
});
|
||||
const result = compare(left, right);
|
||||
|
||||
expect(result.coverage.status).toBe("truncated");
|
||||
expect(result.coverage.left_omitted_fields).toBeGreaterThan(0);
|
||||
expect(result.coverage.right_omitted_fields).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
type GraphSource = Awaited<ReturnType<typeof analyzeGraph>>;
|
||||
|
||||
const compare = (
|
||||
left: GraphSource,
|
||||
right: GraphSource,
|
||||
options: {
|
||||
readonly leftExportName?: string;
|
||||
readonly rightExportName?: string;
|
||||
readonly limits?: Partial<{
|
||||
readonly max_candidate_exports: number;
|
||||
readonly max_return_variants: number;
|
||||
readonly max_changes: number;
|
||||
}>;
|
||||
} = {},
|
||||
) =>
|
||||
compareJavaScriptExportShapes({
|
||||
left: {
|
||||
evidenceId: left.evidence.evidence_id,
|
||||
graph: left.graph,
|
||||
modulePath: "parser.mjs",
|
||||
exportName: options.leftExportName ?? "default",
|
||||
},
|
||||
right: {
|
||||
evidenceId: right.evidence.evidence_id,
|
||||
graph: right.graph,
|
||||
modulePath: "parser.mjs",
|
||||
exportName: options.rightExportName ?? "default",
|
||||
},
|
||||
limits: {
|
||||
max_candidate_exports: options.limits?.max_candidate_exports ?? 100,
|
||||
max_return_variants: options.limits?.max_return_variants ?? 128,
|
||||
max_changes: options.limits?.max_changes ?? 1_000,
|
||||
},
|
||||
});
|
||||
|
||||
const analyzeGraph = async (root: string) => {
|
||||
const authority = await permissionAuthorityForRoot(
|
||||
root,
|
||||
["investigation_input"],
|
||||
["investigation_input"],
|
||||
);
|
||||
const result = await analyzeJavaScriptApplication(authority, {
|
||||
input_path: root,
|
||||
approved: true,
|
||||
});
|
||||
if (!result.ok) throw result.error;
|
||||
return parseApplicationGraphEvidence(result.value);
|
||||
};
|
||||
|
||||
const analyzeSources = async (
|
||||
temporary: string[],
|
||||
sources: { readonly left: string; readonly right: string },
|
||||
): Promise<[GraphSource, GraphSource]> => {
|
||||
const root = await temporaryRoot(temporary);
|
||||
const leftRoot = join(root, "left");
|
||||
const rightRoot = join(root, "right");
|
||||
await Promise.all([mkdir(leftRoot), mkdir(rightRoot)]);
|
||||
await Promise.all([
|
||||
writeFile(join(leftRoot, "parser.mjs"), sources.left),
|
||||
writeFile(join(rightRoot, "parser.mjs"), sources.right),
|
||||
]);
|
||||
return Promise.all([analyzeGraph(leftRoot), analyzeGraph(rightRoot)]);
|
||||
};
|
||||
|
||||
const sourceOwnedParsers = async (
|
||||
temporary: string[],
|
||||
): Promise<[GraphSource, GraphSource]> => {
|
||||
const root = await temporaryRoot(temporary);
|
||||
const leftRoot = join(root, "left");
|
||||
const rightRoot = join(root, "right");
|
||||
await Promise.all([mkdir(leftRoot), mkdir(rightRoot)]);
|
||||
await Promise.all([
|
||||
copyFile(
|
||||
resolve("tests/fixtures/replay/parser.mjs"),
|
||||
join(leftRoot, "parser.mjs"),
|
||||
),
|
||||
copyFile(
|
||||
resolve("tests/fixtures/replay/parser-v2.mjs"),
|
||||
join(rightRoot, "parser.mjs"),
|
||||
),
|
||||
]);
|
||||
return Promise.all([analyzeGraph(leftRoot), analyzeGraph(rightRoot)]);
|
||||
};
|
||||
|
||||
const temporaryRoot = async (temporary: string[]): Promise<string> => {
|
||||
const root = await createTestTempDirectory("rea-export-shapes-");
|
||||
temporary.push(root);
|
||||
return root;
|
||||
};
|
||||
Reference in new issue
Block a user