fix(native-runtime): contain backup verification deadline aborts (#19556)

Reject an already-aborted backup verification before constructing its file stream so cancellation does not raise an uncaught AbortError. Validation: 60 focused tests and all applicable CI pass.

Greptile scored the exact head 5/5, and no review threads remain unresolved.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Devin FoleyandPaperclip authored and GitHub committed 2026-11-06 12:04:56 -08:00
1 parent 91b641757b
commit 50038fac70
2 files changed
+54

No files matched your search

@@ -0,0 +1,53 @@
import { createHash } from "node:crypto";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createNativeHarnessBackupStamp, verifyNativeHarnessBackupStampAsync } from "./native-harness-backup-stamp.js";
const hooks = vi.hoisted(() => ({ afterOpen: undefined as (() => void) | undefined }));
vi.mock("node:fs/promises", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:fs/promises")>();
const open = async (...args: Parameters<typeof actual.open>) => {
const handle = await actual.open(...args);
hooks.afterOpen?.();
return handle;
};
return { ...actual, default: { ...actual, open }, open };
});
describe("verifyNativeHarnessBackupStampAsync cancellation", () => {
let base: string;
beforeEach(async () => {
base = await fs.realpath(await fs.mkdtemp(path.join(os.tmpdir(), "native-harness-stamp-abort-")));
vi.stubEnv("PAPERCLIP_RUNNER_STATE_DIR", base);
});
afterEach(async () => {
hooks.afterOpen = undefined;
vi.unstubAllEnvs();
await fs.rm(base, { recursive: true, force: true });
});
it("rejects with the signal reason and raises no uncaught AbortError when it aborts before a file read", async () => {
const sessionScopeId = "session-scope", completedAt = "2026-11-06T00:00:00.000Z";
const current = path.join(base, createHash("sha256").update(sessionScopeId).digest("hex"), "failover-backups", "current");
await fs.mkdir(path.join(current, "home"), { recursive: true });
await fs.writeFile(path.join(current, "home", "history.json"), "{}\n");
await fs.writeFile(path.join(current, "manifest.json"), JSON.stringify({
schema: "paperclip.native-harness-backup.v1", normalizedSessionId: "session", runnerInstanceId: "runner",
sourceProviderLeaseId: "lease", completedAt, directories: [{ name: "home", sha256: `sha256:${"0".repeat(64)}`, bytes: 3 }],
}));
const stamp = createNativeHarnessBackupStamp({ manifestPath: path.join(current, "manifest.json"), sessionScopeId,
authorizedProviderLeaseId: "lease", normalizedSessionId: "session", runnerInstanceId: "runner", completedAt });
const controller = new AbortController(), reason = new Error("verification cancelled");
hooks.afterOpen = () => controller.abort(reason);
const uncaught: unknown[] = [];
const record = (error: unknown) => { uncaught.push(error); };
process.on("uncaughtException", record);
try {
await expect(verifyNativeHarnessBackupStampAsync(stamp, "lease", controller.signal)).rejects.toBe(reason);
await new Promise(resolve => setTimeout(resolve, 50));
} finally { process.off("uncaughtException", record); }
expect(uncaught).toEqual([]);
});
});
@@ -238,6 +238,7 @@ export async function verifyNativeHarnessBackupStampAsync(value: unknown, expect
try {
const before = await handle.stat();
if (!before.isFile() || stats.ino !== before.ino || stats.dev !== before.dev || stats.size !== before.size) throw new Error("native_harness_backup_source_changed");
signal.throwIfAborted();
for await (const chunk of handle.createReadStream({ signal, autoClose: false })) {
bytes += chunk.length; if (bytes > directory.bytes) throw new Error("native_harness_backup_size_mismatch"); hash.update(chunk);
}