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
- Capture a populated
items[] — send a real DM between two accounts, or get a payload sample from the backend.
- Tighten
DMConversationDTO to the real keys; delete the alternates.
- Replace the tolerance tests with contract tests against the captured payload.
- 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
- 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.
- 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.
Follow-up from PR #69 (G22), recorded during the 2026-09-13 tracker reconciliation.
Current state — deliberate tolerance, not an oversight
GET /api/dm/conversationsshipped 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 —
DMConversationDTOwas made deliberately permissive:lastMessage/latestMessage/message) and a flattened one (the row is the newest message, the naturalGROUP BY pairKeyoutput)DMMessageResponseaccepts{message},{data}, and the bare objectTests 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:
nilinstead of failing loudlyThe work
items[]— send a real DM between two accounts, or get a payload sample from the backend.DMConversationDTOto the real keys; delete the alternates.Acceptance
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/conversationsis still empty on the.envrecon 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
items[]row, pasted into the issue. No writes, no second account, and it is the whole requirement.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-preferencesconfirmsdirect_messageis 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.