mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Normalize stale Patrol acknowledgement state on load
The findings store now clears acknowledgement timestamps that predate a recorded regression when persisted findings are loaded, persists the normalized state, and keeps regression lifecycle metadata explicit.
This commit is contained in:
@@ -511,7 +511,10 @@ Patrol actually succeeds or resolves them for a Patrol-owned reason.
|
||||
The shared findings lifecycle must also treat a regressed issue as a new active
|
||||
occurrence. When a resolved finding reappears, `internal/ai/findings.go` must
|
||||
clear any stale acknowledgement timestamp from the prior occurrence instead of
|
||||
carrying that acknowledgement forward onto the regressed active issue.
|
||||
carrying that acknowledgement forward onto the regressed active issue. The
|
||||
same owner must normalize already-persisted active findings on load when a
|
||||
stored acknowledgement predates the last recorded regression, then persist the
|
||||
cleaned state back through the canonical findings store.
|
||||
AI chat tool-name labels, pending-tool headers, and assistant status copy now
|
||||
also route through the shared frontend identifier-label helper, so the chat
|
||||
surfaces do not keep their own underscore-stripping behavior separate from
|
||||
|
||||
@@ -658,6 +658,7 @@ func (s *FindingsStore) SetPersistence(p FindingsPersistence) error {
|
||||
// Reset derived indices/caches before rehydrating.
|
||||
s.byResource = make(map[string][]string)
|
||||
s.activeCounts = make(map[FindingSeverity]int)
|
||||
normalizedLoadedState := false
|
||||
if rules != nil {
|
||||
s.suppressionRules = make(map[string]*SuppressionRule, len(rules))
|
||||
for id, r := range rules {
|
||||
@@ -672,6 +673,9 @@ func (s *FindingsStore) SetPersistence(p FindingsPersistence) error {
|
||||
if f == nil {
|
||||
continue
|
||||
}
|
||||
if normalizeLoadedFinding(f) {
|
||||
normalizedLoadedState = true
|
||||
}
|
||||
// Ensure derived fields are consistent after load.
|
||||
f.syncLoopState()
|
||||
s.findings[id] = f
|
||||
@@ -681,6 +685,9 @@ func (s *FindingsStore) SetPersistence(p FindingsPersistence) error {
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
if normalizedLoadedState {
|
||||
s.scheduleSave()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -866,6 +873,19 @@ func isAllowedLoopTransition(from, to string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeLoadedFinding(f *Finding) bool {
|
||||
if f == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if f.ResolvedAt == nil && f.LastRegressionAt != nil && f.AcknowledgedAt != nil && !f.AcknowledgedAt.After(*f.LastRegressionAt) {
|
||||
f.AcknowledgedAt = nil
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// syncLoopStateLocked recomputes loop state and records a lifecycle event if it changed.
|
||||
// Caller must hold s.mu.
|
||||
func (s *FindingsStore) syncLoopStateLocked(f *Finding) {
|
||||
|
||||
@@ -78,6 +78,51 @@ func TestFindingsStore_SetPersistence_LoadsFindings(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindingsStore_SetPersistence_NormalizesRegressedAcknowledgementState(t *testing.T) {
|
||||
store := NewFindingsStore()
|
||||
store.saveDebounce = 5 * time.Millisecond
|
||||
now := time.Now()
|
||||
acknowledgedAt := now.Add(-2 * time.Hour)
|
||||
lastRegressionAt := now.Add(-time.Hour)
|
||||
saved := make(chan map[string]*Finding, 1)
|
||||
|
||||
p := &recordingPersistence{
|
||||
findings: map[string]*Finding{
|
||||
"regressed": {
|
||||
ID: "regressed",
|
||||
Severity: FindingSeverityWarning,
|
||||
ResourceID: "res-3",
|
||||
Title: "Regressed",
|
||||
LastSeenAt: now,
|
||||
AcknowledgedAt: &acknowledgedAt,
|
||||
LastRegressionAt: &lastRegressionAt,
|
||||
},
|
||||
},
|
||||
saved: saved,
|
||||
}
|
||||
|
||||
if err := store.SetPersistence(p); err != nil {
|
||||
t.Fatalf("SetPersistence failed: %v", err)
|
||||
}
|
||||
|
||||
got := store.Get("regressed")
|
||||
if got == nil {
|
||||
t.Fatal("expected finding to load")
|
||||
}
|
||||
if got.AcknowledgedAt != nil {
|
||||
t.Fatal("expected stale acknowledgement to be cleared on load")
|
||||
}
|
||||
|
||||
select {
|
||||
case persisted := <-saved:
|
||||
if persisted["regressed"].AcknowledgedAt != nil {
|
||||
t.Fatal("expected normalized acknowledgement state to be persisted")
|
||||
}
|
||||
case <-time.After(200 * time.Millisecond):
|
||||
t.Fatal("timed out waiting for normalized findings save")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindingsStore_scheduleSave_NoPersistence(t *testing.T) {
|
||||
store := NewFindingsStore()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user