@@ -477,3 +477,177 @@ func TestEngineApplySnapshot_Concurrent(t *testing.T) {
477477 t .Fatalf ("expected route group %d, got %d" , maxVersion , route .GroupID )
478478 }
479479}
480+
481+ // TestEngineSnapshotAt_RecordsApplySnapshot is the M2 round-trip
482+ // witness for the Composed-1 versioned-snapshot ring (design doc
483+ // §M2): every successful ApplySnapshot records the (version, routes)
484+ // pair so SnapshotAt(v) can resolve OwnerOf(k) for any in-flight
485+ // transaction that observed v at BeginTxn.
486+ func TestEngineSnapshotAt_RecordsApplySnapshot (t * testing.T ) {
487+ e := NewEngine ()
488+ if err := e .ApplySnapshot (CatalogSnapshot {
489+ Version : 1 ,
490+ Routes : []RouteDescriptor {
491+ {RouteID : 10 , Start : []byte ("" ), End : []byte ("m" ), GroupID : 7 , State : RouteStateActive },
492+ {RouteID : 11 , Start : []byte ("m" ), End : nil , GroupID : 9 , State : RouteStateActive },
493+ },
494+ }); err != nil {
495+ t .Fatalf ("apply snapshot v1: %v" , err )
496+ }
497+
498+ snap , ok := e .SnapshotAt (1 )
499+ if ! ok {
500+ t .Fatal ("expected SnapshotAt(1) to return the v1 snapshot" )
501+ }
502+ if snap .Version () != 1 {
503+ t .Fatalf ("expected snapshot version 1, got %d" , snap .Version ())
504+ }
505+ if owner , found := snap .OwnerOf ([]byte ("a" )); ! found || owner != 7 {
506+ t .Fatalf ("expected key 'a' owner=7 in v1 snapshot; got owner=%d found=%v" , owner , found )
507+ }
508+ if owner , found := snap .OwnerOf ([]byte ("z" )); ! found || owner != 9 {
509+ t .Fatalf ("expected key 'z' owner=9 in v1 snapshot; got owner=%d found=%v" , owner , found )
510+ }
511+ }
512+
513+ // TestEngineSnapshotAt_PreservesHistoryAcrossVersions verifies the
514+ // M3-critical property: after ApplySnapshot has moved the catalog
515+ // forward, a SnapshotAt for the PRIOR version still returns the old
516+ // routes. Without this, the M3 verifyComposed1 gate could not
517+ // resolve a txn whose observedVer is behind the current catalog —
518+ // exactly the case the design doc §3 G1c trace requires.
519+ func TestEngineSnapshotAt_PreservesHistoryAcrossVersions (t * testing.T ) {
520+ e := NewEngine ()
521+ if err := e .ApplySnapshot (CatalogSnapshot {
522+ Version : 1 ,
523+ Routes : []RouteDescriptor {
524+ {RouteID : 10 , Start : []byte ("" ), End : nil , GroupID : 1 , State : RouteStateActive },
525+ },
526+ }); err != nil {
527+ t .Fatalf ("apply v1: %v" , err )
528+ }
529+ if err := e .ApplySnapshot (CatalogSnapshot {
530+ Version : 2 ,
531+ Routes : []RouteDescriptor {
532+ {RouteID : 11 , Start : []byte ("" ), End : nil , GroupID : 2 , State : RouteStateActive },
533+ },
534+ }); err != nil {
535+ t .Fatalf ("apply v2: %v" , err )
536+ }
537+
538+ snapV1 , ok := e .SnapshotAt (1 )
539+ if ! ok {
540+ t .Fatal ("expected v1 still in history after v2 applied" )
541+ }
542+ if owner , _ := snapV1 .OwnerOf ([]byte ("k" )); owner != 1 {
543+ t .Fatalf ("v1 snapshot must still show group=1 owner; got %d" , owner )
544+ }
545+ snapV2 , ok := e .SnapshotAt (2 )
546+ if ! ok {
547+ t .Fatal ("expected v2 in history" )
548+ }
549+ if owner , _ := snapV2 .OwnerOf ([]byte ("k" )); owner != 2 {
550+ t .Fatalf ("v2 snapshot must show group=2 owner; got %d" , owner )
551+ }
552+ }
553+
554+ // TestEngineSnapshotAt_FIFOEviction verifies that the ring respects
555+ // historyDepth: once more than depth versions have been applied, the
556+ // oldest is evicted and SnapshotAt returns (zero, false) for it.
557+ // The M3 gate (design doc §4.3) treats the not-found case as a hard
558+ // retryable error, so retention depth is a liveness knob — this
559+ // test pins the eviction order so a future depth change does not
560+ // silently break the M3 contract.
561+ func TestEngineSnapshotAt_FIFOEviction (t * testing.T ) {
562+ t .Parallel ()
563+ e := NewEngine ()
564+ // Force a tiny depth so the test is bounded and explicit. The
565+ // direct field write is safe because `e` is local to this test
566+ // goroutine and the depth is set before any ApplySnapshot fires;
567+ // once the Engine is published to concurrent readers, the depth
568+ // would have to flow through a constructor option (claude review
569+ // on PR #894 — fragile-but-test-local lock contract).
570+ e .historyDepth = 3
571+
572+ for v := uint64 (1 ); v <= 5 ; v ++ {
573+ if err := e .ApplySnapshot (CatalogSnapshot {
574+ Version : v ,
575+ Routes : []RouteDescriptor {
576+ {RouteID : v , Start : []byte ("" ), End : nil , GroupID : v , State : RouteStateActive },
577+ },
578+ }); err != nil {
579+ t .Fatalf ("apply v%d: %v" , v , err )
580+ }
581+ }
582+
583+ if _ , ok := e .SnapshotAt (1 ); ok {
584+ t .Fatal ("v1 must be evicted (oldest) under depth=3 after v2..v5 applied" )
585+ }
586+ if _ , ok := e .SnapshotAt (2 ); ok {
587+ t .Fatal ("v2 must be evicted (second-oldest) under depth=3 after v3..v5 applied" )
588+ }
589+ for v := uint64 (3 ); v <= 5 ; v ++ {
590+ snap , ok := e .SnapshotAt (v )
591+ if ! ok {
592+ t .Fatalf ("expected v%d retained (depth=3 keeps the 3 most recent)" , v )
593+ }
594+ if owner , _ := snap .OwnerOf ([]byte ("k" )); owner != v {
595+ t .Fatalf ("v%d snapshot must show group=%d owner; got %d" , v , v , owner )
596+ }
597+ }
598+ }
599+
600+ // TestEngineSnapshotAt_UnknownVersionReturnsNotFound documents the
601+ // M3-relevant contract: a version that has never been applied (e.g.
602+ // in the future, or a typo) returns (zero, false). The M3 gate
603+ // uses this signal to emit ErrComposed1VersionGCd and trigger a
604+ // coordinator retry.
605+ func TestEngineSnapshotAt_UnknownVersionReturnsNotFound (t * testing.T ) {
606+ e := NewEngineWithDefaultRoute ()
607+ if _ , ok := e .SnapshotAt (42 ); ok {
608+ t .Fatal ("expected SnapshotAt for an unknown version to return false" )
609+ }
610+ }
611+
612+ // TestEngineSnapshotAt_SeedsVersionZeroForDefaultRoute verifies that
613+ // the NewEngineWithDefaultRoute path records the version-0 default
614+ // route snapshot. Without this, every txn that observed v=0 (the
615+ // common case before any ApplySnapshot lands) would fall through
616+ // to the M3 not-found path and trigger a spurious retry on its first
617+ // commit.
618+ func TestEngineSnapshotAt_SeedsVersionZeroForDefaultRoute (t * testing.T ) {
619+ e := NewEngineWithDefaultRoute ()
620+ snap , ok := e .SnapshotAt (0 )
621+ if ! ok {
622+ t .Fatal ("expected NewEngineWithDefaultRoute to seed a v0 history entry" )
623+ }
624+ if snap .Version () != 0 {
625+ t .Fatalf ("expected seed snapshot version 0, got %d" , snap .Version ())
626+ }
627+ // Default route covers the full keyspace ⇒ every key resolves to
628+ // defaultGroupID at v0.
629+ owner , ok := snap .OwnerOf ([]byte ("anything" ))
630+ if ! ok || owner != defaultGroupID {
631+ t .Fatalf ("expected v0 snapshot to resolve every key to defaultGroupID=%d; got owner=%d found=%v" , defaultGroupID , owner , ok )
632+ }
633+ }
634+
635+ // TestEngineSnapshotAt_BareEngineHasNoHistory documents that an
636+ // Engine constructed via the bare struct literal (e.g. via internal
637+ // test seams) has a nil history map and SnapshotAt always returns
638+ // (zero, false). recordHistorySnapshot is nil-safe so ApplySnapshot
639+ // still works on such an engine, but the M3 gate will treat every
640+ // SnapshotAt as "not in ring" → soft-fail-as-retry, which is the
641+ // correct posture for unconfigured engines.
642+ func TestEngineSnapshotAt_BareEngineHasNoHistory (t * testing.T ) {
643+ e := & Engine {routes : make ([]Route , 0 )} // bare struct literal — no history
644+ if err := e .ApplySnapshot (CatalogSnapshot {
645+ Version : 1 ,
646+ Routes : []RouteDescriptor {{RouteID : 1 , Start : []byte ("" ), End : nil , GroupID : 1 , State : RouteStateActive }},
647+ }); err != nil {
648+ t .Fatalf ("apply on bare engine should succeed: %v" , err )
649+ }
650+ if _ , ok := e .SnapshotAt (1 ); ok {
651+ t .Fatal ("bare engine has no history ring; SnapshotAt should always be false" )
652+ }
653+ }
0 commit comments