rea/tests/javascriptApplicationWorkfl...

292 lines
9.7 KiB
TypeScript

import { rm } from "node:fs/promises";
import { afterEach, describe, expect, it } from "vitest";
import { createTestTempDirectory } from "./fixtures/temporaryDirectory.js";
import { analyzeJavaScriptApplication } from "../src/application/JavaScriptApplicationService.js";
import { compareApplicationVersionsEvidence } from "../src/application/JavaScriptApplicationWorkflowService.js";
import { createEvidence } from "../src/domain/evidence.js";
import { applicationVersionComparisonResultSchema } from "../src/domain/javascriptApplicationVersionComparisonSchemas.js";
import { compareJavaScriptApplicationVersions } from "../src/domain/javascriptApplicationVersionComparison.js";
import { createJavaScriptApplicationGraph } from "../src/domain/javascriptApplicationGraph.js";
import { traceApplicationFeature } from "../src/domain/javascriptFeatureTrace.js";
import { applicationFeatureTraceResultSchema } from "../src/domain/javascriptFeatureTraceSchemas.js";
import {
APPLICATION_GRAPH_DIGESTS,
buildSyntheticJavaScriptApplicationGraph,
} from "./fixtures/javascriptApplicationGraph.js";
import { writeVersionedJavaScriptApplicationFixtures } from "./fixtures/javascriptArtifactApplication.js";
import { permissionAuthorityForRoot } from "./fixtures/permissionAuthority.js";
describe("JavaScript application workflows", () => {
const temporary: string[] = [];
afterEach(async () => {
await Promise.all(
temporary
.splice(0)
.map(async (path) => rm(path, { recursive: true, force: true })),
);
});
it("traces a contextBridge API through IPC into linked native evidence", () => {
const graph = buildSyntheticJavaScriptApplicationGraph();
const nativeEvidence = createEvidence(
{
path: "/tmp/synthetic.node",
sha256: APPLICATION_GRAPH_DIGESTS.nativeAddon,
format: "elf",
architecture: "x86_64",
},
{ id: "ghidra", name: "Ghidra", version: "fixture" },
{
operation: "analyze_function",
parameters: { address: "openProject" },
result: { name: "openProject", address: "0x1000" },
limitations: ["Synthetic native provider observation."],
},
);
const result = traceApplicationFeature({
sourceEvidenceId: `ev_${"a".repeat(64)}`,
graph,
nativeEvidence: [nativeEvidence],
seed: {
kind: "api",
value: "desktopApi",
match: "exact",
case_sensitive: false,
},
direction: "outgoing",
limits: traceLimits(),
});
expect(() =>
applicationFeatureTraceResultSchema.parse(result),
).not.toThrow();
expect(result.summary).toMatchObject({
matched_seeds: 1,
native_handoffs: 1,
});
expect(result.native_handoffs).toContainEqual(
expect.objectContaining({
artifact_sha256: APPLICATION_GRAPH_DIGESTS.nativeAddon,
status: "evidence-linked",
evidence_ids: [nativeEvidence.evidence_id],
requested_exports: expect.arrayContaining(["openProject"]),
}),
);
expect(result.paths).toEqual(
expect.arrayContaining([
expect.objectContaining({ end_kind: "native-export" }),
]),
);
expect(
result.graph?.edges.some(
({ evidence }) =>
evidence.authority === "static-relationship-inference" &&
evidence.state === "inferred",
),
).toBe(true);
});
it("keeps no-match and bounded frontiers explicit", () => {
const graph = buildSyntheticJavaScriptApplicationGraph();
const noMatch = traceApplicationFeature({
sourceEvidenceId: `ev_${"a".repeat(64)}`,
graph,
nativeEvidence: [],
seed: {
kind: "route",
value: "/missing",
match: "exact",
case_sensitive: false,
},
direction: "both",
limits: traceLimits(),
});
expect(noMatch).toMatchObject({
graph: null,
coverage: { status: "no-match" },
summary: { matched_seeds: 0 },
});
const bounded = traceApplicationFeature({
sourceEvidenceId: `ev_${"a".repeat(64)}`,
graph,
nativeEvidence: [],
seed: {
kind: "api",
value: "desktopApi",
match: "exact",
case_sensitive: false,
},
direction: "outgoing",
limits: { ...traceLimits(), max_depth: 1 },
});
expect(bounded.coverage).toMatchObject({ status: "truncated" });
expect(bounded.coverage.omitted_nodes).toBeGreaterThan(0);
expect(bounded.summary.native_handoffs).toBe(0);
});
it("matches rechunked modules by exact source and minified modules by structural fingerprint", async () => {
const root = await createTestTempDirectory("rea-application-versions-");
temporary.push(root);
const fixtures = await writeVersionedJavaScriptApplicationFixtures(root);
const [left, right] = await Promise.all([
analyzeFixture(fixtures.left, true),
analyzeFixture(fixtures.right, true),
]);
const compared = compareApplicationVersionsEvidence({ left, right });
if (!compared.ok) throw compared.error;
const result = applicationVersionComparisonResultSchema.parse(
compared.value.normalized_result,
);
const modules = result.items.filter(
({ node_kind: kind }) => kind === "javascript-module",
);
expect(modules).toEqual(
expect.arrayContaining([
expect.objectContaining({
match: expect.objectContaining({
status: "matched",
basis: "exact-module-source-digest",
confidence: "exact",
}),
}),
expect.objectContaining({
status: "changed",
match: expect.objectContaining({
status: "matched",
basis: "structural-fingerprint",
confidence: "medium",
}),
}),
]),
);
expect(result.matching.ambiguous).toBeGreaterThanOrEqual(4);
expect(modules.filter(({ match }) => match.status === "ambiguous")).toEqual(
expect.arrayContaining([expect.objectContaining({ status: "unknown" })]),
);
expect(result.summary.added).toBeGreaterThan(0);
expect(result.summary.removed).toBeGreaterThan(0);
expect(result.graph.edges).toEqual(
expect.arrayContaining([
expect.objectContaining({
relation: "changed_from",
evidence: expect.objectContaining({
authority: "cross-version-comparison",
state: "inferred",
}),
}),
]),
);
expect(result.limitations.join(" ")).toMatch(
/module ordinals|fuzzy pairing/u,
);
});
it("pairs source-map originals without promoting the match to exact", async () => {
const root = await createTestTempDirectory("rea-source-map-versions-");
temporary.push(root);
const fixtures = await writeVersionedJavaScriptApplicationFixtures(root);
const [left, right] = await Promise.all([
analyzeFixture(fixtures.left, true),
analyzeFixture(fixtures.right, true),
]);
const compared = compareApplicationVersionsEvidence({ left, right });
if (!compared.ok) throw compared.error;
const result = applicationVersionComparisonResultSchema.parse(
compared.value.normalized_result,
);
expect(result.items).toContainEqual(
expect.objectContaining({
node_kind: "source-module",
status: "changed",
match: expect.objectContaining({
basis: "source-map-identity",
confidence: "high",
}),
}),
);
});
it("keeps incomplete one-sided absence unknown and reports output truncation", () => {
const complete = buildSyntheticJavaScriptApplicationGraph();
const rootNode = complete.nodes.find(({ node_id: id }) =>
complete.root_node_ids.includes(id),
);
if (rootNode === undefined) throw new Error("Synthetic graph root missing");
const partial = createJavaScriptApplicationGraph({
schema: "JavaScriptApplicationGraph",
schema_version: 1,
root_node_ids: [rootNode.node_id],
nodes: [rootNode],
edges: [],
coverage: {
status: "partial",
truncated: true,
omitted_count: complete.nodes.length - 1,
limits: [{ name: "fixture_nodes", value: 1, unit: "items" }],
},
limitations: ["Synthetic partial graph."],
});
const input = {
left: {
evidenceId: `ev_${"a".repeat(64)}`,
rootArtifactSha256: APPLICATION_GRAPH_DIGESTS.package,
graph: complete,
},
right: {
evidenceId: `ev_${"b".repeat(64)}`,
rootArtifactSha256: APPLICATION_GRAPH_DIGESTS.package,
graph: partial,
},
leftNativeEvidence: [],
rightNativeEvidence: [],
limits: {
max_comparison_items: 1,
max_candidate_nodes: 1,
max_graph_nodes: 20,
max_graph_edges: 20,
},
};
const first = compareJavaScriptApplicationVersions(input);
const second = compareJavaScriptApplicationVersions(input);
expect(first.comparison_id).toBe(second.comparison_id);
expect(first.coverage).toMatchObject({
status: "truncated",
right_graph_status: "partial",
});
expect(first.coverage.omitted_comparison_items).toBeGreaterThan(0);
expect(first.summary.removed).toBe(0);
expect(first.summary.unknown).toBeGreaterThan(0);
});
});
const traceLimits = () => ({
max_seed_matches: 25,
max_depth: 12,
max_nodes: 2_000,
max_edges: 4_000,
max_paths: 100,
});
const analyzeFixture = async (root: string, sourceMaps = false) => {
const authority = await permissionAuthorityForRoot(
root,
["investigation_input"],
["investigation_input"],
);
const result = await analyzeJavaScriptApplication(authority, {
input_path: root,
approved: true,
source_map_read_approved: sourceMaps,
});
if (!result.ok) throw result.error;
return result.value;
};