From fc4a9a05922ce8474fd0717bc800343b0a95bd41 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Wed, 7 Oct 2026 23:58:07 +0800 Subject: [PATCH] fix(artifacts): preserve reader failure diagnostics (#987) * fix(artifacts): preserve filesystem and DMG diagnostics * fix(artifacts): classify DMG verification failures * fix(artifacts): distinguish inaccessible disk images * fix(artifacts): classify damaged image checksums --- src/artifacts/DirectoryArtifactReader.ts | 68 ++++- src/artifacts/NativeDmgArtifactReader.ts | 177 ++++++++++++- src/process/ExecFileOutput.test.ts | 9 + src/process/ExecFileOutput.ts | 2 + .../artifactProvider.directoryErrors.test.ts | 57 ++++ .../native/nativeDmgArtifactReader.test.ts | 244 +++++++++++++++++- 6 files changed, 537 insertions(+), 20 deletions(-) create mode 100644 tests/boundary/providers/native/artifactProvider.directoryErrors.test.ts diff --git a/src/artifacts/DirectoryArtifactReader.ts b/src/artifacts/DirectoryArtifactReader.ts index cf4205c9e..1a6cf2657 100644 --- a/src/artifacts/DirectoryArtifactReader.ts +++ b/src/artifacts/DirectoryArtifactReader.ts @@ -13,34 +13,48 @@ import { export class DirectoryArtifactReader implements ArtifactReader { readonly format = "directory" as const; readonly #rootPromise: Promise; + readonly #rootPath: string; constructor(root: string) { + this.#rootPath = root; this.#rootPromise = realpath(root); } async *entries(signal?: AbortSignal): AsyncIterable { - const root = await this.#rootPromise; + const root = await this.#rootPromise.catch((cause: unknown) => { + throw directoryIoFailure("resolve artifact root", this.#rootPath, cause); + }); const pending = [root]; while (pending.length > 0) { abortIfNeeded(signal); const directory = pending.pop(); if (directory === undefined) break; - await assertContainedDirectory(root, directory); + await withDirectoryIoContext("inspect directory", directory, () => + assertContainedDirectory(root, directory), + ); const children: string[] = []; - const handle = await opendir(directory); + const handle = await opendir(directory).catch((cause: unknown) => { + throw directoryIoFailure("open directory", directory, cause); + }); try { for await (const child of handle) children.push(child.name); + } catch (cause: unknown) { + throw directoryIoFailure("read directory", directory, cause); } finally { // best-effort cleanup: directory-handle close must not mask traversal. await handle.close().catch(() => undefined); } - await assertContainedDirectory(root, directory); + await withDirectoryIoContext("inspect directory", directory, () => + assertContainedDirectory(root, directory), + ); children.sort((left, right) => left.localeCompare(right, "en")); const directories: string[] = []; for (const name of children) { abortIfNeeded(signal); const absolute = join(directory, name); - const metadata = await lstat(absolute); + const metadata = await lstat(absolute).catch((cause: unknown) => { + throw directoryIoFailure("inspect entry", absolute, cause); + }); const path = relative(root, absolute).split(sep).join("/"); const kind = metadata.isSymbolicLink() ? "symlink" @@ -86,9 +100,17 @@ export class DirectoryArtifactReader implements ArtifactReader { const handle = await open( entry.adapterKey, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK, - ); + ).catch((cause: unknown) => { + throw directoryIoFailure("open entry", entry.adapterKey, cause); + }); try { - const observed = await handle.stat(); + const observed = await handle.stat().catch((cause: unknown) => { + throw directoryIoFailure( + "inspect opened entry", + entry.adapterKey, + cause, + ); + }); if ( !observed.isFile() || entry.sourceIdentity === undefined || @@ -117,6 +139,38 @@ export class DirectoryArtifactReader implements ArtifactReader { } } +const directoryIoFailure = ( + operation: string, + path: string, + cause: unknown, +): ArtifactReaderFailure => { + if (cause instanceof ArtifactReaderFailure) return cause; + const code = + typeof cause === "object" && + cause !== null && + "code" in cause && + typeof cause.code === "string" + ? ` (${cause.code})` + : ""; + return new ArtifactReaderFailure( + "io", + `Could not ${operation} at ${path}${code}`, + { cause }, + ); +}; + +const withDirectoryIoContext = async ( + operation: string, + path: string, + action: () => Promise, +): Promise => { + try { + return await action(); + } catch (cause: unknown) { + throw directoryIoFailure(operation, path, cause); + } +}; + const abortIfNeeded = (signal?: AbortSignal): void => { if (signal?.aborted === true) throw new ArtifactReaderFailure( diff --git a/src/artifacts/NativeDmgArtifactReader.ts b/src/artifacts/NativeDmgArtifactReader.ts index b5c6ee73a..2c8caecc1 100644 --- a/src/artifacts/NativeDmgArtifactReader.ts +++ b/src/artifacts/NativeDmgArtifactReader.ts @@ -37,13 +37,18 @@ export interface NativeDmgHost { arguments_: readonly string[], signal?: AbortSignal, options?: { readonly timeoutMs: number }, - ): Promise<{ readonly stdout: string; readonly exitCode: number }>; + ): Promise<{ + readonly stdout: string; + readonly stderr?: string; + readonly exitCode: number; + readonly cause?: unknown; + }>; } const systemHost: NativeDmgHost = { async run(arguments_, signal, options) { try { - const { stdout } = await execFileOutput( + const { stdout, stderr } = await execFileOutput( "/usr/bin/hdiutil", [...arguments_], { @@ -51,13 +56,23 @@ const systemHost: NativeDmgHost = { ...(signal === undefined ? {} : { signal }), }, ); - return { stdout, exitCode: 0 }; + return { stdout, stderr, exitCode: 0 }; } catch (cause: unknown) { if (cause instanceof Error && cause.name === "AbortError") throw new ArtifactReaderFailure("cancelled", "DMG operation cancelled"); - throw new ArtifactReaderFailure("format", "hdiutil rejected the DMG", { - cause, - }); + const exitCode = processExitCode(cause); + if (exitCode !== undefined) + return { + stdout: processOutput(cause, "stdout"), + stderr: processOutput(cause, "stderr"), + exitCode, + cause, + }; + throw new ArtifactReaderFailure( + commandFailureReason(cause, arguments_[0]), + `hdiutil ${arguments_[0] ?? "operation"} failed: ${describeCommandFailure(cause, arguments_)}`, + { cause }, + ); } }, }; @@ -250,15 +265,157 @@ const runChecked = async ( signal?: AbortSignal, options?: { readonly timeoutMs: number }, ): Promise<{ readonly stdout: string; readonly exitCode: 0 }> => { - const result = await host.run(arguments_, signal, options); - if (result.exitCode !== 0) + let result: Awaited>; + try { + result = await host.run(arguments_, signal, options); + } catch (cause: unknown) { + if (cause instanceof ArtifactReaderFailure) throw cause; + if (cause instanceof Error && cause.name === "AbortError") + throw new ArtifactReaderFailure("cancelled", "DMG operation cancelled", { + cause, + }); throw new ArtifactReaderFailure( - "unavailable", - `hdiutil ${arguments_[0] ?? "operation"} failed with a non-zero exit code`, + commandFailureReason(cause, arguments_[0]), + `hdiutil ${arguments_[0] ?? "operation"} failed: ${describeCommandFailure( + cause, + arguments_, + )}`, + { cause }, ); + } + if (result.exitCode !== 0) { + const failure = { + command: "/usr/bin/hdiutil", + exitCode: result.exitCode, + ...(result.cause === undefined + ? {} + : { code: processExitCode(result.cause) }), + stdout: result.stdout, + ...(result.stderr === undefined ? {} : { stderr: result.stderr }), + }; + const reason = commandFailureReason(failure, arguments_[0]); + const details = describeCommandFailure(failure, arguments_); + const unknownVerifyFailure = + arguments_[0] === "verify" && reason === "unavailable"; + throw new ArtifactReaderFailure( + reason, + unknownVerifyFailure + ? `hdiutil verify failed; captured diagnostics do not establish the failure cause: ${details}` + : `hdiutil ${arguments_[0] ?? "operation"} failed: ${details}`, + result.cause === undefined ? undefined : { cause: result.cause }, + ); + } return { stdout: result.stdout, exitCode: 0 }; }; +const describeCommandFailure = ( + cause: unknown, + arguments_: readonly string[], +): string => { + const fields: Record = { + command: "/usr/bin/hdiutil", + arguments: [...arguments_], + }; + if (typeof cause === "object" && cause !== null) { + for (const key of [ + "code", + "exitCode", + "errno", + "syscall", + "signal", + "stdout", + "stderr", + ] as const) { + const value = Reflect.get(cause, key); + if (typeof value === "string" || typeof value === "number") + fields[key] = value; + } + const code = Reflect.get(cause, "code"); + if (typeof code === "number") fields.exitCode = code; + } + if (cause instanceof ArtifactReaderFailure) fields.message = cause.message; + else if (cause instanceof Error) fields.message = cause.message; + return JSON.stringify(fields); +}; + +const processExitCode = (cause: unknown): number | undefined => { + if (typeof cause !== "object" || cause === null) return undefined; + const exitCode = Reflect.get(cause, "exitCode"); + if (typeof exitCode === "number") return exitCode; + const code = Reflect.get(cause, "code"); + return typeof code === "number" ? code : undefined; +}; + +const processOutput = (cause: unknown, field: "stdout" | "stderr"): string => { + if (typeof cause !== "object" || cause === null) return ""; + const value = Reflect.get(cause, field); + return typeof value === "string" ? value : ""; +}; + +const commandFailureReason = ( + cause: unknown, + operation: string | undefined, +): ArtifactReaderFailure["reason"] => { + if (typeof cause !== "object" || cause === null) return "unavailable"; + const code = Reflect.get(cause, "code"); + const exitCode = Reflect.get(cause, "exitCode"); + if (typeof code === "number" || typeof exitCode === "number") + return operation === "verify" ? verifyFailureReason(cause) : "unavailable"; + if (code === "ENOENT") { + const syscall = Reflect.get(cause, "syscall"); + return typeof syscall === "string" && syscall.startsWith("spawn") + ? "unavailable" + : "io"; + } + if ( + code === "EACCES" || + code === "EPERM" || + code === "EIO" || + code === "ENOTDIR" || + code === "EISDIR" || + code === "ENODEV" || + code === "EROFS" || + code === "EMFILE" || + code === "ENFILE" + ) + return "io"; + return "unavailable"; +}; + +const verifyFailureReason = ( + cause: unknown, +): ArtifactReaderFailure["reason"] => { + const diagnostic = verifyFailureDiagnostic( + processOutput(cause, "stdout"), + processOutput(cause, "stderr"), + ); + switch (diagnostic) { + case "image not recognized": + return "format"; + case "invalid checksum": + case "image data corrupted": + return "integrity"; + case "No such file or directory": + case "Permission denied": + case "Input/output error": + return "io"; + default: + return "unavailable"; + } +}; + +const verifyFailureDiagnostic = ( + stdout: string, + stderr: string, +): string | undefined => { + const prefix = "hdiutil: verify failed - "; + return `${stdout}\n${stderr}` + .split(/\r?\n/u) + .map((line) => line.trim()) + .find((line) => line.startsWith(prefix)) + ?.slice(prefix.length); +}; + const command = ( arguments_: readonly string[], effects: ArtifactCommand["effects"], diff --git a/src/process/ExecFileOutput.test.ts b/src/process/ExecFileOutput.test.ts index 1dd81155c..bb963987b 100644 --- a/src/process/ExecFileOutput.test.ts +++ b/src/process/ExecFileOutput.test.ts @@ -23,4 +23,13 @@ describe("execFileOutput", () => { ), ).rejects.toMatchObject({ code: "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" }); }); + + it("retains both captured streams on a failed child process", async () => { + await expect( + execFileOutput(process.execPath, [ + "-e", + 'process.stdout.write("out"); process.stderr.write("err"); process.exitCode = 7;', + ]), + ).rejects.toMatchObject({ code: 7, stdout: "out", stderr: "err" }); + }); }); diff --git a/src/process/ExecFileOutput.ts b/src/process/ExecFileOutput.ts index 53360848a..232a93a7c 100644 --- a/src/process/ExecFileOutput.ts +++ b/src/process/ExecFileOutput.ts @@ -25,6 +25,8 @@ export const execFileOutput = ( }, (error, stdout, stderr) => { if (error !== null) { + Reflect.set(error, "stdout", stdout); + Reflect.set(error, "stderr", stderr); reject(error); return; } diff --git a/tests/boundary/providers/native/artifactProvider.directoryErrors.test.ts b/tests/boundary/providers/native/artifactProvider.directoryErrors.test.ts new file mode 100644 index 000000000..25264cfa1 --- /dev/null +++ b/tests/boundary/providers/native/artifactProvider.directoryErrors.test.ts @@ -0,0 +1,57 @@ +import { chmod, mkdir, writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +import { describe, expect, it } from "vitest"; + +import { parseBinaryTarget } from "../../../../src/application/BinaryTargetResolver.js"; +import { ArtifactProvider } from "../../../../src/artifacts/ArtifactProvider.js"; +import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js"; +import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js"; + +describe("artifact directory diagnostics", () => { + it.skipIf(process.platform === "win32")( + "preserves the inaccessible member path and errno through provider projection", + async () => { + const root = await createTestTempDirectory("rea-directory-eacces-"); + const appPath = join(root, "Denied.app"); + const contentsPath = join(appPath, "Contents"); + const executablePath = join(contentsPath, "MacOS", "Fixture"); + const blockedPath = join(contentsPath, "blocked.txt"); + await mkdir(join(contentsPath, "MacOS"), { recursive: true }); + await writeFile( + join(contentsPath, "Info.plist"), + "CFBundleExecutableFixture", + ); + const machHeader = Buffer.alloc(8); + machHeader.writeUInt32BE(0xfeedfacf, 0); + machHeader.writeUInt32BE(0x0100000c, 4); + await writeFile(executablePath, machHeader); + await writeFile(blockedPath, "member contents"); + await chmod(blockedPath, 0); + try { + const target = await parseBinaryTarget(appPath); + expect(target.ok).toBe(true); + if (!target.ok) return; + const result = await new ArtifactProvider() + .createClient(target.value) + .execute("inventory_artifact", {}); + expect(result.ok).toBe(false); + if (result.ok) return; + expect(projectAnalysisError(result.error)).toMatchObject({ + code: "artifact_operation_failed", + details: { + operation: "inventory_artifact", + reason: "io", + }, + }); + const projected = projectAnalysisError(result.error); + const detail = projected.details?.detail; + expect(typeof detail).toBe("string"); + expect(detail).toContain("Denied.app/Contents/blocked.txt"); + expect(detail).toContain("EACCES"); + } finally { + await chmod(blockedPath, 0o600); + } + }, + ); +}); diff --git a/tests/boundary/providers/native/nativeDmgArtifactReader.test.ts b/tests/boundary/providers/native/nativeDmgArtifactReader.test.ts index b04633b96..67495684c 100644 --- a/tests/boundary/providers/native/nativeDmgArtifactReader.test.ts +++ b/tests/boundary/providers/native/nativeDmgArtifactReader.test.ts @@ -1,9 +1,12 @@ -import { mkdir, realpath, writeFile } from "node:fs/promises"; +import { chmod, mkdir, readFile, realpath, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { build } from "plist"; import { describe, expect, it } from "vitest"; +import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js"; +import { execFileOutput } from "../../../../src/process/ExecFileOutput.js"; +import { ArtifactReaderFailure } from "../../../../src/artifacts/ArtifactReader.js"; import { NativeDmgArtifactReader, type NativeDmgHost, @@ -53,9 +56,19 @@ describe("native DMG artifact reader", () => { it("rejects non-zero results and surfaces detach failure during attach cleanup", async () => { await expect( NativeDmgArtifactReader.create("/tmp/image.dmg", undefined, { - run: () => Promise.resolve({ stdout: "", exitCode: 1 }), + run: () => + Promise.resolve({ + stdout: "verify output", + stderr: "hdiutil: verify failed - image not recognized", + exitCode: 1, + }), }), - ).rejects.toThrow("non-zero exit code"); + ).rejects.toMatchObject({ + reason: "format", + message: expect.stringMatching( + /"arguments":\["verify","\/tmp\/image\.dmg"\].*"exitCode":1.*"stderr":"hdiutil: verify failed - image not recognized"/u, + ), + }); const host: NativeDmgHost = { run(arguments_) { @@ -91,6 +104,231 @@ describe("native DMG artifact reader", () => { }); }); +describe("native DMG command failure diagnostics", () => { + it("keeps unknown attach failures unavailable with command evidence", async () => { + await expect( + NativeDmgArtifactReader.create("/tmp/image.dmg", undefined, { + run(arguments_) { + if (arguments_[0] === "verify") + return Promise.resolve({ stdout: "", exitCode: 0 }); + return Promise.resolve({ + stdout: "attach output", + stderr: "attach failed for an unknown reason", + exitCode: 2, + }); + }, + }), + ).rejects.toMatchObject({ + reason: "unavailable", + message: expect.stringMatching( + /"arguments":\["attach".*"exitCode":2.*"stderr":"attach failed for an unknown reason"/u, + ), + }); + }); + + it("keeps unrecognized numeric verify failures unknown and maps the observed I/O diagnostic", async () => { + const unknown = await NativeDmgArtifactReader.create( + "/tmp/image.dmg", + undefined, + { + run: () => + Promise.resolve({ + stdout: "verify stdout", + stderr: "hdiutil: verify failed - an unrecognized system failure", + exitCode: 1, + }), + }, + ).catch((cause: unknown) => cause); + expect(unknown).toBeInstanceOf(ArtifactReaderFailure); + if (!(unknown instanceof ArtifactReaderFailure)) return; + expect(unknown.reason).toBe("unavailable"); + expect(unknown.message).toContain("do not establish the failure cause"); + expect(unknown.message).toContain('"exitCode":1'); + expect(unknown.message).toContain('"stdout":"verify stdout"'); + expect(unknown.message).toContain("an unrecognized system failure"); + + await expect( + NativeDmgArtifactReader.create("/tmp/image.dmg", undefined, { + run: () => + Promise.resolve({ + stdout: "", + stderr: "hdiutil: verify failed - Input/output error\n", + exitCode: 1, + }), + }), + ).rejects.toMatchObject({ + reason: "io", + message: expect.stringContaining("Input/output error"), + }); + + await expect( + NativeDmgArtifactReader.create("/tmp/image.dmg", undefined, { + run: () => + Promise.resolve({ + stdout: "checksum output", + stderr: "hdiutil: verify failed - image data corrupted\n", + exitCode: 1, + }), + }), + ).rejects.toMatchObject({ + reason: "integrity", + message: expect.stringContaining("image data corrupted"), + }); + }); + + it("distinguishes child launch and host permission failures", async () => { + await expect( + NativeDmgArtifactReader.create("/tmp/image.dmg", undefined, { + run: () => + Promise.reject( + Object.assign(new Error("could not launch hdiutil"), { + code: "ENOENT", + syscall: "spawn /usr/bin/hdiutil", + }), + ), + }), + ).rejects.toMatchObject({ + reason: "unavailable", + message: expect.stringContaining('"code":"ENOENT"'), + }); + + await expect( + NativeDmgArtifactReader.create("/tmp/image.dmg", undefined, { + run: () => + Promise.reject( + Object.assign(new Error("permission denied"), { + code: "EACCES", + syscall: "spawn /usr/bin/hdiutil", + }), + ), + }), + ).rejects.toMatchObject({ + reason: "io", + message: expect.stringContaining('"code":"EACCES"'), + }); + }); +}); + +describe("native DMG real verification diagnostics", () => { + it.skipIf(process.platform !== "darwin")( + "classifies a real hdiutil verify refusal as a format failure with captured output", + async () => { + const root = await createTestTempDirectory("rea-invalid-dmg-"); + const path = join(root, "invalid.dmg"); + await writeFile(path, "This is not a disk image.\n"); + const failure = await NativeDmgArtifactReader.create(path).catch( + (cause: unknown) => cause, + ); + expect(failure).toBeInstanceOf(ArtifactReaderFailure); + if (!(failure instanceof ArtifactReaderFailure)) return; + expect(failure.reason).toBe("format"); + expect(failure.message).toContain('"command":"/usr/bin/hdiutil"'); + expect(failure.message).toContain('"arguments":["verify"'); + expect(failure.message).toContain('"exitCode":1'); + expect(failure.message).toContain('"stdout":""'); + expect(failure.message).toContain('"stderr":"'); + }, + ); + + it.skipIf(process.platform !== "darwin")( + "classifies real hdiutil target access failures as I/O, separately from malformed images", + async () => { + const root = await createTestTempDirectory("rea-dmg-target-errors-"); + const missingPath = join(root, "missing.dmg"); + const unreadablePath = join(root, "unreadable.dmg"); + await writeFile(unreadablePath, "not a disk image\n"); + await chmod(unreadablePath, 0); + try { + for (const [path, expectedDiagnostic] of [ + [missingPath, "No such file or directory"], + [unreadablePath, "Permission denied"], + ] as const) { + const failure = await NativeDmgArtifactReader.create(path).catch( + (cause: unknown) => cause, + ); + expect(failure).toBeInstanceOf(ArtifactReaderFailure); + if (!(failure instanceof ArtifactReaderFailure)) continue; + expect(failure.reason).toBe("io"); + expect(failure.message).toContain('"command":"/usr/bin/hdiutil"'); + expect(failure.message).toContain('"arguments":["verify"'); + expect(failure.message).toContain('"exitCode":1'); + expect(failure.message).toContain('"stdout":""'); + expect(failure.message).toContain(expectedDiagnostic); + expect(failure.cause).toBeInstanceOf(Error); + if (!(failure.cause instanceof Error)) continue; + expect(Reflect.get(failure.cause, "code")).toBe(1); + expect(Reflect.get(failure.cause, "stderr")).toContain( + expectedDiagnostic, + ); + } + } finally { + await chmod(unreadablePath, 0o600); + } + }, + ); + + it.skipIf(process.platform !== "darwin")( + "classifies a checksum mismatch in an hdiutil-created DMG as an integrity failure", + async () => { + const root = await createTestTempDirectory( + "rea-dmg-checksum-corruption-", + ); + const source = join(root, "source"); + await mkdir(source); + await writeFile(join(source, "payload.bin"), Buffer.alloc(65_536, 0x5a)); + const imagePath = join(root, "fixture.dmg"); + await execFileOutput("/usr/bin/hdiutil", [ + "create", + "-quiet", + "-srcfolder", + source, + "-volname", + "Fixture", + "-format", + "UDRO", + imagePath, + ]); + const valid = await execFileOutput("/usr/bin/hdiutil", [ + "verify", + imagePath, + ]); + expect(valid.stderr).toContain("is VALID"); + + const image = await readFile(imagePath); + const footer = image.subarray(-512); + expect(footer.subarray(0, 4).toString("ascii")).toBe("koly"); + const dataForkOffset = Number(footer.readBigUInt64BE(24)); + const dataForkLength = Number(footer.readBigUInt64BE(32)); + expect(dataForkLength).toBeGreaterThan(4096); + const corruptionOffset = dataForkOffset + 4096; + expect(corruptionOffset).toBeLessThan(dataForkOffset + dataForkLength); + const originalByte = image[corruptionOffset]; + if (originalByte === undefined) + throw new Error("expected a byte in the generated image data fork"); + image[corruptionOffset] = originalByte ^ 0x01; + await writeFile(imagePath, image); + + const failure = await NativeDmgArtifactReader.create(imagePath).catch( + (cause: unknown) => cause, + ); + expect(failure).toBeInstanceOf(ArtifactReaderFailure); + if (!(failure instanceof ArtifactReaderFailure)) return; + expect(failure.reason).toBe("integrity"); + expect(failure.message).toContain('"command":"/usr/bin/hdiutil"'); + expect(failure.message).toContain('"arguments":["verify"'); + expect(failure.message).toContain('"exitCode":1'); + expect(failure.message).toContain("calculated CRC32"); + expect(failure.message).toContain("expected CRC32"); + expect(failure.message).toContain("verify failed - invalid checksum"); + expect(failure.cause).toBeInstanceOf(Error); + if (!(failure.cause instanceof Error)) return; + expect(Reflect.get(failure.cause, "code")).toBe(1); + expect(Reflect.get(failure.cause, "stderr")).toContain("is INVALID"); + }, + 60_000, + ); +}); + it("owns the canonical mount root and detaches each observed whole image once", async () => { const calls: string[][] = []; const host: NativeDmgHost = {