test: refactor factory setup for coverage (#790)
* test: add git helper and guard annotations * chore: align release metadata with 0.40.0 * test: refactor factory setup for coverage
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
"name": "beads",
|
||||
"source": "./",
|
||||
"description": "AI-supervised issue tracker for coding workflows",
|
||||
"version": "0.39.1"
|
||||
"version": "0.40.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "beads",
|
||||
"description": "AI-supervised issue tracker for coding workflows. Manage tasks, discover work, and maintain context with simple CLI commands.",
|
||||
"version": "0.39.1",
|
||||
"version": "0.40.0",
|
||||
"author": {
|
||||
"name": "Steve Yegge",
|
||||
"url": "https://github.com/steveyegge"
|
||||
|
||||
6
cmd/bd/setup/exit.go
Normal file
6
cmd/bd/setup/exit.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package setup
|
||||
|
||||
import "os"
|
||||
|
||||
// setupExit is used by setup commands to exit the process. Tests can stub this.
|
||||
var setupExit = os.Exit
|
||||
@@ -1,7 +1,9 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
@@ -100,127 +102,152 @@ For more details, see README.md and docs/QUICKSTART.md.
|
||||
<!-- END BEADS INTEGRATION -->
|
||||
`
|
||||
|
||||
var (
|
||||
errAgentsFileMissing = errors.New("agents file not found")
|
||||
errBeadsSectionMissing = errors.New("beads section missing")
|
||||
)
|
||||
|
||||
type factoryEnv struct {
|
||||
agentsPath string
|
||||
stdout io.Writer
|
||||
stderr io.Writer
|
||||
}
|
||||
|
||||
var factoryEnvProvider = defaultFactoryEnv
|
||||
|
||||
func defaultFactoryEnv() factoryEnv {
|
||||
return factoryEnv{
|
||||
agentsPath: "AGENTS.md",
|
||||
stdout: os.Stdout,
|
||||
stderr: os.Stderr,
|
||||
}
|
||||
}
|
||||
|
||||
// InstallFactory installs Factory.ai/Droid integration
|
||||
func InstallFactory() {
|
||||
agentsPath := "AGENTS.md"
|
||||
env := factoryEnvProvider()
|
||||
if err := installFactory(env); err != nil {
|
||||
setupExit(1)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("Installing Factory.ai (Droid) integration...")
|
||||
func installFactory(env factoryEnv) error {
|
||||
fmt.Fprintln(env.stdout, "Installing Factory.ai (Droid) integration...")
|
||||
|
||||
// Check if AGENTS.md exists
|
||||
var currentContent string
|
||||
data, err := os.ReadFile(agentsPath)
|
||||
data, err := os.ReadFile(env.agentsPath)
|
||||
if err == nil {
|
||||
currentContent = string(data)
|
||||
} else if !os.IsNotExist(err) {
|
||||
fmt.Fprintf(os.Stderr, "Error: failed to read AGENTS.md: %v\n", err)
|
||||
os.Exit(1)
|
||||
fmt.Fprintf(env.stderr, "Error: failed to read %s: %v\n", env.agentsPath, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// If file exists, check if we already have beads section
|
||||
if currentContent != "" {
|
||||
if strings.Contains(currentContent, factoryBeginMarker) {
|
||||
// Update existing section
|
||||
newContent := updateBeadsSection(currentContent)
|
||||
if err := atomicWriteFile(agentsPath, []byte(newContent)); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: write AGENTS.md: %v\n", err)
|
||||
os.Exit(1)
|
||||
if err := atomicWriteFile(env.agentsPath, []byte(newContent)); err != nil {
|
||||
fmt.Fprintf(env.stderr, "Error: write %s: %v\n", env.agentsPath, err)
|
||||
return err
|
||||
}
|
||||
fmt.Println("✓ Updated existing beads section in AGENTS.md")
|
||||
fmt.Fprintln(env.stdout, "✓ Updated existing beads section in AGENTS.md")
|
||||
} else {
|
||||
// Append to existing file
|
||||
newContent := currentContent + "\n\n" + factoryBeadsSection
|
||||
if err := atomicWriteFile(agentsPath, []byte(newContent)); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: write AGENTS.md: %v\n", err)
|
||||
os.Exit(1)
|
||||
if err := atomicWriteFile(env.agentsPath, []byte(newContent)); err != nil {
|
||||
fmt.Fprintf(env.stderr, "Error: write %s: %v\n", env.agentsPath, err)
|
||||
return err
|
||||
}
|
||||
fmt.Println("✓ Added beads section to existing AGENTS.md")
|
||||
fmt.Fprintln(env.stdout, "✓ Added beads section to existing AGENTS.md")
|
||||
}
|
||||
} else {
|
||||
// Create new AGENTS.md with template
|
||||
newContent := createNewAgentsFile()
|
||||
if err := atomicWriteFile(agentsPath, []byte(newContent)); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: write AGENTS.md: %v\n", err)
|
||||
os.Exit(1)
|
||||
if err := atomicWriteFile(env.agentsPath, []byte(newContent)); err != nil {
|
||||
fmt.Fprintf(env.stderr, "Error: write %s: %v\n", env.agentsPath, err)
|
||||
return err
|
||||
}
|
||||
fmt.Println("✓ Created new AGENTS.md with beads integration")
|
||||
fmt.Fprintln(env.stdout, "✓ Created new AGENTS.md with beads integration")
|
||||
}
|
||||
|
||||
fmt.Printf("\n✓ Factory.ai (Droid) integration installed\n")
|
||||
fmt.Printf(" File: %s\n", agentsPath)
|
||||
fmt.Println("\nFactory Droid will automatically read AGENTS.md on session start.")
|
||||
fmt.Println("No additional configuration needed!")
|
||||
fmt.Fprintln(env.stdout, "\n✓ Factory.ai (Droid) integration installed")
|
||||
fmt.Fprintf(env.stdout, " File: %s\n", env.agentsPath)
|
||||
fmt.Fprintln(env.stdout, "\nFactory Droid will automatically read AGENTS.md on session start.")
|
||||
fmt.Fprintln(env.stdout, "No additional configuration needed!")
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckFactory checks if Factory.ai integration is installed
|
||||
func CheckFactory() {
|
||||
agentsPath := "AGENTS.md"
|
||||
env := factoryEnvProvider()
|
||||
if err := checkFactory(env); err != nil {
|
||||
setupExit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if AGENTS.md exists
|
||||
data, err := os.ReadFile(agentsPath)
|
||||
func checkFactory(env factoryEnv) error {
|
||||
data, err := os.ReadFile(env.agentsPath)
|
||||
if os.IsNotExist(err) {
|
||||
fmt.Println("✗ AGENTS.md not found")
|
||||
fmt.Println(" Run: bd setup factory")
|
||||
os.Exit(1)
|
||||
fmt.Fprintln(env.stdout, "✗ AGENTS.md not found")
|
||||
fmt.Fprintln(env.stdout, " Run: bd setup factory")
|
||||
return errAgentsFileMissing
|
||||
} else if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: failed to read AGENTS.md: %v\n", err)
|
||||
os.Exit(1)
|
||||
fmt.Fprintf(env.stderr, "Error: failed to read %s: %v\n", env.agentsPath, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if it contains beads section
|
||||
content := string(data)
|
||||
if strings.Contains(content, factoryBeginMarker) {
|
||||
fmt.Println("✓ Factory.ai integration installed:", agentsPath)
|
||||
fmt.Println(" Beads section found in AGENTS.md")
|
||||
} else {
|
||||
fmt.Println("⚠ AGENTS.md exists but no beads section found")
|
||||
fmt.Println(" Run: bd setup factory (to add beads section)")
|
||||
os.Exit(1)
|
||||
fmt.Fprintf(env.stdout, "✓ Factory.ai integration installed: %s\n", env.agentsPath)
|
||||
fmt.Fprintln(env.stdout, " Beads section found in AGENTS.md")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Fprintln(env.stdout, "⚠ AGENTS.md exists but no beads section found")
|
||||
fmt.Fprintln(env.stdout, " Run: bd setup factory (to add beads section)")
|
||||
return errBeadsSectionMissing
|
||||
}
|
||||
|
||||
// RemoveFactory removes Factory.ai integration
|
||||
func RemoveFactory() {
|
||||
agentsPath := "AGENTS.md"
|
||||
env := factoryEnvProvider()
|
||||
if err := removeFactory(env); err != nil {
|
||||
setupExit(1)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("Removing Factory.ai (Droid) integration...")
|
||||
|
||||
// Read current content
|
||||
data, err := os.ReadFile(agentsPath)
|
||||
func removeFactory(env factoryEnv) error {
|
||||
fmt.Fprintln(env.stdout, "Removing Factory.ai (Droid) integration...")
|
||||
data, err := os.ReadFile(env.agentsPath)
|
||||
if os.IsNotExist(err) {
|
||||
fmt.Println("No AGENTS.md file found")
|
||||
return
|
||||
fmt.Fprintln(env.stdout, "No AGENTS.md file found")
|
||||
return nil
|
||||
} else if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: failed to read AGENTS.md: %v\n", err)
|
||||
os.Exit(1)
|
||||
fmt.Fprintf(env.stderr, "Error: failed to read %s: %v\n", env.agentsPath, err)
|
||||
return err
|
||||
}
|
||||
|
||||
content := string(data)
|
||||
|
||||
// Check if beads section exists
|
||||
if !strings.Contains(content, factoryBeginMarker) {
|
||||
fmt.Println("No beads section found in AGENTS.md")
|
||||
return
|
||||
fmt.Fprintln(env.stdout, "No beads section found in AGENTS.md")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove beads section
|
||||
newContent := removeBeadsSection(content)
|
||||
|
||||
// If file would be empty after removal, delete it
|
||||
trimmed := strings.TrimSpace(newContent)
|
||||
if trimmed == "" {
|
||||
if err := os.Remove(agentsPath); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: failed to remove AGENTS.md: %v\n", err)
|
||||
os.Exit(1)
|
||||
if err := os.Remove(env.agentsPath); err != nil {
|
||||
fmt.Fprintf(env.stderr, "Error: failed to remove %s: %v\n", env.agentsPath, err)
|
||||
return err
|
||||
}
|
||||
fmt.Println("✓ Removed AGENTS.md (file was empty after removing beads section)")
|
||||
} else {
|
||||
// Write back modified content
|
||||
if err := atomicWriteFile(agentsPath, []byte(newContent)); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: write AGENTS.md: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("✓ Removed beads section from AGENTS.md")
|
||||
fmt.Fprintf(env.stdout, "✓ Removed %s (file was empty after removing beads section)\n", env.agentsPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := atomicWriteFile(env.agentsPath, []byte(newContent)); err != nil {
|
||||
fmt.Fprintf(env.stderr, "Error: write %s: %v\n", env.agentsPath, err)
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(env.stdout, "✓ Removed beads section from AGENTS.md")
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateBeadsSection replaces the beads section in existing content
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -137,480 +139,249 @@ func TestCreateNewAgentsFile(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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 newFactoryTestEnv(t *testing.T) (factoryEnv, *bytes.Buffer, *bytes.Buffer) {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
stdout := &bytes.Buffer{}
|
||||
stderr := &bytes.Buffer{}
|
||||
return factoryEnv{
|
||||
agentsPath: filepath.Join(dir, "AGENTS.md"),
|
||||
stdout: stdout,
|
||||
stderr: stderr,
|
||||
}, stdout, stderr
|
||||
}
|
||||
|
||||
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)
|
||||
func stubFactoryEnvProvider(t *testing.T, env factoryEnv) {
|
||||
t.Helper()
|
||||
orig := factoryEnvProvider
|
||||
factoryEnvProvider = func() factoryEnv {
|
||||
return env
|
||||
}
|
||||
t.Cleanup(func() { factoryEnvProvider = orig })
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
if err := os.Chdir(tmpDir); err != nil {
|
||||
t.Fatalf("failed to change to temp directory: %v", err)
|
||||
func TestInstallFactoryCreatesNewFile(t *testing.T) {
|
||||
env, stdout, _ := newFactoryTestEnv(t)
|
||||
if err := installFactory(env); err != nil {
|
||||
t.Fatalf("installFactory returned error: %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")
|
||||
data, err := os.ReadFile(env.agentsPath)
|
||||
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, factoryBeginMarker) || !strings.Contains(content, factoryEndMarker) {
|
||||
t.Fatal("missing factory markers in new file")
|
||||
}
|
||||
if !strings.Contains(content, factoryEndMarker) {
|
||||
t.Error("AGENTS.md missing end marker")
|
||||
if !strings.Contains(stdout.String(), "Factory.ai (Droid) integration installed") {
|
||||
t.Error("expected success message in stdout")
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
func TestInstallFactoryUpdatesExistingSection(t *testing.T) {
|
||||
env, _, _ := newFactoryTestEnv(t)
|
||||
initial := `# Header
|
||||
|
||||
<!-- BEGIN BEADS INTEGRATION -->
|
||||
Old beads content
|
||||
Old content
|
||||
<!-- END BEADS INTEGRATION -->
|
||||
|
||||
Other content`
|
||||
if err := os.WriteFile("AGENTS.md", []byte(oldContent), 0644); err != nil {
|
||||
t.Fatalf("failed to create AGENTS.md: %v", err)
|
||||
# Footer`
|
||||
if err := os.WriteFile(env.agentsPath, []byte(initial), 0644); err != nil {
|
||||
t.Fatalf("failed to seed AGENTS.md: %v", err)
|
||||
}
|
||||
|
||||
// Run InstallFactory
|
||||
InstallFactory()
|
||||
|
||||
// Verify file was updated
|
||||
data, err := os.ReadFile("AGENTS.md")
|
||||
if err := installFactory(env); err != nil {
|
||||
t.Fatalf("installFactory returned error: %v", err)
|
||||
}
|
||||
data, err := os.ReadFile(env.agentsPath)
|
||||
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, "Old content") {
|
||||
t.Error("old beads section should be 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")
|
||||
if !strings.Contains(content, "# Footer") {
|
||||
t.Error("content after beads section should remain")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveFactory(t *testing.T) {
|
||||
origDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get working directory: %v", err)
|
||||
func TestInstallFactoryReportsWriteError(t *testing.T) {
|
||||
env, _, stderr := newFactoryTestEnv(t)
|
||||
if err := os.Mkdir(env.agentsPath, 0o755); err != nil {
|
||||
t.Fatalf("failed to create 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,
|
||||
},
|
||||
if err := installFactory(env); err == nil {
|
||||
t.Fatal("expected error when agents path is directory")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
if !strings.Contains(stderr.String(), "failed to read") {
|
||||
t.Error("expected error message in stderr")
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
func TestCheckFactoryScenarios(t *testing.T) {
|
||||
t.Run("missing file", func(t *testing.T) {
|
||||
env, stdout, _ := newFactoryTestEnv(t)
|
||||
err := checkFactory(env)
|
||||
if !errors.Is(err, errAgentsFileMissing) {
|
||||
t.Fatalf("expected errAgentsFileMissing, got %v", err)
|
||||
}
|
||||
}()
|
||||
if !strings.Contains(stdout.String(), "Run: bd setup factory") {
|
||||
t.Error("expected guidance message")
|
||||
}
|
||||
})
|
||||
|
||||
// Should not panic when file doesn't exist
|
||||
RemoveFactory()
|
||||
t.Run("missing section", func(t *testing.T) {
|
||||
env, stdout, _ := newFactoryTestEnv(t)
|
||||
if err := os.WriteFile(env.agentsPath, []byte("# Project"), 0644); err != nil {
|
||||
t.Fatalf("failed to write file: %v", err)
|
||||
}
|
||||
err := checkFactory(env)
|
||||
if !errors.Is(err, errBeadsSectionMissing) {
|
||||
t.Fatalf("expected errBeadsSectionMissing, got %v", err)
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "no beads section") {
|
||||
t.Error("expected warning output")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
env, stdout, _ := newFactoryTestEnv(t)
|
||||
if err := os.WriteFile(env.agentsPath, []byte(factoryBeadsSection), 0644); err != nil {
|
||||
t.Fatalf("failed to seed file: %v", err)
|
||||
}
|
||||
if err := checkFactory(env); err != nil {
|
||||
t.Fatalf("checkFactory returned error: %v", err)
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "integration installed") {
|
||||
t.Error("expected success output")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
func TestRemoveFactoryScenarios(t *testing.T) {
|
||||
t.Run("remove section and keep file", func(t *testing.T) {
|
||||
env, stdout, _ := newFactoryTestEnv(t)
|
||||
content := "# Top\n\n" + factoryBeadsSection + "\n\n# Bottom"
|
||||
if err := os.WriteFile(env.agentsPath, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("failed to seed AGENTS.md: %v", err)
|
||||
}
|
||||
}()
|
||||
if err := removeFactory(env); err != nil {
|
||||
t.Fatalf("removeFactory returned error: %v", err)
|
||||
}
|
||||
data, err := os.ReadFile(env.agentsPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read AGENTS.md: %v", err)
|
||||
}
|
||||
if strings.Contains(string(data), factoryBeginMarker) {
|
||||
t.Error("beads section should be removed")
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "Removed beads section") {
|
||||
t.Error("expected removal message")
|
||||
}
|
||||
})
|
||||
|
||||
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)
|
||||
}
|
||||
t.Run("delete file when only beads", func(t *testing.T) {
|
||||
env, stdout, _ := newFactoryTestEnv(t)
|
||||
if err := os.WriteFile(env.agentsPath, []byte(factoryBeadsSection), 0644); err != nil {
|
||||
t.Fatalf("failed to seed AGENTS.md: %v", err)
|
||||
}
|
||||
if err := removeFactory(env); err != nil {
|
||||
t.Fatalf("removeFactory returned error: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(env.agentsPath); !os.IsNotExist(err) {
|
||||
t.Fatal("AGENTS.md should be removed")
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "file was empty") {
|
||||
t.Error("expected deletion message")
|
||||
}
|
||||
})
|
||||
|
||||
// Should not panic or modify file
|
||||
RemoveFactory()
|
||||
t.Run("missing file", func(t *testing.T) {
|
||||
env, stdout, _ := newFactoryTestEnv(t)
|
||||
if err := removeFactory(env); err != nil {
|
||||
t.Fatalf("removeFactory returned error: %v", err)
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "No AGENTS.md file found") {
|
||||
t.Error("expected info message for missing file")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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 TestWrapperExitsOnError(t *testing.T) {
|
||||
t.Run("InstallFactory", func(t *testing.T) {
|
||||
cap := stubSetupExit(t)
|
||||
env := factoryEnv{agentsPath: filepath.Join(t.TempDir(), "dir"), stdout: &bytes.Buffer{}, stderr: &bytes.Buffer{}}
|
||||
if err := os.Mkdir(env.agentsPath, 0o755); err != nil {
|
||||
t.Fatalf("failed to create directory: %v", err)
|
||||
}
|
||||
stubFactoryEnvProvider(t, env)
|
||||
InstallFactory()
|
||||
if !cap.called || cap.code != 1 {
|
||||
t.Fatal("InstallFactory should exit on error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CheckFactory", func(t *testing.T) {
|
||||
cap := stubSetupExit(t)
|
||||
env := factoryEnv{agentsPath: filepath.Join(t.TempDir(), "missing"), stdout: &bytes.Buffer{}, stderr: &bytes.Buffer{}}
|
||||
stubFactoryEnvProvider(t, env)
|
||||
CheckFactory()
|
||||
if !cap.called || cap.code != 1 {
|
||||
t.Fatal("CheckFactory should exit on error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("RemoveFactory", func(t *testing.T) {
|
||||
cap := stubSetupExit(t)
|
||||
env := factoryEnv{agentsPath: filepath.Join(t.TempDir(), "AGENTS.md"), stdout: &bytes.Buffer{}, stderr: &bytes.Buffer{}}
|
||||
if err := os.WriteFile(env.agentsPath, []byte(factoryBeadsSection), 0644); err != nil {
|
||||
t.Fatalf("failed to seed file: %v", err)
|
||||
}
|
||||
if err := os.Chmod(env.agentsPath, 0o000); err != nil {
|
||||
t.Fatalf("failed to chmod file: %v", err)
|
||||
}
|
||||
stubFactoryEnvProvider(t, env)
|
||||
RemoveFactory()
|
||||
if !cap.called || cap.code != 1 {
|
||||
t.Fatal("RemoveFactory should exit on error")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
required := []string{"bd create", "bd update", "bd close", "bd ready", "discovered-from"}
|
||||
for _, token := range required {
|
||||
if !strings.Contains(section, token) {
|
||||
t.Errorf("factoryBeadsSection missing %q", token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFactoryMarkers(t *testing.T) {
|
||||
// Verify markers are properly formatted
|
||||
if !strings.Contains(factoryBeginMarker, "BEGIN") {
|
||||
t.Error("Begin marker should contain 'BEGIN'")
|
||||
t.Error("begin marker should mention 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'")
|
||||
t.Error("end marker should mention END")
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
t.Error("section should start with begin marker")
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(strings.TrimSpace(factoryBeadsSection), factoryEndMarker) {
|
||||
t.Error("factoryBeadsSection should end with end marker")
|
||||
trimmed := strings.TrimSpace(factoryBeadsSection)
|
||||
if !strings.HasSuffix(trimmed, factoryEndMarker) {
|
||||
t.Error("section 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")
|
||||
if !strings.Contains(updated, "# Header") || !strings.Contains(updated, "# Footer") {
|
||||
t.Error("update should preserve surrounding content")
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
22
cmd/bd/setup/setup_test_helpers.go
Normal file
22
cmd/bd/setup/setup_test_helpers.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package setup
|
||||
|
||||
import "testing"
|
||||
|
||||
type exitCapture struct {
|
||||
called bool
|
||||
code int
|
||||
}
|
||||
|
||||
func stubSetupExit(t *testing.T) *exitCapture {
|
||||
t.Helper()
|
||||
cap := &exitCapture{}
|
||||
orig := setupExit
|
||||
setupExit = func(code int) {
|
||||
cap.called = true
|
||||
cap.code = code
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
setupExit = orig
|
||||
})
|
||||
return cap
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/sh
|
||||
# bd-shim v1
|
||||
# bd-hooks-version: 0.39.1
|
||||
# bd-hooks-version: 0.40.0
|
||||
#
|
||||
# bd (beads) post-checkout hook - thin shim
|
||||
#
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/sh
|
||||
# bd-shim v1
|
||||
# bd-hooks-version: 0.39.1
|
||||
# bd-hooks-version: 0.40.0
|
||||
#
|
||||
# bd (beads) post-merge hook - thin shim
|
||||
#
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/sh
|
||||
# bd-shim v1
|
||||
# bd-hooks-version: 0.39.1
|
||||
# bd-hooks-version: 0.40.0
|
||||
#
|
||||
# bd (beads) pre-commit hook - thin shim
|
||||
#
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/sh
|
||||
# bd-shim v1
|
||||
# bd-hooks-version: 0.39.1
|
||||
# bd-hooks-version: 0.40.0
|
||||
#
|
||||
# bd (beads) pre-push hook - thin shim
|
||||
#
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
|
||||
var (
|
||||
// Version is the current version of bd (overridden by ldflags at build time)
|
||||
Version = "0.40.0"
|
||||
Version = "0.40.0"
|
||||
// Build can be set via ldflags at compile time
|
||||
Build = "dev"
|
||||
// Commit and branch the git revision the binary was built from (optional ldflag)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "beads-mcp"
|
||||
version = "0.39.1"
|
||||
version = "0.40.0"
|
||||
description = "MCP server for beads issue tracker."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -4,4 +4,4 @@ This package provides an MCP (Model Context Protocol) server that exposes
|
||||
beads (bd) issue tracker functionality to MCP Clients.
|
||||
"""
|
||||
|
||||
__version__ = "0.39.1"
|
||||
__version__ = "0.40.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@beads/bd",
|
||||
"version": "0.39.1",
|
||||
"version": "0.40.0",
|
||||
"description": "Beads issue tracker - lightweight memory system for coding agents with native binary support",
|
||||
"main": "bin/bd.js",
|
||||
"bin": {
|
||||
|
||||
Reference in New Issue
Block a user