Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions packages/sandbox/src/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import {
InvalidBackupConfigError,
PortNotExposedError,
ProcessExitedBeforeReadyError,
ProcessNotFoundError,
ProcessReadyTimeoutError,
SandboxError,
SessionAlreadyExistsError
Expand Down Expand Up @@ -3363,8 +3364,13 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {

async getProcess(id: string, sessionId?: string): Promise<Process | null> {
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;
}

Expand Down
79 changes: 78 additions & 1 deletion packages/sandbox/tests/sandbox.test.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -2566,7 +2566,7 @@

// All three callers are awaiting the same underlying work; the parent
// container destroy must only be invoked once.
expect(superDestroy.calls()).toBe(1);

Check failure on line 2569 in packages/sandbox/tests/sandbox.test.ts

View workflow job for this annotation

GitHub Actions / quality / sdk-tests

tests/sandbox.test.ts > Sandbox - Automatic Session Management > destroy() coalescing > coalesces concurrent destroy() calls onto a single teardown

AssertionError: expected +0 to be 1 // Object.is equality - Expected + Received - 1 + 0 ❯ tests/sandbox.test.ts:2569:36

superDestroy.resolve();
await expect(Promise.all([first, second, third])).resolves.toEqual([
Expand All @@ -2576,7 +2576,7 @@
]);
});

it('propagates the same rejection to all coalesced callers', async () => {

Check failure on line 2579 in packages/sandbox/tests/sandbox.test.ts

View workflow job for this annotation

GitHub Actions / quality / sdk-tests

tests/sandbox.test.ts > Sandbox - Automatic Session Management > destroy() coalescing > propagates the same rejection to all coalesced callers

Error: Test timed out in 10000ms. If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". ❯ tests/sandbox.test.ts:2579:5
const superDestroy = stubSuperDestroy();
const first = sandbox.destroy();
const second = sandbox.destroy();
Expand All @@ -2590,7 +2590,7 @@
it('runs a fresh teardown for a later destroy() after the previous one settles', async () => {
const first = stubSuperDestroy();
const firstCall = sandbox.destroy();
expect(first.calls()).toBe(1);

Check failure on line 2593 in packages/sandbox/tests/sandbox.test.ts

View workflow job for this annotation

GitHub Actions / quality / sdk-tests

tests/sandbox.test.ts > Sandbox - Automatic Session Management > destroy() coalescing > runs a fresh teardown for a later destroy() after the previous one settles

AssertionError: expected +0 to be 1 // Object.is equality - Expected + Received - 1 + 0 ❯ tests/sandbox.test.ts:2593:29
first.resolve();
await firstCall;

Expand Down Expand Up @@ -2719,3 +2719,80 @@
});
});
});

// ---------------------------------------------------------------------------
// 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(<T>(cb: () => Promise<T>) => 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();
});
});
Loading