- Introduce tests for analyze, recipe, workon commands - Expand scaffold tests with language detection and context harvesting - Add tests for config getters, git utilities (tags, logs, diff) - Enhance linter with primary language detection tests - Cover logger level setting branches - New prompts loading tests with local/global fallback - Todo bootstrap and structure tests - Comprehensive workon flow tests including file moves, git integration, README updates - Update README coverage from 54% to 62%
475 lines
13 KiB
Go
475 lines
13 KiB
Go
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()
|
|
}
|