From 7245ff66434a07b97e10ca03f8b9f4458941f78c Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Mon, 5 Oct 2026 13:56:23 +1100 Subject: [PATCH] Ignore status reports older than the stored generation --- internal/store/store.go | 12 ++++++++---- internal/store/store_integration_test.go | 22 +++++++++++++++++++++- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/internal/store/store.go b/internal/store/store.go index 6826890..a636262 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -281,13 +281,17 @@ func (s *Store) UpsertDevice(ctx context.Context, d model.Device) error { } // RecordDeviceStatus stores an agent's apply report. reported_generation only -// advances on applied; every report updates the last-status columns. +// advances on applied; the last-status columns only take reports at or above +// the stored status generation, so late or replayed reports never clobber newer state. func (s *Store) RecordDeviceStatus(ctx context.Context, name string, r model.StatusReport) error { tag, err := s.pool.Exec(ctx, ` UPDATE devices SET - reported_generation = CASE WHEN $2 = 'applied' THEN $3 ELSE reported_generation END, - status = $2, status_generation = $3, status_error = $4, - status_at = now(), last_seen = now() + reported_generation = CASE WHEN $2 = 'applied' THEN GREATEST(reported_generation, $3) ELSE reported_generation END, + status = CASE WHEN $3 >= status_generation THEN $2 ELSE status END, + status_error = CASE WHEN $3 >= status_generation THEN $4 ELSE status_error END, + status_at = CASE WHEN $3 >= status_generation THEN now() ELSE status_at END, + status_generation = GREATEST(status_generation, $3), + last_seen = now() WHERE name = $1`, name, string(r.Status), r.Generation, r.Error) if err != nil { diff --git a/internal/store/store_integration_test.go b/internal/store/store_integration_test.go index dae0157..1c0aa1f 100644 --- a/internal/store/store_integration_test.go +++ b/internal/store/store_integration_test.go @@ -196,7 +196,27 @@ func TestRecordDeviceStatus(t *testing.T) { t.Errorf("failed must not advance applied generation: %+v", got) } - if err := s.RecordDeviceStatus(ctx, "fw1", model.StatusReport{Status: "bogus", Generation: 9}); err == nil { + got = report(model.StatusReport{Status: model.StatusApplied, Generation: 9}) + if got.AppliedGeneration != 9 || got.Status != model.StatusApplied || got.Generation != 9 { + t.Errorf("applied 9: %+v", got) + } + + got = report(model.StatusReport{Status: model.StatusApplied, Generation: 4}) + if got.AppliedGeneration != 9 || got.Status != model.StatusApplied || got.Generation != 9 { + t.Errorf("older applied must not regress: %+v", got) + } + + got = report(model.StatusReport{Status: model.StatusReverted, Generation: 8, Error: "stale"}) + if got.AppliedGeneration != 9 || got.Status != model.StatusApplied || got.Generation != 9 || got.Error != "" { + t.Errorf("older reverted must not overwrite newer status: %+v", got) + } + + got = report(model.StatusReport{Status: model.StatusReverted, Generation: 9, Error: "lost api"}) + if got.AppliedGeneration != 9 || got.Status != model.StatusReverted || got.Generation != 9 || got.Error != "lost api" { + t.Errorf("same-generation reverted after applied must update: %+v", got) + } + + if err := s.RecordDeviceStatus(ctx, "fw1", model.StatusReport{Status: "bogus", Generation: 10}); err == nil { t.Error("db should reject unknown status") } if err := s.RecordDeviceStatus(ctx, "nope", model.StatusReport{Status: model.StatusApplied}); err != store.ErrNotFound {