mirror of
https://github.com/morluto/rea.git
synced 2026-10-29 10:55:57 +00:00
* fix(comparisons)!: preserve unknowns across partial observations Share absence and digest semantics, keep unobserved managed bodies unknown, and match unique managed type/name/signatures before structural fallback. BREAKING CHANGE: Managed comparison metadata adds exact-signature matching and its count, and name_matching now reports exact-signature-fallback. * fix(javascript): preserve projected result binding semantics Share syntax-aware property reads and assigned-result resolution. Retain destructuring paths and downgrade downstream whole-result relationships instead of resolving nested or wrapped calls as aliases. * fix(native)!: admit complete parser values and preserve typed failures Decode Mach headers by named columns, validate complete slice keys and lipo alignment, and preserve codesign runtime flags and tagged parser errors. Rename the Ghidra test dossier fixture and connect fake-backed suites to real verification lanes. BREAKING CHANGE: Native load-command results no longer contain the unused, always-null file_offset field. * fix(process): preserve host identity and actionable boundary failures Thread selected platform and environment through process and Doctor seams, stamp observed producer identity in Evidence, and retain PTY, filesystem, lease and lazy-provider failure causes. Drain cancelled lazy loads and omit exited descendants from verified lineage. * fix(browser): preserve cleanup and document-transition evidence Make V8 and Playwright teardown cancellation-aware, expose primary failures alongside incomplete cleanup, share frame-tree and navigation rules, and clear stale WebMCP registrations without pruning unrelated child frames. * fix(adapters): align managed targets and typed CLI outcomes Inspect managed PE/CLI targets independently of native provider selection, reuse successful managed selections, and keep evidence bound to the actual inspected target. Classify canonical CLI error envelopes and exhaustive setup/update/uninstall outcomes, audit registered CLI coverage, and assert platform-specific runtime results. * docs: refresh boundary contracts and shipped roadmap behavior
397 lines
12 KiB
TypeScript
397 lines
12 KiB
TypeScript
import { expect, it } from "vitest";
|
|
|
|
import { buildJavaScriptSemanticGraph } from "./JavaScriptSemanticGraphBuilder.js";
|
|
import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
|
|
import type { JavaScriptArtifactFile } from "./JavaScriptArtifactFiles.js";
|
|
import { queryJavaScriptSemanticGraph } from "../domain/javascriptSemanticQuery.js";
|
|
import { analyzeJavaScriptSemantics } from "../domain/javascriptSemanticAnalysis.js";
|
|
|
|
const SHA256 = "a".repeat(64);
|
|
const GRAPH_ID = `jag_${"b".repeat(64)}`;
|
|
|
|
it("projects literal events and exact timer cancellation", () => {
|
|
const graph = graphFor(`
|
|
function run(bus, dynamicName) {
|
|
const handle = setInterval(tick, 10);
|
|
bus.on("ready", handler);
|
|
bus.once(dynamicName, handler);
|
|
bus.emit("ready");
|
|
bus.removeListener("ready", handler);
|
|
clearInterval(handle);
|
|
}
|
|
`);
|
|
|
|
const relations = new Set(graph.relations.map(({ relation }) => relation));
|
|
for (const expected of [
|
|
"cancels-timer",
|
|
"dispatches-candidate",
|
|
"registers-listener",
|
|
"removes-listener",
|
|
"schedules-timer",
|
|
] as const)
|
|
expect(relations.has(expected)).toBe(true);
|
|
expect(
|
|
graph.nodes.find(
|
|
({ kind, properties }) =>
|
|
kind === "event" && properties.event_name === "ready",
|
|
),
|
|
).toBeDefined();
|
|
expect(
|
|
graph.unknowns.some(
|
|
({ family, detail }) =>
|
|
family === "event" && detail.includes("Dynamic once event name"),
|
|
),
|
|
).toBe(true);
|
|
expect(
|
|
graph.coverage.families.find(({ family }) => family === "event"),
|
|
).toMatchObject({ status: "partial" });
|
|
expect(
|
|
graph.coverage.families.find(({ family }) => family === "timer"),
|
|
).toMatchObject({ status: "partial" });
|
|
});
|
|
|
|
it("projects child-process creation, I/O, listeners, and signals", () => {
|
|
const graph = graphFor(`
|
|
import { spawn } from "node:child_process";
|
|
function run(command) {
|
|
const child = spawn(
|
|
command,
|
|
["--mode", "fast"],
|
|
{ env: process.env, stdio: "pipe" },
|
|
);
|
|
child.on("exit", onExit);
|
|
child.once("error", onError);
|
|
child.kill("SIGTERM");
|
|
}
|
|
`);
|
|
|
|
const relations = new Set(graph.relations.map(({ relation }) => relation));
|
|
for (const expected of [
|
|
"connects-stdio",
|
|
"forwards-signal",
|
|
"listens-error",
|
|
"listens-exit",
|
|
"spawns",
|
|
"supplies-argv",
|
|
"supplies-env",
|
|
] as const)
|
|
expect(relations.has(expected)).toBe(true);
|
|
expect(
|
|
graph.nodes.find(
|
|
({ kind, properties }) =>
|
|
kind === "child-process" &&
|
|
properties.method === "spawn" &&
|
|
properties.command === null,
|
|
),
|
|
).toBeDefined();
|
|
expect(
|
|
graph.unknowns.some(
|
|
({ family, detail }) =>
|
|
family === "child-process" &&
|
|
detail.includes("command remains unresolved"),
|
|
),
|
|
).toBe(true);
|
|
expect(
|
|
graph.coverage.families.find(({ family }) => family === "child-process"),
|
|
).toMatchObject({ status: "partial" });
|
|
});
|
|
|
|
it("keeps destructured request, spawn, and timer results as projections", () => {
|
|
const graph = graphFor(`
|
|
import { spawn } from "node:child_process";
|
|
async function run() {
|
|
const { data } = await fetch("https://example.test");
|
|
const [ child ] = spawn("worker", []);
|
|
child.on("exit", done);
|
|
const { handle } = setTimeout(done, 5);
|
|
clearTimeout(handle);
|
|
return data.json();
|
|
}
|
|
`);
|
|
const projections = graph.relations.filter(
|
|
({ relation }) => relation === "destructures",
|
|
);
|
|
expect(
|
|
projections.map(({ properties }) => properties.projection_path),
|
|
).toEqual(expect.arrayContaining([["data"], [0], ["handle"]]));
|
|
expect(
|
|
projections.every(
|
|
({ properties }) => properties.projection_resolution === "complete",
|
|
),
|
|
).toBe(true);
|
|
expect(
|
|
graph.relations.find(
|
|
({ relation, properties }) =>
|
|
relation === "consumed-by" && properties.method === "json",
|
|
),
|
|
).toMatchObject({ resolution: "candidate" });
|
|
expect(
|
|
graph.relations.find(({ relation }) => relation === "listens-exit"),
|
|
).toMatchObject({ resolution: "candidate" });
|
|
expect(
|
|
graph.relations.find(({ relation }) => relation === "cancels-timer"),
|
|
).toMatchObject({ resolution: "candidate" });
|
|
expect(
|
|
graph.relations.some(
|
|
({ relation, resolution }) =>
|
|
["consumed-by", "listens-exit", "cancels-timer"].includes(relation) &&
|
|
resolution === "resolved",
|
|
),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("does not alias nested call arguments to an outer assigned result", () => {
|
|
const graph = graphFor(`
|
|
import { spawn } from "node:child_process";
|
|
function run() {
|
|
const child = wrap(spawn("worker", []));
|
|
child.on("exit", done);
|
|
const handle = String(setTimeout(done, 5));
|
|
clearTimeout(handle);
|
|
}
|
|
`);
|
|
expect(
|
|
graph.relations.some(({ relation }) =>
|
|
["listens-exit", "cancels-timer", "destructures"].includes(relation),
|
|
),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("projects config precedence, request fields, and boundaries", () => {
|
|
const graph = graphFor(`
|
|
import { readFileSync } from "node:fs";
|
|
async function run(schema) {
|
|
const endpoint = process.env.API_URL ?? "https://fallback.test";
|
|
const raw = readFileSync("./config.json", "utf8");
|
|
const parsed = JSON.parse(raw);
|
|
const port = Number(process.argv[2]);
|
|
const validated = schema.parse(parsed);
|
|
const response = await fetch(endpoint, {
|
|
method: "POST",
|
|
body: validated,
|
|
});
|
|
return response.json();
|
|
}
|
|
`);
|
|
|
|
const relations = new Set(graph.relations.map(({ relation }) => relation));
|
|
for (const expected of [
|
|
"coerces",
|
|
"constructs-request",
|
|
"consumed-by",
|
|
"defaults",
|
|
"overrides",
|
|
"parses",
|
|
"reads-argv",
|
|
"reads-config",
|
|
"reads-environment",
|
|
"supplies-request-field",
|
|
] as const)
|
|
expect(relations.has(expected)).toBe(true);
|
|
expect(
|
|
graph.nodes.find(
|
|
({ kind, properties }) =>
|
|
kind === "request" && properties.method === "fetch",
|
|
),
|
|
).toBeDefined();
|
|
expect(
|
|
graph.nodes.filter(({ kind }) => kind === "boundary"),
|
|
).not.toHaveLength(0);
|
|
expect(
|
|
graph.coverage.families.find(({ family }) => family === "configuration"),
|
|
).toMatchObject({ status: "partial" });
|
|
expect(
|
|
graph.coverage.families.find(({ family }) => family === "request"),
|
|
).toMatchObject({ status: "partial" });
|
|
expect(
|
|
graph.coverage.families.find(({ family }) => family === "boundary"),
|
|
).toMatchObject({ status: "partial" });
|
|
});
|
|
|
|
it("projects built-in resource acquisition and release", () => {
|
|
const graph = graphFor(`
|
|
import { open } from "node:fs/promises";
|
|
import { connect } from "node:net";
|
|
async function run(path) {
|
|
const file = await open(path);
|
|
const socket = connect({ port: 9000 });
|
|
await file.close();
|
|
socket.end();
|
|
}
|
|
`);
|
|
|
|
const relations = new Set(graph.relations.map(({ relation }) => relation));
|
|
expect(relations.has("acquires")).toBe(true);
|
|
expect(relations.has("releases")).toBe(true);
|
|
expect(graph.nodes.filter(({ kind }) => kind === "resource")).toHaveLength(2);
|
|
expect(
|
|
graph.coverage.families.find(
|
|
({ family }) => family === "resource-lifecycle",
|
|
),
|
|
).toMatchObject({ status: "partial" });
|
|
});
|
|
|
|
it.each(["+= 1", "++"])(
|
|
"projects both property dependencies for %s",
|
|
(operator) => {
|
|
const graph = graphFor(`
|
|
const source = { count: 1 };
|
|
source.count ${operator};
|
|
`);
|
|
const relations = graph.relations.map(({ relation }) => relation);
|
|
expect(relations).toContain("reads-property");
|
|
expect(relations).toContain("writes-property");
|
|
},
|
|
);
|
|
it("does not project a literal property slot for dynamic destructuring", () => {
|
|
const graph = graphFor(`
|
|
const source = { token: "TOKEN" };
|
|
const key = getKey();
|
|
const { [key]: value } = source;
|
|
`);
|
|
|
|
expect(
|
|
graph.relations.filter(({ relation }) => relation === "destructures"),
|
|
).toEqual([]);
|
|
expect(
|
|
graph.nodes.find(
|
|
({ kind, properties }) =>
|
|
kind === "property-slot" && properties.name === "key",
|
|
),
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it("projects literal seeds and static object flow", () => {
|
|
const graph = graphFor(`
|
|
const source = { token: "TOKEN", count: 1 };
|
|
const { token } = source;
|
|
const copy = { ...source };
|
|
source.count = 2;
|
|
const read = source.token;
|
|
`);
|
|
|
|
const relations = new Set(graph.relations.map(({ relation }) => relation));
|
|
for (const expected of [
|
|
"destructures",
|
|
"reads-property",
|
|
"spreads",
|
|
"writes-property",
|
|
] as const)
|
|
expect(relations.has(expected)).toBe(true);
|
|
expect(
|
|
graph.nodes.find(
|
|
({ kind, properties }) =>
|
|
kind === "property-slot" && properties.name === "token",
|
|
),
|
|
).toBeDefined();
|
|
const literal = queryJavaScriptSemanticGraph(graph, {
|
|
seed: { kind: "literal", value: "TOKEN" },
|
|
direction: "forward-influence",
|
|
});
|
|
expect(literal.status).toBe("ambiguous");
|
|
expect(literal.summary.total_seed_matches).toBe(3);
|
|
const property = queryJavaScriptSemanticGraph(graph, {
|
|
seed: { kind: "property", name: "token" },
|
|
direction: "forward-influence",
|
|
});
|
|
expect(property.status).toBe("found");
|
|
});
|
|
|
|
it("does not promote transformed return expressions to resolved aliases", () => {
|
|
const graph = graphFor(`
|
|
function increment(value) { return value + 1; }
|
|
const input = 1;
|
|
const output = increment(input);
|
|
`);
|
|
const returnNode = graph.nodes.find(({ kind }) => kind === "return-site");
|
|
if (returnNode === undefined) throw new Error("Expected return site");
|
|
expect(
|
|
graph.relations.filter(
|
|
({ relation, target_node_id }) =>
|
|
relation === "aliases" && target_node_id === returnNode.node_id,
|
|
),
|
|
).toEqual([expect.objectContaining({ resolution: "candidate" })]);
|
|
const output = graph.nodes.find(
|
|
({ kind, label }) => kind === "binding" && label === "output",
|
|
);
|
|
const input = graph.nodes.find(
|
|
({ kind, label }) => kind === "binding" && label === "input",
|
|
);
|
|
if (output === undefined || input === undefined)
|
|
throw new Error("Expected caller bindings");
|
|
const strict = queryJavaScriptSemanticGraph(graph, {
|
|
seed: { kind: "semantic-node", node_id: output.node_id },
|
|
direction: "backward-provenance",
|
|
});
|
|
expect(strict.nodes.map(({ node_id }) => node_id)).not.toContain(
|
|
input.node_id,
|
|
);
|
|
expect(strict.status).toBe("ambiguous");
|
|
});
|
|
|
|
it("uses unavailable evidence when no source produced semantic IR", () => {
|
|
const graph = buildJavaScriptSemanticGraph({
|
|
rootArtifactSha256: SHA256,
|
|
applicationGraph: { graph_id: GRAPH_ID, nodes: [] },
|
|
analysis: emptyAnalysis(),
|
|
});
|
|
expect(graph.nodes[0]?.evidence).toMatchObject({
|
|
authority: "unknown",
|
|
state: "unavailable",
|
|
location: { available: false, reason: "not-observed" },
|
|
});
|
|
});
|
|
|
|
const emptyAnalysis = (): JavaScriptArtifactAnalysis => ({
|
|
files: [],
|
|
packages: [],
|
|
json_modules: [],
|
|
html_scripts: [],
|
|
source_maps: [],
|
|
visited_ast_nodes: 0,
|
|
findings: 0,
|
|
modules: 0,
|
|
parse_failures: 0,
|
|
truncated_scopes: 0,
|
|
limitations: [],
|
|
});
|
|
|
|
const graphFor = (source: string) => {
|
|
const file: JavaScriptArtifactFile = {
|
|
path: "app.js",
|
|
container_sha256: SHA256,
|
|
sha256: SHA256,
|
|
bytes: Buffer.byteLength(source),
|
|
inventory_artifact_id: `art_${SHA256}`,
|
|
kind: "javascript",
|
|
unpacked: false,
|
|
text: { included: true, value: source },
|
|
};
|
|
const analysis: JavaScriptArtifactAnalysis = {
|
|
files: [
|
|
{
|
|
file,
|
|
javascript: null,
|
|
semantic: {
|
|
ir: analyzeJavaScriptSemantics(source),
|
|
},
|
|
},
|
|
],
|
|
packages: [],
|
|
json_modules: [],
|
|
html_scripts: [],
|
|
source_maps: [],
|
|
visited_ast_nodes: 0,
|
|
findings: 0,
|
|
modules: 0,
|
|
parse_failures: 0,
|
|
truncated_scopes: 0,
|
|
limitations: [],
|
|
};
|
|
return buildJavaScriptSemanticGraph({
|
|
rootArtifactSha256: SHA256,
|
|
applicationGraph: { graph_id: GRAPH_ID, nodes: [] },
|
|
analysis,
|
|
});
|
|
};
|