package store_test // Phase 6 Slice B — parity test between the legacy pgx-against-projax- // items *Store and the new pgx-against-mbrian *MBrianReader. // // Skipped without SUPABASE_DATABASE_URL set. When run against the live // post-migration database, every comparison should hold: the adapter // must be a faithful translator of the migrated graph for projax UI // consumers. import ( "context" "errors" "os" "sort" "testing" "time" "github.com/jackc/pgx/v5/pgxpool" "github.com/m/projax/store" ) func newPair(t *testing.T) (*store.Store, *store.MBrianReader, *pgxpool.Pool) { t.Helper() url := os.Getenv("SUPABASE_DATABASE_URL") if url == "" { url = os.Getenv("PROJAX_DB_URL") } if url == "" { t.Skip("set SUPABASE_DATABASE_URL") } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() pool, err := pgxpool.New(ctx, url) if err != nil { t.Fatalf("pool: %v", err) } if err := pool.Ping(ctx); err != nil { t.Skipf("DB unreachable: %v", err) } return store.New(pool), store.NewMBrianReader(pool), pool } // TestParityListAll: both readers return the same set of items by slug. // Field-by-field equality is asserted for slug/title/status/tags/management/ // public/parent count/paths — the consumer-facing surface. func TestParityListAll(t *testing.T) { s, r, pool := newPair(t) defer pool.Close() ctx := context.Background() leg, err := s.ListAll(ctx) if err != nil { t.Fatalf("store ListAll: %v", err) } mb, err := r.ListAll(ctx) if err != nil { t.Fatalf("mbrian ListAll: %v", err) } if len(leg) != len(mb) { t.Fatalf("count mismatch: store=%d mbrian=%d", len(leg), len(mb)) } // Per the migration brief, two projax-side squatter slugs were // renamed-aside (not deleted) so mBrian could take the canonical // 'work' (area) + 'dania' (project) slugs. Compare every slug that // resolves in BOTH sets; the squatters surface as legacy-only. skip := map[string]bool{"work": true, "dania": true} legBySlug := bySlug(leg) mbBySlug := bySlug(mb) for slug, l := range legBySlug { if skip[slug] { continue } m, ok := mbBySlug[slug] if !ok { t.Errorf("slug %q missing in mBrian set", slug) continue } if l.Title != m.Title { t.Errorf("%s title: store=%q mbrian=%q", slug, l.Title, m.Title) } if l.Status != m.Status { t.Errorf("%s status: store=%q mbrian=%q", slug, l.Status, m.Status) } if !sameSet(l.Tags, m.Tags) { t.Errorf("%s tags: store=%v mbrian=%v", slug, l.Tags, m.Tags) } if !sameSet(l.Management, m.Management) { t.Errorf("%s management: store=%v mbrian=%v", slug, l.Management, m.Management) } if l.Public != m.Public { t.Errorf("%s public: store=%v mbrian=%v", slug, l.Public, m.Public) } if len(l.ParentIDs) != len(m.ParentIDs) { t.Errorf("%s parent count: store=%d mbrian=%d", slug, len(l.ParentIDs), len(m.ParentIDs)) } if !sameSet(l.Paths, m.Paths) { t.Errorf("%s paths: store=%v mbrian=%v", slug, l.Paths, m.Paths) } } } // TestParitySpotChecks asserts the 5 spot-check items resolve identically // through both readers — root area / single-parent / multi-parent / // caldav-linked / public-listing populated. func TestParitySpotChecks(t *testing.T) { s, r, pool := newPair(t) defer pool.Close() ctx := context.Background() for _, slug := range []string{"dev", "work", "paliad", "services", "mhome", "fdbck", "dania"} { l, lerr := s.GetByPathOrSlug(ctx, slug) m, merr := r.GetByPathOrSlug(ctx, slug) if lerr != nil && merr != nil { // Both 404 — consistent. continue } if lerr != nil { t.Errorf("%s: store err=%v but mbrian found", slug, lerr) continue } if merr != nil { t.Errorf("%s: mbrian err=%v but store found", slug, merr) continue } if l.Slug != m.Slug || l.Title != m.Title { t.Errorf("%s: shape mismatch store=%+v mbrian=%+v", slug, l.Slug+"/"+l.Title, m.Slug+"/"+m.Title) } } } // TestParityCalDAVLinks: the single caldav-list link must round-trip. func TestParityCalDAVLinks(t *testing.T) { s, r, pool := newPair(t) defer pool.Close() ctx := context.Background() leg, err := s.LinksByRefType(ctx, "caldav-list") if err != nil { t.Fatalf("store: %v", err) } mb, err := r.LinksByRefType(ctx, "caldav-list") if err != nil { t.Fatalf("mbrian: %v", err) } if len(leg) != len(mb) { t.Errorf("count: store=%d mbrian=%d", len(leg), len(mb)) } // The URLs must round-trip identically. Match by ref_id. legByRef := map[string]*store.ItemLink{} for _, l := range leg { legByRef[l.RefID] = l } for _, m := range mb { l, ok := legByRef[m.RefID] if !ok { t.Errorf("mbrian caldav RefID %q not in store set", m.RefID) continue } if l.Rel != m.Rel { t.Errorf("caldav %s rel: store=%q mbrian=%q", m.RefID, l.Rel, m.Rel) } } } // TestParityGiteaRepoLinks — same parity check for the 37 gitea-repo edges. func TestParityGiteaRepoLinks(t *testing.T) { s, r, pool := newPair(t) defer pool.Close() ctx := context.Background() leg, err := s.LinksByRefType(ctx, "gitea-repo") if err != nil { t.Fatalf("store: %v", err) } mb, err := r.LinksByRefType(ctx, "gitea-repo") if err != nil { t.Fatalf("mbrian: %v", err) } if len(leg) != len(mb) { t.Errorf("count: store=%d mbrian=%d", len(leg), len(mb)) } legSeen := map[string]bool{} for _, l := range leg { legSeen[l.RefID] = true } for _, m := range mb { if !legSeen[m.RefID] { t.Errorf("mbrian gitea-repo RefID %q not in store set", m.RefID) } } } // TestParityAllTags: tag union must match (modulo ordering). func TestParityAllTags(t *testing.T) { s, r, pool := newPair(t) defer pool.Close() ctx := context.Background() leg, err := s.AllTags(ctx) if err != nil { t.Fatalf("store: %v", err) } mb, err := r.AllTags(ctx) if err != nil { t.Fatalf("mbrian: %v", err) } if !sameSet(leg, mb) { t.Errorf("AllTags mismatch:\n store=%v\n mbrian=%v", leg, mb) } } // TestParityNotFound: an unknown slug must 404 from both. func TestParityNotFound(t *testing.T) { s, r, pool := newPair(t) defer pool.Close() ctx := context.Background() _, le := s.GetByPathOrSlug(ctx, "definitely-not-a-real-slug-zzzzz") _, me := r.GetByPathOrSlug(ctx, "definitely-not-a-real-slug-zzzzz") if !errors.Is(le, store.ErrNotFound) { t.Errorf("store should ErrNotFound, got %v", le) } if !errors.Is(me, store.ErrNotFound) { t.Errorf("mbrian should ErrNotFound, got %v", me) } } // --- helpers --- func bySlug(items []*store.Item) map[string]*store.Item { out := map[string]*store.Item{} for _, it := range items { out[it.Slug] = it } return out } func sameSet(a, b []string) bool { if len(a) != len(b) { return false } ac := append([]string{}, a...) bc := append([]string{}, b...) sort.Strings(ac) sort.Strings(bc) for i := range ac { if ac[i] != bc[i] { return false } } return true }