From b86ce0a7e09dac41f5532d035551357670c8db12 Mon Sep 17 00:00:00 2001 From: aaron-sangster Date: Tue, 6 Jan 2026 03:11:17 +0000 Subject: [PATCH] fix(doctor): query metadata table instead of config for last_import_time (#916) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sync_divergence check was querying the wrong table. The sync code writes last_import_time to the metadata table, but doctor was looking in config. This caused spurious "No last_import_time recorded" warnings even when sync was working correctly. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.5 --- cmd/bd/doctor/sync_divergence.go | 2 +- cmd/bd/doctor/sync_divergence_test.go | 53 ++++++++++++++++++++++++--- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/cmd/bd/doctor/sync_divergence.go b/cmd/bd/doctor/sync_divergence.go index 4371dcd4..4c76a9f2 100644 --- a/cmd/bd/doctor/sync_divergence.go +++ b/cmd/bd/doctor/sync_divergence.go @@ -180,7 +180,7 @@ func checkSQLiteMtimeDivergence(path, beadsDir string) *SyncDivergenceIssue { defer db.Close() var lastImportTimeStr string - err = db.QueryRow("SELECT value FROM config WHERE key = 'last_import_time'").Scan(&lastImportTimeStr) + err = db.QueryRow("SELECT value FROM metadata WHERE key = 'last_import_time'").Scan(&lastImportTimeStr) if err != nil { // No last_import_time recorded - this is a potential issue return &SyncDivergenceIssue{ diff --git a/cmd/bd/doctor/sync_divergence_test.go b/cmd/bd/doctor/sync_divergence_test.go index c9a515cf..79fa6b28 100644 --- a/cmd/bd/doctor/sync_divergence_test.go +++ b/cmd/bd/doctor/sync_divergence_test.go @@ -150,7 +150,7 @@ func TestCheckSQLiteMtimeDivergence(t *testing.T) { t.Fatal(err) } _, _ = db.Exec("CREATE TABLE issues (id TEXT)") - _, _ = db.Exec("CREATE TABLE config (key TEXT, value TEXT)") + _, _ = db.Exec("CREATE TABLE metadata (key TEXT, value TEXT)") db.Close() issue := checkSQLiteMtimeDivergence(dir, beadsDir) @@ -173,7 +173,7 @@ func TestCheckSQLiteMtimeDivergence(t *testing.T) { t.Fatal(err) } _, _ = db.Exec("CREATE TABLE issues (id TEXT)") - _, _ = db.Exec("CREATE TABLE config (key TEXT, value TEXT)") + _, _ = db.Exec("CREATE TABLE metadata (key TEXT, value TEXT)") db.Close() // Create JSONL @@ -214,8 +214,8 @@ func TestCheckSQLiteMtimeDivergence(t *testing.T) { t.Fatal(err) } _, _ = db.Exec("CREATE TABLE issues (id TEXT)") - _, _ = db.Exec("CREATE TABLE config (key TEXT, value TEXT)") - _, _ = db.Exec("INSERT INTO config (key, value) VALUES (?, ?)", + _, _ = db.Exec("CREATE TABLE metadata (key TEXT, value TEXT)") + _, _ = db.Exec("INSERT INTO metadata (key, value) VALUES (?, ?)", "last_import_time", importTime.Format(time.RFC3339)) db.Close() @@ -239,9 +239,9 @@ func TestCheckSQLiteMtimeDivergence(t *testing.T) { t.Fatal(err) } _, _ = db.Exec("CREATE TABLE issues (id TEXT)") - _, _ = db.Exec("CREATE TABLE config (key TEXT, value TEXT)") + _, _ = db.Exec("CREATE TABLE metadata (key TEXT, value TEXT)") oldTime := time.Now().Add(-1 * time.Hour) - _, _ = db.Exec("INSERT INTO config (key, value) VALUES (?, ?)", + _, _ = db.Exec("INSERT INTO metadata (key, value) VALUES (?, ?)", "last_import_time", oldTime.Format(time.RFC3339)) db.Close() @@ -263,6 +263,47 @@ func TestCheckSQLiteMtimeDivergence(t *testing.T) { } } }) + + // Regression test: verify we read from metadata table, not config table. + // The sync code writes to metadata, so doctor must read from there. + // This catches the bug where doctor queried 'config' instead of 'metadata'. + t.Run("reads from metadata table not config", func(t *testing.T) { + dir := mkTmpDirInTmp(t, "bd-mtime-table-*") + beadsDir := filepath.Join(dir, ".beads") + if err := os.MkdirAll(beadsDir, 0755); err != nil { + t.Fatal(err) + } + + // Create JSONL first + jsonlPath := filepath.Join(beadsDir, "issues.jsonl") + if err := os.WriteFile(jsonlPath, []byte(`{"id":"test-1"}`+"\n"), 0644); err != nil { + t.Fatal(err) + } + + // Get JSONL mtime + jsonlInfo, _ := os.Stat(jsonlPath) + importTime := jsonlInfo.ModTime() + + // Create database with BOTH config and metadata tables (realistic schema) + // Put last_import_time ONLY in metadata (as real sync code does) + dbPath := filepath.Join(beadsDir, "beads.db") + db, err := sql.Open("sqlite3", dbPath) + if err != nil { + t.Fatal(err) + } + _, _ = db.Exec("CREATE TABLE issues (id TEXT)") + _, _ = db.Exec("CREATE TABLE config (key TEXT, value TEXT)") + _, _ = db.Exec("CREATE TABLE metadata (key TEXT, value TEXT)") + // Only insert into metadata, NOT config + _, _ = db.Exec("INSERT INTO metadata (key, value) VALUES (?, ?)", + "last_import_time", importTime.Format(time.RFC3339)) + db.Close() + + issue := checkSQLiteMtimeDivergence(dir, beadsDir) + if issue != nil { + t.Errorf("expected nil issue when last_import_time is in metadata table, got %+v", issue) + } + }) } func TestCheckUncommittedBeadsChanges(t *testing.T) {