Files
superwork/tui/internal/cc/session_watcher_test.go
2026-06-23 05:02:15 +08:00

202 lines
4.4 KiB
Go

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)
}
}