package tui import ( "context" "strings" "testing" "superwork-tui/internal/issue" ) func TestClaudeCmd(t *testing.T) { tests := []struct { name string opts claudeCmdOpts want string }{ { name: "fresh prompt no profile", opts: claudeCmdOpts{Prompt: "do the thing"}, want: "claude --dangerously-skip-permissions 'do the thing'", }, { name: "resume no profile", opts: claudeCmdOpts{ResumeSessionID: "abc123"}, want: "claude --dangerously-skip-permissions --resume 'abc123'", }, { name: "with profile", opts: claudeCmdOpts{Prompt: "go", ProfilePath: "/my/profile.json"}, want: "claude --dangerously-skip-permissions --settings '/my/profile.json' 'go'", }, { name: "space in profile path", opts: claudeCmdOpts{Prompt: "go", ProfilePath: "/has space/p.json"}, want: "claude --dangerously-skip-permissions --settings '/has space/p.json' 'go'", }, { name: "effort high", opts: claudeCmdOpts{Prompt: "impl", EffortHigh: true}, want: "claude --effort high --dangerously-skip-permissions 'impl'", }, { name: "with system prompt command", opts: claudeCmdOpts{Prompt: "do it", SystemPromptCommand: "serena prompts print"}, want: `claude --dangerously-skip-permissions --system-prompt="$(serena prompts print)" 'do it'`, }, { name: "system prompt with spaces in cmd", opts: claudeCmdOpts{Prompt: "go", SystemPromptCommand: "my cmd --flag"}, want: `claude --dangerously-skip-permissions --system-prompt="$(my cmd --flag)" 'go'`, }, { name: "system prompt with profile: settings before system-prompt", opts: claudeCmdOpts{Prompt: "go", ProfilePath: "/p.json", SystemPromptCommand: "get-prompt"}, want: `claude --dangerously-skip-permissions --settings '/p.json' --system-prompt="$(get-prompt)" 'go'`, }, { name: "resume with system prompt command", opts: claudeCmdOpts{ResumeSessionID: "sid123", SystemPromptCommand: "get-prompt"}, want: `claude --dangerously-skip-permissions --system-prompt="$(get-prompt)" --resume 'sid123'`, }, } for _, tc := range tests { got := claudeCmd(tc.opts) if got != tc.want { t.Errorf("%s: got %q, want %q", tc.name, got, tc.want) } } } func TestWindowName(t *testing.T) { tests := []struct { num int phase Phase want string }{ {42, PhaseBrain, "42-brain"}, {1, PhaseImpl, "1-impl"}, {99, PhaseReview, "99-review"}, {7, PhaseTest, "7-test"}, } for _, tc := range tests { got := windowName(tc.num, tc.phase) if got != tc.want { t.Errorf("windowName(%d, %q) = %q, want %q", tc.num, tc.phase, got, tc.want) } } } func TestClaudeResumeCmd(t *testing.T) { if got := claudeResumeCmd("abc123", "", ""); got != "claude --dangerously-skip-permissions --resume 'abc123'" { t.Errorf("no profile: got %q", got) } if got := claudeResumeCmd("abc123", "/path/to/profile.json", ""); got != "claude --dangerously-skip-permissions --settings '/path/to/profile.json' --resume 'abc123'" { t.Errorf("with profile: got %q", got) } if got := claudeResumeCmd("abc123", "/has space/p.json", ""); got != "claude --dangerously-skip-permissions --settings '/has space/p.json' --resume 'abc123'" { t.Errorf("path with space must be single-quoted: got %q", got) } // SystemPromptCommand must appear after --settings and before --resume. want := `claude --dangerously-skip-permissions --settings '/p.json' --system-prompt="$(get-prompt)" --resume 'abc123'` if got := claudeResumeCmd("abc123", "/p.json", "get-prompt"); got != want { t.Errorf("with system-prompt: got %q, want %q", got, want) } // No profile, with SystemPromptCommand. want2 := `claude --dangerously-skip-permissions --system-prompt="$(echo hi)" --resume 'abc123'` if got := claudeResumeCmd("abc123", "", "echo hi"); got != want2 { t.Errorf("no profile with system-prompt: got %q, want %q", got, want2) } } // TestResumeSessionID_Quoting asserts that session IDs with shell metacharacters // are single-quoted in the generated command (I3 security fix). func TestResumeSessionID_Quoting(t *testing.T) { tests := []struct { id string want string }{ {"sess-abc", "--resume 'sess-abc'"}, {"sess abc", "--resume 'sess abc'"}, {"sess;rm${IFS}-rf/", "--resume 'sess;rm${IFS}-rf/'"}, {"it's-tricky", `--resume 'it'\''s-tricky'`}, } for _, tc := range tests { got := claudeCmd(claudeCmdOpts{ResumeSessionID: tc.id}) if !strings.Contains(got, tc.want) { t.Errorf("id=%q: got %q, want substring %q", tc.id, got, tc.want) } } } func TestReconcileTabs(t *testing.T) { issues := []issue.Issue{ {Number: 5}, {Number: 10}, } alive := []string{"5-brain", "10-impl", "10-review"} got := reconcileTabs(issues, alive) iss5 := got[0] if !iss5.BrainstormTabOpen { t.Error("issue 5: BrainstormTabOpen should be true") } if iss5.ImplementTabOpen || iss5.ReviewTabOpen || iss5.TestTabOpen { t.Error("issue 5: unexpected tabs open") } iss10 := got[1] if iss10.BrainstormTabOpen { t.Error("issue 10: BrainstormTabOpen should be false") } if !iss10.ImplementTabOpen { t.Error("issue 10: ImplementTabOpen should be true") } if !iss10.ReviewTabOpen { t.Error("issue 10: ReviewTabOpen should be true") } if iss10.TestTabOpen { t.Error("issue 10: TestTabOpen should be false") } } // recorder captures tmux args sequences for assertion. type recorder struct { calls [][]string // responses maps the first two args (subcommand + relevant flag) to output. responses map[string][]byte } func (r *recorder) run(args ...string) ([]byte, error) { r.calls = append(r.calls, args) if r.responses != nil { key := args[0] if out, ok := r.responses[key]; ok { return out, nil } } return nil, nil } func TestOpenOrFocus_WindowExists(t *testing.T) { // Window already exists → EnsureSession + WindowExists + SelectWindow only (no NewWindow). rec := &recorder{ responses: map[string][]byte{ // has-session → exists (nil error, empty output) "has-session": {}, // list-windows returns the window we're looking for "list-windows": []byte("5-brain\n"), }, } mgr := NewSessionManager(rec.run) iss := issue.Issue{Number: 5} err := mgr.OpenOrFocus(context.Background(), PhaseBrain, iss, "/cwd", "claude --resume xyz") if err != nil { t.Fatalf("unexpected error: %v", err) } // Exists path: has-session, list-windows, select-window — no new-window. got := subcommands(rec.calls) want := []string{"has-session", "list-windows", "select-window"} if !equalSeq(got, want) { t.Errorf("call sequence = %v, want %v", got, want) } } func subcommands(calls [][]string) []string { var out []string for _, call := range calls { if len(call) > 0 { out = append(out, call[0]) } } return out } func equalSeq(a, b []string) bool { if len(a) != len(b) { return false } for i := range a { if a[i] != b[i] { return false } } return true } func TestOpenOrFocus_WindowMissing(t *testing.T) { // Window does not exist → EnsureSession + WindowExists + NewWindow + SelectWindow. rec := &recorder{ responses: map[string][]byte{ "has-session": {}, "list-windows": []byte("other-window\n"), }, } mgr := NewSessionManager(rec.run) iss := issue.Issue{Number: 5} err := mgr.OpenOrFocus(context.Background(), PhaseBrain, iss, "/cwd", "claude --resume xyz") if err != nil { t.Fatalf("unexpected error: %v", err) } // Missing path: has-session, list-windows, new-window, select-window. got := subcommands(rec.calls) want := []string{"has-session", "list-windows", "new-window", "select-window"} if !equalSeq(got, want) { t.Errorf("call sequence = %v, want %v", got, want) } } func TestInject(t *testing.T) { rec := &recorder{} mgr := NewSessionManager(rec.run) iss := issue.Issue{Number: 3} err := mgr.Inject(context.Background(), PhaseImpl, iss, "hello world") if err != nil { t.Fatalf("unexpected error: %v", err) } var hasSendKeys bool for _, call := range rec.calls { if len(call) > 0 && call[0] == "send-keys" { hasSendKeys = true } } if !hasSendKeys { t.Error("expected send-keys call") } } func TestClose(t *testing.T) { rec := &recorder{} mgr := NewSessionManager(rec.run) iss := issue.Issue{Number: 8} err := mgr.Close(context.Background(), PhaseReview, iss, "superwork") if err != nil { t.Fatalf("unexpected error: %v", err) } var hasKillWindow bool for _, call := range rec.calls { if len(call) > 0 && call[0] == "kill-window" { hasKillWindow = true } } if !hasKillWindow { t.Error("expected kill-window call") } }