Files
beads/cmd/bd/import_mtime_test.go
Matteo Landi 4bca2067ff fix(import): Update database mtime after import to prevent bd doctor false warnings (#263)
When 'bd sync --import-only' completes, it imports JSONL changes into
the database but doesn't update the database file's modification time.
This causes 'bd doctor' to incorrectly warn that 'JSONL is newer than
database' even when they're in sync.

Root cause: SQLite in WAL mode writes to beads.db-wal; the main beads.db
mtime often doesn't change until a checkpoint. bd doctor compares JSONL
mtime to beads.db mtime, so it can misfire without an mtime bump.

The fix adds touchDatabaseFile() that:
- Only runs when import actually made changes (not dry-run, not unchanged)
- Sets DB mtime to max(JSONL mtime, now) + 1ns to handle clock skew
- Is best-effort (logs warning on failure, doesn't fail import)
- Includes tests for basic touch and clock skew scenarios

Fixes: bd-g3ey
2025-11-08 11:36:05 -08:00

89 lines
2.4 KiB
Go

package main
import (
"os"
"path/filepath"
"testing"
"time"
)
// TestTouchDatabaseFile verifies the touchDatabaseFile helper function
func TestTouchDatabaseFile(t *testing.T) {
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.db")
// Create a test file
if err := os.WriteFile(testFile, []byte("test"), 0600); err != nil {
t.Fatalf("Failed to create test file: %v", err)
}
// Get initial mtime
infoBefore, err := os.Stat(testFile)
if err != nil {
t.Fatalf("Failed to stat file: %v", err)
}
// Wait a bit to ensure mtime difference (1s for filesystems with coarse resolution)
time.Sleep(1 * time.Second)
// Touch the file
if err := touchDatabaseFile(testFile, ""); err != nil {
t.Fatalf("touchDatabaseFile failed: %v", err)
}
// Get new mtime
infoAfter, err := os.Stat(testFile)
if err != nil {
t.Fatalf("Failed to stat file after touch: %v", err)
}
// Verify mtime was updated
if !infoAfter.ModTime().After(infoBefore.ModTime()) {
t.Errorf("File mtime should be updated after touch")
}
}
// TestTouchDatabaseFileWithClockSkew verifies handling of future JSONL timestamps
func TestTouchDatabaseFileWithClockSkew(t *testing.T) {
tmpDir := t.TempDir()
dbFile := filepath.Join(tmpDir, "test.db")
jsonlFile := filepath.Join(tmpDir, "issues.jsonl")
// Create test files
if err := os.WriteFile(dbFile, []byte("db"), 0600); err != nil {
t.Fatalf("Failed to create db file: %v", err)
}
if err := os.WriteFile(jsonlFile, []byte("jsonl"), 0600); err != nil {
t.Fatalf("Failed to create jsonl file: %v", err)
}
// Set JSONL mtime to 1 hour in the future (simulating clock skew)
futureTime := time.Now().Add(1 * time.Hour)
if err := os.Chtimes(jsonlFile, futureTime, futureTime); err != nil {
t.Fatalf("Failed to set future mtime: %v", err)
}
// Touch the DB file with JSONL path
if err := touchDatabaseFile(dbFile, jsonlFile); err != nil {
t.Fatalf("touchDatabaseFile failed: %v", err)
}
// Get DB mtime
dbInfo, err := os.Stat(dbFile)
if err != nil {
t.Fatalf("Failed to stat db file: %v", err)
}
jsonlInfo, err := os.Stat(jsonlFile)
if err != nil {
t.Fatalf("Failed to stat jsonl file: %v", err)
}
// Verify DB mtime is at least as new as JSONL mtime
// (should be JSONL mtime + 1ns to handle clock skew)
if dbInfo.ModTime().Before(jsonlInfo.ModTime()) {
t.Errorf("DB mtime should be >= JSONL mtime when JSONL is in future")
t.Errorf("DB mtime: %v, JSONL mtime: %v", dbInfo.ModTime(), jsonlInfo.ModTime())
}
}