mirror of
https://github.com/morluto/rea
synced 2026-10-28 06:46:47 -04:00
bab49aaf7c
Centralize completed file observations and cleanup error projection across stable-file providers while preserving each provider’s failure classification and exact cleanup leases. Cover BEAM finalization cancellation and devicetree cleanup retries. Validated 223 focused lifecycle and MCP tests and check:fast.
208 lines
7.5 KiB
TypeScript
208 lines
7.5 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { fstatSync } from "node:fs";
|
|
import type { FileHandle } from "node:fs/promises";
|
|
import * as fileSystem from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { afterEach, expect, it, vi } from "vitest";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { ProviderCleanupError } from "../../../src/domain/providerCleanupError.js";
|
|
import { pythonPycFixture } from "../../../src/python/PythonPyc.fixture.js";
|
|
import { PythonPycProvider } from "../../../src/python/PythonPycProvider.js";
|
|
import { PythonPycService } from "../../../src/application/python/PythonPycService.js";
|
|
import { BeamService } from "../../../src/application/erlang/BeamService.js";
|
|
import { BeamProvider } from "../../../src/erlang/BeamProvider.js";
|
|
import { erlangBeamFixture } from "../../../src/erlang/Beam.fixture.js";
|
|
import { DevicetreeService } from "../../../src/application/binaryDiagnostics/DevicetreeService.js";
|
|
import { DevicetreeProvider } from "../../../src/firmware/devicetree/DevicetreeProvider.js";
|
|
import { devicetreeFixture } from "../../../src/firmware/devicetree/Devicetree.fixture.js";
|
|
|
|
const injected = vi.hoisted(() => ({
|
|
opened: undefined as
|
|
| ((path: unknown, handle: FileHandle) => void)
|
|
| undefined,
|
|
}));
|
|
|
|
vi.mock("node:fs/promises", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("node:fs/promises")>();
|
|
return {
|
|
...actual,
|
|
open: async (...args: Parameters<typeof actual.open>) => {
|
|
const handle = await actual.open(...args);
|
|
injected.opened?.(args[0], handle);
|
|
return handle;
|
|
},
|
|
};
|
|
});
|
|
|
|
afterEach(() => {
|
|
injected.opened = undefined;
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
const providers = [
|
|
{
|
|
name: "Python pyc",
|
|
operation: "inspect_python_pyc",
|
|
bytes: pythonPycFixture(),
|
|
create: () => new PythonPycService(new PythonPycProvider()),
|
|
},
|
|
{
|
|
name: "Erlang BEAM",
|
|
operation: "inspect_erlang_beam",
|
|
bytes: erlangBeamFixture(),
|
|
create: () => new BeamService(new BeamProvider()),
|
|
},
|
|
{
|
|
name: "devicetree",
|
|
operation: "inspect_devicetree",
|
|
bytes: devicetreeFixture(),
|
|
create: () => new DevicetreeService(new DevicetreeProvider()),
|
|
},
|
|
];
|
|
|
|
const outcomes = [
|
|
{ stage: "completed read", cancelDuringClose: false, failRead: false },
|
|
{
|
|
stage: "cancellation during close",
|
|
cancelDuringClose: true,
|
|
failRead: false,
|
|
},
|
|
{
|
|
stage: "read failure then cancellation",
|
|
cancelDuringClose: true,
|
|
failRead: true,
|
|
},
|
|
];
|
|
|
|
it.each(
|
|
providers.flatMap((provider) =>
|
|
outcomes.map((outcome) => ({ ...provider, ...outcome })),
|
|
),
|
|
)(
|
|
"$name preserves acquired facts and resolves cleanup after retries: $stage",
|
|
async ({ operation, bytes, create, cancelDuringClose, failRead }) => {
|
|
const root = await createTestTempDirectory("rea-python-beam-close-owner-");
|
|
const path = join(root, "selected-artifact");
|
|
await fileSystem.writeFile(path, bytes);
|
|
const releases: (() => Promise<void>)[] = [];
|
|
const descriptors: number[] = [];
|
|
const controller = new AbortController();
|
|
const reason = { source: "cancelled during descriptor finalization" };
|
|
const readFailure = new Error("injected physical artifact read failure");
|
|
let denyClose = true;
|
|
injected.opened = (openedPath, file) => {
|
|
if (String(openedPath) !== path) return;
|
|
descriptors.push(file.fd);
|
|
if (failRead) vi.spyOn(file, "read").mockRejectedValueOnce(readFailure);
|
|
const close = file.close.bind(file);
|
|
releases.push(close);
|
|
vi.spyOn(file, "close").mockImplementation(async () => {
|
|
if (cancelDuringClose) controller.abort(reason);
|
|
if (denyClose)
|
|
throw new Error("injected artifact descriptor close failure");
|
|
await close();
|
|
});
|
|
};
|
|
const service = create();
|
|
try {
|
|
const result = await service.inspect(
|
|
{ path },
|
|
{ signal: controller.signal },
|
|
);
|
|
if (result.ok) throw new Error("Expected incomplete descriptor cleanup");
|
|
expect(result.error.cleanupIncomplete).toBe(true);
|
|
expect(result.error.cleanupResources).toEqual([path]);
|
|
const expectedSnapshot = failRead
|
|
? undefined
|
|
: {
|
|
kind: "file-snapshot-hash",
|
|
path,
|
|
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
bytes: bytes.length,
|
|
};
|
|
expect(result.error.partialObservation).toEqual(expectedSnapshot);
|
|
if (failRead)
|
|
expect(result.error.cleanupPrimary?.cause).toBe(readFailure);
|
|
else if (cancelDuringClose) {
|
|
expect(result.error.cleanupPrimary?._tag).toBe(
|
|
"AnalysisCancelledError",
|
|
);
|
|
expect(result.error.cleanupPrimary?.cause).toBe(reason);
|
|
}
|
|
expect(fstatSync(descriptors[0]!)).toBeDefined();
|
|
const closeErrors: ProviderCleanupError[] = [];
|
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
const failure: unknown = await service.close().then(
|
|
() => undefined,
|
|
(cause: unknown) => cause,
|
|
);
|
|
expect(failure).toBeInstanceOf(ProviderCleanupError);
|
|
if (!(failure instanceof ProviderCleanupError))
|
|
throw new Error("Expected a translated provider cleanup failure");
|
|
expect(failure.operation).toBe(operation);
|
|
expect(failure.cleanupIncomplete).toBe(true);
|
|
expect(failure.cleanupResources).toEqual([path]);
|
|
closeErrors.push(failure);
|
|
}
|
|
expect(descriptors).toHaveLength(1);
|
|
expect(result.error.cleanupIncomplete).toBe(true);
|
|
denyClose = false;
|
|
await service.close();
|
|
expect(() => fstatSync(descriptors[0]!)).toThrow();
|
|
for (const failure of [result.error, ...closeErrors]) {
|
|
expect(failure.cleanupIncomplete).toBe(false);
|
|
expect(failure.cleanupResources).toEqual([]);
|
|
}
|
|
expect(result.error.partialObservation).toEqual(expectedSnapshot);
|
|
} finally {
|
|
denyClose = false;
|
|
await service.close();
|
|
for (const close of releases) await close();
|
|
}
|
|
},
|
|
);
|
|
|
|
it.each([{ source: "BEAM successful close" }, null])(
|
|
"preserves BEAM cancellation during successful close with reason %j",
|
|
async (reason) => {
|
|
const root = await createTestTempDirectory("rea-beam-successful-close-");
|
|
const path = join(root, "selected.beam");
|
|
const bytes = erlangBeamFixture();
|
|
await fileSystem.writeFile(path, bytes);
|
|
const controller = new AbortController();
|
|
const handles: FileHandle[] = [];
|
|
injected.opened = (openedPath, handle) => {
|
|
if (String(openedPath) !== path) return;
|
|
handles.push(handle);
|
|
const close = handle.close.bind(handle);
|
|
vi.spyOn(handle, "close").mockImplementation(async () => {
|
|
controller.abort(reason);
|
|
await close();
|
|
});
|
|
};
|
|
const service = new BeamService(new BeamProvider());
|
|
try {
|
|
const result = await service.inspect(
|
|
{ path },
|
|
{ signal: controller.signal },
|
|
);
|
|
if (result.ok) throw new Error("Expected finalization cancellation");
|
|
expect(result.error._tag).toBe("AnalysisCancelledError");
|
|
expect(result.error.cause).toBe(reason);
|
|
expect(result.error.cleanupIncomplete).toBe(false);
|
|
expect(result.error.partialObservation).toEqual({
|
|
kind: "file-snapshot-hash",
|
|
path,
|
|
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
bytes: bytes.length,
|
|
});
|
|
expect(handles).toHaveLength(1);
|
|
expect(handles[0]?.fd).toBe(-1);
|
|
await service.close();
|
|
expect(handles).toHaveLength(1);
|
|
} finally {
|
|
await service.close();
|
|
}
|
|
},
|
|
);
|