mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-26 22:37:03 +00:00
d2973b24d7
* 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)
49 lines
1.8 KiB
Go
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)
|
|
}
|
|
}
|