Files
core-api/tests
jpmschweitzerandClaude Sonnet 5 7c67a4a46b feat(scribe): archive, trash and look up a mailbox entry by message number (T-13/T-9)
The user wants a way to get rid of a message that is no longer useful. A
message is stored once and shared by every recipient; `history` is how
agents rebuild context (D-66), so deleting the message itself would take
it from every other reader. Deletion is therefore per mailbox entry
(D-70): `archive` (kept, out of the default view) and `trash` (out of the
mailbox, restorable). True erasure of content stays `redact` — not this
round.

Migration 009 (down_revision="008") adds
scribe_mailbox_entries.archived_at and trashed_at — independent nullable
timestamps, mirroring book_records.archived_at/trashed_at (migration
005). No data step: every existing entry is neither.

PATCH /scribe/mailbox/{entry_id} takes `archived` and `trashed` booleans
beside `read` (true sets the timestamp if not already set, false clears
it); the response carries both. GET /scribe/mailbox takes `view`: `inbox`
(default, neither archived nor trashed), `archive` (archived and not
trashed), `trash` (trashed, regardless of archived_at — trashing hides an
entry from the archive view too), `all` (no filter on either column).
`find` now excludes trashed entries (archived ones still match).

T-9: GET /scribe/mailbox?message_id=N is a direct lookup, not a filtered
list — the unique constraint on (address, message_id) guarantees zero or
one, in any view, ignoring unread/tag/before_id entirely. That is the
point: a client that already knows the message number never pages the
unread-first/id-descending listing to find it.

Judgement call: the listing's own cursor (`before_id`, a single `id`
comparison) still does not match its two-key sort (unread-first, then id
descending) — the pre-existing gap the dossier flagged as optional to
fix. Left unchanged: T-9's message_id lookup is the actual answer to "a
client pages to find one," and reworking the general cursor is a larger,
separate concern (which field(s) a keyset cursor needs) that risks the
already-passing test_mailbox_lists_unread_first for no requirement this
round asked for.

Judgement call: archived_at and trashed_at are independent timestamps,
never cross-clearing each other. Trashing an already-archived entry does
not clear archived_at, so restoring it (trashed=false) returns it to the
archive view, not the inbox — the view filter's own trash-takes-priority
rule is what keeps it out of view=archive while trashed, not a write-time
side effect.

Tests: 12 new (6 archive/trash flow + 4 message_id lookup + 2 migration
round-trip). Full suite 554 -> 566 passed.

Mutation checks (Edit, reverted after each):
- view=inbox dropped its trashed_at filter ->
  test_trash_hides_from_inbox_archive_and_find_shows_in_trash failed
  (assert 1 == 0).
- message_id lookup dropped the caller-address filter ->
  test_message_id_answers_nothing_for_a_message_not_in_their_mailbox
  failed (assert 1 == 0).
- trash updated every mailbox entry for the message instead of the
  caller's own row ->
  test_trashing_one_recipients_entry_leaves_the_others_untouched failed
  (assert 0 == 1).
All three reverted; full suite re-verified green (566 passed).

Migration 008 -> 009 -> 008 -> 009 round-tripped against core_api_test,
both manually (alembic CLI) and in
TestMigration009RoundTrip. Production stays at 008 — every alembic
invocation derived POSTGRES_* from TEST_DATABASE_URL.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-19 22:54:35 +02:00
..
2025-12-14 12:04:05 +01:00