/ sdks / sandbox / javascript / tests / sandbox.create.test.mjs
sandbox.create.test.mjs
  1  import assert from "node:assert/strict";
  2  import test from "node:test";
  3  
  4  import {
  5    ConnectionConfig,
  6    DEFAULT_EGRESS_PORT,
  7    DEFAULT_EXECD_PORT,
  8    DEFAULT_TIMEOUT_SECONDS,
  9    Sandbox,
 10  } from "../dist/index.js";
 11  
 12  function createAdapterFactory() {
 13    const recordedRequests = [];
 14    const endpointCalls = [];
 15    const egressStackCalls = [];
 16    const egressService = {
 17      async getPolicy() {
 18        return {
 19          defaultAction: "deny",
 20          egress: [{ action: "allow", target: "pypi.org" }],
 21        };
 22      },
 23      async patchRules() {},
 24    };
 25    const sandboxes = {
 26      async createSandbox(req) {
 27        recordedRequests.push(req);
 28        return { id: "sandbox-test-id", expiresAt: null };
 29      },
 30      async getSandbox() {
 31        throw new Error("not implemented");
 32      },
 33      async listSandboxes() {
 34        throw new Error("not implemented");
 35      },
 36      async deleteSandbox() {},
 37      async pauseSandbox() {},
 38      async resumeSandbox() {},
 39      async renewSandboxExpiration() {
 40        throw new Error("not implemented");
 41      },
 42      async getSandboxEndpoint(_sandboxId, port) {
 43        endpointCalls.push(port);
 44        return { endpoint: `127.0.0.1:${port}`, headers: { "x-port": String(port) } };
 45      },
 46    };
 47  
 48    const adapterFactory = {
 49      createLifecycleStack() {
 50        return { sandboxes };
 51      },
 52      createExecdStack() {
 53        return {
 54          commands: {},
 55          files: {},
 56          health: {},
 57          metrics: {},
 58        };
 59      },
 60      createEgressStack(opts) {
 61        egressStackCalls.push(opts);
 62        return { egress: egressService };
 63      },
 64    };
 65  
 66    return { adapterFactory, recordedRequests, endpointCalls, egressStackCalls };
 67  }
 68  
 69  test("Sandbox.create omits timeout when timeoutSeconds is null", async () => {
 70    const { adapterFactory, recordedRequests } = createAdapterFactory();
 71  
 72    await Sandbox.create({
 73      adapterFactory,
 74      connectionConfig: { domain: "http://127.0.0.1:8080" },
 75      image: "python:3.12",
 76      timeoutSeconds: null,
 77      skipHealthCheck: true,
 78    });
 79  
 80    assert.equal(recordedRequests.length, 1);
 81    assert.ok(!Object.hasOwn(recordedRequests[0], "timeout"));
 82  });
 83  
 84  test("Sandbox.create forwards secureAccess", async () => {
 85    const { adapterFactory, recordedRequests } = createAdapterFactory();
 86  
 87    await Sandbox.create({
 88      adapterFactory,
 89      connectionConfig: { domain: "http://127.0.0.1:8080" },
 90      image: "python:3.12",
 91      secureAccess: true,
 92      skipHealthCheck: true,
 93    });
 94  
 95    assert.equal(recordedRequests.length, 1);
 96    assert.equal(recordedRequests[0].secureAccess, true);
 97  });
 98  
 99  test("Sandbox.create floors finite timeoutSeconds", async () => {
100    const { adapterFactory, recordedRequests } = createAdapterFactory();
101  
102    await Sandbox.create({
103      adapterFactory,
104      connectionConfig: { domain: "http://127.0.0.1:8080" },
105      image: "python:3.12",
106      timeoutSeconds: 61.9,
107      skipHealthCheck: true,
108    });
109  
110    assert.equal(recordedRequests.length, 1);
111    assert.equal(recordedRequests[0].timeout, 61);
112  });
113  
114  test("Sandbox.create uses the default timeout when timeoutSeconds is undefined", async () => {
115    const { adapterFactory, recordedRequests } = createAdapterFactory();
116  
117    await Sandbox.create({
118      adapterFactory,
119      connectionConfig: { domain: "http://127.0.0.1:8080" },
120      image: "python:3.12",
121      skipHealthCheck: true,
122    });
123  
124    assert.equal(recordedRequests.length, 1);
125    assert.equal(recordedRequests[0].timeout, DEFAULT_TIMEOUT_SECONDS);
126  });
127  
128  test("Sandbox.create rejects non-finite timeoutSeconds", async () => {
129    for (const timeoutSeconds of [Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY]) {
130      const { adapterFactory } = createAdapterFactory();
131      await assert.rejects(
132        Sandbox.create({
133          adapterFactory,
134          connectionConfig: { domain: "http://127.0.0.1:8080" },
135          image: "python:3.12",
136          timeoutSeconds,
137          skipHealthCheck: true,
138        }),
139        /timeoutSeconds must be a finite number/
140      );
141    }
142  });
143  
144  test("Sandbox creates and reuses egress service during sandbox lifecycle", async () => {
145    const { adapterFactory, endpointCalls, egressStackCalls } = createAdapterFactory();
146  
147    const sandbox = await Sandbox.create({
148      adapterFactory,
149      connectionConfig: { domain: "http://127.0.0.1:8080" },
150      image: "python:3.12",
151      skipHealthCheck: true,
152    });
153  
154    await sandbox.getEgressPolicy();
155    await sandbox.patchEgressRules([{ action: "allow", target: "www.github.com" }]);
156  
157    assert.deepEqual(endpointCalls, [DEFAULT_EXECD_PORT, DEFAULT_EGRESS_PORT]);
158    assert.equal(egressStackCalls.length, 1);
159    assert.equal(egressStackCalls[0].egressBaseUrl, `http://127.0.0.1:${DEFAULT_EGRESS_PORT}`);
160    assert.deepEqual(egressStackCalls[0].endpointHeaders, { "x-port": String(DEFAULT_EGRESS_PORT) });
161  });
162  
163  test("Sandbox.create passes OSSFS volume to request", async () => {
164    const { adapterFactory, recordedRequests } = createAdapterFactory();
165  
166    await Sandbox.create({
167      adapterFactory,
168      connectionConfig: { domain: "http://127.0.0.1:8080" },
169      image: "python:3.12",
170      skipHealthCheck: true,
171      volumes: [
172        {
173          name: "oss-data",
174          ossfs: {
175            bucket: "my-bucket",
176            endpoint: "oss-cn-hangzhou.aliyuncs.com",
177            version: "2.0",
178            accessKeyId: "ak-id",
179            accessKeySecret: "ak-secret",
180          },
181          mountPath: "/data",
182          readOnly: false,
183        },
184      ],
185    });
186  
187    assert.equal(recordedRequests.length, 1);
188    assert.equal(recordedRequests[0].volumes.length, 1);
189    assert.equal(recordedRequests[0].volumes[0].name, "oss-data");
190    assert.equal(recordedRequests[0].volumes[0].ossfs.bucket, "my-bucket");
191    assert.equal(recordedRequests[0].volumes[0].ossfs.endpoint, "oss-cn-hangzhou.aliyuncs.com");
192  });
193  
194  test("Sandbox.create rejects volume with no backend", async () => {
195    const { adapterFactory } = createAdapterFactory();
196  
197    await assert.rejects(
198      Sandbox.create({
199        adapterFactory,
200        connectionConfig: { domain: "http://127.0.0.1:8080" },
201        image: "python:3.12",
202        skipHealthCheck: true,
203        volumes: [{ name: "empty", mountPath: "/mnt" }],
204      }),
205      /must specify exactly one backend \(host, pvc, ossfs\)/
206    );
207  });
208  
209  test("Sandbox.create rejects volume with multiple backends", async () => {
210    const { adapterFactory } = createAdapterFactory();
211  
212    await assert.rejects(
213      Sandbox.create({
214        adapterFactory,
215        connectionConfig: { domain: "http://127.0.0.1:8080" },
216        image: "python:3.12",
217        skipHealthCheck: true,
218        volumes: [
219          {
220            name: "conflicting",
221            host: { path: "/tmp" },
222            ossfs: {
223              bucket: "b",
224              endpoint: "e",
225              accessKeyId: "id",
226              accessKeySecret: "secret",
227            },
228            mountPath: "/mnt",
229          },
230        ],
231      }),
232      /must specify exactly one backend \(host, pvc, ossfs\)/
233    );
234  });
235  
236  test("Sandbox.create accepts host volume with windows drive path", async () => {
237    const { adapterFactory, recordedRequests } = createAdapterFactory();
238  
239    await Sandbox.create({
240      adapterFactory,
241      connectionConfig: { domain: "http://127.0.0.1:8080" },
242      image: "python:3.12",
243      skipHealthCheck: true,
244      volumes: [{ name: "host-vol", host: { path: "D:/sandbox-mnt/ReMe" }, mountPath: "/mnt" }],
245    });
246  
247    assert.equal(recordedRequests.length, 1);
248    assert.equal(recordedRequests[0].volumes[0].host.path, "D:/sandbox-mnt/ReMe");
249  });
250  
251  test("Sandbox.create rejects host volume with relative path", async () => {
252    const { adapterFactory } = createAdapterFactory();
253  
254    await assert.rejects(
255      Sandbox.create({
256        adapterFactory,
257        connectionConfig: { domain: "http://127.0.0.1:8080" },
258        image: "python:3.12",
259        skipHealthCheck: true,
260        volumes: [{ name: "host-vol", host: { path: "relative/path" }, mountPath: "/mnt" }],
261      }),
262      /Host path must be an absolute path starting with '\/' or a Windows drive letter/
263    );
264  });
265  
266  test("Sandbox.create validates host path before transport initialization", async () => {
267    const { adapterFactory } = createAdapterFactory();
268    const connectionConfig = new ConnectionConfig({ domain: "http://127.0.0.1:8080" });
269    let transportInitialized = false;
270    connectionConfig.withTransportIfMissing = () => {
271      transportInitialized = true;
272      throw new Error("transport initialized");
273    };
274  
275    await assert.rejects(
276      Sandbox.create({
277        adapterFactory,
278        connectionConfig,
279        image: "python:3.12",
280        skipHealthCheck: true,
281        volumes: [{ name: "host-vol", host: { path: "relative/path" }, mountPath: "/mnt" }],
282      }),
283      /Host path must be an absolute path starting with '\/' or a Windows drive letter/
284    );
285    assert.equal(transportInitialized, false);
286  });
287  
288  test("Sandbox.create treats null backends as absent", async () => {
289    const { adapterFactory, recordedRequests } = createAdapterFactory();
290  
291    await Sandbox.create({
292      adapterFactory,
293      connectionConfig: { domain: "http://127.0.0.1:8080" },
294      image: "python:3.12",
295      skipHealthCheck: true,
296      volumes: [
297        {
298          name: "host-with-null-ossfs",
299          host: { path: "/tmp" },
300          ossfs: null,
301          pvc: undefined,
302          mountPath: "/mnt",
303        },
304      ],
305    });
306  
307    assert.equal(recordedRequests.length, 1);
308    assert.equal(recordedRequests[0].volumes[0].host.path, "/tmp");
309  });