11
This commit is contained in:
@@ -0,0 +1,287 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user