* test: add git helper and guard annotations * chore: align release metadata with 0.40.0 * test: refactor factory setup for coverage
388 lines
10 KiB
Go
388 lines
10 KiB
Go
package setup
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"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
|
|
|
|
<!-- BEGIN BEADS INTEGRATION -->
|
|
Old content here
|
|
<!-- END BEADS INTEGRATION -->
|
|
|
|
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
|
|
|
|
<!-- BEGIN BEADS INTEGRATION -->
|
|
Old content
|
|
<!-- END BEADS INTEGRATION -->`,
|
|
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
|
|
|
|
<!-- BEGIN BEADS INTEGRATION -->
|
|
Beads content
|
|
<!-- END BEADS INTEGRATION -->
|
|
|
|
More content`,
|
|
expected: `# My Project
|
|
More content`,
|
|
},
|
|
{
|
|
name: "remove section at end",
|
|
content: `# My Project
|
|
|
|
Content
|
|
|
|
<!-- BEGIN BEADS INTEGRATION -->
|
|
Beads content
|
|
<!-- END BEADS INTEGRATION -->`,
|
|
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<!-- BEGIN BEADS INTEGRATION -->\nContent",
|
|
expected: "# My Project\n<!-- BEGIN BEADS INTEGRATION -->\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 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 stubFactoryEnvProvider(t *testing.T, env factoryEnv) {
|
|
t.Helper()
|
|
orig := factoryEnvProvider
|
|
factoryEnvProvider = func() factoryEnv {
|
|
return env
|
|
}
|
|
t.Cleanup(func() { factoryEnvProvider = orig })
|
|
}
|
|
|
|
func TestInstallFactoryCreatesNewFile(t *testing.T) {
|
|
env, stdout, _ := newFactoryTestEnv(t)
|
|
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, factoryBeginMarker) || !strings.Contains(content, factoryEndMarker) {
|
|
t.Fatal("missing factory markers in new file")
|
|
}
|
|
if !strings.Contains(stdout.String(), "Factory.ai (Droid) integration installed") {
|
|
t.Error("expected success message in stdout")
|
|
}
|
|
}
|
|
|
|
func TestInstallFactoryUpdatesExistingSection(t *testing.T) {
|
|
env, _, _ := newFactoryTestEnv(t)
|
|
initial := `# Header
|
|
|
|
<!-- BEGIN BEADS INTEGRATION -->
|
|
Old content
|
|
<!-- END BEADS INTEGRATION -->
|
|
|
|
# Footer`
|
|
if err := os.WriteFile(env.agentsPath, []byte(initial), 0644); err != nil {
|
|
t.Fatalf("failed to seed AGENTS.md: %v", err)
|
|
}
|
|
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 content") {
|
|
t.Error("old beads section should be replaced")
|
|
}
|
|
if !strings.Contains(content, "# Footer") {
|
|
t.Error("content after beads section should remain")
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
if err := installFactory(env); err == nil {
|
|
t.Fatal("expected error when agents path is directory")
|
|
}
|
|
if !strings.Contains(stderr.String(), "failed to read") {
|
|
t.Error("expected error message in stderr")
|
|
}
|
|
}
|
|
|
|
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")
|
|
}
|
|
})
|
|
|
|
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 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")
|
|
}
|
|
})
|
|
|
|
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")
|
|
}
|
|
})
|
|
|
|
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")
|
|
}
|
|
})
|
|
}
|
|
|
|
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) {
|
|
section := factoryBeadsSection
|
|
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) {
|
|
if !strings.Contains(factoryBeginMarker, "BEGIN") {
|
|
t.Error("begin marker should mention BEGIN")
|
|
}
|
|
if !strings.Contains(factoryEndMarker, "END") {
|
|
t.Error("end marker should mention END")
|
|
}
|
|
}
|
|
|
|
func TestMarkersMatch(t *testing.T) {
|
|
if !strings.HasPrefix(factoryBeadsSection, factoryBeginMarker) {
|
|
t.Error("section should start with begin marker")
|
|
}
|
|
trimmed := strings.TrimSpace(factoryBeadsSection)
|
|
if !strings.HasSuffix(trimmed, factoryEndMarker) {
|
|
t.Error("section should end with end marker")
|
|
}
|
|
}
|
|
|
|
func TestUpdateBeadsSectionPreservesWhitespace(t *testing.T) {
|
|
content := "# Header\n\n" + factoryBeadsSection + "\n\n# Footer"
|
|
updated := updateBeadsSection(content)
|
|
if !strings.Contains(updated, "# Header") || !strings.Contains(updated, "# Footer") {
|
|
t.Error("update should preserve surrounding content")
|
|
}
|
|
}
|