258 lines
7.5 KiB
Go
258 lines
7.5 KiB
Go
//go:build integration
|
|
// +build integration
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/steveyegge/beads/internal/storage/sqlite"
|
|
"github.com/steveyegge/beads/internal/types"
|
|
)
|
|
|
|
// TestExportUpdatesDatabaseMtime verifies that export updates database mtime
|
|
// to be >= JSONL mtime, fixing issues #278, #301, #321
|
|
func TestExportUpdatesDatabaseMtime(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping slow test in short mode")
|
|
}
|
|
|
|
tmpDir := t.TempDir()
|
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
|
if err := os.Mkdir(beadsDir, 0750); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
dbPath := filepath.Join(beadsDir, "beads.db")
|
|
jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
|
|
|
|
// Create and populate database
|
|
store, err := sqlite.New(context.Background(), dbPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create store: %v", err)
|
|
}
|
|
defer store.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Initialize database with 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,
|
|
Priority: 2,
|
|
IssueType: types.TypeTask,
|
|
}
|
|
if err := store.CreateIssue(ctx, issue, "test-actor"); err != nil {
|
|
t.Fatalf("Failed to create issue: %v", err)
|
|
}
|
|
|
|
// Wait a bit to ensure mtime difference
|
|
time.Sleep(1 * time.Second)
|
|
|
|
// Export to JSONL (simulates daemon export)
|
|
if err := exportToJSONLWithStore(ctx, store, jsonlPath); err != nil {
|
|
t.Fatalf("Export failed: %v", err)
|
|
}
|
|
|
|
// Update metadata after export (bd-ymj fix)
|
|
mockLogger := newTestLogger()
|
|
updateExportMetadata(ctx, store, jsonlPath, mockLogger, "")
|
|
|
|
// Get JSONL mtime
|
|
jsonlInfo, err := os.Stat(jsonlPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to stat JSONL after export: %v", err)
|
|
}
|
|
|
|
// WITHOUT the fix, JSONL would be newer than DB here
|
|
// Simulating the old buggy behavior before calling TouchDatabaseFile
|
|
dbInfoAfterExport, err := os.Stat(dbPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to stat database after export: %v", err)
|
|
}
|
|
|
|
// In old buggy behavior, JSONL mtime > DB mtime
|
|
t.Logf("Before TouchDatabaseFile: DB mtime=%v, JSONL mtime=%v",
|
|
dbInfoAfterExport.ModTime(), jsonlInfo.ModTime())
|
|
|
|
// Now apply the fix
|
|
if err := TouchDatabaseFile(dbPath, jsonlPath); err != nil {
|
|
t.Fatalf("TouchDatabaseFile failed: %v", err)
|
|
}
|
|
|
|
// Get final database mtime
|
|
dbInfoAfterTouch, err := os.Stat(dbPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to stat database after touch: %v", err)
|
|
}
|
|
|
|
t.Logf("After TouchDatabaseFile: DB mtime=%v, JSONL mtime=%v",
|
|
dbInfoAfterTouch.ModTime(), jsonlInfo.ModTime())
|
|
|
|
// VERIFY: Database mtime should be >= JSONL mtime
|
|
if dbInfoAfterTouch.ModTime().Before(jsonlInfo.ModTime()) {
|
|
t.Errorf("Database mtime should be >= JSONL mtime after export")
|
|
t.Errorf("DB mtime: %v, JSONL mtime: %v",
|
|
dbInfoAfterTouch.ModTime(), jsonlInfo.ModTime())
|
|
}
|
|
|
|
// VERIFY: validatePreExport should now pass (not block on next export)
|
|
if err := validatePreExport(ctx, store, jsonlPath); err != nil {
|
|
t.Errorf("validatePreExport should pass after TouchDatabaseFile, but got error: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestDaemonExportScenario simulates the full daemon auto-export workflow
|
|
// that was causing issue #278 (daemon shutting down after export)
|
|
func TestDaemonExportScenario(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping slow test in short mode")
|
|
}
|
|
|
|
tmpDir := t.TempDir()
|
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
|
if err := os.Mkdir(beadsDir, 0750); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
dbPath := filepath.Join(beadsDir, "beads.db")
|
|
jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
|
|
|
|
// Create and populate database
|
|
store, err := sqlite.New(context.Background(), dbPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create store: %v", err)
|
|
}
|
|
defer store.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Initialize database with issue_prefix
|
|
if err := store.SetConfig(ctx, "issue_prefix", "bd"); err != nil {
|
|
t.Fatalf("Failed to set issue_prefix: %v", err)
|
|
}
|
|
|
|
// Step 1: User creates an issue (e.g., bd close bd-123)
|
|
now := time.Now()
|
|
issue := &types.Issue{
|
|
ID: "bd-123",
|
|
Title: "User created issue",
|
|
Status: types.StatusClosed,
|
|
Priority: 1,
|
|
IssueType: types.TypeTask,
|
|
ClosedAt: &now,
|
|
}
|
|
if err := store.CreateIssue(ctx, issue, "test-user"); err != nil {
|
|
t.Fatalf("Failed to create issue: %v", err)
|
|
}
|
|
|
|
// Database is now newer than JSONL (JSONL doesn't exist yet)
|
|
time.Sleep(1 * time.Second)
|
|
|
|
// Step 2: Daemon auto-exports after delay (30s-4min in real scenario)
|
|
// This simulates the daemon's export cycle
|
|
if err := exportToJSONLWithStore(ctx, store, jsonlPath); err != nil {
|
|
t.Fatalf("Daemon export failed: %v", err)
|
|
}
|
|
|
|
// Daemon updates metadata after export (bd-ymj fix)
|
|
mockLogger := newTestLogger()
|
|
updateExportMetadata(ctx, store, jsonlPath, mockLogger, "")
|
|
|
|
// THIS IS THE FIX: daemon now calls TouchDatabaseFile after export
|
|
if err := TouchDatabaseFile(dbPath, jsonlPath); err != nil {
|
|
t.Fatalf("TouchDatabaseFile failed: %v", err)
|
|
}
|
|
|
|
// Step 3: User runs bd sync shortly after
|
|
// WITHOUT the fix, this would fail with "JSONL is newer than database"
|
|
// WITH the fix, this should succeed
|
|
if err := validatePreExport(ctx, store, jsonlPath); err != nil {
|
|
t.Errorf("Daemon export scenario failed: validatePreExport blocked after daemon export")
|
|
t.Errorf("This is the bug from issue #278/#301/#321: %v", err)
|
|
}
|
|
|
|
// Verify we can export again (simulates bd sync)
|
|
jsonlPathTemp := jsonlPath + ".sync"
|
|
if err := exportToJSONLWithStore(ctx, store, jsonlPathTemp); err != nil {
|
|
t.Errorf("Second export (bd sync) failed: %v", err)
|
|
}
|
|
os.Remove(jsonlPathTemp)
|
|
}
|
|
|
|
// TestMultipleExportCycles verifies repeated export cycles don't cause issues
|
|
func TestMultipleExportCycles(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping slow test in short mode")
|
|
}
|
|
|
|
tmpDir := t.TempDir()
|
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
|
if err := os.Mkdir(beadsDir, 0750); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
dbPath := filepath.Join(beadsDir, "beads.db")
|
|
jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
|
|
|
|
// Create and populate database
|
|
store, err := sqlite.New(context.Background(), dbPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create store: %v", err)
|
|
}
|
|
defer store.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Initialize database with issue_prefix
|
|
if err := store.SetConfig(ctx, "issue_prefix", "test"); err != nil {
|
|
t.Fatalf("Failed to set issue_prefix: %v", err)
|
|
}
|
|
|
|
// Run multiple export cycles
|
|
for i := 0; i < 5; i++ {
|
|
// Add an issue
|
|
issue := &types.Issue{
|
|
ID: "test-" + string(rune('a'+i)),
|
|
Title: "Test Issue " + string(rune('A'+i)),
|
|
Status: types.StatusOpen,
|
|
Priority: 2,
|
|
IssueType: types.TypeTask,
|
|
}
|
|
if err := store.CreateIssue(ctx, issue, "test-actor"); err != nil {
|
|
t.Fatalf("Cycle %d: Failed to create issue: %v", i, err)
|
|
}
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Export (with fix)
|
|
if err := exportToJSONLWithStore(ctx, store, jsonlPath); err != nil {
|
|
t.Fatalf("Cycle %d: Export failed: %v", i, err)
|
|
}
|
|
|
|
// Update metadata after export (bd-ymj fix)
|
|
mockLogger := newTestLogger()
|
|
updateExportMetadata(ctx, store, jsonlPath, mockLogger, "")
|
|
|
|
// Apply fix
|
|
if err := TouchDatabaseFile(dbPath, jsonlPath); err != nil {
|
|
t.Fatalf("Cycle %d: TouchDatabaseFile failed: %v", i, err)
|
|
}
|
|
|
|
// Verify validation passes
|
|
if err := validatePreExport(ctx, store, jsonlPath); err != nil {
|
|
t.Errorf("Cycle %d: validatePreExport failed: %v", i, err)
|
|
}
|
|
}
|
|
}
|