package workspaceruntime import ( "os" "path/filepath" "testing" "inbox/internal/domain/lane" "inbox/internal/domain/workspace" ) func TestDriftedWhenRunnerPortIsMissing(t *testing.T) { ws := workspace.Workspace{ ID: "ws_1", RootPath: "/tmp/ws", } item := lane.Record{ ID: "chain_1", ContainerName: "lane-demo", WorktreePath: "/tmp/ws-chain", } runtime := &containerRuntime{serverPort: 3000} info := validInspect(ws, item, "/tmp/lane-worker", "/tmp/inbox", "codex-sha-1") delete(info.NetworkSettings.Ports, runnerContainerPort) if !runtime.laneDrifted(info, ws, item, "/tmp/lane-worker", "/tmp/inbox", "codex-sha-1") { t.Fatalf("expected container without %s mapping to drift", runnerContainerPort) } } func TestDriftedWhenRunnerPortIsPublished(t *testing.T) { ws := workspace.Workspace{ ID: "ws_1", RootPath: "/tmp/ws", } item := lane.Record{ ID: "chain_1", ContainerName: "lane-demo", WorktreePath: "/tmp/ws-chain", } runtime := &containerRuntime{serverPort: 3000} info := validInspect(ws, item, "/tmp/lane-worker", "/tmp/inbox", "codex-sha-1") if runtime.laneDrifted(info, ws, item, "/tmp/lane-worker", "/tmp/inbox", "codex-sha-1") { t.Fatalf("expected container with %s mapping to remain reusable", runnerContainerPort) } } func TestDriftedWhenContainerUserIsNotRoot(t *testing.T) { ws := workspace.Workspace{ID: "ws_1", RootPath: "/tmp/ws"} item := lane.Record{ ID: "chain_1", ContainerName: "lane-demo", WorktreePath: "/tmp/ws-chain", } runtime := &containerRuntime{serverPort: 3000} info := validInspect(ws, item, "/tmp/lane-worker", "/tmp/inbox", "codex-sha-1") info.Config.User = "runner" if !runtime.laneDrifted(info, ws, item, "/tmp/lane-worker", "/tmp/inbox", "codex-sha-1") { t.Fatalf("expected non-root container user to drift") } } func TestDriftedWhenRuntimeCodexFingerprintChanges(t *testing.T) { ws := workspace.Workspace{ID: "ws_1", RootPath: "/tmp/ws"} item := lane.Record{ ID: "chain_1", ContainerName: "lane-demo", WorktreePath: "/tmp/ws-chain", } runtime := &containerRuntime{serverPort: 3000} info := validInspect(ws, item, "/tmp/lane-worker", "/tmp/inbox", "codex-sha-1") if !runtime.laneDrifted(info, ws, item, "/tmp/lane-worker", "/tmp/inbox", "codex-sha-2") { t.Fatalf("expected runtime codex fingerprint mismatch to drift") } } func TestRuntimeCodexFingerprint(t *testing.T) { root := t.TempDir() if err := os.WriteFile(filepath.Join(root, "config.toml"), []byte("model = \"gpt-5.3-codex\"\n"), 0644); err != nil { t.Fatalf("WriteFile(config.toml) error = %v", err) } if err := os.WriteFile(filepath.Join(root, "auth.json"), []byte("{\"OPENAI_API_KEY\":\"token\"}\n"), 0600); err != nil { t.Fatalf("WriteFile(auth.json) error = %v", err) } sum1, err := runtimeCodexFingerprint(root) if err != nil { t.Fatalf("runtimeCodexFingerprint() error = %v", err) } if err := os.WriteFile(filepath.Join(root, "auth.json"), []byte("{\"OPENAI_API_KEY\":\"changed\"}\n"), 0600); err != nil { t.Fatalf("WriteFile(auth.json) error = %v", err) } sum2, err := runtimeCodexFingerprint(root) if err != nil { t.Fatalf("runtimeCodexFingerprint() error = %v", err) } if sum1 == "" || sum2 == "" { t.Fatalf("expected non-empty fingerprints, got %q and %q", sum1, sum2) } if sum1 == sum2 { t.Fatalf("expected fingerprint to change when runtime codex contents change") } } func TestRuntimeCodexFingerprintChangesForNestedSkillFiles(t *testing.T) { root := t.TempDir() skillDir := filepath.Join(root, "skills", "inbox") if err := os.MkdirAll(skillDir, 0755); err != nil { t.Fatalf("MkdirAll(skillDir) error = %v", err) } if err := os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte("# Inbox\n"), 0644); err != nil { t.Fatalf("WriteFile(SKILL.md) error = %v", err) } sum1, err := runtimeCodexFingerprint(root) if err != nil { t.Fatalf("runtimeCodexFingerprint() error = %v", err) } if err := os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte("# Inbox\n\nUpdated\n"), 0644); err != nil { t.Fatalf("WriteFile(SKILL.md) error = %v", err) } sum2, err := runtimeCodexFingerprint(root) if err != nil { t.Fatalf("runtimeCodexFingerprint() error = %v", err) } if sum1 == sum2 { t.Fatalf("expected nested skill file change to affect fingerprint") } } func validInspect(ws workspace.Workspace, item lane.Record, workerBinary, inboxBinary, codexFingerprint string) podmanInspect { var info podmanInspect info.ImageName = runtimeBaseImage info.Path = laneWorkerContainerPath info.Config.Image = runtimeBaseImage info.Config.User = "root" info.Config.WorkingDir = "/workspace" info.Config.Env = []string{ "INBOX_WORKSPACE=/workspace", "INBOX_WORKSPACE_ID=" + ws.ID, "INBOX_LANE_ID=" + item.ID, "INBOX_RUNTIME_AGENT_ID=" + item.ContainerName, "INBOX_API_URL=http://host.containers.internal:3000", "HOME=/root", "INBOX_RUNTIME_CODEX_SHA=" + codexFingerprint, } info.NetworkSettings.Ports = map[string][]struct { HostIP string `json:"HostIp"` HostPort string `json:"HostPort"` }{ runnerContainerPort: {{ HostIP: "127.0.0.1", HostPort: "40123", }}, } info.Mounts = []struct { Source string `json:"Source"` Destination string `json:"Destination"` }{ {Source: item.WorktreePath, Destination: "/workspace"}, {Source: workerBinary, Destination: laneWorkerContainerPath}, {Source: inboxBinary, Destination: inboxContainerPath}, } return info }