155 lines
5.1 KiB
TypeScript
155 lines
5.1 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
|
|
|
vi.mock('../../src/env', () => ({ env: { boxIdleMs: 1000 } }));
|
|
vi.mock('../../src/runtime/docker', () => ({
|
|
ensureUserContainer: vi.fn((args: { username: string }) =>
|
|
Promise.resolve(`spoon-box-${args.username}`),
|
|
),
|
|
stopWorkspaceContainer: vi.fn(() => Promise.resolve()),
|
|
userContainerName: (username: string) => `spoon-box-${username}`,
|
|
listWorkspaceContainerNames: vi.fn(() => Promise.resolve([])),
|
|
}));
|
|
|
|
const load = async () => await import('../../src/user-container');
|
|
|
|
describe('box registry', () => {
|
|
beforeEach(() => vi.useFakeTimers());
|
|
afterEach(async () => {
|
|
const module = await load();
|
|
module._resetBoxRegistryForTests();
|
|
vi.useRealTimers();
|
|
vi.resetModules();
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
test('serializes concurrent acquire into a single docker run', async () => {
|
|
const module = await load();
|
|
const docker = await import('../../src/runtime/docker');
|
|
const [a, b] = await Promise.all([
|
|
module.acquireUserBox({
|
|
username: 'alice',
|
|
workdir: '/w',
|
|
containerHome: '/home/alice',
|
|
}),
|
|
module.acquireUserBox({
|
|
username: 'alice',
|
|
workdir: '/w',
|
|
containerHome: '/home/alice',
|
|
}),
|
|
]);
|
|
expect(docker.ensureUserContainer).toHaveBeenCalledTimes(1);
|
|
expect(a.boxName).toBe('spoon-box-alice');
|
|
a.release();
|
|
await vi.advanceTimersByTimeAsync(2000);
|
|
expect(docker.stopWorkspaceContainer).not.toHaveBeenCalled();
|
|
b.release();
|
|
await vi.advanceTimersByTimeAsync(2000);
|
|
expect(docker.stopWorkspaceContainer).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test('double-release of one handle does not reap a box held elsewhere', async () => {
|
|
const module = await load();
|
|
const docker = await import('../../src/runtime/docker');
|
|
const a = await module.acquireUserBox({
|
|
username: 'bob',
|
|
workdir: '/w',
|
|
containerHome: '/home/bob',
|
|
});
|
|
const b = await module.acquireUserBox({
|
|
username: 'bob',
|
|
workdir: '/w',
|
|
containerHome: '/home/bob',
|
|
});
|
|
a.release();
|
|
a.release();
|
|
await vi.advanceTimersByTimeAsync(2000);
|
|
expect(docker.stopWorkspaceContainer).not.toHaveBeenCalled();
|
|
b.release();
|
|
await vi.advanceTimersByTimeAsync(2000);
|
|
expect(docker.stopWorkspaceContainer).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test('reconcileExistingBoxes adopts orphans with zero refs and reaps them', async () => {
|
|
const module = await load();
|
|
const docker = await import('../../src/runtime/docker');
|
|
(
|
|
docker.listWorkspaceContainerNames as unknown as ReturnType<typeof vi.fn>
|
|
).mockResolvedValueOnce(['spoon-box-carol']);
|
|
await module.reconcileExistingBoxes();
|
|
expect(module.runningBoxUsernames().has('carol')).toBe(true);
|
|
await vi.advanceTimersByTimeAsync(2000);
|
|
expect(docker.stopWorkspaceContainer).toHaveBeenCalledWith(
|
|
'spoon-box-carol',
|
|
);
|
|
});
|
|
|
|
test('retries ensure after the first acquire fails', async () => {
|
|
const module = await load();
|
|
const docker = await import('../../src/runtime/docker');
|
|
const ensure = vi.mocked(docker.ensureUserContainer);
|
|
ensure
|
|
.mockRejectedValueOnce(new Error('docker failed'))
|
|
.mockResolvedValueOnce('spoon-box-dana');
|
|
|
|
await expect(
|
|
module.acquireUserBox({
|
|
username: 'dana',
|
|
workdir: '/w',
|
|
containerHome: '/home/dana',
|
|
}),
|
|
).rejects.toThrow('docker failed');
|
|
|
|
const handle = await module.acquireUserBox({
|
|
username: 'dana',
|
|
workdir: '/w',
|
|
containerHome: '/home/dana',
|
|
});
|
|
expect(ensure).toHaveBeenCalledTimes(2);
|
|
expect(handle.boxName).toBe('spoon-box-dana');
|
|
});
|
|
|
|
test('finishes an old idle reap before a new acquire can ensure the box', async () => {
|
|
const module = await load();
|
|
const docker = await import('../../src/runtime/docker');
|
|
const ensure = vi.mocked(docker.ensureUserContainer);
|
|
const stop = vi.mocked(docker.stopWorkspaceContainer);
|
|
const events: string[] = [];
|
|
let finishStop!: () => void;
|
|
const stopDone = new Promise<void>((resolve) => {
|
|
finishStop = resolve;
|
|
});
|
|
ensure.mockReset().mockImplementation((args: { username: string }) => {
|
|
events.push('ensure');
|
|
return Promise.resolve(`spoon-box-${args.username}`);
|
|
});
|
|
stop.mockReset().mockImplementationOnce(() => {
|
|
events.push('stop:start');
|
|
return stopDone.then(() => {
|
|
events.push('stop:end');
|
|
});
|
|
});
|
|
|
|
const first = await module.acquireUserBox({
|
|
username: 'erin',
|
|
workdir: '/w',
|
|
containerHome: '/home/erin',
|
|
});
|
|
first.release();
|
|
await vi.advanceTimersByTimeAsync(1000);
|
|
|
|
const secondPromise = module.acquireUserBox({
|
|
username: 'erin',
|
|
workdir: '/w',
|
|
containerHome: '/home/erin',
|
|
});
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
expect(events).toEqual(['ensure', 'stop:start']);
|
|
|
|
finishStop();
|
|
const second = await secondPromise;
|
|
expect(second.boxName).toBe('spoon-box-erin');
|
|
expect(events).toEqual(['ensure', 'stop:start', 'stop:end', 'ensure']);
|
|
});
|
|
});
|