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 });