diff --git a/cmd/bd/setup/aider_test.go b/cmd/bd/setup/aider_test.go new file mode 100644 index 00000000..99468869 --- /dev/null +++ b/cmd/bd/setup/aider_test.go @@ -0,0 +1,422 @@ +package setup + +import ( + "os" + "strings" + "testing" +) + +func TestAiderConfigTemplate(t *testing.T) { + // Verify template contains required content + if !strings.Contains(aiderConfigTemplate, "read:") { + t.Error("aiderConfigTemplate missing 'read:' directive") + } + if !strings.Contains(aiderConfigTemplate, ".aider/BEADS.md") { + t.Error("aiderConfigTemplate missing reference to BEADS.md") + } +} + +func TestAiderBeadsInstructions(t *testing.T) { + requiredContent := []string{ + "bd ready", + "bd create", + "bd update", + "bd close", + "bd sync", + "/run", + "bug", + "feature", + "task", + "epic", + } + + for _, req := range requiredContent { + if !strings.Contains(aiderBeadsInstructions, req) { + t.Errorf("aiderBeadsInstructions missing required content: %q", req) + } + } +} + +func TestAiderReadmeTemplate(t *testing.T) { + requiredContent := []string{ + "Aider + Beads Integration", + "/run", + "bd ready", + "bd create", + "bd close", + "bd sync", + } + + for _, req := range requiredContent { + if !strings.Contains(aiderReadmeTemplate, req) { + t.Errorf("aiderReadmeTemplate missing required content: %q", req) + } + } +} + +func TestInstallAider(t *testing.T) { + origDir, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + + tmpDir := t.TempDir() + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp directory: %v", err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatalf("failed to restore working directory: %v", err) + } + }() + + InstallAider() + + // Verify all files were created + files := []struct { + path string + content string + }{ + {".aider.conf.yml", aiderConfigTemplate}, + {".aider/BEADS.md", aiderBeadsInstructions}, + {".aider/README.md", aiderReadmeTemplate}, + } + + for _, f := range files { + if !FileExists(f.path) { + t.Errorf("File was not created: %s", f.path) + continue + } + + data, err := os.ReadFile(f.path) + if err != nil { + t.Errorf("Failed to read %s: %v", f.path, err) + continue + } + + if string(data) != f.content { + t.Errorf("File %s content doesn't match expected template", f.path) + } + } +} + +func TestInstallAider_ExistingDirectory(t *testing.T) { + origDir, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + + tmpDir := t.TempDir() + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp directory: %v", err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatalf("failed to restore working directory: %v", err) + } + }() + + // Pre-create the directory + if err := os.MkdirAll(".aider", 0755); err != nil { + t.Fatalf("failed to create directory: %v", err) + } + + // Should not fail + InstallAider() + + // Verify files were created + if !FileExists(".aider/BEADS.md") { + t.Error("BEADS.md not created") + } +} + +func TestInstallAiderIdempotent(t *testing.T) { + origDir, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + + tmpDir := t.TempDir() + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp directory: %v", err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatalf("failed to restore working directory: %v", err) + } + }() + + // Run twice + InstallAider() + firstData, _ := os.ReadFile(".aider.conf.yml") + + InstallAider() + secondData, _ := os.ReadFile(".aider.conf.yml") + + if string(firstData) != string(secondData) { + t.Error("InstallAider should be idempotent") + } +} + +func TestRemoveAider(t *testing.T) { + origDir, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + + tmpDir := t.TempDir() + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp directory: %v", err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatalf("failed to restore working directory: %v", err) + } + }() + + // Install first + InstallAider() + + // Verify files exist + files := []string{".aider.conf.yml", ".aider/BEADS.md", ".aider/README.md"} + for _, f := range files { + if !FileExists(f) { + t.Fatalf("File should exist before removal: %s", f) + } + } + + // Remove + RemoveAider() + + // Verify files are gone + for _, f := range files { + if FileExists(f) { + t.Errorf("File should have been removed: %s", f) + } + } +} + +func TestRemoveAider_NoFiles(t *testing.T) { + origDir, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + + tmpDir := t.TempDir() + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp directory: %v", err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatalf("failed to restore working directory: %v", err) + } + }() + + // Should not panic when files don't exist + RemoveAider() +} + +func TestRemoveAider_PartialFiles(t *testing.T) { + origDir, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + + tmpDir := t.TempDir() + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp directory: %v", err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatalf("failed to restore working directory: %v", err) + } + }() + + // Create only the config file + if err := os.WriteFile(".aider.conf.yml", []byte(aiderConfigTemplate), 0644); err != nil { + t.Fatalf("failed to create config file: %v", err) + } + + // Should not panic + RemoveAider() + + // Config should be removed + if FileExists(".aider.conf.yml") { + t.Error("Config file should have been removed") + } +} + +func TestRemoveAider_DirectoryCleanup(t *testing.T) { + origDir, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + + tmpDir := t.TempDir() + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp directory: %v", err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatalf("failed to restore working directory: %v", err) + } + }() + + // Install + InstallAider() + + // Remove + RemoveAider() + + // Directory should be cleaned up if empty + // (the implementation tries to remove it but ignores errors) + if DirExists(".aider") { + // This is acceptable - directory might not be removed if not empty + // or the implementation doesn't remove it + } +} + +func TestRemoveAider_DirectoryWithOtherFiles(t *testing.T) { + origDir, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + + tmpDir := t.TempDir() + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp directory: %v", err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatalf("failed to restore working directory: %v", err) + } + }() + + // Install + InstallAider() + + // Add another file to .aider directory + if err := os.WriteFile(".aider/other.txt", []byte("keep me"), 0644); err != nil { + t.Fatalf("failed to create other file: %v", err) + } + + // Remove + RemoveAider() + + // Directory should still exist (has other files) + if !DirExists(".aider") { + t.Error("Directory should not be removed when it has other files") + } + + // Other file should still exist + if !FileExists(".aider/other.txt") { + t.Error("Other files should be preserved") + } +} + +func TestCheckAider_NotInstalled(t *testing.T) { + origDir, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + + tmpDir := t.TempDir() + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp directory: %v", err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatalf("failed to restore working directory: %v", err) + } + }() + + // CheckAider calls os.Exit(1) when not installed + // We can't easily test that, but we document expected behavior +} + +func TestCheckAider_Installed(t *testing.T) { + origDir, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + + tmpDir := t.TempDir() + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp directory: %v", err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatalf("failed to restore working directory: %v", err) + } + }() + + // Install first + InstallAider() + + // Should not panic or exit + CheckAider() +} + +func TestAiderInstructionsWorkflowPattern(t *testing.T) { + // Verify instructions contain the workflow pattern Aider users need + instructions := aiderBeadsInstructions + + // Should mention the /run command pattern + if !strings.Contains(instructions, "/run bd ready") { + t.Error("Should mention /run bd ready") + } + if !strings.Contains(instructions, "/run bd sync") { + t.Error("Should mention /run bd sync") + } + + // Should explain that Aider requires explicit commands + if !strings.Contains(instructions, "Aider requires") { + t.Error("Should explain Aider's explicit command requirement") + } +} + +func TestAiderReadmeForHumans(t *testing.T) { + // README should be helpful for humans, not just AI + readme := aiderReadmeTemplate + + // Should have human-friendly sections + if !strings.Contains(readme, "Quick Start") { + t.Error("README should have Quick Start section") + } + if !strings.Contains(readme, "How This Works") { + t.Error("README should explain how it works") + } +} + +func TestAiderFilePaths(t *testing.T) { + // Verify paths match Aider conventions + origDir, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + + tmpDir := t.TempDir() + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp directory: %v", err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatalf("failed to restore working directory: %v", err) + } + }() + + InstallAider() + + // Check expected file paths + expectedPaths := []string{ + ".aider.conf.yml", + ".aider/BEADS.md", + ".aider/README.md", + } + + for _, path := range expectedPaths { + if !FileExists(path) { + t.Errorf("Expected file at %s", path) + } + } +} diff --git a/cmd/bd/setup/cursor_test.go b/cmd/bd/setup/cursor_test.go new file mode 100644 index 00000000..ee44c796 --- /dev/null +++ b/cmd/bd/setup/cursor_test.go @@ -0,0 +1,309 @@ +package setup + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestCursorRulesTemplate(t *testing.T) { + // Verify template contains required content + requiredContent := []string{ + "bd prime", + "bd ready", + "bd create", + "bd update", + "bd close", + "bd sync", + "BEADS INTEGRATION", + } + + for _, req := range requiredContent { + if !strings.Contains(cursorRulesTemplate, req) { + t.Errorf("cursorRulesTemplate missing required content: %q", req) + } + } +} + +func TestInstallCursor(t *testing.T) { + origDir, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + + tmpDir := t.TempDir() + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp directory: %v", err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatalf("failed to restore working directory: %v", err) + } + }() + + InstallCursor() + + // Verify file was created + rulesPath := ".cursor/rules/beads.mdc" + if !FileExists(rulesPath) { + t.Fatal("Cursor rules file was not created") + } + + // Verify content + data, err := os.ReadFile(rulesPath) + if err != nil { + t.Fatalf("failed to read rules file: %v", err) + } + + if string(data) != cursorRulesTemplate { + t.Error("Rules file content doesn't match template") + } +} + +func TestInstallCursor_ExistingDirectory(t *testing.T) { + origDir, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + + tmpDir := t.TempDir() + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp directory: %v", err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatalf("failed to restore working directory: %v", err) + } + }() + + // Pre-create the directory + if err := os.MkdirAll(".cursor/rules", 0755); err != nil { + t.Fatalf("failed to create directory: %v", err) + } + + // Should not fail + InstallCursor() + + // Verify file was created + if !FileExists(".cursor/rules/beads.mdc") { + t.Fatal("Cursor rules file was not created") + } +} + +func TestInstallCursor_OverwriteExisting(t *testing.T) { + origDir, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + + tmpDir := t.TempDir() + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp directory: %v", err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatalf("failed to restore working directory: %v", err) + } + }() + + // Create existing file with different content + rulesPath := ".cursor/rules/beads.mdc" + if err := os.MkdirAll(filepath.Dir(rulesPath), 0755); err != nil { + t.Fatalf("failed to create directory: %v", err) + } + if err := os.WriteFile(rulesPath, []byte("old content"), 0644); err != nil { + t.Fatalf("failed to create old file: %v", err) + } + + InstallCursor() + + // Verify content was overwritten + data, err := os.ReadFile(rulesPath) + if err != nil { + t.Fatalf("failed to read rules file: %v", err) + } + + if string(data) == "old content" { + t.Error("Old content was not overwritten") + } + if string(data) != cursorRulesTemplate { + t.Error("Content doesn't match template") + } +} + +func TestInstallCursorIdempotent(t *testing.T) { + origDir, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + + tmpDir := t.TempDir() + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp directory: %v", err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatalf("failed to restore working directory: %v", err) + } + }() + + // Run twice + InstallCursor() + firstData, _ := os.ReadFile(".cursor/rules/beads.mdc") + + InstallCursor() + secondData, _ := os.ReadFile(".cursor/rules/beads.mdc") + + if string(firstData) != string(secondData) { + t.Error("InstallCursor should be idempotent") + } +} + +func TestRemoveCursor(t *testing.T) { + origDir, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + + tmpDir := t.TempDir() + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp directory: %v", err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatalf("failed to restore working directory: %v", err) + } + }() + + // Install first + InstallCursor() + + // Verify file exists + rulesPath := ".cursor/rules/beads.mdc" + if !FileExists(rulesPath) { + t.Fatal("File should exist before removal") + } + + // Remove + RemoveCursor() + + // Verify file is gone + if FileExists(rulesPath) { + t.Error("File should have been removed") + } +} + +func TestRemoveCursor_NoFile(t *testing.T) { + origDir, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + + tmpDir := t.TempDir() + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp directory: %v", err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatalf("failed to restore working directory: %v", err) + } + }() + + // Should not panic when file doesn't exist + RemoveCursor() +} + +func TestCheckCursor_NotInstalled(t *testing.T) { + origDir, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + + tmpDir := t.TempDir() + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp directory: %v", err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatalf("failed to restore working directory: %v", err) + } + }() + + // CheckCursor calls os.Exit(1) when not installed + // We can't easily test that, but we document expected behavior +} + +func TestCheckCursor_Installed(t *testing.T) { + origDir, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + + tmpDir := t.TempDir() + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp directory: %v", err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatalf("failed to restore working directory: %v", err) + } + }() + + // Install first + InstallCursor() + + // Should not panic or exit + CheckCursor() +} + +func TestCursorRulesPath(t *testing.T) { + // Verify the path is correct for Cursor IDE + expectedPath := ".cursor/rules/beads.mdc" + + // These are the paths used in the implementation + origDir, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + + tmpDir := t.TempDir() + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp directory: %v", err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatalf("failed to restore working directory: %v", err) + } + }() + + InstallCursor() + + // Verify the file was created at the expected path + if !FileExists(expectedPath) { + t.Errorf("Expected file at %s", expectedPath) + } +} + +func TestCursorTemplateFormatting(t *testing.T) { + // Verify template is well-formed + template := cursorRulesTemplate + + // Should have both markers + if !strings.Contains(template, "BEGIN BEADS INTEGRATION") { + t.Error("Missing BEGIN marker") + } + if !strings.Contains(template, "END BEADS INTEGRATION") { + t.Error("Missing END marker") + } + + // Should have workflow section + if !strings.Contains(template, "## Workflow") { + t.Error("Missing Workflow section") + } + + // Should have context loading section + if !strings.Contains(template, "## Context Loading") { + t.Error("Missing Context Loading section") + } +} diff --git a/cmd/bd/setup/factory_test.go b/cmd/bd/setup/factory_test.go new file mode 100644 index 00000000..5d96cda1 --- /dev/null +++ b/cmd/bd/setup/factory_test.go @@ -0,0 +1,616 @@ +package setup + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestUpdateBeadsSection(t *testing.T) { + tests := []struct { + name string + content string + expected string + }{ + { + name: "replace existing section", + content: `# My Project + +Some content + + +Old content here + + +More content after`, + expected: `# My Project + +Some content + +` + factoryBeadsSection + ` +More content after`, + }, + { + name: "append when no markers exist", + content: "# My Project\n\nSome content", + expected: "# My Project\n\nSome content\n\n" + factoryBeadsSection, + }, + { + name: "handle section at end of file", + content: `# My Project + + +Old content +`, + expected: `# My Project + +` + factoryBeadsSection, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := updateBeadsSection(tt.content) + if got != tt.expected { + t.Errorf("updateBeadsSection() mismatch\ngot:\n%s\nwant:\n%s", got, tt.expected) + } + }) + } +} + +func TestRemoveBeadsSection(t *testing.T) { + tests := []struct { + name string + content string + expected string + }{ + { + name: "remove section in middle", + content: `# My Project + + +Beads content + + +More content`, + expected: `# My Project +More content`, + }, + { + name: "remove section at end", + content: `# My Project + +Content + + +Beads content +`, + expected: `# My Project + +Content`, + }, + { + name: "no markers - return unchanged", + content: "# My Project\n\nNo beads section", + expected: "# My Project\n\nNo beads section", + }, + { + name: "only begin marker - return unchanged", + content: "# My Project\n\nContent", + expected: "# My Project\n\nContent", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := removeBeadsSection(tt.content) + if got != tt.expected { + t.Errorf("removeBeadsSection() mismatch\ngot:\n%q\nwant:\n%q", got, tt.expected) + } + }) + } +} + +func TestCreateNewAgentsFile(t *testing.T) { + content := createNewAgentsFile() + + // Verify it contains required elements + if !strings.Contains(content, "# Project Instructions for AI Agents") { + t.Error("Missing header in new agents file") + } + + if !strings.Contains(content, factoryBeginMarker) { + t.Error("Missing begin marker in new agents file") + } + + if !strings.Contains(content, factoryEndMarker) { + t.Error("Missing end marker in new agents file") + } + + if !strings.Contains(content, "## Build & Test") { + t.Error("Missing Build & Test section") + } + + if !strings.Contains(content, "## Architecture Overview") { + t.Error("Missing Architecture Overview section") + } +} + +func TestCheckFactory(t *testing.T) { + // Save original working directory + origDir, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + + tests := []struct { + name string + setupFile bool + fileContent string + expectExit bool + expectMessage string + }{ + { + name: "no AGENTS.md file", + setupFile: false, + expectExit: true, + expectMessage: "AGENTS.md not found", + }, + { + name: "AGENTS.md without beads section", + setupFile: true, + fileContent: "# Project\n\nNo beads here", + expectExit: true, + expectMessage: "no beads section found", + }, + { + name: "AGENTS.md with beads section", + setupFile: true, + fileContent: "# Project\n\n" + factoryBeadsSection, + expectExit: false, + expectMessage: "integration installed", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create temp directory and change to it + tmpDir := t.TempDir() + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp directory: %v", err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatalf("failed to restore working directory: %v", err) + } + }() + + if tt.setupFile { + if err := os.WriteFile("AGENTS.md", []byte(tt.fileContent), 0644); err != nil { + t.Fatalf("failed to create test file: %v", err) + } + } + + // We can't easily test os.Exit, so we just verify the function doesn't panic + // for the success case + if !tt.expectExit { + // This should not panic + func() { + defer func() { + if r := recover(); r != nil { + t.Errorf("CheckFactory panicked: %v", r) + } + }() + // Note: CheckFactory calls os.Exit on failure, so we can't test those cases directly + // We would need to refactor to use a testable exit function + }() + } + }) + } +} + +func TestInstallFactory_NewFile(t *testing.T) { + // Save original working directory + origDir, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + + tmpDir := t.TempDir() + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp directory: %v", err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatalf("failed to restore working directory: %v", err) + } + }() + + // Run InstallFactory + InstallFactory() + + // Verify file was created + data, err := os.ReadFile("AGENTS.md") + if err != nil { + t.Fatalf("failed to read AGENTS.md: %v", err) + } + + content := string(data) + if !strings.Contains(content, factoryBeginMarker) { + t.Error("AGENTS.md missing begin marker") + } + if !strings.Contains(content, factoryEndMarker) { + t.Error("AGENTS.md missing end marker") + } +} + +func TestInstallFactory_ExistingWithoutBeads(t *testing.T) { + origDir, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + + tmpDir := t.TempDir() + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp directory: %v", err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatalf("failed to restore working directory: %v", err) + } + }() + + // Create existing AGENTS.md without beads section + existingContent := "# My Custom Agents File\n\nExisting content\n" + if err := os.WriteFile("AGENTS.md", []byte(existingContent), 0644); err != nil { + t.Fatalf("failed to create AGENTS.md: %v", err) + } + + // Run InstallFactory + InstallFactory() + + // Verify file was updated + data, err := os.ReadFile("AGENTS.md") + if err != nil { + t.Fatalf("failed to read AGENTS.md: %v", err) + } + + content := string(data) + if !strings.Contains(content, "My Custom Agents File") { + t.Error("Lost existing content") + } + if !strings.Contains(content, factoryBeginMarker) { + t.Error("AGENTS.md missing begin marker") + } +} + +func TestInstallFactory_ExistingWithBeads(t *testing.T) { + origDir, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + + tmpDir := t.TempDir() + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp directory: %v", err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatalf("failed to restore working directory: %v", err) + } + }() + + // Create existing AGENTS.md with old beads section + oldContent := `# My Project + + +Old beads content + + +Other content` + if err := os.WriteFile("AGENTS.md", []byte(oldContent), 0644); err != nil { + t.Fatalf("failed to create AGENTS.md: %v", err) + } + + // Run InstallFactory + InstallFactory() + + // Verify file was updated + data, err := os.ReadFile("AGENTS.md") + if err != nil { + t.Fatalf("failed to read AGENTS.md: %v", err) + } + + content := string(data) + if strings.Contains(content, "Old beads content") { + t.Error("Old beads content should have been replaced") + } + if !strings.Contains(content, "Other content") { + t.Error("Lost content after beads section") + } + if !strings.Contains(content, "Issue Tracking with bd") { + t.Error("Missing new beads section content") + } +} + +func TestRemoveFactory(t *testing.T) { + origDir, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + + tests := []struct { + name string + initialContent string + expectFile bool + expectedContent string + }{ + { + name: "remove beads section, keep other content", + initialContent: "# Project\n\n" + factoryBeadsSection + "\n\n## Other Section\n\nContent", + expectFile: true, + expectedContent: "# Project\n\n## Other Section\n\nContent", + }, + { + name: "remove file when only beads section", + initialContent: factoryBeadsSection, + expectFile: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir := t.TempDir() + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp directory: %v", err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatalf("failed to restore working directory: %v", err) + } + }() + + if err := os.WriteFile("AGENTS.md", []byte(tt.initialContent), 0644); err != nil { + t.Fatalf("failed to create AGENTS.md: %v", err) + } + + RemoveFactory() + + _, err := os.Stat("AGENTS.md") + fileExists := err == nil + + if fileExists != tt.expectFile { + t.Errorf("file exists = %v, want %v", fileExists, tt.expectFile) + } + + if tt.expectFile { + data, err := os.ReadFile("AGENTS.md") + if err != nil { + t.Fatalf("failed to read AGENTS.md: %v", err) + } + if string(data) != tt.expectedContent { + t.Errorf("content mismatch\ngot: %q\nwant: %q", string(data), tt.expectedContent) + } + } + }) + } +} + +func TestRemoveFactory_NoFile(t *testing.T) { + origDir, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + + tmpDir := t.TempDir() + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp directory: %v", err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatalf("failed to restore working directory: %v", err) + } + }() + + // Should not panic when file doesn't exist + RemoveFactory() +} + +func TestRemoveFactory_NoBeadsSection(t *testing.T) { + origDir, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + + tmpDir := t.TempDir() + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp directory: %v", err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatalf("failed to restore working directory: %v", err) + } + }() + + content := "# Project\n\nNo beads here" + if err := os.WriteFile("AGENTS.md", []byte(content), 0644); err != nil { + t.Fatalf("failed to create AGENTS.md: %v", err) + } + + // Should not panic or modify file + RemoveFactory() + + data, err := os.ReadFile("AGENTS.md") + if err != nil { + t.Fatalf("failed to read AGENTS.md: %v", err) + } + if string(data) != content { + t.Error("File should not have been modified") + } +} + +func TestFactoryBeadsSectionContent(t *testing.T) { + // Verify the beads section contains expected documentation + section := factoryBeadsSection + + requiredContent := []string{ + "bd create", + "bd update", + "bd close", + "bd ready", + "bug", + "feature", + "task", + "epic", + "discovered-from", + } + + for _, req := range requiredContent { + if !strings.Contains(section, req) { + t.Errorf("factoryBeadsSection missing required content: %q", req) + } + } +} + +func TestFactoryMarkers(t *testing.T) { + // Verify markers are properly formatted + if !strings.Contains(factoryBeginMarker, "BEGIN") { + t.Error("Begin marker should contain 'BEGIN'") + } + if !strings.Contains(factoryEndMarker, "END") { + t.Error("End marker should contain 'END'") + } + if !strings.Contains(factoryBeginMarker, "BEADS") { + t.Error("Begin marker should contain 'BEADS'") + } + if !strings.Contains(factoryEndMarker, "BEADS") { + t.Error("End marker should contain 'BEADS'") + } +} + +func TestInstallFactoryIdempotent(t *testing.T) { + origDir, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + + tmpDir := t.TempDir() + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp directory: %v", err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatalf("failed to restore working directory: %v", err) + } + }() + + // Run InstallFactory twice + InstallFactory() + firstData, _ := os.ReadFile("AGENTS.md") + + InstallFactory() + secondData, _ := os.ReadFile("AGENTS.md") + + // Content should be identical + if string(firstData) != string(secondData) { + t.Error("InstallFactory should be idempotent") + } + + // Should only have one beads section + content := string(secondData) + beginCount := strings.Count(content, factoryBeginMarker) + if beginCount != 1 { + t.Errorf("Expected 1 begin marker, got %d", beginCount) + } +} + +func TestInstallFactory_DirectoryError(t *testing.T) { + origDir, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + + tmpDir := t.TempDir() + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp directory: %v", err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatalf("failed to restore working directory: %v", err) + } + }() + + // Create AGENTS.md as a directory to cause an error + if err := os.Mkdir("AGENTS.md", 0755); err != nil { + t.Fatalf("failed to create directory: %v", err) + } + + // InstallFactory should handle this gracefully (or exit) + // We can't easily test os.Exit, but verify it doesn't panic + defer func() { + if r := recover(); r != nil { + t.Errorf("InstallFactory panicked: %v", r) + } + }() +} + +// Test internal marker constants +func TestMarkersMatch(t *testing.T) { + // Ensure the section template contains both markers + if !strings.HasPrefix(factoryBeadsSection, factoryBeginMarker) { + t.Error("factoryBeadsSection should start with begin marker") + } + + if !strings.HasSuffix(strings.TrimSpace(factoryBeadsSection), factoryEndMarker) { + t.Error("factoryBeadsSection should end with end marker") + } +} + +func TestUpdateBeadsSectionPreservesWhitespace(t *testing.T) { + // Test that whitespace around content is preserved + content := "# Header\n\n" + factoryBeadsSection + "\n\n# Footer" + + // Update should be idempotent for content that already has current section + updated := updateBeadsSection(content) + + if !strings.Contains(updated, "# Header") { + t.Error("Lost header") + } + if !strings.Contains(updated, "# Footer") { + t.Error("Lost footer") + } +} + +func TestCheckFactory_SubdirectoryPath(t *testing.T) { + origDir, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + + tmpDir := t.TempDir() + subDir := filepath.Join(tmpDir, "subdir") + if err := os.MkdirAll(subDir, 0755); err != nil { + t.Fatalf("failed to create subdirectory: %v", err) + } + + // Create AGENTS.md in tmpDir + content := "# Project\n\n" + factoryBeadsSection + if err := os.WriteFile(filepath.Join(tmpDir, "AGENTS.md"), []byte(content), 0644); err != nil { + t.Fatalf("failed to create AGENTS.md: %v", err) + } + + // Change to subdirectory - AGENTS.md should not be found + if err := os.Chdir(subDir); err != nil { + t.Fatalf("failed to change to temp directory: %v", err) + } + defer func() { + if err := os.Chdir(origDir); err != nil { + t.Fatalf("failed to restore working directory: %v", err) + } + }() + + // CheckFactory looks for AGENTS.md in current directory, not parent + // So it should fail in subdirectory + // We can't test os.Exit, but this documents the expected behavior +}