Files
pad/internal/cli/item_summary_seq_test.go
xarmian d2973b24d7 feat(items): expected_seq — an OCC token that can tell two same-second writes apart (BUG-3037) (#1351)
* feat(items): expected_seq — the strong OCC token (BUG-3037)

* test(items): the same-second race, with each token (BUG-3037)

* test(cli): the summary shape carries the seq token (BUG-3037, matrix hole)

* test(web): the pane sends the seq token, with the cached-row arm (BUG-3037)

* test(web): the OCC retry re-reads the token through tokenOf (BUG-3037, matrix hole)

* docs: CLAUDE.md carries the v0.34 tool-surface entry (BUG-3037)

* test(e2e): the pane's field write is guarded by seq (BUG-3037)

* fix(mcp,webmcp): forward the OCC token and refuse a fractional seq (codex round 1)

* test(items): the reproduction no longer races the wall clock (BUG-3037)

* fix(items): seq decides when both OCC tokens are sent (codex round 2)

* fix(cli): --expected-seq is sent by presence, so an explicit 0 is not dropped (codex round 3)

* fix(cli): render a seq conflict with the token the caller sent (codex round 4)

* docs(mcp): agent instructions recommend expected_seq (codex round 5)
2026-09-13 22:38:15 -04:00

49 lines
1.8 KiB
Go

package cli
import (
"testing"
"github.com/PerpetualSoftware/pad/internal/models"
)
// BUG-3037 — the SUMMARY shape must carry `seq`.
//
// `seq` is the optimistic-concurrency token a caller round-trips as
// `expected_seq`, and the summary is what `pad item list` and both MCP
// transports return BY DEFAULT (v0.9). A token absent from the shape a caller
// actually reads is a token that caller cannot send: it would silently fall back
// to `expected_updated_at`, which cannot tell two writes inside one second
// apart — the exact defect this token exists to close, reintroduced by a
// projection rather than by a comparison.
//
// This exists because the mutation matrix found it: deleting `Seq` from the
// projection left every other BUG-3037 test green.
func TestToItemSummaries_CarriesTheSeqToken(t *testing.T) {
summaries := ToItemSummaries([]models.Item{
{ID: "item-1", Title: "First", Slug: "first", Seq: 42},
{ID: "item-2", Title: "Second", Slug: "second", Seq: 7},
})
if len(summaries) != 2 {
t.Fatalf("expected 2 summaries, got %d", len(summaries))
}
// Per row, not just "some row has a seq": a projection that stamped one
// value onto every row would pass a weaker assertion.
if summaries[0].Seq != 42 {
t.Errorf("summaries[0].Seq = %d, want 42", summaries[0].Seq)
}
if summaries[1].Seq != 7 {
t.Errorf("summaries[1].Seq = %d, want 7", summaries[1].Seq)
}
}
// The token must survive JSON, and must be present even when it is zero — a
// `seq` the server declines to serialise is a `seq` the client cannot send back,
// which is why the field has no `omitempty`.
func TestItemSummary_SerialisesSeqAlways(t *testing.T) {
blob := mustMarshal(t, ToItemSummaries([]models.Item{{ID: "i", Title: "T", Slug: "t", Seq: 0}})[0])
if !containsKey(blob, `"seq"`) {
t.Errorf("a zero seq was omitted from the summary JSON: %s", blob)
}
}