cart: back projection cache with platform/catalog.ProjectionStore
Consumer side of the catalog-projection seam (C1): - catalogProjectionCache now wraps ProjectionStore[catalog.Projection] (identity transform — the cart's overlay uses ~all fields); delivery handler routes bus frames via HandleFrame (snapshot begin|chunk|end + epoch-stamped deltas). - Per-pod cold-start-ready .bin under CART_PROJECTION_DIR (pod-local, never shared) + IsDeleted tombstone passthrough; HTTP fetch demoted to last-resort. - Tests: test-only Apply shim + mustCache helper keep existing cases green; new snapshot-framing integration test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,7 @@ import (
|
||||
// TestProjectionCache_ApplyUpserts covers the happy path: a batch of N
|
||||
// projections reaches the cache via Apply, all are visible via Get.
|
||||
func TestProjectionCache_ApplyUpserts(t *testing.T) {
|
||||
c := newCatalogProjectionCache()
|
||||
c := mustCache(t)
|
||||
updates := []catalog.ProjectionUpdate{
|
||||
{Projection: catalog.Projection{ID: "id-A", SKU: "A", PriceIncVat: money.Cents(100_00), TaxClass: "standard"}},
|
||||
{Projection: catalog.Projection{ID: "id-B", SKU: "B", PriceIncVat: money.Cents(150_00), TaxClass: "reduced"}},
|
||||
@@ -48,7 +48,7 @@ func TestProjectionCache_ApplyUpserts(t *testing.T) {
|
||||
// - IsDeleted returns true
|
||||
// - the same entry continues to count toward Len (size accounting)
|
||||
func TestProjectionCache_DeleteTombstone(t *testing.T) {
|
||||
c := newCatalogProjectionCache()
|
||||
c := mustCache(t)
|
||||
c.Apply([]catalog.ProjectionUpdate{
|
||||
{Projection: catalog.Projection{ID: "id-A", SKU: "A", PriceIncVat: money.Cents(100_00)}},
|
||||
})
|
||||
@@ -81,7 +81,7 @@ func TestProjectionCache_DeleteTombstone(t *testing.T) {
|
||||
// clears the tombstone (live entry replaces it): Get returns the projection,
|
||||
// IsDeleted flips to false. Mirrors how the live catalog keeps flipping state.
|
||||
func TestProjectionCache_BusResurrect(t *testing.T) {
|
||||
c := newCatalogProjectionCache()
|
||||
c := mustCache(t)
|
||||
c.Apply([]catalog.ProjectionUpdate{
|
||||
{Projection: catalog.Projection{SKU: "A", PriceIncVat: money.Cents(50_00)}},
|
||||
{Projection: catalog.Projection{SKU: "A"}, Deleted: true},
|
||||
@@ -110,7 +110,7 @@ func TestProjectionCache_BusResurrect(t *testing.T) {
|
||||
// SKU is a no-op (defensive — bus should never publish "" but cheaper to log
|
||||
// + drop than to populate an ambiguous cache slot).
|
||||
func TestProjectionCache_EmptySKUIgnored(t *testing.T) {
|
||||
c := newCatalogProjectionCache()
|
||||
c := mustCache(t)
|
||||
upserts, _ := c.Apply([]catalog.ProjectionUpdate{
|
||||
{Projection: catalog.Projection{SKU: "", PriceIncVat: money.Cents(100_00)}},
|
||||
})
|
||||
@@ -125,7 +125,7 @@ func TestProjectionCache_EmptySKUIgnored(t *testing.T) {
|
||||
// TestProjectionCache_BusRaceSafe runs Apply + Get concurrently under -race
|
||||
// to lock down the lock order. Run via `go test -race ./cmd/cart/...`.
|
||||
func TestProjectionCache_BusRaceSafe(t *testing.T) {
|
||||
c := newCatalogProjectionCache()
|
||||
c := mustCache(t)
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
@@ -151,7 +151,7 @@ func TestProjectionCache_BusRaceSafe(t *testing.T) {
|
||||
// the per-batch counters report the right split. Without this the cache could
|
||||
// drift on count semantics across batched events.
|
||||
func TestApply_MixedUpsertDelete(t *testing.T) {
|
||||
c := newCatalogProjectionCache()
|
||||
c := mustCache(t)
|
||||
// Order matters: prior in-cache state for A is upserted twice (latest wins),
|
||||
// B is tombstoned, C cold-upserted, D cold-tombstoned.
|
||||
updates := []catalog.ProjectionUpdate{
|
||||
|
||||
Reference in New Issue
Block a user