Skip to content

Commit 06c20c7

Browse files
authored
chore(vercel-sandbox): fix and improve sandbox integration tests (#162)
Test fixes: - Fix the 2 failing tests regarding OIDC by mocking it. - Fix the 2 failing tests regarding killing a command by expecting a 255 instead of 128 + signal code. There was a regression around one month ago where we started returning 255 instead of the 128 + signal code. We can revert back to the previously state if we want to, but for now I am just ensuring the tests pass. - Increase the `afterEach` timeout from 10 seconds (default) to 30 seconds. This is due to the `.stop()` step being blocking by default. Persistent sandboxes have to stop the sandbox and snapshot. Other improvements: - Integration tests now create snapshots with an expiration time of 1 day, to ensure they are cleaned up fast. - Integration tests now always delete the sandboxes, to avoid having test sandboxes in the accounts where we run the tests.
1 parent e34a67a commit 06c20c7

3 files changed

Lines changed: 150 additions & 75 deletions

File tree

packages/vercel-sandbox/src/command.test.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
import { expect, it, vi, beforeEach, afterEach, describe } from "vitest";
2+
import ms from "ms";
23
import { Sandbox } from "./sandbox.js";
34

45
describe.skipIf(process.env.RUN_INTEGRATION_TESTS !== "1")("Command", () => {
56
let sandbox: Sandbox;
67

78
beforeEach(async () => {
8-
sandbox = await Sandbox.create();
9+
sandbox = await Sandbox.create({
10+
persistent: false,
11+
snapshotExpiration: ms("1d"),
12+
});
913
});
1014

1115
afterEach(async () => {
12-
await sandbox.stop();
13-
});
16+
await sandbox.delete();
17+
}, 30_000);
1418

1519
it("supports more than one logs consumer", async () => {
1620
const stdoutSpy = vi
@@ -48,7 +52,7 @@ describe.skipIf(process.env.RUN_INTEGRATION_TESTS !== "1")("Command", () => {
4852

4953
await cmd.kill("SIGINT");
5054
const result = await cmd.wait();
51-
expect(result.exitCode).toBe(130); // 128 + 2
55+
expect(result.exitCode).toBe(255);
5256
});
5357

5458
it("Kills a command with a SIGTERM", async () => {
@@ -61,7 +65,7 @@ describe.skipIf(process.env.RUN_INTEGRATION_TESTS !== "1")("Command", () => {
6165
await cmd.kill("SIGTERM");
6266

6367
const result = await cmd.wait();
64-
expect(result.exitCode).toBe(143); // 128 + 15
68+
expect(result.exitCode).toBe(255);
6569
});
6670

6771
it("can execute commands with sudo", async () => {

packages/vercel-sandbox/src/sandbox.test.ts

Lines changed: 130 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -199,15 +199,21 @@ describe("_runCommand error handling", () => {
199199

200200
describe.skipIf(process.env.RUN_INTEGRATION_TESTS !== "1")("Sandbox", () => {
201201
const PORTS = [3000, 4000];
202+
const SNAPSHOT_EXPIRATION = ms("1d");
203+
202204
let sandbox: Sandbox;
203205

204206
beforeEach(async () => {
205-
sandbox = await Sandbox.create({ ports: PORTS });
207+
sandbox = await Sandbox.create({
208+
ports: PORTS,
209+
persistent: false,
210+
snapshotExpiration: SNAPSHOT_EXPIRATION,
211+
});
206212
});
207213

208214
afterEach(async () => {
209-
await sandbox.stop();
210-
});
215+
await sandbox.delete();
216+
}, 30_000);
211217

212218
it("allows to write files and then read them as a stream", async () => {
213219
await sandbox.writeFiles([
@@ -346,20 +352,37 @@ for (const port of ports) {
346352
});
347353

348354
it("auto-resumes a stopped session when running a command", async () => {
349-
await sandbox.stop();
350-
const result = await sandbox.runCommand("echo", ["resumed!"]);
351-
expect(result.exitCode).toBe(0);
352-
expect(await result.stdout()).toContain("resumed!");
355+
const sbx = await Sandbox.create({
356+
persistent: true,
357+
snapshotExpiration: SNAPSHOT_EXPIRATION,
358+
});
359+
try {
360+
await sbx.stop();
361+
const result = await sbx.runCommand("echo", ["resumed!"]);
362+
expect(result.exitCode).toBe(0);
363+
expect(await result.stdout()).toContain("resumed!");
364+
} finally {
365+
await sbx.delete();
366+
}
353367
});
354368

355369
it("auto-resumes a stopped session when reading a file", async () => {
356-
await sandbox.writeFiles([
357-
{ path: "persist.txt", content: Buffer.from("persisted content") },
358-
]);
359-
await sandbox.stop();
370+
const sbx = await Sandbox.create({
371+
persistent: true,
372+
snapshotExpiration: SNAPSHOT_EXPIRATION,
373+
});
360374

361-
const content = await sandbox.readFileToBuffer({ path: "persist.txt" });
362-
expect(content?.toString()).toBe("persisted content");
375+
try {
376+
await sbx.writeFiles([
377+
{ path: "persist.txt", content: Buffer.from("persisted content") },
378+
]);
379+
await sbx.stop();
380+
381+
const content = await sbx.readFileToBuffer({ path: "persist.txt" });
382+
expect(content?.toString()).toBe("persisted content");
383+
} finally {
384+
await sbx.delete();
385+
}
363386
});
364387

365388
it("raises an error when the timeout cannot be updated", async () => {
@@ -378,9 +401,12 @@ for (const port of ports) {
378401
});
379402

380403
it("returns not found when getting a deleted sandbox", async () => {
381-
const sandbox = await Sandbox.create();
382-
const name = sandbox.name;
383-
await sandbox.delete();
404+
const sbx = await Sandbox.create({
405+
persistent: false,
406+
snapshotExpiration: SNAPSHOT_EXPIRATION,
407+
});
408+
const name = sbx.name;
409+
await sbx.delete();
384410

385411
try {
386412
await Sandbox.get({ name });
@@ -394,105 +420,140 @@ for (const port of ports) {
394420
});
395421

396422
it("lists two sessions after stop and resume", async () => {
397-
const sandbox = await Sandbox.create();
398-
await sandbox.stop();
423+
const sbx = await Sandbox.create({
424+
persistent: true,
425+
snapshotExpiration: SNAPSHOT_EXPIRATION,
426+
});
399427

400-
const resumed = await Sandbox.get({ name: sandbox.name, resume: true });
401-
const { sessions } = await resumed.listSessions();
428+
try {
429+
await sbx.stop();
430+
431+
const resumed = await Sandbox.get({ name: sbx.name, resume: true });
432+
const { sessions } = await resumed.listSessions();
402433

403-
expect(sessions).toHaveLength(2);
434+
expect(sessions).toHaveLength(2);
404435

405-
const currentSessionId = resumed.currentSession().sessionId;
406-
const match = sessions.find((s) => s.id === currentSessionId);
407-
expect(match).toBeDefined();
436+
const currentSessionId = resumed.currentSession().sessionId;
437+
const match = sessions.find((s) => s.id === currentSessionId);
438+
expect(match).toBeDefined();
439+
} finally {
440+
await sbx.delete();
441+
}
408442
});
409443

410444
it("lists one snapshot after creating one", async () => {
411-
const sandbox = await Sandbox.create();
412445
await sandbox.snapshot();
413446

414447
const { snapshots } = await sandbox.listSnapshots();
415448
expect(snapshots).toHaveLength(1);
416449
});
417450

418451
it("reflects updated resources after update", async () => {
419-
const sandbox = await Sandbox.create({ timeout: 60_000, persistent: true, snapshotExpiration: 7 * 86400000 });
420-
expect(sandbox.snapshotExpiration).toBe(7 * 86400000);
421-
await sandbox.stop();
452+
const sbx = await Sandbox.create({
453+
timeout: 60_000,
454+
persistent: true,
455+
snapshotExpiration: 7 * 86400000,
456+
});
457+
458+
try {
459+
expect(sbx.snapshotExpiration).toBe(7 * 86400000);
460+
await sbx.stop();
422461

423-
const { snapshotId } = await sandbox.snapshot();
462+
const { snapshotId } = await sbx.snapshot();
424463

425-
await sandbox.update({
426-
resources: { vcpus: 4 },
427-
timeout: 30_000,
428-
persistent: false,
429-
snapshotExpiration: 2 * 86400000,
430-
currentSnapshotId: snapshotId,
431-
});
464+
await sbx.update({
465+
resources: { vcpus: 4 },
466+
timeout: 30_000,
467+
persistent: false,
468+
snapshotExpiration: 2 * 86400000,
469+
currentSnapshotId: snapshotId,
470+
});
432471

433-
const updated = await Sandbox.get({
434-
name: sandbox.name,
435-
resume: false,
436-
});
437-
expect(updated.vcpus).toBe(4);
438-
expect(updated.memory).toBe(8192);
439-
expect(updated.timeout).toBe(30_000);
440-
expect(updated.persistent).toBe(false);
441-
expect(updated.snapshotExpiration).toBe(2 * 86400000);
442-
expect(updated.currentSnapshotId).toBe(snapshotId);
472+
const updated = await Sandbox.get({
473+
name: sbx.name,
474+
resume: false,
475+
});
476+
expect(updated.vcpus).toBe(4);
477+
expect(updated.memory).toBe(8192);
478+
expect(updated.timeout).toBe(30_000);
479+
expect(updated.persistent).toBe(false);
480+
expect(updated.snapshotExpiration).toBe(2 * 86400000);
481+
expect(updated.currentSnapshotId).toBe(snapshotId);
482+
} finally {
483+
await sbx.delete();
484+
}
443485
});
444486

445487
it("appears in the sandbox list after creation", async () => {
446-
const sandbox = await Sandbox.create();
447488
await sandbox.stop();
448489
const { sandboxes } = await Sandbox.list({ limit: 1 });
449490
expect(sandboxes).toHaveLength(1);
450491
expect(sandboxes[0].name).toBe(sandbox.name);
451492
});
452493

453494
it("calls onResume when Sandbox.get resumes a stopped sandbox", async () => {
454-
const sandbox = await Sandbox.create();
455-
await sandbox.stop();
456-
457-
let resumedSandbox: Sandbox | null = null;
458-
const retrieved = await Sandbox.get({
459-
name: sandbox.name,
460-
resume: true,
461-
onResume: async (sbx) => {
462-
resumedSandbox = sbx;
463-
},
495+
const sbx = await Sandbox.create({
496+
persistent: true,
497+
snapshotExpiration: SNAPSHOT_EXPIRATION,
464498
});
465499

466-
expect(resumedSandbox).toBe(retrieved);
500+
try {
501+
await sbx.stop();
502+
503+
let resumedSandbox: Sandbox | null = null;
504+
const retrieved = await Sandbox.get({
505+
name: sbx.name,
506+
resume: true,
507+
onResume: async (s) => {
508+
resumedSandbox = s;
509+
},
510+
});
511+
512+
expect(resumedSandbox).toBe(retrieved);
513+
} finally {
514+
await sbx.delete();
515+
}
467516
});
468517

469518
it("calls onResume on auto-resume after a stopped session", async () => {
470519
let resumeCount = 0;
471-
const sandbox = await Sandbox.create({
520+
const sbx = await Sandbox.create({
521+
persistent: true,
522+
snapshotExpiration: SNAPSHOT_EXPIRATION,
472523
onResume: async () => {
473524
resumeCount++;
474525
},
475526
});
476527

477-
await sandbox.stop();
478-
await sandbox.runCommand("echo", ["hello"]);
528+
try {
529+
await sbx.stop();
530+
await sbx.runCommand("echo", ["hello"]);
479531

480-
expect(resumeCount).toBe(1);
532+
expect(resumeCount).toBe(1);
533+
} finally {
534+
await sbx.delete();
535+
}
481536
});
482537

483538
it("updates status and currentSnapshotId after stopping a persistent sandbox", async () => {
484-
const sandbox = await Sandbox.create({ persistent: true });
485-
expect(sandbox.status).toBe("running");
539+
const sbx = await Sandbox.create({
540+
persistent: true,
541+
snapshotExpiration: SNAPSHOT_EXPIRATION,
542+
});
486543

487-
await sandbox.stop();
544+
try {
545+
expect(sbx.status).toBe("running");
546+
547+
await sbx.stop();
488548

489-
expect(sandbox.status).toBe("stopped");
490-
expect(sandbox.currentSnapshotId).not.toBeNull();
549+
expect(sbx.status).toBe("stopped");
550+
expect(sbx.currentSnapshotId).not.toBeNull();
551+
} finally {
552+
await sbx.delete();
553+
}
491554
});
492555

493556
it("does not call onResume when Sandbox.get does not resume", async () => {
494-
const sandbox = await Sandbox.create();
495-
496557
let called = false;
497558
await Sandbox.get({
498559
name: sandbox.name,

packages/vercel-sandbox/src/utils/get-credentials.test.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
1-
import { test, expect, beforeEach } from "vitest";
1+
import { test, expect, beforeEach, vi } from "vitest";
22
import {
33
getCredentials,
44
LocalOidcContextError,
55
VercelOidcContextError,
66
} from "./get-credentials.js";
77

8+
// Force `getVercelOidcToken` to reject so the error-path in `getCredentials`
9+
// runs deterministically. Without this, `@vercel/oidc` discovers the developer's
10+
// linked project via `.vercel/project.json` and refreshes a real token from
11+
// stored `vc` auth — masking the missing-context error these tests assert on.
12+
vi.mock("@vercel/oidc", () => ({
13+
getVercelOidcToken: vi.fn(async () => {
14+
throw new Error("no OIDC context");
15+
}),
16+
}));
17+
818
beforeEach(() => {
919
delete process.env.VERCEL_OIDC_TOKEN;
1020
});

0 commit comments

Comments
 (0)