Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/lib/actions/sandbox/process-recovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,43 @@ describe("recreated sandbox OpenShell readiness", () => {
expect(sleeps).toEqual([3]);
});

it("rides out a transient Error phase past the old 30s budget by default (#7227)", () => {
// No timeoutSeconds option and no env override: the default recovery budget
// must be large enough (120s, aligned with connect's readiness wait) to keep
// retrying a cold-start phase:Error settling window that exceeds the old
// 30s / 11-attempt budget. The 12th probe (past the old 11-attempt cap) must
// still be reached, so the primary dashboard/API forward is not abandoned.
delete process.env.NEMOCLAW_GATEWAY_RECOVERY_WAIT_SECONDS;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Restore the previous environment value.

This test mutates process.env directly and never restores the prior value, so later tests can become order-dependent. Save the value and restore it in a finally block.

Proposed fix
+    const previousRecoveryWait = process.env.NEMOCLAW_GATEWAY_RECOVERY_WAIT_SECONDS;
     delete process.env.NEMOCLAW_GATEWAY_RECOVERY_WAIT_SECONDS;
+    try {
       // existing assertions
+    } finally {
+      if (previousRecoveryWait === undefined) {
+        delete process.env.NEMOCLAW_GATEWAY_RECOVERY_WAIT_SECONDS;
+      } else {
+        process.env.NEMOCLAW_GATEWAY_RECOVERY_WAIT_SECONDS = previousRecoveryWait;
+      }
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/actions/sandbox/process-recovery.test.ts` at line 118, Update the
test setup around the NEMOCLAW_GATEWAY_RECOVERY_WAIT_SECONDS environment
variable to save its prior value, then restore that exact value in a finally
block after the test completes, including when assertions or setup fail.
Preserve the existing test behavior while preventing process.env state from
leaking into later tests.

Source: Path instructions

const errorPhase = {
status: 1,
output: OPENSHELL_TRANSIENT_ERROR_PHASE_STDERR.trim(),
stdout: "",
stderr: OPENSHELL_TRANSIENT_ERROR_PHASE_STDERR,
};
const captureOpenshellImpl = vi.fn();
for (let attempt = 0; attempt < 11; attempt += 1) {
captureOpenshellImpl.mockReturnValueOnce(errorPhase);
}
captureOpenshellImpl.mockReturnValueOnce({
status: 0,
output: "",
stdout: "",
stderr: "",
});

expect(
waitForRecreatedSandboxOpenShellReady("recreated-box", {
beforeProbe: () => true,
captureOpenshellImpl,
intervalSeconds: 3,
sleepImpl: () => {},
// no timeoutSeconds -> exercise the default budget; the old 30s default
// capped at 11 attempts and would have given up before the 12th probe.
}),
).toBe(true);
expect(captureOpenshellImpl).toHaveBeenCalledTimes(12);
});

it("retries the exact supervisor reconnect states exposed during direct recreation", () => {
const reconnecting = [
OPENSHELL_SUPERVISOR_NOT_CONNECTED_STDERR,
Expand Down
17 changes: 15 additions & 2 deletions src/lib/actions/sandbox/process-recovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,16 @@ function recreatedSandboxOpenShellReadinessFailureDetail(
return openshellError ? `${detail} Last OpenShell readiness error: ${openshellError}` : detail;
}

// Default seconds to wait for OpenShell to re-register a recreated sandbox as
// Ready before giving up and surfacing the manual-recover hint. Aligned with
// `connect`'s readiness budget (`waitForSandboxReadyOrExit` defaults to 120s):
// both prove the same post-recreate sandbox readiness, but this path used to
// give up 4x sooner (30s), so a cold-start `phase: Error` settling window that
// exceeded 30s but was within `connect`'s 120s left the primary dashboard/API
// forward unstarted — exactly why `connect --probe-only` recovers what `start`
// abandons (#7227). Env-tunable via NEMOCLAW_GATEWAY_RECOVERY_WAIT_SECONDS.
const GATEWAY_RECOVERY_WAIT_DEFAULT_SECONDS = 120;

/**
* Wait until OpenShell has re-registered a directly recreated sandbox as
* ready. This probe deliberately has no direct-Docker or SSH fallback: it is
Expand All @@ -718,7 +728,10 @@ function waitForRecreatedSandboxOpenShellReadyResult(
Number.isFinite(options.timeoutSeconds) &&
options.timeoutSeconds >= 0
? options.timeoutSeconds
: readNonNegativeNumberEnv("NEMOCLAW_GATEWAY_RECOVERY_WAIT_SECONDS", 30);
: readNonNegativeNumberEnv(
"NEMOCLAW_GATEWAY_RECOVERY_WAIT_SECONDS",
GATEWAY_RECOVERY_WAIT_DEFAULT_SECONDS,
);
const intervalSeconds = readNonNegativeNumberEnv(
"NEMOCLAW_GATEWAY_RECOVERY_POLL_INTERVAL_SECONDS",
options.intervalSeconds ?? 3,
Expand Down Expand Up @@ -914,7 +927,7 @@ export function waitForRecoveredSandboxGateway(
Number.isFinite(options.timeoutSeconds) &&
options.timeoutSeconds >= 0
? options.timeoutSeconds
: 30;
: GATEWAY_RECOVERY_WAIT_DEFAULT_SECONDS;
const timeoutSeconds = readNonNegativeNumberEnv(
"NEMOCLAW_GATEWAY_RECOVERY_WAIT_SECONDS",
requestedTimeoutSeconds,
Expand Down
Loading