Files
beads/cmd/bd/init_test.go
Steve Yegge c4c5c8063a Fix: Change default JSONL filename from beads.jsonl to issues.jsonl
The canonical beads database name is issues.jsonl. Tens of thousands of users
have issues.jsonl, and beads.jsonl was only used by the Beads project itself
due to git history pollution.

Changes:
- Updated bd doctor to warn about beads.jsonl instead of issues.jsonl
- Changed default config from beads.jsonl to issues.jsonl
- Reversed precedence in checkGitForIssues to prefer issues.jsonl
- Updated git merge driver config to use issues.jsonl
- Updated all tests to expect issues.jsonl as the default

issues.jsonl is now the canonical default; beads.jsonl is legacy

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-21 23:34:22 -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/issues.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 %A %B"); 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/issues.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/issues.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/issues.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/issues.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 %A %B"
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)
}
}