mirror of
https://github.com/morluto/rea.git
synced 2026-11-13 10:12:50 +00:00
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:
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();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user