cart: back projection cache with platform/catalog.ProjectionStore
Build and Publish / BuildAndDeployArm64 (push) Failing after 5s
Build and Publish / BuildAndDeployAmd64 (push) Failing after 3s

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:
2026-06-28 21:05:55 +02:00
co-authored by Claude Opus 4.8
parent a3eab70de8
commit d711348863
5 changed files with 177 additions and 93 deletions
+7 -7
View File
@@ -142,7 +142,7 @@ func TestApplyProjectionOverlay_NilMsgSafe(t *testing.T) {
// bypass the tombstone branch and fall through to the HTTP fetch — failing
// the test for a wiring reason rather than the contract under test.
func TestAddSkuToCartHandler_TombstoneReturns404(t *testing.T) {
idx := newCatalogProjectionCache()
idx := mustCache(t)
idx.Apply([]catalog.ProjectionUpdate{
{Projection: catalog.Projection{SKU: "DEL", PriceIncVat: money.Cents(10_00)}},
{Projection: catalog.Projection{SKU: "DEL"}, Deleted: true},
@@ -181,7 +181,7 @@ func TestAddSkuToCartHandler_TombstoneReturns404(t *testing.T) {
// TestIsDeletedVsGet asserts the two methods don't conflict: a tombstoned SKU
// is reported by both IsDeleted (true) and Get (miss).
func TestIsDeletedVsGet(t *testing.T) {
c := newCatalogProjectionCache()
c := mustCache(t)
c.Apply([]catalog.ProjectionUpdate{
{Projection: catalog.Projection{SKU: "DL", PriceIncVat: money.Cents(10_00)}},
{Projection: catalog.Projection{SKU: "DL"}, Deleted: true},
@@ -395,7 +395,7 @@ func TestBuildItemGroups_CacheOnlySkipsHTTP(t *testing.T) {
prod := newFakeProductServer(t)
t.Setenv("PRODUCT_BASE_URL", prod.srv.URL)
idx := newCatalogProjectionCache()
idx := mustCache(t)
idx.Apply([]catalog.ProjectionUpdate{
{Projection: catalog.Projection{
SKU: "TOP",
@@ -463,7 +463,7 @@ func TestBuildItemGroups_HTTPFallbackWhenChildrenPresent(t *testing.T) {
prod := newFakeProductServer(t)
t.Setenv("PRODUCT_BASE_URL", prod.srv.URL)
idx := newCatalogProjectionCache()
idx := mustCache(t)
idx.Apply([]catalog.ProjectionUpdate{
{Projection: catalog.Projection{
SKU: "PARENT", PriceIncVat: money.Cents(99_00),
@@ -518,7 +518,7 @@ func TestAddSkuToCartHandler_CacheOnlySkipsHTTP(t *testing.T) {
defer failSrv.Close()
t.Setenv("PRODUCT_BASE_URL", failSrv.URL)
idx := newCatalogProjectionCache()
idx := mustCache(t)
idx.Apply([]catalog.ProjectionUpdate{
{Projection: catalog.Projection{
SKU: "OK", PriceIncVat: money.Cents(99_00),
@@ -550,7 +550,7 @@ func TestBuildItemGroups_HTTPFallbackWhenCacheMiss(t *testing.T) {
prod := newFakeProductServer(t)
t.Setenv("PRODUCT_BASE_URL", prod.srv.URL)
idx := newCatalogProjectionCache() // empty
idx := mustCache(t) // empty
groups := buildItemGroups(context.Background(), []Item{
{Sku: "MISS", Quantity: 1},
}, "se", idx)
@@ -575,7 +575,7 @@ func TestBuildItemGroups_HTTPFallbackWhenHasRequiredFieldsFalse(t *testing.T) {
prod := newFakeProductServer(t)
t.Setenv("PRODUCT_BASE_URL", prod.srv.URL)
idx := newCatalogProjectionCache()
idx := mustCache(t)
idx.Apply([]catalog.ProjectionUpdate{
{Projection: catalog.Projection{
SKU: "PARTIAL", PriceIncVat: money.Cents(99_00),