|
| 1 | +//go:build e2e |
| 2 | + |
| 3 | +package e2e |
| 4 | + |
| 5 | +import ( |
| 6 | + "context" |
| 7 | + "fmt" |
| 8 | + "testing" |
| 9 | +) |
| 10 | + |
| 11 | +func TestLifecycle(t *testing.T) { |
| 12 | + // Step 1: Create sandbox and wait until running. |
| 13 | + sb := newSandbox(t) |
| 14 | + if sb.Status != "running" { |
| 15 | + t.Fatalf("expected status 'running' after create, got %q", sb.Status) |
| 16 | + } |
| 17 | + |
| 18 | + // Step 2: Get — verify the sandbox is reachable and fields match. |
| 19 | + getCtx, getCancel := context.WithTimeout(context.Background(), defaultTimeout) |
| 20 | + defer getCancel() |
| 21 | + getOut, getErr, getCode := runCLICtx(getCtx, "sandbox", "get", "-o", "json", sb.ID) |
| 22 | + if getCode != 0 { |
| 23 | + t.Fatalf("sandbox get failed (exit %d)\nstdout: %s\nstderr: %s", getCode, getOut, getErr) |
| 24 | + } |
| 25 | + got := mustJSON[SandboxView](t, getOut) |
| 26 | + if got.ID != sb.ID { |
| 27 | + t.Errorf("get: ID mismatch: want %q, got %q", sb.ID, got.ID) |
| 28 | + } |
| 29 | + if testShape != "" && got.Shape != testShape { |
| 30 | + t.Errorf("get: Shape mismatch: want %q, got %q", testShape, got.Shape) |
| 31 | + } |
| 32 | + if got.Status != "running" { |
| 33 | + t.Errorf("get: expected status 'running', got %q", got.Status) |
| 34 | + } |
| 35 | + |
| 36 | + // Step 3: Edit — set auto-pause to 30m (1800 seconds). |
| 37 | + editCtx, editCancel := context.WithTimeout(context.Background(), defaultTimeout) |
| 38 | + defer editCancel() |
| 39 | + editOut, editErr, editCode := runCLICtx(editCtx, "sandbox", "edit", sb.ID, "--auto-pause", "30m", "-o", "json") |
| 40 | + if editCode != 0 { |
| 41 | + t.Fatalf("sandbox edit failed (exit %d)\nstdout: %s\nstderr: %s", editCode, editOut, editErr) |
| 42 | + } |
| 43 | + edited := mustJSON[SandboxView](t, editOut) |
| 44 | + if edited.AutoPauseAfterSeconds == nil { |
| 45 | + t.Fatalf("edit: AutoPauseAfterSeconds is nil; expected 1800") |
| 46 | + } |
| 47 | + if *edited.AutoPauseAfterSeconds != 1800 { |
| 48 | + t.Errorf("edit: AutoPauseAfterSeconds: want 1800, got %d", *edited.AutoPauseAfterSeconds) |
| 49 | + } |
| 50 | + |
| 51 | + // Step 4: Pause — command polls internally and renders JSON when not on TTY. |
| 52 | + pauseCtx, pauseCancel := context.WithTimeout(context.Background(), createTimeout) |
| 53 | + defer pauseCancel() |
| 54 | + pauseOut, pauseErr, pauseCode := runCLICtx(pauseCtx, "sandbox", "pause", sb.ID, "-o", "json") |
| 55 | + if pauseCode != 0 { |
| 56 | + t.Fatalf("sandbox pause failed (exit %d)\nstdout: %s\nstderr: %s", pauseCode, pauseOut, pauseErr) |
| 57 | + } |
| 58 | + paused := mustJSON[SandboxView](t, pauseOut) |
| 59 | + if paused.Status != "paused" { |
| 60 | + t.Errorf("pause: expected status 'paused', got %q", paused.Status) |
| 61 | + } |
| 62 | + |
| 63 | + // Step 5: Resume — command polls internally and renders JSON when not on TTY. |
| 64 | + resumeCtx, resumeCancel := context.WithTimeout(context.Background(), createTimeout) |
| 65 | + defer resumeCancel() |
| 66 | + resumeOut, resumeErr, resumeCode := runCLICtx(resumeCtx, "sandbox", "resume", sb.ID, "-o", "json") |
| 67 | + if resumeCode != 0 { |
| 68 | + t.Fatalf("sandbox resume failed (exit %d)\nstdout: %s\nstderr: %s", resumeCode, resumeOut, resumeErr) |
| 69 | + } |
| 70 | + resumed := mustJSON[SandboxView](t, resumeOut) |
| 71 | + if resumed.Status != "running" { |
| 72 | + t.Errorf("resume: expected status 'running', got %q", resumed.Status) |
| 73 | + } |
| 74 | + |
| 75 | + // Step 6: Fork — command polls until running (default) and renders JSON. |
| 76 | + // Note: sandbox/fork does not expose a --name flag; the server assigns a name. |
| 77 | + forkName := fmt.Sprintf("e2e-%s-fork", runID) |
| 78 | + _ = forkName // no --name flag; kept for tracing only |
| 79 | + forkCtx, forkCancel := context.WithTimeout(context.Background(), createTimeout) |
| 80 | + defer forkCancel() |
| 81 | + forkOut, forkErr, forkCode := runCLICtx(forkCtx, "sandbox", "fork", sb.ID, "-o", "json") |
| 82 | + if forkCode != 0 { |
| 83 | + t.Fatalf("sandbox fork failed (exit %d)\nstdout: %s\nstderr: %s", forkCode, forkOut, forkErr) |
| 84 | + } |
| 85 | + forked := mustJSON[SandboxView](t, forkOut) |
| 86 | + if forked.ID == sb.ID { |
| 87 | + t.Errorf("fork: expected new sandbox ID, got same ID %q", forked.ID) |
| 88 | + } |
| 89 | + // Register cleanup for the forked sandbox. |
| 90 | + t.Cleanup(func() { |
| 91 | + runCLI("sandbox", "rm", "--force", forked.ID) //nolint:errcheck |
| 92 | + }) |
| 93 | + // Fork auto-resumes by default; wait to confirm it is running. |
| 94 | + waitRunning(t, forked.ID) |
| 95 | + |
| 96 | + // Step 7: Rm — explicitly delete the original sandbox. |
| 97 | + rmCtx, rmCancel := context.WithTimeout(context.Background(), defaultTimeout) |
| 98 | + defer rmCancel() |
| 99 | + rmOut, rmErr, rmCode := runCLICtx(rmCtx, "sandbox", "rm", "--force", sb.ID) |
| 100 | + if rmCode != 0 { |
| 101 | + t.Fatalf("sandbox rm failed (exit %d)\nstdout: %s\nstderr: %s", rmCode, rmOut, rmErr) |
| 102 | + } |
| 103 | + // Verify removal: get should fail with a non-zero exit (404). |
| 104 | + verCtx, verCancel := context.WithTimeout(context.Background(), defaultTimeout) |
| 105 | + defer verCancel() |
| 106 | + _, _, verCode := runCLICtx(verCtx, "sandbox", "get", "-o", "json", sb.ID) |
| 107 | + if verCode == 0 { |
| 108 | + t.Errorf("expected non-zero exit after rm, but sandbox get succeeded for %q", sb.ID) |
| 109 | + } |
| 110 | +} |
0 commit comments