fix: bd doctor false positive for molecule/wisp prefix variants

The prefix mismatch check in bd doctor was reporting warnings when
issues were created via molecule workflows (bd mol pour), which
intentionally use a different prefix pattern (<base>-mol instead
of just <base>).

Added recognition of valid workflow prefix variants:
- <prefix>-mol (molecules from bd mol pour)
- <prefix>-wisp (ephemeral wisps)
- <prefix>-eph (ephemeral issues)

These are intentional prefix extensions for visual distinction, not
actual mismatches. The check now only warns for truly mismatched
prefixes (e.g., different project entirely).

Added comprehensive regression tests for all prefix variant cases.

Fixes #811

Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-30 17:43:47 -08:00
parent 4ab85eeb9d
commit aff38708e0
2 changed files with 153 additions and 1 deletions

View File

@@ -480,8 +480,21 @@ func CheckDatabaseJSONLSync(path string) DoctorCheck {
}
// Only warn if majority of issues have wrong prefix
// BUT: recognize that <prefix>-mol and <prefix>-wisp are valid variants
// created by molecule/wisp workflows (see internal/storage/sqlite/queries.go:166-170)
if mostCommonPrefix != dbPrefix && maxCount > jsonlCount/2 {
issues = append(issues, fmt.Sprintf("Prefix mismatch: database uses %q but most JSONL issues use %q", dbPrefix, mostCommonPrefix))
// Check if the common prefix is a known workflow variant of the db prefix
isValidVariant := false
for _, suffix := range []string{"-mol", "-wisp", "-eph"} {
if mostCommonPrefix == dbPrefix+suffix {
isValidVariant = true
break
}
}
if !isValidVariant {
issues = append(issues, fmt.Sprintf("Prefix mismatch: database uses %q but most JSONL issues use %q", dbPrefix, mostCommonPrefix))
}
}
}

View File

@@ -870,6 +870,145 @@ also not json
}
}
// TestCheckDatabaseJSONLSync_MoleculePrefix verifies that molecule/wisp prefixes
// are recognized as valid variants and don't trigger false positive warnings.
// Regression test for GitHub issue #811.
func TestCheckDatabaseJSONLSync_MoleculePrefix(t *testing.T) {
tests := []struct {
name string
dbPrefix string
jsonlContent string
expectWarning bool
warningMessage string
}{
{
name: "mol prefix is valid variant",
dbPrefix: "my-project",
// 3 out of 4 issues have the -mol prefix (majority)
jsonlContent: `{"id":"my-project-mol-001","title":"Mol Issue 1"}
{"id":"my-project-mol-002","title":"Mol Issue 2"}
{"id":"my-project-mol-003","title":"Mol Issue 3"}
{"id":"my-project-004","title":"Regular Issue"}
`,
expectWarning: false, // Should NOT warn - mol is a valid variant
warningMessage: "",
},
{
name: "wisp prefix is valid variant",
dbPrefix: "my-project",
jsonlContent: `{"id":"my-project-wisp-001","title":"Wisp Issue 1"}
{"id":"my-project-wisp-002","title":"Wisp Issue 2"}
{"id":"my-project-wisp-003","title":"Wisp Issue 3"}
`,
expectWarning: false, // Should NOT warn - wisp is a valid variant
warningMessage: "",
},
{
name: "eph prefix is valid variant",
dbPrefix: "my-project",
jsonlContent: `{"id":"my-project-eph-001","title":"Ephemeral Issue 1"}
{"id":"my-project-eph-002","title":"Ephemeral Issue 2"}
{"id":"my-project-eph-003","title":"Ephemeral Issue 3"}
`,
expectWarning: false, // Should NOT warn - eph is a valid variant
warningMessage: "",
},
{
name: "unrelated prefix SHOULD warn",
dbPrefix: "my-project",
jsonlContent: `{"id":"other-project-001","title":"Wrong Project 1"}
{"id":"other-project-002","title":"Wrong Project 2"}
{"id":"other-project-003","title":"Wrong Project 3"}
`,
expectWarning: true, // SHOULD warn - different project entirely
warningMessage: "Prefix mismatch",
},
{
name: "mixed valid variants do not warn",
dbPrefix: "bd",
jsonlContent: `{"id":"bd-mol-001","title":"Mol Issue"}
{"id":"bd-wisp-001","title":"Wisp Issue"}
{"id":"bd-001","title":"Regular Issue"}
`,
expectWarning: false, // All are valid variants of "bd"
warningMessage: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpDir := t.TempDir()
beadsDir := filepath.Join(tmpDir, ".beads")
if err := os.MkdirAll(beadsDir, 0755); err != nil {
t.Fatal(err)
}
// Create database with config table containing the prefix
dbPath := filepath.Join(beadsDir, "beads.db")
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
t.Fatalf("failed to create database: %v", err)
}
// Create issues table
_, err = db.Exec(`CREATE TABLE issues (id TEXT PRIMARY KEY, title TEXT, status TEXT)`)
if err != nil {
db.Close()
t.Fatalf("failed to create issues table: %v", err)
}
// Create config table with prefix
_, err = db.Exec(`CREATE TABLE config (key TEXT PRIMARY KEY, value TEXT)`)
if err != nil {
db.Close()
t.Fatalf("failed to create config table: %v", err)
}
_, err = db.Exec(`INSERT INTO config (key, value) VALUES ('issue_prefix', ?)`, tt.dbPrefix)
if err != nil {
db.Close()
t.Fatalf("failed to insert prefix: %v", err)
}
// Count issues in JSONL and insert matching count into DB
lines := strings.Split(strings.TrimSpace(tt.jsonlContent), "\n")
issueCount := 0
for _, line := range lines {
if strings.TrimSpace(line) != "" {
issueCount++
}
}
for i := 0; i < issueCount; i++ {
_, err = db.Exec(`INSERT INTO issues (id, title, status) VALUES (?, ?, ?)`,
fmt.Sprintf("db-issue-%d", i), fmt.Sprintf("DB Issue %d", i), "open")
if err != nil {
db.Close()
t.Fatalf("failed to insert issue: %v", err)
}
}
db.Close()
// Create JSONL file
jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
if err := os.WriteFile(jsonlPath, []byte(tt.jsonlContent), 0600); err != nil {
t.Fatalf("failed to create JSONL: %v", err)
}
check := CheckDatabaseJSONLSync(tmpDir)
hasPrefixWarning := strings.Contains(check.Message, "Prefix mismatch")
if tt.expectWarning && !hasPrefixWarning {
t.Errorf("expected prefix mismatch warning, but got: status=%s, message=%s",
check.Status, check.Message)
}
if !tt.expectWarning && hasPrefixWarning {
t.Errorf("did NOT expect prefix mismatch warning, but got: status=%s, message=%s",
check.Status, check.Message)
}
})
}
}
func TestCountJSONLIssues_Performance(t *testing.T) {
if testing.Short() {
t.Skip("skipping performance test in short mode")