mirror of
https://github.com/morluto/rea.git
synced 2026-11-11 17:28:08 +00:00
Return the validated descriptor and its original metadata from regular-file admission. Propagate that contract through owned reads, hashes, snapshots, artifact inventory and provider file workflows instead of replacing initial facts with another stat. Keep arbitrary output-handle operations separate. Preserve final descriptor/path identity proofs, borrowed-source verification, BigInt precision, cancellation provenance and failed-close ownership. Remove the obsolete source and owned-ZIP baseline stages and update their consumers and regressions. Tie read completion faults to actual reads instead of the retired duplicate stat; instrument real handles without fake handle wrappers. Validation: fresh runtime/catalog build; focused source, lifecycle, filesystem, CLI/MCP and authored provider-fixture tests; full static checks; docs checks; test discovery; duplication check. Platform-specific cases remain skipped on macOS. No installed provider or device behavior was verified.
163 lines
5.0 KiB
TypeScript
163 lines
5.0 KiB
TypeScript
import type { FileHandle } from "node:fs/promises";
|
|
import { writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
|
|
import { expect, it, onTestFinished, vi } from "vitest";
|
|
|
|
import { ArtifactResourceScope } from "../../../src/artifacts/ArtifactResourceScope.js";
|
|
import { MachOSliceArtifactReader } from "../../../src/artifacts/MachOSliceArtifactReader.js";
|
|
import { ZipArtifactReader } from "../../../src/artifacts/ZipArtifactReader.js";
|
|
import { ok } from "../../../src/domain/result.js";
|
|
import type { NativeCommandRunner } from "../../../src/native/CommandRunner.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.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;
|
|
},
|
|
};
|
|
});
|
|
|
|
const runner: NativeCommandRunner = {
|
|
run: async () =>
|
|
ok({
|
|
tool: "lipo",
|
|
executable: "/usr/bin/lipo",
|
|
executableSha256: "1".repeat(64),
|
|
toolVersion: null,
|
|
versionReason: "fixture",
|
|
arguments: [],
|
|
stdout: "",
|
|
stderr: "",
|
|
stdoutBytes: 0,
|
|
stderrBytes: 0,
|
|
exitCode: 0,
|
|
signal: null,
|
|
}),
|
|
};
|
|
|
|
const scenarios = [
|
|
{
|
|
reader: "ZIP",
|
|
createReader: (path: string) => new ZipArtifactReader(path, "zip"),
|
|
stage: "admission",
|
|
failOnStat: 1,
|
|
},
|
|
{
|
|
reader: "Mach-O slice",
|
|
createReader: (path: string) =>
|
|
new MachOSliceArtifactReader(path, {}, runner),
|
|
stage: "admission",
|
|
failOnStat: 1,
|
|
},
|
|
{
|
|
reader: "Mach-O slice",
|
|
createReader: (path: string) =>
|
|
new MachOSliceArtifactReader(path, {}, runner),
|
|
stage: "post-admission-verification",
|
|
failOnStat: 2,
|
|
},
|
|
] as const;
|
|
|
|
it.each(scenarios)(
|
|
"retains $reader reader $stage owner",
|
|
async ({ createReader, stage, failOnStat: initialFailOnStat }) => {
|
|
const root = await createTestTempDirectory("rea-owned-reader-cleanup-");
|
|
const path = join(root, "source");
|
|
await writeFile(path, "fixture source");
|
|
|
|
const statFailure = Object.assign(
|
|
new Error(`primary ${stage} stat failure`),
|
|
{ code: "EIO", errno: -5, syscall: "fstat" },
|
|
);
|
|
const closeFailure = new Error(
|
|
`close failed after invalidation during ${stage}`,
|
|
);
|
|
let descriptor: FileHandle | undefined;
|
|
let statCalls = 0;
|
|
let closeAttempts = 0;
|
|
let failOnStat: number = initialFailOnStat;
|
|
injected.opened = (openedPath, handle) => {
|
|
if (openedPath !== path || descriptor !== undefined) return;
|
|
descriptor = handle;
|
|
const stat = handle.stat.bind(handle);
|
|
const close = handle.close.bind(handle);
|
|
vi.spyOn(handle, "stat").mockImplementation(async (...args) => {
|
|
statCalls += 1;
|
|
if (statCalls === failOnStat) {
|
|
failOnStat = -1;
|
|
throw statFailure;
|
|
}
|
|
return stat(...args);
|
|
});
|
|
vi.spyOn(handle, "close").mockImplementation(async () => {
|
|
closeAttempts += 1;
|
|
await close();
|
|
throw closeFailure;
|
|
});
|
|
};
|
|
|
|
const reader = createReader(path);
|
|
const scope = new ArtifactResourceScope();
|
|
const owner = { kind: "reader" as const, resource: path, reader };
|
|
onTestFinished(async () => {
|
|
injected.opened = undefined;
|
|
await scope.close().catch(() => undefined);
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
const primary = await (async () => {
|
|
try {
|
|
for await (const _entry of reader.entries()) {
|
|
// Admission or its first fresh verification fails before any entry.
|
|
}
|
|
} catch (cause: unknown) {
|
|
return cause;
|
|
}
|
|
throw new Error("Expected reader initialization to fail");
|
|
})();
|
|
expect(primary).toBeInstanceOf(Error);
|
|
expect(causeChainContains(primary, statFailure)).toBe(true);
|
|
|
|
const release = await scope.release(owner);
|
|
expect(release).toMatchObject({
|
|
kind: "failed",
|
|
cause: closeFailure,
|
|
owner,
|
|
});
|
|
if (release.kind !== "failed")
|
|
throw new Error("Expected owned cleanup failure");
|
|
expect(release.cause).toBe(closeFailure);
|
|
expect(release.lease.released).toBe(false);
|
|
expect(descriptor?.fd).toBe(-1);
|
|
await expect(scope.close()).rejects.toMatchObject({
|
|
reason: "unavailable",
|
|
cleanup: { reason: closeFailure.message, resources: [path] },
|
|
cleanupLeases: [release.lease],
|
|
});
|
|
// Native close invalidated the fd before rejecting. Retrying the same owner
|
|
// must remain failed without another syscall or a false successful release.
|
|
expect(closeAttempts).toBe(1);
|
|
},
|
|
);
|
|
|
|
const causeChainContains = (cause: unknown, expected: unknown): boolean => {
|
|
let current: unknown = cause;
|
|
while (current instanceof Error) {
|
|
if (current === expected) return true;
|
|
current = current.cause;
|
|
}
|
|
return false;
|
|
};
|