mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
154 lines
5.8 KiB
TypeScript
154 lines
5.8 KiB
TypeScript
import { open, writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
|
|
import { expect, it, vi } from "vitest";
|
|
|
|
import { ArtifactResourceScope } from "../../../src/artifacts/ArtifactResourceScope.js";
|
|
import { readJsonFile } from "../../../src/application/JsonFiles.js";
|
|
import { RegularFileCleanupFailure } from "../../../src/filesystem/OwnedRegularFile.js";
|
|
import { OwnedFileHandle } from "../../../src/filesystem/OwnedFileHandle.js";
|
|
import { EvidenceFileError } from "../../../src/domain/evidenceErrors.js";
|
|
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
|
|
it("keeps a cleanup-shaped caller cancellation opaque before JSON admission", async () => {
|
|
const root = await createTestTempDirectory("rea-json-opaque-cancel-");
|
|
const path = join(root, "input.json");
|
|
await writeFile(path, "{}");
|
|
const owner = new OwnedFileHandle(await open(path, "r"));
|
|
const reason = new RegularFileCleanupFailure(
|
|
path,
|
|
owner,
|
|
{ kind: "completed", value: null },
|
|
new Error("foreign owner"),
|
|
);
|
|
const controller = new AbortController();
|
|
controller.abort(reason);
|
|
const close = vi.spyOn(owner, "close");
|
|
try {
|
|
const result = await readJsonFile(path, controller.signal);
|
|
if (result.ok) throw new Error("Expected cancellation");
|
|
expect(result.error).toMatchObject({
|
|
_tag: "AnalysisCancelledError",
|
|
cleanupIncomplete: false,
|
|
});
|
|
expect(result.error.cause).toBe(reason);
|
|
expect(close).not.toHaveBeenCalled();
|
|
expect(owner.handle.fd).toBeGreaterThanOrEqual(0);
|
|
} finally {
|
|
close.mockRestore();
|
|
await owner.close();
|
|
}
|
|
});
|
|
|
|
it("retains and retries a JSON file owner after a persistent close failure", async () => {
|
|
const root = await createTestTempDirectory("rea-json-file-owner-");
|
|
const path = join(root, "input.json");
|
|
await writeFile(path, '{"value":"retained"}');
|
|
const resources = new ArtifactResourceScope();
|
|
const closeFailure = new Error("injected persistent close failure");
|
|
let attempts = 0;
|
|
const close = vi
|
|
.spyOn(OwnedFileHandle.prototype, "close")
|
|
.mockImplementation(async () => {
|
|
attempts += 1;
|
|
throw closeFailure;
|
|
});
|
|
try {
|
|
const result = await readJsonFile(path, undefined, resources);
|
|
expect(result.ok).toBe(false);
|
|
if (result.ok) throw new Error("Expected a typed JSON cleanup failure");
|
|
expect(result.error).toBeInstanceOf(EvidenceFileError);
|
|
expect(result.error.cleanupIncomplete).toBe(true);
|
|
const failure = result.error.cause;
|
|
expect(failure).toBeInstanceOf(RegularFileCleanupFailure);
|
|
if (!(failure instanceof RegularFileCleanupFailure))
|
|
throw new Error("Expected the failed file owner to remain visible");
|
|
expect(failure.cleanupCause).toBe(closeFailure);
|
|
expect(failure.outcome).toMatchObject({
|
|
kind: "completed",
|
|
value: { ok: true, value: { value: "retained" } },
|
|
});
|
|
expect(failure.owner.handle.fd).toBeGreaterThanOrEqual(0);
|
|
expect(attempts).toBe(2);
|
|
const blocked = await readJsonFile(path, undefined, resources);
|
|
if (blocked.ok)
|
|
throw new Error("Expected retained cleanup to block admission");
|
|
expect(attempts).toBe(3);
|
|
expect(blocked.error.cleanupIncomplete).toBe(true);
|
|
close.mockRestore();
|
|
await resources.close();
|
|
expect(failure.owner.handle.fd).toBe(-1);
|
|
expect(result.error.cleanupIncomplete).toBe(false);
|
|
expect(blocked.error.cleanupIncomplete).toBe(false);
|
|
} finally {
|
|
close.mockRestore();
|
|
await resources.close();
|
|
}
|
|
});
|
|
|
|
it("retains malformed JSON over cancellation during failed final close", async () => {
|
|
const root = await createTestTempDirectory("rea-json-invalid-close-");
|
|
const path = join(root, "input.json");
|
|
await writeFile(path, '{"unterminated":');
|
|
const resources = new ArtifactResourceScope();
|
|
const controller = new AbortController();
|
|
const reason = { caller: "stop after parsing" };
|
|
const close = vi
|
|
.spyOn(OwnedFileHandle.prototype, "close")
|
|
.mockImplementation(async () => {
|
|
controller.abort(reason);
|
|
throw new Error("controlled close failure");
|
|
});
|
|
try {
|
|
const result = await readJsonFile(path, controller.signal, resources);
|
|
if (result.ok) throw new Error("Expected invalid JSON and cleanup failure");
|
|
expect(projectAnalysisError(result.error)).toMatchObject({
|
|
code: "cleanup_incomplete",
|
|
details: {
|
|
diagnostics: {
|
|
primary_error: {
|
|
code: "evidence_integrity_mismatch",
|
|
details: { reason: "invalid-json" },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
close.mockRestore();
|
|
await resources.close();
|
|
expect(result.error.cleanupIncomplete).toBe(false);
|
|
expect(result.error.cleanupPrimary).toMatchObject({
|
|
reason: "invalid-json",
|
|
});
|
|
} finally {
|
|
close.mockRestore();
|
|
await resources.close();
|
|
}
|
|
});
|
|
|
|
it("returns parsed JSON when a failed file close is recovered by the resource scope", async () => {
|
|
const root = await createTestTempDirectory("rea-json-file-close-retry-");
|
|
const path = join(root, "input.json");
|
|
await writeFile(path, '{"value":"retained"}');
|
|
const resources = new ArtifactResourceScope();
|
|
const closeFailure = new Error("injected transient close failure");
|
|
const originalClose = OwnedFileHandle.prototype.close;
|
|
let attempts = 0;
|
|
const close = vi
|
|
.spyOn(OwnedFileHandle.prototype, "close")
|
|
.mockImplementation(async function (this: OwnedFileHandle) {
|
|
attempts += 1;
|
|
if (attempts === 1) throw closeFailure;
|
|
await originalClose.call(this);
|
|
});
|
|
try {
|
|
const parsed = await readJsonFile(path, undefined, resources);
|
|
if (!parsed.ok) throw parsed.error;
|
|
expect(parsed.value).toEqual({ value: "retained" });
|
|
expect(attempts).toBe(2);
|
|
} finally {
|
|
close.mockRestore();
|
|
await resources.close();
|
|
}
|
|
});
|