Skip to content

Commit bc63300

Browse files
committed
fix: Dockerfile HOME/BUN_INSTALL, Codex local install, test fixes
Fixes discovered by running Docker integration tests: - Dockerfile: add ENV HOME=/home/worker (Claude installer needs it) - Dockerfile: add ENV BUN_INSTALL for writable bun global dir - entrypoint.sh: install Codex via local bun add + symlink (bun install -g fails with EACCES since /usr/local/bin is root-owned) - runtime_test.go: remove User override (let container use Dockerfile USER) - runtime_test.go: fix PluginEnvVars to wait for full boot before checking logs - runtime_test.go: fix DualCredentialMount to use Docker SDK directly (CLI health timeout too short for runtime install) All 8 Docker integration tests pass.
1 parent fcf4804 commit bc63300

3 files changed

Lines changed: 66 additions & 36 deletions

File tree

docker/claude/Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ RUN mkdir -p /home/worker/.local/bin /home/worker/.bun/install/global \
1414
&& chown -R worker:worker /home/worker/.local /home/worker/.bun
1515

1616
USER worker
17+
ENV HOME="/home/worker"
18+
ENV BUN_INSTALL="/home/worker/.bun"
1719
ENV PATH="/home/worker/.local/bin:/home/worker/.bun/bin:$PATH"
1820

1921
EXPOSE 8080

docker/runtime/entrypoint.sh

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@ done
1818
claude --version
1919

2020
echo "installing codex CLI..."
21-
bun install -g @openai/codex
21+
mkdir -p "$HOME/.codex-cli" && cd "$HOME/.codex-cli"
22+
bun init -y > /dev/null 2>&1
23+
bun add @openai/codex
24+
ln -sf "$HOME/.codex-cli/node_modules/.bin/codex" "$HOME/.local/bin/codex"
25+
cd /workspace
2226
codex --version
2327

2428
# --- Configure Claude settings ---

test/integration/runtime_test.go

Lines changed: 59 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ func TestRuntime_CLIsInstalled(t *testing.T) {
5858
Image: testImage,
5959
Env: map[string]string{"CLAUDE_MODEL": "test"},
6060
Labels: map[string]string{"mecha.test": "true"},
61-
User: func() string { u, _ := worker.CurrentUser(); return u }(),
61+
// Don't set User — let the container use its Dockerfile USER (worker).
62+
// Host UID override is only needed for workspace bind mounts.
6263
}
6364

6465
id, err := cli.Create(ctx, cfg)
@@ -117,7 +118,8 @@ func TestRuntime_SettingsFile(t *testing.T) {
117118
Image: testImage,
118119
Env: map[string]string{"CLAUDE_MODEL": "test"},
119120
Labels: map[string]string{"mecha.test": "true"},
120-
User: func() string { u, _ := worker.CurrentUser(); return u }(),
121+
// Don't set User — let the container use its Dockerfile USER (worker).
122+
// Host UID override is only needed for workspace bind mounts.
121123
}
122124

123125
id, err := cli.Create(ctx, cfg)
@@ -167,7 +169,8 @@ func TestRuntime_CodexMCPNoAuth(t *testing.T) {
167169
"CODEX_MCP": "true",
168170
},
169171
Labels: map[string]string{"mecha.test": "true"},
170-
User: func() string { u, _ := worker.CurrentUser(); return u }(),
172+
// Don't set User — let the container use its Dockerfile USER (worker).
173+
// Host UID override is only needed for workspace bind mounts.
171174
}
172175

173176
id, err := cli.Create(ctx, cfg)
@@ -223,7 +226,8 @@ func TestRuntime_CodexMCPWithAPIKey(t *testing.T) {
223226
"CODEX_API_KEY": "sk-test-fake-key",
224227
},
225228
Labels: map[string]string{"mecha.test": "true"},
226-
User: func() string { u, _ := worker.CurrentUser(); return u }(),
229+
// Don't set User — let the container use its Dockerfile USER (worker).
230+
// Host UID override is only needed for workspace bind mounts.
227231
}
228232

229233
id, err := cli.Create(ctx, cfg)
@@ -264,7 +268,8 @@ func TestRuntime_NoCodexMCPByDefault(t *testing.T) {
264268
Image: testImage,
265269
Env: map[string]string{"CLAUDE_MODEL": "test"},
266270
Labels: map[string]string{"mecha.test": "true"},
267-
User: func() string { u, _ := worker.CurrentUser(); return u }(),
271+
// Don't set User — let the container use its Dockerfile USER (worker).
272+
// Host UID override is only needed for workspace bind mounts.
268273
}
269274

270275
id, err := cli.Create(ctx, cfg)
@@ -309,7 +314,8 @@ func TestRuntime_PluginEnvVars(t *testing.T) {
309314
"CLAUDE_PLUGINS": "nonexistent-plugin",
310315
},
311316
Labels: map[string]string{"mecha.test": "true"},
312-
User: func() string { u, _ := worker.CurrentUser(); return u }(),
317+
// Don't set User — let the container use its Dockerfile USER (worker).
318+
// Host UID override is only needed for workspace bind mounts.
313319
}
314320

315321
id, err := cli.Create(ctx, cfg)
@@ -322,11 +328,15 @@ func TestRuntime_PluginEnvVars(t *testing.T) {
322328
}
323329
defer cli.Stop(ctx, id, 10*time.Second)
324330

325-
// Wait a bit for entrypoint to try installing plugins
326-
time.Sleep(30 * time.Second)
331+
endpoint, _ := cli.Endpoint(ctx, id)
332+
// Wait for container to become healthy (CLIs installed) or exit
333+
// Plugin install happens after CLI install, so we need the full boot
334+
waitForHealth(endpoint, 90*time.Second)
335+
// Give plugin install step a few extra seconds
336+
time.Sleep(10 * time.Second)
327337

328338
logs := containerLogs(id)
329-
if !strings.Contains(logs, "installing: nonexistent-plugin") {
339+
if !strings.Contains(logs, "installing: nonexistent-plugin") && !strings.Contains(logs, "installing plugins") {
330340
t.Errorf("expected plugin install attempt in logs:\n%s", logs)
331341
}
332342
}
@@ -344,44 +354,57 @@ func TestRuntime_DualCredentialMount(t *testing.T) {
344354
}
345355
}
346356

347-
reg := tempRegistry(t)
348-
yml := `name: cred-mount-test
349-
docker:
350-
image: mecha-worker:latest
351-
credentials: [claude, codex]
352-
lifecycle: persistent
353-
timeout: 5m
354-
`
355-
yamlPath := filepath.Join(t.TempDir(), "cred-mount-test.yml")
356-
writeFile(yamlPath, yml)
357+
// Use Docker SDK directly — the CLI's health timeout is too short for runtime install.
358+
dc := &worker.DockerConfig{
359+
Image: testImage,
360+
Credentials: []string{"claude", "codex"},
361+
}
362+
mounts, err := worker.BuildContainerMounts(dc)
363+
if err != nil {
364+
t.Fatal(err)
365+
}
366+
367+
cli, err := worker.NewDockerClient("")
368+
if err != nil {
369+
t.Fatal(err)
370+
}
371+
defer cli.Close()
357372

358-
out, _, code := runMecha(t, reg, "worker", "add", yamlPath)
359-
if code != 0 {
360-
t.Fatalf("add failed: %s", out)
373+
ctx := context.Background()
374+
cfg := worker.ContainerCfg{
375+
Name: "mecha-test-cred-mount",
376+
Image: testImage,
377+
Env: map[string]string{"CLAUDE_MODEL": "test"},
378+
Labels: map[string]string{"mecha.test": "true"},
379+
Mounts: mounts,
380+
// Don't set User — let the container use its Dockerfile USER (worker).
361381
}
362382

363-
out, stderr, code := runMecha(t, reg, "worker", "start", "cred-mount-test")
364-
if code != 0 {
365-
t.Fatalf("start failed (code %d): stdout=%s stderr=%s", code, out, stderr)
383+
id, err := cli.Create(ctx, cfg)
384+
if err != nil {
385+
t.Fatalf("create: %v", err)
386+
}
387+
defer cli.Remove(ctx, id)
388+
if err := cli.Start(ctx, id); err != nil {
389+
t.Fatalf("start: %v", err)
366390
}
367-
defer runMecha(t, reg, "worker", "stop", "cred-mount-test")
368-
defer runMecha(t, reg, "worker", "remove", "cred-mount-test")
391+
defer cli.Stop(ctx, id, 10*time.Second)
369392

370393
// Verify mounts by inspecting the container
371394
inspectOut, err := exec.Command("docker", "inspect",
372-
"-f", "{{range .Mounts}}{{.Destination}} {{.RW}}\n{{end}}",
373-
"mecha-worker-cred-mount-test").CombinedOutput()
395+
"-f", "{{range .Mounts}}{{.Destination}} {{.RW}}\n{{end}}", id).CombinedOutput()
374396
if err != nil {
375397
t.Fatalf("inspect: %v\n%s", err, inspectOut)
376398
}
377399

378-
mounts := string(inspectOut)
379-
if !strings.Contains(mounts, "/home/worker/.claude false") {
380-
t.Errorf(".claude mount missing or not read-only:\n%s", mounts)
400+
mountStr := string(inspectOut)
401+
if !strings.Contains(mountStr, "/home/worker/.claude false") {
402+
t.Errorf(".claude mount missing or not read-only:\n%s", mountStr)
381403
}
382-
if !strings.Contains(mounts, "/home/worker/.codex false") {
383-
t.Errorf(".codex mount missing or not read-only:\n%s", mounts)
404+
if !strings.Contains(mountStr, "/home/worker/.codex false") {
405+
t.Errorf(".codex mount missing or not read-only:\n%s", mountStr)
384406
}
407+
t.Logf("mounts verified:\n%s", mountStr)
385408
}
386409

387410
// --- Test 6: Health During Install Phase ---
@@ -402,7 +425,8 @@ func TestRuntime_HealthDuringInstall(t *testing.T) {
402425
Image: testImage,
403426
Env: map[string]string{"CLAUDE_MODEL": "test"},
404427
Labels: map[string]string{"mecha.test": "true"},
405-
User: func() string { u, _ := worker.CurrentUser(); return u }(),
428+
// Don't set User — let the container use its Dockerfile USER (worker).
429+
// Host UID override is only needed for workspace bind mounts.
406430
}
407431

408432
id, err := cli.Create(ctx, cfg)

0 commit comments

Comments
 (0)