package cc import ( "context" "os" "path/filepath" "testing" "time" ) // --- pure UUID extraction tests --- func TestExtractIDFromRolloutFilename(t *testing.T) { cases := []struct { name string want string }{ { "rollout-2026-04-27T02-57-26-019dcb27-58a6-70a1-a5d1-bfc7f3ed9d0a.jsonl", "019dcb27-58a6-70a1-a5d1-bfc7f3ed9d0a", }, { "rollout-2026-01-01T00-00-00-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee.jsonl", "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", }, { // No UUID — should return empty string. "rollout-2026-04-27T02-57-26.jsonl", "", }, { // Not a rollout file. "session.jsonl", "", }, } for _, tc := range cases { got := extractIDFromRolloutFilename(tc.name) if got != tc.want { t.Errorf("extractIDFromRolloutFilename(%q) = %q, want %q", tc.name, got, tc.want) } } } func TestCodexSessionsDir(t *testing.T) { ts := time.Date(2026, 4, 27, 2, 57, 26, 0, time.UTC) got := CodexSessionsDir("/home/alice", ts) want := "/home/alice/.codex/sessions/2026/04/27" if got != want { t.Errorf("CodexSessionsDir = %q, want %q", got, want) } } // --- watcher tests --- func TestWatchForNewCodexSession_DetectsRolloutByFilename(t *testing.T) { dir := t.TempDir() ctx := context.Background() ch := make(chan struct { id string err error }, 1) go func() { id, err := WatchForNewCodexSession(ctx, CodexSessionWatchOpts{ SessionsDir: dir, Timeout: 2 * time.Second, }) ch <- struct { id string err error }{id, err} }() time.Sleep(50 * time.Millisecond) uuid := "019dcb27-58a6-70a1-a5d1-bfc7f3ed9d0a" fname := "rollout-2026-04-27T02-57-26-" + uuid + ".jsonl" f, err := os.Create(filepath.Join(dir, fname)) if err != nil { t.Fatal(err) } f.Close() r := <-ch if r.err != nil { t.Fatalf("unexpected error: %v", r.err) } if r.id != uuid { t.Errorf("got id %q, want %q", r.id, uuid) } } func TestWatchForNewCodexSession_FallbackToFirstLine(t *testing.T) { dir := t.TempDir() ctx := context.Background() ch := make(chan struct { id string err error }, 1) go func() { id, err := WatchForNewCodexSession(ctx, CodexSessionWatchOpts{ SessionsDir: dir, Timeout: 2 * time.Second, }) ch <- struct { id string err error }{id, err} }() time.Sleep(50 * time.Millisecond) // File name without a UUID — triggers first-line fallback. wantID := "fallback-thread-id-0001" fname := "rollout-2026-04-27T02-57-26-no-uuid-here.jsonl" content := `{"timestamp":"2026-04-27T02:57:26Z","type":"session_meta","payload":{"id":"` + wantID + `","cwd":"/tmp"}}` + "\n" if err := os.WriteFile(filepath.Join(dir, fname), []byte(content), 0o644); err != nil { t.Fatal(err) } r := <-ch if r.err != nil { t.Fatalf("unexpected error: %v", r.err) } if r.id != wantID { t.Errorf("got id %q, want %q", r.id, wantID) } } func TestWatchForNewCodexSession_TimeoutReturnsEmptyNil(t *testing.T) { dir := t.TempDir() ctx := context.Background() id, err := WatchForNewCodexSession(ctx, CodexSessionWatchOpts{ SessionsDir: dir, Timeout: 200 * time.Millisecond, }) if err != nil { t.Fatalf("unexpected error: %v", err) } if id != "" { t.Errorf("expected empty string on timeout, got %q", id) } } // TestWatchForNewCodexSession_FirstOfDay proves the primary fix: when // SessionsDir does not exist at call time (first codex session of the day), // MkdirAll creates it and the watcher still captures the rollout file. func TestWatchForNewCodexSession_FirstOfDay(t *testing.T) { base := t.TempDir() // SessionsDir does NOT exist yet — simulates first-of-day scenario. sessDir := filepath.Join(base, "2099", "12", "31") ctx := context.Background() ch := make(chan struct { id string err error }, 1) go func() { id, err := WatchForNewCodexSession(ctx, CodexSessionWatchOpts{ SessionsDir: sessDir, Timeout: 300 * time.Millisecond, }) ch <- struct { id string err error }{id, err} }() // Wait for watcher to start and MkdirAll to create the dir. time.Sleep(50 * time.Millisecond) uuid := "019dcb27-58a6-70a1-a5d1-bfc7f3ed9d0a" fname := "rollout-2099-12-31T00-00-00-" + uuid + ".jsonl" f, err := os.Create(filepath.Join(sessDir, fname)) if err != nil { t.Fatalf("create rollout file: %v", err) } f.Close() r := <-ch if r.err != nil { t.Fatalf("unexpected error: %v", r.err) } if r.id != uuid { t.Errorf("got id %q, want %q (first-of-day session not captured)", r.id, uuid) } } // TestWatchForNewCodexSession_MkdirAllFailReturnsEmptyNil verifies that when // MkdirAll itself cannot create the dir (e.g. path under a file), the watcher // returns ("", nil) without panicking. func TestWatchForNewCodexSession_MkdirAllFailReturnsEmptyNil(t *testing.T) { base := t.TempDir() // Create a plain file so MkdirAll on a sub-path will fail. blocker := filepath.Join(base, "blocker") if err := os.WriteFile(blocker, []byte("x"), 0o644); err != nil { t.Fatal(err) } ctx := context.Background() id, err := WatchForNewCodexSession(ctx, CodexSessionWatchOpts{ SessionsDir: filepath.Join(blocker, "subdir"), Timeout: 300 * time.Millisecond, }) if err != nil { t.Fatalf("unexpected error: %v", err) } if id != "" { t.Errorf("expected empty string when MkdirAll fails, got %q", id) } } func TestWatchForNewCodexSession_ContextCancel(t *testing.T) { dir := t.TempDir() ctx, cancel := context.WithCancel(context.Background()) ch := make(chan struct { id string err error }, 1) go func() { id, err := WatchForNewCodexSession(ctx, CodexSessionWatchOpts{ SessionsDir: dir, Timeout: 5 * time.Second, }) ch <- struct { id string err error }{id, err} }() time.Sleep(30 * time.Millisecond) cancel() r := <-ch if r.err != nil { t.Fatalf("unexpected error on cancel: %v", r.err) } if r.id != "" { t.Errorf("expected empty id on cancel, got %q", r.id) } }