Skip to content

Commit a0bcb1d

Browse files
mchwangclaude
andcommitted
Test that the real docker is found once at load and that shims refuse without one (#189)
Review round 47: a unit test loads the fixture with docker on PATH, then puts another first, and checks REAL_DOCKER keeps the first; loaded with no docker, requireRealDocker refuses. A docker found through a relative PATH entry is resolved to an absolute path. The never-started agent container's comment gives the real reason it is tracked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 59ed09f commit a0bcb1d

3 files changed

Lines changed: 31 additions & 5 deletions

File tree

‎test/agent-container.test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -983,8 +983,8 @@ describe('real Docker agent isolation', () => {
983983

984984
it('never removes an agent container found by name after a killed create when a label is missing', async () => {
985985
const data = fixture(), live = await profile(data, 'planning', 'must-not-run');
986-
// Tracked for afterAll should the test time out: never started and without the runner label, it is no holder a
987-
// sweep finds once its network is gone (#189).
986+
// Tracked for afterAll should the test time out: it has no runner label, and until it goes the task volumes it
987+
// mounts cannot be removed (#189).
988988
containers.add(live.name);
989989
// The create lands without the runner label, and its client is killed before it reports the ID. Only this
990990
// container's: should the test time out with the shim still on PATH, other tests' creates pass through.

‎test/docker-sweep.test.ts‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
1+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
22
import { tmpdir } from 'node:os';
33
import { join } from 'node:path';
44
import { spawnSync } from 'node:child_process';
@@ -173,3 +173,26 @@ it.each([
173173
expect(warn.mock.calls.filter(call => String(call[0]).endsWith('failed:'))).toEqual(warnings);
174174
expect(readFileSync(calls, 'utf8').split('\n')).toContain(`rm --force ${a}`);
175175
});
176+
177+
it('finds the real docker once, at load, and refuses a shim when none was found', async () => {
178+
const root = mkdtempSync(join(tmpdir(), 'codeboost-sweep-')); roots.push(root);
179+
// Two directories, each with a docker and the sh the lookup runs, and one with only sh.
180+
const bin = (name: string, docker: boolean) => {
181+
const dir = join(root, name); mkdirSync(dir);
182+
symlinkSync(spawnSync('sh', ['-c', 'command -v sh'], { encoding: 'utf8' }).stdout.trim(), join(dir, 'sh'));
183+
if (docker) writeFileSync(join(dir, 'docker'), '#!/bin/sh\nexit 0\n', { mode: 0o755 });
184+
return dir;
185+
};
186+
const first = bin('first', true), second = bin('second', true), none = bin('none', false);
187+
vi.stubEnv('PATH', first);
188+
vi.resetModules();
189+
const loaded = await import('./fixtures/docker-sweep.ts') as typeof Sweep;
190+
// A shim put on PATH later does not change it.
191+
vi.stubEnv('PATH', second);
192+
expect(loaded.REAL_DOCKER).toBe(join(first, 'docker'));
193+
expect(loaded.requireRealDocker()).toBe(join(first, 'docker'));
194+
vi.stubEnv('PATH', none);
195+
vi.resetModules();
196+
const missing = await import('./fixtures/docker-sweep.ts') as typeof Sweep;
197+
expect(() => missing.requireRealDocker()).toThrow('No docker on PATH when the test files loaded.');
198+
});

‎test/fixtures/docker-sweep.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import { spawnSync, type SpawnSyncReturns } from 'node:child_process';
2+
import { isAbsolute, resolve } from 'node:path';
23
import { RUNNER_LABEL } from '../../agents/labels.ts';
34

45
/**
56
* The real `docker`, found once when this module loads, before any test puts a shim on PATH. A sweep that runs while a
67
* shim is still on PATH (after a test timed out inside one) would otherwise wait on the shim's own child, which the
78
* call's SIGKILL does not reach.
89
*/
9-
export const REAL_DOCKER = spawnSync('sh', ['-c', 'command -v docker'], { encoding: 'utf8' }).stdout?.trim() || 'docker';
10+
const FOUND_DOCKER = spawnSync('sh', ['-c', 'command -v docker'], { encoding: 'utf8' }).stdout?.trim();
11+
// Resolved, as a PATH entry may be relative.
12+
export const REAL_DOCKER = FOUND_DOCKER ? resolve(FOUND_DOCKER) : 'docker';
1013
/**
1114
* The daemon `docker` talks to when this module loads. A test that times out may leave DOCKER_HOST or DOCKER_CONTEXT
1215
* changed (agent-supervisor points DOCKER_HOST at a missing socket until its handle settles), so every call here
@@ -20,7 +23,7 @@ const dockerEnv = () => {
2023
};
2124
/** The real `docker` for a shim to call; refuses when none was found, since a shim calling `docker` would call itself. */
2225
export function requireRealDocker(): string {
23-
if (!REAL_DOCKER.startsWith('/')) throw new Error('No docker on PATH when the test files loaded.');
26+
if (!isAbsolute(REAL_DOCKER)) throw new Error('No docker on PATH when the test files loaded.');
2427
return REAL_DOCKER;
2528
}
2629

0 commit comments

Comments
 (0)