mirror of
https://github.com/morluto/rea.git
synced 2026-11-10 16:57:19 +00:00
* refactor(analysis): centralize resource budgets and recovery facts * feat(analysis): expose ASAR and Ghidra runtime admission controls * feat(artifacts): expose reader admission and preserve limited recognition * feat(javascript): expose recoverable semantic precision budgets * feat(web): add recoverable source-map budgets and compact retained views * feat(web): make historical capture decoding budgets recoverable * fix(desktop): enforce selected view policies across capture and export * feat(runtime): make Frida and LLDB retention budgets selectable * feat(native): expose diagnostic budgets and preserve exhaustion causes * feat(managed): expose recoverable snapshot and representation budgets * feat(metadata): make Go and Rust inspection budgets recoverable * feat(wasm): expose representation and producer admissions * feat(evm): expose per-call worker resource budgets * feat(android): make resource and string-pool omissions recoverable * feat(producers): expose command and recorded-capture budgets * docs(analysis): record recovery contracts and integrated validation * fix(docs): link calibration oracle from published verification pages * fix(docs): restore desktop task anchors and receipt links * test(analysis): align fixtures with selectable budget contracts * fix(analysis): remove source-location projector dependency cycle * test(analysis): refresh budget recovery and verifier regressions * test(analysis): preserve format and publication recovery expectations * test(windows): align conformance with owned runtime admission * fix(desktop): preserve selected alias kinds in exported state * test(analysis): align provider fixtures with recoverable budgets * test(desktop): validate emitted alias metadata structurally * fix(ghidra): classify bounded Windows endpoint refusals * test(cli): assert selectable artifact admission recovery * fix(cli): preserve capture provenance in environment builds
365 lines
12 KiB
TypeScript
365 lines
12 KiB
TypeScript
import { readFile, writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { expect, it } from "vitest";
|
|
import { z } from "zod";
|
|
|
|
import { parseEvidence, type Evidence } from "../../../src/domain/evidence.js";
|
|
import { parseEvidenceBundle } from "../../../src/domain/evidenceBundle.js";
|
|
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
|
|
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
|
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { cliTest, type SharedMcpCall } from "../../support/cli/cliFixture.js";
|
|
|
|
const cases = [
|
|
{
|
|
name: "semantic evaluator precision",
|
|
args: ["--semantic-precision", '{"primitive_candidates":512}'],
|
|
controls: { semantic_precision: { primitive_candidates: 512 } },
|
|
},
|
|
{
|
|
name: "both limits",
|
|
args: ["--max-heap-mb", "128", "--analysis-timeout-ms", "30000"],
|
|
controls: { max_heap_mb: 128, analysis_timeout_ms: 30000 },
|
|
},
|
|
{
|
|
name: "heap limit alone",
|
|
args: ["--max-heap-mb", "256"],
|
|
controls: { max_heap_mb: 256 },
|
|
},
|
|
{
|
|
name: "timeout alone",
|
|
args: ["--analysis-timeout-ms", "40000"],
|
|
controls: { analysis_timeout_ms: 40000 },
|
|
},
|
|
{
|
|
name: "omitted limits",
|
|
args: [],
|
|
controls: {},
|
|
},
|
|
];
|
|
|
|
cliTest(
|
|
"names the server deadline control in CLI timeout recovery",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-js-cli-timeout-recovery-");
|
|
await writeFile(join(root, "main.js"), "export const observed = 7;\n");
|
|
const response = await cli.run({
|
|
arguments: [
|
|
"analyze-javascript-application",
|
|
root,
|
|
"--max-heap-mb",
|
|
"128",
|
|
"--analysis-timeout-ms",
|
|
"1",
|
|
"--format",
|
|
"json",
|
|
],
|
|
environment: { NODE_OPTIONS: "--max-old-space-size=512" },
|
|
});
|
|
expect(response.exitCode).not.toBe(0);
|
|
expect(response.json).toMatchObject({
|
|
error: "JavaScript application analysis failed",
|
|
code: "provider_timeout",
|
|
message: expect.stringContaining("analysis_timeout_ms"),
|
|
remediation: {
|
|
action: expect.stringContaining(
|
|
"rea analyze-javascript-application <input_path> --analysis-timeout-ms",
|
|
),
|
|
},
|
|
recovery: { kind: "inspect_timeout_state", timeout_ms: 1 },
|
|
});
|
|
},
|
|
);
|
|
|
|
cliTest.for(cases)(
|
|
"preserves $name in CLI Evidence without adding ambient defaults",
|
|
async ({ args, controls }, { cli }) => {
|
|
const root = await createTestTempDirectory("rea-js-limit-provenance-");
|
|
await writeFile(join(root, "main.js"), "export const observed = 7;\n");
|
|
const response = await cli.run({
|
|
arguments: [
|
|
"analyze-javascript-application",
|
|
root,
|
|
...args,
|
|
"--format",
|
|
"json",
|
|
],
|
|
environment: { NODE_OPTIONS: "--max-old-space-size=512" },
|
|
});
|
|
expect(response.exitCode, response.stdout + response.stderr).toBe(0);
|
|
const evidence = parseEvidence(response.json);
|
|
expect(evidence.parameters).toEqual({
|
|
input_path: root,
|
|
format: "auto",
|
|
integrity_policy: "fail",
|
|
...controls,
|
|
});
|
|
},
|
|
);
|
|
|
|
it("preserves MCP analysis limits in retained Evidence and an exported bundle round trip", async () => {
|
|
const root = await createTestTempDirectory("rea-js-limit-mcp-");
|
|
await writeFile(join(root, "main.js"), "export const observed = 7;\n");
|
|
const bundleRoot = await createTestTempDirectory("rea-js-limit-bundle-");
|
|
const path = join(bundleRoot, "evidence.json");
|
|
const { call } = await connectLocalToolsMcp({
|
|
NODE_OPTIONS: "--max-old-space-size=512",
|
|
});
|
|
const expected = new Map<string, unknown>();
|
|
for (const { controls } of cases) {
|
|
const response = await call("analyze_javascript_application", {
|
|
input_path: root,
|
|
...controls,
|
|
detail: "summary",
|
|
});
|
|
expect(response.isError, JSON.stringify(response.content)).not.toBe(true);
|
|
const { parent_evidence_id } = z
|
|
.object({
|
|
normalized_result: z.object({ parent_evidence_id: z.string() }),
|
|
})
|
|
.parse(response.structuredContent).normalized_result;
|
|
expected.set(parent_evidence_id, {
|
|
input_path: root,
|
|
format: "auto",
|
|
integrity_policy: "fail",
|
|
...controls,
|
|
});
|
|
}
|
|
expect(expected.size).toBe(cases.length);
|
|
const retained = await getBundle(call);
|
|
for (const [evidence_id, parameters] of expected)
|
|
expect(
|
|
retained.records.find((record) => record.evidence_id === evidence_id)
|
|
?.parameters,
|
|
).toEqual(parameters);
|
|
const exported = await call("export_evidence_bundle", { path });
|
|
expect(exported.isError).not.toBe(true);
|
|
expect(parseEvidenceBundle(JSON.parse(await readFile(path, "utf8")))).toEqual(
|
|
retained,
|
|
);
|
|
const closed = await call("close_binary", {});
|
|
expect(closed.isError).not.toBe(true);
|
|
expect(await getBundle(call)).toEqual(retained);
|
|
const reset = await call("reset_investigation", {});
|
|
expect(reset.isError).not.toBe(true);
|
|
expect((await getBundle(call)).records).toEqual([]);
|
|
const imported = await call("import_evidence_bundle", { path });
|
|
expect(imported.isError, JSON.stringify(imported.content)).not.toBe(true);
|
|
expect(await getBundle(call)).toEqual(retained);
|
|
}, 60_000);
|
|
|
|
cliTest(
|
|
"retains caller limits and applicable file scopes after an actual analysis timeout",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-js-limit-timeout-");
|
|
const sourcePaths = [
|
|
"a.js",
|
|
"a.js.map",
|
|
"data.json",
|
|
"index.html",
|
|
"package.json",
|
|
"z.node",
|
|
];
|
|
await Promise.all(
|
|
(
|
|
[
|
|
["a.js", "export const observed = 7;\n"],
|
|
[
|
|
"a.js.map",
|
|
JSON.stringify({
|
|
version: 3,
|
|
sources: [],
|
|
names: [],
|
|
mappings: "",
|
|
}),
|
|
],
|
|
["data.json", '{"observed":7}'],
|
|
["index.html", '<script src="./a.js"></script>'],
|
|
["package.json", '{"name":"file-scope-control","main":"a.js"}'],
|
|
["z-excluded.js", "export const excluded = true;\n"],
|
|
["z.node", Buffer.from([0, 1, 2, 3])],
|
|
["zz-excluded.node", Buffer.from([3, 2, 1, 0])],
|
|
] as const
|
|
).map(([path, content]) => writeFile(join(root, path), content)),
|
|
);
|
|
const { client, call } = await connectLocalToolsMcp({
|
|
NODE_OPTIONS: "--max-old-space-size=512",
|
|
});
|
|
const response = await call("analyze_javascript_application", {
|
|
input_path: root,
|
|
max_heap_mb: 128,
|
|
analysis_timeout_ms: 1,
|
|
source_paths: sourcePaths,
|
|
});
|
|
expect(parseMcpToolError(response)).toMatchObject({
|
|
error: {
|
|
code: "provider_timeout",
|
|
message: expect.stringContaining("server/provider deadline"),
|
|
remediation: {
|
|
action: expect.stringContaining("MCP client"),
|
|
},
|
|
recovery: {
|
|
kind: "inspect_timeout_state",
|
|
operation: "analyze_javascript_application",
|
|
timeout_ms: 1,
|
|
},
|
|
},
|
|
});
|
|
const bundle = await getBundle(call);
|
|
const partial = bundle.records.find(
|
|
(record) => record.operation === "analyze_javascript_application",
|
|
);
|
|
expect(partial?.parameters).toEqual({
|
|
input_path: root,
|
|
format: "auto",
|
|
integrity_policy: "fail",
|
|
max_heap_mb: 128,
|
|
analysis_timeout_ms: 1,
|
|
source_paths: sourcePaths,
|
|
});
|
|
if (partial === undefined)
|
|
throw new Error("Timeout Evidence was not retained");
|
|
const cliArguments = [
|
|
"analyze-javascript-application",
|
|
root,
|
|
"--max-heap-mb",
|
|
"128",
|
|
...sourcePaths.flatMap((path) => ["--source-path", path]),
|
|
"--format",
|
|
"json",
|
|
];
|
|
const cliTimeout = await cli.run({
|
|
arguments: [...cliArguments, "--analysis-timeout-ms", "1"],
|
|
});
|
|
expect(cliTimeout.exitCode).toBe(1);
|
|
const cliFailure = z
|
|
.object({
|
|
code: z.literal("provider_timeout"),
|
|
details: z.object({ partial_observation: z.unknown() }),
|
|
})
|
|
.parse(cliTimeout.json);
|
|
const cliPartial = parseEvidence(cliFailure.details.partial_observation);
|
|
const control = await call("analyze_javascript_application", {
|
|
input_path: root,
|
|
max_heap_mb: 128,
|
|
source_paths: sourcePaths,
|
|
});
|
|
expect(control.isError).not.toBe(true);
|
|
const controlEvidence = parseEvidence(control.structuredContent);
|
|
const cliControl = await cli.run({ arguments: cliArguments });
|
|
expect(cliControl.exitCode, cliControl.stderr).toBe(0);
|
|
const cliControlEvidence = parseEvidence(cliControl.json);
|
|
expect(cliControlEvidence.evidence_id).toBe(controlEvidence.evidence_id);
|
|
// Observe both adapters and successful controls before testing applicability.
|
|
for (const [evidence, interrupted] of [
|
|
[partial, true],
|
|
[cliPartial, true],
|
|
[controlEvidence, false],
|
|
[cliControlEvidence, false],
|
|
] as const)
|
|
expectFileAnalysisApplicability(evidence, interrupted);
|
|
await client.ping();
|
|
},
|
|
60_000,
|
|
);
|
|
|
|
cliTest(
|
|
"keeps partial Evidence when a module package source times out",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-js-limit-module-timeout-");
|
|
await Promise.all([
|
|
writeFile(join(root, "package.json"), '{"type":"module"}'),
|
|
writeFile(join(root, "a.js"), "export const observed = 7;\n"),
|
|
]);
|
|
// A module package with .js sources runs preload discovery first, which
|
|
// must leave the timeout to the source pass that retains partial facts.
|
|
const result = await cli.run({
|
|
arguments: [
|
|
"analyze-javascript-application",
|
|
root,
|
|
"--analysis-timeout-ms",
|
|
"1",
|
|
"--format",
|
|
"json",
|
|
],
|
|
});
|
|
expect(result.exitCode).toBe(1);
|
|
const failure = z
|
|
.object({
|
|
code: z.literal("provider_timeout"),
|
|
details: z.object({ partial_observation: z.unknown() }),
|
|
})
|
|
.parse(result.json);
|
|
const partial = parseEvidence(failure.details.partial_observation);
|
|
expect(partial.operation).toBe("analyze_javascript_application");
|
|
expect(partial.limitations).toContainEqual(
|
|
expect.stringMatching(/^Preload discovery could not read a\.js \(/u),
|
|
);
|
|
},
|
|
);
|
|
|
|
async function getBundle(call: SharedMcpCall) {
|
|
const response = await call("get_evidence_bundle", { detail: "complete" });
|
|
expect(response.isError).not.toBe(true);
|
|
return parseEvidenceBundle(
|
|
z.object({ result: z.unknown() }).parse(response.structuredContent).result,
|
|
);
|
|
}
|
|
|
|
function expectFileAnalysisApplicability(
|
|
evidence: Evidence,
|
|
interrupted: boolean,
|
|
) {
|
|
const result = javascriptApplicationAnalysisResultSchema.parse(
|
|
evidence.normalized_result,
|
|
);
|
|
const scopeObservations = result.graph.nodes
|
|
.filter(({ kind }) => kind === "unknown")
|
|
.flatMap(({ observations }) => observations);
|
|
const scopes = (path: string) =>
|
|
scopeObservations.filter(({ properties }) => properties.path === path);
|
|
expect(result.graph.nodes.some(({ kind }) => kind === "native-addon")).toBe(
|
|
true,
|
|
);
|
|
for (const path of ["z.node", "zz-excluded.node"])
|
|
expect(scopes(path), path).toEqual([]);
|
|
const excluded = scopes("z-excluded.js");
|
|
expect(excluded).toHaveLength(1);
|
|
expect(excluded[0]).toMatchObject({
|
|
properties: { operation: "select-javascript-sources" },
|
|
evidence: {
|
|
authority: "ast-static-analysis",
|
|
state: "unavailable",
|
|
confidence: "unknown",
|
|
},
|
|
});
|
|
expect(excluded[0]?.properties).not.toHaveProperty("analysis_failure");
|
|
for (const [path, operation] of [
|
|
["a.js", "analyze-javascript-source"],
|
|
["a.js.map", "parse-local-source-map"],
|
|
["data.json", "parse-json-module"],
|
|
["index.html", "parse-html-script-references"],
|
|
["package.json", "parse-package-json"],
|
|
] as const) {
|
|
if (!interrupted) expect(scopes(path), path).toEqual([]);
|
|
else {
|
|
expect(scopes(path), path).toHaveLength(1);
|
|
expect(scopes(path)[0]).toMatchObject({
|
|
properties: {
|
|
operation,
|
|
analysis_status: path === "a.js" ? "interrupted" : "not-started",
|
|
},
|
|
evidence: {
|
|
authority: path === "a.js" ? "ast-static-analysis" : "artifact-bytes",
|
|
state: "unavailable",
|
|
confidence: "unknown",
|
|
coverage: { status: "partial" },
|
|
},
|
|
});
|
|
}
|
|
}
|
|
if (interrupted) expect(result.graph.coverage.status).toBe("partial");
|
|
else expect(result.statistics.parsed_javascript_files).toBe(1);
|
|
}
|