package cc import ( "context" "os" "path/filepath" "testing" "time" ) // --- pure encoding tests --- func TestEncodeCwdForProjectsDir(t *testing.T) { cases := []struct { cwd string want string }{ {"/home/user/project", "-home-user-project"}, {"/tmp/my.project", "-tmp-my-project"}, {"/a/b/c", "-a-b-c"}, {"/single", "-single"}, } for _, tc := range cases { got := EncodeCwdForProjectsDir(tc.cwd) if got != tc.want { t.Errorf("EncodeCwdForProjectsDir(%q) = %q, want %q", tc.cwd, got, tc.want) } } } func TestClaudeProjectsDir(t *testing.T) { got := ClaudeProjectsDir("/home/alice", "/home/alice/myrepo") want := "/home/alice/.claude/projects/-home-alice-myrepo" if got != want { t.Errorf("ClaudeProjectsDir = %q, want %q", got, want) } } // --- watcher tests --- func TestWatchForNewSession_DetectsNewFile(t *testing.T) { dir := t.TempDir() ctx := context.Background() type result struct { id string err error } ch := make(chan result, 1) go func() { id, err := WatchForNewSession(ctx, SessionWatchOpts{ ProjectsDir: dir, Timeout: 2 * time.Second, }) ch <- result{id, err} }() // Give the watcher time to start. time.Sleep(50 * time.Millisecond) // Create a new .jsonl file — this should resolve the watcher. sessionID := "abc123sessionid" f, err := os.Create(filepath.Join(dir, sessionID+".jsonl")) if err != nil { t.Fatal(err) } f.Close() r := <-ch if r.err != nil { t.Fatalf("unexpected error: %v", r.err) } if r.id != sessionID { t.Errorf("got id %q, want %q", r.id, sessionID) } } func TestWatchForNewSession_IgnoresExistingFile(t *testing.T) { dir := t.TempDir() // Pre-existing file must be ignored. existing := "existing-session" f, _ := os.Create(filepath.Join(dir, existing+".jsonl")) f.Close() ctx := context.Background() ch := make(chan struct { id string err error }, 1) go func() { id, err := WatchForNewSession(ctx, SessionWatchOpts{ ProjectsDir: dir, Timeout: 200 * time.Millisecond, }) ch <- struct { id string err error }{id, err} }() // No new file created — should timeout returning ("", nil). r := <-ch if r.err != nil { t.Fatalf("unexpected error: %v", r.err) } if r.id != "" { t.Errorf("expected empty id on timeout, got %q", r.id) } } func TestWatchForNewSession_TimeoutReturnsEmptyNil(t *testing.T) { dir := t.TempDir() ctx := context.Background() id, err := WatchForNewSession(ctx, SessionWatchOpts{ ProjectsDir: 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) } } // TestWatchForNewSession_MissingDirThenCreated proves the symmetric fix for the // claude watcher: ProjectsDir absent at call time is created by MkdirAll, and a // new session file written afterwards is still captured. func TestWatchForNewSession_MissingDirThenCreated(t *testing.T) { base := t.TempDir() // ProjectsDir does NOT exist yet. projDir := filepath.Join(base, "projects", "-home-user-myrepo") ctx := context.Background() ch := make(chan struct { id string err error }, 1) go func() { id, err := WatchForNewSession(ctx, SessionWatchOpts{ ProjectsDir: projDir, Timeout: 300 * time.Millisecond, }) ch <- struct { id string err error }{id, err} }() // Allow MkdirAll + watcher.Add to complete. time.Sleep(50 * time.Millisecond) sessionID := "newclaudesessionabc" f, err := os.Create(filepath.Join(projDir, sessionID+".jsonl")) if err != nil { t.Fatalf("create session file: %v", err) } f.Close() r := <-ch if r.err != nil { t.Fatalf("unexpected error: %v", r.err) } if r.id != sessionID { t.Errorf("got id %q, want %q (missing-dir session not captured)", r.id, sessionID) } } func TestWatchForNewSession_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 := WatchForNewSession(ctx, SessionWatchOpts{ ProjectsDir: dir, Timeout: 5 * time.Second, }) ch <- struct { id string err error }{id, err} }() time.Sleep(30 * time.Millisecond) cancel() r := <-ch // Context cancellation returns ("", nil) matching timeout semantics. 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) } }