Skip to content

contract(dm): tighten the conversations decoder once a populated items[] is captured #76

Description

@Adron

Follow-up from PR #69 (G22), recorded during the 2026-09-13 tracker reconciliation.

Current state — deliberate tolerance, not an oversight

GET /api/dm/conversations shipped without its populated shape ever being observed. The test account's inbox is empty, and populating it (sending a real DM from the shared contract-test account) was out of scope.

Rather than guess one spelling and risk a silent all-nil decode — exactly the G21 link-metadata defect — DMConversationDTO was made deliberately permissive:

  • accepts a nested row (lastMessage / latestMessage / message) and a flattened one (the row is the newest message, the natural GROUP BY pairKey output)
  • alternates for participant and unread count
  • DMMessageResponse accepts {message}, {data}, and the bare object
  • every field optional, so an unknown key degrades one field instead of failing the page

Tests pin that tolerance in both directions.

Why this should not stay

Permissive decoding is the right call when the shape is unknown, and the wrong default once it is known. While it stands:

  • a renamed key degrades silently to nil instead of failing loudly
  • the tests assert tolerance rather than truth, so they cannot catch drift
  • nobody can tell from the code which spelling the server actually uses

The work

  1. Capture a populated items[] — send a real DM between two accounts, or get a payload sample from the backend.
  2. Tighten DMConversationDTO to the real keys; delete the alternates.
  3. Replace the tolerance tests with contract tests against the captured payload.
  4. Keep exactly one deliberate escape hatch if the shape proves genuinely polymorphic — and say so in a comment.

Acceptance

  • The decoder names one shape.
  • The fixture is captured, not fabricated.
  • A renamed key now fails a test instead of degrading to nil.

Status (2026-09-15) — still blocked on the same capture, and here is why that was not resolved

This issue's step 1 is "Capture a populated items[] — send a real DM between two accounts, or get a payload sample from the backend."

Probed again on 2026-09-15. GET /api/dm/conversations is still empty on the .env recon account, so the populated shape remains unobserved and the permissive decoder stays exactly as it is.

Populating it was considered and not done. It needs a second real account to send from — the recon account cannot DM itself — and creating one, or sending from a personal account, writes a message into someone's real inbox to satisfy a contract test. That is a heavier side effect than a read-only probe, and it is the owner's call rather than something to do unannounced.

The tolerance is doing its job, and that is the danger

Nothing about the current state is broken; the decoder accepts both the nested and flattened shapes and tests pin that tolerance in both directions. But as this issue says, permissive decoding is the right call when the shape is unknown and the wrong default once it is known — and nothing forces the issue. The pane works, so nobody looks.

Two ways to unblock, in order of preference

  1. A payload sample from the backend — one real items[] row, pasted into the issue. No writes, no second account, and it is the whole requirement.
  2. A second recon account. Worth having for more than this: list sharing, org membership and follow requests all have "the other person" paths that are currently untestable end to end for the same reason.

Folding this into the backend-ask list (#58) is reasonable if option 1 is the route.

Adjacent finding

The DM notification side did move: /api/user/notification-preferences confirms direct_message is a real server event (see #77 and #95). That says nothing about the conversations payload, but it is the same feature area and worth reading together.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    parityWeb-parity gap with the InterlinedList web app

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions