401 lines
12 KiB
TypeScript
401 lines
12 KiB
TypeScript
import { execFile } from "node:child_process";
|
|
import { copyFile, mkdir, rm, writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { promisify } from "node:util";
|
|
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
|
|
import {
|
|
JAVASCRIPT_APPLICATION_VERSION_COMPARISON_EXAMPLE,
|
|
JAVASCRIPT_FEATURE_TRACE_FULL_EVIDENCE_EXAMPLE,
|
|
JAVASCRIPT_FEATURE_TRACE_EXAMPLE,
|
|
JAVASCRIPT_VERSION_COMPARISON_FULL_EVIDENCE_EXAMPLE,
|
|
SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE,
|
|
} from "../../../src/contracts/javascriptApplicationWorkflowExamples.js";
|
|
import {
|
|
createEvidenceBundle,
|
|
serializeEvidenceBundle,
|
|
} from "../../../src/domain/evidenceBundle.js";
|
|
import { analyzeJavaScriptApplication } from "../../../src/application/JavaScriptApplicationService.js";
|
|
import { REPLAY_MACHINE_RUN_EXAMPLE } from "../../../src/contracts/replayMachineExample.js";
|
|
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascriptApplicationAnalysis.js";
|
|
|
|
const execute = promisify(execFile);
|
|
const temporary: string[] = [];
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(
|
|
temporary
|
|
.splice(0)
|
|
.map(async (path) => rm(path, { recursive: true, force: true })),
|
|
);
|
|
});
|
|
|
|
describe("application workflow CLI parity", () => {
|
|
it("accepts inline trace JSON and file-backed comparison JSON", async () => {
|
|
const traced = await runCli([
|
|
"trace-application-feature",
|
|
JSON.stringify(JAVASCRIPT_FEATURE_TRACE_FULL_EVIDENCE_EXAMPLE),
|
|
"--json",
|
|
]);
|
|
expect(traced).toMatchObject({
|
|
operation: "trace_application_feature",
|
|
normalized_result: { source_evidence_id: expect.any(String) },
|
|
});
|
|
|
|
const root = await createTestTempDirectory("rea-application-cli-");
|
|
temporary.push(root);
|
|
const comparisonPath = join(root, "comparison.json");
|
|
await writeFile(
|
|
comparisonPath,
|
|
JSON.stringify(JAVASCRIPT_VERSION_COMPARISON_FULL_EVIDENCE_EXAMPLE),
|
|
);
|
|
const compared = await runCli([
|
|
"compare-application-versions",
|
|
comparisonPath,
|
|
"--json",
|
|
]);
|
|
expect(compared).toMatchObject({
|
|
operation: "compare_application_versions",
|
|
normalized_result: {
|
|
summary: { unknown: expect.any(Number) },
|
|
},
|
|
});
|
|
const sourceCompared = await runCli([
|
|
"compare-source-to-bundle",
|
|
JSON.stringify({
|
|
reference: SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE.reference,
|
|
application: JAVASCRIPT_FEATURE_TRACE_FULL_EVIDENCE_EXAMPLE.application,
|
|
}),
|
|
"--json",
|
|
]);
|
|
expect(sourceCompared).toMatchObject({
|
|
operation: "compare_source_to_bundle",
|
|
normalized_result: { reference: expect.any(Object) },
|
|
});
|
|
}, 20_000);
|
|
|
|
it("traces the same authenticated semantic graph through the CLI", async () => {
|
|
const root = await createTestTempDirectory("rea-semantic-cli-");
|
|
temporary.push(root);
|
|
await writeFile(
|
|
join(root, "app.js"),
|
|
"function add(value) { return value + 1; } add(2);",
|
|
);
|
|
const analyzed = await analyzeJavaScriptApplication({
|
|
input_path: root,
|
|
});
|
|
if (!analyzed.ok) throw analyzed.error;
|
|
const result = javascriptApplicationAnalysisResultSchema.parse(
|
|
analyzed.value.normalized_result,
|
|
);
|
|
const seed = result.semantic_graph.relations[0]?.source_node_id;
|
|
if (seed === undefined)
|
|
throw new TypeError("Expected at least one semantic relation");
|
|
|
|
const traced = await runCli([
|
|
"trace-javascript-semantics",
|
|
JSON.stringify({
|
|
application: analyzed.value,
|
|
query: {
|
|
seed: { kind: "semantic-node", node_id: seed },
|
|
direction: "forward-influence",
|
|
include_ambiguous_dynamic_edges: true,
|
|
},
|
|
}),
|
|
"--json",
|
|
]);
|
|
expect(traced).toMatchObject({
|
|
operation: "trace_javascript_semantics",
|
|
predicate_type: "rea.javascript-semantic-trace/v1",
|
|
normalized_result: {
|
|
source_evidence_id: analyzed.value.evidence_id,
|
|
source_graph_id: result.semantic_graph.graph_id,
|
|
},
|
|
});
|
|
}, 20_000);
|
|
});
|
|
|
|
describe("application workflow CLI Evidence resolution", () => {
|
|
it("resolves Evidence IDs from an explicitly authorized bundle", async () => {
|
|
const root = await createTestTempDirectory("rea-application-bundle-cli-");
|
|
temporary.push(root);
|
|
const bundlePath = join(root, "evidence.json");
|
|
await writeFile(
|
|
bundlePath,
|
|
serializeEvidenceBundle(
|
|
createEvidenceBundle([
|
|
JAVASCRIPT_VERSION_COMPARISON_FULL_EVIDENCE_EXAMPLE.left,
|
|
JAVASCRIPT_VERSION_COMPARISON_FULL_EVIDENCE_EXAMPLE.right,
|
|
]),
|
|
),
|
|
);
|
|
const environment = {
|
|
REA_EVIDENCE_ROOTS_JSON: JSON.stringify([root]),
|
|
};
|
|
|
|
const traced = await runCli(
|
|
[
|
|
"trace-application-feature",
|
|
JSON.stringify({
|
|
...JAVASCRIPT_FEATURE_TRACE_EXAMPLE,
|
|
application_evidence_id:
|
|
JAVASCRIPT_FEATURE_TRACE_FULL_EVIDENCE_EXAMPLE.application
|
|
.evidence_id,
|
|
}),
|
|
"--evidence-bundle",
|
|
bundlePath,
|
|
"--json",
|
|
],
|
|
environment,
|
|
);
|
|
expect(traced).toMatchObject({
|
|
operation: "trace_application_feature",
|
|
normalized_result: { source_evidence_id: expect.any(String) },
|
|
});
|
|
|
|
const compared = await runCli(
|
|
[
|
|
"compare-application-versions",
|
|
JSON.stringify({
|
|
...JAVASCRIPT_APPLICATION_VERSION_COMPARISON_EXAMPLE,
|
|
left_evidence_id:
|
|
JAVASCRIPT_VERSION_COMPARISON_FULL_EVIDENCE_EXAMPLE.left
|
|
.evidence_id,
|
|
right_evidence_id:
|
|
JAVASCRIPT_VERSION_COMPARISON_FULL_EVIDENCE_EXAMPLE.right
|
|
.evidence_id,
|
|
}),
|
|
"--evidence-bundle",
|
|
bundlePath,
|
|
"--json",
|
|
],
|
|
environment,
|
|
);
|
|
expect(compared).toMatchObject({
|
|
operation: "compare_application_versions",
|
|
normalized_result: { summary: expect.any(Object) },
|
|
});
|
|
|
|
const missingRecord = await runCli(
|
|
[
|
|
"trace-application-feature",
|
|
JSON.stringify(JAVASCRIPT_FEATURE_TRACE_EXAMPLE),
|
|
"--evidence-bundle",
|
|
bundlePath,
|
|
"--json",
|
|
],
|
|
environment,
|
|
);
|
|
expect(missingRecord).toMatchObject({
|
|
code: "evidence_integrity_mismatch",
|
|
details: { reason: "missing" },
|
|
});
|
|
|
|
const outsideRoot = await createTestTempDirectory(
|
|
"rea-outside-bundle-cli-",
|
|
);
|
|
temporary.push(outsideRoot);
|
|
const outsideBundlePath = join(outsideRoot, "evidence.json");
|
|
await writeFile(
|
|
outsideBundlePath,
|
|
serializeEvidenceBundle(createEvidenceBundle([])),
|
|
);
|
|
const denied = await runCli(
|
|
[
|
|
"trace-application-feature",
|
|
JSON.stringify(JAVASCRIPT_FEATURE_TRACE_EXAMPLE),
|
|
"--evidence-bundle",
|
|
outsideBundlePath,
|
|
"--json",
|
|
],
|
|
environment,
|
|
);
|
|
expect(denied).toMatchObject({ code: "permission_required" });
|
|
}, 30_000);
|
|
});
|
|
|
|
describe("application workflow CLI export Evidence", () => {
|
|
it("compares exact export shapes from an authorized Evidence bundle", async () => {
|
|
const root = await createTestTempDirectory("rea-export-shape-cli-");
|
|
temporary.push(root);
|
|
const leftRoot = join(root, "left");
|
|
const rightRoot = join(root, "right");
|
|
await Promise.all([mkdir(leftRoot), mkdir(rightRoot)]);
|
|
await Promise.all([
|
|
copyFile(
|
|
join(process.cwd(), "tests/fixtures/replay/parser.mjs"),
|
|
join(leftRoot, "parser.mjs"),
|
|
),
|
|
copyFile(
|
|
join(process.cwd(), "tests/fixtures/replay/parser-v2.mjs"),
|
|
join(rightRoot, "parser.mjs"),
|
|
),
|
|
]);
|
|
const [left, right] = await Promise.all([
|
|
analyzeJavaScriptApplication({
|
|
input_path: leftRoot,
|
|
}),
|
|
analyzeJavaScriptApplication({
|
|
input_path: rightRoot,
|
|
}),
|
|
]);
|
|
if (!left.ok) throw left.error;
|
|
if (!right.ok) throw right.error;
|
|
const bundlePath = join(root, "evidence.json");
|
|
await writeFile(
|
|
bundlePath,
|
|
serializeEvidenceBundle(createEvidenceBundle([left.value, right.value])),
|
|
);
|
|
const compared = await runCli(
|
|
[
|
|
"compare-javascript-export-shapes",
|
|
JSON.stringify({
|
|
left_evidence_id: left.value.evidence_id,
|
|
right_evidence_id: right.value.evidence_id,
|
|
left_module_path: "parser.mjs",
|
|
left_export_name: "default",
|
|
right_module_path: "parser.mjs",
|
|
right_export_name: "default",
|
|
}),
|
|
"--evidence-bundle",
|
|
bundlePath,
|
|
"--json",
|
|
],
|
|
{ REA_EVIDENCE_ROOTS_JSON: JSON.stringify([root]) },
|
|
);
|
|
expect(compared).toMatchObject({
|
|
operation: "compare_javascript_export_shapes",
|
|
predicate_type: "rea.javascript-export-shape-comparison/v1",
|
|
normalized_result: {
|
|
summary: { added: 1, removed: 0, changed: 0, unknown: 0 },
|
|
changes: [
|
|
{
|
|
status: "added",
|
|
path: "/depth",
|
|
right: { availability: "literal", value: 1 },
|
|
},
|
|
],
|
|
},
|
|
});
|
|
}, 20_000);
|
|
});
|
|
|
|
describe("application workflow CLI validation and replay", () => {
|
|
it("returns safe actionable JSON validation details", async () => {
|
|
const malformed = await runCli([
|
|
"trace-application-feature",
|
|
"{not-json",
|
|
"--json",
|
|
]);
|
|
expect(malformed).toMatchObject({
|
|
code: "invalid_request",
|
|
retryable: true,
|
|
details: {
|
|
issues: [{ path: [], reason: "invalid_format", expected: "JSON" }],
|
|
},
|
|
});
|
|
expect(JSON.stringify(malformed)).not.toContain("not-json");
|
|
|
|
const missing = await runCli(["trace-application-feature", "{}", "--json"]);
|
|
expect(missing).toMatchObject({
|
|
code: "invalid_request",
|
|
details: {
|
|
issues: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
path: ["seed"],
|
|
reason: "missing_argument",
|
|
}),
|
|
]),
|
|
},
|
|
});
|
|
}, 20_000);
|
|
|
|
it("runs a replay machine from inline and file-backed JSON", async () => {
|
|
const input = REPLAY_MACHINE_RUN_EXAMPLE;
|
|
const inline = await runCli([
|
|
"run-replay-machine",
|
|
JSON.stringify(input),
|
|
"--json",
|
|
]);
|
|
expect(inline).toMatchObject({
|
|
final_state: "complete",
|
|
decisions: [{ outcome: "matched" }],
|
|
transition_journal: [
|
|
{ captured_aliases: [{ name: "token", sensitive: true }] },
|
|
],
|
|
});
|
|
expect(JSON.stringify(inline)).not.toContain("opaque");
|
|
|
|
const root = await createTestTempDirectory("rea-replay-machine-cli-");
|
|
temporary.push(root);
|
|
const inputPath = join(root, "run.json");
|
|
await writeFile(inputPath, JSON.stringify(input));
|
|
const fileBacked = await runCli([
|
|
"run-replay-machine",
|
|
inputPath,
|
|
"--json",
|
|
]);
|
|
expect(fileBacked).toEqual(inline);
|
|
|
|
const invalid = await runCli([
|
|
"run-replay-machine",
|
|
JSON.stringify({
|
|
machine: input.machine,
|
|
events: [
|
|
{
|
|
protocol: "http",
|
|
connection: "not_applicable",
|
|
at_ms: 0,
|
|
method: null,
|
|
path: "/callback",
|
|
headers: {},
|
|
body: "private-cli-body",
|
|
},
|
|
],
|
|
}),
|
|
"--json",
|
|
]);
|
|
expect(invalid).toMatchObject({
|
|
code: "invalid_request",
|
|
details: {
|
|
issues: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
path: ["events", 0, "method"],
|
|
}),
|
|
]),
|
|
},
|
|
});
|
|
expect(JSON.stringify(invalid)).not.toContain("private-cli-body");
|
|
}, 20_000);
|
|
});
|
|
|
|
const runCli = async (
|
|
arguments_: readonly string[],
|
|
environment: Readonly<Record<string, string>> = {},
|
|
): Promise<unknown> => {
|
|
try {
|
|
const { stdout } = await execute(
|
|
process.execPath,
|
|
["scripts/rea.mjs", ...arguments_],
|
|
{
|
|
cwd: process.cwd(),
|
|
env: { ...process.env, ...environment },
|
|
maxBuffer: 16 * 1_024 * 1_024,
|
|
},
|
|
);
|
|
return JSON.parse(stdout);
|
|
} catch (cause: unknown) {
|
|
if (
|
|
typeof cause === "object" &&
|
|
cause !== null &&
|
|
"stdout" in cause &&
|
|
typeof cause.stdout === "string"
|
|
)
|
|
return JSON.parse(cause.stdout);
|
|
throw cause;
|
|
}
|
|
};
|