@@ -217,6 +217,147 @@ func TestConcurrentSnapshotInvariant(t *testing.T) {
217217 }
218218}
219219
220+ func TestReplayInvariantValidationErrors (t * testing.T ) {
221+ tests := []struct {
222+ name string
223+ check func (Snapshot ) error
224+ snapshot Snapshot
225+ want string
226+ }{
227+ {
228+ name : "summary replay session count" , check : validateSummaryReplayWindow ,
229+ want : "session count" ,
230+ },
231+ {
232+ name : "summary replay events" , check : validateSummaryReplayWindow ,
233+ snapshot : Snapshot {Sessions : []SessionSnapshot {{ID : standardSessionID }}},
234+ want : "retained events" ,
235+ },
236+ {
237+ name : "event count" , check : validateEvents (eventExpectation {content : "expected" }),
238+ snapshot : Snapshot {Sessions : []SessionSnapshot {{ID : standardSessionID }}},
239+ want : "event count" ,
240+ },
241+ {
242+ name : "tool event count" , check : validateToolCall ,
243+ snapshot : Snapshot {Sessions : []SessionSnapshot {{ID : standardSessionID }}},
244+ want : "event count" ,
245+ },
246+ {
247+ name : "state value" , check : validateStateUpdate ,
248+ snapshot : Snapshot {Sessions : []SessionSnapshot {{ID : standardSessionID }}},
249+ want : "state" ,
250+ },
251+ {
252+ name : "summary update session count" , check : validateSummaryUpdate ,
253+ want : "session count" ,
254+ },
255+ {
256+ name : "tracks missing" , check : validateTracks ,
257+ snapshot : Snapshot {Sessions : []SessionSnapshot {{ID : standardSessionID }}},
258+ want : "tracks" ,
259+ },
260+ {
261+ name : "concurrent unknown session" , check : validateConcurrentSnapshot ,
262+ snapshot : Snapshot {Sessions : []SessionSnapshot {{ID : "unknown" }, {ID : "session-2" }}},
263+ want : "unexpected session" ,
264+ },
265+ {
266+ name : "recovery event contents" , check : validateRecoverySnapshot ,
267+ snapshot : Snapshot {
268+ Sessions : []SessionSnapshot {{
269+ Events : []EventSnapshot {{Content : "wrong" }, {Content : "retried" }},
270+ State : map [string ]StateValueSnapshot {"status" : JSONStateValue ("recovered" )},
271+ }},
272+ Memories : []MemorySnapshot {{}},
273+ },
274+ want : "event contents" ,
275+ },
276+ {
277+ name : "wrong only session id" ,
278+ check : func (snapshot Snapshot ) error {
279+ _ , err := onlySession (snapshot , standardSessionID )
280+ return err
281+ },
282+ snapshot : Snapshot {Sessions : []SessionSnapshot {{ID : "wrong" }}},
283+ want : "session id" ,
284+ },
285+ {
286+ name : "missing named session" ,
287+ check : func (snapshot Snapshot ) error {
288+ _ , err := findSessionSnapshot (snapshot , standardSessionID )
289+ return err
290+ },
291+ want : "not found" ,
292+ },
293+ }
294+ for _ , test := range tests {
295+ t .Run (test .name , func (t * testing.T ) {
296+ err := test .check (test .snapshot )
297+ if err == nil || ! strings .Contains (err .Error (), test .want ) {
298+ t .Fatalf ("invariant error = %v, want %q" , err , test .want )
299+ }
300+ })
301+ }
302+ }
303+
304+ func TestValidateSummaryRejectsInvalidPersistedFields (t * testing.T ) {
305+ valid := SummarySnapshot {
306+ SessionID : standardSessionID ,
307+ FilterKey : "branch/main" ,
308+ Text : "summary" ,
309+ Version : 1 ,
310+ UpdatedAt : standardTime ,
311+ Boundary : map [string ]any {
312+ "filter_key" : "branch/main" , "last_event_id" : "event-1" , "cutoff_at" : standardTime ,
313+ },
314+ }
315+ tests := []struct {
316+ name string
317+ mutate func (* SummarySnapshot )
318+ want string
319+ }{
320+ {name : "main fields" , mutate : func (summary * SummarySnapshot ) {
321+ summary .Version = 0
322+ }, want : "summary =" },
323+ {name : "boundary filter" , mutate : func (summary * SummarySnapshot ) {
324+ summary .Boundary ["filter_key" ] = "wrong"
325+ }, want : "filter_key" },
326+ {name : "boundary event" , mutate : func (summary * SummarySnapshot ) {
327+ delete (summary .Boundary , "last_event_id" )
328+ }, want : "last_event_id" },
329+ {name : "boundary cutoff" , mutate : func (summary * SummarySnapshot ) {
330+ delete (summary .Boundary , "cutoff_at" )
331+ }, want : "cutoff_at" },
332+ }
333+ for _ , test := range tests {
334+ t .Run (test .name , func (t * testing.T ) {
335+ summary := valid
336+ summary .Boundary = make (map [string ]any , len (valid .Boundary ))
337+ for key , value := range valid .Boundary {
338+ summary .Boundary [key ] = value
339+ }
340+ test .mutate (& summary )
341+ err := validateSummary (summary , standardSessionID , "summary" )
342+ if err == nil || ! strings .Contains (err .Error (), test .want ) {
343+ t .Fatalf ("validateSummary() error = %v, want %q" , err , test .want )
344+ }
345+ })
346+ }
347+ }
348+
349+ func TestSameStringSetRejectsInvalidValues (t * testing.T ) {
350+ if sameStringSet ([]any {"one" }, "one" , "two" ) {
351+ t .Fatal ("sameStringSet() accepted mismatched lengths" )
352+ }
353+ if sameStringSet ([]any {1 }, "one" ) {
354+ t .Fatal ("sameStringSet() accepted a non-string value" )
355+ }
356+ if sameStringSet ([]any {"two" }, "one" ) {
357+ t .Fatal ("sameStringSet() accepted an unexpected string" )
358+ }
359+ }
360+
220361func replayCaseByName (t * testing.T , cases []ReplayCase , name string ) ReplayCase {
221362 t .Helper ()
222363 for _ , replayCase := range cases {
0 commit comments