fix(database): B-1 schema integrity and migration fixes
- Fix migration 024 self-insert and bad column reference (F-B1-1, F-B1-2) Uses existing enabled/auto_run columns instead of non-existent deprecated - Abort server on migration failure instead of warning (F-B1-11) main.go now calls log.Fatalf, prints [INFO] only on success - Fix migration 018 scanner_config filename suffix (F-B1-3) Renumbered to 027 with .up.sql suffix - Remove GRANT to non-existent role in scanner_config (F-B1-4) - Resolve duplicate migration numbers 009 and 012 (F-B1-13) Renamed to 009b and 012b for unique lexical sorting - Add IF NOT EXISTS to all non-idempotent migrations (F-B1-15) Fixed: 011, 012, 017, 023, 023a - Replace N+1 dashboard stats loop with GetAllUpdateStats (F-B1-6) Single aggregate query replaces per-agent loop - Add composite index on agent_commands(status, sent_at) (F-B1-5) New migration 028 with partial index for timeout service - Add background refresh token cleanup goroutine (F-B1-10) 24-hour ticker calls CleanupExpiredTokens - ETHOS log format in migration runner (no emojis) All 55 tests pass (41 server + 14 agent). No regressions. See docs/B1_Fix_Implementation.md and DEV-025 through DEV-028. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -79,7 +79,8 @@ func TestMigrationFailureReturnsError(t *testing.T) {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestServerStartsAfterMigrationFailure(t *testing.T) {
|
||||
// Read main.go and inspect the migration error handling block
|
||||
// POST-FIX (F-B1-11): main.go now aborts on migration failure.
|
||||
// This test confirms the Warning pattern is gone.
|
||||
mainPath := filepath.Join("..", "..", "cmd", "server", "main.go")
|
||||
content, err := os.ReadFile(mainPath)
|
||||
if err != nil {
|
||||
@@ -88,30 +89,22 @@ func TestServerStartsAfterMigrationFailure(t *testing.T) {
|
||||
|
||||
src := string(content)
|
||||
|
||||
// Find the migration error block
|
||||
if !strings.Contains(src, "Warning: Migration failed") {
|
||||
t.Fatal("[ERROR] [server] [database] cannot find migration error handling in main.go")
|
||||
}
|
||||
|
||||
// The NORMAL startup migration error (not --migrate flag) logs a warning, NOT a fatal.
|
||||
// Main.go has TWO migration paths:
|
||||
// 1. --migrate flag (line ~183): log.Fatal — correct behavior
|
||||
// 2. Normal startup (line ~191): fmt.Printf("Warning:...") — THIS IS THE BUG
|
||||
// We specifically check the normal startup path.
|
||||
// The old bug: fmt.Printf("Warning: Migration failed...") must be gone
|
||||
if strings.Contains(src, `fmt.Printf("Warning: Migration failed`) {
|
||||
t.Log("[INFO] [server] [database] F-B1-11 P0 confirmed: normal startup swallows migration errors")
|
||||
} else {
|
||||
t.Error("[ERROR] [server] [database] cannot find the migration error swallowing pattern")
|
||||
t.Error("[ERROR] [server] [database] F-B1-11 NOT FIXED: main.go still swallows migration errors")
|
||||
}
|
||||
|
||||
// [OK] is printed unconditionally after the if block
|
||||
migrationBlock := extractBlock(src, "db.Migrate(migrationsPath)", `Database migrations completed`)
|
||||
// Must now use log.Fatalf for migration failure
|
||||
migrationBlock := extractBlock(src, "db.Migrate(migrationsPath)", `migrations_complete`)
|
||||
if migrationBlock == "" {
|
||||
t.Fatal("[ERROR] [server] [database] cannot find migration block in main.go")
|
||||
}
|
||||
|
||||
t.Log("[INFO] [server] [database] F-B1-11 P0 confirmed: main.go swallows migration errors and prints [OK]")
|
||||
t.Log("[INFO] [server] [database] after fix: migration failure must call log.Fatal or os.Exit(1)")
|
||||
if !strings.Contains(migrationBlock, "log.Fatalf") {
|
||||
t.Error("[ERROR] [server] [database] migration error handler does not use log.Fatalf")
|
||||
}
|
||||
|
||||
t.Log("[INFO] [server] [database] F-B1-11 FIXED: migration failure now aborts server")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -124,6 +117,7 @@ func TestServerStartsAfterMigrationFailure(t *testing.T) {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestServerMustAbortOnMigrationFailure(t *testing.T) {
|
||||
// POST-FIX (F-B1-11): Confirms log.Fatalf is used for migration failure
|
||||
mainPath := filepath.Join("..", "..", "cmd", "server", "main.go")
|
||||
content, err := os.ReadFile(mainPath)
|
||||
if err != nil {
|
||||
@@ -131,14 +125,19 @@ func TestServerMustAbortOnMigrationFailure(t *testing.T) {
|
||||
}
|
||||
|
||||
src := string(content)
|
||||
|
||||
// The normal startup migration error handler (NOT --migrate flag) should abort
|
||||
// Currently it uses fmt.Printf("Warning:...") and continues
|
||||
if strings.Contains(src, `fmt.Printf("Warning: Migration failed`) {
|
||||
t.Errorf("[ERROR] [server] [database] normal startup swallows migration errors with Warning.\n"+
|
||||
"F-B1-11 P0: main.go must call log.Fatal or os.Exit(1) on migration failure.\n"+
|
||||
"The [OK] message must only print on genuine success.")
|
||||
migrationBlock := extractBlock(src, "db.Migrate(migrationsPath)", `migrations_complete`)
|
||||
if migrationBlock == "" {
|
||||
t.Fatal("[ERROR] [server] [database] cannot find migration block")
|
||||
}
|
||||
|
||||
if !strings.Contains(migrationBlock, "log.Fatalf") {
|
||||
t.Errorf("[ERROR] [server] [database] migration error handler must use log.Fatalf")
|
||||
}
|
||||
// Success message must only appear after the error check
|
||||
if strings.Contains(src, `fmt.Printf("Warning: Migration failed`) {
|
||||
t.Errorf("[ERROR] [server] [database] old warning pattern still present")
|
||||
}
|
||||
t.Log("[INFO] [server] [database] F-B1-11 FIXED: server aborts on migration failure")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -148,6 +147,7 @@ func TestServerMustAbortOnMigrationFailure(t *testing.T) {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestMigrationRunnerDetectsDuplicateNumbers(t *testing.T) {
|
||||
// POST-FIX (F-B1-13): No duplicate migration numbers should exist.
|
||||
migrationsPath := filepath.Join("migrations")
|
||||
files, err := os.ReadDir(migrationsPath)
|
||||
if err != nil {
|
||||
@@ -155,6 +155,7 @@ func TestMigrationRunnerDetectsDuplicateNumbers(t *testing.T) {
|
||||
}
|
||||
|
||||
// Extract numeric prefixes from .up.sql files
|
||||
// Note: "009b" and "012b" are distinct from "009" and "012" — not duplicates
|
||||
prefixCount := make(map[string][]string)
|
||||
for _, file := range files {
|
||||
if !strings.HasSuffix(file.Name(), ".up.sql") {
|
||||
@@ -167,20 +168,18 @@ func TestMigrationRunnerDetectsDuplicateNumbers(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Document duplicates
|
||||
duplicates := 0
|
||||
for prefix, names := range prefixCount {
|
||||
if len(names) > 1 {
|
||||
duplicates++
|
||||
t.Logf("[WARNING] [server] [database] duplicate migration number %s: %v", prefix, names)
|
||||
t.Errorf("[ERROR] [server] [database] duplicate migration prefix %s: %v", prefix, names)
|
||||
}
|
||||
}
|
||||
|
||||
if duplicates == 0 {
|
||||
t.Error("[ERROR] [server] [database] F-B1-13 already fixed: no duplicate migration numbers found")
|
||||
if duplicates > 0 {
|
||||
t.Errorf("[ERROR] [server] [database] F-B1-13 NOT FIXED: %d duplicates remain", duplicates)
|
||||
}
|
||||
|
||||
t.Logf("[INFO] [server] [database] F-B1-13 confirmed: %d duplicate migration numbers found", duplicates)
|
||||
t.Log("[INFO] [server] [database] F-B1-13 FIXED: no duplicate migration numbers")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -190,6 +189,8 @@ func TestMigrationRunnerDetectsDuplicateNumbers(t *testing.T) {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestMigrationRunnerShouldRejectDuplicateNumbers(t *testing.T) {
|
||||
// POST-FIX (F-B1-13): All migration prefixes are unique.
|
||||
// Duplicates resolved by renaming: 009→009b, 012→012b
|
||||
migrationsPath := filepath.Join("migrations")
|
||||
files, err := os.ReadDir(migrationsPath)
|
||||
if err != nil {
|
||||
@@ -209,11 +210,10 @@ func TestMigrationRunnerShouldRejectDuplicateNumbers(t *testing.T) {
|
||||
|
||||
for prefix, count := range prefixCount {
|
||||
if count > 1 {
|
||||
t.Errorf("[ERROR] [server] [database] migration number %s has %d files.\n"+
|
||||
"F-B1-13: each migration number must be unique.\n"+
|
||||
"After fix: renumber or merge duplicate migrations.", prefix, count)
|
||||
t.Errorf("[ERROR] [server] [database] migration prefix %s has %d files", prefix, count)
|
||||
}
|
||||
}
|
||||
t.Log("[INFO] [server] [database] F-B1-13 FIXED: all migration prefixes are unique")
|
||||
}
|
||||
|
||||
// extractBlock extracts text between two markers in a source string
|
||||
|
||||
Reference in New Issue
Block a user