Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
f8a9a58
refactor(onboard): lower cognitive complexity ratchet to 245
cv Jun 14, 2026
65d7578
refactor(rebuild): lower cognitive complexity ratchet to 244
cv Jun 14, 2026
433ba8a
refactor(rebuild): lower cognitive complexity ratchet to 243
cv Jun 14, 2026
30e2b10
refactor(onboard): move setupNim validation helpers
cv Jun 14, 2026
bd07191
Merge branch 'ratchet/245-setup-nim-cognitive-complexity' into ratche…
cv Jun 14, 2026
fda1c05
Merge branch 'ratchet/244-rebuild-cognitive-complexity' into ratchet/…
cv Jun 14, 2026
cdc58d7
test(rebuild): cover interactive confirmation
cv Jun 14, 2026
7778b05
Merge branch 'main' into ratchet/245-setup-nim-cognitive-complexity
cv Jun 15, 2026
b451a5b
fix(onboard): preserve local NIM fallback state
cv Jun 15, 2026
9e3ddf5
Merge branch 'ratchet/245-setup-nim-cognitive-complexity' into ratche…
cv Jun 15, 2026
08743b0
Merge branch 'ratchet/244-rebuild-cognitive-complexity' into ratchet/…
cv Jun 15, 2026
ed3170a
test(onboard): cover setupNim selection validator state
cv Jun 15, 2026
4ef8d6f
Merge branch 'ratchet/245-setup-nim-cognitive-complexity' into ratche…
cv Jun 15, 2026
3df8744
Merge branch 'ratchet/244-rebuild-cognitive-complexity' into ratchet/…
cv Jun 15, 2026
9194819
Merge remote-tracking branch 'origin/main' into ratchet/244-rebuild-c…
cv Jun 15, 2026
00724d3
Merge branch 'ratchet/244-rebuild-cognitive-complexity' into ratchet/…
cv Jun 15, 2026
ebe72ba
test(onboard): align sandbox registration fixture
cv Jun 15, 2026
a23904d
Merge branch 'ratchet/244-rebuild-cognitive-complexity' into ratchet/…
cv Jun 15, 2026
cb69607
test(rebuild): cover active session warning
cv Jun 15, 2026
86b83a0
Merge branch 'ratchet/244-rebuild-cognitive-complexity' into ratchet/…
cv Jun 15, 2026
94402c3
Merge remote-tracking branch 'origin/main' into ratchet/243-rebuild-c…
cv Jun 15, 2026
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
2 changes: 1 addition & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@
"noExcessiveCognitiveComplexity": {
"level": "error",
"options": {
"maxAllowedComplexity": 244
"maxAllowedComplexity": 243
}
}
},
Expand Down
58 changes: 34 additions & 24 deletions src/lib/actions/sandbox/rebuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,35 @@ function countActiveSandboxSessionsForRebuild(sandboxName: string): number {
}
}

async function confirmSandboxRebuildIfNeeded(
skipConfirm: boolean,
rebuildActiveSessionCount: number,
): Promise<boolean> {
if (skipConfirm) return true;

if (rebuildActiveSessionCount > 0) {
const plural = rebuildActiveSessionCount > 1 ? "sessions" : "session";
console.log(
` ${YW}⚠ Active SSH ${plural} detected (${rebuildActiveSessionCount} connection${rebuildActiveSessionCount > 1 ? "s" : ""})${R}`,
);
console.log(
` Rebuilding will terminate ${rebuildActiveSessionCount === 1 ? "the" : "all"} active ${plural} with a Broken pipe error.`,
);
console.log("");
}
console.log(" This will:");
console.log(" 1. Back up workspace state");
console.log(" 2. Destroy and recreate the sandbox with the current image");
console.log(" 3. Restore workspace state into the new sandbox");
console.log("");
const answer = await askPrompt(" Proceed? [y/N]: ");
if (answer.trim().toLowerCase() !== "y" && answer.trim().toLowerCase() !== "yes") {
console.log(" Cancelled.");
return false;
}
return true;
}

async function reapplyMessagingManifestAfterOpenClawDoctor(
sandboxName: string,
plan: SandboxMessagingPlan | null,
Expand Down Expand Up @@ -424,30 +453,11 @@ export async function rebuildSandbox(
}
console.log("");

let rebuildConfirmed = false;
if (!skipConfirm) {
if (rebuildActiveSessionCount > 0) {
const plural = rebuildActiveSessionCount > 1 ? "sessions" : "session";
console.log(
` ${YW}⚠ Active SSH ${plural} detected (${rebuildActiveSessionCount} connection${rebuildActiveSessionCount > 1 ? "s" : ""})${R}`,
);
console.log(
` Rebuilding will terminate ${rebuildActiveSessionCount === 1 ? "the" : "all"} active ${plural} with a Broken pipe error.`,
);
console.log("");
}
console.log(" This will:");
console.log(" 1. Back up workspace state");
console.log(" 2. Destroy and recreate the sandbox with the current image");
console.log(" 3. Restore workspace state into the new sandbox");
console.log("");
const answer = await askPrompt(" Proceed? [y/N]: ");
if (answer.trim().toLowerCase() !== "y" && answer.trim().toLowerCase() !== "yes") {
console.log(" Cancelled.");
return;
}
rebuildConfirmed = true;
}
const rebuildConfirmed = await confirmSandboxRebuildIfNeeded(
skipConfirm,
rebuildActiveSessionCount,
);
if (!rebuildConfirmed) return;

// Step 0: Preflight — verify recreate preconditions BEFORE destroying
// anything. The most common rebuild failure is a missing provider
Expand Down
39 changes: 39 additions & 0 deletions test/rebuild-credential-preflight.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,45 @@ function registryHasSandbox(fixture: ReturnType<typeof createFixture>): boolean

describe("Issue #2273: atomic rebuild", () => {
describe("Layer 2: preflight credential check", () => {
it("cancels interactive rebuild before credential preflight or backup on non-affirmative input", {
timeout: 60_000,
}, () => {
const f = createFixture({
credentialEnv: "NVIDIA_INFERENCE_API_KEY",
providerRegistered: false,
});

const result = runRebuild(f, {}, { yes: false, input: "n\n" });
const output = (result.stderr || "") + (result.stdout || "");

expect(result.status).toBe(0);
expect(output).toContain("Proceed? [y/N]:");
expect(output).toContain("Cancelled.");
expect(output).not.toContain("preflight failed");
expect(output).not.toContain("Backing up sandbox state");
expect(registryHasSandbox(f)).toBe(true);
});

it("accepts trimmed case-insensitive yes input before continuing rebuild", {
timeout: 60_000,
}, () => {
const f = createFixture({
credentialEnv: "NVIDIA_INFERENCE_API_KEY",
savedCredential: {
key: "NVIDIA_INFERENCE_API_KEY",
value: "nvapi-test-key-for-rebuild",
},
});

const result = runRebuild(f, {}, { yes: false, input: " YES \n" });
const output = (result.stderr || "") + (result.stdout || "");

expect(output).toContain("Proceed? [y/N]:");
expect(output).not.toContain("Cancelled.");
expect(output).not.toContain("preflight failed");
expect(output).toContain("Backing up sandbox state");
});

it("prints active SSH session warning before interactive confirmation", {
timeout: 60_000,
}, () => {
Expand Down