From 11c45a66f7345fa0e7fe31e1ca91f37a4982d306 Mon Sep 17 00:00:00 2001 From: Alan Buscaglia Date: Fri, 29 May 2026 13:07:06 +0200 Subject: [PATCH] fix(sync): re-export edited observations by checking UpdatedAt in filterNewData (#447) Observations edited via mem_update have UpdatedAt > CreatedAt but unchanged CreatedAt, so the previous cutoff check (CreatedAt only) silently dropped them from every subsequent sync. Include the observation when either CreatedAt or UpdatedAt is after the cutoff so edits are always re-exported. --- internal/sync/sync.go | 2 +- internal/sync/sync_test.go | 49 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/internal/sync/sync.go b/internal/sync/sync.go index 48ed7f85..629a4a1f 100644 --- a/internal/sync/sync.go +++ b/internal/sync/sync.go @@ -1112,7 +1112,7 @@ func (sy *Syncer) filterNewData(data *store.ExportData, lastChunkTime string) *C } for _, o := range data.Observations { - if normalizeTime(o.CreatedAt) > cutoff { + if normalizeTime(o.CreatedAt) > cutoff || normalizeTime(o.UpdatedAt) > cutoff { chunk.Observations = append(chunk.Observations, o) } } diff --git a/internal/sync/sync_test.go b/internal/sync/sync_test.go index b8047c85..99dfe39b 100644 --- a/internal/sync/sync_test.go +++ b/internal/sync/sync_test.go @@ -2198,6 +2198,55 @@ func TestFilterFunctionsAndTimeNormalization(t *testing.T) { } } +// TestFilterNewDataIncludesEditedObservations verifies that an observation whose +// CreatedAt is before the sync cutoff but whose UpdatedAt is after the cutoff is +// included in the filtered export (issue #447). +func TestFilterNewDataIncludesEditedObservations(t *testing.T) { + data := &store.ExportData{ + Version: "0.1.0", + ExportedAt: "2025-01-01 00:00:00", + Observations: []store.Observation{ + // created before cutoff, never edited -> should be EXCLUDED + {ID: 1, SessionID: "s1", CreatedAt: "2025-01-01 09:00:00", UpdatedAt: "2025-01-01 09:00:00"}, + // created before cutoff, edited AFTER cutoff -> should be INCLUDED + {ID: 2, SessionID: "s1", CreatedAt: "2025-01-01 09:00:00", UpdatedAt: "2025-01-01 11:00:00"}, + // created after cutoff -> should be INCLUDED (existing behaviour) + {ID: 3, SessionID: "s1", CreatedAt: "2025-01-01 11:00:00", UpdatedAt: "2025-01-01 11:00:00"}, + }, + } + + cutoff := "2025-01-01T10:30:00Z" + sy := New(nil, t.TempDir()) + filtered := sy.filterNewData(data, cutoff) + + ids := make([]int64, 0, len(filtered.Observations)) + for _, o := range filtered.Observations { + ids = append(ids, o.ID) + } + + // ID 1 must be absent; IDs 2 and 3 must be present. + for _, id := range ids { + if id == 1 { + t.Fatalf("filterNewData included observation ID 1 (stale, unedited) — should have been excluded; ids=%v", ids) + } + } + found2, found3 := false, false + for _, id := range ids { + if id == 2 { + found2 = true + } + if id == 3 { + found3 = true + } + } + if !found2 { + t.Fatalf("filterNewData excluded observation ID 2 (edited after cutoff) — should have been included; ids=%v", ids) + } + if !found3 { + t.Fatalf("filterNewData excluded observation ID 3 (created after cutoff) — should have been included; ids=%v", ids) + } +} + func TestFilterByProjectEntityLevel(t *testing.T) { projA := "proj-a"