- Renamed config.json to metadata.json to clarify purpose (database metadata) - Fixed config.yaml/config.json conflict by making Viper explicitly load only config.yaml - Added automatic migration from config.json to metadata.json on first read - Fixed jsonOutput variable shadowing across 22 command files - Updated bd init to create both metadata.json and config.yaml template - Fixed 5 failing JSON output tests - All tests passing Resolves config file confusion and makes config.yaml work correctly. Closes #178 (global flags), addresses config issues from #193 Amp-Thread-ID: https://ampcode.com/threads/T-e6ac8192-e18f-4ed7-83bc-4a5986718bb7 Co-authored-by: Amp <amp@ampcode.com>
852 lines
22 KiB
Go
852 lines
22 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/steveyegge/beads/internal/types"
|
|
)
|
|
|
|
func TestShowCommand(t *testing.T) {
|
|
// Save original global state
|
|
origStore := store
|
|
origDBPath := dbPath
|
|
origDaemonClient := daemonClient
|
|
defer func() {
|
|
store = origStore
|
|
dbPath = origDBPath
|
|
daemonClient = origDaemonClient
|
|
}()
|
|
|
|
tmpDir := t.TempDir()
|
|
testDB := filepath.Join(tmpDir, "test.db")
|
|
|
|
// Create test store and set it globally
|
|
testStore := newTestStore(t, testDB)
|
|
defer testStore.Close()
|
|
|
|
store = testStore
|
|
dbPath = testDB
|
|
daemonClient = nil // Force direct mode
|
|
|
|
// Ensure BEADS_NO_DAEMON is set
|
|
os.Setenv("BEADS_NO_DAEMON", "1")
|
|
defer os.Unsetenv("BEADS_NO_DAEMON")
|
|
|
|
ctx := context.Background()
|
|
|
|
// Create test issues
|
|
issue1 := &types.Issue{
|
|
Title: "First Test Issue",
|
|
Description: "This is a test description",
|
|
Priority: 1,
|
|
IssueType: types.TypeBug,
|
|
Status: types.StatusOpen,
|
|
}
|
|
if err := testStore.CreateIssue(ctx, issue1, "test-user"); err != nil {
|
|
t.Fatalf("Failed to create issue1: %v", err)
|
|
}
|
|
|
|
issue2 := &types.Issue{
|
|
Title: "Second Test Issue",
|
|
Description: "Another description",
|
|
Design: "Design notes here",
|
|
Notes: "Some notes",
|
|
Priority: 2,
|
|
IssueType: types.TypeFeature,
|
|
Status: types.StatusInProgress,
|
|
Assignee: "alice",
|
|
}
|
|
if err := testStore.CreateIssue(ctx, issue2, "test-user"); err != nil {
|
|
t.Fatalf("Failed to create issue2: %v", err)
|
|
}
|
|
|
|
// Add label to issue1
|
|
if err := testStore.AddLabel(ctx, issue1.ID, "critical", "test-user"); err != nil {
|
|
t.Fatalf("Failed to add label: %v", err)
|
|
}
|
|
|
|
// Add dependency: issue2 depends on issue1
|
|
dep := &types.Dependency{
|
|
IssueID: issue2.ID,
|
|
DependsOnID: issue1.ID,
|
|
Type: types.DepBlocks,
|
|
}
|
|
if err := testStore.AddDependency(ctx, dep, "test-user"); err != nil {
|
|
t.Fatalf("Failed to add dependency: %v", err)
|
|
}
|
|
|
|
t.Run("show single issue", func(t *testing.T) {
|
|
// Capture output
|
|
var buf bytes.Buffer
|
|
oldStdout := os.Stdout
|
|
r, w, _ := os.Pipe()
|
|
os.Stdout = w
|
|
|
|
// Reset command state
|
|
rootCmd.SetArgs([]string{"show", issue1.ID})
|
|
showCmd.Flags().Set("json", "false")
|
|
|
|
err := rootCmd.Execute()
|
|
|
|
// Restore stdout and read output
|
|
w.Close()
|
|
buf.ReadFrom(r)
|
|
os.Stdout = oldStdout
|
|
output := buf.String()
|
|
|
|
if err != nil {
|
|
t.Fatalf("show command failed: %v", err)
|
|
}
|
|
|
|
// Verify output contains issue details
|
|
if !strings.Contains(output, issue1.ID) {
|
|
t.Errorf("Output should contain issue ID %s", issue1.ID)
|
|
}
|
|
if !strings.Contains(output, issue1.Title) {
|
|
t.Errorf("Output should contain issue title %s", issue1.Title)
|
|
}
|
|
if !strings.Contains(output, "critical") {
|
|
t.Error("Output should contain label 'critical'")
|
|
}
|
|
})
|
|
|
|
t.Run("show single issue with JSON output", func(t *testing.T) {
|
|
// Capture output
|
|
var buf bytes.Buffer
|
|
oldStdout := os.Stdout
|
|
r, w, _ := os.Pipe()
|
|
os.Stdout = w
|
|
|
|
// Reset command state
|
|
jsonOutput = true
|
|
defer func() { jsonOutput = false }()
|
|
rootCmd.SetArgs([]string{"show", issue1.ID, "--json"})
|
|
|
|
err := rootCmd.Execute()
|
|
|
|
// Restore stdout and read output
|
|
w.Close()
|
|
buf.ReadFrom(r)
|
|
os.Stdout = oldStdout
|
|
output := buf.String()
|
|
|
|
if err != nil {
|
|
t.Fatalf("show command failed: %v", err)
|
|
}
|
|
|
|
// Parse JSON output
|
|
var result []map[string]interface{}
|
|
if err := json.Unmarshal([]byte(output), &result); err != nil {
|
|
t.Fatalf("Failed to parse JSON output: %v\nOutput: %s", err, output)
|
|
}
|
|
|
|
if len(result) != 1 {
|
|
t.Fatalf("Expected 1 issue in result, got %d", len(result))
|
|
}
|
|
|
|
if result[0]["id"] != issue1.ID {
|
|
t.Errorf("Expected issue ID %s, got %v", issue1.ID, result[0]["id"])
|
|
}
|
|
if result[0]["title"] != issue1.Title {
|
|
t.Errorf("Expected title %s, got %v", issue1.Title, result[0]["title"])
|
|
}
|
|
|
|
// Verify labels are included
|
|
labels, ok := result[0]["labels"].([]interface{})
|
|
if !ok || len(labels) == 0 {
|
|
t.Error("Expected labels in JSON output")
|
|
}
|
|
})
|
|
|
|
t.Run("show multiple issues", func(t *testing.T) {
|
|
// Capture output
|
|
var buf bytes.Buffer
|
|
oldStdout := os.Stdout
|
|
r, w, _ := os.Pipe()
|
|
os.Stdout = w
|
|
|
|
// Reset command state
|
|
jsonOutput = true
|
|
defer func() { jsonOutput = false }()
|
|
rootCmd.SetArgs([]string{"show", issue1.ID, issue2.ID, "--json"})
|
|
|
|
err := rootCmd.Execute()
|
|
|
|
// Restore stdout and read output
|
|
w.Close()
|
|
buf.ReadFrom(r)
|
|
os.Stdout = oldStdout
|
|
output := buf.String()
|
|
|
|
if err != nil {
|
|
t.Fatalf("show command failed: %v", err)
|
|
}
|
|
|
|
// Parse JSON output
|
|
var result []map[string]interface{}
|
|
if err := json.Unmarshal([]byte(output), &result); err != nil {
|
|
t.Fatalf("Failed to parse JSON output: %v", err)
|
|
}
|
|
|
|
if len(result) != 2 {
|
|
t.Fatalf("Expected 2 issues in result, got %d", len(result))
|
|
}
|
|
})
|
|
|
|
t.Run("show with dependencies", func(t *testing.T) {
|
|
// Capture output
|
|
var buf bytes.Buffer
|
|
oldStdout := os.Stdout
|
|
r, w, _ := os.Pipe()
|
|
os.Stdout = w
|
|
|
|
// Reset command state
|
|
jsonOutput = true
|
|
defer func() { jsonOutput = false }()
|
|
rootCmd.SetArgs([]string{"show", issue2.ID, "--json"})
|
|
|
|
err := rootCmd.Execute()
|
|
|
|
// Restore stdout and read output
|
|
w.Close()
|
|
buf.ReadFrom(r)
|
|
os.Stdout = oldStdout
|
|
output := buf.String()
|
|
|
|
if err != nil {
|
|
t.Fatalf("show command failed: %v", err)
|
|
}
|
|
|
|
// Parse JSON output
|
|
var result []map[string]interface{}
|
|
if err := json.Unmarshal([]byte(output), &result); err != nil {
|
|
t.Fatalf("Failed to parse JSON output: %v", err)
|
|
}
|
|
|
|
// Verify dependencies are included
|
|
deps, ok := result[0]["dependencies"].([]interface{})
|
|
if !ok || len(deps) == 0 {
|
|
t.Error("Expected dependencies in JSON output")
|
|
}
|
|
})
|
|
|
|
t.Run("show with compaction", func(t *testing.T) {
|
|
// Create a compacted issue
|
|
now := time.Now()
|
|
compactedIssue := &types.Issue{
|
|
Title: "Compacted Issue",
|
|
Description: "Original long description",
|
|
Priority: 1,
|
|
IssueType: types.TypeTask,
|
|
Status: types.StatusClosed,
|
|
ClosedAt: &now,
|
|
CompactionLevel: 1,
|
|
OriginalSize: 100,
|
|
CompactedAt: &now,
|
|
}
|
|
if err := testStore.CreateIssue(ctx, compactedIssue, "test-user"); err != nil {
|
|
t.Fatalf("Failed to create compacted issue: %v", err)
|
|
}
|
|
|
|
// Capture output
|
|
var buf bytes.Buffer
|
|
oldStdout := os.Stdout
|
|
r, w, _ := os.Pipe()
|
|
os.Stdout = w
|
|
|
|
// Reset command state
|
|
rootCmd.SetArgs([]string{"show", compactedIssue.ID})
|
|
showCmd.Flags().Set("json", "false")
|
|
|
|
err := rootCmd.Execute()
|
|
|
|
// Restore stdout and read output
|
|
w.Close()
|
|
buf.ReadFrom(r)
|
|
os.Stdout = oldStdout
|
|
output := buf.String()
|
|
|
|
if err != nil {
|
|
t.Fatalf("show command failed: %v", err)
|
|
}
|
|
|
|
// Verify compaction indicators are shown
|
|
// Note: Case-insensitive check since output might have "Compacted" (capitalized)
|
|
outputLower := strings.ToLower(output)
|
|
if !strings.Contains(outputLower, "compacted") {
|
|
t.Errorf("Output should indicate issue is compacted, got: %s", output)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestUpdateCommand(t *testing.T) {
|
|
// Save original global state
|
|
origStore := store
|
|
origDBPath := dbPath
|
|
origDaemonClient := daemonClient
|
|
defer func() {
|
|
store = origStore
|
|
dbPath = origDBPath
|
|
daemonClient = origDaemonClient
|
|
}()
|
|
|
|
tmpDir := t.TempDir()
|
|
testDB := filepath.Join(tmpDir, "test.db")
|
|
|
|
// Create test store and set it globally
|
|
testStore := newTestStore(t, testDB)
|
|
defer testStore.Close()
|
|
|
|
store = testStore
|
|
dbPath = testDB
|
|
daemonClient = nil // Force direct mode
|
|
|
|
// Ensure BEADS_NO_DAEMON is set
|
|
os.Setenv("BEADS_NO_DAEMON", "1")
|
|
defer os.Unsetenv("BEADS_NO_DAEMON")
|
|
|
|
ctx := context.Background()
|
|
|
|
// Create test issue
|
|
issue := &types.Issue{
|
|
Title: "Test Issue",
|
|
Description: "Original description",
|
|
Priority: 2,
|
|
IssueType: types.TypeTask,
|
|
Status: types.StatusOpen,
|
|
}
|
|
if err := testStore.CreateIssue(ctx, issue, "test-user"); err != nil {
|
|
t.Fatalf("Failed to create issue: %v", err)
|
|
}
|
|
|
|
t.Run("update status", func(t *testing.T) {
|
|
// Reset command state
|
|
rootCmd.SetArgs([]string{"update", issue.ID, "--status", "in_progress"})
|
|
|
|
err := rootCmd.Execute()
|
|
if err != nil {
|
|
t.Fatalf("update command failed: %v", err)
|
|
}
|
|
|
|
// Verify issue was updated
|
|
updated, err := testStore.GetIssue(ctx, issue.ID)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get updated issue: %v", err)
|
|
}
|
|
if updated.Status != types.StatusInProgress {
|
|
t.Errorf("Expected status %s, got %s", types.StatusInProgress, updated.Status)
|
|
}
|
|
})
|
|
|
|
t.Run("update priority", func(t *testing.T) {
|
|
// Reset command state
|
|
rootCmd.SetArgs([]string{"update", issue.ID, "--priority", "0"})
|
|
|
|
err := rootCmd.Execute()
|
|
if err != nil {
|
|
t.Fatalf("update command failed: %v", err)
|
|
}
|
|
|
|
// Verify issue was updated
|
|
updated, err := testStore.GetIssue(ctx, issue.ID)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get updated issue: %v", err)
|
|
}
|
|
if updated.Priority != 0 {
|
|
t.Errorf("Expected priority 0, got %d", updated.Priority)
|
|
}
|
|
})
|
|
|
|
t.Run("update title", func(t *testing.T) {
|
|
newTitle := "Updated Test Issue"
|
|
|
|
// Reset command state
|
|
rootCmd.SetArgs([]string{"update", issue.ID, "--title", newTitle})
|
|
|
|
err := rootCmd.Execute()
|
|
if err != nil {
|
|
t.Fatalf("update command failed: %v", err)
|
|
}
|
|
|
|
// Verify issue was updated
|
|
updated, err := testStore.GetIssue(ctx, issue.ID)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get updated issue: %v", err)
|
|
}
|
|
if updated.Title != newTitle {
|
|
t.Errorf("Expected title %s, got %s", newTitle, updated.Title)
|
|
}
|
|
})
|
|
|
|
t.Run("update assignee", func(t *testing.T) {
|
|
// Reset command state
|
|
rootCmd.SetArgs([]string{"update", issue.ID, "--assignee", "bob"})
|
|
|
|
err := rootCmd.Execute()
|
|
if err != nil {
|
|
t.Fatalf("update command failed: %v", err)
|
|
}
|
|
|
|
// Verify issue was updated
|
|
updated, err := testStore.GetIssue(ctx, issue.ID)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get updated issue: %v", err)
|
|
}
|
|
if updated.Assignee != "bob" {
|
|
t.Errorf("Expected assignee bob, got %s", updated.Assignee)
|
|
}
|
|
})
|
|
|
|
t.Run("update description", func(t *testing.T) {
|
|
newDesc := "New description text"
|
|
|
|
// Reset command state
|
|
rootCmd.SetArgs([]string{"update", issue.ID, "--description", newDesc})
|
|
|
|
err := rootCmd.Execute()
|
|
if err != nil {
|
|
t.Fatalf("update command failed: %v", err)
|
|
}
|
|
|
|
// Verify issue was updated
|
|
updated, err := testStore.GetIssue(ctx, issue.ID)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get updated issue: %v", err)
|
|
}
|
|
if updated.Description != newDesc {
|
|
t.Errorf("Expected description %s, got %s", newDesc, updated.Description)
|
|
}
|
|
})
|
|
|
|
t.Run("update multiple fields", func(t *testing.T) {
|
|
// Reset command state
|
|
rootCmd.SetArgs([]string{"update", issue.ID,
|
|
"--status", "closed",
|
|
"--priority", "1",
|
|
"--assignee", "charlie"})
|
|
|
|
err := rootCmd.Execute()
|
|
if err != nil {
|
|
t.Fatalf("update command failed: %v", err)
|
|
}
|
|
|
|
// Verify issue was updated
|
|
updated, err := testStore.GetIssue(ctx, issue.ID)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get updated issue: %v", err)
|
|
}
|
|
if updated.Status != types.StatusClosed {
|
|
t.Errorf("Expected status %s, got %s", types.StatusClosed, updated.Status)
|
|
}
|
|
if updated.Priority != 1 {
|
|
t.Errorf("Expected priority 1, got %d", updated.Priority)
|
|
}
|
|
if updated.Assignee != "charlie" {
|
|
t.Errorf("Expected assignee charlie, got %s", updated.Assignee)
|
|
}
|
|
})
|
|
|
|
t.Run("update multiple issues", func(t *testing.T) {
|
|
// Create second test issue
|
|
issue2 := &types.Issue{
|
|
Title: "Second Test Issue",
|
|
Priority: 2,
|
|
IssueType: types.TypeBug,
|
|
Status: types.StatusOpen,
|
|
}
|
|
if err := testStore.CreateIssue(ctx, issue2, "test-user"); err != nil {
|
|
t.Fatalf("Failed to create issue2: %v", err)
|
|
}
|
|
|
|
// Reset both issues to open
|
|
testStore.UpdateIssue(ctx, issue.ID, map[string]interface{}{"status": types.StatusOpen}, "test-user")
|
|
|
|
// Reset command state
|
|
rootCmd.SetArgs([]string{"update", issue.ID, issue2.ID, "--status", "in_progress"})
|
|
|
|
err := rootCmd.Execute()
|
|
if err != nil {
|
|
t.Fatalf("update command failed: %v", err)
|
|
}
|
|
|
|
// Verify both issues were updated
|
|
updated1, _ := testStore.GetIssue(ctx, issue.ID)
|
|
updated2, _ := testStore.GetIssue(ctx, issue2.ID)
|
|
|
|
if updated1.Status != types.StatusInProgress {
|
|
t.Errorf("Expected issue1 status %s, got %s", types.StatusInProgress, updated1.Status)
|
|
}
|
|
if updated2.Status != types.StatusInProgress {
|
|
t.Errorf("Expected issue2 status %s, got %s", types.StatusInProgress, updated2.Status)
|
|
}
|
|
})
|
|
|
|
t.Run("update with JSON output", func(t *testing.T) {
|
|
// Capture output
|
|
var buf bytes.Buffer
|
|
oldStdout := os.Stdout
|
|
r, w, _ := os.Pipe()
|
|
os.Stdout = w
|
|
|
|
// Reset command state
|
|
jsonOutput = true
|
|
defer func() { jsonOutput = false }()
|
|
rootCmd.SetArgs([]string{"update", issue.ID, "--priority", "3", "--json"})
|
|
|
|
err := rootCmd.Execute()
|
|
|
|
// Restore stdout and read output
|
|
w.Close()
|
|
buf.ReadFrom(r)
|
|
os.Stdout = oldStdout
|
|
output := buf.String()
|
|
|
|
if err != nil {
|
|
t.Fatalf("update command failed: %v", err)
|
|
}
|
|
|
|
// Parse JSON output
|
|
var result []map[string]interface{}
|
|
if err := json.Unmarshal([]byte(output), &result); err != nil {
|
|
t.Fatalf("Failed to parse JSON output: %v", err)
|
|
}
|
|
|
|
if len(result) != 1 {
|
|
t.Fatalf("Expected 1 issue in result, got %d", len(result))
|
|
}
|
|
|
|
// Verify priority was updated
|
|
priority := int(result[0]["priority"].(float64))
|
|
if priority != 3 {
|
|
t.Errorf("Expected priority 3, got %d", priority)
|
|
}
|
|
})
|
|
|
|
t.Run("update design notes", func(t *testing.T) {
|
|
designNotes := "New design approach"
|
|
|
|
// Reset command state
|
|
rootCmd.SetArgs([]string{"update", issue.ID, "--design", designNotes})
|
|
|
|
err := rootCmd.Execute()
|
|
if err != nil {
|
|
t.Fatalf("update command failed: %v", err)
|
|
}
|
|
|
|
// Verify issue was updated
|
|
updated, err := testStore.GetIssue(ctx, issue.ID)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get updated issue: %v", err)
|
|
}
|
|
if updated.Design != designNotes {
|
|
t.Errorf("Expected design %s, got %s", designNotes, updated.Design)
|
|
}
|
|
})
|
|
|
|
t.Run("update notes", func(t *testing.T) {
|
|
notes := "Additional notes here"
|
|
|
|
// Reset command state
|
|
rootCmd.SetArgs([]string{"update", issue.ID, "--notes", notes})
|
|
|
|
err := rootCmd.Execute()
|
|
if err != nil {
|
|
t.Fatalf("update command failed: %v", err)
|
|
}
|
|
|
|
// Verify issue was updated
|
|
updated, err := testStore.GetIssue(ctx, issue.ID)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get updated issue: %v", err)
|
|
}
|
|
if updated.Notes != notes {
|
|
t.Errorf("Expected notes %s, got %s", notes, updated.Notes)
|
|
}
|
|
})
|
|
|
|
t.Run("update acceptance criteria", func(t *testing.T) {
|
|
acceptance := "Must pass all tests"
|
|
|
|
// Reset command state
|
|
rootCmd.SetArgs([]string{"update", issue.ID, "--acceptance", acceptance})
|
|
|
|
err := rootCmd.Execute()
|
|
if err != nil {
|
|
t.Fatalf("update command failed: %v", err)
|
|
}
|
|
|
|
// Verify issue was updated
|
|
updated, err := testStore.GetIssue(ctx, issue.ID)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get updated issue: %v", err)
|
|
}
|
|
if updated.AcceptanceCriteria != acceptance {
|
|
t.Errorf("Expected acceptance criteria %s, got %s", acceptance, updated.AcceptanceCriteria)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestEditCommand(t *testing.T) {
|
|
// Note: The edit command opens an interactive editor and is difficult to test
|
|
// in an automated fashion without complex mocking. We test what we can:
|
|
// - That the command exists and can be invoked
|
|
// - That it properly validates input (issue ID required)
|
|
|
|
// Save original global state
|
|
origStore := store
|
|
origDBPath := dbPath
|
|
origDaemonClient := daemonClient
|
|
defer func() {
|
|
store = origStore
|
|
dbPath = origDBPath
|
|
daemonClient = origDaemonClient
|
|
}()
|
|
|
|
tmpDir := t.TempDir()
|
|
testDB := filepath.Join(tmpDir, "test.db")
|
|
|
|
// Create test store and set it globally
|
|
testStore := newTestStore(t, testDB)
|
|
defer testStore.Close()
|
|
|
|
store = testStore
|
|
dbPath = testDB
|
|
daemonClient = nil // Force direct mode
|
|
|
|
// Ensure BEADS_NO_DAEMON is set
|
|
os.Setenv("BEADS_NO_DAEMON", "1")
|
|
defer os.Unsetenv("BEADS_NO_DAEMON")
|
|
|
|
ctx := context.Background()
|
|
|
|
// Create test issue
|
|
issue := &types.Issue{
|
|
Title: "Test Issue",
|
|
Description: "Original description",
|
|
Priority: 1,
|
|
IssueType: types.TypeTask,
|
|
Status: types.StatusOpen,
|
|
}
|
|
if err := testStore.CreateIssue(ctx, issue, "test-user"); err != nil {
|
|
t.Fatalf("Failed to create issue: %v", err)
|
|
}
|
|
|
|
t.Run("edit command validation", func(t *testing.T) {
|
|
// Test that edit command requires an issue ID argument
|
|
rootCmd.SetArgs([]string{"edit"})
|
|
|
|
// Capture stderr
|
|
oldStderr := os.Stderr
|
|
r, w, _ := os.Pipe()
|
|
os.Stderr = w
|
|
|
|
err := rootCmd.Execute()
|
|
|
|
// Restore stderr
|
|
w.Close()
|
|
var buf bytes.Buffer
|
|
buf.ReadFrom(r)
|
|
os.Stderr = oldStderr
|
|
|
|
// Should fail with argument validation error
|
|
if err == nil {
|
|
t.Error("Expected error when no issue ID provided to edit command")
|
|
}
|
|
})
|
|
|
|
// Testing the actual interactive editor flow would require mocking the editor
|
|
// process, which is complex and fragile. Manual testing is more appropriate.
|
|
}
|
|
|
|
func TestCloseCommand(t *testing.T) {
|
|
// Save original global state
|
|
origStore := store
|
|
origDBPath := dbPath
|
|
origDaemonClient := daemonClient
|
|
defer func() {
|
|
store = origStore
|
|
dbPath = origDBPath
|
|
daemonClient = origDaemonClient
|
|
}()
|
|
|
|
tmpDir := t.TempDir()
|
|
testDB := filepath.Join(tmpDir, "test.db")
|
|
|
|
// Create test store and set it globally
|
|
testStore := newTestStore(t, testDB)
|
|
defer testStore.Close()
|
|
|
|
store = testStore
|
|
dbPath = testDB
|
|
daemonClient = nil // Force direct mode
|
|
|
|
// Ensure BEADS_NO_DAEMON is set
|
|
os.Setenv("BEADS_NO_DAEMON", "1")
|
|
defer os.Unsetenv("BEADS_NO_DAEMON")
|
|
|
|
ctx := context.Background()
|
|
|
|
t.Run("close single issue", func(t *testing.T) {
|
|
// Create test issue
|
|
issue := &types.Issue{
|
|
Title: "Test Issue",
|
|
Priority: 1,
|
|
IssueType: types.TypeTask,
|
|
Status: types.StatusOpen,
|
|
}
|
|
if err := testStore.CreateIssue(ctx, issue, "test-user"); err != nil {
|
|
t.Fatalf("Failed to create issue: %v", err)
|
|
}
|
|
|
|
// Reset command state
|
|
rootCmd.SetArgs([]string{"close", issue.ID, "--reason", "Completed"})
|
|
|
|
err := rootCmd.Execute()
|
|
if err != nil {
|
|
t.Fatalf("close command failed: %v", err)
|
|
}
|
|
|
|
// Verify issue was closed
|
|
closed, err := testStore.GetIssue(ctx, issue.ID)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get closed issue: %v", err)
|
|
}
|
|
if closed.Status != types.StatusClosed {
|
|
t.Errorf("Expected status %s, got %s", types.StatusClosed, closed.Status)
|
|
}
|
|
if closed.ClosedAt == nil {
|
|
t.Error("Expected ClosedAt to be set")
|
|
}
|
|
})
|
|
|
|
t.Run("close multiple issues", func(t *testing.T) {
|
|
// Create test issues
|
|
issue1 := &types.Issue{
|
|
Title: "First Issue",
|
|
Priority: 1,
|
|
IssueType: types.TypeTask,
|
|
Status: types.StatusOpen,
|
|
}
|
|
if err := testStore.CreateIssue(ctx, issue1, "test-user"); err != nil {
|
|
t.Fatalf("Failed to create issue1: %v", err)
|
|
}
|
|
|
|
issue2 := &types.Issue{
|
|
Title: "Second Issue",
|
|
Priority: 1,
|
|
IssueType: types.TypeTask,
|
|
Status: types.StatusOpen,
|
|
}
|
|
if err := testStore.CreateIssue(ctx, issue2, "test-user"); err != nil {
|
|
t.Fatalf("Failed to create issue2: %v", err)
|
|
}
|
|
|
|
// Reset command state
|
|
rootCmd.SetArgs([]string{"close", issue1.ID, issue2.ID, "--reason", "Done"})
|
|
|
|
err := rootCmd.Execute()
|
|
if err != nil {
|
|
t.Fatalf("close command failed: %v", err)
|
|
}
|
|
|
|
// Verify both issues were closed
|
|
closed1, _ := testStore.GetIssue(ctx, issue1.ID)
|
|
closed2, _ := testStore.GetIssue(ctx, issue2.ID)
|
|
|
|
if closed1.Status != types.StatusClosed {
|
|
t.Errorf("Expected issue1 status %s, got %s", types.StatusClosed, closed1.Status)
|
|
}
|
|
if closed2.Status != types.StatusClosed {
|
|
t.Errorf("Expected issue2 status %s, got %s", types.StatusClosed, closed2.Status)
|
|
}
|
|
})
|
|
|
|
t.Run("close with JSON output", func(t *testing.T) {
|
|
// Create test issue
|
|
issue := &types.Issue{
|
|
Title: "JSON Test Issue",
|
|
Priority: 1,
|
|
IssueType: types.TypeTask,
|
|
Status: types.StatusOpen,
|
|
}
|
|
if err := testStore.CreateIssue(ctx, issue, "test-user"); err != nil {
|
|
t.Fatalf("Failed to create issue: %v", err)
|
|
}
|
|
|
|
// Capture output
|
|
var buf bytes.Buffer
|
|
oldStdout := os.Stdout
|
|
r, w, _ := os.Pipe()
|
|
os.Stdout = w
|
|
|
|
// Reset command state
|
|
jsonOutput = true
|
|
defer func() { jsonOutput = false }()
|
|
rootCmd.SetArgs([]string{"close", issue.ID, "--reason", "Fixed", "--json"})
|
|
|
|
err := rootCmd.Execute()
|
|
|
|
// Restore stdout and read output
|
|
w.Close()
|
|
buf.ReadFrom(r)
|
|
os.Stdout = oldStdout
|
|
output := buf.String()
|
|
|
|
if err != nil {
|
|
t.Fatalf("close command failed: %v", err)
|
|
}
|
|
|
|
// Parse JSON output
|
|
var result []map[string]interface{}
|
|
if err := json.Unmarshal([]byte(output), &result); err != nil {
|
|
t.Fatalf("Failed to parse JSON output: %v", err)
|
|
}
|
|
|
|
if len(result) != 1 {
|
|
t.Fatalf("Expected 1 issue in result, got %d", len(result))
|
|
}
|
|
|
|
// Verify issue is closed
|
|
if result[0]["status"] != string(types.StatusClosed) {
|
|
t.Errorf("Expected status %s, got %v", types.StatusClosed, result[0]["status"])
|
|
}
|
|
})
|
|
|
|
t.Run("close without reason", func(t *testing.T) {
|
|
// Create test issue
|
|
issue := &types.Issue{
|
|
Title: "No Reason Issue",
|
|
Priority: 1,
|
|
IssueType: types.TypeTask,
|
|
Status: types.StatusOpen,
|
|
}
|
|
if err := testStore.CreateIssue(ctx, issue, "test-user"); err != nil {
|
|
t.Fatalf("Failed to create issue: %v", err)
|
|
}
|
|
|
|
// Reset command state (no --reason flag)
|
|
rootCmd.SetArgs([]string{"close", issue.ID})
|
|
|
|
err := rootCmd.Execute()
|
|
if err != nil {
|
|
t.Fatalf("close command failed: %v", err)
|
|
}
|
|
|
|
// Verify issue was closed (should use default reason "Closed")
|
|
closed, err := testStore.GetIssue(ctx, issue.ID)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get closed issue: %v", err)
|
|
}
|
|
if closed.Status != types.StatusClosed {
|
|
t.Errorf("Expected status %s, got %s", types.StatusClosed, closed.Status)
|
|
}
|
|
})
|
|
}
|