mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
test(lifecycle): target owned finalization boundaries
This commit is contained in:
@@ -795,7 +795,10 @@ it
|
||||
const output = { stdout: "", stderr: "upstream warning", truncated: false };
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
category:
|
||||
scenario === "late-cancellation" ? "cancelled" : "execution_failure",
|
||||
scenario === "late-cancellation" ||
|
||||
scenario === "cancelled-cleanup-failure"
|
||||
? "cancelled"
|
||||
: "execution_failure",
|
||||
details:
|
||||
scenario === "late-cancellation"
|
||||
? { captured_output: output }
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
deliverAnalysisEvidence,
|
||||
} from "../../../src/application/EvidenceBundleCommands.js";
|
||||
import { EvidenceLedger } from "../../../src/application/investigation/EvidenceLedger.js";
|
||||
import { RegularFileCleanupFailure } from "../../../src/filesystem/OwnedRegularFile.js";
|
||||
import { ArtifactResourceScope } from "../../../src/artifacts/ArtifactResourceScope.js";
|
||||
import { OwnedFileHandle } from "../../../src/filesystem/OwnedFileHandle.js";
|
||||
import { OwnedStagedOutput } from "../../../src/filesystem/OwnedStagedOutput.js";
|
||||
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
|
||||
@@ -210,27 +210,38 @@ it("preserves the failed-close owner when import is also cancelled", async () =>
|
||||
const cancellationReason = new JsonInputPhysicalReadFailure(
|
||||
new Error("opaque cancellation reason"),
|
||||
);
|
||||
let owner: OwnedFileHandle | undefined;
|
||||
const resources = new ArtifactResourceScope();
|
||||
const close = vi
|
||||
.spyOn(OwnedFileHandle.prototype, "close")
|
||||
.mockImplementationOnce(async () => {
|
||||
.mockImplementation(async function (this: OwnedFileHandle) {
|
||||
owner = this;
|
||||
controller.abort(cancellationReason);
|
||||
throw cleanupCause;
|
||||
});
|
||||
let failure: unknown;
|
||||
try {
|
||||
try {
|
||||
await readImmutableEvidenceBundle(path, controller.signal);
|
||||
} catch (cause: unknown) {
|
||||
failure = cause;
|
||||
}
|
||||
expect(failure).toBeInstanceOf(RegularFileCleanupFailure);
|
||||
if (!(failure instanceof RegularFileCleanupFailure))
|
||||
throw new Error("Expected the retained file handle owner");
|
||||
expect(failure.cleanupCause).toBe(cleanupCause);
|
||||
const result = await readImmutableEvidenceBundle(
|
||||
path,
|
||||
controller.signal,
|
||||
resources,
|
||||
);
|
||||
if (result.ok) throw new Error("Expected failed finalization");
|
||||
expect(result.error).toMatchObject({
|
||||
_tag: "EvidenceFileError",
|
||||
cleanupIncomplete: true,
|
||||
cleanupPrimary: {
|
||||
_tag: "AnalysisCancelledError",
|
||||
cause: cancellationReason,
|
||||
},
|
||||
});
|
||||
expect(owner?.handle.fd).toBeGreaterThanOrEqual(0);
|
||||
close.mockRestore();
|
||||
await resources.close();
|
||||
expect(owner?.handle.fd).toBe(-1);
|
||||
expect(result.error.cleanupIncomplete).toBe(false);
|
||||
} finally {
|
||||
close.mockRestore();
|
||||
if (failure instanceof RegularFileCleanupFailure)
|
||||
await failure.owner.close();
|
||||
await resources.close();
|
||||
}
|
||||
expect(await readEvidenceBundle(path)).toMatchObject({ ok: true });
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import { androidResultSchemas } from "../../../src/domain/android/androidAnalysi
|
||||
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
|
||||
import { snapshotAndroidEngine } from "../../../src/android/AndroidTargetSnapshot.js";
|
||||
import { hashStableFile } from "../../../src/filesystem/StableFileHash.js";
|
||||
import * as stableHash from "../../../src/filesystem/StableFileHash.js";
|
||||
import { OwnedFileHandle } from "../../../src/filesystem/OwnedFileHandle.js";
|
||||
import { RegularFileCleanupFailure } from "../../../src/filesystem/OwnedRegularFile.js";
|
||||
import { PrivateRuntimeRoot } from "../../../src/process/PrivateRuntimeRoot.js";
|
||||
@@ -23,24 +24,56 @@ import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
const it = test.skipIf(process.platform === "win32");
|
||||
|
||||
it("retains a failed input hash descriptor until provider shutdown retries it", async () => {
|
||||
const { service, apk, provider, launches } = await setup();
|
||||
const closeCause = new Error("fixture source hash close unavailable");
|
||||
const owners: OwnedFileHandle[] = [];
|
||||
// Select the provider's source hash after resolver and engine admission complete.
|
||||
const failInputHashCloses = (path: string, failures: number) => {
|
||||
const originalHash = stableHash.hashStableFile;
|
||||
const originalClose = OwnedFileHandle.prototype.close;
|
||||
const close = vi
|
||||
.spyOn(OwnedFileHandle.prototype, "close")
|
||||
.mockImplementationOnce(async function (this: OwnedFileHandle) {
|
||||
owners.push(this);
|
||||
throw closeCause;
|
||||
})
|
||||
.mockImplementationOnce(async function (this: OwnedFileHandle) {
|
||||
owners.push(this);
|
||||
throw closeCause;
|
||||
let hashingInput = false;
|
||||
const retained = new Set<OwnedFileHandle>();
|
||||
const owners: OwnedFileHandle[] = [];
|
||||
const close = vi.fn(async (owner: OwnedFileHandle) => {
|
||||
owners.push(owner);
|
||||
if (owners.length <= failures)
|
||||
throw new Error("fixture input hash close unavailable");
|
||||
return originalClose.call(owner);
|
||||
});
|
||||
const hash = vi
|
||||
.spyOn(stableHash, "hashStableFile")
|
||||
.mockImplementation(async (selected, options) => {
|
||||
hashingInput = selected === path;
|
||||
try {
|
||||
return await originalHash(selected, options);
|
||||
} finally {
|
||||
hashingInput = false;
|
||||
}
|
||||
});
|
||||
const closing = vi
|
||||
.spyOn(OwnedFileHandle.prototype, "close")
|
||||
.mockImplementation(async function (this: OwnedFileHandle) {
|
||||
if (!hashingInput && !retained.has(this)) return originalClose.call(this);
|
||||
retained.add(this);
|
||||
return close(this);
|
||||
});
|
||||
return {
|
||||
close,
|
||||
owners,
|
||||
restore: () => {
|
||||
closing.mockRestore();
|
||||
hash.mockRestore();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
it("retains a failed input hash descriptor until provider shutdown retries it", async () => {
|
||||
const { apk, provider, launches } = await setup();
|
||||
const target = await parseBinaryTarget(apk);
|
||||
if (!target.ok) throw target.error;
|
||||
const fault = failInputHashCloses(target.value.path, 2);
|
||||
const { close, owners } = fault;
|
||||
try {
|
||||
const result = await service.execute("inspect_android_package", {
|
||||
path: apk,
|
||||
const result = await provider.execute(target.value, {
|
||||
operation: "inspect_android_package",
|
||||
input: { path: apk },
|
||||
});
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
@@ -57,25 +90,22 @@ it("retains a failed input hash descriptor until provider shutdown retries it",
|
||||
expect(owners[0]!.handle.fd).toBe(-1);
|
||||
expect(launches).toHaveLength(0);
|
||||
} finally {
|
||||
close.mockRestore();
|
||||
fault.restore();
|
||||
await provider.close();
|
||||
await verifyCleanup(launches);
|
||||
}
|
||||
});
|
||||
|
||||
it("clears hash cleanup uncertainty when the provider immediately retries the same handle", async () => {
|
||||
const { service, apk, provider, launches } = await setup();
|
||||
const closeCause = new Error("fixture source hash close unavailable");
|
||||
const originalClose = OwnedFileHandle.prototype.close;
|
||||
const close = vi
|
||||
.spyOn(OwnedFileHandle.prototype, "close")
|
||||
.mockRejectedValueOnce(closeCause)
|
||||
.mockImplementation(async function (this: OwnedFileHandle) {
|
||||
return originalClose.call(this);
|
||||
});
|
||||
const { apk, provider, launches } = await setup();
|
||||
const target = await parseBinaryTarget(apk);
|
||||
if (!target.ok) throw target.error;
|
||||
const fault = failInputHashCloses(target.value.path, 1);
|
||||
const { close } = fault;
|
||||
try {
|
||||
const result = await service.execute("inspect_android_package", {
|
||||
path: apk,
|
||||
const result = await provider.execute(target.value, {
|
||||
operation: "inspect_android_package",
|
||||
input: { path: apk },
|
||||
});
|
||||
expect(result).toMatchObject({ ok: false });
|
||||
if (result.ok) throw new Error("Expected the hash close failure");
|
||||
@@ -84,7 +114,7 @@ it("clears hash cleanup uncertainty when the provider immediately retries the sa
|
||||
expect(result.error.cause).toBeInstanceOf(AggregateError);
|
||||
expect(launches).toHaveLength(0);
|
||||
} finally {
|
||||
close.mockRestore();
|
||||
fault.restore();
|
||||
await provider.close();
|
||||
await verifyCleanup(launches);
|
||||
}
|
||||
@@ -100,22 +130,16 @@ it("drains hash descriptors even when session shutdown fails and retains its wor
|
||||
const workspace = launches[0]?.cwd;
|
||||
if (workspace === undefined)
|
||||
throw new Error("Expected an acquired workspace");
|
||||
const originalClose = OwnedFileHandle.prototype.close;
|
||||
let fileCloseCalls = 0;
|
||||
const fileClose = vi
|
||||
.spyOn(OwnedFileHandle.prototype, "close")
|
||||
.mockImplementation(async function (this: OwnedFileHandle) {
|
||||
fileCloseCalls += 1;
|
||||
if (fileCloseCalls === 1)
|
||||
throw new Error("fixture input hash close failure");
|
||||
return originalClose.call(this);
|
||||
});
|
||||
const target = await parseBinaryTarget(apk);
|
||||
if (!target.ok) throw target.error;
|
||||
const fault = failInputHashCloses(target.value.path, 1);
|
||||
const sessionClose = vi
|
||||
.spyOn(JadxSession.prototype, "close")
|
||||
.mockRejectedValueOnce(new Error("fixture session cleanup failure"));
|
||||
try {
|
||||
const outcome = await service.execute("inspect_android_package", {
|
||||
path: apk,
|
||||
const outcome = await provider.execute(target.value, {
|
||||
operation: "inspect_android_package",
|
||||
input: { path: apk },
|
||||
});
|
||||
expect(outcome).toMatchObject({
|
||||
ok: false,
|
||||
@@ -125,17 +149,17 @@ it("drains hash descriptors even when session shutdown fails and retains its wor
|
||||
},
|
||||
});
|
||||
if (outcome.ok) throw new Error("Expected retained session cleanup");
|
||||
expect(fileCloseCalls).toBe(2);
|
||||
expect(fault.close).toHaveBeenCalledTimes(2);
|
||||
await expect(access(workspace)).resolves.toBeUndefined();
|
||||
|
||||
sessionClose.mockRestore();
|
||||
fileClose.mockRestore();
|
||||
fault.restore();
|
||||
await provider.close();
|
||||
expect(outcome.error.cleanupIncomplete).toBe(false);
|
||||
await verifyCleanup(launches);
|
||||
} finally {
|
||||
sessionClose.mockRestore();
|
||||
fileClose.mockRestore();
|
||||
fault.restore();
|
||||
await provider.close();
|
||||
await verifyCleanup(launches);
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ it.skipIf(process.platform === "win32").each(["reader", "import"] as const)(
|
||||
);
|
||||
|
||||
it.skipIf(process.platform === "win32").each(["contents", "root"] as const)(
|
||||
"retains a changed %s as a graph limitation after its directory handle closes",
|
||||
"retains observed entries when %s changes after its directory handle closes",
|
||||
async (changed) => {
|
||||
const parent = await createTestTempDirectory("rea-reference-root-race-");
|
||||
const root = join(parent, "observed");
|
||||
@@ -192,14 +192,28 @@ it.skipIf(process.platform === "win32").each(["contents", "root"] as const)(
|
||||
"already observed source",
|
||||
);
|
||||
await expect(readFile(source)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
if (changed === "root") {
|
||||
expect(imported).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
code: "io",
|
||||
message: expect.stringContaining(
|
||||
"Directory identity could not be verified: ENOENT",
|
||||
),
|
||||
partial: {
|
||||
entries: [{ status: "read", kind: "file", path: "source.txt" }],
|
||||
bytesRead: Buffer.byteLength("already observed source"),
|
||||
},
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
expect(imported.ok, JSON.stringify(imported)).toBe(true);
|
||||
if (!imported.ok) return;
|
||||
expect(imported.value.inventory_state).toBe("partial");
|
||||
expect(imported.value.limitations).toContainEqual(
|
||||
expect.stringContaining(
|
||||
changed === "contents"
|
||||
? "Reference root enumeration is incomplete: Reference root changed"
|
||||
: "Reference root enumeration is incomplete: Directory identity could not be verified: ENOENT",
|
||||
"Reference root enumeration is incomplete: Reference root changed",
|
||||
),
|
||||
);
|
||||
expect(imported.value.entries).toEqual([
|
||||
|
||||
Reference in New Issue
Block a user