diff --git a/packages/sandbox/src/sandbox.ts b/packages/sandbox/src/sandbox.ts index 1691c0d57..170c9887e 100644 --- a/packages/sandbox/src/sandbox.ts +++ b/packages/sandbox/src/sandbox.ts @@ -65,6 +65,7 @@ import { InvalidBackupConfigError, PortNotExposedError, ProcessExitedBeforeReadyError, + ProcessNotFoundError, ProcessReadyTimeoutError, SandboxError, SessionAlreadyExistsError @@ -3363,8 +3364,13 @@ export class Sandbox extends Container implements ISandbox { async getProcess(id: string, sessionId?: string): Promise { const session = sessionId ?? (await this.ensureDefaultSession()); - const response = await this.client.processes.getProcess(id); - if (!response.process) { + const response = await this.client.processes + .getProcess(id) + .catch((e: unknown) => { + if (e instanceof ProcessNotFoundError) return null; + throw e; + }); + if (!response?.process) { return null; } diff --git a/packages/sandbox/tests/sandbox.test.ts b/packages/sandbox/tests/sandbox.test.ts index a744e009a..471c8150c 100644 --- a/packages/sandbox/tests/sandbox.test.ts +++ b/packages/sandbox/tests/sandbox.test.ts @@ -1,6 +1,6 @@ import { Container } from '@cloudflare/containers'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { PortNotExposedError } from '../src/errors'; +import { PortNotExposedError, ProcessNotFoundError } from '../src/errors'; import { connect, Sandbox } from '../src/sandbox'; // Mock dependencies before imports @@ -2719,3 +2719,80 @@ describe('Sandbox - Automatic Session Management', () => { }); }); }); + +// --------------------------------------------------------------------------- +// Sandbox.getProcess() — behaviour across HTTP and RPC transports +// --------------------------------------------------------------------------- + +describe('Sandbox.getProcess()', () => { + async function makeSandbox(transport: 'http' | 'rpc') { + const ctx = { + storage: { + get: vi.fn().mockResolvedValue(null), + put: vi.fn().mockResolvedValue(undefined), + delete: vi.fn().mockResolvedValue(undefined), + list: vi.fn().mockResolvedValue(new Map()) + } as any, + blockConcurrencyWhile: vi + .fn() + .mockImplementation((cb: () => Promise) => cb()), + waitUntil: vi.fn(), + id: { toString: () => 'test-id', equals: vi.fn(), name: 'test' } as any + }; + const env = transport === 'rpc' ? { SANDBOX_TRANSPORT: 'rpc' } : {}; + const sb = new Sandbox(ctx as any, env); + await vi.waitFor(() => + expect(ctx.blockConcurrencyWhile).toHaveBeenCalled() + ); + // For RPC transport, sb.client is a ContainerControlClient whose sub-stubs + // are capnweb Proxies that reject vi.spyOn. Replace the whole client with a + // plain mock object after construction — the individual tests fill in the + // methods they need. + if (transport === 'rpc') { + (sb as any).client = { + getTransportMode: () => 'rpc', + utils: { + createSession: vi + .fn() + .mockResolvedValue({ + success: true, + id: 'default', + message: 'ok' + } as any) + }, + processes: {} + }; + } else { + vi.spyOn(sb.client.utils, 'createSession').mockResolvedValue({ + success: true, + id: 'default', + message: 'ok' + } as any); + } + return sb; + } + + it('HTTP: response with no process field returns null', async () => { + const sb = await makeSandbox('http'); + vi.spyOn(sb.client.processes, 'getProcess').mockResolvedValue({ + success: true, + process: undefined, + timestamp: '' + } as any); + expect(await sb.getProcess('x')).toBeNull(); + }); + + it('RPC: thrown ProcessNotFoundError returns null', async () => { + const sb = await makeSandbox('rpc'); + (sb.client.processes as any).getProcess = vi.fn().mockRejectedValue( + new ProcessNotFoundError({ + code: 'PROCESS_NOT_FOUND', + message: 'Process x not found', + context: { processId: 'x' }, + httpStatus: 404, + timestamp: '' + } as any) + ); + expect(await sb.getProcess('x')).toBeNull(); + }); +});