diff --git a/src/lib/upgrade-sandboxes-action.ts b/src/lib/upgrade-sandboxes-action.ts index 80868205170..9db9fb4782d 100644 --- a/src/lib/upgrade-sandboxes-action.ts +++ b/src/lib/upgrade-sandboxes-action.ts @@ -15,6 +15,11 @@ import { parseLiveSandboxNames } from "./runtime-recovery"; import { rebuildSandbox } from "./sandbox-rebuild-action"; import * as sandboxVersion from "./sandbox-version"; import { B, D, G, R, YW } from "./terminal-style"; +import { + classifyUpgradeableSandboxes, + shouldSkipUpgradeConfirmation, + splitRebuildableSandboxes, +} from "./upgrade-sandboxes-helpers"; // ── Upgrade sandboxes (#1904) ──────────────────────────────────── // Detect sandboxes running stale agent versions and offer to rebuild them. @@ -24,8 +29,7 @@ export async function upgradeSandboxes( ): Promise { const normalized = normalizeUpgradeSandboxesOptions(options); const checkOnly = normalized.check === true; - const auto = normalized.auto === true; - const skipConfirm = auto || normalized.yes === true; + const skipConfirm = shouldSkipUpgradeConfirmation(normalized); const sandboxes = registry.listSandboxes().sandboxes; if (sandboxes.length === 0) { @@ -43,25 +47,11 @@ export async function upgradeSandboxes( const liveNames = parseLiveSandboxNames(liveResult.output || ""); // Classify sandboxes as stale, unknown, or current - const stale = []; - const unknown = []; - for (const sb of sandboxes) { - const versionCheck = sandboxVersion.checkAgentVersion(sb.name); - if (versionCheck.isStale) { - stale.push({ - name: sb.name, - current: versionCheck.sandboxVersion, - expected: versionCheck.expectedVersion, - running: liveNames.has(sb.name), - }); - } else if (versionCheck.detectionMethod === "unavailable") { - unknown.push({ - name: sb.name, - expected: versionCheck.expectedVersion, - running: liveNames.has(sb.name), - }); - } - } + const { stale, unknown } = classifyUpgradeableSandboxes( + sandboxes, + liveNames, + sandboxVersion.checkAgentVersion, + ); if (stale.length === 0 && unknown.length === 0) { console.log(" All sandboxes are up to date."); @@ -95,8 +85,7 @@ export async function upgradeSandboxes( return; } - const rebuildable = stale.filter((s: { running: boolean }) => s.running); - const stopped = stale.filter((s: { running: boolean }) => !s.running); + const { rebuildable, stopped } = splitRebuildableSandboxes(stale); if (stopped.length > 0) { console.log(` ${D}Skipping ${stopped.length} stopped sandbox(es) — start them first.${R}`); } diff --git a/src/lib/upgrade-sandboxes-helpers.test.ts b/src/lib/upgrade-sandboxes-helpers.test.ts new file mode 100644 index 00000000000..649d8a6b3ca --- /dev/null +++ b/src/lib/upgrade-sandboxes-helpers.test.ts @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it } from "vitest"; + +import { + classifyUpgradeableSandboxes, + shouldSkipUpgradeConfirmation, + splitRebuildableSandboxes, + type SandboxVersionCheck, +} from "./upgrade-sandboxes-helpers"; + +describe("upgrade sandboxes helpers", () => { + it("detects upgrade confirmation bypass modes", () => { + expect(shouldSkipUpgradeConfirmation({ auto: true })).toBe(true); + expect(shouldSkipUpgradeConfirmation({ yes: true })).toBe(true); + expect(shouldSkipUpgradeConfirmation({ check: true })).toBe(false); + }); + + it("classifies stale and unknown sandboxes with running state", () => { + const checks: Record = { + staleRunning: { isStale: true, sandboxVersion: "1.0.0", expectedVersion: "2.0.0" }, + staleStopped: { isStale: true, sandboxVersion: null, expectedVersion: "2.0.0" }, + unknown: { isStale: false, detectionMethod: "unavailable", expectedVersion: "2.0.0" }, + current: { isStale: false, sandboxVersion: "2.0.0", expectedVersion: "2.0.0" }, + }; + + expect( + classifyUpgradeableSandboxes( + [ + { name: "staleRunning" }, + { name: "staleStopped" }, + { name: "unknown" }, + { name: "current" }, + ], + new Set(["staleRunning", "unknown"]), + (name) => checks[name], + ), + ).toEqual({ + stale: [ + { name: "staleRunning", current: "1.0.0", expected: "2.0.0", running: true }, + { name: "staleStopped", current: null, expected: "2.0.0", running: false }, + ], + unknown: [{ name: "unknown", expected: "2.0.0", running: true }], + }); + }); + + it("splits stale sandboxes into rebuildable and stopped groups", () => { + expect( + splitRebuildableSandboxes([ + { name: "alpha", running: true, expected: "2.0.0" }, + { name: "beta", running: false, expected: "2.0.0" }, + ]), + ).toEqual({ + rebuildable: [{ name: "alpha", running: true, expected: "2.0.0" }], + stopped: [{ name: "beta", running: false, expected: "2.0.0" }], + }); + }); +}); diff --git a/src/lib/upgrade-sandboxes-helpers.ts b/src/lib/upgrade-sandboxes-helpers.ts new file mode 100644 index 00000000000..07b8d93c78e --- /dev/null +++ b/src/lib/upgrade-sandboxes-helpers.ts @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import type { UpgradeSandboxesOptions } from "./lifecycle-options"; + +export type SandboxVersionCheck = { + isStale: boolean; + sandboxVersion?: string | null; + expectedVersion?: string | null; + detectionMethod?: string | null; +}; + +export type UpgradeSandboxCandidate = { + name: string; + current?: string | null; + expected?: string | null; + running: boolean; +}; + +export type UpgradeClassification = { + stale: UpgradeSandboxCandidate[]; + unknown: UpgradeSandboxCandidate[]; +}; + +export function shouldSkipUpgradeConfirmation(options: UpgradeSandboxesOptions): boolean { + return options.auto === true || options.yes === true; +} + +export function classifyUpgradeableSandboxes( + sandboxes: Array<{ name: string }>, + liveNames: ReadonlySet, + checkVersion: (name: string) => SandboxVersionCheck, +): UpgradeClassification { + const stale: UpgradeSandboxCandidate[] = []; + const unknown: UpgradeSandboxCandidate[] = []; + for (const sandbox of sandboxes) { + const versionCheck = checkVersion(sandbox.name); + if (versionCheck.isStale) { + stale.push({ + name: sandbox.name, + current: versionCheck.sandboxVersion, + expected: versionCheck.expectedVersion, + running: liveNames.has(sandbox.name), + }); + } else if (versionCheck.detectionMethod === "unavailable") { + unknown.push({ + name: sandbox.name, + expected: versionCheck.expectedVersion, + running: liveNames.has(sandbox.name), + }); + } + } + return { stale, unknown }; +} + +export function splitRebuildableSandboxes(stale: UpgradeSandboxCandidate[]): { + rebuildable: UpgradeSandboxCandidate[]; + stopped: UpgradeSandboxCandidate[]; +} { + const rebuildable: UpgradeSandboxCandidate[] = []; + const stopped: UpgradeSandboxCandidate[] = []; + for (const sandbox of stale) { + if (sandbox.running) { + rebuildable.push(sandbox); + } else { + stopped.push(sandbox); + } + } + return { rebuildable, stopped }; +}