Files
reverse-engineering-anything/tests/acceptance/applications/staticObservedEndpoints.test.ts
T
gensokyo-dev 3708538e85 feat(javascript): compare static endpoints with HAR captures (#9090)
* feat(javascript): compare static endpoints with HAR captures

* test(javascript): preserve original HAR fixture bytes during formatting

* test(javascript): load the byte-preserved HAR fixture
2026-11-06 13:34:26 +08:00

410 lines
13 KiB
TypeScript

import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { createHash } from "node:crypto";
import { expect } from "vitest";
import { cliTest, type TestCli } from "../../support/cli/cliFixture.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
import { advertisedMcpToolSchemas } from "../../fixtures/advertisedMcpTool.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import {
createEvidence,
parseEvidence,
type Evidence,
} from "../../../src/domain/evidence.js";
import {
createEvidenceBundle,
evidenceBundleSchema,
} from "../../../src/domain/evidenceBundle.js";
import {
staticObservedEndpointComparisonSchema,
type StaticObservedEndpointComparison,
} from "../../../src/domain/javascript/staticObservedEndpointSchemas.js";
import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js";
import { webNetworkCaptureSchema } from "../../../src/domain/webNetworkCapture.js";
const prepareInputs = async (cli: TestCli) => {
const root = await createTestTempDirectory("rea-static-observed-endpoints-");
const appPath = join(root, "app");
await mkdir(appPath);
const fixture = new URL(
"../../fixtures/javascript-endpoint-capture/",
import.meta.url,
);
const sourcePath = join(appPath, "app.js");
const source = await readFile(new URL("app.js.txt", fixture), "utf8");
await writeFile(sourcePath, source);
await writeFile(
join(appPath, "index.html"),
await readFile(new URL("index.html.txt", fixture)),
);
const harPath = join(root, "capture.har");
const harBytes = await readFile(
new URL("recording.har.txt", fixture),
"utf8",
);
expect(createHash("sha256").update(harBytes).digest("hex")).toBe(
"f0e7d2c869c811d4272a7b2916c6531c8c6cc7938336a35575e2a6b7b6346714",
);
await writeFile(harPath, harBytes);
const run = cli.sharedCliRun?.bind(cli) ?? cli.run.bind(cli);
const analyzed = await run({
arguments: [
"analyze-javascript-application",
appPath,
"--artifact-format",
"directory",
"--json",
],
});
expect(analyzed.exitCode, analyzed.stderr).toBe(0);
const application = parseEvidence(analyzed.json);
const inspected = await run({
arguments: ["inspect-web-network-capture", harPath, "har", "--json"],
});
expect(inspected.exitCode, inspected.stderr).toBe(0);
const capture = parseEvidence(inspected.json);
const input = { application, capture, origin: "http://127.0.0.1:42845" };
const inputPath = join(root, "comparison-input.json");
await writeFile(inputPath, JSON.stringify(input));
return {
root,
sourcePath,
source,
harPath,
harBytes,
run,
application,
capture,
input,
inputPath,
};
};
type McpCall = Awaited<ReturnType<typeof connectLocalToolsMcp>>["call"];
const assertPagedComparison = async (
call: McpCall,
report: StaticObservedEndpointComparison,
complete: Evidence,
input: Parameters<McpCall>[1],
) => {
const pageRequest = {
source: { kind: "retained-evidence", evidence_id: complete.evidence_id },
view: {
kind: "endpoint-comparison",
collection: "static-endpoints",
fields: ["path", "status", "reason"],
offset: 0,
limit: 2,
},
};
const first = await call("inspect_analysis_view", pageRequest);
const firstView = analysisViewResultSchema.parse(
parseEvidence(first.structuredContent).normalized_result,
);
expect(firstView).toMatchObject({
source_coverage: report.source_coverage,
summary: report.summary,
coverage: { examined: 2, total: 7, exhausted: false, next_offset: 2 },
});
if (firstView.kind !== "endpoint-comparison")
throw new Error("Expected endpoint page");
const inline = await call("compare_static_and_observed_endpoints", {
...input,
detail: "complete",
view: pageRequest.view,
});
expect(inline.isError, JSON.stringify(inline.content)).not.toBe(true);
expect(parseEvidence(inline.structuredContent).normalized_result).toEqual(
firstView,
);
expect(firstView.items).toEqual(
report.static_endpoints
.slice(0, 2)
.map(
({ endpoint_id, node_id, observation_id, path, status, reason }) => ({
endpoint_id,
node_id,
observation_id,
path,
status,
reason,
}),
),
);
const empty = await call("inspect_analysis_view", {
...pageRequest,
view: { ...pageRequest.view, statuses: [], fields: [] },
});
expect(
parseEvidence(empty.structuredContent).normalized_result,
).toMatchObject({
items: [],
coverage: { total: 0, exhausted: true },
summary: report.summary,
});
const matchPage = await call("inspect_analysis_view", {
source: pageRequest.source,
view: { kind: "endpoint-comparison", collection: "matches", fields: [] },
});
expect(
parseEvidence(matchPage.structuredContent).normalized_result,
).toMatchObject({
items: report.matches.map(({ match_id, endpoint_id, group_id }) => ({
match_id,
endpoint_id,
group_id,
})),
});
return { firstView, pageRequest };
};
const assertCliPublication = async (
context: Awaited<ReturnType<typeof prepareInputs>>,
comparison: {
readonly complete: Evidence;
readonly summary: Awaited<ReturnType<McpCall>>;
readonly views: Awaited<ReturnType<typeof assertPagedComparison>>;
},
) => {
const { root, run, inputPath, application, capture } = context;
const {
complete,
summary,
views: { firstView, pageRequest },
} = comparison;
const bundlePath = join(root, "comparison.bundle.json");
const cliSummary = await run({
arguments: [
"compare-static-and-observed-endpoints",
inputPath,
"--detail",
"summary",
"--evidence_output",
bundlePath,
"--json",
],
});
expect(cliSummary.exitCode, cliSummary.stderr).toBe(0);
expect(cliSummary.json).toMatchObject({
summary: summary.structuredContent,
});
const bundle = evidenceBundleSchema.parse(
JSON.parse(await readFile(bundlePath, "utf8")),
);
expect(new Set(bundle.records.map(({ evidence_id }) => evidence_id))).toEqual(
new Set([
application.evidence_id,
capture.evidence_id,
complete.evidence_id,
]),
);
const fileViewPath = join(root, "page-input.json");
await writeFile(
fileViewPath,
JSON.stringify({
...pageRequest,
source: {
kind: "evidence-file",
path: bundlePath,
evidence_id: complete.evidence_id,
},
}),
);
const cliPage = await run({
arguments: ["inspect-analysis-view", fileViewPath, "--json"],
});
expect(cliPage.exitCode, cliPage.stderr).toBe(0);
expect(parseEvidence(cliPage.json).normalized_result).toEqual(firstView);
const inlineCli = await run({
arguments: [
"compare-static-and-observed-endpoints",
inputPath,
"--detail",
"summary",
"--view",
JSON.stringify(pageRequest.view),
"--evidence_output",
join(root, "selected.bundle.json"),
"--json",
],
});
expect(inlineCli.exitCode, inlineCli.stderr).toBe(0);
expect(inlineCli.json).toMatchObject({
summary: { normalized_result: firstView },
});
const invalidView = await run({
arguments: [
"compare-static-and-observed-endpoints",
inputPath,
"--view",
JSON.stringify(pageRequest.view),
"--json",
],
});
expect(invalidView.exitCode).not.toBe(0);
expect(invalidView.json).toMatchObject({ code: "invalid_request" });
const missingSink = await run({
arguments: [
"compare-static-and-observed-endpoints",
inputPath,
"--detail",
"summary",
"--json",
],
});
expect(missingSink.exitCode).not.toBe(0);
expect(missingSink.json).toMatchObject({ code: "invalid_request" });
};
cliTest(
"compares real source and decoded HAR through CLI/MCP, retains complete facts and pages stable identities",
async ({ cli }) => {
const context = await prepareInputs(cli);
const {
root,
sourcePath,
source,
harPath,
harBytes,
run,
application,
capture,
input,
inputPath,
} = context;
const compared = await run({
arguments: ["compare-static-and-observed-endpoints", inputPath, "--json"],
});
expect(compared.exitCode, compared.stderr).toBe(0);
const complete = parseEvidence(compared.json);
const report = staticObservedEndpointComparisonSchema.parse(
complete.normalized_result,
);
expect(report.summary).toMatchObject({
static_network_endpoint_observations: 7,
matched: 3,
not_observed_in_capture: 1,
unknown: 3,
observed_without_static_match: 3,
});
expect(new Set(report.matches.map(({ basis }) => basis))).toEqual(
new Set(["exact", "template-compatible"]),
);
expect(complete.evidence_links).toEqual(
[application.evidence_id, capture.evidence_id].sort(),
);
expect(JSON.stringify(report)).not.toMatch(/kept-source-only|fragment|KEY/);
const { client, call } = await connectLocalToolsMcp({
REA_MCP_INPUT_SCHEMA_PROFILE: "full",
REA_MCP_TOOL_FAMILIES: "application,session",
});
const schemas = await advertisedMcpToolSchemas(
client,
"compare_static_and_observed_endpoints",
);
expect(schemas.ajv.validate(schemas.inputSchema, input)).toBe(true);
expect(
schemas.ajv.validate(schemas.inputSchema, { ...input, detail: "wrong" }),
).toBe(false);
const retained = {
application: {
kind: "retained-evidence",
evidence_id: application.evidence_id,
},
capture: { kind: "retained-evidence", evidence_id: capture.evidence_id },
origin: input.origin,
};
let bundleNumber = 0;
const importRecords = async (
records: Parameters<typeof createEvidenceBundle>[0],
) => {
const path = join(root, `sources-${bundleNumber++}.bundle.json`);
await writeFile(path, JSON.stringify(createEvidenceBundle(records)));
const response = await call("import_evidence_bundle", { path });
expect(response.isError, JSON.stringify(response.content)).not.toBe(true);
};
await importRecords([application, capture]);
const fullMcp = await call("compare_static_and_observed_endpoints", {
...retained,
detail: "complete",
});
expect(fullMcp.isError, JSON.stringify(fullMcp.content)).not.toBe(true);
expect(parseEvidence(fullMcp.structuredContent)).toEqual(complete);
const summary = await call(
"compare_static_and_observed_endpoints",
retained,
);
expect(summary.isError).not.toBe(true);
const summaryView = analysisViewResultSchema.parse(
parseEvidence(summary.structuredContent).normalized_result,
);
expect(summaryView).toMatchObject({
kind: "summary",
parent_evidence_id: complete.evidence_id,
summary: {
summary: report.summary,
source_coverage: report.source_coverage,
},
});
expect(summaryView).not.toHaveProperty("static_endpoints");
const views = await assertPagedComparison(call, report, complete, retained);
await assertCliPublication(context, { complete, summary, views });
const tampered = await call("compare_static_and_observed_endpoints", {
...retained,
capture: { ...capture, normalized_result: {} },
});
expect(parseMcpToolError(tampered).error).toMatchObject({
code: "evidence_integrity_mismatch",
});
const sourceCapture = webNetworkCaptureSchema.parse(
capture.normalized_result,
);
const replacement = (parameters: object, result: unknown) =>
createEvidence(undefined, capture.provider, {
operation: capture.operation,
parameters,
result,
confidence: "observed",
});
const declared = replacement(
{ ...capture.parameters, sensitive_value_count: 1 },
sourceCapture,
);
await importRecords([declared]);
const refused = await call("compare_static_and_observed_endpoints", {
...retained,
capture: { kind: "retained-evidence", evidence_id: declared.evidence_id },
});
expect(parseMcpToolError(refused).error).toMatchObject({
code: "unsupported_target",
});
expect(JSON.stringify(refused)).toContain("sensitive_values_unretained");
const subset = replacement(capture.parameters, {
...sourceCapture,
records: sourceCapture.records.slice(0, 1),
});
await importRecords([subset]);
const limited = await call("compare_static_and_observed_endpoints", {
...retained,
capture: { kind: "retained-evidence", evidence_id: subset.evidence_id },
detail: "complete",
});
expect(limited.isError).not.toBe(true);
expect(
staticObservedEndpointComparisonSchema
.parse(parseEvidence(limited.structuredContent).normalized_result)
.static_endpoints.find(({ path }) => path === "/api/admin/export"),
).toMatchObject({ status: "unknown", reason: "capture-incomplete" });
expect(await readFile(sourcePath, "utf8")).toBe(source);
expect(
createHash("sha256")
.update(await readFile(harPath))
.digest("hex"),
).toBe(createHash("sha256").update(harBytes).digest("hex"));
},
);