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:
@@ -0,0 +1,72 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"git.k6n.net/mats/platform/catalog"
|
||||
"git.k6n.net/mats/platform/money"
|
||||
)
|
||||
|
||||
// frameMeta builds the event.Meta a producer stamps for a snapshot frame / delta.
|
||||
func frameMeta(phase string, epoch int64) map[string]string {
|
||||
m := map[string]string{catalog.MetaEpoch: strconv.FormatInt(epoch, 10), catalog.MetaTenant: "se"}
|
||||
if phase != "" {
|
||||
m[catalog.MetaSnapshotPhase] = phase
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func enc(t *testing.T, ups []catalog.ProjectionUpdate) []byte {
|
||||
t.Helper()
|
||||
b, err := catalog.EncodeUpdates(ups)
|
||||
if err != nil {
|
||||
t.Fatalf("encode: %v", err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// TestCartCache_SnapshotThenDelta exercises the cart's cache through the real bus
|
||||
// framing: a full begin→chunk→end snapshot makes SKUs Get-able cold (no Apply),
|
||||
// then an epoch-matched delta overrides one and tombstones another.
|
||||
func TestCartCache_SnapshotThenDelta(t *testing.T) {
|
||||
c := mustCache(t)
|
||||
|
||||
const epoch = int64(1000)
|
||||
if err := c.HandleFrame(frameMeta(catalog.SnapshotBegin, epoch), enc(t, nil)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := c.HandleFrame(frameMeta(catalog.SnapshotChunk, epoch), enc(t, []catalog.ProjectionUpdate{
|
||||
{Projection: catalog.Projection{ID: "sku:A", SKU: "A", PriceIncVat: money.Cents(100_00), TaxClass: "standard", ItemID: 1}},
|
||||
{Projection: catalog.Projection{ID: "sku:B", SKU: "B", PriceIncVat: money.Cents(50_00), TaxClass: "reduced", ItemID: 2}},
|
||||
})); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// end carries the total count for the completeness guard.
|
||||
endMeta := frameMeta(catalog.SnapshotEnd, epoch)
|
||||
endMeta[catalog.MetaCount] = "2"
|
||||
if err := c.HandleFrame(endMeta, enc(t, nil)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if got, ok := c.Get("A"); !ok || got.PriceIncVat != 100_00 {
|
||||
t.Fatalf("A from snapshot = %+v ok=%v, want 10000", got, ok)
|
||||
}
|
||||
|
||||
// Epoch-matched delta: bump A's price, delete B.
|
||||
if err := c.HandleFrame(frameMeta("", epoch), enc(t, []catalog.ProjectionUpdate{
|
||||
{Projection: catalog.Projection{SKU: "A", PriceIncVat: money.Cents(90_00), TaxClass: "standard", ItemID: 1}},
|
||||
{Projection: catalog.Projection{SKU: "B"}, Deleted: true},
|
||||
})); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, _ := c.Get("A"); got.PriceIncVat != 90_00 {
|
||||
t.Fatalf("A after delta = %d, want 9000", got.PriceIncVat)
|
||||
}
|
||||
if _, ok := c.Get("B"); ok {
|
||||
t.Fatalf("B after delete should miss")
|
||||
}
|
||||
if !c.IsDeleted("B") {
|
||||
t.Fatalf("B should be tombstoned (skip HTTP fetch)")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user