fix(files): preserve physical read failures across late cancellation

Tag physical read outcomes at the owning boundary and preserve opaque caller cancellation reasons through JSON pipelines and reference imports. Share named bounded-read options across consumers and retain failed cleanup ownership for retry.

Closes #5686
This commit is contained in:
morluto committed 2026-10-21 06:14:03 +08:00
1 parent 4d7de00d4c
commit 949966712e
22 files changed
+1203 -257

No files matched your search

@@ -1,11 +1,15 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { analysisViewCaptureEvidence } from "../../../tests/fixtures/analysisView.js";
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
import { JsonInputPhysicalReadFailure } from "../JsonInputFile.js";
import { failFileReadAfterAbort } from "../../../tests/support/failFileReadAfterAbort.js";
import { holdFirstFileRead } from "../../../tests/support/holdFirstFileRead.js";
import {
estimateJsonValueBytes,
readEvidenceFileSource,
@@ -43,12 +47,23 @@ describe("evidence-file source", () => {
it("refuses a cancelled request before reading and reads within the caller's resource scope", async () => {
const controller = new AbortController();
controller.abort();
expect(
await readEvidenceFileSource("/nonexistent/capture.json", {
const reason = new JsonInputPhysicalReadFailure(
new Error("opaque caller reason with physical-failure shape"),
);
controller.abort(reason);
const cancelled = await readEvidenceFileSource(
"/nonexistent/capture.json",
{
signal: controller.signal,
}),
).toMatchObject({ ok: false, error: { _tag: "AnalysisCancelledError" } });
},
);
expect(cancelled).toMatchObject({
ok: false,
error: { _tag: "AnalysisCancelledError" },
});
if (cancelled.ok || !(cancelled.error instanceof AnalysisCancelledError))
throw new Error("Expected cancelled analysis-view read");
expect(cancelled.error.cause).toBe(reason);
const path = join(
await createTestTempDirectory("rea-evidence-file-source-"),
"capture.json",
@@ -62,3 +77,120 @@ describe("evidence-file source", () => {
expect(read.value).toEqual(JSON.parse(JSON.stringify(parent)));
});
});
describe("evidence-file read failure provenance", () => {
it("keeps a physical null read cause after the analysis-view request aborts", async () => {
const root = await createTestTempDirectory("rea-view-late-io-");
const path = join(root, "capture.json");
await writeFile(path, JSON.stringify(analysisViewCaptureEvidence()));
const controller = new AbortController();
await failFileReadAfterAbort(
path,
controller,
new Error("later abort"),
null,
);
try {
const result = await readEvidenceFileSource(path, {
signal: controller.signal,
});
expect(result).toMatchObject({
ok: false,
error: { _tag: "EvidenceFileError", reason: "io", path },
});
if (result.ok) throw new Error("Expected physical JSON read failure");
expect(result.error.cause).toBeNull();
} finally {
vi.restoreAllMocks();
}
});
it("preserves identical physical and abort causes for analysis-view reads", async () => {
const root = await createTestTempDirectory("rea-view-identical-abort-");
const path = join(root, "capture.json");
await writeFile(path, JSON.stringify(analysisViewCaptureEvidence()));
const controller = new AbortController();
const physical = Object.assign(new Error("descriptor EIO"), {
code: "EIO",
});
await failFileReadAfterAbort(path, controller, physical, physical);
try {
const result = await readEvidenceFileSource(path, {
signal: controller.signal,
});
expect(result).toMatchObject({
ok: false,
error: { _tag: "EvidenceFileError", reason: "io" },
});
if (result.ok) throw new Error("Expected physical JSON read failure");
expect(result.error.cause).toBe(physical);
} finally {
vi.restoreAllMocks();
}
});
it("preserves the caller cancellation reason during an analysis-view read", async () => {
const root = await createTestTempDirectory("rea-view-read-cancel-");
const path = join(root, "capture.json");
await writeFile(path, JSON.stringify(analysisViewCaptureEvidence()));
const reader = await holdFirstFileRead(path);
const controller = new AbortController();
const reason = new Error("stop analysis-view read");
try {
const pending = readEvidenceFileSource(path, {
signal: controller.signal,
});
await reader.firstRead;
controller.abort(reason);
reader.release();
const result = await pending;
expect(result).toMatchObject({
ok: false,
error: {
_tag: "AnalysisCancelledError",
operation: "inspect_analysis_view",
},
});
if (result.ok || !(result.error instanceof AnalysisCancelledError))
throw new Error("Expected cancelled analysis-view read");
expect(result.error.cause).toBe(reason);
} finally {
controller.abort(reason);
reader.release();
vi.restoreAllMocks();
}
});
it("does not treat a caller reason with physical-read shape as a physical read", async () => {
const root = await createTestTempDirectory("rea-view-opaque-cancel-");
const path = join(root, "capture.json");
await writeFile(path, JSON.stringify(analysisViewCaptureEvidence()));
const reader = await holdFirstFileRead(path);
const controller = new AbortController();
const reason = new JsonInputPhysicalReadFailure(
new Error("opaque caller reason with physical-failure shape"),
);
try {
const pending = readEvidenceFileSource(path, {
signal: controller.signal,
});
await reader.firstRead;
controller.abort(reason);
reader.release();
const result = await pending;
expect(result).toMatchObject({
ok: false,
error: { _tag: "AnalysisCancelledError", cause: reason },
});
if (result.ok || !(result.error instanceof AnalysisCancelledError))
throw new Error(
"Expected caller cancellation, not physical I/O failure",
);
expect(result.error.cause).toBe(reason);
} finally {
controller.abort(reason);
reader.release();
vi.restoreAllMocks();
}
});
});