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
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
shouldAutoStart,
shouldSelfHeal,
computeDrawerStatus,
buildSandboxStartArgs,
type BranchMapEntryLike,
} from "./sandbox-lifecycle-context";

Expand Down Expand Up @@ -130,6 +131,26 @@ describe("shouldSelfHeal", () => {
});
});

describe("buildSandboxStartArgs", () => {
test("includes sandboxProviderKind when the thread has a locked kind", () => {
// Regression: user-driven start/retry/resume used to omit
// sandboxProviderKind (unlike auto-start/self-heal), so retrying a
// failed locked-provider thread could get provisioned on the wrong
// provider instead of the one the preview is locked to.
expect(buildSandboxStartArgs("vmcp1", "main", "user-desktop")).toEqual({
virtualMcpId: "vmcp1",
branch: "main",
sandboxProviderKind: "user-desktop",
});
});

test("omits branch and sandboxProviderKind when absent", () => {
expect(buildSandboxStartArgs("vmcp1", null, null)).toEqual({
virtualMcpId: "vmcp1",
});
});
});

describe("computeDrawerStatus", () => {
test("suspended → suspended", () => {
expect(computeDrawerStatus({ kind: "suspended" })).toBe("suspended");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,21 @@ export function shouldSelfHeal(args: ShouldSelfHealArgs): boolean {
);
}

/** Shared SANDBOX_START arg-builder so every call site (auto-start,
* self-heal, user-driven start/retry/resume) scopes provisioning to the
* locked provider kind identically — a caller that omits it can get
* provisioned on the wrong provider. */
export function buildSandboxStartArgs(
virtualMcpId: string,
branch: string | null,
sandboxProviderKind: SandboxProviderKind | null,
): SandboxStartArgs {
const args: SandboxStartArgs = { virtualMcpId };
if (branch) args.branch = branch;
if (sandboxProviderKind) args.sandboxProviderKind = sandboxProviderKind;
return args;
}

export function computeDrawerStatus(state: PreviewState): DrawerStatus {
switch (state.kind) {
case "suspended":
Expand Down Expand Up @@ -239,9 +254,11 @@ export function SandboxLifecycleProvider({
useEffect(() => {
if (!autoStartEligible || !virtualMcpId) return;
autoStartAttemptedForBranchRef.current.add(autoStartDedupKey);
const args: SandboxStartArgs = { virtualMcpId };
if (branch) args.branch = branch;
if (sandboxProviderKind) args.sandboxProviderKind = sandboxProviderKind;
const args = buildSandboxStartArgs(
virtualMcpId,
branch,
sandboxProviderKind,
);
startVmMutate(args, {
onSuccess: (data) => {
if (data?.branch && !branch) setCurrentTaskBranch(data.branch);
Expand Down Expand Up @@ -276,9 +293,11 @@ export function SandboxLifecycleProvider({
if (!selfHealEligible || !deadVmId || !virtualMcpId) return;
// oxlint-disable-next-line ban-ref-current-assignment/ban-ref-current-assignment -- record dead handle to dedup repeat 404s
reprovisionedForVmIdRef.current = deadVmId;
const args: SandboxStartArgs = { virtualMcpId };
if (branch) args.branch = branch;
if (sandboxProviderKind) args.sandboxProviderKind = sandboxProviderKind;
const args = buildSandboxStartArgs(
virtualMcpId,
branch,
sandboxProviderKind,
);
startVmMutate(args, {
onSuccess: (data) => {
if (data?.branch && !branch) setCurrentTaskBranch(data.branch);
Expand All @@ -300,8 +319,11 @@ export function SandboxLifecycleProvider({
// User-driven actions.
const start = () => {
if (!virtualMcpId) return;
const args: SandboxStartArgs = { virtualMcpId };
if (branch) args.branch = branch;
const args = buildSandboxStartArgs(
virtualMcpId,
branch,
sandboxProviderKind,
);
startVmMutate(args, {
onSuccess: (data) => {
if (data?.branch && !branch) setCurrentTaskBranch(data.branch);
Expand Down
Loading