713 lines
21 KiB
Go
713 lines
21 KiB
Go
package tui
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"superwork-tui/internal/issue"
|
|
)
|
|
|
|
// contains is a helper for string containment checks.
|
|
func contains(s, substr string) bool {
|
|
return strings.Contains(s, substr)
|
|
}
|
|
|
|
// TestReviewCmd_NoSessionID tests that reviewCmd returns error when ReviewSessionID is empty.
|
|
func TestReviewCmd_NoSessionID(t *testing.T) {
|
|
rec := &recorder{
|
|
responses: map[string][]byte{
|
|
"has-session": {},
|
|
"list-windows": []byte(""),
|
|
},
|
|
}
|
|
sm := NewSessionManager(rec.run)
|
|
iss := issue.Issue{Number: 5, ReviewSessionID: ""}
|
|
restore := overrideWorkspaceRoot(t.TempDir())
|
|
defer restore()
|
|
|
|
cmd := reviewCmd(iss, sm)
|
|
msg := cmd()
|
|
result, ok := msg.(reviewResultMsg)
|
|
if !ok {
|
|
t.Fatalf("expected reviewResultMsg, got %T", msg)
|
|
}
|
|
if result.err == nil {
|
|
t.Error("expected error when ReviewSessionID is empty")
|
|
}
|
|
}
|
|
|
|
// TestReviewCmd_OpensWindow tests that reviewCmd calls OpenOrFocus with codex command when worktree exists.
|
|
func TestReviewCmd_OpensWindow(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
worktreeDir := tmpDir + "/worktree-review"
|
|
if err := os.MkdirAll(worktreeDir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rec := &recorder{
|
|
responses: map[string][]byte{
|
|
"has-session": {},
|
|
"list-windows": []byte(""),
|
|
},
|
|
}
|
|
sm := NewSessionManager(rec.run)
|
|
iss := issue.Issue{Number: 5, ReviewSessionID: "sess-abc123", WorktreePath: "worktree-review"}
|
|
restore := overrideWorkspaceRoot(tmpDir)
|
|
defer restore()
|
|
|
|
cmd := reviewCmd(iss, sm)
|
|
msg := cmd()
|
|
result, ok := msg.(reviewResultMsg)
|
|
if !ok {
|
|
t.Fatalf("expected reviewResultMsg, got %T", msg)
|
|
}
|
|
if result.err != nil {
|
|
t.Fatalf("unexpected error: %v", result.err)
|
|
}
|
|
|
|
var newWindowCmd string
|
|
for _, call := range rec.calls {
|
|
if len(call) > 0 && call[0] == "new-window" {
|
|
newWindowCmd = call[len(call)-1]
|
|
}
|
|
}
|
|
want := "codex resume -c model_reasoning_effort=xhigh --dangerously-bypass-approvals-and-sandbox 'sess-abc123'"
|
|
if newWindowCmd != want {
|
|
t.Errorf("new-window command = %q, want %q", newWindowCmd, want)
|
|
}
|
|
}
|
|
|
|
// TestReviewCmd_EmptyWorktreePath tests that reviewCmd hard-errors when WorktreePath is empty.
|
|
func TestReviewCmd_EmptyWorktreePath(t *testing.T) {
|
|
rec := &recorder{
|
|
responses: map[string][]byte{
|
|
"has-session": {},
|
|
"list-windows": []byte(""),
|
|
},
|
|
}
|
|
sm := NewSessionManager(rec.run)
|
|
iss := issue.Issue{Number: 8, ReviewSessionID: "sess-xyz", WorktreePath: ""}
|
|
restore := overrideWorkspaceRoot(t.TempDir())
|
|
defer restore()
|
|
|
|
msg := reviewCmd(iss, sm)()
|
|
result, ok := msg.(reviewResultMsg)
|
|
if !ok {
|
|
t.Fatalf("expected reviewResultMsg, got %T", msg)
|
|
}
|
|
if result.err == nil {
|
|
t.Fatal("expected error when WorktreePath is empty")
|
|
}
|
|
if !contains(result.err.Error(), "worktree 不存在") {
|
|
t.Errorf("error %q should mention worktree 不存在", result.err.Error())
|
|
}
|
|
// No tmux new-window call should have been made.
|
|
for _, call := range rec.calls {
|
|
if len(call) > 0 && call[0] == "new-window" {
|
|
t.Error("expected no new-window call when WorktreePath is empty")
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestReviewCmd_MissingWorktreeDir tests that reviewCmd hard-errors when WorktreePath dir doesn't exist.
|
|
func TestReviewCmd_MissingWorktreeDir(t *testing.T) {
|
|
rec := &recorder{
|
|
responses: map[string][]byte{
|
|
"has-session": {},
|
|
"list-windows": []byte(""),
|
|
},
|
|
}
|
|
sm := NewSessionManager(rec.run)
|
|
iss := issue.Issue{Number: 9, ReviewSessionID: "sess-abc", WorktreePath: "nonexistent-worktree"}
|
|
restore := overrideWorkspaceRoot(t.TempDir())
|
|
defer restore()
|
|
|
|
msg := reviewCmd(iss, sm)()
|
|
result, ok := msg.(reviewResultMsg)
|
|
if !ok {
|
|
t.Fatalf("expected reviewResultMsg, got %T", msg)
|
|
}
|
|
if result.err == nil {
|
|
t.Fatal("expected error when worktree directory does not exist on disk")
|
|
}
|
|
if !contains(result.err.Error(), "worktree 不存在") {
|
|
t.Errorf("error %q should mention worktree 不存在", result.err.Error())
|
|
}
|
|
// No tmux new-window call should have been made.
|
|
for _, call := range rec.calls {
|
|
if len(call) > 0 && call[0] == "new-window" {
|
|
t.Error("expected no new-window call when worktree dir is missing")
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestReviewCmd_WindowName tests that the review window name is {n}-review.
|
|
func TestReviewCmd_WindowName(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
worktreeDir := tmpDir + "/wt-review"
|
|
if err := os.MkdirAll(worktreeDir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rec := &recorder{
|
|
responses: map[string][]byte{
|
|
"has-session": {},
|
|
"list-windows": []byte(""),
|
|
},
|
|
}
|
|
sm := NewSessionManager(rec.run)
|
|
iss := issue.Issue{Number: 7, ReviewSessionID: "some-session", WorktreePath: "wt-review"}
|
|
restore := overrideWorkspaceRoot(tmpDir)
|
|
defer restore()
|
|
|
|
reviewCmd(iss, sm)()
|
|
|
|
var newWindowName string
|
|
for _, call := range rec.calls {
|
|
if len(call) > 0 && call[0] == "new-window" {
|
|
for i, arg := range call {
|
|
if arg == "-n" && i+1 < len(call) {
|
|
newWindowName = call[i+1]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if newWindowName != "7-review" {
|
|
t.Errorf("window name = %q, want 7-review", newWindowName)
|
|
}
|
|
}
|
|
|
|
// TestTestCmd_Resume tests that testCmd resumes with --resume when TestSessionID exists.
|
|
func TestTestCmd_Resume(t *testing.T) {
|
|
rec := &recorder{
|
|
responses: map[string][]byte{
|
|
"has-session": {},
|
|
"list-windows": []byte(""),
|
|
},
|
|
}
|
|
sm := NewSessionManager(rec.run)
|
|
iss := issue.Issue{Number: 3, TestSessionID: "test-sess-xyz"}
|
|
restore := overrideWorkspaceRoot(t.TempDir())
|
|
defer restore()
|
|
|
|
msg := testCmd(iss, sm)()
|
|
result, ok := msg.(testResultMsg)
|
|
if !ok {
|
|
t.Fatalf("expected testResultMsg, got %T", msg)
|
|
}
|
|
if result.err != nil {
|
|
t.Fatalf("unexpected error: %v", result.err)
|
|
}
|
|
|
|
var newWindowCmd string
|
|
for _, call := range rec.calls {
|
|
if len(call) > 0 && call[0] == "new-window" {
|
|
newWindowCmd = call[len(call)-1]
|
|
}
|
|
}
|
|
if newWindowCmd == "" {
|
|
t.Fatal("no new-window call found")
|
|
}
|
|
if !contains(newWindowCmd, "--resume 'test-sess-xyz'") {
|
|
t.Errorf("resume command %q missing --resume 'test-sess-xyz'", newWindowCmd)
|
|
}
|
|
}
|
|
|
|
// TestTestCmd_FreshInWorktree tests that testCmd uses worktree prompt when worktree exists.
|
|
func TestTestCmd_FreshInWorktree(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
// Create a real subdirectory that will be the worktree path.
|
|
worktreeDir := tmpDir + "/worktree"
|
|
if err := os.MkdirAll(worktreeDir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rec := &recorder{
|
|
responses: map[string][]byte{
|
|
"has-session": {},
|
|
"list-windows": []byte(""),
|
|
},
|
|
}
|
|
sm := NewSessionManager(rec.run)
|
|
iss := issue.Issue{Number: 4, WorktreePath: "worktree"}
|
|
restore := overrideWorkspaceRoot(tmpDir)
|
|
defer restore()
|
|
|
|
msg := testCmd(iss, sm)()
|
|
result, ok := msg.(testResultMsg)
|
|
if !ok {
|
|
t.Fatalf("expected testResultMsg, got %T", msg)
|
|
}
|
|
if result.err != nil {
|
|
t.Fatalf("unexpected error: %v", result.err)
|
|
}
|
|
|
|
var newWindowCmd string
|
|
for _, call := range rec.calls {
|
|
if len(call) > 0 && call[0] == "new-window" {
|
|
newWindowCmd = call[len(call)-1]
|
|
}
|
|
}
|
|
if !contains(newWindowCmd, "worktree 工作目录") {
|
|
t.Errorf("fresh worktree command %q missing expected prompt text", newWindowCmd)
|
|
}
|
|
}
|
|
|
|
// TestTestCmd_FreshMainBranch tests that testCmd uses PR prompt when no worktree.
|
|
func TestTestCmd_FreshMainBranch(t *testing.T) {
|
|
rec := &recorder{
|
|
responses: map[string][]byte{
|
|
"has-session": {},
|
|
"list-windows": []byte(""),
|
|
},
|
|
}
|
|
sm := NewSessionManager(rec.run)
|
|
iss := issue.Issue{Number: 5, PR: "42"} // no WorktreePath
|
|
restore := overrideWorkspaceRoot(t.TempDir())
|
|
defer restore()
|
|
|
|
msg := testCmd(iss, sm)()
|
|
result, ok := msg.(testResultMsg)
|
|
if !ok {
|
|
t.Fatalf("expected testResultMsg, got %T", msg)
|
|
}
|
|
if result.err != nil {
|
|
t.Fatalf("unexpected error: %v", result.err)
|
|
}
|
|
|
|
var newWindowCmd string
|
|
for _, call := range rec.calls {
|
|
if len(call) > 0 && call[0] == "new-window" {
|
|
newWindowCmd = call[len(call)-1]
|
|
}
|
|
}
|
|
if !contains(newWindowCmd, "#42号pr") {
|
|
t.Errorf("main branch command %q missing #42号pr", newWindowCmd)
|
|
}
|
|
}
|
|
|
|
// TestTestCmd_NoWorktreeNoPR tests that testCmd errors when no worktree and no PR.
|
|
func TestTestCmd_NoWorktreeNoPR(t *testing.T) {
|
|
rec := &recorder{}
|
|
sm := NewSessionManager(rec.run)
|
|
iss := issue.Issue{Number: 6} // no WorktreePath, no PR, no TestSessionID
|
|
restore := overrideWorkspaceRoot(t.TempDir())
|
|
defer restore()
|
|
|
|
msg := testCmd(iss, sm)()
|
|
result, ok := msg.(testResultMsg)
|
|
if !ok {
|
|
t.Fatalf("expected testResultMsg, got %T", msg)
|
|
}
|
|
if result.err == nil {
|
|
t.Error("expected error when no worktree and no PR")
|
|
}
|
|
}
|
|
|
|
// TestTestCmd_ProfilePriority tests TestProfilePath > ProfilePath.
|
|
func TestTestCmd_ProfilePriority(t *testing.T) {
|
|
rec := &recorder{
|
|
responses: map[string][]byte{
|
|
"has-session": {},
|
|
"list-windows": []byte(""),
|
|
},
|
|
}
|
|
sm := NewSessionManager(rec.run)
|
|
iss := issue.Issue{
|
|
Number: 9,
|
|
PR: "10",
|
|
ProfilePath: "/profile/default.json",
|
|
TestProfilePath: "/profile/test.json",
|
|
}
|
|
restore := overrideWorkspaceRoot(t.TempDir())
|
|
defer restore()
|
|
|
|
testCmd(iss, sm)()
|
|
|
|
var newWindowCmd string
|
|
for _, call := range rec.calls {
|
|
if len(call) > 0 && call[0] == "new-window" {
|
|
newWindowCmd = call[len(call)-1]
|
|
}
|
|
}
|
|
if !contains(newWindowCmd, "/profile/test.json") {
|
|
t.Errorf("command %q should use TestProfilePath /profile/test.json", newWindowCmd)
|
|
}
|
|
if contains(newWindowCmd, "/profile/default.json") {
|
|
t.Errorf("command %q should NOT use ProfilePath when TestProfilePath is set", newWindowCmd)
|
|
}
|
|
}
|
|
|
|
// TestInjectFeedback_ImplWindowExists tests impl window is primary target.
|
|
func TestInjectFeedback_ImplWindowExists(t *testing.T) {
|
|
rec := &recorder{
|
|
responses: map[string][]byte{
|
|
"has-session": {},
|
|
"list-windows": []byte("5-impl\n5-test\n"),
|
|
},
|
|
}
|
|
sm := NewSessionManager(rec.run)
|
|
iss := issue.Issue{Number: 5}
|
|
result := InjectFeedback(sm, iss, "test feedback")
|
|
if !result {
|
|
t.Error("expected InjectFeedback to return true when impl window exists")
|
|
}
|
|
var injectedTarget string
|
|
for _, call := range rec.calls {
|
|
if len(call) > 0 && call[0] == "send-keys" {
|
|
for _, a := range call {
|
|
if contains(a, "5-impl") {
|
|
injectedTarget = a
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if injectedTarget == "" {
|
|
t.Error("expected send-keys to target 5-impl window")
|
|
}
|
|
}
|
|
|
|
// TestInjectFeedback_FallbackToTest tests fallback to test window when impl missing.
|
|
func TestInjectFeedback_FallbackToTest(t *testing.T) {
|
|
rec := &recorder{
|
|
responses: map[string][]byte{
|
|
"has-session": {},
|
|
"list-windows": []byte("5-test\n"),
|
|
},
|
|
}
|
|
sm := NewSessionManager(rec.run)
|
|
iss := issue.Issue{Number: 5}
|
|
result := InjectFeedback(sm, iss, "test feedback")
|
|
if !result {
|
|
t.Error("expected InjectFeedback to return true when test window exists")
|
|
}
|
|
}
|
|
|
|
// TestInjectFeedback_NeitherWindow tests that false is returned when neither window exists.
|
|
func TestInjectFeedback_NeitherWindow(t *testing.T) {
|
|
rec := &recorder{
|
|
responses: map[string][]byte{
|
|
"has-session": {},
|
|
"list-windows": []byte("5-brain\n"),
|
|
},
|
|
}
|
|
sm := NewSessionManager(rec.run)
|
|
iss := issue.Issue{Number: 5}
|
|
result := InjectFeedback(sm, iss, "feedback")
|
|
if result {
|
|
t.Error("expected InjectFeedback to return false when neither impl nor test window exists")
|
|
}
|
|
}
|
|
|
|
// TestDeleteCmd_WindowsClosedFirst verifies windows are closed before other cleanup.
|
|
func TestDeleteCmd_WindowsClosedFirst(t *testing.T) {
|
|
rec := &recorder{
|
|
responses: map[string][]byte{
|
|
"has-session": {},
|
|
"list-windows": []byte("5-brain\n5-impl\n"),
|
|
},
|
|
}
|
|
sm := NewSessionManager(rec.run)
|
|
iss := issue.Issue{Number: 5, Branch: "", WorktreePath: "", PR: ""}
|
|
restore := overrideWorkspaceRoot(t.TempDir())
|
|
defer restore()
|
|
|
|
// gitea calls will fail (no real server) but window closure happens first.
|
|
cmd := deleteCmd(iss, sm)
|
|
_ = cmd()
|
|
|
|
var killWindowCalls int
|
|
for _, call := range rec.calls {
|
|
if len(call) > 0 && call[0] == "kill-window" {
|
|
killWindowCalls++
|
|
}
|
|
}
|
|
if killWindowCalls == 0 {
|
|
t.Error("expected kill-window calls for existing windows (5-brain, 5-impl)")
|
|
}
|
|
|
|
var firstListWindows, firstKillWindow int = -1, -1
|
|
for i, call := range rec.calls {
|
|
if len(call) > 0 {
|
|
if call[0] == "list-windows" && firstListWindows == -1 {
|
|
firstListWindows = i
|
|
}
|
|
if call[0] == "kill-window" && firstKillWindow == -1 {
|
|
firstKillWindow = i
|
|
}
|
|
}
|
|
}
|
|
if firstListWindows == -1 || firstKillWindow == -1 {
|
|
t.Logf("calls: %v", rec.calls)
|
|
t.Skip("could not find both list-windows and kill-window calls")
|
|
}
|
|
if firstListWindows > firstKillWindow {
|
|
t.Error("expected list-windows (check) before kill-window")
|
|
}
|
|
}
|
|
|
|
// TestDeleteCmd_FullCleanupOrder asserts the destructive cleanup runs in the
|
|
// order mandated by handleDeleteIssue: close windows → remove worktree (force) →
|
|
// Gitea resources (close PR, delete branch, delete issue). A real worktree is
|
|
// created so its removal is observable on disk; the Gitea step is recorded via
|
|
// an injected fn so no live host is needed.
|
|
func TestDeleteCmd_FullCleanupOrder(t *testing.T) {
|
|
workspaceRoot := initGitRepo(t)
|
|
relWorktree := ".claude/worktrees/abcd1234"
|
|
absWorktree := filepath.Join(workspaceRoot, relWorktree)
|
|
if out, err := exec.Command("git", "-C", workspaceRoot, "worktree", "add", absWorktree, "-b", "feature/abcd1234").CombinedOutput(); err != nil {
|
|
t.Fatalf("worktree add: %v\n%s", err, out)
|
|
}
|
|
|
|
restore := overrideWorkspaceRoot(workspaceRoot)
|
|
defer restore()
|
|
|
|
var order []string
|
|
rec := &recorder{
|
|
responses: map[string][]byte{
|
|
"has-session": {},
|
|
"list-windows": []byte("5-impl\n"),
|
|
},
|
|
}
|
|
sm := NewSessionManager(rec.run)
|
|
|
|
origGitea := deleteGiteaResourcesFn
|
|
deleteGiteaResourcesFn = func(_ context.Context, _ string, _ issue.Issue) error {
|
|
order = append(order, "gitea")
|
|
return nil
|
|
}
|
|
defer func() { deleteGiteaResourcesFn = origGitea }()
|
|
|
|
iss := issue.Issue{
|
|
Number: 5,
|
|
Branch: "feature/abcd1234",
|
|
WorktreePath: relWorktree,
|
|
PR: "42",
|
|
}
|
|
|
|
msg := deleteCmd(iss, sm)()
|
|
result, ok := msg.(deleteResultMsg)
|
|
if !ok {
|
|
t.Fatalf("expected deleteResultMsg, got %T", msg)
|
|
}
|
|
if result.err != nil {
|
|
t.Fatalf("unexpected delete error: %v", result.err)
|
|
}
|
|
|
|
// Step 1: window closed (kill-window for 5-impl).
|
|
var killSeen bool
|
|
for _, call := range rec.calls {
|
|
if len(call) > 0 && call[0] == "kill-window" {
|
|
killSeen = true
|
|
}
|
|
}
|
|
if !killSeen {
|
|
t.Error("expected kill-window call for 5-impl")
|
|
}
|
|
|
|
// Step 2: worktree removed from disk (proves it ran before the gitea step,
|
|
// which only completes after a successful remove given fail-stop ordering).
|
|
if _, statErr := os.Stat(absWorktree); !os.IsNotExist(statErr) {
|
|
t.Errorf("worktree not removed: stat err = %v", statErr)
|
|
}
|
|
|
|
// Step 3: gitea cleanup ran exactly once, after the local steps.
|
|
if len(order) != 1 || order[0] != "gitea" {
|
|
t.Fatalf("expected gitea cleanup to run exactly once, got %v", order)
|
|
}
|
|
}
|
|
|
|
// TestDeleteCmd_WorktreeFailureStopsBeforeGitea asserts that when the worktree
|
|
// removal fails, the Gitea cleanup is never reached (fail-stop ordering).
|
|
func TestDeleteCmd_WorktreeFailureStopsBeforeGitea(t *testing.T) {
|
|
workspaceRoot := initGitRepo(t)
|
|
relWorktree := ".claude/worktrees/deadbeef"
|
|
absWorktree := filepath.Join(workspaceRoot, relWorktree)
|
|
// Create a plain directory (not a registered worktree) so `git worktree
|
|
// remove --force` fails — exercising the fail-stop path.
|
|
if err := os.MkdirAll(absWorktree, 0o755); err != nil {
|
|
t.Fatalf("mkdir: %v", err)
|
|
}
|
|
|
|
restore := overrideWorkspaceRoot(workspaceRoot)
|
|
defer restore()
|
|
|
|
rec := &recorder{
|
|
responses: map[string][]byte{
|
|
"has-session": {},
|
|
"list-windows": []byte(""),
|
|
},
|
|
}
|
|
sm := NewSessionManager(rec.run)
|
|
|
|
giteaCalled := false
|
|
origGitea := deleteGiteaResourcesFn
|
|
deleteGiteaResourcesFn = func(_ context.Context, _ string, _ issue.Issue) error {
|
|
giteaCalled = true
|
|
return nil
|
|
}
|
|
defer func() { deleteGiteaResourcesFn = origGitea }()
|
|
|
|
iss := issue.Issue{Number: 7, Branch: "feature/deadbeef", WorktreePath: relWorktree, PR: "99"}
|
|
msg := deleteCmd(iss, sm)()
|
|
result, ok := msg.(deleteResultMsg)
|
|
if !ok {
|
|
t.Fatalf("expected deleteResultMsg, got %T", msg)
|
|
}
|
|
if result.err == nil {
|
|
t.Fatal("expected worktree removal failure error")
|
|
}
|
|
if !contains(result.err.Error(), "remove worktree") {
|
|
t.Errorf("expected remove worktree error, got %v", result.err)
|
|
}
|
|
if giteaCalled {
|
|
t.Error("Gitea cleanup must not run after worktree removal failure")
|
|
}
|
|
}
|
|
|
|
// TestBrainstormStartCmd_FreshSession_OpensWindowAndReturnsStartedMsg asserts that a fresh
|
|
// brainstorm (empty SessionID) opens a tmux window and returns brainstormStartedMsg
|
|
// (not sessionActionResultMsg), so the model can chain the watcher.
|
|
func TestBrainstormStartCmd_FreshSession_OpensWindowAndReturnsStartedMsg(t *testing.T) {
|
|
rec := &recorder{
|
|
responses: map[string][]byte{
|
|
"has-session": {},
|
|
"list-windows": []byte(""),
|
|
},
|
|
}
|
|
sm := NewSessionManager(rec.run)
|
|
iss := issue.Issue{Number: 11, SessionID: ""}
|
|
restore := overrideWorkspaceRoot(t.TempDir())
|
|
defer restore()
|
|
|
|
msg := brainstormStartCmd(iss, sm)()
|
|
|
|
started, ok := msg.(brainstormStartedMsg)
|
|
if !ok {
|
|
t.Fatalf("expected brainstormStartedMsg on fresh start, got %T", msg)
|
|
}
|
|
if started.issueNumber != 11 {
|
|
t.Errorf("issueNumber = %d, want 11", started.issueNumber)
|
|
}
|
|
if started.workspaceRoot == "" {
|
|
t.Error("workspaceRoot must not be empty")
|
|
}
|
|
|
|
var hasNewWindow bool
|
|
for _, call := range rec.calls {
|
|
if len(call) > 0 && call[0] == "new-window" {
|
|
hasNewWindow = true
|
|
}
|
|
}
|
|
if !hasNewWindow {
|
|
t.Error("expected new-window call when opening fresh brainstorm")
|
|
}
|
|
}
|
|
|
|
// TestBrainstormStartCmd_WatcherCapturesSessionID asserts that watchBrainstormSessionCmd
|
|
// returns brainstormSessionCapturedMsg with the captured sessionId, and that
|
|
// writeBrainstormSessionCmd calls MergeStateJSON with "sessionId".
|
|
func TestBrainstormStartCmd_WatcherCapturesSessionID(t *testing.T) {
|
|
const capturedID = "brain-sess-abc123"
|
|
|
|
origWatch := watchBrainstormFn
|
|
defer func() { watchBrainstormFn = origWatch }()
|
|
watchBrainstormFn = func(_ string) (string, error) {
|
|
return capturedID, nil
|
|
}
|
|
|
|
restore := overrideWorkspaceRoot(t.TempDir())
|
|
defer restore()
|
|
|
|
msg := watchBrainstormSessionCmd(42, t.TempDir())()
|
|
|
|
captured, ok := msg.(brainstormSessionCapturedMsg)
|
|
if !ok {
|
|
t.Fatalf("expected brainstormSessionCapturedMsg, got %T", msg)
|
|
}
|
|
if captured.issueNumber != 42 {
|
|
t.Errorf("issueNumber = %d, want 42", captured.issueNumber)
|
|
}
|
|
if captured.sessionID != capturedID {
|
|
t.Errorf("sessionID = %q, want %q", captured.sessionID, capturedID)
|
|
}
|
|
}
|
|
|
|
// TestBrainstormStartCmd_WatcherTimeout asserts that a timed-out watcher returns
|
|
// brainstormSessionCapturedMsg with an empty sessionID (not a hard error).
|
|
func TestBrainstormStartCmd_WatcherTimeout(t *testing.T) {
|
|
origWatch := watchBrainstormFn
|
|
defer func() { watchBrainstormFn = origWatch }()
|
|
watchBrainstormFn = func(_ string) (string, error) {
|
|
return "", nil // simulate timeout / no session found
|
|
}
|
|
|
|
msg := watchBrainstormSessionCmd(7, t.TempDir())()
|
|
|
|
captured, ok := msg.(brainstormSessionCapturedMsg)
|
|
if !ok {
|
|
t.Fatalf("expected brainstormSessionCapturedMsg on timeout, got %T", msg)
|
|
}
|
|
if captured.sessionID != "" {
|
|
t.Errorf("expected empty sessionID on timeout, got %q", captured.sessionID)
|
|
}
|
|
}
|
|
|
|
// TestTestCmd_ResumeCarriesSystemPromptCommand verifies that when testCmd builds
|
|
// a resume command the resulting tmux new-window arg contains --system-prompt.
|
|
// We verify this by inspecting claudeCmd directly (unit plumbing test) since
|
|
// config.Load is not interceptable from package tui without a seam in config.
|
|
func TestTestCmd_ResumeCarriesSystemPromptCommand(t *testing.T) {
|
|
// Direct unit test: claudeCmd with ResumeSessionID + SystemPromptCommand.
|
|
got := claudeCmd(claudeCmdOpts{
|
|
ResumeSessionID: "test-sess-xyz",
|
|
ProfilePath: "/p.json",
|
|
SystemPromptCommand: "get-sys-prompt",
|
|
})
|
|
// TS order: --dangerously-skip-permissions --settings <p> --system-prompt="$(cmd)" --resume <id>
|
|
want := `claude --dangerously-skip-permissions --settings '/p.json' --system-prompt="$(get-sys-prompt)" --resume 'test-sess-xyz'`
|
|
if got != want {
|
|
t.Errorf("test-resume claudeCmd:\n got %q\n want %q", got, want)
|
|
}
|
|
}
|
|
|
|
// TestClaudeResumeCmd_CarriesSystemPromptCommand verifies the brainstorm-resume
|
|
// helper passes SystemPromptCommand in correct flag order.
|
|
func TestClaudeResumeCmd_CarriesSystemPromptCommand(t *testing.T) {
|
|
got := claudeResumeCmd("brain-sess-123", "/prof.json", "echo sys-prompt")
|
|
want := `claude --dangerously-skip-permissions --settings '/prof.json' --system-prompt="$(echo sys-prompt)" --resume 'brain-sess-123'`
|
|
if got != want {
|
|
t.Errorf("brainstorm-resume:\n got %q\n want %q", got, want)
|
|
}
|
|
}
|
|
|
|
// TestFlagOrder_ImplementResume verifies the implement resume flag order matches TS exactly:
|
|
// claude --effort high --dangerously-skip-permissions [--settings] [--system-prompt] --resume <id>
|
|
func TestFlagOrder_ImplementResume(t *testing.T) {
|
|
got := claudeCmd(claudeCmdOpts{
|
|
ResumeSessionID: "impl-sess",
|
|
ProfilePath: "/impl.json",
|
|
EffortHigh: true,
|
|
SystemPromptCommand: "get-prompt",
|
|
})
|
|
want := `claude --effort high --dangerously-skip-permissions --settings '/impl.json' --system-prompt="$(get-prompt)" --resume 'impl-sess'`
|
|
if got != want {
|
|
t.Errorf("implement-resume flag order:\n got %q\n want %q", got, want)
|
|
}
|
|
}
|
|
|
|
// TestFlagOrder_FreshPrompt verifies fresh-prompt flag order:
|
|
// claude --dangerously-skip-permissions [--settings] [--system-prompt] <prompt>
|
|
func TestFlagOrder_FreshPrompt(t *testing.T) {
|
|
got := claudeCmd(claudeCmdOpts{
|
|
Prompt: "do the work",
|
|
ProfilePath: "/p.json",
|
|
SystemPromptCommand: "cat /sys",
|
|
})
|
|
want := `claude --dangerously-skip-permissions --settings '/p.json' --system-prompt="$(cat /sys)" 'do the work'`
|
|
if got != want {
|
|
t.Errorf("fresh-prompt flag order:\n got %q\n want %q", got, want)
|
|
}
|
|
}
|