mirror of
https://github.com/morluto/rea.git
synced 2026-11-11 01:13:39 +00:00
* fix(cli): publish partial Evidence to --evidence_output on failure publishCliAnalysisEvidence returned a failed result unchanged before any --evidence_output handling. When a source reached its deadline, or another failure carried partial Evidence, the requested file was not written and --detail summary still printed the complete partial Evidence inline, which reached more than 64 MB on a large build. A failure whose details.partial_observation is authenticated Evidence now publishes it through the same projection and new-file publication as a successful result. The failure keeps its code, message and remediation. With --detail summary, details.partial_observation holds the summary view, and details.diagnostics.evidence_bundle reports the path and bytes. A refused publication keeps the complete partial Evidence inline and adds details.diagnostics.evidence_bundle_error with the refusal and a limitation. Without --evidence_output, for cancellation, and for raw provider observations the failure output is unchanged. Fixes #7787 * fix(cli): keep committed partial Evidence receipts on refused publication When publishing the partial Evidence of a failed analysis, a refusal can follow a commit. Staging cleanup that stays incomplete after its retry returns cleanup_incomplete with published_path and bytes, and a cancellation after commit returns cancelled with diagnostics.evidence_bundle. The failure path read only the second form, so a committed file with incomplete cleanup was reported as not published, without its receipt or the cleanup resources, and the full partial Evidence went back inline. Keep the original analysis failure and record the receipt from either form in details.diagnostics.evidence_bundle. Incomplete publication cleanup adds published, published_path, bytes, cleanup and resources to details, the shape a successful run's cleanup_incomplete reports; resources join any the analysis failure already reports, and its own cleanup reason is kept. Once the file is committed, the delivered view (the summary with --detail summary) replaces the inline partial Evidence. Receipts omit a path that holds an excluded sensitive value, as on success. Tests drive the real staged publication through its commit point: cleanup that stays incomplete, cleanup confirmed by retry, and SIGINT after commit. A composition test checks receipt redaction on the failure path. * fix(cli): report incomplete partial publication cleanup --------- Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
242 lines
8.7 KiB
TypeScript
242 lines
8.7 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { writeFile, readFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { expect, it } from "vitest";
|
|
import { WebNetworkCaptureService } from "../../src/application/WebNetworkCaptureService.js";
|
|
import { decodeHarCapture } from "../../src/browser/history/HarCapture.js";
|
|
import {
|
|
deliverCliAnalysisEvidence,
|
|
publishCliAnalysisEvidence,
|
|
} from "../../src/cli/analysisEvidenceDelivery.js";
|
|
import { createEvidence } from "../../src/domain/evidence.js";
|
|
import { parseEvidenceBundle } from "../../src/domain/evidenceBundle.js";
|
|
import { projectAnalysisError } from "../../src/domain/analysisErrorProjection.js";
|
|
import { analysisErrorProjectionSchema } from "../../src/contracts/errorSchemas.js";
|
|
import { AnalysisTimeoutError } from "../../src/domain/analysisErrorCore.js";
|
|
import { err, ok } from "../../src/domain/result.js";
|
|
import { historicalHar } from "../fixtures/historicalHar.js";
|
|
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
|
|
|
it("excludes marked output identity from late CLI publication failure while retaining capture Evidence", async () => {
|
|
const root = await createTestTempDirectory("rea-capture-delivery-");
|
|
const outputPath = join(root, "private-publication-marker.json");
|
|
await writeFile(outputPath, "existing caller file");
|
|
const bytes = JSON.stringify(historicalHar());
|
|
const values = ["private-publication-marker"];
|
|
const input = {
|
|
capture_path: join(root, "capture.har"),
|
|
format: "har" as const,
|
|
sensitive_values: values,
|
|
};
|
|
const service = new WebNetworkCaptureService({
|
|
inspect: async () =>
|
|
ok({
|
|
...decodeHarCapture(bytes, values),
|
|
artifact: {
|
|
path: input.capture_path,
|
|
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
bytes: Buffer.byteLength(bytes),
|
|
},
|
|
format: "har" as const,
|
|
runtime_attribution: "unknown" as const,
|
|
limitations: [],
|
|
}),
|
|
});
|
|
const output = analysisErrorProjectionSchema.parse(
|
|
await deliverCliAnalysisEvidence({
|
|
run: () => service.inspect(input),
|
|
service,
|
|
providerId: "web-network-capture",
|
|
operation: "inspect_web_network_capture",
|
|
signal: new AbortController().signal,
|
|
detail: "complete",
|
|
evidenceOutput: outputPath,
|
|
sensitiveValues: values,
|
|
}),
|
|
);
|
|
expect(output.code).toBe("invalid_request");
|
|
expect(output.details).toHaveProperty(
|
|
"partial_observation.normalized_result.artifact.path",
|
|
input.capture_path,
|
|
);
|
|
expect(JSON.stringify(output)).not.toContain(values[0]);
|
|
expect(await readFile(outputPath, "utf8")).toBe("existing caller file");
|
|
});
|
|
|
|
it.each([true, false])(
|
|
"publishes summary bytes while excluding only a marked receipt identity: %s",
|
|
async (marked) => {
|
|
const root = await createTestTempDirectory("rea-capture-publication-");
|
|
const outputPath = join(root, "private-publication-marker.json");
|
|
const values = marked ? ["private-publication-marker"] : [];
|
|
const bytes = JSON.stringify(historicalHar());
|
|
const input = {
|
|
capture_path: join(root, "capture.har"),
|
|
format: "har" as const,
|
|
sensitive_values: values,
|
|
};
|
|
const service = new WebNetworkCaptureService({
|
|
inspect: async () =>
|
|
ok({
|
|
...decodeHarCapture(bytes, values),
|
|
artifact: {
|
|
path: input.capture_path,
|
|
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
bytes: Buffer.byteLength(bytes),
|
|
},
|
|
format: "har" as const,
|
|
runtime_attribution: "unknown" as const,
|
|
limitations: [],
|
|
}),
|
|
});
|
|
const output = await deliverCliAnalysisEvidence({
|
|
run: () => service.inspect(input),
|
|
service,
|
|
providerId: "web-network-capture",
|
|
operation: "inspect_web_network_capture",
|
|
signal: new AbortController().signal,
|
|
detail: "summary",
|
|
evidenceOutput: outputPath,
|
|
sensitiveValues: values,
|
|
});
|
|
const persisted = await readFile(outputPath);
|
|
expect(output).toMatchObject({
|
|
evidence_bundle: { bytes: persisted.length },
|
|
});
|
|
if (marked) {
|
|
expect(output).not.toHaveProperty("evidence_bundle.path");
|
|
expect(JSON.stringify(output)).not.toContain(values[0]);
|
|
} else expect(output).toHaveProperty("evidence_bundle.path", outputPath);
|
|
expect(JSON.parse(persisted.toString())).toHaveProperty(
|
|
"records.0.operation",
|
|
"inspect_web_network_capture",
|
|
);
|
|
},
|
|
);
|
|
|
|
it.each([true, false])(
|
|
"publishes partial Evidence from a failure while excluding only a marked receipt identity: %s",
|
|
async (marked) => {
|
|
const root = await createTestTempDirectory("rea-capture-partial-");
|
|
const outputPath = join(root, "private-publication-marker.json");
|
|
const values = marked ? ["private-publication-marker"] : [];
|
|
const bytes = JSON.stringify(historicalHar());
|
|
const input = {
|
|
capture_path: join(root, "capture.har"),
|
|
format: "har" as const,
|
|
sensitive_values: values,
|
|
};
|
|
const service = new WebNetworkCaptureService({
|
|
inspect: async () =>
|
|
ok({
|
|
...decodeHarCapture(bytes, values),
|
|
artifact: {
|
|
path: input.capture_path,
|
|
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
bytes: Buffer.byteLength(bytes),
|
|
},
|
|
format: "har" as const,
|
|
runtime_attribution: "unknown" as const,
|
|
limitations: [],
|
|
}),
|
|
});
|
|
const completed = await service.inspect(input);
|
|
if (!completed.ok) throw completed.error;
|
|
const output = analysisErrorProjectionSchema.parse(
|
|
await deliverCliAnalysisEvidence({
|
|
run: async () =>
|
|
err(
|
|
new AnalysisTimeoutError("inspect_web_network_capture", 1, {
|
|
partialObservation: completed.value,
|
|
}),
|
|
),
|
|
service,
|
|
providerId: "web-network-capture",
|
|
operation: "inspect_web_network_capture",
|
|
signal: new AbortController().signal,
|
|
detail: "summary",
|
|
evidenceOutput: outputPath,
|
|
sensitiveValues: values,
|
|
}),
|
|
);
|
|
const persisted = await readFile(outputPath);
|
|
expect(output.code).toBe("provider_timeout");
|
|
expect(output.details).toHaveProperty(
|
|
"diagnostics.evidence_bundle.bytes",
|
|
persisted.length,
|
|
);
|
|
if (marked) {
|
|
expect(output.details).not.toHaveProperty(
|
|
"diagnostics.evidence_bundle.path",
|
|
);
|
|
expect(JSON.stringify(output)).not.toContain(values[0]);
|
|
} else
|
|
expect(output.details).toHaveProperty(
|
|
"diagnostics.evidence_bundle.path",
|
|
outputPath,
|
|
);
|
|
expect(JSON.parse(persisted.toString())).toHaveProperty(
|
|
"records.0.evidence_id",
|
|
completed.value.evidence_id,
|
|
);
|
|
},
|
|
);
|
|
|
|
it.each([true, false])(
|
|
"preserves separately captured sources during partial publication (refused: %s)",
|
|
async (refused) => {
|
|
const root = await createTestTempDirectory("rea-partial-captured-sources-");
|
|
const outputPath = join(root, "partial.json");
|
|
if (refused) await writeFile(outputPath, "existing caller file");
|
|
const provider = { id: "fixture", name: "Fixture", version: "1" };
|
|
const source = createEvidence(undefined, provider, {
|
|
operation: "source_fixture",
|
|
parameters: {},
|
|
result: { observed: 7 },
|
|
});
|
|
const partial = createEvidence(undefined, provider, {
|
|
operation: "derived_fixture",
|
|
parameters: {},
|
|
result: { completed_sources: 1 },
|
|
evidenceLinks: [source.evidence_id],
|
|
});
|
|
const failure = projectAnalysisError(
|
|
new AnalysisTimeoutError("derived_fixture", 1, {
|
|
partialObservation: partial,
|
|
}),
|
|
);
|
|
const output = analysisErrorProjectionSchema.parse(
|
|
await publishCliAnalysisEvidence(
|
|
{ ok: false, error: failure, sourceEvidence: [source] },
|
|
{
|
|
providerId: "fixture",
|
|
operation: "derived_fixture",
|
|
signal: new AbortController().signal,
|
|
detail: "complete",
|
|
evidenceOutput: outputPath,
|
|
},
|
|
),
|
|
);
|
|
expect(output.code).toBe("provider_timeout");
|
|
if (refused) {
|
|
expect(await readFile(outputPath, "utf8")).toBe("existing caller file");
|
|
expect(output.details).toMatchObject({
|
|
source_closure: {
|
|
publication: "not_published",
|
|
complete: true,
|
|
records: [source],
|
|
omitted_count: 0,
|
|
},
|
|
});
|
|
} else {
|
|
const bundle = parseEvidenceBundle(
|
|
JSON.parse(await readFile(outputPath, "utf8")),
|
|
);
|
|
expect(
|
|
new Set(bundle.records.map((record) => record.evidence_id)),
|
|
).toEqual(new Set([source.evidence_id, partial.evidence_id]));
|
|
expect(output.details).not.toHaveProperty("source_closure");
|
|
}
|
|
},
|
|
);
|