package workon import ( "os" "os/exec" "path/filepath" "strings" "testing" ) func TestRunRejectsEmptyTitle(t *testing.T) { err := Run("", "", false, false) if err == nil { t.Fatal("expected error for empty title, got nil") } if !strings.Contains(err.Error(), "todo_item_title is required") { t.Errorf("unexpected error message: %s", err.Error()) } } func TestMoveQueuedToDoing(t *testing.T) { tmpDir := t.TempDir() origDir, _ := os.Getwd() if err := os.Chdir(tmpDir); err != nil { t.Fatal(err) } defer func() { _ = os.Chdir(origDir) }() // Set up todo structure if err := os.MkdirAll("todo/queued", 0755); err != nil { t.Fatal(err) } if err := os.MkdirAll("todo/doing", 0755); err != nil { t.Fatal(err) } if err := os.WriteFile("todo/queued/test-item.md", []byte("# Test Item\n"), 0644); err != nil { t.Fatal(err) } if err := moveQueuedToDoing("test-item"); err != nil { t.Fatalf("moveQueuedToDoing failed: %v", err) } // Verify source is gone if _, err := os.Stat("todo/queued/test-item.md"); !os.IsNotExist(err) { t.Error("expected queued file to be removed") } // Verify destination exists if _, err := os.Stat("todo/doing/test-item.md"); err != nil { t.Errorf("expected doing file to exist: %v", err) } } func TestMoveQueuedToDoingMissingFile(t *testing.T) { tmpDir := t.TempDir() origDir, _ := os.Getwd() if err := os.Chdir(tmpDir); err != nil { t.Fatal(err) } defer func() { _ = os.Chdir(origDir) }() if err := os.MkdirAll("todo/queued", 0755); err != nil { t.Fatal(err) } if err := os.MkdirAll("todo/doing", 0755); err != nil { t.Fatal(err) } err := moveQueuedToDoing("nonexistent") if err == nil { t.Fatal("expected error for missing queued file, got nil") } } func TestCreateFixFile(t *testing.T) { tmpDir := t.TempDir() path := filepath.Join(tmpDir, "my-fix.md") if err := createFixFile(path, "my-fix"); err != nil { t.Fatalf("createFixFile failed: %v", err) } data, err := os.ReadFile(path) if err != nil { t.Fatalf("failed to read created file: %v", err) } content := string(data) if !strings.Contains(content, "# my-fix") { t.Errorf("expected title heading, got: %s", content) } if !strings.Contains(content, "## Work Plan") { t.Errorf("expected Work Plan heading, got: %s", content) } } func TestReadFileContent(t *testing.T) { tmpDir := t.TempDir() t.Run("existing file", func(t *testing.T) { path := filepath.Join(tmpDir, "exists.md") expected := "hello world" if err := os.WriteFile(path, []byte(expected), 0644); err != nil { t.Fatal(err) } got := readFileContent(path) if got != expected { t.Errorf("readFileContent = %q, want %q", got, expected) } }) t.Run("missing file returns empty string", func(t *testing.T) { got := readFileContent(filepath.Join(tmpDir, "missing.md")) if got != "" { t.Errorf("readFileContent for missing file = %q, want empty", got) } }) } func TestCompleteItemMovesFile(t *testing.T) { tmpDir := t.TempDir() origDir, _ := os.Getwd() if err := os.Chdir(tmpDir); err != nil { t.Fatal(err) } defer func() { _ = os.Chdir(origDir) }() // Set up todo structure with a doing item if err := os.MkdirAll("todo/doing", 0755); err != nil { t.Fatal(err) } if err := os.MkdirAll("todo/completed", 0755); err != nil { t.Fatal(err) } if err := os.WriteFile("todo/doing/test-item.md", []byte("# Test\n"), 0644); err != nil { t.Fatal(err) } // completeItem will try to git commit — that will fail in a non-git temp dir, // but we can verify the file move happened before the commit step _ = completeItem("test-item", true) // For a fix, file should be moved regardless of commit outcome if _, err := os.Stat("todo/completed/test-item.md"); err != nil { t.Errorf("expected completed file to exist: %v", err) } if _, err := os.Stat("todo/doing/test-item.md"); !os.IsNotExist(err) { t.Error("expected doing file to be removed") } } func TestUpdateReadmeIndex(t *testing.T) { tmpDir := t.TempDir() origDir, _ := os.Getwd() if err := os.Chdir(tmpDir); err != nil { t.Fatal(err) } defer func() { _ = os.Chdir(origDir) }() if err := os.MkdirAll("todo", 0755); err != nil { t.Fatal(err) } readme := `# Todo Index ## Queued * [1] [my-feature](queued/my-feature.md): My feature * [2] [other-item](queued/other-item.md): Other item ## Completed * [old-item](./completed/old-item.md) : old-item *(done)* ` if err := os.WriteFile("todo/README.md", []byte(readme), 0644); err != nil { t.Fatal(err) } if err := updateReadmeIndex("my-feature"); err != nil { t.Fatalf("updateReadmeIndex failed: %v", err) } data, err := os.ReadFile("todo/README.md") if err != nil { t.Fatal(err) } result := string(data) // Should have removed the queued entry if strings.Contains(result, "queued/my-feature.md") { t.Error("expected my-feature to be removed from Queued section") } // Should still have the other queued item if !strings.Contains(result, "queued/other-item.md") { t.Error("expected other-item to remain in Queued section") } // Should have added to Completed section if !strings.Contains(result, "completed/my-feature.md") { t.Error("expected my-feature to appear in Completed section") } // Old completed item should still be there if !strings.Contains(result, "completed/old-item.md") { t.Error("expected old-item to remain in Completed section") } } func TestUpdateReadmeIndexMissingReadme(t *testing.T) { tmpDir := t.TempDir() origDir, _ := os.Getwd() if err := os.Chdir(tmpDir); err != nil { t.Fatal(err) } defer func() { _ = os.Chdir(origDir) }() err := updateReadmeIndex("anything") if err == nil { t.Fatal("expected error when README doesn't exist, got nil") } } // initTempGitRepo creates a temp dir with a git repo and returns it. // The caller is responsible for chdir-ing back. func initTempGitRepo(t *testing.T) string { t.Helper() tmpDir := t.TempDir() cmds := [][]string{ {"git", "init"}, {"git", "config", "user.email", "test@test.com"}, {"git", "config", "user.name", "Test"}, } for _, args := range cmds { cmd := exec.Command(args[0], args[1:]...) cmd.Dir = tmpDir if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("git init setup failed (%v): %s", err, out) } } return tmpDir } func TestCreateGitBranchNew(t *testing.T) { tmpDir := initTempGitRepo(t) origDir, _ := os.Getwd() if err := os.Chdir(tmpDir); err != nil { t.Fatal(err) } defer func() { _ = os.Chdir(origDir) }() // Need at least one commit for branches to work if err := os.WriteFile("init.txt", []byte("init"), 0644); err != nil { t.Fatal(err) } _ = exec.Command("git", "add", ".").Run() _ = exec.Command("git", "commit", "-m", "initial").Run() if err := createGitBranch("feature/test-branch"); err != nil { t.Fatalf("createGitBranch failed: %v", err) } // Verify we're on the new branch out, err := exec.Command("git", "branch", "--show-current").Output() if err != nil { t.Fatalf("git branch --show-current failed: %v", err) } branch := strings.TrimSpace(string(out)) if branch != "feature/test-branch" { t.Errorf("expected branch 'feature/test-branch', got %q", branch) } } func TestCreateGitBranchExisting(t *testing.T) { tmpDir := initTempGitRepo(t) origDir, _ := os.Getwd() if err := os.Chdir(tmpDir); err != nil { t.Fatal(err) } defer func() { _ = os.Chdir(origDir) }() // Initial commit + create branch if err := os.WriteFile("init.txt", []byte("init"), 0644); err != nil { t.Fatal(err) } _ = exec.Command("git", "add", ".").Run() _ = exec.Command("git", "commit", "-m", "initial").Run() _ = exec.Command("git", "checkout", "-b", "fix/existing").Run() _ = exec.Command("git", "checkout", "-").Run() // back to main/master // Should checkout existing branch, not error if err := createGitBranch("fix/existing"); err != nil { t.Fatalf("createGitBranch for existing branch failed: %v", err) } out, _ := exec.Command("git", "branch", "--show-current").Output() branch := strings.TrimSpace(string(out)) if branch != "fix/existing" { t.Errorf("expected branch 'fix/existing', got %q", branch) } } func TestCommitChangesInGitRepo(t *testing.T) { tmpDir := initTempGitRepo(t) origDir, _ := os.Getwd() if err := os.Chdir(tmpDir); err != nil { t.Fatal(err) } defer func() { _ = os.Chdir(origDir) }() // Initial commit if err := os.WriteFile("init.txt", []byte("init"), 0644); err != nil { t.Fatal(err) } _ = exec.Command("git", "add", ".").Run() _ = exec.Command("git", "commit", "-m", "initial").Run() // Create a todo file to commit if err := os.MkdirAll("todo/doing", 0755); err != nil { t.Fatal(err) } if err := os.WriteFile("todo/doing/test.md", []byte("# Test\n"), 0644); err != nil { t.Fatal(err) } if err := commitChanges("test commit message"); err != nil { t.Fatalf("commitChanges failed: %v", err) } // Verify the commit was created out, err := exec.Command("git", "log", "--oneline", "-1").Output() if err != nil { t.Fatalf("git log failed: %v", err) } if !strings.Contains(string(out), "test commit message") { t.Errorf("expected commit message in log, got: %s", out) } } func TestCommitChangesNothingToCommit(t *testing.T) { tmpDir := initTempGitRepo(t) origDir, _ := os.Getwd() if err := os.Chdir(tmpDir); err != nil { t.Fatal(err) } defer func() { _ = os.Chdir(origDir) }() // Initial commit if err := os.WriteFile("init.txt", []byte("init"), 0644); err != nil { t.Fatal(err) } _ = exec.Command("git", "add", ".").Run() _ = exec.Command("git", "commit", "-m", "initial").Run() // Create empty todo dir but no files to stage if err := os.MkdirAll("todo", 0755); err != nil { t.Fatal(err) } // Should fail — nothing to commit err := commitChanges("empty commit") if err == nil { t.Error("expected error when nothing to commit, got nil") } } func TestCompleteItemWithGit(t *testing.T) { tmpDir := initTempGitRepo(t) origDir, _ := os.Getwd() if err := os.Chdir(tmpDir); err != nil { t.Fatal(err) } defer func() { _ = os.Chdir(origDir) }() // Initial commit if err := os.WriteFile("init.txt", []byte("init"), 0644); err != nil { t.Fatal(err) } _ = exec.Command("git", "add", ".").Run() _ = exec.Command("git", "commit", "-m", "initial").Run() // Set up todo structure for _, d := range []string{"todo/doing", "todo/completed"} { if err := os.MkdirAll(d, 0755); err != nil { t.Fatal(err) } } if err := os.WriteFile("todo/doing/my-fix.md", []byte("# Fix\n"), 0644); err != nil { t.Fatal(err) } // Stage and commit the doing file first so git tracks it _ = exec.Command("git", "add", "todo/").Run() _ = exec.Command("git", "commit", "-m", "add doing item").Run() // Now complete as a fix (no README update) if err := completeItem("my-fix", true); err != nil { t.Fatalf("completeItem failed: %v", err) } // Verify moved if _, err := os.Stat("todo/completed/my-fix.md"); err != nil { t.Errorf("expected completed file: %v", err) } if _, err := os.Stat("todo/doing/my-fix.md"); !os.IsNotExist(err) { t.Error("expected doing file to be gone") } // Verify commit out, _ := exec.Command("git", "log", "--oneline", "-1").Output() if !strings.Contains(string(out), "Complete work on my-fix") { t.Errorf("expected completion commit, got: %s", out) } } func TestCompleteItemNonFixUpdatesReadme(t *testing.T) { tmpDir := initTempGitRepo(t) origDir, _ := os.Getwd() if err := os.Chdir(tmpDir); err != nil { t.Fatal(err) } defer func() { _ = os.Chdir(origDir) }() // Initial commit if err := os.WriteFile("init.txt", []byte("init"), 0644); err != nil { t.Fatal(err) } _ = exec.Command("git", "add", ".").Run() _ = exec.Command("git", "commit", "-m", "initial").Run() // Set up todo structure with README for _, d := range []string{"todo/doing", "todo/completed"} { if err := os.MkdirAll(d, 0755); err != nil { t.Fatal(err) } } readme := "# Todo\n\n## Queued\n\n* [my-todo](queued/my-todo.md): My todo\n\n## Completed\n\n" if err := os.WriteFile("todo/README.md", []byte(readme), 0644); err != nil { t.Fatal(err) } if err := os.WriteFile("todo/doing/my-todo.md", []byte("# My Todo\n"), 0644); err != nil { t.Fatal(err) } _ = exec.Command("git", "add", "todo/").Run() _ = exec.Command("git", "commit", "-m", "add todo item").Run() // Complete as a non-fix (should update README) if err := completeItem("my-todo", false); err != nil { t.Fatalf("completeItem (non-fix) failed: %v", err) } data, err := os.ReadFile("todo/README.md") if err != nil { t.Fatal(err) } result := string(data) if strings.Contains(result, "queued/my-todo.md") { t.Error("expected my-todo removed from Queued") } if !strings.Contains(result, "completed/my-todo.md") { t.Error("expected my-todo added to Completed") } } func TestRunCnaddIfAvailable(t *testing.T) { // Just exercise the function — it should not panic regardless // of whether cnadd is installed runCnaddIfAvailable("test-item") } func TestOpenIDEIfConfigured(t *testing.T) { // With no IDE configured (default), this should silently skip openIDEIfConfigured() }