Files
morluto 60149282fc test: strengthen integration oracles and verification (#8295)
* test: strengthen integration oracles and verification

* test: bound lifecycle gates and strengthen workflow checks

* test: verify complete paged and extracted results

* test: preserve bytecode and cancellation evidence

* test: strengthen retained evidence and fixture cleanup

* test: require cross-projection bridge identities

* test: retain fixture owners through cleanup failures

* test: verify graph examples and project selectors

* test: tighten provider and lifecycle regressions

* test: consolidate hex coverage and bound cutter cleanup

* test: bound subprocess output waits and strengthen graph checks

* test: check mcp recovery and preserve cleanup owners

* test: verify structured mcp argument failures

* test: preserve artifact cleanup and gdb limit evidence

* test: preserve evidence identities and bound provider gates

* test: retain browser and comparison cleanup owners

* test: bound startup and strengthen fixture oracles

* test: bound lifecycle gates and verify recovered evidence

* test: verify public outcomes and lifecycle ownership

* test: assert precise evidence and release lifecycle gates

* test: verify selected facts and bound cleanup waits

* test: check retained evidence and decoded records

* test: verify final reports and retain cleanup ownership

* test: bound reviewed startup gates and retain cleanup

* test: verify PE errors and preserve MCP cleanup

* test: preserve MCP fixture cleanup ownership

* test: release provider gates before late startup
2026-11-01 00:15:28 +08:00

146 lines
4.6 KiB
TypeScript

import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";
import { z } from "zod";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { processTest } from "../../support/process/processFixture.js";
processTest(
"returns a nonzero exit for a streamed JSON analysis failure",
async ({ processes }) => {
const root = await createTestTempDirectory("rea-cli-js-missing-");
const result = await processes.run(
process.execPath,
[
"scripts/rea.mjs",
"analyze-javascript-application",
join(root, "missing"),
"--json",
],
{ timeoutMs: 20_000 },
);
expect(result.exitCode).toBe(1);
expect(JSON.parse(result.stdout)).toMatchObject({
code: "invalid_request",
details: { issues: [{ path: ["input_path"], reason: "invalid_value" }] },
});
},
);
// `analyze` routes a JavaScript directory to the same workflow and must keep
// its typed cancellation contract.
processTest.skipIf(process.platform === "win32").for([
{
command: "analyze-javascript-application",
phase: "analyze_javascript_application",
parsedFiles: null,
},
{
command: "analyze",
phase: "analyze_javascript_application",
parsedFiles: null,
},
{
command: "analyze-javascript-application",
phase: "build_javascript_application_graph",
parsedFiles: 1,
},
{
command: "analyze-javascript-application",
phase: "hash_javascript_application_evidence",
parsedFiles: 1,
},
])(
"returns typed cancellation and exit 130 through $command during $phase",
async ({ command, phase, parsedFiles }, { processes }) => {
const root = await createTestTempDirectory("rea-cli-js-cancellation-");
await writeFile(join(root, "main.js"), "export const observed = 1;\n");
const nodeArguments: string[] = [];
if (parsedFiles !== null) {
// A tiny graph/hash phase may finish before the parent receives stderr.
// Invoke the real signal handler synchronously in the owned CLI child at
// that progress boundary; the two early-phase cases still use OS SIGINT.
const controls = await createTestTempDirectory("rea-cli-cancel-control-");
const preload = join(controls, "cancel-at-phase.mjs");
await writeFile(
preload,
`
const write = process.stderr.write.bind(process.stderr);
let requested = false;
process.stderr.write = function(chunk, ...args) {
const written = write(chunk, ...args);
let update;
try { update = JSON.parse(String(chunk)); } catch {}
if (!requested && update?.rea_progress?.phase === ${JSON.stringify(phase)}) {
requested = true;
process.emit("SIGINT");
}
return written;
};
`,
);
nodeArguments.push("--import", preload);
}
const child = processes.spawn(process.execPath, [
...nodeArguments,
"scripts/rea.mjs",
command,
root,
"--json",
]);
child.stdin.end();
let stdout = "",
partial = "",
requested = false;
const progressSchema = z.object({
rea_progress: z.object({ phase: z.string() }),
});
child.stdout.on("data", (chunk: Buffer) => {
stdout += chunk.toString();
});
child.stderr.on("data", (chunk: Buffer) => {
partial += chunk.toString();
let newline: number;
while ((newline = partial.indexOf("\n")) >= 0) {
const line = partial.slice(0, newline);
partial = partial.slice(newline + 1);
const parsed = progressSchema.safeParse(JSON.parse(line));
if (
parsed.success &&
parsed.data.rea_progress.phase === phase &&
!requested
) {
requested = true;
if (parsedFiles === null) child.kill("SIGINT");
}
}
});
const exited = await new Promise<{
code: number | null;
signal: NodeJS.Signals | null;
}>((resolve, reject) => {
child.once("error", reject);
child.once("close", (code, signal) => resolve({ code, signal }));
});
expect(requested).toBe(true);
expect(exited).toEqual({ code: 130, signal: null });
expect(JSON.parse(stdout)).toMatchObject({
code: "cancelled",
details: { reason: "cancelled" },
});
if (parsedFiles !== null)
expect(JSON.parse(stdout)).toMatchObject({
details: {
partial_observation: {
normalized_result: {
statistics: {
parsed_javascript_files: parsedFiles,
parse_failures: 0,
},
},
},
},
});
},
);