Files
reverse-engineering-anything/tests/boundary/cli/managedMemberSummaryCli.test.ts
Kaoru0822andClaude Opus 5.5 4a4bbab3c3 fix(cli): keep committed evidence bundle receipts after cancellation (#6221)
Summary delivery with --evidence_output wrote the bundle, then let the
command's close/cancellation check replace the result. A SIGINT/SIGTERM
during staging cleanup after commit returned typed cancellation without
the receipt, and a persistent staging cleanup failure after commit was
rewrapped as a provider cleanup error that dropped published_path.

Every --evidence_output command now selects its view first and publishes
through publishEvidenceDelivery, the owner inspect-managed-members already
used. Post-commit cancellation keeps {path, bytes} under
details.diagnostics.evidence_bundle; incomplete cleanup keeps
details.published_path and bytes; a cleanup retry that succeeds returns
the normal receipt; pre-commit cancellation creates nothing and names
the command's operation.

Fixes #6095

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 20:32:26 -07:00

514 lines
17 KiB
TypeScript

import { pathToFileURL } from "node:url";
import {
mkdir,
readFile,
readdir,
realpath,
unlink,
writeFile,
} from "node:fs/promises";
import { join, resolve } from "node:path";
import { expect } from "vitest";
import { inspectAnalysisView } from "../../../src/application/analysisView/AnalysisViewService.js";
import {
analysisCliErrorEnvelopeSchema,
analysisErrorProjectionSchema,
} from "../../../src/contracts/errorSchemas.js";
import {
analysisViewResultSchema,
managedMemberSummaryViewResultSchema,
} from "../../../src/domain/analysisView/analysisView.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { parseEvidenceBundle } from "../../../src/domain/evidenceBundle.js";
import { managedMemberInspectionSchema } from "../../../src/domain/managed/managedArtifact.js";
import { buildManagedPeFixture } from "../../../src/dotnet/ManagedPe.fixture.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
const memberFixture = (): Buffer =>
buildManagedPeFixture({
methodRowCount: 2,
memberRefRowCount: 3,
// ldarg.0; ldfld field; call member ref; ldstr "ok"; pop; ret
ilBody: Buffer.from([
(18 << 2) | 2,
0x02,
0x7b,
1,
0,
0,
0x04,
0x28,
1,
0,
0,
0x0a,
0x72,
1,
0,
0,
0x70,
0x26,
0x2a,
]),
userStringHeap: Buffer.from([0, 5, 0x6f, 0, 0x6b, 0, 0]),
exportedTypes: [
{
namespace: "System",
name: "Object",
flags: 0x0020_0001,
implementationRaw: (1 << 2) | 1,
},
],
});
cliTest(
"advertises durable summary and portable paging options in CLI help",
async ({ cli }) => {
for (const command of [
"inspect-managed-members",
"inspect-analysis-view",
]) {
const result = await cli.run({ arguments: [command, "--help"] });
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("--evidence-bundle");
if (command === "inspect-managed-members") {
expect(result.stdout).toContain("--detail");
expect(result.stdout).toContain("summary");
expect(result.stdout).toContain("complete");
}
}
},
);
cliTest(
"retains complete member Evidence for exact CLI pages after process exit and target removal",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-member-summary-cli-");
const path = join(root, "fixture.dll");
const bundlePath = join(root, "members.json");
await writeFile(path, memberFixture());
const typeNames = ["Fixture.Program", "Missing.Type"];
const arguments_ = [
"inspect-managed-members",
path,
...typeNames.flatMap((type) => ["--type", type]),
"--json",
];
const omitted = await cli.run({ arguments: arguments_ });
expect(omitted.exitCode).toBe(0);
const complete = parseEvidence(omitted.json);
const completeBundlePath = join(root, "complete.json");
const explicit = await cli.run({
arguments: [
...arguments_,
"--detail",
"complete",
"--evidence-bundle",
completeBundlePath,
],
});
expect(explicit.exitCode).toBe(0);
expect(parseEvidence(explicit.json).evidence_id).toBe(complete.evidence_id);
expect(
parseEvidenceBundle(
JSON.parse(await readFile(completeBundlePath, "utf8")),
).records[0]?.evidence_id,
).toBe(complete.evidence_id);
const reply = await cli.run({
cwd: root,
arguments: [
...arguments_,
"--detail",
"summary",
"--evidence-bundle",
"members.json",
],
});
expect(reply.exitCode).toBe(0);
const receipt = parseEvidence(reply.json);
const summary = managedMemberSummaryViewResultSchema.parse(
receipt.normalized_result,
);
const originalBytes = await readFile(bundlePath, "utf8");
const bundle = parseEvidenceBundle(JSON.parse(originalBytes));
expect(bundle.records).toHaveLength(1);
const parent = bundle.records[0]!;
expect(parent.evidence_id).toBe(complete.evidence_id);
expect(parent.normalized_result).toEqual(complete.normalized_result);
expect(parent.raw_result).toEqual(complete.raw_result);
expect(parent.subject).toEqual(complete.subject);
expect(parent.locations).toEqual(complete.locations);
expect(parent.parameters).toEqual({ type_names: typeNames });
expect(receipt.evidence_links).toEqual([parent.evidence_id]);
const portable = inspectAnalysisView({
source: { kind: "inline", evidence: parent },
view: { kind: "summary" },
});
if (!portable.ok) throw portable.error;
expect(receipt.evidence_id).toBe(portable.value.evidence_id);
expect(receipt.normalized_result).toEqual(portable.value.normalized_result);
const report = managedMemberInspectionSchema.parse(
parent.normalized_result,
);
expect(summary.summary.selection).toEqual({
type_names: typeNames,
unmatched_type_names: ["Missing.Type"],
});
expect(summary.summary.coverage).toEqual(report.coverage);
expect(summary.summary.identity_scope).toEqual(report.identity_scope);
expect(summary.summary).not.toHaveProperty("member_refs");
expect(summary.summary).not.toHaveProperty("methods");
await unlink(path);
const otherDirectory = join(root, "later-process");
await mkdir(otherDirectory);
for (const collection of [
"types",
"exported_types",
"fields",
"methods",
"member_refs",
"call_edges",
"field_accesses",
"user_strings",
] as const) {
const rows =
collection === "user_strings"
? report.user_strings?.literals
: report[collection];
if (rows == null) throw new Error(`${collection} was not examined`);
expect(rows.length, collection).toBeGreaterThan(0);
expect(summary.summary.reported_counts[collection]).toBe(rows.length);
const paged: unknown[] = [];
for (let offset: number | null = 0; offset !== null;) {
const response = await cli.run({
cwd: otherDirectory,
arguments: [
"inspect-analysis-view",
JSON.stringify({
source: {
kind: "retained-evidence",
evidence_id: parent.evidence_id,
},
view: { kind: "managed-members", collection, offset, limit: 1 },
}),
"--evidence-bundle",
bundlePath,
"--json",
],
});
expect(response.exitCode).toBe(0);
const evidence = parseEvidence(response.json);
expect(evidence.evidence_links).toEqual([parent.evidence_id]);
expect(evidence.subject).toEqual(parent.subject);
const page = analysisViewResultSchema.parse(evidence.normalized_result);
if (page.kind !== "managed-members") throw new Error("Wrong page kind");
expect(page.page.collection).toBe(collection);
expect(page.selection).toEqual(report.selection);
expect(page.source_coverage).toEqual(report.coverage);
expect(page.source_metadata_status).toBe(report.metadata.status);
expect(page.coverage.total).toBe(rows.length);
paged.push(...page.page.rows);
offset = page.coverage.next_offset;
}
expect(paged).toEqual(rows);
}
expect(await readFile(bundlePath, "utf8")).toBe(originalBytes);
},
180_000,
);
cliTest(
"refuses summary without a durable destination before opening the target",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-member-summary-missing-");
const result = await cli.run({
arguments: [
"inspect-managed-members",
join(root, "absent.dll"),
"--detail",
"summary",
"--json",
],
});
expect(result.exitCode).toBe(1);
expect(result.json).toMatchObject({
code: "invalid_request",
details: {
issues: [
expect.objectContaining({
path: ["evidence_bundle"],
message: expect.stringContaining("summary"),
}),
],
},
});
expect(result.json).not.toHaveProperty("evidence_id");
},
);
cliTest(
"does not replace caller files or report successful summary when bundle publication fails",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-member-summary-write-");
const path = join(root, "fixture.dll");
const bytes = buildManagedPeFixture();
await writeFile(path, bytes);
const complete = await cli.run({
arguments: ["inspect-managed-members", path, "--json"],
});
expect(complete.exitCode).toBe(0);
const parent = parseEvidence(complete.json);
for (const destination of [path, join(root, "absent", "bundle.json")]) {
const result = await cli.run({
arguments: [
"inspect-managed-members",
path,
"--detail",
"summary",
"--evidence-bundle",
destination,
"--json",
],
});
expect(result.exitCode).toBe(1);
expect(result.json).toMatchObject({
code: "invalid_request",
details: {
path: destination,
reason: destination === path ? "exists" : "missing",
},
});
const {
error: _label,
input_path: _inputPath,
input_reason: _inputReason,
...projection
} = analysisCliErrorEnvelopeSchema.parse(result.json);
const failure = analysisErrorProjectionSchema.parse(projection);
if (failure.code !== "invalid_request")
throw new Error(`Unexpected publication failure: ${failure.code}`);
const retained = parseEvidence(failure.details?.partial_observation);
expect(retained.evidence_id).toBe(parent.evidence_id);
expect(retained.normalized_result).toEqual(parent.normalized_result);
expect(result.json).not.toHaveProperty("evidence_id");
expect(result.json).not.toHaveProperty("normalized_result");
}
expect(await readFile(path)).toEqual(bytes);
expect(await readdir(root)).toEqual(["fixture.dll"]);
},
);
cliTest(
"preserves provider refusal without publishing an Evidence bundle",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-member-summary-provider-");
const path = join(root, "absent.dll");
const destination = join(root, "bundle.json");
const complete = await cli.run({
arguments: ["inspect-managed-members", path, "--json"],
});
const summary = await cli.run({
arguments: [
"inspect-managed-members",
path,
"--detail",
"summary",
"--evidence-bundle",
destination,
"--json",
],
});
expect(complete.exitCode).toBe(1);
expect(summary.exitCode).toBe(1);
expect(summary.json).toEqual(complete.json);
await expect(readFile(destination)).rejects.toMatchObject({
code: "ENOENT",
});
},
);
cliTest(
"keeps unavailable managed member coverage explicit in saved summary Evidence",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-member-summary-unknown-");
const path = join(root, "malformed.dll");
const destination = join(root, "bundle.json");
await writeFile(path, buildManagedPeFixture({ virtualSectionSize: 0 }));
const result = await cli.run({
arguments: [
"inspect-managed-members",
path,
"--detail",
"summary",
"--evidence-bundle",
destination,
"--json",
],
});
expect(result.exitCode).toBe(0);
const summary = managedMemberSummaryViewResultSchema.parse(
parseEvidence(result.json).normalized_result,
);
expect(summary.summary).toMatchObject({
metadata: { status: "malformed" },
coverage: { state: "unavailable" },
reported_counts: { methods: 0 },
});
expect(summary.unknowns.length).toBeGreaterThan(0);
const bundle = parseEvidenceBundle(
JSON.parse(await readFile(destination, "utf8")),
);
const report = managedMemberInspectionSchema.parse(
bundle.records[0]!.normalized_result,
);
expect(summary.summary.coverage).toEqual(report.coverage);
expect(summary.limitations).toEqual(report.limitations);
},
);
cliTest.for(
(process.platform === "win32"
? (["event"] as const)
: (["event", "os"] as const)
).flatMap((delivery) => [
{ signal: "SIGINT" as const, status: 130, delivery },
{ signal: "SIGTERM" as const, status: 143, delivery },
]),
)(
"keeps a committed receipt when $signal ($delivery) is queued during final cleanup",
async ({ signal, status, delivery }, { cli }) => {
const root = await createTestTempDirectory("rea-managed-queued-signal-");
const target = join(root, "fixture.dll");
const destination = join(root, "parent.json");
const marker = join(root, "signal.json");
const preload = resolve("tests/fixtures/evidencePublicationPreload.mjs");
const prefix = join(await realpath(root), ".rea-write-");
await writeFile(target, memberFixture());
const reply = await cli.run({
cwd: root,
arguments: [
"inspect-managed-members",
target,
"--type",
"Fixture.Program",
"--detail",
"summary",
"--evidence-bundle",
destination,
"--json",
],
environment: {
HOME: root,
REA_LOG_LEVEL: "silent",
REA_TEST_EVIDENCE_PUBLICATION: JSON.stringify({
prefix,
marker,
signal,
delivery,
}),
NODE_OPTIONS: `--max-old-space-size=2048 --import=${pathToFileURL(preload).href}`,
},
});
expect(reply.exitCode).toBe(status);
const { error: _label, ...projection } =
analysisCliErrorEnvelopeSchema.parse(reply.json);
const error = analysisErrorProjectionSchema.parse(projection);
expect(error.code).toBe("cancelled");
expect(error.details?.operation).toBe("inspect_managed_members");
const bytes = await readFile(destination);
const parent = parseEvidenceBundle(JSON.parse(bytes.toString()))
.records[0]!;
expect(error.details).toMatchObject({
diagnostics: {
evidence_bundle: { path: destination, bytes: bytes.length },
},
});
const partial = parseEvidence(error.details?.partial_observation);
expect(partial.evidence_links).toEqual([parent.evidence_id]);
expect(partial.normalized_result).toMatchObject({
kind: "summary",
parent_evidence_id: parent.evidence_id,
});
expect(JSON.parse(await readFile(marker, "utf8"))).toMatchObject({
signal,
listeners: expect.any(Number),
});
expect(
JSON.parse(await readFile(marker, "utf8")).listeners,
).toBeGreaterThan(0);
expect(
(await readdir(root)).some((name) => name.startsWith(".rea-write-")),
).toBe(false);
},
);
cliTest.for(
(process.platform === "win32"
? (["event"] as const)
: (["event", "os"] as const)
).flatMap((delivery) => [
{ signal: "SIGINT" as const, status: 130, delivery },
{ signal: "SIGTERM" as const, status: 143, delivery },
]),
)(
"retains complete default Evidence when $signal ($delivery) is queued during sealing",
async ({ signal, status, delivery }, { cli }) => {
const root = await createTestTempDirectory("rea-managed-complete-signal-");
const target = join(root, "fixture.dll"),
marker = join(root, "signal.json");
await writeFile(target, memberFixture());
const preload = resolve(
"tests/fixtures/evidenceCompletionSignalPreload.mjs",
);
const reply = await cli.run({
arguments: [
"inspect-managed-members",
target,
"--type",
"Fixture.Program",
"--json",
],
environment: {
HOME: root,
REA_LOG_LEVEL: "silent",
REA_TEST_EVIDENCE_COMPLETION: JSON.stringify({
operation: "inspect_managed_members",
target_path: target,
marker,
signal,
delivery,
}),
NODE_OPTIONS: `--max-old-space-size=2048 --import=${pathToFileURL(preload).href}`,
},
});
expect(reply.exitCode).toBe(status);
const { error: _label, ...projection } =
analysisCliErrorEnvelopeSchema.parse(reply.json);
const error = analysisErrorProjectionSchema.parse(projection);
expect(error.code).toBe("cancelled");
expect(error.details?.operation).toBe("inspect_managed_members");
const partial = parseEvidence(error.details?.partial_observation);
expect(partial.operation).toBe("inspect_managed_members");
expect(partial.subject?.local_path).toBe(target);
expect(
managedMemberInspectionSchema.parse(partial.normalized_result).methods,
).toHaveLength(2);
const proof = JSON.parse(await readFile(marker, "utf8"));
expect(proof).toMatchObject({
evidence_id: partial.evidence_id,
signal,
delivery,
});
expect(proof.listeners).toBeGreaterThan(0);
expect((await readdir(root)).sort()).toEqual([
"fixture.dll",
"signal.json",
]);
},
);