# Testing Runbook — `@coda/sandbox`

This runbook covers how to run the sandbox test suite, how tests are configured, and how to write new tests.

---

## Test Suite Overview

The sandbox has 48 tests across 8 suites. Unit suites mock ivm internals where possible; integration suites use real isolates.

| Suite           | File                                            | Tests  | Covers                                                                                                                   |
| --------------- | ----------------------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------ |
| Protocol        | `src/bridge/protocol.test.ts`                   | 7      | `SandboxMessage` JSON round-trips, discriminated union shapes, default values                                            |
| Session         | `src/bridge/session.test.ts`                    | 5      | Async queue push/pull, data request resolve/reject, abort, send-after-abort no-op                                        |
| WASM Loader     | `src/execution/wasm/wasm-loader.test.ts`        | 3      | Load module from disk, return `undefined` for unknown key, cache returns same reference                                  |
| Isolate Manager | `src/execution/isolate/isolate-manager.test.ts` | 5      | Acquire/release lifecycle, capacity rejection, warm pool pre-creation, drain, no reuse after release                     |
| Code Executor   | `src/execution/isolate/code-executor.test.ts`   | 7      | Simple execution, async execution, complex data flow, runtime errors, syntax errors, timeout errors, code size limit     |
| Context Builder | `src/execution/isolate/context-builder.test.ts` | 5      | `dataProxy` availability, `reportStatus` availability, `console.log` capture, no host globals leaked, console output cap |
| E2E Integration | `src/integration/end-to-end.test.ts`            | 5      | Simple execution round-trip, data request round-trip, status event emission, timeout enforcement, abort mid-execution    |
| Security        | `src/integration/security.test.ts`              | 11     | All security scenarios (see the [Security](../../compliance/sandbox-security.md) doc)                                    |
| **Total**       |                                                 | **48** | Full coverage of all components                                                                                          |

---

## Running Tests

### From the monorepo root

```bash
# Full test run (lint + unit tests + coverage)
pnpm --filter @coda/sandbox test

# Unit tests only
pnpm --filter @coda/sandbox test:unit
```

### From the sandbox directory

```bash
cd sandbox

# Full test run
pnpm test

# Unit tests only (skips lint)
pnpm test:unit

# Unit tests with coverage report
pnpm test:unit --coverage
```

### Running a specific test file

```bash
node --experimental-vm-modules node_modules/jest/bin/jest.js src/bridge/session.test.ts
```

### Running tests matching a name pattern

```bash
node --experimental-vm-modules node_modules/jest/bin/jest.js --testNamePattern "timeout"
```

### Watch mode (re-runs on file changes)

```bash
node --experimental-vm-modules node_modules/jest/bin/jest.js --watch
```

---

## Why `--experimental-vm-modules`

Jest needs this Node.js flag to run ES modules natively. Without it, Jest's internal module system cannot handle `import`/`export` syntax in TypeScript compiled to ESM — it attempts to parse the file as CommonJS and fails with a syntax error on the first `import` statement.

The flag has been available since Node 12 and is stable enough for development and CI use. It is set automatically when tests are run via the `pnpm test:unit` script, so you only need to pass it manually when invoking `jest.js` directly.

---

## Why the Direct `jest.js` Binary (not the `jest` shim)

When you run `node ./node_modules/.bin/jest`, Node.js executes the shell shim at that path as JavaScript — but the shim is a bash script, not a JS file. This causes a parse error.

The direct path `node_modules/jest/bin/jest.js` points to the actual JavaScript entry point for Jest. This is what the `pnpm test:unit` script uses internally. Use this form when you need to pass Node.js flags like `--experimental-vm-modules` before the Jest binary path.

---

## Test Configuration Details

Configuration lives in `sandbox/jest.config.json`.

**Key settings:**

- `extensionsToTreatAsEsm: [".ts"]` — tells Jest to treat `.ts` files as ES modules, enabling native `import`/`export` handling
- `useESM: true` in the `ts-jest` transform — instructs ts-jest to produce ESM output rather than transpiling to CommonJS
- `testEnvironment: "node"` — isolated-vm is a native addon that requires a real Node.js environment; jsdom is incompatible
- `testTimeout: 10000` — default 10s timeout; individual integration tests with timeout scenarios override this per-test

**`moduleNameMapper` entries:**

- `"^(\\.{1,2}/.*)\\.js$": "$1"` — maps `.js` import extensions to no extension, which allows ts-jest to resolve `.ts` source files. TypeScript ESM convention requires `.js` extensions in source imports; this mapper bridges the gap during testing.
- `"^@sandbox/(.*)$": "<rootDir>/src/$1"` — resolves the `@sandbox/*` path alias to the `src/` directory, consistent with the `tsconfig.json` `paths` configuration.

---

## Writing New Tests

### Unit tests (mock ivm)

Unit tests should mock `isolated-vm` to avoid the overhead and side-effects of real isolate creation:

```typescript
jest.mock("isolated-vm", () => ({
  default: {
    Isolate: jest.fn().mockImplementation(() => ({
      createContext: jest
        .fn()
        .mockResolvedValue({ global: { setSync: jest.fn() } }),
      compileScript: jest.fn().mockResolvedValue({ run: jest.fn() }),
      dispose: jest.fn(),
    })),
  },
}));
```

- Keep each test independent — no shared state between tests. Each test should create and tear down its own instances.
- Do not rely on test execution order.
- Assert on observable outputs (messages emitted, errors thrown) rather than internal state.

### Integration tests (real ivm)

Integration tests use real isolates and must be cleaned up properly to prevent the test process from hanging:

```typescript
afterAll(async () => {
  await engine.shutdown();
});
```

- Set a test-level timeout above the isolate timeout for tests that deliberately trigger timeouts:

```typescript
it("times out correctly", async () => {
  // ...
}, 5000); // jest timeout > isolate timeout
```

- Do not share a single engine instance across tests that mutate configuration. Create a fresh engine per `describe` block if configuration differs.

### General guidelines

- Co-locate test files with source: `src/foo/bar.test.ts` alongside `src/foo/bar.ts`
- Import using `.js` extension even in test files: `import { Foo } from './foo.js'`
- Use `afterAll` (not `afterEach`) for engine/manager teardown to avoid repeated setup overhead
- For tests that verify error types, use `expect(fn).rejects.toThrow(SandboxTimeoutError)` rather than catching manually

---

## Code Coverage

- **Target:** 80%+ line coverage
- **Generate report:** `pnpm test:unit --coverage`
- **Output directory:** `sandbox/coverage/` (gitignored)
- **View HTML report:** `open coverage/lcov-report/index.html`

**What is intentionally not covered:**

- Error branches that require specific V8 internal conditions to fire (e.g., exact OOM behavior depends on GC timing and is environment-dependent)
- Native addon error paths that can only be triggered by corrupting isolate state

Coverage is enforced in CI. PRs that drop line coverage below threshold will fail the coverage check.
