diff --git a/scripts/verify/android/windows-guardian-isolation.mjs b/scripts/verify/android/windows-guardian-isolation.mjs index 1a6308970a..98fa9553fb 100644 --- a/scripts/verify/android/windows-guardian-isolation.mjs +++ b/scripts/verify/android/windows-guardian-isolation.mjs @@ -12,7 +12,10 @@ const marker = "REA synthetic guardian isolation test\n"; const exists = (path) => stat(path).then( () => true, - () => false, + (cause) => { + if (cause.code === "ENOENT" || cause.code === "ENOTDIR") return false; + throw cause; + }, ); if (process.argv[1] === source && process.argv[2] === "owner") { diff --git a/tests/boundary/process/androidGuardianObservation.test.ts b/tests/boundary/process/androidGuardianObservation.test.ts new file mode 100644 index 0000000000..d8f6ade9fe --- /dev/null +++ b/tests/boundary/process/androidGuardianObservation.test.ts @@ -0,0 +1,68 @@ +import { EventEmitter } from "node:events"; +import { PassThrough } from "node:stream"; +import { expect, it, vi } from "vitest"; + +const observation = vi.hoisted(() => ({ + serial: 0, + failure: Object.assign(new Error("inert workspace observation"), { + code: "ENOENT", + }), +})); +vi.mock("node:fs/promises", async (original) => { + const fs = await original(); + return { + ...fs, + stat: async (...args: Parameters) => { + if (!String(args[0]).startsWith("/rea-inert/")) return fs.stat(...args); + if (String(args[0]).endsWith("synthetic.txt")) return {}; + throw observation.failure; + }, + }; +}); +vi.mock("node:child_process", async (original) => { + const cp = await original(); + return { + ...cp, + spawn: (...args: Parameters) => { + if (args[1]?.[1] !== "owner") return cp.spawn(...args); + const events = new EventEmitter(); + const child = Object.assign(events, { + stdout: new PassThrough(), + stderr: new PassThrough(), + exitCode: null as number | null, + signalCode: null, + kill() { + this.exitCode = 0; + events.emit("close", 0); + return true; + }, + }); + const path = `/rea-inert/owned-${++observation.serial}`; + queueMicrotask(() => child.stdout.write(JSON.stringify({ path }) + "\n")); + return child; + }, + }; +}); +const { verifyWindowsGuardianIsolation } = await vi.importActual<{ + verifyWindowsGuardianIsolation(): Promise; +}>("../../../scripts/verify/android/windows-guardian-isolation.mjs"); + +it.each(["ENOENT", "ENOTDIR", "EACCES", "EIO"])( + "admits only known workspace absence after owner exit (%s)", + async (code) => { + observation.failure = Object.assign(new Error(`inert stat ${code}`), { + code, + }); + const original = Object.getOwnPropertyDescriptor(process, "platform"); + if (original === undefined) throw new Error("Platform descriptor missing"); + Object.defineProperty(process, "platform", { ...original, value: "win32" }); + try { + const result = verifyWindowsGuardianIsolation(); + if (code === "ENOENT" || code === "ENOTDIR") + await expect(result).resolves.toBeUndefined(); + else await expect(result).rejects.toBe(observation.failure); + } finally { + Object.defineProperty(process, "platform", original); + } + }, +);