package suite import "testing" // S1 rows are (item_id, revision, gone, touched_at) and nothing else // (doctrine §6). Draft 1 of memoria's feed carried a visible-to-team field and // that was a leak: `visible=false, gone=false` tells a consumer that a // guest-only item exists, at what revision and when it was touched — exactly // what S3's "404, never 403" exists to hide. func TestS1DecodeRows(t *testing.T) { body := []byte(`{"cursor":"c-92","rows":[ {"item_id":"doc-1","revision":7,"gone":false,"touched_at":"2026-09-05T09:00:00Z"}, {"item_id":"doc-2","revision":2,"gone":true,"touched_at":"2026-09-05T09:01:00Z"} ]}`) feed, err := DecodeS1(200, body) if err != nil { t.Fatalf("unexpected error: %v", err) } if feed.Cursor != "c-92" { t.Errorf("cursor: got %q, want %q", feed.Cursor, "c-92") } if len(feed.Rows) != 2 { t.Fatalf("got %d rows, want 2", len(feed.Rows)) } if feed.Rows[0].ItemID != "doc-1" || feed.Rows[0].Revision != 7 || feed.Rows[0].Gone { t.Errorf("row 0: got %+v", feed.Rows[0]) } if !feed.Rows[1].Gone { t.Errorf("row 1: got gone=false, want the tombstone") } } // A feed row carrying anything beyond the four fields is refused, loudly. The // contract says a row has no title, no content and no visibility flag; a // sibling sending one is either leaking or running a different contract, and // silently ignoring the field would let either go unnoticed for as long as // nobody grepped the store. func TestS1RejectsRowCarryingContent(t *testing.T) { body := []byte(`{"cursor":"c-92","rows":[ {"item_id":"doc-1","revision":7,"gone":false,"touched_at":"2026-09-05T09:00:00Z","title":"Q3 plan"} ]}`) _, err := DecodeS1(200, body) if err == nil { t.Fatal("got nil error for a row carrying a title, want a refusal") } } // Departure from team-visibility and deletion are the same row, and the // client must not offer a caller any way to tell them apart (§4.2). func TestS1GoneCarriesNoReason(t *testing.T) { body := []byte(`{"cursor":"c-1","rows":[ {"item_id":"doc-1","revision":7,"gone":true,"touched_at":"2026-09-05T09:00:00Z","reason":"went_private"} ]}`) _, err := DecodeS1(200, body) if err == nil { t.Fatal("got nil error for a gone row carrying a reason, want a refusal") } } // An empty feed is legitimate and must keep working: it means nothing changed. func TestS1EmptyFeedIsValid(t *testing.T) { feed, err := DecodeS1(200, []byte(`{"cursor":"c-1","rows":[]}`)) if err != nil { t.Fatalf("unexpected error: %v", err) } if feed.Cursor != "c-1" || len(feed.Rows) != 0 { t.Errorf("got %+v, want an empty feed at cursor c-1", feed) } } // Bodies that parse but say nothing. `null` and `{}` both decode without // error into a zero Feed, which a caller would read as an empty feed at an // empty cursor — and committing that cursor loses the consumer's place. // Reported by the GPT-6 module review, finding 8. func TestS1RejectsBodiesThatSayNothing(t *testing.T) { for _, body := range []string{`null`, `{}`, `{"rows":[]}`} { if _, err := DecodeS1(200, []byte(body)); err == nil { t.Errorf("accepted %s, want a refusal", body) } } } // A row missing its identity, or carrying null where a value belongs, decodes // to a live zero-value row: item "" at revision 0, present rather than gone. func TestS1RejectsIncompleteRows(t *testing.T) { bodies := []string{ `{"cursor":"c-1","rows":[{}]}`, `{"cursor":"c-1","rows":[{"item_id":"","revision":7,"gone":false,"touched_at":"2026-09-05T09:00:00Z"}]}`, `{"cursor":"c-1","rows":[{"item_id":"doc-1","revision":7,"gone":null,"touched_at":null}]}`, } for _, body := range bodies { if _, err := DecodeS1(200, []byte(body)); err == nil { t.Errorf("accepted %s, want a refusal", body) } } } // Trailing content after a complete page, and duplicate keys inside a row. // json.Decoder.Decode reads one value and stops, so a second document — one // carrying a title, say — rides along unread; and a duplicated `gone` key is // resolved silently, so `gone:true,gone:false` loses a tombstone. func TestS1RejectsAmbiguousBodies(t *testing.T) { bodies := []string{ `{"cursor":"c-1","rows":[]} {"cursor":"c-2","rows":[{"title":"Q3 plan"}]}`, `{"cursor":"c-1","rows":[{"item_id":"doc-1","revision":7,"gone":true,"gone":false,"touched_at":"2026-09-05T09:00:00Z"}]}`, `{"cursor":"c-1","cursor":"c-2","rows":[]}`, } for _, body := range bodies { if _, err := DecodeS1(200, []byte(body)); err == nil { t.Errorf("accepted %s, want a refusal", body) } } } // A non-2xx feed answer is an error, not an empty feed. An empty feed means // "nothing changed"; returning that for a failed call would advance a // consumer's cursor past changes it never saw. func TestS1NonSuccessIsAnError(t *testing.T) { _, err := DecodeS1(503, []byte(`{"cursor":"c-1","rows":[]}`)) if err == nil { t.Fatal("got nil error for a 503, want a refusal") } }