Skip to content

Commit 820c2a2

Browse files
mchwangclaude
andcommitted
Every Docker client call the branch adds or moves in a test has a time limit (#189)
Review round 49: synchronous calls cannot be stopped by a Vitest timer, so each one the branch added, moved or rewrote (removals, inspects, the foreign creates and the ack-script run) takes CLIENT_LIMIT, 60 s with SIGKILL, as main's docker() helper did; the supervisor's removal message names a kill by signal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 515fe0c commit 820c2a2

4 files changed

Lines changed: 24 additions & 19 deletions

File tree

‎test/agent-container.test.ts‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { execFileSync, spawnSync } from 'node:child_process';
2-
import { removeByName, removeLabelledObjects, removeRunnerObjects, requireRealDocker, TEST_FILE_LABEL, TEST_RUN_LABEL } from './fixtures/docker-sweep.ts';
2+
import { CLIENT_LIMIT, removeByName, removeLabelledObjects, removeRunnerObjects, requireRealDocker, TEST_FILE_LABEL, TEST_RUN_LABEL } from './fixtures/docker-sweep.ts';
33
import { fixtureGit } from './fixtures/git.ts';
44
import { createHash, randomBytes, randomUUID } from 'node:crypto';
55
import { chmodSync, existsSync, mkdtempSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync,
@@ -490,8 +490,8 @@ describe('real Docker agent isolation', () => {
490490
docker('rm', '--force', valid.name); containers.delete(valid.name);
491491
} finally {
492492
// The container first: a volume it mounts cannot be removed.
493-
spawnSync('docker', ['rm', '--force', valid.name], { stdio: 'ignore' });
494-
spawnSync('docker', ['volume', 'rm', '--force', rogue], { stdio: 'ignore' });
493+
spawnSync('docker', ['rm', '--force', valid.name], { stdio: 'ignore', ...CLIENT_LIMIT });
494+
spawnSync('docker', ['volume', 'rm', '--force', rogue], { stdio: 'ignore', ...CLIENT_LIMIT });
495495
}
496496
}, 60_000);
497497

@@ -819,7 +819,7 @@ describe('real Docker agent isolation', () => {
819819
expect(docker('network', 'inspect', '--format', '{{.Id}}', network.slice(0, 12))).toBe(lookalike);
820820
// And an inspect naming a missing container exits non-zero yet prints the others, the answer the shim below gives.
821821
const partial = spawnSync('docker', ['container', 'inspect', '--format', '{{.Id}}', holders[0]!, `codeboost-gone-${randomUUID()}`],
822-
{ encoding: 'utf8' });
822+
{ encoding: 'utf8', ...CLIENT_LIMIT });
823823
expect(partial.status).not.toBe(0);
824824
expect(partial.stdout).toContain(holders[0]);
825825
expect(partial.stderr).toMatch(/^Error response from daemon: No such container: codeboost-gone-/m);
@@ -846,7 +846,7 @@ describe('real Docker agent isolation', () => {
846846
const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
847847
try {
848848
for (const [kind, name] of byName) removeByName(kind, name, 'test');
849-
expect(byName.filter(([kind, name]) => spawnSync('docker', [kind, 'inspect', name], { stdio: 'ignore' }).status === 0)).toEqual([]);
849+
expect(byName.filter(([kind, name]) => spawnSync('docker', [kind, 'inspect', name], { stdio: 'ignore', ...CLIENT_LIMIT }).status === 0)).toEqual([]);
850850
for (const [kind, name] of byName) removeByName(kind, name, 'test');
851851
expect(warn).not.toHaveBeenCalled();
852852
} finally { warn.mockRestore(); }
@@ -856,7 +856,7 @@ describe('real Docker agent isolation', () => {
856856
removeByName('network', lookalike.slice(0, 12), 'test');
857857
expect(docker('container', 'inspect', '--format', '{{.Id}}', bystanders[0]!.slice(0, 12))).toBe(bystanders[0]);
858858
removeByName('container', bystanders[0]!.slice(0, 12), 'test');
859-
const exists = (kind: string, ref: string) => spawnSync('docker', [kind, 'inspect', ref], { stdio: 'ignore' }).status === 0;
859+
const exists = (kind: string, ref: string) => spawnSync('docker', [kind, 'inspect', ref], { stdio: 'ignore', ...CLIENT_LIMIT }).status === 0;
860860
for (const holder of holders) expect(exists('container', holder)).toBe(false);
861861
expect([exists('network', network), exists('network', prefixNamed), exists('volume', volume)]).toEqual([false, false, false]);
862862
expect(exists('container', labelled)).toBe(false);
@@ -893,7 +893,7 @@ describe('real Docker agent isolation', () => {
893893
// First, so afterAll does not refuse a keeper this test finds left: its file-label sweep removes that keeper, then
894894
// its runner sweep the volumes.
895895
taskFilesystems.splice(taskFilesystems.indexOf(left), 1);
896-
expect(spawnSync('docker', ['container', 'inspect', left.keeper], { stdio: 'ignore' }).status).not.toBe(0);
896+
expect(spawnSync('docker', ['container', 'inspect', left.keeper], { stdio: 'ignore', ...CLIENT_LIMIT }).status).not.toBe(0);
897897
// Its volumes carry the runner label, so ordinary cleanup can remove them now that no keeper holds them.
898898
removeTaskFilesystems(left);
899899
expect(byAllocation(allocationId)).toEqual([]);
@@ -2559,7 +2559,7 @@ describe('real Docker agent isolation', () => {
25592559
removeTaskFilesystems(handle);
25602560
expect(isRecoveredTaskStorage(handle)).toBe(false);
25612561
for (const [kind, ref] of [['container', left.keeper], ['volume', left.workVolume], ['volume', left.metadataVolume]] as const)
2562-
expect(spawnSync('docker', [kind, 'inspect', ref], { stdio: 'ignore' }).status).not.toBe(0);
2562+
expect(spawnSync('docker', [kind, 'inspect', ref], { stdio: 'ignore', ...CLIENT_LIMIT }).status).not.toBe(0);
25632563
}, 180_000);
25642564

25652565
it('never removes a task keeper replaced by another runner, but still removes the owned volumes', () => {
@@ -2580,7 +2580,7 @@ describe('real Docker agent isolation', () => {
25802580
expect(spawnSync('docker', ['volume', 'inspect', filesystems.workVolume]).status).not.toBe(0);
25812581
passed = true;
25822582
} finally {
2583-
spawnSync('docker', ['rm', '--force', filesystems.keeper], { stdio: 'ignore' });
2583+
spawnSync('docker', ['rm', '--force', filesystems.keeper], { stdio: 'ignore', ...CLIENT_LIMIT });
25842584
// A failure above stays the one reported: what the removal leaves, the allocation sweep below removes, the keeper
25852585
// with another runner's label included (#189). After a pass, the storage must release once the foreign keeper is
25862586
// gone, so its failure fails the test. The sweep may take up to 5.5 minutes, which the test's budget allows for.

‎test/agent-network.test.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { assertVendorNetwork, createVendorNetwork, removeVendorNetwork, VENDOR_H
99
VendorNetworkCreationCleanupError,
1010
type VendorNetwork } from '../agents/network/network.ts';
1111
import { captureInvocation, type InvocationInput } from '../agents/contract.ts';
12-
import { removeByName, removeLabelledObjects, removeRunnerObjects, requireRealDocker, TEST_FILE_LABEL } from './fixtures/docker-sweep.ts';
12+
import { CLIENT_LIMIT, removeByName, removeLabelledObjects, removeRunnerObjects, requireRealDocker, TEST_FILE_LABEL } from './fixtures/docker-sweep.ts';
1313
// Each run has its own runner owner, so the sweep after this file removes only what this run left behind. Network and
1414
// proxy names are random, so the fixed attempt IDs below reach only labels, where this owner tells runs apart.
1515
const TEST_RUNNER_OWNER = randomBytes(16).toString('hex');
@@ -135,7 +135,7 @@ describe('vendor-only egress', () => {
135135
const made = existsSync(recorded) ? readFileSync(recorded, 'utf8').split('\n').filter(Boolean) : [];
136136
foreign = made[0] ?? '';
137137
// afterAll reads the record again: the object may land after this removal (#189).
138-
if (foreign) survived = spawnSync('docker', ['network', 'inspect', foreign], { stdio: 'ignore' }).status === 0;
138+
if (foreign) survived = spawnSync('docker', ['network', 'inspect', foreign], { stdio: 'ignore', ...CLIENT_LIMIT }).status === 0;
139139
for (const name of made) removeByName('network', name, 'finally');
140140
rmSync(shim, { recursive: true, force: true });
141141
}
@@ -191,7 +191,7 @@ describe('vendor-only egress', () => {
191191
const made = existsSync(recorded) ? readFileSync(recorded, 'utf8').split('\n').filter(Boolean) : [];
192192
foreign = made[0] ?? '';
193193
// afterAll reads the record again: the object may land after this removal (#189).
194-
if (foreign) survived = spawnSync('docker', ['container', 'inspect', foreign], { stdio: 'ignore' }).status === 0;
194+
if (foreign) survived = spawnSync('docker', ['container', 'inspect', foreign], { stdio: 'ignore', ...CLIENT_LIMIT }).status === 0;
195195
for (const name of made) removeByName('container', name, 'finally');
196196
rmSync(shim, { recursive: true, force: true });
197197
}
@@ -247,7 +247,7 @@ describe('vendor-only egress', () => {
247247
const made = existsSync(recorded) ? readFileSync(recorded, 'utf8').split('\n').filter(Boolean) : [];
248248
impostor = made[0] ?? '';
249249
// afterAll reads the record again: the object may land after this removal (#189).
250-
if (impostor) survived = spawnSync('docker', ['container', 'inspect', impostor], { stdio: 'ignore' }).status === 0;
250+
if (impostor) survived = spawnSync('docker', ['container', 'inspect', impostor], { stdio: 'ignore', ...CLIENT_LIMIT }).status === 0;
251251
for (const name of made) removeByName('container', name, 'finally');
252252
rmSync(shim, { recursive: true, force: true });
253253
}

‎test/agent-supervisor.test.ts‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { randomBytes, randomUUID } from 'node:crypto';
22
import { execFileSync, spawnSync } from 'node:child_process';
3-
import { removeByName, removeRunnerObjects, requireRealDocker } from './fixtures/docker-sweep.ts';
3+
import { CLIENT_LIMIT, removeByName, removeRunnerObjects, requireRealDocker } from './fixtures/docker-sweep.ts';
44
import { fixtureGit } from './fixtures/git.ts';
55
import { chmodSync, mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
66
import { tmpdir } from 'node:os';
@@ -393,7 +393,7 @@ describe('container invocation supervisor', () => {
393393
try {
394394
// Inside the try, so its removal runs even if the create's client fails after the daemon made it (#189).
395395
execFileSync('docker', ['create', '--name', current.name, '--label', 'io.codeboost.invocation=foreign',
396-
'--entrypoint', 'true', imageId], { stdio: 'ignore' });
396+
'--entrypoint', 'true', imageId], { stdio: 'ignore', ...CLIENT_LIMIT });
397397
const result = await startProfileInvocation(current).settled;
398398
expect(result.stopReason).toBe('capture-failure');
399399
expect(result.stderr).toMatch(/already in use|Conflict/);
@@ -407,8 +407,8 @@ describe('container invocation supervisor', () => {
407407
} finally {
408408
// A failure above stays the one reported, whatever exit status this Docker version gives for a missing name. After
409409
// a pass the container exists, and its removal must succeed, as on main.
410-
const removed = spawnSync('docker', ['rm', '--force', current.name], { encoding: 'utf8' });
411-
if (passed && removed.status !== 0) throw new Error(`Removing ${current.name} failed: ${removed.error?.message ?? removed.stderr?.trim()}`);
410+
const removed = spawnSync('docker', ['rm', '--force', current.name], { encoding: 'utf8', ...CLIENT_LIMIT });
411+
if (passed && removed.status !== 0) throw new Error(`Removing ${current.name} failed: ${removed.error?.message ?? (removed.signal ? `killed by ${removed.signal}` : removed.stderr?.trim())}`);
412412
}
413413
}, 3 * 60_000);
414414

@@ -450,7 +450,7 @@ describe('container invocation supervisor', () => {
450450
onTestFinished(() => removeByName('container', current.name, 'onTestFinished'), 90_000); // two calls of up to 30 s
451451
try {
452452
execFileSync('docker', ['create', '--name', current.name, '--label', 'io.codeboost.invocation=someone-else',
453-
'--entrypoint', 'true', imageId], { stdio: 'ignore' });
453+
'--entrypoint', 'true', imageId], { stdio: 'ignore', ...CLIENT_LIMIT });
454454
await expectRejected(startProfileInvocation(current, { timeoutMs: 10 * 60_000 + 1 }), 'ceiling');
455455
expect(isContainerProfileAuthentic(current)).toBe(false);
456456
expect(spawnSync('docker', ['container', 'inspect', current.name], { stdio: 'ignore' }).status).toBe(0);
@@ -658,7 +658,7 @@ describe('container invocation supervisor', () => {
658658
execFileSync('docker', ['run', '-d', '--rm', '--name', name, '--network', 'none', '--cap-drop=ALL',
659659
'--security-opt=no-new-privileges', '--user', '10001:10001', '--entrypoint', 'sleep',
660660
'--tmpfs', '/run/codeboost-control:rw,nosuid,nodev,noexec,size=65536,nr_inodes=16,uid=0,gid=0,mode=0711',
661-
imageId, '120'], { stdio: 'ignore' });
661+
imageId, '120'], { stdio: 'ignore', ...CLIENT_LIMIT });
662662
const acknowledge = () => spawnSync('docker', ['exec', '--user', '0', name, 'node', '-e', ACKNOWLEDGEMENT_SCRIPT,
663663
token], { encoding: 'utf8' });
664664
const content = () => execFileSync('docker', ['exec', name, 'cat', file], { encoding: 'utf8' });

‎test/fixtures/docker-sweep.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ const dockerEnv = () => {
2121
for (const [name, value] of Object.entries(LOADED_ENV)) if (value === undefined) delete env[name]; else env[name] = value;
2222
return env;
2323
};
24+
/**
25+
* A Docker client call's own limit, for a test's synchronous calls (#189): no Vitest timer can stop one, so a stalled
26+
* daemon would otherwise hold the whole suite.
27+
*/
28+
export const CLIENT_LIMIT = { timeout: 60_000, killSignal: 'SIGKILL' as const };
2429
/** The real `docker` for a shim to call; refuses when none was found, since a shim calling `docker` would call itself. */
2530
export function requireRealDocker(): string {
2631
if (!isAbsolute(REAL_DOCKER)) throw new Error('No docker on PATH when the test files loaded.');

0 commit comments

Comments
 (0)