package model import ( "fmt" "testing" "time" ) // The model package shares a single in-memory SQLite across all tests // via TestMain in task_cas_test.go. We MUST NOT swap DB out or null // it in Cleanup — doing so wedges every other test in the package. // // Strategy: AutoMigrate the audit table once (idempotent on SQLite), // then DELETE rows at the top of each test to isolate cases. func setupAuditTest(t *testing.T) { t.Helper() if err := DB.AutoMigrate(&AgentAuditEvent{}); err != nil { t.Fatalf("migrate: %v", err) } if err := DB.Exec("DELETE FROM agent_audit_events").Error; err != nil { t.Fatalf("truncate: %v", err) } } func TestInsertAgentAuditEvent_PersistsRow(t *testing.T) { // Regression for H1 — the previous implementation appended to an // in-process map that was wiped on every container restart. This // test pins the new behaviour: rows survive in DB. setupAuditTest(t) evt := &AgentAuditEvent{ EventID: "evt_test_001", Event: "deployment.accepted", Actor: "agent_control_plane", Resource: "dep_abc", UserID: "user-42", ChannelID: "channel-1", BindingScope: "project-main", DeploymentID: "dep_abc", CorrelationID: "corr-xyz", RequestID: "req-001", } InsertAgentAuditEvent(evt) var got AgentAuditEvent if err := DB.Where("event_id = ?", "evt_test_001").First(&got).Error; err != nil { t.Fatalf("not persisted: %v", err) } if got.Result != "ok" { t.Errorf("default Result should be 'ok', got %q", got.Result) } if got.SchemaVersion != 1 { t.Errorf("default SchemaVersion should be 1, got %d", got.SchemaVersion) } if got.OccurredAt == 0 { t.Errorf("OccurredAt should be auto-stamped, got 0") } } func TestInsertAgentAuditEvent_NilGuards(t *testing.T) { // Production safety: audit writes run inside hot paths (right // after a deployment mutation). A nil DB or nil event MUST NOT // panic — better to drop the audit row than to fail the user // API call. defer func() { if r := recover(); r != nil { t.Fatalf("InsertAgentAuditEvent panicked: %v", r) } }() InsertAgentAuditEvent(nil) // nil evt prev := DB DB = nil InsertAgentAuditEvent(&AgentAuditEvent{EventID: "x"}) // nil DB DB = prev } func TestListAgentAuditEvents_FilterAndPaginate(t *testing.T) { setupAuditTest(t) now := time.Now().UnixMilli() for i := 0; i < 5; i++ { uid := "user-A" if i%2 == 0 { uid = "user-B" } InsertAgentAuditEvent(&AgentAuditEvent{ EventID: fmt.Sprintf("evt_%d", i), Event: "deployment.accepted", UserID: uid, DeploymentID: "dep_X", OccurredAt: now + int64(i)*1000, }) } // All rows visible without filter. rows, total, err := ListAgentAuditEvents(ListAgentAuditEventsFilter{}) if err != nil { t.Fatalf("list: %v", err) } if total != 5 || len(rows) != 5 { t.Errorf("expected 5 rows, got total=%d len=%d", total, len(rows)) } // user_id filter narrows to 3 (i=0,2,4 → user-B). _, total, err = ListAgentAuditEvents(ListAgentAuditEventsFilter{UserID: "user-B"}) if err != nil { t.Fatalf("list with userid: %v", err) } if total != 3 { t.Errorf("user-B should have 3 rows, got %d", total) } // Newest-first ordering. evt_4 inserted last → top. rows, _, _ = ListAgentAuditEvents(ListAgentAuditEventsFilter{}) if rows[0].EventID != "evt_4" { t.Errorf("expected newest-first, got %q on top", rows[0].EventID) } } func TestListAgentAuditEventsByDeployment_Chronological(t *testing.T) { // Detail-drawer reads need oldest-first for a top-to-bottom // timeline. Confirms ascending order independent of insertion // order. setupAuditTest(t) InsertAgentAuditEvent(&AgentAuditEvent{ EventID: "evt_late", Event: "x", DeploymentID: "dep_T", OccurredAt: 9000, }) InsertAgentAuditEvent(&AgentAuditEvent{ EventID: "evt_early", Event: "x", DeploymentID: "dep_T", OccurredAt: 1000, }) rows, err := ListAgentAuditEventsByDeployment("dep_T") if err != nil { t.Fatalf("query: %v", err) } if len(rows) != 2 || rows[0].EventID != "evt_early" { t.Fatalf("expected [evt_early, evt_late] order, got %+v", rows) } }