test(filesystem): exercise digest cancellation and admission ownership

Cover ADB pull, package and bugreport physical failures and completed cancellation across successful and failed closes. Verify artifact retention callback timing and Pcap failed-admission owner recovery.

Refs #4598. Controlled real host files and simulated ADB output; native close faults and Android device behavior remain unverified.
This commit is contained in:
morluto committed 2026-10-17 05:47:35 +08:00
1 parent e65c3955f9
commit cd9252bc63
3 files changed
+224 -5

No files matched your search

@@ -13,6 +13,7 @@ import { readStableArtifact } from "../../../src/artifacts/readStableArtifact.js
import {
ArtifactFileCleanupFailure,
ArtifactFileCancelled,
withArtifactFileHandle,
} from "../../../src/artifacts/ArtifactFileOperation.js";
import { ArtifactResourceScope } from "../../../src/artifacts/ArtifactResourceScope.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
@@ -254,3 +255,41 @@ it("transfers the exact owner even when the caller's retention boundary rejects
await failure.cleanupLease.release();
expect(file.handle.fd).toBe(-1);
});
it("does not turn a retention callback abort into completed-file cancellation", async () => {
const file = await heldDescriptor();
const controller = new AbortController();
class AbortingScope extends ArtifactResourceScope {
override retain(
owner: Parameters<ArtifactResourceScope["retain"]>[0],
): void {
controller.abort(null);
super.retain(owner);
}
}
const scope = new AbortingScope();
try {
const failure: unknown = await withArtifactFileHandle(
file.path,
file.handle,
scope,
controller.signal,
async () => file.bytes,
).catch((cause: unknown) => cause);
expect(failure).toBeInstanceOf(ArtifactFileCleanupFailure);
if (!(failure instanceof ArtifactFileCleanupFailure))
throw new Error("Missing file cleanup owner");
expect(failure.fileCleanupFailure.outcome).toEqual({
kind: "completed",
value: file.bytes,
});
expect(failure.fileCleanupFailure.completionCancellation).toBeUndefined();
expect(failure.reason).not.toBe("cancelled");
file.release();
await failure.cleanupLease.release();
expect(file.handle.fd).toBe(-1);
} finally {
file.release();
await scope.close();
}
});
@@ -7,12 +7,15 @@
* refusal paths, and cancellation before spawn.
*/
import { createHash } from "node:crypto";
import { writeFile } from "node:fs/promises";
import { open, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it, onTestFinished } from "vitest";
import { expect, it, onTestFinished, vi } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { PcapProvider } from "../../../src/pcap/PcapProvider.js";
import { PcapSnapshot } from "../../../src/pcap/PcapSnapshot.js";
import * as regularFile from "../../../src/filesystem/RegularFile.js";
import { OwnedFileHandle } from "../../../src/filesystem/OwnedFileHandle.js";
import type { PcapCommandLauncher } from "../../../src/pcap/PcapCommand.js";
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
import type { PcapRequest } from "../../../src/domain/pcap/pcapAnalysis.js";
@@ -200,3 +203,37 @@ it.runIf(!hasNativeBound)(
expect(projected.remediation.action).toBe(projected.message);
},
);
it("retains a verification admission owner before translating cancellation", async () => {
const root = await createTestTempDirectory("rea-pcap-verify-admission-");
const path = join(root, "held.bin");
await writeFile(path, "held capture bytes");
const handle = await open(path, "r");
const owner = new OwnedFileHandle(handle);
const controller = new AbortController();
const reason = null;
const snapshot = await PcapSnapshot.create();
const admission = vi
.spyOn(regularFile, "openRegularFile")
.mockImplementationOnce(async () => {
controller.abort(reason);
throw new regularFile.RegularFileAdmissionFailure(
snapshot.path,
new regularFile.RegularFileOperationCancelled(reason),
new Error("admission close unavailable"),
owner,
);
});
try {
await expect(
snapshot.verify({ bytes: 0, sha256: "unused" }, path, controller.signal),
).rejects.toMatchObject({ _tag: "AnalysisCancelledError", cause: reason });
expect(handle.fd).toBeGreaterThanOrEqual(0);
await snapshot.close();
expect(handle.fd).toBe(-1);
} finally {
admission.mockRestore();
await snapshot.close();
await owner.close();
}
});
@@ -1,13 +1,15 @@
import { readFile, writeFile } from "node:fs/promises";
import { readFile, writeFile, type FileHandle } from "node:fs/promises";
import { join } from "node:path";
import { afterEach, expect, onTestFinished, test, vi } from "vitest";
import { AdbProvider } from "../../../../src/adb/AdbProvider.js";
import * as regularFile from "../../../../src/filesystem/RegularFile.js";
import { OwnedFileHandle } from "../../../../src/filesystem/OwnedFileHandle.js";
import {
execFileBytes,
execFileOutput,
} from "../../../../src/process/ExecFileOutput.js";
import { AnalysisCancelledError } from "../../../../src/domain/analysisErrorCore.js";
import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
@@ -76,7 +78,7 @@ const fixture = async () => {
return { root, provider, failFirstClose };
};
test.each([
const digestCases = [
{
label: "single-file pull",
retainedPath: (root: string) => join(root, "payload.bin"),
@@ -122,7 +124,9 @@ test.each([
}),
expected: { bytes: 12, serial: "device", sha256: expect.any(String) },
},
])(
] as const;
test.each(digestCases)(
"preserves the linked hash cleanup error through $label",
async ({ request, expected, retainedPath }) => {
const f = await fixture();
@@ -150,3 +154,142 @@ test.each([
);
},
);
const errorChainSome = (
error: unknown,
predicate: (error: unknown) => boolean,
): boolean => {
if (predicate(error)) return true;
if (!(error instanceof Error)) return false;
return (
errorChainSome(error.cause, predicate) ||
(error instanceof AggregateError &&
error.errors.some((child: unknown) => errorChainSome(child, predicate)))
);
};
for (const closeFails of [false, true]) {
test.each(digestCases)(
`preserves a read failure through $label when close aborts (close fails: ${String(closeFails)})`,
async ({ request, retainedPath }) => {
const f = await fixture();
const path = retainedPath(f.root);
const controller = new AbortController();
const primary = Object.assign(
new Error("injected physical digest read EIO"),
{ code: "EIO" },
);
const nativeOpen = regularFile.openRegularFile;
let opened: FileHandle | undefined;
vi.spyOn(regularFile, "openRegularFile").mockImplementation(
async (selected, options) => {
const file = await nativeOpen(selected, options);
if (selected === path) {
opened = file;
vi.spyOn(file, "read").mockRejectedValueOnce(primary);
}
return file;
},
);
const nativeClose = OwnedFileHandle.prototype.close;
let allowClose = !closeFails;
let owner: OwnedFileHandle | undefined;
vi.spyOn(OwnedFileHandle.prototype, "close").mockImplementation(
async function (this: OwnedFileHandle) {
if (this.handle === opened) {
owner = this;
controller.abort("cancelled after the physical failure");
if (!allowClose) throw new Error("digest close remains blocked");
}
return nativeClose.call(this);
},
);
try {
const result = await f.provider.execute(request(f.root), {
signal: controller.signal,
});
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected digest read failure");
expect(projectAnalysisError(result.error).code).not.toBe("cancelled");
expect(
errorChainSome(
result.error,
(cause) => cause instanceof AnalysisCancelledError,
),
).toBe(false);
expect(errorChainSome(result.error, (cause) => cause === primary)).toBe(
true,
);
if (closeFails) expect(result.error.cleanupIncomplete).toBe(true);
allowClose = true;
await f.provider.close();
expect(owner?.handle.fd).toBe(-1);
expect(result.error.cleanupIncomplete).toBe(false);
} finally {
allowClose = true;
await f.provider.close();
if (owner !== undefined) await nativeClose.call(owner);
}
},
);
}
for (const closeFails of [false, true]) {
test.each(digestCases)(
`retains exact cancellation and completed digest through $label (close fails: ${String(closeFails)})`,
async ({ request, retainedPath }) => {
const f = await fixture();
const path = retainedPath(f.root);
const controller = new AbortController();
const reason = null;
const nativeOpen = regularFile.openRegularFile;
let opened: FileHandle | undefined;
vi.spyOn(regularFile, "openRegularFile").mockImplementation(
async (selected, options) => {
const handle = await nativeOpen(selected, options);
if (selected === path) opened = handle;
return handle;
},
);
const nativeClose = OwnedFileHandle.prototype.close;
let allowClose = !closeFails;
vi.spyOn(OwnedFileHandle.prototype, "close").mockImplementation(
async function (this: OwnedFileHandle) {
if (this.handle === opened) {
controller.abort(reason);
if (!allowClose)
throw new Error("digest close remains unavailable");
}
await nativeClose.call(this);
},
);
try {
const result = await f.provider.execute(request(f.root), {
signal: controller.signal,
});
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected completion cancellation");
expect(projectAnalysisError(result.error).code).toBe(
closeFails ? "cleanup_incomplete" : "cancelled",
);
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
expect(result.error.cause).toBe(reason);
expect(
JSON.stringify(
projectAnalysisError(result.error).details?.partial_observation,
),
).toContain('"sha256"');
expect(result.error.cleanupIncomplete).toBe(closeFails);
allowClose = true;
await f.provider.close();
expect(opened?.fd).toBe(-1);
expect(result.error.cleanupIncomplete).toBe(false);
} finally {
allowClose = true;
await f.provider.close();
if (opened !== undefined && opened.fd >= 0)
await nativeClose.call(new OwnedFileHandle(opened));
}
},
);
}