diff --git a/CHANGELOG.md b/CHANGELOG.md index f48820a..8bed081 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,43 +2,39 @@ ## [2.2.0](https://github.com/codesandbox/codesandbox-sdk/compare/v2.1.0...v2.2.0) (2025-09-02) - ### Features -* Add traceparent for all requests to API ([#180](https://github.com/codesandbox/codesandbox-sdk/issues/180)) ([b6f4846](https://github.com/codesandbox/codesandbox-sdk/commit/b6f484665de0bf0533127e098ff0ef1aa641a84b)) -* batch writes ([#175](https://github.com/codesandbox/codesandbox-sdk/issues/175)) ([493c5d5](https://github.com/codesandbox/codesandbox-sdk/commit/493c5d52d1eaa527d099b0270a5b0e78a694abbd)) - +- Add traceparent for all requests to API ([#180](https://github.com/codesandbox/codesandbox-sdk/issues/180)) ([b6f4846](https://github.com/codesandbox/codesandbox-sdk/commit/b6f484665de0bf0533127e098ff0ef1aa641a84b)) +- batch writes ([#175](https://github.com/codesandbox/codesandbox-sdk/issues/175)) ([493c5d5](https://github.com/codesandbox/codesandbox-sdk/commit/493c5d52d1eaa527d099b0270a5b0e78a694abbd)) ### Bug Fixes -* ensure private preview on private sandbox ([#179](https://github.com/codesandbox/codesandbox-sdk/issues/179)) ([04381a0](https://github.com/codesandbox/codesandbox-sdk/commit/04381a071fb54aa385ad40ed7ff6d489945565ef)) -* prevent api config overrides ([#177](https://github.com/codesandbox/codesandbox-sdk/issues/177)) ([a9ec1a7](https://github.com/codesandbox/codesandbox-sdk/commit/a9ec1a78c2c83a53dfd0649dfa1764589b9fa671)) -* Queue messages on lost connection and reconnect also on hibernation ([#176](https://github.com/codesandbox/codesandbox-sdk/issues/176)) ([c5a8ffd](https://github.com/codesandbox/codesandbox-sdk/commit/c5a8ffdf4bcba321c4d3c9581f752c5e72a3bc8f)) +- ensure private preview on private sandbox ([#179](https://github.com/codesandbox/codesandbox-sdk/issues/179)) ([04381a0](https://github.com/codesandbox/codesandbox-sdk/commit/04381a071fb54aa385ad40ed7ff6d489945565ef)) +- prevent api config overrides ([#177](https://github.com/codesandbox/codesandbox-sdk/issues/177)) ([a9ec1a7](https://github.com/codesandbox/codesandbox-sdk/commit/a9ec1a78c2c83a53dfd0649dfa1764589b9fa671)) +- Queue messages on lost connection and reconnect also on hibernation ([#176](https://github.com/codesandbox/codesandbox-sdk/issues/176)) ([c5a8ffd](https://github.com/codesandbox/codesandbox-sdk/commit/c5a8ffdf4bcba321c4d3c9581f752c5e72a3bc8f)) ## [2.1.0](https://github.com/codesandbox/codesandbox-sdk/compare/v2.0.7...v2.1.0) (2025-08-22) - ### Features -* add fetching single sandbox ([#142](https://github.com/codesandbox/codesandbox-sdk/issues/142)) ([2f58d43](https://github.com/codesandbox/codesandbox-sdk/commit/2f58d43ee44c98eeb7d0e917c8b423a80d202585)) -* add listRunning method to sandboxes namespace ([#145](https://github.com/codesandbox/codesandbox-sdk/issues/145)) ([6050dbd](https://github.com/codesandbox/codesandbox-sdk/commit/6050dbd289058c782e634a7ce9e25d9cf5921276)) -* add open telemetry for sandboxes methods ([#147](https://github.com/codesandbox/codesandbox-sdk/issues/147)) ([b331315](https://github.com/codesandbox/codesandbox-sdk/commit/b3313153b357dfda0d666a6a56ea79f6aa3dbbdf)) -* add tracing to Sandbox and SandboxClient, also allow passing to browser and node connectors ([#150](https://github.com/codesandbox/codesandbox-sdk/issues/150)) ([6ef2bf5](https://github.com/codesandbox/codesandbox-sdk/commit/6ef2bf51120068e35ff43607e3353a69d8fbf070)) -* Debug Sandboxes through CLI ([#163](https://github.com/codesandbox/codesandbox-sdk/issues/163)) ([9af1cdd](https://github.com/codesandbox/codesandbox-sdk/commit/9af1cdd0657c1a74572dc473ac6e04f6e1a40cd5)) -* enhance container setup logging in build command ([836a7a6](https://github.com/codesandbox/codesandbox-sdk/commit/836a7a6ed1dc7c73d737e04475083faf8d6d8fc4)) -* enhance container setup logging in build command ([a6f9fe7](https://github.com/codesandbox/codesandbox-sdk/commit/a6f9fe7c93450cfeded21048f62a6a2f0b842091)) -* private sandbox, public hosts with public-hosts privacy ([#154](https://github.com/codesandbox/codesandbox-sdk/issues/154)) ([dce7caf](https://github.com/codesandbox/codesandbox-sdk/commit/dce7cafd719e398b1f1421776bd55fa5601eccd0)) - +- add fetching single sandbox ([#142](https://github.com/codesandbox/codesandbox-sdk/issues/142)) ([2f58d43](https://github.com/codesandbox/codesandbox-sdk/commit/2f58d43ee44c98eeb7d0e917c8b423a80d202585)) +- add listRunning method to sandboxes namespace ([#145](https://github.com/codesandbox/codesandbox-sdk/issues/145)) ([6050dbd](https://github.com/codesandbox/codesandbox-sdk/commit/6050dbd289058c782e634a7ce9e25d9cf5921276)) +- add open telemetry for sandboxes methods ([#147](https://github.com/codesandbox/codesandbox-sdk/issues/147)) ([b331315](https://github.com/codesandbox/codesandbox-sdk/commit/b3313153b357dfda0d666a6a56ea79f6aa3dbbdf)) +- add tracing to Sandbox and SandboxClient, also allow passing to browser and node connectors ([#150](https://github.com/codesandbox/codesandbox-sdk/issues/150)) ([6ef2bf5](https://github.com/codesandbox/codesandbox-sdk/commit/6ef2bf51120068e35ff43607e3353a69d8fbf070)) +- Debug Sandboxes through CLI ([#163](https://github.com/codesandbox/codesandbox-sdk/issues/163)) ([9af1cdd](https://github.com/codesandbox/codesandbox-sdk/commit/9af1cdd0657c1a74572dc473ac6e04f6e1a40cd5)) +- enhance container setup logging in build command ([836a7a6](https://github.com/codesandbox/codesandbox-sdk/commit/836a7a6ed1dc7c73d737e04475083faf8d6d8fc4)) +- enhance container setup logging in build command ([a6f9fe7](https://github.com/codesandbox/codesandbox-sdk/commit/a6f9fe7c93450cfeded21048f62a6a2f0b842091)) +- private sandbox, public hosts with public-hosts privacy ([#154](https://github.com/codesandbox/codesandbox-sdk/issues/154)) ([dce7caf](https://github.com/codesandbox/codesandbox-sdk/commit/dce7cafd719e398b1f1421776bd55fa5601eccd0)) ### Bug Fixes -* Add custom retry delay support for startVM API calls ([#156](https://github.com/codesandbox/codesandbox-sdk/issues/156)) ([ce3a282](https://github.com/codesandbox/codesandbox-sdk/commit/ce3a2823e66198f453d257a68757226d50e3bf17)) -* Decouple pitcher-client ([#148](https://github.com/codesandbox/codesandbox-sdk/issues/148)) ([3a6f9ea](https://github.com/codesandbox/codesandbox-sdk/commit/3a6f9ea213d978dc5a896bfc4d275deae6608abe)) -* friendly 503 error for overloaded Sandbox ([#172](https://github.com/codesandbox/codesandbox-sdk/issues/172)) ([f9987b1](https://github.com/codesandbox/codesandbox-sdk/commit/f9987b1a0b625bd580b94b6035c17d09d561cbbc)) -* include response handling in retries and dispose clients in build to avoid reconnecst ([#162](https://github.com/codesandbox/codesandbox-sdk/issues/162)) ([f70903a](https://github.com/codesandbox/codesandbox-sdk/commit/f70903a593c51bdfcdbf49d86b7b7bdce7cfe4a4)) -* properly dispose and prevent wakeups ([#170](https://github.com/codesandbox/codesandbox-sdk/issues/170)) ([029e3a5](https://github.com/codesandbox/codesandbox-sdk/commit/029e3a554010fe278eaeb6632f9df45264cbdc29)) -* Stabilize websocket connection ([#166](https://github.com/codesandbox/codesandbox-sdk/issues/166)) ([cb2f330](https://github.com/codesandbox/codesandbox-sdk/commit/cb2f330897c5e4c26637bc39a85d0e28dc4331d6)) -* update log line length to be smaller ([9a3099f](https://github.com/codesandbox/codesandbox-sdk/commit/9a3099fc3984c6ff01fa901ac1616cbc7b883119)) +- Add custom retry delay support for startVM API calls ([#156](https://github.com/codesandbox/codesandbox-sdk/issues/156)) ([ce3a282](https://github.com/codesandbox/codesandbox-sdk/commit/ce3a2823e66198f453d257a68757226d50e3bf17)) +- Decouple pitcher-client ([#148](https://github.com/codesandbox/codesandbox-sdk/issues/148)) ([3a6f9ea](https://github.com/codesandbox/codesandbox-sdk/commit/3a6f9ea213d978dc5a896bfc4d275deae6608abe)) +- friendly 503 error for overloaded Sandbox ([#172](https://github.com/codesandbox/codesandbox-sdk/issues/172)) ([f9987b1](https://github.com/codesandbox/codesandbox-sdk/commit/f9987b1a0b625bd580b94b6035c17d09d561cbbc)) +- include response handling in retries and dispose clients in build to avoid reconnecst ([#162](https://github.com/codesandbox/codesandbox-sdk/issues/162)) ([f70903a](https://github.com/codesandbox/codesandbox-sdk/commit/f70903a593c51bdfcdbf49d86b7b7bdce7cfe4a4)) +- properly dispose and prevent wakeups ([#170](https://github.com/codesandbox/codesandbox-sdk/issues/170)) ([029e3a5](https://github.com/codesandbox/codesandbox-sdk/commit/029e3a554010fe278eaeb6632f9df45264cbdc29)) +- Stabilize websocket connection ([#166](https://github.com/codesandbox/codesandbox-sdk/issues/166)) ([cb2f330](https://github.com/codesandbox/codesandbox-sdk/commit/cb2f330897c5e4c26637bc39a85d0e28dc4331d6)) +- update log line length to be smaller ([9a3099f](https://github.com/codesandbox/codesandbox-sdk/commit/9a3099fc3984c6ff01fa901ac1616cbc7b883119)) ## [2.0.7](https://github.com/codesandbox/codesandbox-sdk/compare/v2.0.6...v2.0.7) (2025-08-06) diff --git a/src/API.ts b/src/API.ts index 8dcead5..544a2af 100644 --- a/src/API.ts +++ b/src/API.ts @@ -55,7 +55,6 @@ import type { } from "./api-clients/client"; import { PitcherManagerResponse } from "./types"; - export interface APIOptions { apiKey: string; config?: Config; diff --git a/src/bin/ui/components/VmTable.tsx b/src/bin/ui/components/VmTable.tsx index a9f626c..8e0054a 100644 --- a/src/bin/ui/components/VmTable.tsx +++ b/src/bin/ui/components/VmTable.tsx @@ -138,23 +138,24 @@ export const VmTable = ({ {/* Header */} - {padString("VM ID", 14)} {padString("Last Active", 14)} {padString("Started At", 14)} {padString("Runtime", 10)} Credits + {padString("VM ID", 14)} {padString("Last Active", 14)}{" "} + {padString("Started At", 14)} {padString("Runtime", 10)} Credits - + {/* Separator */} {"─".repeat(Math.min(terminalWidth - 2, 60))} - + {/* Data rows */} {visibleVms.map((vm, visibleIndex) => { const actualIndex = startIndex + visibleIndex; const isSelected = selectedIndex === actualIndex; - + // Safely get VM ID and handle edge cases - const vmId = (vm?.id && typeof vm.id === 'string') ? vm.id : "N/A"; - + const vmId = vm?.id && typeof vm.id === "string" ? vm.id : "N/A"; + // Skip rendering if VM is completely invalid if (!vm) { return ( @@ -163,26 +164,34 @@ export const VmTable = ({ ); } - + return ( - - {padString(vmId, 14)} {padString(formatDate(vm.last_active_at), 14)} {padString(formatDate(vm.session_started_at), 14)} {padString(calculateRuntime(vm.session_started_at, vm.last_active_at), 10)} {vm.credit_basis || "N/A"} cr/hr + {padString(vmId, 14)}{" "} + {padString(formatDate(vm.last_active_at), 14)}{" "} + {padString(formatDate(vm.session_started_at), 14)}{" "} + {padString( + calculateRuntime(vm.session_started_at, vm.last_active_at), + 10 + )}{" "} + {vm.credit_basis || "N/A"} cr/hr ); })} - + {/* VM count and range info */} {vmsSorted.length <= maxVisibleRows ? `${vmsSorted.length} VMs total` - : `Showing ${startIndex + 1}-${endIndex} of ${vmsSorted.length} VMs` - } + : `Showing ${startIndex + 1}-${endIndex} of ${ + vmsSorted.length + } VMs`} diff --git a/src/bin/ui/hooks/useVmInput.ts b/src/bin/ui/hooks/useVmInput.ts index 4582f20..a7e591e 100644 --- a/src/bin/ui/hooks/useVmInput.ts +++ b/src/bin/ui/hooks/useVmInput.ts @@ -54,7 +54,7 @@ export const useVmInput = ({ vms, onSubmit }: UseVmInputOptions) => { setSelectedVmIndex(newIndex); const vm = vms[newIndex]; - const vmId = (vm?.id && typeof vm.id === 'string') ? vm.id : null; + const vmId = vm?.id && typeof vm.id === "string" ? vm.id : null; setSelectedVm(vmId); // Set the selected VM ID in the text input diff --git a/src/bin/ui/views/Dashboard.tsx b/src/bin/ui/views/Dashboard.tsx index 77ab059..2adc970 100644 --- a/src/bin/ui/views/Dashboard.tsx +++ b/src/bin/ui/views/Dashboard.tsx @@ -56,25 +56,27 @@ export const Dashboard = () => { if (!a.session_started_at && !b.session_started_at) return 0; if (!a.session_started_at) return 1; if (!b.session_started_at) return -1; - + const dateA = new Date(a.session_started_at); const dateB = new Date(b.session_started_at); - + // Check for invalid dates if (isNaN(dateA.getTime()) && isNaN(dateB.getTime())) return 0; if (isNaN(dateA.getTime())) return 1; if (isNaN(dateB.getTime())) return -1; - + return dateA.getTime() - dateB.getTime(); }) : []; - // Calculate visible rows dynamically based on terminal size - // Account for UI elements: instructions (1), sandbox label (1), input field (1), + // Account for UI elements: instructions (1), sandbox label (1), input field (1), // table title (1), table header (1), separator (1), bottom margin (2) const uiElementRows = 8; - const maxVisibleRows = Math.max(1, Math.floor((terminalHeight - uiElementRows) * 0.7) - 3); + const maxVisibleRows = Math.max( + 1, + Math.floor((terminalHeight - uiElementRows) * 0.7) - 3 + ); const { sandboxId, diff --git a/src/bin/ui/views/Sandbox.tsx b/src/bin/ui/views/Sandbox.tsx index a057d43..0c54865 100644 --- a/src/bin/ui/views/Sandbox.tsx +++ b/src/bin/ui/views/Sandbox.tsx @@ -3,6 +3,7 @@ import { Box, Text, useInput } from "ink"; import { useView } from "../viewContext"; import { useQuery } from "@tanstack/react-query"; import { useSDK } from "../sdkContext"; +import { exec } from "child_process"; export const Sandbox = () => { const { view, setView } = useView<"sandbox">(); @@ -61,9 +62,16 @@ export const Sandbox = () => { const getMenuOptions = () => { switch (sandboxState) { case "RUNNING": - return ["Open", "Terminal", "Hibernate", "Shutdown", "Restart"]; + return [ + "Open", + "Open editor in browser", + "Terminal", + "Hibernate", + "Shutdown", + "Restart", + ]; case "IDLE": - return ["Start"]; + return ["Start", "Open editor in browser"]; default: return []; } @@ -80,6 +88,24 @@ export const Sandbox = () => { case "Open": setView({ name: "open", params: { id: view.params.id } }); break; + case "Open editor in browser": + const url = `https://codesandbox.io/s/${view.params.id}`; + const platform = process.platform; + + // Open browser based on platform + const command = + platform === "darwin" + ? `open "${url}"` + : platform === "win32" + ? `start "${url}"` + : `xdg-open "${url}"`; // Linux + + exec(command, (error) => { + if (error) { + console.error(`Failed to open browser: ${error.message}`); + } + }); + break; case "Hibernate": case "Shutdown": setSandboxState("PENDING"); diff --git a/tests/sandbox-creation.test.ts b/tests/sandbox-creation.test.ts index e93444b..ba9b2e6 100644 --- a/tests/sandbox-creation.test.ts +++ b/tests/sandbox-creation.test.ts @@ -1,80 +1,80 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest' -import nock from 'nock' -import { CodeSandbox } from '../src/index' -import { - mockForkSandboxSuccess, - mockStartVMSuccess, - setupTestEnvironment, - cleanupTestEnvironment -} from './test-utils' - -describe('Sandbox Creation', () => { +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import nock from "nock"; +import { CodeSandbox } from "../src/index"; +import { + mockForkSandboxSuccess, + mockStartVMSuccess, + setupTestEnvironment, + cleanupTestEnvironment, +} from "./test-utils"; + +describe("Sandbox Creation", () => { beforeEach(() => { - setupTestEnvironment() - }) + setupTestEnvironment(); + }); afterEach(() => { - cleanupTestEnvironment() - }) + cleanupTestEnvironment(); + }); - it('should successfully create and start a sandbox', async () => { + it("should successfully create and start a sandbox", async () => { // Mock the fork sandbox API call (pcz35m is the default template) - const forkScope = mockForkSandboxSuccess('test-sandbox-123', { - title: 'Test Sandbox', - description: 'Integration test sandbox', + const forkScope = mockForkSandboxSuccess("test-sandbox-123", { + title: "Test Sandbox", + description: "Integration test sandbox", privacy: 1, - tags: ['integration-test', 'sdk'] - }) + tags: ["integration-test", "sdk"], + }); // Mock the start VM API call - use regex to match any ID - const startScope = mockStartVMSuccess('test-sandbox-123') + const startScope = mockStartVMSuccess("test-sandbox-123"); // Initialize SDK - const sdk = new CodeSandbox() - + const sdk = new CodeSandbox(); + // Create sandbox const sandbox = await sdk.sandboxes.create({ - title: 'Test Sandbox', - description: 'Integration test sandbox', - privacy: 'unlisted', - tags: ['integration-test'] - }) + title: "Test Sandbox", + description: "Integration test sandbox", + privacy: "unlisted", + tags: ["integration-test"], + }); // Verify sandbox was created successfully - expect(sandbox).toBeDefined() - expect(sandbox.id).toBe('test-sandbox-123') - + expect(sandbox).toBeDefined(); + expect(sandbox.id).toBe("test-sandbox-123"); + // Verify all API calls were made - expect(forkScope.isDone()).toBe(true) - expect(startScope.isDone()).toBe(true) - }, 10000) // 10 second timeout for integration test + expect(forkScope.isDone()).toBe(true); + expect(startScope.isDone()).toBe(true); + }, 10000); // 10 second timeout for integration test - it('should use default template when no id is provided', async () => { + it("should use default template when no id is provided", async () => { // Mock default template call - pcz35m is the default template - const forkScope = mockForkSandboxSuccess('default-sandbox-456') + const forkScope = mockForkSandboxSuccess("default-sandbox-456"); - const startScope = mockStartVMSuccess('default-sandbox-456') + const startScope = mockStartVMSuccess("default-sandbox-456"); + + const sdk = new CodeSandbox(); - const sdk = new CodeSandbox() - // Create sandbox without specifying template id - const sandbox = await sdk.sandboxes.create() + const sandbox = await sdk.sandboxes.create(); - expect(sandbox).toBeDefined() - expect(sandbox.id).toBe('default-sandbox-456') - expect(forkScope.isDone()).toBe(true) - expect(startScope.isDone()).toBe(true) - }) + expect(sandbox).toBeDefined(); + expect(sandbox.id).toBe("default-sandbox-456"); + expect(forkScope.isDone()).toBe(true); + expect(startScope.isDone()).toBe(true); + }); - it('should handle API errors gracefully', async () => { + it("should handle API errors gracefully", async () => { // Mock fork sandbox failure - nock('https://api.codesandbox.io') - .post('/sandbox/pcz35m/fork') - .reply(500, { message: 'Internal server error' }) + nock("https://api.codesandbox.io") + .post("/sandbox/pcz35m/fork") + .reply(500, { message: "Internal server error" }); + + const sdk = new CodeSandbox(); - const sdk = new CodeSandbox() - // Expect the creation to throw an error - await expect(sdk.sandboxes.create()).rejects.toThrow() - }) -}) \ No newline at end of file + await expect(sdk.sandboxes.create()).rejects.toThrow(); + }); +}); diff --git a/tests/sandbox-lifecycle.test.ts b/tests/sandbox-lifecycle.test.ts index 41d4fa1..7967bd5 100644 --- a/tests/sandbox-lifecycle.test.ts +++ b/tests/sandbox-lifecycle.test.ts @@ -1,135 +1,139 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest' -import { CodeSandbox } from '../src/index' -import { +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { CodeSandbox } from "../src/index"; +import { mockHibernateSuccess, mockHibernateFailure, mockStartVMSuccess, mockShutdownSuccess, mockShutdownFailure, mockStartVMFailure, - setupTestEnvironment, - cleanupTestEnvironment -} from './test-utils' + setupTestEnvironment, + cleanupTestEnvironment, +} from "./test-utils"; -describe('Sandbox lifecycle operations', () => { +describe("Sandbox lifecycle operations", () => { beforeEach(() => { - setupTestEnvironment() - }) + setupTestEnvironment(); + }); afterEach(() => { - cleanupTestEnvironment() - }) + cleanupTestEnvironment(); + }); + + it("should successfully hibernate a sandbox", async () => { + const sandboxId = "test-sandbox-hibernate"; - it('should successfully hibernate a sandbox', async () => { - const sandboxId = 'test-sandbox-hibernate' - // Mock hibernate API call - const hibernateScope = mockHibernateSuccess(sandboxId) + const hibernateScope = mockHibernateSuccess(sandboxId); + + const sdk = new CodeSandbox(); - const sdk = new CodeSandbox() - // Test hibernate operation - await expect(sdk.sandboxes.hibernate(sandboxId)).resolves.not.toThrow() - + await expect(sdk.sandboxes.hibernate(sandboxId)).resolves.not.toThrow(); + // Verify API call was made - expect(hibernateScope.isDone()).toBe(true) - }) + expect(hibernateScope.isDone()).toBe(true); + }); + + it("should successfully resume a sandbox", async () => { + const sandboxId = "test-sandbox-resume"; - it('should successfully resume a sandbox', async () => { - const sandboxId = 'test-sandbox-resume' - // Mock resume (startVm) API call - const resumeScope = mockStartVMSuccess(sandboxId, 'RESUME') + const resumeScope = mockStartVMSuccess(sandboxId, "RESUME"); + + const sdk = new CodeSandbox(); - const sdk = new CodeSandbox() - // Test resume operation - const sandbox = await sdk.sandboxes.resume(sandboxId) - + const sandbox = await sdk.sandboxes.resume(sandboxId); + // Verify sandbox was resumed successfully - expect(sandbox).toBeDefined() - expect(sandbox.id).toBe(sandboxId) - expect(sandbox.bootupType).toBe('RESUME') - expect(resumeScope.isDone()).toBe(true) - }) - - it('should successfully restart a sandbox', async () => { - const sandboxId = 'test-sandbox-restart' - + expect(sandbox).toBeDefined(); + expect(sandbox.id).toBe(sandboxId); + expect(sandbox.bootupType).toBe("RESUME"); + expect(resumeScope.isDone()).toBe(true); + }); + + it("should successfully restart a sandbox", async () => { + const sandboxId = "test-sandbox-restart"; + // Mock shutdown API call - const shutdownScope = mockShutdownSuccess(sandboxId) + const shutdownScope = mockShutdownSuccess(sandboxId); // Mock start VM API call (after shutdown) - const startScope = mockStartVMSuccess(sandboxId, 'CLEAN') + const startScope = mockStartVMSuccess(sandboxId, "CLEAN"); + + const sdk = new CodeSandbox(); - const sdk = new CodeSandbox() - // Test restart operation - const sandbox = await sdk.sandboxes.restart(sandboxId) - + const sandbox = await sdk.sandboxes.restart(sandboxId); + // Verify sandbox was restarted successfully - expect(sandbox).toBeDefined() - expect(sandbox.id).toBe(sandboxId) - expect(sandbox.bootupType).toBe('CLEAN') - expect(shutdownScope.isDone()).toBe(true) - expect(startScope.isDone()).toBe(true) - }) - - it('should retry API calls on failure and eventually succeed', async () => { - const sandboxId = 'test-sandbox-retry-success' - + expect(sandbox).toBeDefined(); + expect(sandbox.id).toBe(sandboxId); + expect(sandbox.bootupType).toBe("CLEAN"); + expect(shutdownScope.isDone()).toBe(true); + expect(startScope.isDone()).toBe(true); + }); + + it("should retry API calls on failure and eventually succeed", async () => { + const sandboxId = "test-sandbox-retry-success"; + // Mock hibernate API to fail twice, then succeed on 3rd attempt - mockHibernateFailure(sandboxId, 2, 'Server error') - const hibernateScope = mockHibernateSuccess(sandboxId) + mockHibernateFailure(sandboxId, 2, "Server error"); + const hibernateScope = mockHibernateSuccess(sandboxId); + + const sdk = new CodeSandbox(); - const sdk = new CodeSandbox() - // Test should succeed after retries - await expect(sdk.sandboxes.hibernate(sandboxId)).resolves.not.toThrow() - + await expect(sdk.sandboxes.hibernate(sandboxId)).resolves.not.toThrow(); + // Verify all retry attempts were made - expect(hibernateScope.isDone()).toBe(true) - }, 10000) // Longer timeout for retry test + expect(hibernateScope.isDone()).toBe(true); + }, 10000); // Longer timeout for retry test + + it("should fail after exhausting all retry attempts", async () => { + const sandboxId = "test-sandbox-retry-fail"; - it('should fail after exhausting all retry attempts', async () => { - const sandboxId = 'test-sandbox-retry-fail' - // Mock hibernate API to fail 3 times (exhaust retries) - mockHibernateFailure(sandboxId, 3, 'Persistent server error') + mockHibernateFailure(sandboxId, 3, "Persistent server error"); + + const sdk = new CodeSandbox(); - const sdk = new CodeSandbox() - // Test should fail after all retries are exhausted - await expect(sdk.sandboxes.hibernate(sandboxId)).rejects.toThrow() - }, 10000) // Longer timeout for retry test + await expect(sdk.sandboxes.hibernate(sandboxId)).rejects.toThrow(); + }, 10000); // Longer timeout for retry test + + it("should handle restart failure during shutdown phase", async () => { + const sandboxId = "test-sandbox-restart-fail"; - it('should handle restart failure during shutdown phase', async () => { - const sandboxId = 'test-sandbox-restart-fail' - // Mock shutdown to fail - mockShutdownFailure(sandboxId, 3, 'Shutdown failed') + mockShutdownFailure(sandboxId, 3, "Shutdown failed"); + + const sdk = new CodeSandbox(); - const sdk = new CodeSandbox() - // Test should fail during shutdown phase - await expect(sdk.sandboxes.restart(sandboxId)).rejects.toThrow('Failed to shutdown VM') - }) + await expect(sdk.sandboxes.restart(sandboxId)).rejects.toThrow( + "Failed to shutdown VM" + ); + }); + + it("should handle restart failure during start phase", async () => { + const sandboxId = "test-sandbox-restart-start-fail"; - it('should handle restart failure during start phase', async () => { - const sandboxId = 'test-sandbox-restart-start-fail' - // Mock successful shutdown - const shutdownScope = mockShutdownSuccess(sandboxId) + const shutdownScope = mockShutdownSuccess(sandboxId); // Mock start VM to fail - mockStartVMFailure(3, 'Start failed') + mockStartVMFailure(3, "Start failed"); + + const sdk = new CodeSandbox(); - const sdk = new CodeSandbox() - // Test should fail during start phase - await expect(sdk.sandboxes.restart(sandboxId)).rejects.toThrow('Failed to start VM') - + await expect(sdk.sandboxes.restart(sandboxId)).rejects.toThrow( + "Failed to start VM" + ); + // Verify both phases were attempted - expect(shutdownScope.isDone()).toBe(true) - }) -}) \ No newline at end of file + expect(shutdownScope.isDone()).toBe(true); + }); +}); diff --git a/tests/sandbox-retry-behavior.test.ts b/tests/sandbox-retry-behavior.test.ts index 4e03c80..e09acbe 100644 --- a/tests/sandbox-retry-behavior.test.ts +++ b/tests/sandbox-retry-behavior.test.ts @@ -1,168 +1,168 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest' -import nock from 'nock' -import { CodeSandbox } from '../src/index' -import { - mockForkSandboxSuccess, - mockStartVMSuccess, - mockStartVMFailure, - setupTestEnvironment, - cleanupTestEnvironment -} from './test-utils' - -describe('Create operation retry behavior', () => { +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import nock from "nock"; +import { CodeSandbox } from "../src/index"; +import { + mockForkSandboxSuccess, + mockStartVMSuccess, + mockStartVMFailure, + setupTestEnvironment, + cleanupTestEnvironment, +} from "./test-utils"; + +describe("Create operation retry behavior", () => { beforeEach(() => { - setupTestEnvironment() - }) + setupTestEnvironment(); + }); afterEach(() => { - cleanupTestEnvironment() - }) + cleanupTestEnvironment(); + }); + + it("should fail immediately on fork API error (no retry for fork)", async () => { + let forkRequestCount = 0; - it('should fail immediately on fork API error (no retry for fork)', async () => { - let forkRequestCount = 0 - // Mock fork to fail once - should fail immediately since fork doesn't retry - const forkScope = nock('https://api.codesandbox.io') - .post('/sandbox/pcz35m/fork') + const forkScope = nock("https://api.codesandbox.io") + .post("/sandbox/pcz35m/fork") .reply(500, () => { - forkRequestCount++ - return { error: { errors: ['Fork failed'] } } - }) + forkRequestCount++; + return { error: { errors: ["Fork failed"] } }; + }); + + const sdk = new CodeSandbox(); - const sdk = new CodeSandbox() - // Should fail immediately without retries - const startTime = Date.now() - await expect(sdk.sandboxes.create()).rejects.toThrow() - const duration = Date.now() - startTime - + const startTime = Date.now(); + await expect(sdk.sandboxes.create()).rejects.toThrow(); + const duration = Date.now() - startTime; + // Should fail quickly (no retry delays) - expect(duration).toBeLessThan(1000) - expect(forkScope.isDone()).toBe(true) - expect(forkRequestCount).toBe(1) // Should only make 1 request (no retries) - }) - - it('should retry start VM failures and eventually succeed', async () => { - let startVMRequestCount = 0 - + expect(duration).toBeLessThan(1000); + expect(forkScope.isDone()).toBe(true); + expect(forkRequestCount).toBe(1); // Should only make 1 request (no retries) + }); + + it("should retry start VM failures and eventually succeed", async () => { + let startVMRequestCount = 0; + // Mock successful fork - const forkScope = mockForkSandboxSuccess('test-sandbox-start-retry') + const forkScope = mockForkSandboxSuccess("test-sandbox-start-retry"); // Mock start VM to fail twice - const failureScope = nock('https://api.codesandbox.io') + const failureScope = nock("https://api.codesandbox.io") .post(/\/vm\/.*\/start/) .times(2) .reply(500, () => { - startVMRequestCount++ - return { error: { errors: ['Start failed'] } } - }) - + startVMRequestCount++; + return { error: { errors: ["Start failed"] } }; + }); + // Then succeed on 3rd attempt - const startScope = nock('https://api.codesandbox.io') + const startScope = nock("https://api.codesandbox.io") .post(/\/vm\/.*\/start/) .reply(200, () => { - startVMRequestCount++ + startVMRequestCount++; return { data: { - bootup_type: 'CLEAN', - cluster: 'test-cluster', + bootup_type: "CLEAN", + cluster: "test-cluster", pitcher_url: `wss://pitcher.codesandbox.io/test-sandbox-start-retry`, - workspace_path: '/project/sandbox', - user_workspace_path: '/project/sandbox', - pitcher_manager_version: '1.0.0', - pitcher_version: '1.0.0', - latest_pitcher_version: '1.0.0', - pitcher_token: `pitcher-token-retry` - } - } - }) - - const sdk = new CodeSandbox() - + workspace_path: "/project/sandbox", + user_workspace_path: "/project/sandbox", + pitcher_manager_version: "1.0.0", + pitcher_version: "1.0.0", + latest_pitcher_version: "1.0.0", + pitcher_token: `pitcher-token-retry`, + }, + }; + }); + + const sdk = new CodeSandbox(); + // Should succeed after start VM retries - const sandbox = await sdk.sandboxes.create() - - expect(sandbox).toBeDefined() - expect(sandbox.id).toBe('test-sandbox-start-retry') - expect(forkScope.isDone()).toBe(true) - expect(failureScope.isDone()).toBe(true) - expect(startScope.isDone()).toBe(true) - expect(startVMRequestCount).toBe(3) // Should make 3 requests (2 failures + 1 success) - }, 10000) // Longer timeout for retries - - it('should fail create after start VM exhausts all retries', async () => { - let startVMRequestCount = 0 - + const sandbox = await sdk.sandboxes.create(); + + expect(sandbox).toBeDefined(); + expect(sandbox.id).toBe("test-sandbox-start-retry"); + expect(forkScope.isDone()).toBe(true); + expect(failureScope.isDone()).toBe(true); + expect(startScope.isDone()).toBe(true); + expect(startVMRequestCount).toBe(3); // Should make 3 requests (2 failures + 1 success) + }, 10000); // Longer timeout for retries + + it("should fail create after start VM exhausts all retries", async () => { + let startVMRequestCount = 0; + // Mock successful fork - const forkScope = mockForkSandboxSuccess('test-sandbox-start-fail') + const forkScope = mockForkSandboxSuccess("test-sandbox-start-fail"); // Mock start VM to fail all 3 retry attempts - const failureScope = nock('https://api.codesandbox.io') + const failureScope = nock("https://api.codesandbox.io") .post(/\/vm\/.*\/start/) .times(3) .reply(500, () => { - startVMRequestCount++ - return { error: { errors: ['Persistent start failure'] } } - }) + startVMRequestCount++; + return { error: { errors: ["Persistent start failure"] } }; + }); + + const sdk = new CodeSandbox(); - const sdk = new CodeSandbox() - // Should fail after exhausting start VM retries - await expect(sdk.sandboxes.create()).rejects.toThrow() - - expect(forkScope.isDone()).toBe(true) - expect(failureScope.isDone()).toBe(true) - expect(startVMRequestCount).toBe(3) // Should make exactly 3 retry attempts - }, 10000) // Longer timeout for retries - - it('should validate retry timing for start VM failures', async () => { - let startVMRequestCount = 0 - + await expect(sdk.sandboxes.create()).rejects.toThrow(); + + expect(forkScope.isDone()).toBe(true); + expect(failureScope.isDone()).toBe(true); + expect(startVMRequestCount).toBe(3); // Should make exactly 3 retry attempts + }, 10000); // Longer timeout for retries + + it("should validate retry timing for start VM failures", async () => { + let startVMRequestCount = 0; + // Mock successful fork - const forkScope = mockForkSandboxSuccess('test-sandbox-timing') + const forkScope = mockForkSandboxSuccess("test-sandbox-timing"); // Mock start VM to fail twice - const failureScope = nock('https://api.codesandbox.io') + const failureScope = nock("https://api.codesandbox.io") .post(/\/vm\/.*\/start/) .times(2) .reply(500, () => { - startVMRequestCount++ - return { error: { errors: ['Start failed'] } } - }) - + startVMRequestCount++; + return { error: { errors: ["Start failed"] } }; + }); + // Then succeed on 3rd attempt - const startScope = nock('https://api.codesandbox.io') + const startScope = nock("https://api.codesandbox.io") .post(/\/vm\/.*\/start/) .reply(200, () => { - startVMRequestCount++ + startVMRequestCount++; return { data: { - bootup_type: 'CLEAN', - cluster: 'test-cluster', + bootup_type: "CLEAN", + cluster: "test-cluster", pitcher_url: `wss://pitcher.codesandbox.io/test-sandbox-timing`, - workspace_path: '/project/sandbox', - user_workspace_path: '/project/sandbox', - pitcher_manager_version: '1.0.0', - pitcher_version: '1.0.0', - latest_pitcher_version: '1.0.0', - pitcher_token: `pitcher-token-timing` - } - } - }) - - const sdk = new CodeSandbox() - - const startTime = Date.now() - const sandbox = await sdk.sandboxes.create() - const duration = Date.now() - startTime - + workspace_path: "/project/sandbox", + user_workspace_path: "/project/sandbox", + pitcher_manager_version: "1.0.0", + pitcher_version: "1.0.0", + latest_pitcher_version: "1.0.0", + pitcher_token: `pitcher-token-timing`, + }, + }; + }); + + const sdk = new CodeSandbox(); + + const startTime = Date.now(); + const sandbox = await sdk.sandboxes.create(); + const duration = Date.now() - startTime; + // Should take at least 400ms (2 retries × 200ms delay each) - expect(duration).toBeGreaterThanOrEqual(300) // Allow some tolerance - expect(sandbox).toBeDefined() - expect(sandbox.id).toBe('test-sandbox-timing') - expect(forkScope.isDone()).toBe(true) - expect(failureScope.isDone()).toBe(true) - expect(startScope.isDone()).toBe(true) - expect(startVMRequestCount).toBe(3) // Should make 3 requests (2 failures + 1 success) - }, 10000) // Longer timeout for retries -}) \ No newline at end of file + expect(duration).toBeGreaterThanOrEqual(300); // Allow some tolerance + expect(sandbox).toBeDefined(); + expect(sandbox.id).toBe("test-sandbox-timing"); + expect(forkScope.isDone()).toBe(true); + expect(failureScope.isDone()).toBe(true); + expect(startScope.isDone()).toBe(true); + expect(startVMRequestCount).toBe(3); // Should make 3 requests (2 failures + 1 success) + }, 10000); // Longer timeout for retries +}); diff --git a/tests/test-utils.ts b/tests/test-utils.ts index 6ad1cf8..26430a6 100644 --- a/tests/test-utils.ts +++ b/tests/test-utils.ts @@ -1,99 +1,116 @@ -import nock from 'nock' +import nock from "nock"; -export const mockForkSandboxSuccess = (sandboxId: string, options?: { - title?: string - description?: string - privacy?: number - tags?: string[] -}) => { - return nock('https://api.codesandbox.io') - .post('/sandbox/pcz35m/fork', { +export const mockForkSandboxSuccess = ( + sandboxId: string, + options?: { + title?: string; + description?: string; + privacy?: number; + tags?: string[]; + } +) => { + return nock("https://api.codesandbox.io") + .post("/sandbox/pcz35m/fork", { privacy: options?.privacy ?? 1, ...(options?.title && { title: options.title }), ...(options?.description && { description: options.description }), - tags: options?.tags ?? ['sdk'], - path: '/SDK' + tags: options?.tags ?? ["sdk"], + path: "/SDK", }) .reply(200, { data: { id: sandboxId, - title: options?.title ?? 'Test Sandbox', + title: options?.title ?? "Test Sandbox", description: options?.description, privacy: options?.privacy ?? 1, - tags: options?.tags ?? ['sdk'], - created_at: '2025-01-21T12:00:00Z', - updated_at: '2025-01-21T12:00:00Z' - } - }) -} + tags: options?.tags ?? ["sdk"], + created_at: "2025-01-21T12:00:00Z", + updated_at: "2025-01-21T12:00:00Z", + }, + }); +}; -export const mockStartVMSuccess = (sandboxId: string, bootupType: 'CLEAN' | 'RESUME' = 'CLEAN') => { - return nock('https://api.codesandbox.io') +export const mockStartVMSuccess = ( + sandboxId: string, + bootupType: "CLEAN" | "RESUME" = "CLEAN" +) => { + return nock("https://api.codesandbox.io") .post(/\/vm\/.*\/start/) .reply(200, { data: { bootup_type: bootupType, - cluster: 'test-cluster', + cluster: "test-cluster", pitcher_url: `wss://pitcher.codesandbox.io/${sandboxId}`, - workspace_path: '/project/sandbox', - user_workspace_path: '/project/sandbox', - pitcher_manager_version: '1.0.0', - pitcher_version: '1.0.0', - latest_pitcher_version: '1.0.0', - pitcher_token: `pitcher-token-${sandboxId.split('-').pop()}` - } - }) -} + workspace_path: "/project/sandbox", + user_workspace_path: "/project/sandbox", + pitcher_manager_version: "1.0.0", + pitcher_version: "1.0.0", + latest_pitcher_version: "1.0.0", + pitcher_token: `pitcher-token-${sandboxId.split("-").pop()}`, + }, + }); +}; -export const mockStartVMFailure = (times: number = 1, errorMessage: string = 'Start failed') => { - return nock('https://api.codesandbox.io') +export const mockStartVMFailure = ( + times: number = 1, + errorMessage: string = "Start failed" +) => { + return nock("https://api.codesandbox.io") .post(/\/vm\/.*\/start/) .times(times) - .reply(500, { error: { errors: [errorMessage] } }) -} + .reply(500, { error: { errors: [errorMessage] } }); +}; export const mockHibernateSuccess = (sandboxId: string) => { - return nock('https://api.codesandbox.io') + return nock("https://api.codesandbox.io") .post(`/vm/${sandboxId}/hibernate`) .reply(200, { data: { - success: true - } - }) -} + success: true, + }, + }); +}; -export const mockHibernateFailure = (sandboxId: string, times: number = 1, errorMessage: string = 'Server error') => { - return nock('https://api.codesandbox.io') +export const mockHibernateFailure = ( + sandboxId: string, + times: number = 1, + errorMessage: string = "Server error" +) => { + return nock("https://api.codesandbox.io") .post(`/vm/${sandboxId}/hibernate`) .times(times) - .reply(500, { error: { errors: [errorMessage] } }) -} + .reply(500, { error: { errors: [errorMessage] } }); +}; export const mockShutdownSuccess = (sandboxId: string) => { - return nock('https://api.codesandbox.io') + return nock("https://api.codesandbox.io") .post(`/vm/${sandboxId}/shutdown`) .reply(200, { data: { - success: true - } - }) -} + success: true, + }, + }); +}; -export const mockShutdownFailure = (sandboxId: string, times: number = 1, errorMessage: string = 'Shutdown failed') => { - return nock('https://api.codesandbox.io') +export const mockShutdownFailure = ( + sandboxId: string, + times: number = 1, + errorMessage: string = "Shutdown failed" +) => { + return nock("https://api.codesandbox.io") .post(`/vm/${sandboxId}/shutdown`) .times(times) - .reply(500, { error: { errors: [errorMessage] } }) -} + .reply(500, { error: { errors: [errorMessage] } }); +}; export const setupTestEnvironment = () => { - process.env.CSB_API_KEY = 'csb_test_key_123' - nock.cleanAll() -} + process.env.CSB_API_KEY = "csb_test_key_123"; + nock.cleanAll(); +}; export const cleanupTestEnvironment = () => { if (!nock.isDone()) { - console.error('Unused nock interceptors:', nock.pendingMocks()) + console.error("Unused nock interceptors:", nock.pendingMocks()); } - nock.cleanAll() -} \ No newline at end of file + nock.cleanAll(); +}; diff --git a/vitest.config.ts b/vitest.config.ts index cba2eed..e941089 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,11 +1,11 @@ -import { defineConfig } from 'vitest/config' +import { defineConfig } from "vitest/config"; export default defineConfig({ test: { - environment: 'node', - include: ['tests/**/*.test.ts'], + environment: "node", + include: ["tests/**/*.test.ts"], }, define: { - CSB_SDK_VERSION: JSON.stringify('2.1.0-rc.4'), + CSB_SDK_VERSION: JSON.stringify("2.1.0-rc.4"), }, -}) \ No newline at end of file +});