Files
beads/cmd/bd/init_test.go
2025-11-09 14:13:48 -08:00

882 lines
25 KiB
Go

package main
import (
"bytes"
"context"
"os"
"path/filepath"
"strings"
"testing"
)
func TestInitCommand(t *testing.T) {
tests := []struct {
name string
prefix string
quiet bool
wantOutputText string
wantNoOutput bool
}{
{
name: "init with default prefix",
prefix: "",
quiet: false,
wantOutputText: "bd initialized successfully",
},
{
name: "init with custom prefix",
prefix: "myproject",
quiet: false,
wantOutputText: "myproject-1, myproject-2",
},
{
name: "init with quiet flag",
prefix: "test",
quiet: true,
wantNoOutput: true,
},
{
name: "init with prefix ending in hyphen",
prefix: "test-",
quiet: false,
wantOutputText: "test-1, test-2",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Reset global state
origDBPath := dbPath
defer func() { dbPath = origDBPath }()
dbPath = ""
// Reset Cobra command state
rootCmd.SetArgs([]string{})
initCmd.Flags().Set("prefix", "")
initCmd.Flags().Set("quiet", "false")
tmpDir := t.TempDir()
originalWd, err := os.Getwd()
if err != nil {
t.Fatalf("Failed to get working directory: %v", err)
}
defer os.Chdir(originalWd)
if err := os.Chdir(tmpDir); err != nil {
t.Fatalf("Failed to change to temp directory: %v", err)
}
// Capture output
var buf bytes.Buffer
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
defer func() {
os.Stdout = oldStdout
}()
// Build command arguments
args := []string{"init"}
if tt.prefix != "" {
args = append(args, "--prefix", tt.prefix)
}
if tt.quiet {
args = append(args, "--quiet")
}
rootCmd.SetArgs(args)
// Run command
err = rootCmd.Execute()
// Restore stdout and read output
w.Close()
buf.ReadFrom(r)
os.Stdout = oldStdout
output := buf.String()
if err != nil {
t.Fatalf("init command failed: %v", err)
}
// Check output
if tt.wantNoOutput {
if output != "" {
t.Errorf("Expected no output with --quiet, got: %s", output)
}
} else if tt.wantOutputText != "" {
if !strings.Contains(output, tt.wantOutputText) {
t.Errorf("Expected output to contain %q, got: %s", tt.wantOutputText, output)
}
}
// Verify .beads directory was created
beadsDir := filepath.Join(tmpDir, ".beads")
if _, err := os.Stat(beadsDir); os.IsNotExist(err) {
t.Error(".beads directory was not created")
}
// Verify .gitignore was created with proper content
gitignorePath := filepath.Join(beadsDir, ".gitignore")
gitignoreContent, err := os.ReadFile(gitignorePath)
if err != nil {
t.Errorf(".gitignore file was not created: %v", err)
} else {
// Check for essential patterns
gitignoreStr := string(gitignoreContent)
expectedPatterns := []string{
"*.db",
"*.db?*",
"*.db-journal",
"*.db-wal",
"*.db-shm",
"daemon.log",
"daemon.pid",
"bd.sock",
"beads.base.jsonl",
"beads.left.jsonl",
"beads.right.jsonl",
"!issues.jsonl",
}
for _, pattern := range expectedPatterns {
if !strings.Contains(gitignoreStr, pattern) {
t.Errorf(".gitignore missing expected pattern: %s", pattern)
}
}
}
// Verify database was created (always beads.db now)
dbPath := filepath.Join(beadsDir, "beads.db")
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
t.Errorf("Database file was not created at %s", dbPath)
}
// Verify database has correct prefix
// Note: This database was already created by init command, just open it
store, err := openExistingTestDB(t, dbPath)
if err != nil {
t.Fatalf("Failed to open database: %v", err)
}
defer store.Close()
ctx := context.Background()
prefix, err := store.GetConfig(ctx, "issue_prefix")
if err != nil {
t.Fatalf("Failed to get issue prefix from database: %v", err)
}
expectedPrefix := tt.prefix
if expectedPrefix == "" {
expectedPrefix = filepath.Base(tmpDir)
} else {
expectedPrefix = strings.TrimRight(expectedPrefix, "-")
}
if prefix != expectedPrefix {
t.Errorf("Expected prefix %q, got %q", expectedPrefix, prefix)
}
// Verify version metadata was set
version, err := store.GetMetadata(ctx, "bd_version")
if err != nil {
t.Errorf("Failed to get bd_version metadata: %v", err)
}
if version == "" {
t.Error("bd_version metadata was not set")
}
})
}
}
// Note: Error case testing is omitted because the init command calls os.Exit()
// on errors, which makes it difficult to test in a unit test context.
func TestInitAlreadyInitialized(t *testing.T) {
// Reset global state
origDBPath := dbPath
defer func() { dbPath = origDBPath }()
dbPath = ""
tmpDir := t.TempDir()
originalWd, err := os.Getwd()
if err != nil {
t.Fatalf("Failed to get working directory: %v", err)
}
defer os.Chdir(originalWd)
if err := os.Chdir(tmpDir); err != nil {
t.Fatalf("Failed to change to temp directory: %v", err)
}
// Initialize once
rootCmd.SetArgs([]string{"init", "--prefix", "test", "--quiet"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("First init failed: %v", err)
}
// Initialize again with same prefix - should succeed (overwrites)
rootCmd.SetArgs([]string{"init", "--prefix", "test", "--quiet"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("Second init failed: %v", err)
}
// Verify database still works (always beads.db now)
dbPath := filepath.Join(tmpDir, ".beads", "beads.db")
store, err := openExistingTestDB(t, dbPath)
if err != nil {
t.Fatalf("Failed to open database: %v", err)
}
defer store.Close()
ctx := context.Background()
prefix, err := store.GetConfig(ctx, "issue_prefix")
if err != nil {
t.Fatalf("Failed to get prefix after re-init: %v", err)
}
if prefix != "test" {
t.Errorf("Expected prefix 'test', got %q", prefix)
}
}
func TestInitWithCustomDBPath(t *testing.T) {
// Save original state
origDBPath := dbPath
defer func() { dbPath = origDBPath }()
tmpDir := t.TempDir()
customDBDir := filepath.Join(tmpDir, "custom", "location")
// Change to a different directory to ensure --db flag is actually used
workDir := filepath.Join(tmpDir, "workdir")
if err := os.MkdirAll(workDir, 0750); err != nil {
t.Fatalf("Failed to create work directory: %v", err)
}
originalWd, err := os.Getwd()
if err != nil {
t.Fatalf("Failed to get working directory: %v", err)
}
defer os.Chdir(originalWd)
if err := os.Chdir(workDir); err != nil {
t.Fatalf("Failed to change to work directory: %v", err)
}
customDBPath := filepath.Join(customDBDir, "test.db")
// Test with BEADS_DB environment variable (replacing --db flag test)
t.Run("init with BEADS_DB pointing to custom path", func(t *testing.T) {
dbPath = "" // Reset global
os.Setenv("BEADS_DB", customDBPath)
defer os.Unsetenv("BEADS_DB")
rootCmd.SetArgs([]string{"init", "--prefix", "custom", "--quiet"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("Init with BEADS_DB failed: %v", err)
}
// Verify database was created at custom location
if _, err := os.Stat(customDBPath); os.IsNotExist(err) {
t.Errorf("Database was not created at custom path %s", customDBPath)
}
// Verify database works
store, err := openExistingTestDB(t, customDBPath)
if err != nil {
t.Fatalf("Failed to open database: %v", err)
}
defer store.Close()
ctx := context.Background()
prefix, err := store.GetConfig(ctx, "issue_prefix")
if err != nil {
t.Fatalf("Failed to get prefix: %v", err)
}
if prefix != "custom" {
t.Errorf("Expected prefix 'custom', got %q", prefix)
}
// Verify .beads/ directory was NOT created in work directory
if _, err := os.Stat(filepath.Join(workDir, ".beads")); err == nil {
t.Error(".beads/ directory should not be created when using BEADS_DB env var")
}
})
// Test with BEADS_DB env var
t.Run("init with BEADS_DB env var", func(t *testing.T) {
dbPath = "" // Reset global
envDBPath := filepath.Join(tmpDir, "env", "location", "env.db")
os.Setenv("BEADS_DB", envDBPath)
defer os.Unsetenv("BEADS_DB")
rootCmd.SetArgs([]string{"init", "--prefix", "envtest", "--quiet"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("Init with BEADS_DB failed: %v", err)
}
// Verify database was created at env location
if _, err := os.Stat(envDBPath); os.IsNotExist(err) {
t.Errorf("Database was not created at BEADS_DB path %s", envDBPath)
}
// Verify database works
store, err := openExistingTestDB(t, envDBPath)
if err != nil {
t.Fatalf("Failed to open database: %v", err)
}
defer store.Close()
ctx := context.Background()
prefix, err := store.GetConfig(ctx, "issue_prefix")
if err != nil {
t.Fatalf("Failed to get prefix: %v", err)
}
if prefix != "envtest" {
t.Errorf("Expected prefix 'envtest', got %q", prefix)
}
})
// Test that BEADS_DB path containing ".beads" doesn't create CWD/.beads
t.Run("init with BEADS_DB path containing .beads", func(t *testing.T) {
dbPath = "" // Reset global
// Path contains ".beads" but is outside work directory
customPath := filepath.Join(tmpDir, "storage", ".beads-backup", "test.db")
os.Setenv("BEADS_DB", customPath)
defer os.Unsetenv("BEADS_DB")
rootCmd.SetArgs([]string{"init", "--prefix", "beadstest", "--quiet"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("Init with custom .beads path failed: %v", err)
}
// Verify database was created at custom location
if _, err := os.Stat(customPath); os.IsNotExist(err) {
t.Errorf("Database was not created at custom path %s", customPath)
}
// Verify .beads/ directory was NOT created in work directory
if _, err := os.Stat(filepath.Join(workDir, ".beads")); err == nil {
t.Error(".beads/ directory should not be created in CWD when BEADS_DB path contains .beads")
}
})
// Test with multiple BEADS_DB variations
t.Run("BEADS_DB with subdirectories", func(t *testing.T) {
dbPath = "" // Reset global
envPath := filepath.Join(tmpDir, "env", "subdirs", "test.db")
os.Setenv("BEADS_DB", envPath)
defer os.Unsetenv("BEADS_DB")
rootCmd.SetArgs([]string{"init", "--prefix", "envtest2", "--quiet"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("Init with BEADS_DB subdirs failed: %v", err)
}
// Verify database was created at env location
if _, err := os.Stat(envPath); os.IsNotExist(err) {
t.Errorf("Database was not created at BEADS_DB path %s", envPath)
}
// Verify .beads/ directory was NOT created in work directory
if _, err := os.Stat(filepath.Join(workDir, ".beads")); err == nil {
t.Error(".beads/ directory should not be created in CWD when BEADS_DB is set")
}
})
}
func TestInitNoDbMode(t *testing.T) {
// Reset global state
origDBPath := dbPath
origNoDb := noDb
defer func() {
dbPath = origDBPath
noDb = origNoDb
}()
dbPath = ""
noDb = false
tmpDir := t.TempDir()
originalWd, err := os.Getwd()
if err != nil {
t.Fatalf("Failed to get working directory: %v", err)
}
defer os.Chdir(originalWd)
if err := os.Chdir(tmpDir); err != nil {
t.Fatalf("Failed to change to temp directory: %v", err)
}
// Initialize with --no-db flag
rootCmd.SetArgs([]string{"init", "--no-db", "--no-daemon", "--prefix", "test", "--quiet"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("Init with --no-db failed: %v", err)
}
// Verify issues.jsonl was created
jsonlPath := filepath.Join(tmpDir, ".beads", "issues.jsonl")
if _, err := os.Stat(jsonlPath); os.IsNotExist(err) {
t.Error("issues.jsonl was not created in --no-db mode")
}
// Verify config.yaml was created with no-db: true
configPath := filepath.Join(tmpDir, ".beads", "config.yaml")
configContent, err := os.ReadFile(configPath)
if err != nil {
t.Fatalf("Failed to read config.yaml: %v", err)
}
configStr := string(configContent)
if !strings.Contains(configStr, "no-db: true") {
t.Error("config.yaml should contain 'no-db: true' in --no-db mode")
}
// Verify subsequent command works without --no-db flag
rootCmd.SetArgs([]string{"create", "test issue", "--json"})
// Capture output to verify it worked
var buf bytes.Buffer
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
err = rootCmd.Execute()
// Restore stdout and read output
w.Close()
buf.ReadFrom(r)
os.Stdout = oldStdout
if err != nil {
t.Fatalf("create command failed in no-db mode: %v", err)
}
// Verify issue was written to JSONL
jsonlContent, err := os.ReadFile(jsonlPath)
if err != nil {
t.Fatalf("Failed to read issues.jsonl: %v", err)
}
if len(jsonlContent) == 0 {
t.Error("issues.jsonl should not be empty after creating issue")
}
if !strings.Contains(string(jsonlContent), "test issue") {
t.Error("issues.jsonl should contain the created issue")
}
// Verify no SQLite database was created
dbPath := filepath.Join(tmpDir, ".beads", "beads.db")
if _, err := os.Stat(dbPath); err == nil {
t.Error("SQLite database should not be created in --no-db mode")
}
}
func TestInitMergeDriverAutoConfiguration(t *testing.T) {
t.Run("merge driver auto-configured during init", func(t *testing.T) {
// Reset global state
origDBPath := dbPath
defer func() { dbPath = origDBPath }()
dbPath = ""
tmpDir := t.TempDir()
originalWd, err := os.Getwd()
if err != nil {
t.Fatalf("Failed to get working directory: %v", err)
}
defer os.Chdir(originalWd)
if err := os.Chdir(tmpDir); err != nil {
t.Fatalf("Failed to change to temp directory: %v", err)
}
// Initialize git repo first
if err := runCommandInDir(tmpDir, "git", "init"); err != nil {
t.Fatalf("Failed to init git: %v", err)
}
// Run bd init with quiet mode
rootCmd.SetArgs([]string{"init", "--prefix", "test", "--quiet"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("Init failed: %v", err)
}
// Verify git config was set
output, err := runCommandInDirWithOutput(tmpDir, "git", "config", "merge.beads.driver")
if err != nil {
t.Fatalf("Failed to get git config: %v", err)
}
if !strings.Contains(output, "bd merge") {
t.Errorf("Expected merge driver to contain 'bd merge', got: %s", output)
}
// Verify .gitattributes was created
gitattrsPath := filepath.Join(tmpDir, ".gitattributes")
content, err := os.ReadFile(gitattrsPath)
if err != nil {
t.Fatalf("Failed to read .gitattributes: %v", err)
}
if !strings.Contains(string(content), ".beads/beads.jsonl merge=beads") {
t.Error(".gitattributes should contain merge driver configuration")
}
})
t.Run("skip merge driver with flag", func(t *testing.T) {
// Reset global state
origDBPath := dbPath
defer func() { dbPath = origDBPath }()
dbPath = ""
tmpDir := t.TempDir()
originalWd, err := os.Getwd()
if err != nil {
t.Fatalf("Failed to get working directory: %v", err)
}
defer os.Chdir(originalWd)
if err := os.Chdir(tmpDir); err != nil {
t.Fatalf("Failed to change to temp directory: %v", err)
}
// Initialize git repo first
if err := runCommandInDir(tmpDir, "git", "init"); err != nil {
t.Fatalf("Failed to init git: %v", err)
}
// Run bd init with --skip-merge-driver
rootCmd.SetArgs([]string{"init", "--prefix", "test", "--skip-merge-driver", "--quiet"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("Init failed: %v", err)
}
// Verify git config was NOT set
_, err = runCommandInDirWithOutput(tmpDir, "git", "config", "merge.beads.driver")
if err == nil {
t.Error("Expected git config to not be set with --skip-merge-driver")
}
// Verify .gitattributes was NOT created
gitattrsPath := filepath.Join(tmpDir, ".gitattributes")
if _, err := os.Stat(gitattrsPath); err == nil {
t.Error(".gitattributes should not be created with --skip-merge-driver")
}
})
t.Run("non-git repo skips merge driver silently", func(t *testing.T) {
// Reset global state
origDBPath := dbPath
defer func() { dbPath = origDBPath }()
dbPath = ""
tmpDir := t.TempDir()
originalWd, err := os.Getwd()
if err != nil {
t.Fatalf("Failed to get working directory: %v", err)
}
defer os.Chdir(originalWd)
if err := os.Chdir(tmpDir); err != nil {
t.Fatalf("Failed to change to temp directory: %v", err)
}
// DON'T initialize git repo
// Run bd init - should succeed even without git
rootCmd.SetArgs([]string{"init", "--prefix", "test", "--quiet"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("Init should succeed in non-git directory: %v", err)
}
// Verify .beads was still created
beadsDir := filepath.Join(tmpDir, ".beads")
if _, err := os.Stat(beadsDir); os.IsNotExist(err) {
t.Error(".beads directory should be created even without git")
}
})
t.Run("detect already-installed merge driver", func(t *testing.T) {
// Reset global state
origDBPath := dbPath
defer func() { dbPath = origDBPath }()
dbPath = ""
tmpDir := t.TempDir()
originalWd, err := os.Getwd()
if err != nil {
t.Fatalf("Failed to get working directory: %v", err)
}
defer os.Chdir(originalWd)
if err := os.Chdir(tmpDir); err != nil {
t.Fatalf("Failed to change to temp directory: %v", err)
}
// Initialize git repo
if err := runCommandInDir(tmpDir, "git", "init"); err != nil {
t.Fatalf("Failed to init git: %v", err)
}
// Pre-configure merge driver manually
if err := runCommandInDir(tmpDir, "git", "config", "merge.beads.driver", "bd merge %A %O %L %R"); err != nil {
t.Fatalf("Failed to set git config: %v", err)
}
// Create .gitattributes with merge driver
gitattrsPath := filepath.Join(tmpDir, ".gitattributes")
initialContent := "# Existing config\n.beads/beads.jsonl merge=beads\n"
if err := os.WriteFile(gitattrsPath, []byte(initialContent), 0644); err != nil {
t.Fatalf("Failed to create .gitattributes: %v", err)
}
// Run bd init - should detect existing config
rootCmd.SetArgs([]string{"init", "--prefix", "test", "--quiet"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("Init failed: %v", err)
}
// Verify git config still exists (not duplicated)
output, err := runCommandInDirWithOutput(tmpDir, "git", "config", "merge.beads.driver")
if err != nil {
t.Fatalf("Git config should still be set: %v", err)
}
if !strings.Contains(output, "bd merge") {
t.Errorf("Expected merge driver to contain 'bd merge', got: %s", output)
}
// Verify .gitattributes wasn't duplicated
content, err := os.ReadFile(gitattrsPath)
if err != nil {
t.Fatalf("Failed to read .gitattributes: %v", err)
}
contentStr := string(content)
// Count occurrences - should only appear once
count := strings.Count(contentStr, ".beads/beads.jsonl merge=beads")
if count != 1 {
t.Errorf("Expected .gitattributes to contain merge config exactly once, found %d times", count)
}
// Should still have the comment
if !strings.Contains(contentStr, "# Existing config") {
t.Error(".gitattributes should preserve existing content")
}
})
t.Run("append to existing .gitattributes", func(t *testing.T) {
// Reset global state
origDBPath := dbPath
defer func() { dbPath = origDBPath }()
dbPath = ""
// Reset Cobra flags
initCmd.Flags().Set("skip-merge-driver", "false")
tmpDir := t.TempDir()
originalWd, err := os.Getwd()
if err != nil {
t.Fatalf("Failed to get working directory: %v", err)
}
defer os.Chdir(originalWd)
if err := os.Chdir(tmpDir); err != nil {
t.Fatalf("Failed to change to temp directory: %v", err)
}
// Initialize git repo
if err := runCommandInDir(tmpDir, "git", "init"); err != nil {
t.Fatalf("Failed to init git: %v", err)
}
// Create .gitattributes with existing content (no newline at end)
gitattrsPath := filepath.Join(tmpDir, ".gitattributes")
existingContent := "*.txt text\n*.jpg binary"
if err := os.WriteFile(gitattrsPath, []byte(existingContent), 0644); err != nil {
t.Fatalf("Failed to create .gitattributes: %v", err)
}
// Run bd init
rootCmd.SetArgs([]string{"init", "--prefix", "test", "--quiet"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("Init failed: %v", err)
}
// Verify .gitattributes was appended to, not overwritten
content, err := os.ReadFile(gitattrsPath)
if err != nil {
t.Fatalf("Failed to read .gitattributes: %v", err)
}
contentStr := string(content)
// Should contain original content
if !strings.Contains(contentStr, "*.txt text") {
t.Error(".gitattributes should preserve original content")
}
if !strings.Contains(contentStr, "*.jpg binary") {
t.Error(".gitattributes should preserve original content")
}
// Should contain beads config
if !strings.Contains(contentStr, ".beads/beads.jsonl merge=beads") {
t.Error(".gitattributes should contain beads merge config")
}
// Beads config should come after existing content
txtIdx := strings.Index(contentStr, "*.txt")
beadsIdx := strings.Index(contentStr, ".beads/beads.jsonl")
if txtIdx >= beadsIdx {
t.Error("Beads config should be appended after existing content")
}
})
t.Run("verify git config has correct settings", func(t *testing.T) {
// Reset global state
origDBPath := dbPath
defer func() { dbPath = origDBPath }()
dbPath = ""
// Reset Cobra flags
initCmd.Flags().Set("skip-merge-driver", "false")
tmpDir := t.TempDir()
originalWd, err := os.Getwd()
if err != nil {
t.Fatalf("Failed to get working directory: %v", err)
}
defer os.Chdir(originalWd)
if err := os.Chdir(tmpDir); err != nil {
t.Fatalf("Failed to change to temp directory: %v", err)
}
// Initialize git repo
if err := runCommandInDir(tmpDir, "git", "init"); err != nil {
t.Fatalf("Failed to init git: %v", err)
}
// Run bd init
rootCmd.SetArgs([]string{"init", "--prefix", "test", "--quiet"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("Init failed: %v", err)
}
// Verify merge.beads.driver is set correctly
driver, err := runCommandInDirWithOutput(tmpDir, "git", "config", "merge.beads.driver")
if err != nil {
t.Fatalf("Failed to get merge.beads.driver: %v", err)
}
driver = strings.TrimSpace(driver)
expected := "bd merge %A %O %L %R"
if driver != expected {
t.Errorf("Expected merge.beads.driver to be %q, got %q", expected, driver)
}
// Verify merge.beads.name is set
name, err := runCommandInDirWithOutput(tmpDir, "git", "config", "merge.beads.name")
if err != nil {
t.Fatalf("Failed to get merge.beads.name: %v", err)
}
name = strings.TrimSpace(name)
if !strings.Contains(name, "bd") {
t.Errorf("Expected merge.beads.name to contain 'bd', got %q", name)
}
})
}
// TestReadFirstIssueFromJSONL_ValidFile verifies reading first issue from valid JSONL
func TestReadFirstIssueFromJSONL_ValidFile(t *testing.T) {
tempDir := t.TempDir()
jsonlPath := filepath.Join(tempDir, "test.jsonl")
// Create test JSONL file with multiple issues
content := `{"id":"bd-1","title":"First Issue","description":"First test"}
{"id":"bd-2","title":"Second Issue","description":"Second test"}
{"id":"bd-3","title":"Third Issue","description":"Third test"}
`
if err := os.WriteFile(jsonlPath, []byte(content), 0o600); err != nil {
t.Fatalf("Failed to write test file: %v", err)
}
issue, err := readFirstIssueFromJSONL(jsonlPath)
if err != nil {
t.Fatalf("readFirstIssueFromJSONL failed: %v", err)
}
if issue == nil {
t.Fatal("Expected non-nil issue, got nil")
}
// Verify we got the FIRST issue
if issue.ID != "bd-1" {
t.Errorf("Expected ID 'bd-1', got '%s'", issue.ID)
}
if issue.Title != "First Issue" {
t.Errorf("Expected title 'First Issue', got '%s'", issue.Title)
}
if issue.Description != "First test" {
t.Errorf("Expected description 'First test', got '%s'", issue.Description)
}
}
// TestReadFirstIssueFromJSONL_EmptyLines verifies skipping empty lines
func TestReadFirstIssueFromJSONL_EmptyLines(t *testing.T) {
tempDir := t.TempDir()
jsonlPath := filepath.Join(tempDir, "test.jsonl")
// Create JSONL with empty lines before first valid issue
content := `
{"id":"bd-1","title":"First Valid Issue"}
{"id":"bd-2","title":"Second Issue"}
`
if err := os.WriteFile(jsonlPath, []byte(content), 0o600); err != nil {
t.Fatalf("Failed to write test file: %v", err)
}
issue, err := readFirstIssueFromJSONL(jsonlPath)
if err != nil {
t.Fatalf("readFirstIssueFromJSONL failed: %v", err)
}
if issue == nil {
t.Fatal("Expected non-nil issue, got nil")
}
if issue.ID != "bd-1" {
t.Errorf("Expected ID 'bd-1', got '%s'", issue.ID)
}
if issue.Title != "First Valid Issue" {
t.Errorf("Expected title 'First Valid Issue', got '%s'", issue.Title)
}
}
// TestReadFirstIssueFromJSONL_EmptyFile verifies handling of empty file
func TestReadFirstIssueFromJSONL_EmptyFile(t *testing.T) {
tempDir := t.TempDir()
jsonlPath := filepath.Join(tempDir, "empty.jsonl")
// Create empty file
if err := os.WriteFile(jsonlPath, []byte(""), 0o600); err != nil {
t.Fatalf("Failed to write test file: %v", err)
}
issue, err := readFirstIssueFromJSONL(jsonlPath)
if err != nil {
t.Fatalf("readFirstIssueFromJSONL should not error on empty file: %v", err)
}
if issue != nil {
t.Errorf("Expected nil issue for empty file, got %+v", issue)
}
}