From 89e543579590a34dd1c8464e85bd5f57922503ea Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Sat, 12 Sep 2026 19:39:11 -0700 Subject: [PATCH 1/5] fix(e2e): restore Docker gateway before sandbox recovery --- test/e2e/fixtures/phases/lifecycle.ts | 19 ++++------- test/e2e/support/e2e-phase-lifecycle.test.ts | 36 ++++++++++++++------ 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/test/e2e/fixtures/phases/lifecycle.ts b/test/e2e/fixtures/phases/lifecycle.ts index 57a24ad1302..0afc959ee7e 100644 --- a/test/e2e/fixtures/phases/lifecycle.ts +++ b/test/e2e/fixtures/phases/lifecycle.ts @@ -734,27 +734,20 @@ export class LifecyclePhaseFixture { previousRuntime: HostGatewayRuntime | null, options: { requireUserService?: boolean; sandboxName?: string } = {}, ): Promise { - if (options.sandboxName && options.requireUserService !== true) { - return await this.host.nemoclaw([options.sandboxName, "status"], { - artifactName: `lifecycle-gateway-recover-through-nemoclaw-status-${options.sandboxName}`, - env: buildAvailabilityProbeEnv(), - timeoutMs: 120_000, - }); - } const userServiceStart = await this.startOpenShellGatewayUserService({ requireAvailable: options.requireUserService, }); if (userServiceStart) return userServiceStart; - if (options.sandboxName) { - return await this.host.nemoclaw([options.sandboxName, "status"], { - artifactName: `lifecycle-gateway-recover-through-nemoclaw-status-${options.sandboxName}`, + if (previousRuntime?.kind === "pid") { + return await this.host.nemoclaw(["status"], { + artifactName: "lifecycle-gateway-recover-through-nemoclaw-status", env: buildAvailabilityProbeEnv(), timeoutMs: 120_000, }); } - if (previousRuntime?.kind === "pid") { - return await this.host.nemoclaw(["status"], { - artifactName: "lifecycle-gateway-recover-through-nemoclaw-status", + if (options.sandboxName) { + return await this.host.nemoclaw([options.sandboxName, "status"], { + artifactName: `lifecycle-gateway-recover-through-nemoclaw-status-${options.sandboxName}`, env: buildAvailabilityProbeEnv(), timeoutMs: 120_000, }); diff --git a/test/e2e/support/e2e-phase-lifecycle.test.ts b/test/e2e/support/e2e-phase-lifecycle.test.ts index 03d909c60df..e7e9c965312 100644 --- a/test/e2e/support/e2e-phase-lifecycle.test.ts +++ b/test/e2e/support/e2e-phase-lifecycle.test.ts @@ -527,7 +527,7 @@ describe("LifecyclePhaseFixture rebuild helpers", () => { }); describe("LifecyclePhaseFixture gateway runtime restart helpers", () => { - it("falls back to PID/container controls when the selected user service is inactive (#10947)", async () => { + it("restores a PID gateway before sandbox recovery when the user service is inactive (#11640)", async () => { const runner = new FakeRunner(); runner.enqueue(shellResult(0, "12345\n")); // resolveHostRuntime pid probe runner.enqueue(shellResult(0)); // forward stop @@ -545,10 +545,9 @@ describe("LifecyclePhaseFixture gateway runtime restart helpers", () => { const sandbox = new SandboxClient(runner); const fx = new LifecyclePhaseFixture(host, sandbox, cleanup, new GatewayClient(host, sandbox)); - await expect(fx.restartGatewayRuntime({ delayMs: 0 })).resolves.toEqual({ - kind: "pid", - id: "12345", - }); + await expect( + fx.restartGatewayRuntime({ delayMs: 0, sandboxName: "e2e-survival" }), + ).resolves.toEqual({ kind: "pid", id: "12345" }); await fx.waitForGatewayConnected({ attempts: 1, intervalMs: 1 }); expect(runner.calls.map((call) => `${call.command} ${call.args.join(" ")}`)).toEqual([ @@ -679,17 +678,15 @@ describe("LifecyclePhaseFixture gateway runtime restart helpers", () => { expect(runner.calls).toHaveLength(2); }); - it("can recover a PID runtime through sandbox-specific status", async () => { + it("restores a container runtime through sandbox-specific status", async () => { const runner = new FakeRunner(); runner.enqueue(shellResult(0, "status recovered\n")); const cleanup = new FakeCleanup(); await expect( fixture(runner, cleanup).startGatewayRuntime( - { kind: "pid", id: "12345" }, - { - sandboxName: "e2e-survival", - }, + { kind: "container", id: "container-1" }, + { sandboxName: "e2e-survival" }, ), ).resolves.toMatchObject({ exitCode: 0 }); @@ -697,6 +694,25 @@ describe("LifecyclePhaseFixture gateway runtime restart helpers", () => { "nemoclaw e2e-survival status", ]); }); + + it("restarts the stopped user service before a PID or sandbox status fallback", async () => { + const runner = new FakeRunner(); + const cleanup = new FakeCleanup(); + const fx = fixture(runner, cleanup); + runner.enqueue(shellResult(0)); // forward stop + runner.enqueue(shellResult(0, stoppedGatewayUserService)); // user service stop + + await fx.stopGatewayRuntime(); + + runner.enqueue(shellResult(0)); // selected user service restart + await fx.startGatewayRuntime({ kind: "pid", id: "12345" }, { sandboxName: "e2e-survival" }); + + expect(runner.calls.map((call) => call.options?.artifactName)).toEqual([ + "lifecycle-gateway-forward-stop", + "lifecycle-gateway-user-service-stop", + "lifecycle-gateway-user-service-restart", + ]); + }); }); describe("LifecyclePhaseFixture profile dispatch", () => { From d69fe56a1139aa591b012085053045fd4063f71d Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Sat, 12 Sep 2026 20:05:55 -0700 Subject: [PATCH 2/5] ci(e2e): run managed activation for lifecycle changes --- .github/workflows/managed-images.yaml | 1 + .../inference/managed/managed-image-publication-workflow.test.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/managed-images.yaml b/.github/workflows/managed-images.yaml index cc4dddd9623..31a55098442 100644 --- a/.github/workflows/managed-images.yaml +++ b/.github/workflows/managed-images.yaml @@ -57,6 +57,7 @@ on: - "src/lib/state/paths.ts" - "src/lib/state/state-root.ts" - "test/e2e/live/mcp-bridge*.ts" + - "test/e2e/fixtures/phases/lifecycle.ts" - "test/e2e/live/managed-image-activation-e2e*.ts" - "src/lib/tool-disclosure.ts" - "tools/mcp-tool-discovery-runtime/**" diff --git a/test/inference/managed/managed-image-publication-workflow.test.ts b/test/inference/managed/managed-image-publication-workflow.test.ts index 11d9eed60b8..d9ed337c71c 100644 --- a/test/inference/managed/managed-image-publication-workflow.test.ts +++ b/test/inference/managed/managed-image-publication-workflow.test.ts @@ -754,6 +754,7 @@ describe("complete managed-image publication workflow", () => { expect(workflow.on?.pull_request?.paths).toEqual( expect.arrayContaining([ "src/lib/onboard/**", + "test/e2e/fixtures/phases/lifecycle.ts", "test/e2e/live/managed-image-activation-e2e*.ts", ]), ); From 77ae5b2fd25b66e8b66ede029cbf9e78d5c8c3fb Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Sat, 12 Sep 2026 20:34:20 -0700 Subject: [PATCH 3/5] fix(e2e): recover the stopped gateway through its sandbox --- test/e2e/fixtures/phases/lifecycle.ts | 21 ++++++-------------- test/e2e/support/e2e-phase-lifecycle.test.ts | 21 ++++++++++++++------ 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/test/e2e/fixtures/phases/lifecycle.ts b/test/e2e/fixtures/phases/lifecycle.ts index 0afc959ee7e..7e51be64fa5 100644 --- a/test/e2e/fixtures/phases/lifecycle.ts +++ b/test/e2e/fixtures/phases/lifecycle.ts @@ -738,22 +738,13 @@ export class LifecyclePhaseFixture { requireAvailable: options.requireUserService, }); if (userServiceStart) return userServiceStart; - if (previousRuntime?.kind === "pid") { - return await this.host.nemoclaw(["status"], { - artifactName: "lifecycle-gateway-recover-through-nemoclaw-status", - env: buildAvailabilityProbeEnv(), - timeoutMs: 120_000, - }); - } - if (options.sandboxName) { - return await this.host.nemoclaw([options.sandboxName, "status"], { - artifactName: `lifecycle-gateway-recover-through-nemoclaw-status-${options.sandboxName}`, - env: buildAvailabilityProbeEnv(), - timeoutMs: 120_000, - }); + if (!options.sandboxName) { + throw new Error( + `A sandbox name is required to recover the stopped ${previousRuntime?.kind ?? "unknown"} gateway runtime.`, + ); } - return await this.host.command("openshell", ["gateway", "start", "--name", "nemoclaw"], { - artifactName: "lifecycle-gateway-start", + return await this.host.nemoclaw([options.sandboxName, "recover"], { + artifactName: `lifecycle-gateway-recover-through-nemoclaw-recover-${options.sandboxName}`, env: buildAvailabilityProbeEnv(), timeoutMs: 120_000, }); diff --git a/test/e2e/support/e2e-phase-lifecycle.test.ts b/test/e2e/support/e2e-phase-lifecycle.test.ts index e7e9c965312..212a4b2956a 100644 --- a/test/e2e/support/e2e-phase-lifecycle.test.ts +++ b/test/e2e/support/e2e-phase-lifecycle.test.ts @@ -538,7 +538,7 @@ describe("LifecyclePhaseFixture gateway runtime restart helpers", () => { runner.enqueue(shellResult(1, "")); // expectHostRuntimeStopped pid probe runner.enqueue(shellResult(0, "")); // expectHostRuntimeStopped container probe runner.enqueue(shellResult(0)); // lifecycle-gateway-stopped true artifact - runner.enqueue(shellResult(0, "status recovered\n")); // start through nemoclaw status + runner.enqueue(shellResult(0, "gateway recovered\n")); // start through sandbox recovery runner.enqueue(shellResult(0, "Connected to nemoclaw\n")); // waitForGatewayConnected const cleanup = new FakeCleanup(); const host = new HostCliClient(runner); @@ -560,7 +560,7 @@ describe("LifecyclePhaseFixture gateway runtime restart helpers", () => { expect.stringContaining("sh -lc pid_file="), "docker container ps --format {{.ID}}\t{{.Names}}", "true ", - "nemoclaw status", + "nemoclaw e2e-survival recover", "openshell status", ]); }); @@ -678,9 +678,9 @@ describe("LifecyclePhaseFixture gateway runtime restart helpers", () => { expect(runner.calls).toHaveLength(2); }); - it("restores a container runtime through sandbox-specific status", async () => { + it("restores a container runtime through sandbox-specific recovery", async () => { const runner = new FakeRunner(); - runner.enqueue(shellResult(0, "status recovered\n")); + runner.enqueue(shellResult(0, "gateway recovered\n")); const cleanup = new FakeCleanup(); await expect( @@ -691,11 +691,11 @@ describe("LifecyclePhaseFixture gateway runtime restart helpers", () => { ).resolves.toMatchObject({ exitCode: 0 }); expect(runner.calls.map((call) => `${call.command} ${call.args.join(" ")}`)).toEqual([ - "nemoclaw e2e-survival status", + "nemoclaw e2e-survival recover", ]); }); - it("restarts the stopped user service before a PID or sandbox status fallback", async () => { + it("restarts the stopped user service before sandbox recovery", async () => { const runner = new FakeRunner(); const cleanup = new FakeCleanup(); const fx = fixture(runner, cleanup); @@ -713,6 +713,15 @@ describe("LifecyclePhaseFixture gateway runtime restart helpers", () => { "lifecycle-gateway-user-service-restart", ]); }); + + it("fails closed without a sandbox recovery target when no user service owns the gateway", async () => { + await expect( + fixture(new FakeRunner(), new FakeCleanup()).startGatewayRuntime({ + kind: "pid", + id: "12345", + }), + ).rejects.toThrow(/sandbox name is required to recover the stopped pid gateway runtime/i); + }); }); describe("LifecyclePhaseFixture profile dispatch", () => { From c000899ae2b70b34a690450bbe51cc530a7096ea Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Sat, 12 Sep 2026 21:04:06 -0700 Subject: [PATCH 4/5] fix(e2e): restart the host gateway through product code --- test/e2e/fixtures/phases/lifecycle.ts | 47 +++++++++++++++----- test/e2e/support/e2e-phase-lifecycle.test.ts | 39 +++++++++++----- 2 files changed, 63 insertions(+), 23 deletions(-) diff --git a/test/e2e/fixtures/phases/lifecycle.ts b/test/e2e/fixtures/phases/lifecycle.ts index 7e51be64fa5..e9760d87073 100644 --- a/test/e2e/fixtures/phases/lifecycle.ts +++ b/test/e2e/fixtures/phases/lifecycle.ts @@ -185,6 +185,19 @@ export function buildOpenShellGatewayUserServiceDiagnosticsScript(): string { ].join("\n"); } +export function buildNemoClawGatewayRecoveryScript(): string { + return [ + '"use strict";', + 'const { startGatewayForRecovery } = require("./dist/lib/onboard");', + "Promise.resolve()", + ' .then(() => startGatewayForRecovery({ gatewayName: "nemoclaw" }))', + " .catch((error) => {", + " console.error(error && error.stack ? error.stack : String(error));", + " process.exit(1);", + " });", + ].join("\n"); +} + export type LifecycleProfile = "post-reboot-recovery" | "dcode-rebuild-invalid-credential"; export interface LifecycleCleanup { @@ -719,7 +732,9 @@ export class LifecyclePhaseFixture { this.stoppedOpenShellGatewayUserService = selection; this.cleanup.add(`lifecycle.gateway-user-service-restart:${selection}`, async () => { if (this.stoppedOpenShellGatewayUserService !== selection) return; - await this.startOpenShellGatewayUserService({ requireAvailable: true }); + await this.startOpenShellGatewayUserService({ + requireAvailable: true, + }); }); return true; } @@ -738,16 +753,20 @@ export class LifecyclePhaseFixture { requireAvailable: options.requireUserService, }); if (userServiceStart) return userServiceStart; - if (!options.sandboxName) { - throw new Error( - `A sandbox name is required to recover the stopped ${previousRuntime?.kind ?? "unknown"} gateway runtime.`, - ); - } - return await this.host.nemoclaw([options.sandboxName, "recover"], { - artifactName: `lifecycle-gateway-recover-through-nemoclaw-recover-${options.sandboxName}`, - env: buildAvailabilityProbeEnv(), - timeoutMs: 120_000, - }); + const result = await this.host.command( + process.execPath, + ["-e", buildNemoClawGatewayRecoveryScript()], + { + artifactName: "lifecycle-gateway-recover-through-nemoclaw-product-start", + env: buildAvailabilityProbeEnv(), + timeoutMs: 120_000, + }, + ); + assertExitZero( + result, + `recover stopped ${previousRuntime?.kind ?? "unknown"} gateway through NemoClaw product start`, + ); + return result; } private async startOpenShellGatewayUserService(options: { @@ -794,7 +813,11 @@ export class LifecyclePhaseFixture { } async restartGatewayRuntime( - options: { delayMs?: number; requireUserService?: boolean; sandboxName?: string } = {}, + options: { + delayMs?: number; + requireUserService?: boolean; + sandboxName?: string; + } = {}, ): Promise { const previousRuntime = await this.stopGatewayRuntime(); if (this.gateway) { diff --git a/test/e2e/support/e2e-phase-lifecycle.test.ts b/test/e2e/support/e2e-phase-lifecycle.test.ts index 212a4b2956a..fa65f4aced4 100644 --- a/test/e2e/support/e2e-phase-lifecycle.test.ts +++ b/test/e2e/support/e2e-phase-lifecycle.test.ts @@ -156,11 +156,17 @@ describe("LifecyclePhaseFixture.trackInstallerGatewayUserService", () => { process.env.XDG_CONFIG_HOME = config; process.env.PATH = `${root}:${previousPath ?? ""}`; fs.mkdirSync(path.dirname(unit), { recursive: true }); - fs.writeFileSync(path.join(root, "systemctl"), "#!/bin/sh\nexit 0\n", { mode: 0o755 }); + fs.writeFileSync(path.join(root, "systemctl"), "#!/bin/sh\nexit 0\n", { + mode: 0o755, + }); runner = new FakeRunner(); cleanup = new FakeCleanup(); runner.run = async (command, options) => { - runner.calls.push({ command: command.command, args: [...command.args], options }); + runner.calls.push({ + command: command.command, + args: [...command.args], + options, + }); const result = spawnSync(command.command, ["-c", command.args[1]!], { env: options?.env, encoding: "utf8", @@ -527,7 +533,7 @@ describe("LifecyclePhaseFixture rebuild helpers", () => { }); describe("LifecyclePhaseFixture gateway runtime restart helpers", () => { - it("restores a PID gateway before sandbox recovery when the user service is inactive (#11640)", async () => { + it("restores a PID gateway through the product start path when the user service is inactive (#11640)", async () => { const runner = new FakeRunner(); runner.enqueue(shellResult(0, "12345\n")); // resolveHostRuntime pid probe runner.enqueue(shellResult(0)); // forward stop @@ -538,7 +544,7 @@ describe("LifecyclePhaseFixture gateway runtime restart helpers", () => { runner.enqueue(shellResult(1, "")); // expectHostRuntimeStopped pid probe runner.enqueue(shellResult(0, "")); // expectHostRuntimeStopped container probe runner.enqueue(shellResult(0)); // lifecycle-gateway-stopped true artifact - runner.enqueue(shellResult(0, "gateway recovered\n")); // start through sandbox recovery + runner.enqueue(shellResult(0, "gateway recovered\n")); // built product gateway start runner.enqueue(shellResult(0, "Connected to nemoclaw\n")); // waitForGatewayConnected const cleanup = new FakeCleanup(); const host = new HostCliClient(runner); @@ -560,7 +566,9 @@ describe("LifecyclePhaseFixture gateway runtime restart helpers", () => { expect.stringContaining("sh -lc pid_file="), "docker container ps --format {{.ID}}\t{{.Names}}", "true ", - "nemoclaw e2e-survival recover", + expect.stringContaining( + `${process.execPath} -e "use strict";\nconst { startGatewayForRecovery }`, + ), "openshell status", ]); }); @@ -678,7 +686,7 @@ describe("LifecyclePhaseFixture gateway runtime restart helpers", () => { expect(runner.calls).toHaveLength(2); }); - it("restores a container runtime through sandbox-specific recovery", async () => { + it("restores a container runtime through the built NemoClaw product start path", async () => { const runner = new FakeRunner(); runner.enqueue(shellResult(0, "gateway recovered\n")); const cleanup = new FakeCleanup(); @@ -690,9 +698,15 @@ describe("LifecyclePhaseFixture gateway runtime restart helpers", () => { ), ).resolves.toMatchObject({ exitCode: 0 }); - expect(runner.calls.map((call) => `${call.command} ${call.args.join(" ")}`)).toEqual([ - "nemoclaw e2e-survival recover", + expect(runner.calls).toHaveLength(1); + expect(runner.calls[0]?.command).toBe(process.execPath); + expect(runner.calls[0]?.args).toEqual([ + "-e", + expect.stringContaining('startGatewayForRecovery({ gatewayName: "nemoclaw" })'), ]); + expect(runner.calls[0]?.options?.artifactName).toBe( + "lifecycle-gateway-recover-through-nemoclaw-product-start", + ); }); it("restarts the stopped user service before sandbox recovery", async () => { @@ -714,13 +728,16 @@ describe("LifecyclePhaseFixture gateway runtime restart helpers", () => { ]); }); - it("fails closed without a sandbox recovery target when no user service owns the gateway", async () => { + it("fails closed when the built NemoClaw product start path cannot recover the gateway", async () => { + const runner = new FakeRunner(); + runner.enqueue(shellResult(1, "gateway recovery failed")); + await expect( - fixture(new FakeRunner(), new FakeCleanup()).startGatewayRuntime({ + fixture(runner, new FakeCleanup()).startGatewayRuntime({ kind: "pid", id: "12345", }), - ).rejects.toThrow(/sandbox name is required to recover the stopped pid gateway runtime/i); + ).rejects.toThrow(/recover stopped pid gateway.*gateway recovery failed/i); }); }); From b71d94d60e9ae53bc1717160c03b88399249b346 Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Sat, 12 Sep 2026 22:09:53 -0700 Subject: [PATCH 5/5] fix(e2e): preserve container gateway recovery owner --- test/e2e/fixtures/phases/lifecycle.ts | 19 +++++++++++ test/e2e/support/e2e-phase-lifecycle.test.ts | 33 +++++++++++++++----- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/test/e2e/fixtures/phases/lifecycle.ts b/test/e2e/fixtures/phases/lifecycle.ts index e9760d87073..672ac0682ce 100644 --- a/test/e2e/fixtures/phases/lifecycle.ts +++ b/test/e2e/fixtures/phases/lifecycle.ts @@ -185,6 +185,7 @@ export function buildOpenShellGatewayUserServiceDiagnosticsScript(): string { ].join("\n"); } +/** Build the host command that executes NemoClaw's production gateway-start path. */ export function buildNemoClawGatewayRecoveryScript(): string { return [ '"use strict";', @@ -745,6 +746,7 @@ export class LifecyclePhaseFixture { ); } + /** Restart the same gateway owner recorded before the lifecycle fixture stopped it. */ async startGatewayRuntime( previousRuntime: HostGatewayRuntime | null, options: { requireUserService?: boolean; sandboxName?: string } = {}, @@ -753,6 +755,23 @@ export class LifecyclePhaseFixture { requireAvailable: options.requireUserService, }); if (userServiceStart) return userServiceStart; + if (previousRuntime?.kind === "container") { + if (!options.sandboxName) { + throw new Error( + "A sandbox name is required to recover the stopped container gateway runtime.", + ); + } + const result = await this.host.nemoclaw([options.sandboxName, "status"], { + artifactName: `lifecycle-gateway-recover-through-nemoclaw-status-${options.sandboxName}`, + env: buildAvailabilityProbeEnv(), + timeoutMs: 120_000, + }); + assertExitZero( + result, + `recover stopped container gateway through sandbox status ${options.sandboxName}`, + ); + return result; + } const result = await this.host.command( process.execPath, ["-e", buildNemoClawGatewayRecoveryScript()], diff --git a/test/e2e/support/e2e-phase-lifecycle.test.ts b/test/e2e/support/e2e-phase-lifecycle.test.ts index fa65f4aced4..52f12ab8a92 100644 --- a/test/e2e/support/e2e-phase-lifecycle.test.ts +++ b/test/e2e/support/e2e-phase-lifecycle.test.ts @@ -686,7 +686,7 @@ describe("LifecyclePhaseFixture gateway runtime restart helpers", () => { expect(runner.calls).toHaveLength(2); }); - it("restores a container runtime through the built NemoClaw product start path", async () => { + it("restores a container runtime through its sandbox-specific recovery path", async () => { const runner = new FakeRunner(); runner.enqueue(shellResult(0, "gateway recovered\n")); const cleanup = new FakeCleanup(); @@ -698,17 +698,34 @@ describe("LifecyclePhaseFixture gateway runtime restart helpers", () => { ), ).resolves.toMatchObject({ exitCode: 0 }); - expect(runner.calls).toHaveLength(1); - expect(runner.calls[0]?.command).toBe(process.execPath); - expect(runner.calls[0]?.args).toEqual([ - "-e", - expect.stringContaining('startGatewayForRecovery({ gatewayName: "nemoclaw" })'), - ]); + expect(runner.calls[0]?.command).toBe("nemoclaw"); + expect(runner.calls[0]?.args).toEqual(["e2e-survival", "status"]); expect(runner.calls[0]?.options?.artifactName).toBe( - "lifecycle-gateway-recover-through-nemoclaw-product-start", + "lifecycle-gateway-recover-through-nemoclaw-status-e2e-survival", ); }); + it("fails closed when container recovery has no sandbox identity", async () => { + await expect( + fixture(new FakeRunner(), new FakeCleanup()).startGatewayRuntime({ + kind: "container", + id: "container-1", + }), + ).rejects.toThrow(/sandbox name is required.*container gateway runtime/i); + }); + + it("propagates a nonzero container recovery result", async () => { + const runner = new FakeRunner(); + runner.enqueue(shellResult(1, "sandbox recovery failed")); + + await expect( + fixture(runner, new FakeCleanup()).startGatewayRuntime( + { kind: "container", id: "container-1" }, + { sandboxName: "e2e-survival" }, + ), + ).rejects.toThrow(/recover stopped container gateway.*sandbox recovery failed/i); + }); + it("restarts the stopped user service before sandbox recovery", async () => { const runner = new FakeRunner(); const cleanup = new FakeCleanup();