test(acceptance): add application workflow MCP parity harness and scenarios
Adds an owned in-memory MCP harness plus acceptance coverage for trace/compare/export-shape workflows over inline and recorded Evidence, including rejection of mismatched evidence references and spoofed providers. All 16 scenarios pass on the current base.
This commit is contained in:
parent
96af2fa744
commit
5ac3dc96f0
|
|
@ -0,0 +1,234 @@
|
|||
import type { Client } from "@modelcontextprotocol/client";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { BinarySession } from "../../../src/application/binary/BinarySession.js";
|
||||
import {
|
||||
JAVASCRIPT_FEATURE_TRACE_FULL_EVIDENCE_EXAMPLE,
|
||||
JAVASCRIPT_FEATURE_TRACE_EXAMPLE,
|
||||
JAVASCRIPT_VERSION_COMPARISON_FULL_EVIDENCE_EXAMPLE,
|
||||
SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE,
|
||||
} from "../../../src/contracts/javascript/javascriptApplicationWorkflowExamples.js";
|
||||
import { createEvidence } from "../../../src/domain/evidence.js";
|
||||
import {
|
||||
createApplicationMcpHarness,
|
||||
type ApplicationMcpHarness,
|
||||
} from "./mcpHarness.js";
|
||||
|
||||
async function runInlineEvidenceScenarios(
|
||||
client: Client,
|
||||
session: BinarySession,
|
||||
): Promise<void> {
|
||||
const traced = await client.callTool({
|
||||
name: "trace_application_feature",
|
||||
arguments: JAVASCRIPT_FEATURE_TRACE_FULL_EVIDENCE_EXAMPLE,
|
||||
});
|
||||
expect(traced.isError).not.toBe(true);
|
||||
expect(traced.structuredContent).toMatchObject({
|
||||
result: {
|
||||
seed: { kind: "module", value: "renderer.js", match: "exact" },
|
||||
summary: { matched_seeds: 1, traced_nodes: 1, unknown_facts: 0 },
|
||||
coverage: {
|
||||
status: "complete-within-source",
|
||||
source_graph_status: "complete",
|
||||
total_seed_matches: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const compared = await client.callTool({
|
||||
name: "compare_application_versions",
|
||||
arguments: {
|
||||
...JAVASCRIPT_VERSION_COMPARISON_FULL_EVIDENCE_EXAMPLE,
|
||||
},
|
||||
});
|
||||
expect(compared.isError).not.toBe(true);
|
||||
expect(compared.structuredContent).toMatchObject({
|
||||
result: {
|
||||
summary: { unchanged: 0, added: 2, removed: 0, changed: 1, unknown: 0 },
|
||||
coverage: {
|
||||
left_graph_status: "complete",
|
||||
right_graph_status: "complete",
|
||||
status: "complete-within-inputs",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const sourceCompared = await client.callTool({
|
||||
name: "compare_source_to_bundle",
|
||||
arguments: {
|
||||
reference: SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE.reference,
|
||||
application: JAVASCRIPT_FEATURE_TRACE_FULL_EVIDENCE_EXAMPLE.application,
|
||||
},
|
||||
});
|
||||
expect(sourceCompared.isError).not.toBe(true);
|
||||
expect(sourceCompared.structuredContent).toMatchObject({
|
||||
result: {
|
||||
reference: {
|
||||
root_sha256: SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE.reference.root_sha256,
|
||||
},
|
||||
scoring: { algorithm: "rea-source-to-bundle-signals" },
|
||||
},
|
||||
});
|
||||
expect(session.exportEvidenceBundle().records.length).toBeGreaterThan(2);
|
||||
}
|
||||
|
||||
async function runInlineWorkflowScenarios(
|
||||
harness: ApplicationMcpHarness,
|
||||
): Promise<void> {
|
||||
const tracedInline = await harness.client.callTool({
|
||||
name: "trace_application_feature",
|
||||
arguments: {
|
||||
...JAVASCRIPT_FEATURE_TRACE_EXAMPLE,
|
||||
},
|
||||
});
|
||||
expect(tracedInline.isError).not.toBe(true);
|
||||
expect(tracedInline.structuredContent).toMatchObject({
|
||||
result: {
|
||||
seed: { kind: "module", value: "renderer.js", match: "exact" },
|
||||
summary: { matched_seeds: 1, traced_nodes: 1, unknown_facts: 0 },
|
||||
coverage: {
|
||||
status: "complete-within-source",
|
||||
source_graph_status: "complete",
|
||||
total_seed_matches: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const comparedInline = await harness.client.callTool({
|
||||
name: "compare_application_versions",
|
||||
arguments: {
|
||||
...JAVASCRIPT_VERSION_COMPARISON_FULL_EVIDENCE_EXAMPLE,
|
||||
},
|
||||
});
|
||||
expect(comparedInline).toMatchObject({
|
||||
structuredContent: {
|
||||
result: {
|
||||
evidence_links: expect.arrayContaining([
|
||||
JAVASCRIPT_VERSION_COMPARISON_FULL_EVIDENCE_EXAMPLE.left.evidence_id,
|
||||
JAVASCRIPT_VERSION_COMPARISON_FULL_EVIDENCE_EXAMPLE.right.evidence_id,
|
||||
]),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const idsRejected = await harness.client.callTool({
|
||||
name: "compare_application_versions",
|
||||
arguments: {
|
||||
left_evidence_id:
|
||||
JAVASCRIPT_VERSION_COMPARISON_FULL_EVIDENCE_EXAMPLE.left.evidence_id,
|
||||
right_evidence_id:
|
||||
JAVASCRIPT_VERSION_COMPARISON_FULL_EVIDENCE_EXAMPLE.right.evidence_id,
|
||||
},
|
||||
});
|
||||
expect(idsRejected.isError).toBe(true);
|
||||
|
||||
const sourceComparedById = await harness.client.callTool({
|
||||
name: "compare_source_to_bundle",
|
||||
arguments: {
|
||||
...SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE,
|
||||
},
|
||||
});
|
||||
expect(sourceComparedById.isError).not.toBe(true);
|
||||
expect(sourceComparedById.structuredContent).toMatchObject({
|
||||
result: {
|
||||
reference: {
|
||||
root_sha256: SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE.reference.root_sha256,
|
||||
inventory_state: "complete",
|
||||
},
|
||||
scoring: { algorithm: "rea-source-to-bundle-signals" },
|
||||
summary: { unchanged: 0, modified: 0, removed: 1, unknown: 0 },
|
||||
items: [
|
||||
{
|
||||
source_path: "src/main.ts",
|
||||
status: "removed",
|
||||
current_node_ids: [],
|
||||
candidates: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function assertRejectedEvidenceReferences(
|
||||
client: Client,
|
||||
session: BinarySession,
|
||||
): Promise<void> {
|
||||
const wrongOperation = createEvidence(
|
||||
undefined,
|
||||
{ id: "fixture", name: "Fixture", version: "1" },
|
||||
{ operation: "inventory_artifact", parameters: {}, result: {} },
|
||||
);
|
||||
const wrongPredicate = createEvidence(
|
||||
undefined,
|
||||
{ id: "fixture", name: "Fixture", version: "1" },
|
||||
{
|
||||
predicateType: "fixture.application/v1",
|
||||
operation: "analyze_javascript_application",
|
||||
parameters: {},
|
||||
result: {},
|
||||
},
|
||||
);
|
||||
expect(session.recordEvidence(wrongOperation).ok).toBe(true);
|
||||
expect(session.recordEvidence(wrongPredicate).ok).toBe(true);
|
||||
for (const record of [wrongOperation, wrongPredicate]) {
|
||||
const rejected = await client.callTool({
|
||||
name: "trace_application_feature",
|
||||
arguments: {
|
||||
...JAVASCRIPT_FEATURE_TRACE_EXAMPLE,
|
||||
application: record,
|
||||
},
|
||||
});
|
||||
expect(rejected).toMatchObject({
|
||||
isError: true,
|
||||
structuredContent: {
|
||||
// A recorded Evidence with the wrong operation or predicate is an
|
||||
// invalid caller input, not a provider failure.
|
||||
error: {
|
||||
code: "invalid_request",
|
||||
category: "invalid_input",
|
||||
retryable: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function assertRejectedInlineEvidence(client: Client): Promise<void> {
|
||||
const duplicateNative = await client.callTool({
|
||||
name: "trace_application_feature",
|
||||
arguments: {
|
||||
...JAVASCRIPT_FEATURE_TRACE_FULL_EVIDENCE_EXAMPLE,
|
||||
native_observations: [
|
||||
JAVASCRIPT_FEATURE_TRACE_FULL_EVIDENCE_EXAMPLE.application,
|
||||
JAVASCRIPT_FEATURE_TRACE_FULL_EVIDENCE_EXAMPLE.application,
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(duplicateNative.isError).toBe(true);
|
||||
|
||||
const spoofed = await client.callTool({
|
||||
name: "trace_application_feature",
|
||||
arguments: {
|
||||
...JAVASCRIPT_FEATURE_TRACE_FULL_EVIDENCE_EXAMPLE,
|
||||
application: {
|
||||
...JAVASCRIPT_FEATURE_TRACE_FULL_EVIDENCE_EXAMPLE.application,
|
||||
provider: { id: "spoofed", name: "spoofed", version: "1" },
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(spoofed.isError).toBe(true);
|
||||
}
|
||||
|
||||
describe("application workflow MCP parity", () => {
|
||||
it("traces and compares authenticated graph Evidence in the session", async () => {
|
||||
const harness = await createApplicationMcpHarness();
|
||||
try {
|
||||
await runInlineEvidenceScenarios(harness.client, harness.session);
|
||||
await runInlineWorkflowScenarios(harness);
|
||||
await assertRejectedEvidenceReferences(harness.client, harness.session);
|
||||
await assertRejectedInlineEvidence(harness.client);
|
||||
} finally {
|
||||
await harness.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,225 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { copyFile, mkdir, rm, writeFile } from "node:fs/promises";
|
||||
import { join, relative, resolve } from "node:path";
|
||||
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
import { analyzeJavaScriptApplication } from "../../../src/application/javascript/JavaScriptApplicationService.js";
|
||||
import { compareJavaScriptExportShapesEvidence } from "../../../src/application/javascript/JavaScriptApplicationWorkflowService.js";
|
||||
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
|
||||
import { javaScriptExportShapeComparisonResultSchema } from "../../../src/domain/javascript/javascriptExportShapeComparisonSchemas.js";
|
||||
import { createApplicationMcpHarness } from "./mcpHarness.js";
|
||||
|
||||
describe("application workflow MCP parity", () => {
|
||||
it("compares exact parser export shapes with inline Evidence", async () => {
|
||||
const root = await createTestTempDirectory("rea-export-shape-mcp-");
|
||||
const leftRoot = join(root, "left");
|
||||
const rightRoot = join(root, "right");
|
||||
await Promise.all([mkdir(leftRoot), mkdir(rightRoot)]);
|
||||
await Promise.all([
|
||||
copyFile(
|
||||
resolve("tests/fixtures/replay/parser.mjs"),
|
||||
join(leftRoot, "parser.mjs"),
|
||||
),
|
||||
copyFile(
|
||||
resolve("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 leftAnalysis = javascriptApplicationAnalysisResultSchema.parse(
|
||||
left.value.normalized_result,
|
||||
);
|
||||
const harness = await createApplicationMcpHarness();
|
||||
const { client, session } = harness;
|
||||
const selectors = {
|
||||
left_module_path: "parser.mjs",
|
||||
left_export_name: "default",
|
||||
right_module_path: "parser.mjs",
|
||||
right_export_name: "default",
|
||||
};
|
||||
try {
|
||||
const relativeApplication = await client.callTool({
|
||||
name: "analyze_javascript_application",
|
||||
arguments: { input_path: relative(process.cwd(), leftRoot) },
|
||||
});
|
||||
expect(relativeApplication.isError).not.toBe(true);
|
||||
expect(relativeApplication.structuredContent).toMatchObject({
|
||||
evidence_id: left.value.evidence_id,
|
||||
result: { input_path: leftAnalysis.input_path },
|
||||
evidence: {
|
||||
subject: { local_path: left.value.subject?.local_path },
|
||||
},
|
||||
});
|
||||
const full = await client.callTool({
|
||||
name: "compare_javascript_export_shapes",
|
||||
arguments: { left: left.value, right: right.value, ...selectors },
|
||||
});
|
||||
expect(full.isError).not.toBe(true);
|
||||
expect(full.structuredContent).toMatchObject({
|
||||
result: {
|
||||
summary: { added: 1, removed: 0, changed: 0, unknown: 0 },
|
||||
changes: [
|
||||
{
|
||||
status: "added",
|
||||
path: "/depth",
|
||||
right: { availability: "literal", value: 1 },
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(session.exportEvidenceBundle().records).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
operation: "compare_javascript_export_shapes",
|
||||
predicate_type: "rea.javascript-export-shape-comparison",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
const analyzed = javascriptApplicationAnalysisResultSchema.parse(
|
||||
left.value.normalized_result,
|
||||
);
|
||||
const seed = analyzed.semantic_graph.relations[0]?.source_node_id;
|
||||
if (seed === undefined)
|
||||
throw new TypeError("Expected at least one semantic relation");
|
||||
const semantic = await client.callTool({
|
||||
name: "trace_javascript_semantics",
|
||||
arguments: {
|
||||
application: left.value,
|
||||
query: {
|
||||
seed: { kind: "semantic-node", node_id: seed },
|
||||
direction: "forward-influence",
|
||||
include_ambiguous_dynamic_edges: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(semantic.isError).not.toBe(true);
|
||||
expect(semantic.structuredContent).toMatchObject({
|
||||
result: {
|
||||
source_evidence_id: left.value.evidence_id,
|
||||
source_graph_id: analyzed.semantic_graph.graph_id,
|
||||
summary: { total_seed_matches: 1 },
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
await harness.close();
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("source-produced export comparison inventories", () => {
|
||||
it("retains large source-produced export inventories through comparison schemas", async () => {
|
||||
const fields = Array.from(
|
||||
{ length: 64 },
|
||||
(_, index) => `field${String(index)}: ${String(index)}`,
|
||||
).join(", ");
|
||||
const returnSites = Array.from(
|
||||
{ length: 33 },
|
||||
(_, index) =>
|
||||
`if (value === ${String(index)}) return { type: "variant-${String(index)}", ${fields} };`,
|
||||
).join("\n");
|
||||
const extraExports = Array.from(
|
||||
{ length: 1_000 },
|
||||
(_, index) =>
|
||||
`export const candidate${String(index)} = ${String(index)};`,
|
||||
).join("\n");
|
||||
const inventorySource = `export default function parse(value) {\n${returnSites}\n throw new Error("no match");\n}\n${extraExports}`;
|
||||
const pairedVariantsSource = `export default function parse(value) {
|
||||
${returnSites}
|
||||
throw new Error("no match");
|
||||
}`;
|
||||
const [inventoryLeft, inventoryRight] = await Promise.all([
|
||||
analyzeSourceEvidence(inventorySource),
|
||||
analyzeSourceEvidence(pairedVariantsSource),
|
||||
]);
|
||||
const variants = compareEvidence(inventoryLeft, inventoryRight);
|
||||
expect(variants.left).toMatchObject({ status: "selected" });
|
||||
expect(variants.coverage).toMatchObject({
|
||||
paired_variants: 33,
|
||||
unpaired_left_variants: 0,
|
||||
unpaired_right_variants: 0,
|
||||
left_source_omitted_variants: 0,
|
||||
right_source_omitted_variants: 0,
|
||||
left_omitted_fields: 0,
|
||||
right_omitted_fields: 0,
|
||||
});
|
||||
|
||||
const missing = compareEvidence(inventoryLeft, inventoryRight, {
|
||||
leftExportName: "missing",
|
||||
});
|
||||
expect(missing.left).toMatchObject({
|
||||
status: "missing",
|
||||
omitted_candidates: 0,
|
||||
});
|
||||
expect(missing.left.candidates).toHaveLength(1_001);
|
||||
|
||||
const changedFieldsLeft = Array.from(
|
||||
{ length: 10_001 },
|
||||
(_, index) => `field${String(index)}: ${String(index)}`,
|
||||
).join(", ");
|
||||
const changedFieldsRight = Array.from(
|
||||
{ length: 10_001 },
|
||||
(_, index) => `field${String(index)}: ${String(index + 1)}`,
|
||||
).join(", ");
|
||||
const [changeLeft, changeRight] = await Promise.all([
|
||||
analyzeSourceEvidence(
|
||||
`export default () => ({ type: "record", ${changedFieldsLeft} });`,
|
||||
),
|
||||
analyzeSourceEvidence(
|
||||
`export default () => ({ type: "record", ${changedFieldsRight} });`,
|
||||
),
|
||||
]);
|
||||
const changes = compareEvidence(changeLeft, changeRight);
|
||||
expect(changes.changes).toHaveLength(10_001);
|
||||
expect(changes.summary).toEqual({
|
||||
added: 0,
|
||||
removed: 0,
|
||||
changed: 10_001,
|
||||
unknown: 0,
|
||||
});
|
||||
expect(changes.coverage).toMatchObject({
|
||||
status: "complete-within-inputs",
|
||||
omitted_changes: 0,
|
||||
left_omitted_fields: 0,
|
||||
right_omitted_fields: 0,
|
||||
});
|
||||
}, 30_000);
|
||||
});
|
||||
|
||||
const analyzeSourceEvidence = async (source: string) => {
|
||||
const root = await createTestTempDirectory("rea-export-shape-inventory-");
|
||||
await writeFile(join(root, "parser.mjs"), source);
|
||||
const analyzed = await analyzeJavaScriptApplication({ input_path: root });
|
||||
if (!analyzed.ok) throw analyzed.error;
|
||||
return analyzed.value;
|
||||
};
|
||||
|
||||
const compareEvidence = (
|
||||
left: Awaited<ReturnType<typeof analyzeSourceEvidence>>,
|
||||
right: Awaited<ReturnType<typeof analyzeSourceEvidence>>,
|
||||
selectors: { readonly leftExportName?: string } = {},
|
||||
) => {
|
||||
const compared = compareJavaScriptExportShapesEvidence({
|
||||
left,
|
||||
right,
|
||||
left_module_path: "parser.mjs",
|
||||
left_export_name: selectors.leftExportName ?? "default",
|
||||
right_module_path: "parser.mjs",
|
||||
right_export_name: "default",
|
||||
});
|
||||
if (!compared.ok) throw compared.error;
|
||||
return javaScriptExportShapeComparisonResultSchema.parse(
|
||||
compared.value.normalized_result,
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
import { execFile } from "node:child_process";
|
||||
import { copyFile, mkdir, rm } 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 { writeVersionedJavaScriptApplicationFixtures } from "../../fixtures/javascriptArtifactApplication.js";
|
||||
|
||||
const execute = promisify(execFile);
|
||||
|
||||
describe("local application workflow verifier", () => {
|
||||
const temporary: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
temporary
|
||||
.splice(0)
|
||||
.map(async (path) => rm(path, { recursive: true, force: true })),
|
||||
);
|
||||
});
|
||||
|
||||
it("reports two-version and trace results", async () => {
|
||||
const root = await createTestTempDirectory("rea-application-verifier-");
|
||||
temporary.push(root);
|
||||
const fixtures = await writeVersionedJavaScriptApplicationFixtures(root);
|
||||
const { stdout } = await execute(
|
||||
process.execPath,
|
||||
[
|
||||
"scripts/verify/javascript/application-workflows.mjs",
|
||||
"--left",
|
||||
fixtures.left,
|
||||
"--right",
|
||||
fixtures.right,
|
||||
"--seed-kind",
|
||||
"module",
|
||||
"--seed-value",
|
||||
"stable",
|
||||
],
|
||||
{ cwd: process.cwd(), maxBuffer: 16 * 1_024 * 1_024 },
|
||||
);
|
||||
const result = JSON.parse(stdout);
|
||||
expect(result).toMatchObject({
|
||||
verified: true,
|
||||
summary: {
|
||||
added: expect.any(Number),
|
||||
removed: expect.any(Number),
|
||||
changed: expect.any(Number),
|
||||
unknown: expect.any(Number),
|
||||
},
|
||||
matching: {
|
||||
exact_module_source_digest: expect.any(Number),
|
||||
structural_fingerprint: expect.any(Number),
|
||||
},
|
||||
coverage: { status: expect.any(String) },
|
||||
trace: { summary: { matched_seeds: expect.any(Number) } },
|
||||
});
|
||||
}, 20_000);
|
||||
|
||||
it("reports an added parser return field", async () => {
|
||||
const root = await createTestTempDirectory("rea-shape-verifier-");
|
||||
temporary.push(root);
|
||||
const left = join(root, "left");
|
||||
const right = join(root, "right");
|
||||
await Promise.all([mkdir(left), mkdir(right)]);
|
||||
await Promise.all([
|
||||
copyFile(
|
||||
join(process.cwd(), "tests/fixtures/replay/parser.mjs"),
|
||||
join(left, "parser.mjs"),
|
||||
),
|
||||
copyFile(
|
||||
join(process.cwd(), "tests/fixtures/replay/parser-v2.mjs"),
|
||||
join(right, "parser.mjs"),
|
||||
),
|
||||
]);
|
||||
const { stdout } = await execute(
|
||||
process.execPath,
|
||||
[
|
||||
"scripts/verify/javascript/application-workflows.mjs",
|
||||
"--left",
|
||||
left,
|
||||
"--right",
|
||||
right,
|
||||
"--left-module-path",
|
||||
"parser.mjs",
|
||||
"--left-export-name",
|
||||
"default",
|
||||
"--right-module-path",
|
||||
"parser.mjs",
|
||||
"--right-export-name",
|
||||
"default",
|
||||
],
|
||||
{ cwd: process.cwd(), maxBuffer: 16 * 1_024 * 1_024 },
|
||||
);
|
||||
const result = JSON.parse(stdout);
|
||||
expect(result.export_shape_comparison).toMatchObject({
|
||||
summary: { added: 1, removed: 0, changed: 0, unknown: 0 },
|
||||
changes: [
|
||||
{
|
||||
status: "added",
|
||||
path: "/depth",
|
||||
right: { availability: "literal", value: 1 },
|
||||
},
|
||||
],
|
||||
});
|
||||
}, 20_000);
|
||||
});
|
||||
|
|
@ -0,0 +1,603 @@
|
|||
import { rm } from "node:fs/promises";
|
||||
import { relative, resolve } from "node:path";
|
||||
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
import { compareApplicationVersionsRequestSchema } from "../../../src/contracts/javascript/applicationWorkflowInputContracts.js";
|
||||
import { analyzeJavaScriptApplication } from "../../../src/application/javascript/JavaScriptApplicationService.js";
|
||||
import {
|
||||
compareApplicationVersionsEvidence,
|
||||
traceApplicationFeatureEvidence,
|
||||
} from "../../../src/application/javascript/JavaScriptApplicationWorkflowService.js";
|
||||
import { reconcileJavaScriptRuntimeEvidence } from "../../../src/application/javascript/JavaScriptRuntimeReconciliationService.js";
|
||||
import { createEvidence } from "../../../src/domain/evidence.js";
|
||||
import { AnalysisInputError } from "../../../src/domain/analysisErrorCore.js";
|
||||
import { jsonValueSchema } from "../../../src/domain/jsonValue.js";
|
||||
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
|
||||
import {
|
||||
applicationVersionComparisonResultSchema,
|
||||
type ApplicationVersionComparisonResult,
|
||||
} from "../../../src/domain/javascript/javascriptApplicationVersionComparisonSchemas.js";
|
||||
import { javascriptRuntimeReconciliationResultSchema } from "../../../src/domain/javascript/javascriptRuntimeReconciliationSchemas.js";
|
||||
import { compareJavaScriptApplicationVersions } from "../../../src/domain/javascript/javascriptApplicationVersionComparison.js";
|
||||
import { createJavaScriptApplicationGraph } from "../../../src/domain/javascript/javascriptApplicationGraph.js";
|
||||
import { traceApplicationFeature } from "../../../src/domain/javascript/javascriptFeatureTrace.js";
|
||||
import { applicationFeatureTraceResultSchema } from "../../../src/domain/javascript/javascriptFeatureTraceSchemas.js";
|
||||
import {
|
||||
APPLICATION_GRAPH_DIGESTS,
|
||||
buildSyntheticJavaScriptApplicationGraph,
|
||||
} from "../../../src/domain/javascript/javascriptApplicationGraph.fixture.js";
|
||||
import { writeVersionedJavaScriptApplicationFixtures } from "../../fixtures/javascriptArtifactApplication.js";
|
||||
import { JAVASCRIPT_APPLICATION_PROVIDER } from "../../../src/application/InvestigationProviders.js";
|
||||
import { JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE } from "../../../src/contracts/javascript/javascriptRuntimeReconciliationExample.js";
|
||||
|
||||
const temporary: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
temporary
|
||||
.splice(0)
|
||||
.map(async (path) => rm(path, { recursive: true, force: true })),
|
||||
);
|
||||
});
|
||||
|
||||
describe("JavaScript application workflows", () => {
|
||||
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",
|
||||
});
|
||||
|
||||
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 explicit and traces the full reachable graph", () => {
|
||||
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",
|
||||
});
|
||||
expect(noMatch).toMatchObject({
|
||||
graph: null,
|
||||
coverage: { status: "no-match" },
|
||||
summary: { matched_seeds: 0 },
|
||||
});
|
||||
|
||||
const complete = traceApplicationFeature({
|
||||
sourceEvidenceId: `ev_${"a".repeat(64)}`,
|
||||
graph,
|
||||
nativeEvidence: [],
|
||||
seed: {
|
||||
kind: "api",
|
||||
value: "desktopApi",
|
||||
match: "exact",
|
||||
case_sensitive: false,
|
||||
},
|
||||
direction: "outgoing",
|
||||
});
|
||||
expect(complete.coverage.status).toBe("complete-within-source");
|
||||
expect(complete.coverage.total_seed_matches).toBe(1);
|
||||
expect(complete.graph?.nodes.length).toBeGreaterThan(1);
|
||||
expect(complete.coverage).not.toHaveProperty("omitted_nodes");
|
||||
});
|
||||
});
|
||||
|
||||
describe("JavaScript application comparison workflows", () => {
|
||||
it("accepts complete inline Evidence and native observation arrays", () => {
|
||||
const native = Array.from({ length: 65 }, (_, index) =>
|
||||
createEvidence(
|
||||
{
|
||||
path: `/tmp/native-${index}.node`,
|
||||
sha256: index.toString(16).padStart(64, "0"),
|
||||
format: "elf",
|
||||
architecture: "x86_64",
|
||||
},
|
||||
{ id: "ghidra", name: "Ghidra", version: "fixture" },
|
||||
{
|
||||
operation: "analyze_function",
|
||||
parameters: { address: `0x${index.toString(16)}` },
|
||||
result: { address: `0x${index.toString(16)}` },
|
||||
},
|
||||
),
|
||||
);
|
||||
const applicationEvidence = (side: string) =>
|
||||
createEvidence(
|
||||
undefined,
|
||||
{ id: "fixture", name: "Fixture", version: "1" },
|
||||
{
|
||||
predicateType: "rea.javascript-application-analysis",
|
||||
operation: "analyze_javascript_application",
|
||||
parameters: { side },
|
||||
result: {},
|
||||
},
|
||||
);
|
||||
const input = {
|
||||
left: applicationEvidence("left"),
|
||||
right: applicationEvidence("right"),
|
||||
left_native_observations: native,
|
||||
};
|
||||
expect(
|
||||
compareApplicationVersionsRequestSchema.safeParse(input).success,
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("complete comparison projections", () => {
|
||||
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),
|
||||
analyzeFixture(fixtures.right),
|
||||
]);
|
||||
const compared = compareApplicationVersionsEvidence({ left, right });
|
||||
if (!compared.ok) throw compared.error;
|
||||
const result = applicationVersionComparisonResultSchema.parse(
|
||||
compared.value.normalized_result,
|
||||
);
|
||||
expectFullComparisonResultSchema(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.coverage).not.toHaveProperty("omitted_graph_nodes");
|
||||
expect(result.coverage).not.toHaveProperty("omitted_graph_edges");
|
||||
expect(result.coverage).not.toHaveProperty("omitted_graph_observations");
|
||||
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,
|
||||
);
|
||||
expectComparisonItemAlgebra(result);
|
||||
});
|
||||
|
||||
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),
|
||||
analyzeFixture(fixtures.right),
|
||||
]);
|
||||
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 source omissions", () => {
|
||||
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",
|
||||
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: [],
|
||||
};
|
||||
|
||||
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",
|
||||
right_graph_omitted_count: complete.nodes.length - 1,
|
||||
});
|
||||
expect(first.summary.removed).toBe(0);
|
||||
expect(first.summary.unknown).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("JavaScript application workflow error diagnostics", () => {
|
||||
it("returns safe explicit Evidence constraints from graph workflows", async () => {
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-application-workflow-diagnostics-",
|
||||
);
|
||||
temporary.push(root);
|
||||
const evidence = await analyzeFixture(root);
|
||||
const subject = evidence.subject;
|
||||
if (subject === null) throw new Error("Analysis Evidence subject missing");
|
||||
const secretPath = "/private/token=do-not-return";
|
||||
const inconsistent = {
|
||||
...evidence,
|
||||
subject: { ...subject, local_path: secretPath },
|
||||
};
|
||||
|
||||
const result = traceApplicationFeatureEvidence({
|
||||
application: inconsistent,
|
||||
seed: { kind: "route", value: "/" },
|
||||
direction: "both",
|
||||
});
|
||||
|
||||
if (result.ok) throw new Error("Expected inconsistent Evidence to fail");
|
||||
expect(result.error).toBeInstanceOf(AnalysisInputError);
|
||||
if (!(result.error instanceof AnalysisInputError)) return;
|
||||
expect(result.error.issues).toContainEqual(
|
||||
expect.objectContaining({
|
||||
reason: "invalid_value",
|
||||
message:
|
||||
"JavaScript application Evidence subject does not match its result",
|
||||
}),
|
||||
);
|
||||
expect(JSON.stringify(result.error.issues)).not.toContain(secretPath);
|
||||
});
|
||||
|
||||
it("preserves schema-authored Evidence validation messages", async () => {
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-application-workflow-schema-diagnostics-",
|
||||
);
|
||||
temporary.push(root);
|
||||
const evidence = await analyzeFixture(root);
|
||||
const subject = evidence.subject;
|
||||
if (subject === null) throw new Error("Analysis Evidence subject missing");
|
||||
const result = javascriptApplicationAnalysisResultSchema.parse(
|
||||
evidence.normalized_result,
|
||||
);
|
||||
const malformedResult = {
|
||||
...result,
|
||||
semantic_graph: {
|
||||
...result.semantic_graph,
|
||||
application_graph_id: `jag_${"f".repeat(64)}`,
|
||||
},
|
||||
};
|
||||
const malformedEvidence = createEvidence(
|
||||
{
|
||||
path: subject.local_path,
|
||||
sha256: subject.digest.sha256,
|
||||
format: subject.format,
|
||||
},
|
||||
JAVASCRIPT_APPLICATION_PROVIDER,
|
||||
{
|
||||
predicateType: evidence.predicate_type,
|
||||
operation: evidence.operation,
|
||||
parameters: evidence.parameters,
|
||||
result: jsonValueSchema.parse(malformedResult),
|
||||
confidence: evidence.confidence,
|
||||
authority: evidence.authority,
|
||||
limitations: evidence.limitations,
|
||||
locations: evidence.locations,
|
||||
evidenceLinks: evidence.evidence_links,
|
||||
},
|
||||
);
|
||||
|
||||
const traced = traceApplicationFeatureEvidence({
|
||||
application: malformedEvidence,
|
||||
seed: { kind: "route", value: "/" },
|
||||
direction: "both",
|
||||
});
|
||||
const reconciled = reconcileJavaScriptRuntimeEvidence({
|
||||
static_layers: [{ role: "application", analysis: malformedEvidence }],
|
||||
runtime_observations: [evidence],
|
||||
});
|
||||
|
||||
expect(traced.ok).toBe(false);
|
||||
expect(reconciled.ok).toBe(false);
|
||||
if (!traced.ok && traced.error instanceof AnalysisInputError)
|
||||
expect(traced.error.issues).toContainEqual(
|
||||
expect.objectContaining({
|
||||
message:
|
||||
"Semantic graph must commit the containing application graph",
|
||||
}),
|
||||
);
|
||||
if (!reconciled.ok && reconciled.error instanceof AnalysisInputError)
|
||||
expect(reconciled.error.issues).toContainEqual(
|
||||
expect.objectContaining({
|
||||
message:
|
||||
"Semantic graph must commit the containing application graph",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("returns supported-operation constraints from runtime reconciliation", async () => {
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-runtime-workflow-diagnostics-",
|
||||
);
|
||||
temporary.push(root);
|
||||
const evidence = await analyzeFixture(root);
|
||||
|
||||
const result = reconcileJavaScriptRuntimeEvidence({
|
||||
static_layers: [{ role: "application", analysis: evidence }],
|
||||
runtime_observations: [evidence],
|
||||
});
|
||||
|
||||
if (result.ok) throw new Error("Expected non-runtime Evidence to fail");
|
||||
expect(result.error).toBeInstanceOf(AnalysisInputError);
|
||||
if (!(result.error instanceof AnalysisInputError)) return;
|
||||
expect(result.error.issues).toContainEqual(
|
||||
expect.objectContaining({
|
||||
reason: "invalid_value",
|
||||
message:
|
||||
"Runtime reconciliation requires inspect_web_page, inspect_electron_page, observe_javascript_runtime, or capture_electron_scenario Evidence",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("JavaScript runtime reconciliation local paths", () => {
|
||||
it("resolves relative file mapping roots before deriving result Evidence", () => {
|
||||
const absoluteRoot = resolve(process.cwd(), "runtime-cache-fixture");
|
||||
const relativeRoot = relative(process.cwd(), absoluteRoot);
|
||||
const base = JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE;
|
||||
const [baseLayer] = base.static_layers;
|
||||
if (baseLayer === undefined) throw new Error("Static layer missing");
|
||||
const input = {
|
||||
...base,
|
||||
static_layers: [
|
||||
{
|
||||
...baseLayer,
|
||||
runtime_mappings: [
|
||||
{ kind: "file-root" as const, root: relativeRoot },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const relativeResult = reconcileJavaScriptRuntimeEvidence(input);
|
||||
const absoluteResult = reconcileJavaScriptRuntimeEvidence({
|
||||
...input,
|
||||
static_layers: [
|
||||
{
|
||||
...input.static_layers[0],
|
||||
runtime_mappings: [{ kind: "file-root", root: absoluteRoot }],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(relativeResult.ok).toBe(true);
|
||||
expect(absoluteResult.ok).toBe(true);
|
||||
if (!relativeResult.ok || !absoluteResult.ok) return;
|
||||
expect(relativeResult.value.evidence_id).toBe(
|
||||
absoluteResult.value.evidence_id,
|
||||
);
|
||||
expect(relativeResult.value.normalized_result).toEqual(
|
||||
absoluteResult.value.normalized_result,
|
||||
);
|
||||
const result = javascriptRuntimeReconciliationResultSchema.parse(
|
||||
relativeResult.value.normalized_result,
|
||||
);
|
||||
expect(result.static_layers[0]?.runtime_mappings[0]).toMatchObject({
|
||||
kind: "file-root",
|
||||
root: absoluteRoot,
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves foreign absolute file roots without matching another path syntax", () => {
|
||||
const base = JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE;
|
||||
const [baseLayer] = base.static_layers;
|
||||
if (baseLayer === undefined) throw new Error("Static layer missing");
|
||||
const remoteWindowsRoot = String.raw`C:\runtime\cache`;
|
||||
const reconciled = reconcileJavaScriptRuntimeEvidence({
|
||||
...base,
|
||||
static_layers: [
|
||||
{
|
||||
...baseLayer,
|
||||
runtime_mappings: [{ kind: "file-root", root: remoteWindowsRoot }],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(reconciled.ok).toBe(true);
|
||||
if (!reconciled.ok) return;
|
||||
const result = javascriptRuntimeReconciliationResultSchema.parse(
|
||||
reconciled.value.normalized_result,
|
||||
);
|
||||
expect(result.static_layers[0]?.runtime_mappings[0]).toMatchObject({
|
||||
kind: "file-root",
|
||||
root: remoteWindowsRoot,
|
||||
});
|
||||
expect(
|
||||
result.reconciliations.some(
|
||||
({ basis }) => basis === "operator-file-mapping",
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
const analyzeFixture = async (root: string) => {
|
||||
const result = await analyzeJavaScriptApplication({
|
||||
input_path: root,
|
||||
});
|
||||
if (!result.ok) throw result.error;
|
||||
return result.value;
|
||||
};
|
||||
|
||||
const expectComparisonItemAlgebra = (
|
||||
result: ApplicationVersionComparisonResult,
|
||||
): void => {
|
||||
const matched = result.items.find(({ match }) => match.status === "matched");
|
||||
expect(matched).toBeDefined();
|
||||
if (matched === undefined) return;
|
||||
expect(
|
||||
applicationVersionComparisonResultSchema.safeParse({
|
||||
...result,
|
||||
items: [{ ...matched, right_node_id: null }],
|
||||
}).success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
applicationVersionComparisonResultSchema.safeParse({
|
||||
...result,
|
||||
items: [
|
||||
{
|
||||
...matched,
|
||||
match: {
|
||||
...matched.match,
|
||||
basis: "none",
|
||||
confidence: "unknown",
|
||||
},
|
||||
},
|
||||
],
|
||||
}).success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
applicationVersionComparisonResultSchema.safeParse({
|
||||
...result,
|
||||
coverage: {
|
||||
...result.coverage,
|
||||
status: "complete-within-inputs",
|
||||
left_graph_status: "partial",
|
||||
},
|
||||
}).success,
|
||||
).toBe(false);
|
||||
};
|
||||
|
||||
const expectFullComparisonResultSchema = (
|
||||
result: ApplicationVersionComparisonResult,
|
||||
): void => {
|
||||
const candidateNodeIds = Array.from(
|
||||
{ length: 1_001 },
|
||||
(_, index) => `jag_node_${index.toString(16).padStart(64, "0")}`,
|
||||
);
|
||||
const expanded = {
|
||||
...result,
|
||||
evidence_links: Array.from(
|
||||
{ length: 131 },
|
||||
(_, index) => `ev_${index.toString(16).padStart(64, "0")}`,
|
||||
),
|
||||
items: result.items.map((item) =>
|
||||
item.match.status === "ambiguous"
|
||||
? {
|
||||
...item,
|
||||
match: {
|
||||
...item.match,
|
||||
...(item.match.candidate_left_node_ids.length === 0
|
||||
? { candidate_right_node_ids: candidateNodeIds }
|
||||
: { candidate_left_node_ids: candidateNodeIds }),
|
||||
},
|
||||
}
|
||||
: item,
|
||||
),
|
||||
};
|
||||
expect(
|
||||
applicationVersionComparisonResultSchema.safeParse(expanded).success,
|
||||
).toBe(true);
|
||||
};
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
||||
|
||||
import { createTestBinarySession } from "../../fixtures/binarySession.js";
|
||||
import type { BinarySession } from "../../../src/application/binary/BinarySession.js";
|
||||
import { observed } from "../../fixtures/analysisExecution.js";
|
||||
import { createServer } from "../../../src/server/createServer.js";
|
||||
|
||||
/** Owned MCP SDK session shared by application workflow scenarios. */
|
||||
export interface ApplicationMcpHarness {
|
||||
readonly client: Client;
|
||||
readonly session: BinarySession;
|
||||
readonly close: () => Promise<void>;
|
||||
}
|
||||
|
||||
/** Connect application workflows through the production MCP registration. */
|
||||
export async function createApplicationMcpHarness(): Promise<ApplicationMcpHarness> {
|
||||
const session = createTestBinarySession(() => ({
|
||||
execute: () => Promise.resolve(observed(null)),
|
||||
close: () => Promise.resolve(),
|
||||
}));
|
||||
const server = createServer(session, session);
|
||||
const client = new Client({
|
||||
name: "application-workflow-test",
|
||||
version: "1",
|
||||
});
|
||||
const [clientTransport, serverTransport] =
|
||||
InMemoryTransport.createLinkedPair();
|
||||
|
||||
await server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
|
||||
return {
|
||||
client,
|
||||
session,
|
||||
close: async () => {
|
||||
await Promise.all([client.close(), server.close(), session.close()]);
|
||||
},
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue