package ledger import ( "context" "errors" "os" "slices" "strings" "testing" "github.com/carlosframework/platform/internal/release" ) func TestManifestRoundTrip(t *testing.T) { st := release.NewDirStore(t.TempDir()) ctx := context.Background() if _, err := ReadManifest(ctx, st, "bab", "platform"); !errors.Is(err, os.ErrNotExist) { t.Fatalf("missing manifest: %v, want os.ErrNotExist", err) } m := Manifest{Schema: Schema, Public: true, Ledgers: []string{"carbon"}, Title: "CARLOS"} if err := WriteManifest(ctx, st, "bab", "platform", m); err != nil { t.Fatal(err) } got, err := ReadManifest(ctx, st, "bab", "platform") if err != nil || !got.Public || got.Title != "CARLOS" || len(got.Ledgers) != 1 { t.Fatalf("%+v %v", got, err) } // Overwrite flips public off — WriteManifest must replace, not create-only. m.Public = false if err := WriteManifest(ctx, st, "bab", "platform", m); err != nil { t.Fatal(err) } got, _ = ReadManifest(ctx, st, "bab", "platform") if got.Public { t.Fatal("manifest overwrite did not take") } } // TestUpdateManifestReadModifyWrite: the manifest is a SET of published // ledgers accrued over many separate publish calls, so the write path that // serves those calls must read-modify-write. A whole-value replace here is // what silently unpublishes an app's other ledgers. func TestUpdateManifestReadModifyWrite(t *testing.T) { st := release.NewDirStore(t.TempDir()) ctx := context.Background() // Missing manifest is the zero value, not an error: the first publish // of an app creates it. m, err := UpdateManifest(ctx, st, "bab", "platform", func(m *Manifest) error { m.Public = true m.Ledgers = append(m.Ledgers, "carbon") m.Title = "CARLOS" return nil }) if err != nil { t.Fatalf("first update: %v", err) } if !m.Public || m.Schema != Schema || m.Title != "CARLOS" || !slices.Equal(m.Ledgers, []string{"carbon"}) { t.Fatalf("first update = %+v, want the created manifest", m) } // A second update sees the first one's state. m, err = UpdateManifest(ctx, st, "bab", "platform", func(m *Manifest) error { m.Ledgers = append(m.Ledgers, "prompts") return nil }) if err != nil { t.Fatalf("second update: %v", err) } if !slices.Equal(m.Ledgers, []string{"carbon", "prompts"}) || m.Title != "CARLOS" { t.Fatalf("second update = %+v, want carbon preserved beside prompts", m) } stored, err := ReadManifest(ctx, st, "bab", "platform") if err != nil || !slices.Equal(stored.Ledgers, []string{"carbon", "prompts"}) { t.Fatalf("stored = %+v %v, want what the update returned", stored, err) } } // conflictOnce loses the first ledgers.json CAS race to a writer that landed // a DIFFERENT manifest — the shape that tells a retry apart from a re-apply. // A loop that retried with its own stale copy would erase "iron". type conflictOnce struct { release.Store lost bool } func (c *conflictOnce) Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) { if strings.HasSuffix(key, "/ledgers.json") && !c.lost { c.lost = true if _, err := c.Store.Put(ctx, key, []byte(`{"schema":1,"public":true,"ledgers":["iron"],"title":"Other"}`), ifMatch); err != nil { return "", err } return "", release.ErrCASConflict } return c.Store.Put(ctx, key, body, ifMatch) } // TestUpdateManifestCASReapplies: on a lost race the update must re-READ and // re-APPLY, not re-Put its stale copy — otherwise two contributors publishing // two ledgers at once is a lost update, and the loser never learns. func TestUpdateManifestCASReapplies(t *testing.T) { st := &conflictOnce{Store: release.NewDirStore(t.TempDir())} m, err := UpdateManifest(context.Background(), st, "bab", "platform", func(m *Manifest) error { m.Public = true if !slices.Contains(m.Ledgers, "carbon") { m.Ledgers = append(m.Ledgers, "carbon") } return nil }) if err != nil { t.Fatalf("update: %v", err) } if !st.lost { t.Fatal("the fake never forced a CAS conflict") } if !slices.Equal(m.Ledgers, []string{"iron", "carbon"}) { t.Fatalf("after a lost race = %+v, want the winner's iron preserved beside carbon", m) } if m.Title != "Other" { t.Fatalf("title = %q, want the winner's title preserved", m.Title) } } // TestUpdateManifestSurfacesLostRaces: contention that outlasts the retries // is reported, never swallowed — the console maps it to a retry-worthy 503. func TestUpdateManifestSurfacesLostRaces(t *testing.T) { st := alwaysConflict{release.NewDirStore(t.TempDir())} if _, err := UpdateManifest(context.Background(), st, "bab", "platform", func(m *Manifest) error { m.Public = true return nil }); !errors.Is(err, release.ErrCASConflict) { t.Fatalf("update under total contention = %v, want a CAS conflict", err) } } // TestUpdateManifestApplyRefusalWritesNothing: apply is where a caller // enforces a rule only the STORED manifest can decide (the console's cap on // how many ledgers an app may publish). A refusal has to abort before the Put // — a loop that wrote first and reported the refusal afterwards would store // exactly the state it just called illegal. func TestUpdateManifestApplyRefusalWritesNothing(t *testing.T) { ctx := context.Background() st := release.NewDirStore(t.TempDir()) if err := WriteManifest(ctx, st, "bab", "platform", Manifest{Public: true, Ledgers: []string{"carbon"}}); err != nil { t.Fatal(err) } refused := errors.New("no") if _, err := UpdateManifest(ctx, st, "bab", "platform", func(m *Manifest) error { m.Ledgers = append(m.Ledgers, "prompts") return refused }); !errors.Is(err, refused) { t.Fatalf("update = %v, want apply's own error returned untouched", err) } stored, err := ReadManifest(ctx, st, "bab", "platform") if err != nil { t.Fatal(err) } if !slices.Equal(stored.Ledgers, []string{"carbon"}) { t.Fatalf("stored = %+v, want the refused change never written", stored) } } type alwaysConflict struct{ release.Store } func (a alwaysConflict) Put(context.Context, string, []byte, string) (string, error) { return "", release.ErrCASConflict }