Files
multica/server/internal/daemon/execenv/codex_home_sessions_test.go
2026-08-09 16:40:07 +08:00

791 lines
33 KiB
Go

package execenv
import (
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
)
// seedFakeRollout writes a fake Codex rollout for sessionID under
// sharedSessions/YYYY/MM/DD, mirroring Codex's real layout, and returns its path.
func seedFakeRollout(t *testing.T, sharedSessions, y, m, d, sessionID string, size int) string {
t.Helper()
dir := filepath.Join(sharedSessions, y, m, d)
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("mkdir rollout dir: %v", err)
}
path := filepath.Join(dir, "rollout-"+y+"-"+m+"-"+d+"T00-00-00-"+sessionID+".jsonl")
body := make([]byte, size)
for i := range body {
body[i] = 'x'
}
if err := os.WriteFile(path, body, 0o644); err != nil {
t.Fatalf("write rollout: %v", err)
}
return path
}
// seedLegacySessionsSymlink recreates the pre-MUL-4424 layout: codex-home's
// sessions is a symlink into the shared ~/.codex/sessions. Skips on Windows
// sessions where symlink creation is unavailable.
func seedLegacySessionsSymlink(t *testing.T, codexHome, sharedSessions string) {
t.Helper()
if err := os.MkdirAll(codexHome, 0o755); err != nil {
t.Fatalf("mkdir codex-home: %v", err)
}
if err := os.MkdirAll(sharedSessions, 0o755); err != nil {
t.Fatalf("mkdir shared sessions: %v", err)
}
if err := os.Symlink(sharedSessions, filepath.Join(codexHome, "sessions")); err != nil {
if runtime.GOOS == "windows" {
t.Skipf("directory symlink unavailable on this Windows session: %v", err)
}
t.Fatalf("seed legacy sessions symlink: %v", err)
}
}
func TestPrepareCodexSessionsDir_FreshCreatesEmptyLocalDir(t *testing.T) {
t.Parallel()
codexHome := filepath.Join(t.TempDir(), "codex-home")
if err := os.MkdirAll(codexHome, 0o755); err != nil {
t.Fatalf("mkdir codex-home: %v", err)
}
sharedHome := t.TempDir()
if err := prepareCodexSessionsDir(codexHome, sharedHome, CodexHomeOptions{}, testLogger()); err != nil {
t.Fatalf("prepareCodexSessionsDir: %v", err)
}
sessions := filepath.Join(codexHome, "sessions")
fi, err := os.Lstat(sessions)
if err != nil {
t.Fatalf("sessions not created: %v", err)
}
if fi.Mode()&os.ModeSymlink != 0 {
t.Error("fresh sessions must be a real dir, not a symlink")
}
if !fi.IsDir() {
t.Error("fresh sessions must be a directory")
}
entries, _ := os.ReadDir(sessions)
if len(entries) != 0 {
t.Errorf("fresh sessions must be empty, has %d entries", len(entries))
}
}
func TestPrepareCodexSessionsDir_RealDirIsAuthoritative(t *testing.T) {
t.Parallel()
codexHome := filepath.Join(t.TempDir(), "codex-home")
sessions := filepath.Join(codexHome, "sessions", "2026", "07", "13")
if err := os.MkdirAll(sessions, 0o755); err != nil {
t.Fatalf("mkdir sessions: %v", err)
}
// A rollout the prior (isolated) run already wrote into the task-local dir.
own := filepath.Join(sessions, "rollout-2026-07-13T00-00-00-own-session.jsonl")
if err := os.WriteFile(own, []byte("keep me"), 0o644); err != nil {
t.Fatalf("write own rollout: %v", err)
}
if err := prepareCodexSessionsDir(codexHome, t.TempDir(), CodexHomeOptions{}, testLogger()); err != nil {
t.Fatalf("prepareCodexSessionsDir: %v", err)
}
// The task-local dir is authoritative — its contents must survive untouched.
if data, err := os.ReadFile(own); err != nil || string(data) != "keep me" {
t.Errorf("task-local rollout must be preserved, got err=%v data=%q", err, data)
}
fi, _ := os.Lstat(filepath.Join(codexHome, "sessions"))
if fi.Mode()&os.ModeSymlink != 0 {
t.Error("authoritative sessions must remain a real dir")
}
}
func TestPrepareCodexSessionsDir_MigratesLegacySymlinkNoResume(t *testing.T) {
t.Parallel()
root := t.TempDir()
codexHome := filepath.Join(root, "codex-home")
sharedSessions := filepath.Join(root, "shared", "sessions")
// Simulate a machine with accumulated global history.
seedFakeRollout(t, sharedSessions, "2026", "07", "13", "other-session-a", 16)
seedFakeRollout(t, sharedSessions, "2026", "07", "12", "other-session-b", 16)
seedLegacySessionsSymlink(t, codexHome, sharedSessions)
// Session-derived state that indexed the whole global history, plus an
// unrelated per-task DB that must NOT be touched.
writeFile(t, filepath.Join(codexHome, "state_5.sqlite"), "stale")
writeFile(t, filepath.Join(codexHome, "state_5.sqlite-wal"), "stale")
writeFile(t, filepath.Join(codexHome, "session_index.jsonl"), "names")
writeFile(t, filepath.Join(codexHome, "goals_1.sqlite"), "keep")
if err := prepareCodexSessionsDir(codexHome, filepath.Dir(sharedSessions), CodexHomeOptions{}, testLogger()); err != nil {
t.Fatalf("prepareCodexSessionsDir: %v", err)
}
sessions := filepath.Join(codexHome, "sessions")
fi, err := os.Lstat(sessions)
if err != nil {
t.Fatalf("sessions missing after migration: %v", err)
}
if fi.Mode()&os.ModeSymlink != 0 {
t.Error("legacy symlink must be replaced with a real dir")
}
// No resume requested -> none of the global history is pulled in.
entries, _ := os.ReadDir(sessions)
if len(entries) != 0 {
t.Errorf("non-resume migration must yield an empty sessions dir, has %d entries", len(entries))
}
// The global history itself must be left intact.
if _, err := os.Stat(filepath.Join(sharedSessions, "2026", "07", "13", "rollout-2026-07-13T00-00-00-other-session-a.jsonl")); err != nil {
t.Errorf("shared history must not be deleted: %v", err)
}
// Rebuildable SQLite state dropped; the thread-name index and unrelated DBs
// are preserved.
assertAbsent(t, filepath.Join(codexHome, "state_5.sqlite"))
assertAbsent(t, filepath.Join(codexHome, "state_5.sqlite-wal"))
assertPresent(t, filepath.Join(codexHome, "session_index.jsonl"))
assertPresent(t, filepath.Join(codexHome, "goals_1.sqlite"))
}
func TestPrepareCodexSessionsDir_MigrateWithResumeRoutesThroughStore(t *testing.T) {
t.Parallel()
root := t.TempDir()
sharedHome := filepath.Join(root, "shared")
codexHome := filepath.Join(root, "codex-home")
sharedSessions := filepath.Join(sharedHome, "sessions")
resumeID := "019f59d9-a6aa-7a53-b173-1eccc4b4c873"
resumeSrc := seedFakeRollout(t, sharedSessions, "2026", "07", "13", resumeID, 32)
seedFakeRollout(t, sharedSessions, "2026", "07", "11", "unrelated-session", 32)
seedLegacySessionsSymlink(t, codexHome, sharedSessions)
writeFile(t, filepath.Join(codexHome, "state_5.sqlite"), "stale")
key := filepath.Join("agent-1", "issue-1")
err := prepareCodexSessionsDir(codexHome, sharedHome, CodexHomeOptions{ResumeSessionID: resumeID, SessionStoreKey: key}, testLogger())
if err != nil {
t.Fatalf("prepareCodexSessionsDir: %v", err)
}
// sessions/ is now a directory link to the per-issue store on the shared
// volume — the cross-volume-safe exposure (a same-volume hard link into the
// store + a directory link into the task home), never a task-local file copy.
sessions := filepath.Join(codexHome, "sessions")
assertSessionsLinkedToStore(t, sessions, codexSessionStoreDir(sharedHome, key))
// The resume rollout is present in the task home (through the link) and
// materialised as a zero-copy hard link from the shared history.
if !CodexResumeRolloutPresent(codexHome, resumeID) {
t.Fatal("resume rollout must be visible in the task CODEX_HOME after migration")
}
stored := filepath.Join(codexSessionStoreDir(sharedHome, key), "2026", "07", "13", filepath.Base(resumeSrc))
assertZeroCopyLink(t, resumeSrc, stored)
// The unrelated session must NOT be pulled in.
if CodexResumeRolloutPresent(codexHome, "unrelated-session") {
t.Error("unrelated shared history must not be exposed to the task")
}
// Stale state dropped so Codex rebuilds from the scoped store.
assertAbsent(t, filepath.Join(codexHome, "state_5.sqlite"))
}
func TestExposeResumeRollout_NoMatchReturnsError(t *testing.T) {
t.Parallel()
shared := filepath.Join(t.TempDir(), "sessions")
if err := os.MkdirAll(shared, 0o755); err != nil {
t.Fatalf("mkdir: %v", err)
}
local := filepath.Join(t.TempDir(), "sessions")
if err := os.MkdirAll(local, 0o755); err != nil {
t.Fatalf("mkdir: %v", err)
}
if err := exposeResumeRollout(shared, local, "missing-session", testLogger()); err == nil {
t.Error("expected error when no rollout matches the resume session ID")
}
}
func TestExposeResumeRollout_LinksLargeFileWithoutCopying(t *testing.T) {
t.Parallel()
root := t.TempDir()
shared := filepath.Join(root, "shared", "sessions")
local := filepath.Join(root, "local", "sessions")
if err := os.MkdirAll(local, 0o755); err != nil {
t.Fatalf("mkdir local: %v", err)
}
// A deliberately large rollout — copying it onto the critical path is
// exactly what MUL-4424 forbids.
const big = 4 << 20 // 4 MiB
src := seedFakeRollout(t, shared, "2026", "07", "13", "big-session", big)
if err := exposeResumeRollout(shared, local, "big-session", testLogger()); err != nil {
t.Fatalf("exposeResumeRollout: %v", err)
}
// Must share an inode with the source — a hard link (or symlink) — never a
// 4 MiB copy. A hard link needs no special privilege and works on Windows
// within a volume, so this path is exercised identically on CI.
dst := filepath.Join(local, "2026", "07", "13", "rollout-2026-07-13T00-00-00-big-session.jsonl")
if _, err := os.Lstat(dst); err != nil {
t.Fatalf("exposed rollout missing: %v", err)
}
assertZeroCopyLink(t, src, dst)
}
func TestResetCodexSessionState_OnlyRemovesSessionDerived(t *testing.T) {
t.Parallel()
home := t.TempDir()
sessionDerived := []string{
"state_5.sqlite", "state_5.sqlite-shm", "state_5.sqlite-wal",
"state_6.sqlite",
}
preserved := []string{
// session_index.jsonl holds thread-name mappings not rebuildable from
// rollouts, so it must survive the SQLite-only reset.
"session_index.jsonl",
"goals_1.sqlite", "logs_2.sqlite", "memories_1.sqlite",
"config.toml", "auth.json", "models_cache.json",
}
for _, n := range append(append([]string{}, sessionDerived...), preserved...) {
writeFile(t, filepath.Join(home, n), "x")
}
resetCodexSessionState(home, testLogger())
for _, n := range sessionDerived {
assertAbsent(t, filepath.Join(home, n))
}
for _, n := range preserved {
assertPresent(t, filepath.Join(home, n))
}
}
func TestPrepareCodexSessionsDir_MigratePreservesSessionIndex(t *testing.T) {
t.Parallel()
root := t.TempDir()
codexHome := filepath.Join(root, "codex-home")
sharedSessions := filepath.Join(root, "shared", "sessions")
seedFakeRollout(t, sharedSessions, "2026", "07", "13", "some-session", 16)
seedLegacySessionsSymlink(t, codexHome, sharedSessions)
writeFile(t, filepath.Join(codexHome, "state_5.sqlite"), "stale")
writeFile(t, filepath.Join(codexHome, "session_index.jsonl"), `{"id":"x","thread_name":"My thread"}`)
if err := prepareCodexSessionsDir(codexHome, filepath.Dir(sharedSessions), CodexHomeOptions{}, testLogger()); err != nil {
t.Fatalf("prepareCodexSessionsDir: %v", err)
}
// SQLite state is rebuilt; the thread-name index (not rebuildable from
// rollouts) is preserved.
assertAbsent(t, filepath.Join(codexHome, "state_5.sqlite"))
assertPresent(t, filepath.Join(codexHome, "session_index.jsonl"))
if data, _ := os.ReadFile(filepath.Join(codexHome, "session_index.jsonl")); string(data) != `{"id":"x","thread_name":"My thread"}` {
t.Errorf("session_index.jsonl content changed: %q", data)
}
}
func TestExposeResumeRollout_FindsCompressedAndFlatLayouts(t *testing.T) {
t.Parallel()
root := t.TempDir()
shared := filepath.Join(root, "shared", "sessions")
local := filepath.Join(root, "local", "sessions")
if err := os.MkdirAll(local, 0o755); err != nil {
t.Fatalf("mkdir local: %v", err)
}
// Codex background-compresses cold rollouts to .jsonl.zst and also supports
// a flat layout (rollout directly under sessions/). Both are legit history.
compressed := seedRolloutAt(t, filepath.Join(shared, "2026", "07", "13", "rollout-2026-07-13T00-00-00-zst-session.jsonl.zst"), 8)
flat := seedRolloutAt(t, filepath.Join(shared, "rollout-2026-07-13T00-00-00-flat-session.jsonl"), 8)
for _, tc := range []struct {
id, src, rel string
}{
{"zst-session", compressed, filepath.Join("2026", "07", "13", "rollout-2026-07-13T00-00-00-zst-session.jsonl.zst")},
{"flat-session", flat, "rollout-2026-07-13T00-00-00-flat-session.jsonl"},
} {
if err := exposeResumeRollout(shared, local, tc.id, testLogger()); err != nil {
t.Fatalf("expose %s: %v", tc.id, err)
}
assertZeroCopyLink(t, tc.src, filepath.Join(local, tc.rel))
}
}
func TestCodexResumeRolloutPresent(t *testing.T) {
t.Parallel()
root := t.TempDir()
codexHome := filepath.Join(root, "codex-home")
sessions := filepath.Join(codexHome, "sessions")
seedRolloutAt(t, filepath.Join(sessions, "2026", "07", "13", "rollout-2026-07-13T00-00-00-nested.jsonl"), 8)
seedRolloutAt(t, filepath.Join(sessions, "rollout-2026-07-13T00-00-00-flat.jsonl.zst"), 8)
for _, id := range []string{"nested", "flat"} {
if !CodexResumeRolloutPresent(codexHome, id) {
t.Errorf("expected rollout %q to be found", id)
}
}
if CodexResumeRolloutPresent(codexHome, "absent") {
t.Error("absent session must not be reported present")
}
if CodexResumeRolloutPresent("", "nested") || CodexResumeRolloutPresent(codexHome, "") {
t.Error("empty codexHome/sessionID must be reported absent")
}
}
func TestPrepareCodexSessionsDir_LocalDirectoryUsesPerIssueStore(t *testing.T) {
t.Parallel()
root := t.TempDir()
codexHome := filepath.Join(root, "codex-home")
if err := os.MkdirAll(codexHome, 0o755); err != nil {
t.Fatalf("mkdir codex-home: %v", err)
}
sharedHome := filepath.Join(root, "shared")
// A machine with accumulated global history that must NOT be exposed to the task.
seedFakeRollout(t, filepath.Join(sharedHome, "sessions"), "2026", "07", "13", "other-a", 16)
seedFakeRollout(t, filepath.Join(sharedHome, "sessions"), "2026", "07", "12", "other-b", 16)
key := filepath.Join("agent-1", "issue-1")
if err := prepareCodexSessionsDir(codexHome, sharedHome, CodexHomeOptions{IsLocalDirectory: true, SessionStoreKey: key}, testLogger()); err != nil {
t.Fatalf("prepareCodexSessionsDir: %v", err)
}
// sessions/ links the per-issue store, not the shared ~/.codex/sessions.
sessions := filepath.Join(codexHome, "sessions")
assertSessionsLinkedToStore(t, sessions, codexSessionStoreDir(sharedHome, key))
// The store holds only this issue's history — the machine's global rollouts
// are invisible, so `initialize` never enumerates them (the MUL-4424 stall).
entries, _ := os.ReadDir(sessions)
if len(entries) != 0 {
t.Errorf("per-issue store must start empty, has %d entries (whole-history leak?)", len(entries))
}
if CodexResumeRolloutPresent(codexHome, "other-a") {
t.Error("machine-global history must not be visible to a local_directory task")
}
}
// With no stable per-issue key (e.g. a non-issue task), a local_directory task
// must NOT collapse back to exposing the whole shared history — it falls back to
// an empty local dir.
func TestPrepareCodexSessionsDir_LocalDirectoryNoKeyFallsBackToEmptyDir(t *testing.T) {
t.Parallel()
root := t.TempDir()
codexHome := filepath.Join(root, "codex-home")
if err := os.MkdirAll(codexHome, 0o755); err != nil {
t.Fatalf("mkdir codex-home: %v", err)
}
sharedHome := filepath.Join(root, "shared")
seedFakeRollout(t, filepath.Join(sharedHome, "sessions"), "2026", "07", "13", "other", 16)
if err := prepareCodexSessionsDir(codexHome, sharedHome, CodexHomeOptions{IsLocalDirectory: true}, testLogger()); err != nil {
t.Fatalf("prepareCodexSessionsDir: %v", err)
}
sessions := filepath.Join(codexHome, "sessions")
fi, err := os.Lstat(sessions)
if err != nil {
t.Fatalf("sessions not created: %v", err)
}
if fi.Mode()&os.ModeSymlink != 0 {
t.Error("no-key local_directory must not link the shared history")
}
if CodexResumeRolloutPresent(codexHome, "other") {
t.Error("machine-global history must not be visible")
}
}
// Two consecutive local_directory tasks share one project dir but get a fresh
// codex-home each (the daemon never reuses their workdir). The second run must
// still see the first run's rollout via the per-issue store — this is the
// regression Elon's review flagged.
func TestPrepareCodexSessionsDir_LocalDirectoryResumeAcrossTaskIDs(t *testing.T) {
t.Parallel()
root := t.TempDir()
sharedHome := filepath.Join(root, "shared")
sessionID := "019f59d9-a6aa-7a53-b173-1eccc4b4c873"
key := filepath.Join("agent-1", "issue-1")
// Round 1: a fresh per-task codex-home links the per-issue store.
home1 := filepath.Join(root, "task-1", "codex-home")
if err := os.MkdirAll(home1, 0o755); err != nil {
t.Fatalf("mkdir home1: %v", err)
}
if err := prepareCodexSessionsDir(home1, sharedHome, CodexHomeOptions{IsLocalDirectory: true, SessionStoreKey: key}, testLogger()); err != nil {
t.Fatalf("round 1 prepare: %v", err)
}
// Codex writes the round-1 rollout through the link into the store.
seedRolloutAt(t, filepath.Join(home1, "sessions", "2026", "07", "13", "rollout-2026-07-13T00-00-00-"+sessionID+".jsonl"), 32)
// Round 2: a brand-new task ID → brand-new codex-home, same issue key.
home2 := filepath.Join(root, "task-2", "codex-home")
if err := os.MkdirAll(home2, 0o755); err != nil {
t.Fatalf("mkdir home2: %v", err)
}
if err := prepareCodexSessionsDir(home2, sharedHome, CodexHomeOptions{IsLocalDirectory: true, SessionStoreKey: key, ResumeSessionID: sessionID}, testLogger()); err != nil {
t.Fatalf("round 2 prepare: %v", err)
}
// The round-2 home must resolve the round-1 rollout through the shared store —
// otherwise the daemon would silently drop the conversation.
if !CodexResumeRolloutPresent(home2, sessionID) {
t.Fatal("round-2 local_directory task cannot see round-1 rollout — context would be silently lost")
}
}
// A managed home migrated on a prior reuse already links the per-issue store; a
// subsequent reuse must treat that link as authoritative and not re-migrate it.
func TestPrepareCodexSessionsDir_ReusedStoreLinkIsAuthoritative(t *testing.T) {
t.Parallel()
root := t.TempDir()
sharedHome := filepath.Join(root, "shared")
codexHome := filepath.Join(root, "codex-home")
if err := os.MkdirAll(codexHome, 0o755); err != nil {
t.Fatalf("mkdir codex-home: %v", err)
}
key := filepath.Join("agent-1", "issue-1")
storeDir := codexSessionStoreDir(sharedHome, key)
if err := os.MkdirAll(storeDir, 0o755); err != nil {
t.Fatalf("mkdir store: %v", err)
}
if err := os.Symlink(storeDir, filepath.Join(codexHome, "sessions")); err != nil {
if runtime.GOOS == "windows" {
t.Skipf("directory symlink unavailable on this Windows session: %v", err)
}
t.Fatalf("seed store link: %v", err)
}
sessionID := "019f59d9-a6aa-7a53-b173-1eccc4b4c873"
seedRolloutAt(t, filepath.Join(storeDir, "2026", "07", "13", "rollout-2026-07-13T00-00-00-"+sessionID+".jsonl"), 16)
if err := prepareCodexSessionsDir(codexHome, sharedHome, CodexHomeOptions{SessionStoreKey: key, ResumeSessionID: sessionID}, testLogger()); err != nil {
t.Fatalf("prepareCodexSessionsDir: %v", err)
}
assertSessionsLinkedToStore(t, filepath.Join(codexHome, "sessions"), storeDir)
if !CodexResumeRolloutPresent(codexHome, sessionID) {
t.Error("existing store rollout must remain resumable after reuse")
}
}
// PruneCodexSessionStores must reclaim per-issue stores idle past retention,
// keep recently-touched ones (active/resumable tasks), isolate issues from one
// another, and be disable-able — the data lifecycle Elon's review required so
// the persistent store can't grow forever (MUL-4424).
func TestPruneCodexSessionStores(t *testing.T) {
home := t.TempDir()
t.Setenv("CODEX_HOME", home)
// Stores live under the profile namespace; the default profile is "".
storeRoot := filepath.Join(home, codexSessionStoreRoot, codexSessionStoreNamespace(""))
freshStore := filepath.Join(storeRoot, "agent-1", "issue-fresh")
staleStore := filepath.Join(storeRoot, "agent-1", "issue-stale")
otherStore := filepath.Join(storeRoot, "agent-2", "issue-x")
seedRolloutAt(t, filepath.Join(freshStore, "2026", "07", "14", "rollout-2026-07-14T00-00-00-a.jsonl"), 16)
seedRolloutAt(t, filepath.Join(staleStore, "2026", "06", "01", "rollout-2026-06-01T00-00-00-b.jsonl"), 16)
seedRolloutAt(t, filepath.Join(otherStore, "2026", "07", "14", "rollout-2026-07-14T00-00-00-c.jsonl"), 16)
now := time.Now()
retention := 14 * 24 * time.Hour
// Age only the stale store's whole tree well past retention.
chtimesTree(t, staleStore, now.Add(-30*24*time.Hour))
removed, bytes := PruneCodexSessionStores("", retention, now, nil, testLogger())
if removed != 1 {
t.Fatalf("removed = %d, want 1 (only the store idle past retention)", removed)
}
if bytes <= 0 {
t.Errorf("bytesFreed = %d, want > 0", bytes)
}
// The stale store is gone; the fresh one and the other agent's store survive
// (per-issue isolation), and agent-1 stays because issue-fresh remains.
assertAbsent(t, staleStore)
assertPresent(t, freshStore)
assertPresent(t, otherStore)
assertPresent(t, filepath.Join(storeRoot, "agent-1"))
// retention <= 0 disables pruning even for an aged store.
chtimesTree(t, freshStore, now.Add(-30*24*time.Hour))
if removed, _ := PruneCodexSessionStores("", 0, now, nil, testLogger()); removed != 0 {
t.Errorf("retention<=0 must disable pruning, removed=%d", removed)
}
assertPresent(t, freshStore)
}
// TestPruneCodexSessionStores_ReopenedStoreNotReclaimed is Elon's blocker repro:
// a store idle past the TTL that a user reopens must NOT be reclaimed by a GC
// cycle that fires before the resumed turn writes its first rollout — mounting
// the store refreshes its activity (MUL-4424).
func TestPruneCodexSessionStores_ReopenedStoreNotReclaimed(t *testing.T) {
home := t.TempDir()
t.Setenv("CODEX_HOME", home)
storeDir := codexSessionStoreDir(home, codexSessionStoreKey("", TaskContextForEnv{AgentID: "agent-1", IssueID: "issue-old"}))
sessionID := "019f59d9-a6aa-7a53-b173-1eccc4b4c873"
// A long-idle (30-day) store that still holds a resumable rollout.
seedRolloutAt(t, filepath.Join(storeDir, "2026", "06", "01", "rollout-2026-06-01T00-00-00-"+sessionID+".jsonl"), 16)
chtimesTree(t, storeDir, time.Now().Add(-30*24*time.Hour))
// The user reopens the issue: a fresh task home remounts the store to resume.
codexHome := filepath.Join(home, "task", "codex-home")
if err := os.MkdirAll(codexHome, 0o755); err != nil {
t.Fatalf("mkdir codex-home: %v", err)
}
if err := linkCodexSessionsToStore(filepath.Join(codexHome, "sessions"), storeDir, filepath.Join(home, "sessions"), sessionID, testLogger()); err != nil {
t.Fatalf("linkCodexSessionsToStore: %v", err)
}
if !CodexResumeRolloutPresent(codexHome, sessionID) {
t.Fatal("resume rollout must be visible after remount")
}
// A GC cycle now — before the resumed turn writes anything — must keep it.
if removed, _ := PruneCodexSessionStores("", 14*24*time.Hour, time.Now(), nil, testLogger()); removed != 0 {
t.Fatalf("removed = %d, want 0 (a just-reopened store must survive GC)", removed)
}
assertPresent(t, storeDir)
if !CodexResumeRolloutPresent(codexHome, sessionID) {
t.Error("resume rollout must still be present after GC")
}
}
// TestPruneCodexSessionStores_ActiveStoreNotReclaimed proves the reservation
// guard closes the stat->remove race: a store idle on disk that reserve refuses
// (a live task holds it) is skipped, then reclaimed once reservable. The
// daemon's atomic reserve-vs-mark protocol is unit-tested separately.
func TestPruneCodexSessionStores_ActiveStoreNotReclaimed(t *testing.T) {
home := t.TempDir()
t.Setenv("CODEX_HOME", home)
storeDir := codexSessionStoreDir(home, codexSessionStoreKey("", TaskContextForEnv{AgentID: "agent-1", IssueID: "issue-active"}))
seedRolloutAt(t, filepath.Join(storeDir, "2026", "06", "01", "rollout-2026-06-01T00-00-00-x.jsonl"), 16)
// Idle past retention on disk (no remount refresh), but currently in use.
chtimesTree(t, storeDir, time.Now().Add(-30*24*time.Hour))
// reserve refuses the in-use store (ok=false) and allows anything else.
held := func(p string) (func(), bool) {
if sameCodexPath(p, storeDir) {
return nil, false
}
return func() {}, true
}
if removed, _ := PruneCodexSessionStores("", 14*24*time.Hour, time.Now(), held, testLogger()); removed != 0 {
t.Fatalf("removed = %d, want 0 (a reserved/in-use store must never be reclaimed)", removed)
}
assertPresent(t, storeDir)
// Once reservable, the same idle store is reclaimed.
if removed, _ := PruneCodexSessionStores("", 14*24*time.Hour, time.Now(), nil, testLogger()); removed != 1 {
t.Fatalf("removed = %d, want 1 (idle store reclaimed when reservable)", removed)
}
assertAbsent(t, storeDir)
}
// TestPruneCodexSessionStores_IsolatesProfiles is Elon's cross-profile blocker:
// two profile-daemons share one ~/.codex, so one daemon's GC must never reclaim
// another profile's store — the in-process reservation guard cannot span
// processes, but the per-profile namespace makes their store trees disjoint so a
// GC only ever sees, and reclaims, its own (MUL-4424).
func TestPruneCodexSessionStores_IsolatesProfiles(t *testing.T) {
home := t.TempDir()
t.Setenv("CODEX_HOME", home)
// A store owned by the "staging" profile daemon, idle past retention.
stagingStore := codexSessionStoreDir(home, codexSessionStoreKey("staging", TaskContextForEnv{AgentID: "agent-1", IssueID: "issue-1"}))
seedRolloutAt(t, filepath.Join(stagingStore, "2026", "06", "01", "rollout-2026-06-01T00-00-00-s.jsonl"), 16)
chtimesTree(t, stagingStore, time.Now().Add(-30*24*time.Hour))
// The production (default) daemon prunes — it must NOT touch staging's store,
// even with no guard, because staging is a different namespace it never scans.
if removed, _ := PruneCodexSessionStores("", 14*24*time.Hour, time.Now(), nil, testLogger()); removed != 0 {
t.Fatalf("removed = %d, want 0 — another profile's store must be out of scope", removed)
}
assertPresent(t, stagingStore)
// The staging daemon prunes its own namespace → reclaims its idle store.
if removed, _ := PruneCodexSessionStores("staging", 14*24*time.Hour, time.Now(), nil, testLogger()); removed != 1 {
t.Fatalf("removed = %d, want 1 — the owning profile reclaims its idle store", removed)
}
assertAbsent(t, stagingStore)
}
// TestCodexSessionStoreNamespaceInjective guards the profile->namespace map
// against the collisions Elon flagged: the CLI treats these profile pairs as
// distinct daemons, so they must never share a store namespace (a lossy
// character-dropping scheme merged them). MUL-4424.
func TestCodexSessionStoreNamespaceInjective(t *testing.T) {
t.Parallel()
pairs := [][2]string{
{"", "default"}, // empty (default) vs a profile literally named "default"
{"staging.prod", "stagingprod"}, // punctuation must not be dropped into a collision
{"a-b", "a_b"},
{"prod", "Prod"},
{"p_default", "default"}, // an encoded-looking name must not alias another
}
for _, p := range pairs {
if a, b := codexSessionStoreNamespace(p[0]), codexSessionStoreNamespace(p[1]); a == b {
t.Errorf("profiles %q and %q collide in namespace %q", p[0], p[1], a)
}
}
// Deterministic for a fixed profile.
if codexSessionStoreNamespace("staging") != codexSessionStoreNamespace("staging") {
t.Error("namespace must be deterministic for a fixed profile")
}
}
// TestCodexSessionStoreNamespace_FitsDirectorySegment guards Elon's round-8
// blocker: a profile the CLI can persist as its own config-dir segment (up to
// the ~255-byte filesystem limit) must yield a namespace that also fits one
// segment and is creatable — a length-expanding encoding (full hex) overflowed
// at 127 bytes. MUL-4424.
func TestCodexSessionStoreNamespace_FitsDirectorySegment(t *testing.T) {
t.Parallel()
base := t.TempDir()
for _, profile := range []string{"", "staging", strings.Repeat("a", 127), strings.Repeat("z", 255)} {
ns := codexSessionStoreNamespace(profile)
if len(ns) > 255 {
t.Errorf("profile (len %d) -> namespace (len %d) exceeds the 255-byte single-segment limit", len(profile), len(ns))
}
if err := os.MkdirAll(filepath.Join(base, ns), 0o755); err != nil {
t.Errorf("namespace for profile (len %d) could not be created: %v", len(profile), err)
}
}
}
// TestPruneCodexSessionStores_NoCrossProfileCollision proves the injective
// namespace holds end-to-end: profiles the CLI treats as distinct never reclaim
// each other's stores, including the "" vs "default" and punctuation cases that
// a lossy sanitizer collapsed (Elon's round-7 repro). MUL-4424.
func TestPruneCodexSessionStores_NoCrossProfileCollision(t *testing.T) {
for _, pair := range [][2]string{{"", "default"}, {"staging.prod", "stagingprod"}} {
home := t.TempDir()
t.Setenv("CODEX_HOME", home)
a, b := pair[0], pair[1]
storeA := codexSessionStoreDir(home, codexSessionStoreKey(a, TaskContextForEnv{AgentID: "agent", IssueID: "issue"}))
storeB := codexSessionStoreDir(home, codexSessionStoreKey(b, TaskContextForEnv{AgentID: "agent", IssueID: "issue"}))
seedRolloutAt(t, filepath.Join(storeA, "2026", "06", "01", "rollout-2026-06-01T00-00-00-a.jsonl"), 16)
seedRolloutAt(t, filepath.Join(storeB, "2026", "06", "01", "rollout-2026-06-01T00-00-00-b.jsonl"), 16)
chtimesTree(t, storeA, time.Now().Add(-30*24*time.Hour))
chtimesTree(t, storeB, time.Now().Add(-30*24*time.Hour))
// Pruning profile a reclaims a's store only — b's must survive.
if removed, _ := PruneCodexSessionStores(a, 14*24*time.Hour, time.Now(), nil, testLogger()); removed != 1 {
t.Fatalf("prune %q removed=%d, want 1", a, removed)
}
assertAbsent(t, storeA)
assertPresent(t, storeB)
// Then pruning profile b reclaims b's store.
if removed, _ := PruneCodexSessionStores(b, 14*24*time.Hour, time.Now(), nil, testLogger()); removed != 1 {
t.Fatalf("prune %q removed=%d, want 1", b, removed)
}
assertAbsent(t, storeB)
}
}
// TestPruneCodexSessionStores_RemovesEmptyAgentDir confirms an agent directory
// is cleaned up once its last issue store ages out, so the tree doesn't leave
// empty <agent>/ shells behind.
func TestPruneCodexSessionStores_RemovesEmptyAgentDir(t *testing.T) {
home := t.TempDir()
t.Setenv("CODEX_HOME", home)
storeRoot := filepath.Join(home, codexSessionStoreRoot, codexSessionStoreNamespace(""))
onlyStore := filepath.Join(storeRoot, "agent-lonely", "issue-1")
seedRolloutAt(t, filepath.Join(onlyStore, "2026", "06", "01", "rollout-2026-06-01T00-00-00-x.jsonl"), 16)
now := time.Now()
chtimesTree(t, onlyStore, now.Add(-30*24*time.Hour))
if removed, _ := PruneCodexSessionStores("", 14*24*time.Hour, now, nil, testLogger()); removed != 1 {
t.Fatalf("removed = %d, want 1", removed)
}
assertAbsent(t, filepath.Join(storeRoot, "agent-lonely"))
}
// chtimesTree sets the atime/mtime of every entry under root to ts.
func chtimesTree(t *testing.T, root string, ts time.Time) {
t.Helper()
err := filepath.WalkDir(root, func(path string, _ os.DirEntry, err error) error {
if err != nil {
return err
}
return os.Chtimes(path, ts, ts)
})
if err != nil {
t.Fatalf("chtimes tree %s: %v", root, err)
}
}
func seedRolloutAt(t *testing.T, path string, size int) string {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("mkdir rollout dir: %v", err)
}
body := make([]byte, size)
for i := range body {
body[i] = 'x'
}
if err := os.WriteFile(path, body, 0o644); err != nil {
t.Fatalf("write rollout: %v", err)
}
return path
}
// assertSessionsLinkedToStore verifies codex-home/sessions is a directory link
// that resolves to the per-issue store (storeDir), i.e. the task home reaches
// only that issue's history and not the machine-global ~/.codex/sessions.
func assertSessionsLinkedToStore(t *testing.T, sessions, storeDir string) {
t.Helper()
fi, err := os.Lstat(sessions)
if err != nil {
t.Fatalf("sessions link missing: %v", err)
}
if runtime.GOOS != "windows" {
if fi.Mode()&os.ModeSymlink == 0 {
t.Fatalf("sessions must link the per-issue store, got mode %v", fi.Mode())
}
if target, _ := os.Readlink(sessions); filepath.Clean(target) != filepath.Clean(storeDir) {
t.Errorf("sessions link target = %q, want store %q", target, storeDir)
}
}
realSessions, err := filepath.EvalSymlinks(sessions)
if err != nil {
t.Fatalf("eval sessions link: %v", err)
}
realStore, err := filepath.EvalSymlinks(storeDir)
if err != nil {
t.Fatalf("eval store: %v", err)
}
if realSessions != realStore {
t.Errorf("sessions resolves to %q, want store %q", realSessions, realStore)
}
}
// assertZeroCopyLink verifies dst resolves to the same inode as src — a hard
// link or symlink, never a byte copy.
func assertZeroCopyLink(t *testing.T, src, dst string) {
t.Helper()
si, err := os.Stat(src)
if err != nil {
t.Fatalf("stat src %s: %v", src, err)
}
di, err := os.Stat(dst)
if err != nil {
t.Fatalf("stat dst %s: %v", dst, err)
}
if !os.SameFile(si, di) {
t.Errorf("%s is a copy of %s, expected a zero-copy hard/sym link", dst, src)
}
}
func writeFile(t *testing.T, path, content string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("mkdir for %s: %v", path, err)
}
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatalf("write %s: %v", path, err)
}
}
func assertAbsent(t *testing.T, path string) {
t.Helper()
if _, err := os.Lstat(path); !os.IsNotExist(err) {
t.Errorf("expected %s to be removed, err=%v", filepath.Base(path), err)
}
}
func assertPresent(t *testing.T, path string) {
t.Helper()
if _, err := os.Lstat(path); err != nil {
t.Errorf("expected %s to be preserved: %v", filepath.Base(path), err)
}
}