Centralizes repository context resolution via RepoContext API, fixing bugs where git commands run in the wrong repo when BEADS_DIR points elsewhere or in worktree scenarios.
684 lines
20 KiB
Go
684 lines
20 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/steveyegge/beads/internal/config"
|
|
"github.com/steveyegge/beads/internal/git"
|
|
"github.com/steveyegge/beads/internal/storage/sqlite"
|
|
"github.com/steveyegge/beads/internal/types"
|
|
)
|
|
|
|
// TestMultiRepoPathResolutionCWDInvariant verifies that path resolution for
|
|
// repos.additional produces the same absolute paths regardless of CWD.
|
|
//
|
|
// The bug (oss-lbp): Running from .beads/ caused paths like "oss/" to become
|
|
// ".beads/oss/" instead of "{repo}/oss/". This test ensures the fix works
|
|
// by verifying resolution from multiple CWDs produces identical results.
|
|
//
|
|
// Covers: T040-T042
|
|
func TestMultiRepoPathResolutionCWDInvariant(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// Store original working directory
|
|
originalWd, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatalf("failed to get original working directory: %v", err)
|
|
}
|
|
defer func() { _ = os.Chdir(originalWd) }()
|
|
|
|
// Create temp repo structure
|
|
// Resolve symlinks to avoid macOS /var -> /private/var issues
|
|
tmpDir, err := filepath.EvalSymlinks(t.TempDir())
|
|
if err != nil {
|
|
t.Fatalf("eval symlinks failed: %v", err)
|
|
}
|
|
|
|
// Setup git repo
|
|
if err := setupGitRepoInDir(t, tmpDir); err != nil {
|
|
t.Fatalf("failed to setup git repo: %v", err)
|
|
}
|
|
|
|
// Create .beads directory structure
|
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
|
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
|
t.Fatalf("failed to create .beads dir: %v", err)
|
|
}
|
|
|
|
// Create subdirectory for testing CWD from subdir
|
|
subDir := filepath.Join(tmpDir, "src", "pkg")
|
|
if err := os.MkdirAll(subDir, 0755); err != nil {
|
|
t.Fatalf("failed to create subdir: %v", err)
|
|
}
|
|
|
|
// Create oss/ directory (the multi-repo target)
|
|
ossDir := filepath.Join(tmpDir, "oss")
|
|
ossBeadsDir := filepath.Join(ossDir, ".beads")
|
|
if err := os.MkdirAll(ossBeadsDir, 0755); err != nil {
|
|
t.Fatalf("failed to create oss/.beads dir: %v", err)
|
|
}
|
|
|
|
// Create config.yaml with relative path
|
|
configContent := `repos:
|
|
primary: "."
|
|
additional:
|
|
- oss/
|
|
`
|
|
configPath := filepath.Join(beadsDir, "config.yaml")
|
|
if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil {
|
|
t.Fatalf("failed to write config file: %v", err)
|
|
}
|
|
|
|
// Create database
|
|
dbPath := filepath.Join(beadsDir, "beads.db")
|
|
store, err := sqlite.New(ctx, dbPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to create store: %v", err)
|
|
}
|
|
defer store.Close()
|
|
|
|
// Set issue prefix
|
|
if err := store.SetConfig(ctx, "issue_prefix", "test"); err != nil {
|
|
t.Fatalf("failed to set issue_prefix: %v", err)
|
|
}
|
|
|
|
// Create a test issue
|
|
issue := &types.Issue{
|
|
ID: "test-1",
|
|
Title: "Test Issue",
|
|
Status: types.StatusOpen,
|
|
IssueType: types.TypeTask,
|
|
Priority: 2,
|
|
}
|
|
if err := store.CreateIssue(ctx, issue, "test"); err != nil {
|
|
t.Fatalf("failed to create issue: %v", err)
|
|
}
|
|
store.Close()
|
|
|
|
// T040: Test from repo root
|
|
t.Run("T040_from_repo_root", func(t *testing.T) {
|
|
if err := os.Chdir(tmpDir); err != nil {
|
|
t.Fatalf("chdir to repo root failed: %v", err)
|
|
}
|
|
git.ResetCaches()
|
|
|
|
// Initialize config
|
|
if err := config.Initialize(); err != nil {
|
|
t.Fatalf("config.Initialize() failed: %v", err)
|
|
}
|
|
|
|
multiRepo := config.GetMultiRepoConfig()
|
|
if multiRepo == nil {
|
|
t.Fatal("GetMultiRepoConfig() returned nil")
|
|
}
|
|
|
|
// The key assertion: "oss/" should resolve to {repo}/oss/
|
|
if len(multiRepo.Additional) != 1 {
|
|
t.Fatalf("expected 1 additional repo, got %d", len(multiRepo.Additional))
|
|
}
|
|
|
|
// Verify config file used is in the right place
|
|
configUsed := config.ConfigFileUsed()
|
|
expectedConfig := filepath.Join(beadsDir, "config.yaml")
|
|
if configUsed != expectedConfig {
|
|
t.Errorf("ConfigFileUsed() = %q, want %q", configUsed, expectedConfig)
|
|
}
|
|
|
|
t.Logf("From repo root: additional[0] = %q", multiRepo.Additional[0])
|
|
t.Logf("ConfigFileUsed() = %q", configUsed)
|
|
})
|
|
|
|
// T041: Test from .beads/ directory (the bug trigger location)
|
|
t.Run("T041_from_beads_directory", func(t *testing.T) {
|
|
if err := os.Chdir(beadsDir); err != nil {
|
|
t.Fatalf("chdir to .beads failed: %v", err)
|
|
}
|
|
git.ResetCaches()
|
|
|
|
// Re-initialize config from new CWD
|
|
if err := config.Initialize(); err != nil {
|
|
t.Fatalf("config.Initialize() failed: %v", err)
|
|
}
|
|
|
|
multiRepo := config.GetMultiRepoConfig()
|
|
if multiRepo == nil {
|
|
t.Fatal("GetMultiRepoConfig() returned nil")
|
|
}
|
|
|
|
if len(multiRepo.Additional) != 1 {
|
|
t.Fatalf("expected 1 additional repo, got %d", len(multiRepo.Additional))
|
|
}
|
|
|
|
// Verify config is still found correctly
|
|
configUsed := config.ConfigFileUsed()
|
|
expectedConfig := filepath.Join(beadsDir, "config.yaml")
|
|
if configUsed != expectedConfig {
|
|
t.Errorf("ConfigFileUsed() = %q, want %q", configUsed, expectedConfig)
|
|
}
|
|
|
|
t.Logf("From .beads/: additional[0] = %q", multiRepo.Additional[0])
|
|
t.Logf("ConfigFileUsed() = %q", configUsed)
|
|
})
|
|
|
|
// T042: Test from subdirectory
|
|
t.Run("T042_from_subdirectory", func(t *testing.T) {
|
|
if err := os.Chdir(subDir); err != nil {
|
|
t.Fatalf("chdir to subdir failed: %v", err)
|
|
}
|
|
git.ResetCaches()
|
|
|
|
// Re-initialize config from new CWD
|
|
if err := config.Initialize(); err != nil {
|
|
t.Fatalf("config.Initialize() failed: %v", err)
|
|
}
|
|
|
|
multiRepo := config.GetMultiRepoConfig()
|
|
if multiRepo == nil {
|
|
t.Fatal("GetMultiRepoConfig() returned nil")
|
|
}
|
|
|
|
if len(multiRepo.Additional) != 1 {
|
|
t.Fatalf("expected 1 additional repo, got %d", len(multiRepo.Additional))
|
|
}
|
|
|
|
// Verify config is still found correctly
|
|
configUsed := config.ConfigFileUsed()
|
|
expectedConfig := filepath.Join(beadsDir, "config.yaml")
|
|
if configUsed != expectedConfig {
|
|
t.Errorf("ConfigFileUsed() = %q, want %q", configUsed, expectedConfig)
|
|
}
|
|
|
|
t.Logf("From subdir: additional[0] = %q", multiRepo.Additional[0])
|
|
t.Logf("ConfigFileUsed() = %q", configUsed)
|
|
})
|
|
}
|
|
|
|
// TestExportToMultiRepoCWDInvariant tests that ExportToMultiRepo produces
|
|
// consistent export paths regardless of CWD.
|
|
//
|
|
// This is an integration test that exercises the actual export code path
|
|
// which was affected by the bug.
|
|
func TestExportToMultiRepoCWDInvariant(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// Store original working directory
|
|
originalWd, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatalf("failed to get original working directory: %v", err)
|
|
}
|
|
defer func() { _ = os.Chdir(originalWd) }()
|
|
|
|
// Create temp repo structure
|
|
tmpDir, err := filepath.EvalSymlinks(t.TempDir())
|
|
if err != nil {
|
|
t.Fatalf("eval symlinks failed: %v", err)
|
|
}
|
|
|
|
// Setup git repo
|
|
if err := setupGitRepoInDir(t, tmpDir); err != nil {
|
|
t.Fatalf("failed to setup git repo: %v", err)
|
|
}
|
|
|
|
// Create .beads directory
|
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
|
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
|
t.Fatalf("failed to create .beads dir: %v", err)
|
|
}
|
|
|
|
// Create oss/.beads directory
|
|
ossBeadsDir := filepath.Join(tmpDir, "oss", ".beads")
|
|
if err := os.MkdirAll(ossBeadsDir, 0755); err != nil {
|
|
t.Fatalf("failed to create oss/.beads dir: %v", err)
|
|
}
|
|
|
|
// Create config.yaml with relative path
|
|
configContent := `repos:
|
|
primary: "."
|
|
additional:
|
|
- oss/
|
|
`
|
|
configPath := filepath.Join(beadsDir, "config.yaml")
|
|
if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil {
|
|
t.Fatalf("failed to write config file: %v", err)
|
|
}
|
|
|
|
// Create database and issue once before CWD tests
|
|
dbPath := filepath.Join(beadsDir, "beads.db")
|
|
store, err := sqlite.New(ctx, dbPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to create store: %v", err)
|
|
}
|
|
|
|
// Set issue prefix
|
|
if err := store.SetConfig(ctx, "issue_prefix", "oss"); err != nil {
|
|
store.Close()
|
|
t.Fatalf("failed to set issue_prefix: %v", err)
|
|
}
|
|
|
|
// Create a test issue for oss/ repo
|
|
issue := &types.Issue{
|
|
ID: "oss-1",
|
|
Title: "OSS Issue",
|
|
Status: types.StatusOpen,
|
|
IssueType: types.TypeTask,
|
|
Priority: 2,
|
|
SourceRepo: "oss/", // This routes to additional repo
|
|
}
|
|
if err := store.CreateIssue(ctx, issue, "oss"); err != nil {
|
|
store.Close()
|
|
t.Fatalf("failed to create issue: %v", err)
|
|
}
|
|
store.Close()
|
|
|
|
// Helper function to run export and check results
|
|
runExportTest := func(t *testing.T, testCwd string) string {
|
|
t.Helper()
|
|
|
|
// Change to test CWD
|
|
if err := os.Chdir(testCwd); err != nil {
|
|
t.Fatalf("chdir to %s failed: %v", testCwd, err)
|
|
}
|
|
git.ResetCaches()
|
|
|
|
// Initialize config
|
|
if err := config.Initialize(); err != nil {
|
|
t.Fatalf("config.Initialize() failed: %v", err)
|
|
}
|
|
|
|
// Open existing store
|
|
store, err := sqlite.New(ctx, dbPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to open store: %v", err)
|
|
}
|
|
defer store.Close()
|
|
|
|
// Run multi-repo export
|
|
results, err := store.ExportToMultiRepo(ctx)
|
|
if err != nil {
|
|
t.Fatalf("ExportToMultiRepo failed: %v", err)
|
|
}
|
|
|
|
// Check that oss/ was exported
|
|
if results == nil {
|
|
t.Fatal("ExportToMultiRepo returned nil results")
|
|
}
|
|
|
|
// The export should create issues.jsonl in oss/.beads/
|
|
expectedPath := filepath.Join(ossBeadsDir, "issues.jsonl")
|
|
if _, err := os.Stat(expectedPath); os.IsNotExist(err) {
|
|
t.Errorf("expected %s to exist after export from %s", expectedPath, testCwd)
|
|
}
|
|
|
|
return expectedPath
|
|
}
|
|
|
|
// Test from repo root
|
|
t.Run("export_from_repo_root", func(t *testing.T) {
|
|
path := runExportTest(t, tmpDir)
|
|
t.Logf("Export from repo root created: %s", path)
|
|
})
|
|
|
|
// Test from .beads/ directory
|
|
t.Run("export_from_beads_dir", func(t *testing.T) {
|
|
path := runExportTest(t, beadsDir)
|
|
t.Logf("Export from .beads/ created: %s", path)
|
|
|
|
// Key assertion: should NOT create .beads/oss/.beads/issues.jsonl
|
|
badPath := filepath.Join(beadsDir, "oss", ".beads", "issues.jsonl")
|
|
if _, err := os.Stat(badPath); err == nil {
|
|
t.Errorf("BUG: export created %s (CWD-relative path)", badPath)
|
|
}
|
|
})
|
|
|
|
// Test from subdirectory
|
|
subDir := filepath.Join(tmpDir, "src")
|
|
if err := os.MkdirAll(subDir, 0755); err != nil {
|
|
t.Fatalf("failed to create subdir: %v", err)
|
|
}
|
|
|
|
t.Run("export_from_subdirectory", func(t *testing.T) {
|
|
path := runExportTest(t, subDir)
|
|
t.Logf("Export from subdir created: %s", path)
|
|
})
|
|
}
|
|
|
|
// TestSyncModePathResolution tests path resolution across different sync modes.
|
|
//
|
|
// Covers: T050-T052
|
|
func TestSyncModePathResolution(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// Store original working directory
|
|
originalWd, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatalf("failed to get original working directory: %v", err)
|
|
}
|
|
defer func() { _ = os.Chdir(originalWd) }()
|
|
|
|
// T050: Normal sync mode path resolution
|
|
t.Run("T050_normal_sync_mode", func(t *testing.T) {
|
|
// Restore CWD at end of subtest to prevent interference with subsequent tests.
|
|
// t.TempDir() cleanup happens after subtest returns, so CWD must be restored first.
|
|
subtestWd, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatalf("failed to get subtest working directory: %v", err)
|
|
}
|
|
defer func() { _ = os.Chdir(subtestWd) }()
|
|
|
|
tmpDir, err := filepath.EvalSymlinks(t.TempDir())
|
|
if err != nil {
|
|
t.Fatalf("eval symlinks failed: %v", err)
|
|
}
|
|
|
|
// Setup git repo
|
|
if err := setupGitRepoInDir(t, tmpDir); err != nil {
|
|
t.Fatalf("failed to setup git repo: %v", err)
|
|
}
|
|
|
|
// Create .beads directory
|
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
|
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
|
t.Fatalf("failed to create .beads dir: %v", err)
|
|
}
|
|
|
|
// Create oss/.beads directory
|
|
ossBeadsDir := filepath.Join(tmpDir, "oss", ".beads")
|
|
if err := os.MkdirAll(ossBeadsDir, 0755); err != nil {
|
|
t.Fatalf("failed to create oss/.beads dir: %v", err)
|
|
}
|
|
|
|
// Create config.yaml with relative path
|
|
configContent := `repos:
|
|
primary: "."
|
|
additional:
|
|
- oss/
|
|
`
|
|
configPath := filepath.Join(beadsDir, "config.yaml")
|
|
if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil {
|
|
t.Fatalf("failed to write config file: %v", err)
|
|
}
|
|
|
|
// Create database
|
|
dbPath := filepath.Join(beadsDir, "beads.db")
|
|
store, err := sqlite.New(ctx, dbPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to create store: %v", err)
|
|
}
|
|
|
|
// Set issue prefix
|
|
if err := store.SetConfig(ctx, "issue_prefix", "test"); err != nil {
|
|
store.Close()
|
|
t.Fatalf("failed to set issue_prefix: %v", err)
|
|
}
|
|
|
|
// Create issue for oss/
|
|
issue := &types.Issue{
|
|
ID: "test-100",
|
|
Title: "Normal mode issue",
|
|
Status: types.StatusOpen,
|
|
IssueType: types.TypeTask,
|
|
Priority: 2,
|
|
SourceRepo: "oss/",
|
|
}
|
|
if err := store.CreateIssue(ctx, issue, "test"); err != nil {
|
|
store.Close()
|
|
t.Fatalf("failed to create issue: %v", err)
|
|
}
|
|
|
|
// Change to repo root and initialize config
|
|
if err := os.Chdir(tmpDir); err != nil {
|
|
store.Close()
|
|
t.Fatalf("chdir failed: %v", err)
|
|
}
|
|
git.ResetCaches()
|
|
|
|
if err := config.Initialize(); err != nil {
|
|
store.Close()
|
|
t.Fatalf("config.Initialize() failed: %v", err)
|
|
}
|
|
|
|
// Export
|
|
results, err := store.ExportToMultiRepo(ctx)
|
|
store.Close()
|
|
if err != nil {
|
|
t.Fatalf("ExportToMultiRepo failed: %v", err)
|
|
}
|
|
|
|
// Verify export created file in correct location
|
|
expectedPath := filepath.Join(ossBeadsDir, "issues.jsonl")
|
|
if _, err := os.Stat(expectedPath); os.IsNotExist(err) {
|
|
t.Errorf("expected %s to exist", expectedPath)
|
|
}
|
|
|
|
t.Logf("Normal sync mode export results: %v", results)
|
|
})
|
|
|
|
// T051: Sync-branch mode with daemon context
|
|
t.Run("T051_sync_branch_mode", func(t *testing.T) {
|
|
// Restore CWD at end of subtest to prevent interference with subsequent tests.
|
|
subtestWd, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatalf("failed to get subtest working directory: %v", err)
|
|
}
|
|
defer func() { _ = os.Chdir(subtestWd) }()
|
|
|
|
tmpDir, err := filepath.EvalSymlinks(t.TempDir())
|
|
if err != nil {
|
|
t.Fatalf("eval symlinks failed: %v", err)
|
|
}
|
|
|
|
// Setup git repo
|
|
if err := setupGitRepoInDir(t, tmpDir); err != nil {
|
|
t.Fatalf("failed to setup git repo: %v", err)
|
|
}
|
|
|
|
// Create .beads directory
|
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
|
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
|
t.Fatalf("failed to create .beads dir: %v", err)
|
|
}
|
|
|
|
// Create oss/.beads directory
|
|
ossBeadsDir := filepath.Join(tmpDir, "oss", ".beads")
|
|
if err := os.MkdirAll(ossBeadsDir, 0755); err != nil {
|
|
t.Fatalf("failed to create oss/.beads dir: %v", err)
|
|
}
|
|
|
|
// Create config.yaml with sync-branch AND multi-repo
|
|
configContent := `sync:
|
|
branch: beads-sync
|
|
repos:
|
|
primary: "."
|
|
additional:
|
|
- oss/
|
|
`
|
|
configPath := filepath.Join(beadsDir, "config.yaml")
|
|
if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil {
|
|
t.Fatalf("failed to write config file: %v", err)
|
|
}
|
|
|
|
// Create the sync branch
|
|
if err := exec.Command("git", "-C", tmpDir, "branch", "beads-sync").Run(); err != nil {
|
|
t.Fatalf("failed to create sync branch: %v", err)
|
|
}
|
|
|
|
// Create database
|
|
dbPath := filepath.Join(beadsDir, "beads.db")
|
|
store, err := sqlite.New(ctx, dbPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to create store: %v", err)
|
|
}
|
|
|
|
// Set issue prefix
|
|
if err := store.SetConfig(ctx, "issue_prefix", "test"); err != nil {
|
|
store.Close()
|
|
t.Fatalf("failed to set issue_prefix: %v", err)
|
|
}
|
|
|
|
// Create issue for oss/
|
|
issue := &types.Issue{
|
|
ID: "test-200",
|
|
Title: "Sync-branch mode issue",
|
|
Status: types.StatusOpen,
|
|
IssueType: types.TypeTask,
|
|
Priority: 2,
|
|
SourceRepo: "oss/",
|
|
}
|
|
if err := store.CreateIssue(ctx, issue, "test"); err != nil {
|
|
store.Close()
|
|
t.Fatalf("failed to create issue: %v", err)
|
|
}
|
|
|
|
// Simulate daemon context: CWD is .beads/
|
|
if err := os.Chdir(beadsDir); err != nil {
|
|
store.Close()
|
|
t.Fatalf("chdir to .beads/ failed: %v", err)
|
|
}
|
|
git.ResetCaches()
|
|
|
|
if err := config.Initialize(); err != nil {
|
|
store.Close()
|
|
t.Fatalf("config.Initialize() failed: %v", err)
|
|
}
|
|
|
|
// Export from daemon-like context
|
|
results, err := store.ExportToMultiRepo(ctx)
|
|
store.Close()
|
|
if err != nil {
|
|
t.Fatalf("ExportToMultiRepo failed: %v", err)
|
|
}
|
|
|
|
// Key assertion: should still export to correct location
|
|
expectedPath := filepath.Join(ossBeadsDir, "issues.jsonl")
|
|
if _, err := os.Stat(expectedPath); os.IsNotExist(err) {
|
|
t.Errorf("expected %s to exist (sync-branch mode from .beads/)", expectedPath)
|
|
}
|
|
|
|
// Verify no spurious directory created
|
|
badPath := filepath.Join(beadsDir, "oss", ".beads", "issues.jsonl")
|
|
if _, err := os.Stat(badPath); err == nil {
|
|
t.Errorf("BUG: created %s (CWD-relative in sync-branch mode)", badPath)
|
|
}
|
|
|
|
t.Logf("Sync-branch mode export results: %v", results)
|
|
})
|
|
|
|
// T052: External BEADS_DIR mode
|
|
t.Run("T052_external_beads_dir_mode", func(t *testing.T) {
|
|
// Restore CWD at end of subtest to prevent interference with subsequent tests.
|
|
subtestWd, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatalf("failed to get subtest working directory: %v", err)
|
|
}
|
|
defer func() { _ = os.Chdir(subtestWd) }()
|
|
|
|
// Create main project repo
|
|
projectDir, err := filepath.EvalSymlinks(t.TempDir())
|
|
if err != nil {
|
|
t.Fatalf("eval symlinks failed: %v", err)
|
|
}
|
|
if err := setupGitRepoInDir(t, projectDir); err != nil {
|
|
t.Fatalf("failed to setup project repo: %v", err)
|
|
}
|
|
|
|
// Create external beads repo
|
|
externalDir, err := filepath.EvalSymlinks(t.TempDir())
|
|
if err != nil {
|
|
t.Fatalf("eval symlinks failed: %v", err)
|
|
}
|
|
if err := setupGitRepoInDir(t, externalDir); err != nil {
|
|
t.Fatalf("failed to setup external repo: %v", err)
|
|
}
|
|
|
|
// Create .beads in external repo
|
|
externalBeadsDir := filepath.Join(externalDir, ".beads")
|
|
if err := os.MkdirAll(externalBeadsDir, 0755); err != nil {
|
|
t.Fatalf("failed to create external .beads: %v", err)
|
|
}
|
|
|
|
// Create oss/.beads in external repo (sibling to external .beads)
|
|
ossBeadsDir := filepath.Join(externalDir, "oss", ".beads")
|
|
if err := os.MkdirAll(ossBeadsDir, 0755); err != nil {
|
|
t.Fatalf("failed to create oss/.beads: %v", err)
|
|
}
|
|
|
|
// Create config.yaml in external repo with relative path
|
|
configContent := `repos:
|
|
primary: "."
|
|
additional:
|
|
- oss/
|
|
`
|
|
configPath := filepath.Join(externalBeadsDir, "config.yaml")
|
|
if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil {
|
|
t.Fatalf("failed to write config file: %v", err)
|
|
}
|
|
|
|
// Create database in external repo
|
|
dbPath := filepath.Join(externalBeadsDir, "beads.db")
|
|
store, err := sqlite.New(ctx, dbPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to create store: %v", err)
|
|
}
|
|
|
|
// Set issue prefix
|
|
if err := store.SetConfig(ctx, "issue_prefix", "ext"); err != nil {
|
|
store.Close()
|
|
t.Fatalf("failed to set issue_prefix: %v", err)
|
|
}
|
|
|
|
// Create issue for oss/ (ext prefix matches issue ID)
|
|
issue := &types.Issue{
|
|
ID: "ext-300",
|
|
Title: "External mode issue",
|
|
Status: types.StatusOpen,
|
|
IssueType: types.TypeTask,
|
|
Priority: 2,
|
|
SourceRepo: "oss/",
|
|
}
|
|
if err := store.CreateIssue(ctx, issue, "ext"); err != nil {
|
|
store.Close()
|
|
t.Fatalf("failed to create issue: %v", err)
|
|
}
|
|
|
|
// Simulate external mode: CWD is project repo, BEADS_DIR points elsewhere
|
|
if err := os.Chdir(projectDir); err != nil {
|
|
store.Close()
|
|
t.Fatalf("chdir to project failed: %v", err)
|
|
}
|
|
git.ResetCaches()
|
|
|
|
// Initialize config from external location
|
|
// In external mode, config is loaded from BEADS_DIR, not CWD
|
|
// We simulate this by changing to external dir for config init
|
|
if err := os.Chdir(externalDir); err != nil {
|
|
store.Close()
|
|
t.Fatalf("chdir to external failed: %v", err)
|
|
}
|
|
git.ResetCaches()
|
|
|
|
if err := config.Initialize(); err != nil {
|
|
store.Close()
|
|
t.Fatalf("config.Initialize() failed: %v", err)
|
|
}
|
|
|
|
// Export
|
|
results, err := store.ExportToMultiRepo(ctx)
|
|
store.Close()
|
|
if err != nil {
|
|
t.Fatalf("ExportToMultiRepo failed: %v", err)
|
|
}
|
|
|
|
// Verify export in correct location
|
|
expectedPath := filepath.Join(ossBeadsDir, "issues.jsonl")
|
|
if _, err := os.Stat(expectedPath); os.IsNotExist(err) {
|
|
t.Errorf("expected %s to exist (external mode)", expectedPath)
|
|
}
|
|
|
|
t.Logf("External BEADS_DIR mode export results: %v", results)
|
|
})
|
|
}
|