draftIdResolved.test.tsfix(tests): types:test reaches zero in both packages
The leftovers, and three of them were assertions or props that could not work.
DEAD SHAPES. `model` is retired from `AgentSummary` (797 of 896 production docs
said `sonnet` and the hardcoded per-tool constants were what ran), yet two
fixtures still set it. `summary` is not an `AgentSharePanel` prop — it takes
`{ agentId, className }`. `onAddTaskToGroup` is no longer on
`AgendaTaskCallbacks`, and the mocks type is a mapped type OVER that interface,
so the extra key was the one thing it could not carry.
GROWN TYPES. `AgentSummary` gained `namespace` — where the agent's FOLDER lives,
which is a different question from `scope`, where its document lives — plus
`defaultFile` and `chatEnabled`.
NARROWING, NOT CASTING. `closest()` answers `Element | null` and `within()` wants
an `HTMLElement`: the non-null `!` covered the null and not the widening, so it
is an `instanceof` check now and a card whose text escaped its container fails
loudly. Same for `PAGE[0].createdAt` and the reference-lookup input.
MOCKS FROM THE REAL THING. `applyUpdate` carries `ApplyUpdateClient`'s signature,
so `mock.calls[1][0].update` — the bytes those cases exist to prove were re-sent
— is a real read rather than an out-of-range one on an empty tuple. Likewise
`logToolCall` (six arguments, five destructured), `createPlaybookExtensions` and
`useSubagentCreation`, whose option types come off the hook rather than being
exported for a test's benefit. Four `as unknown as` casts go with them.
`feedScopeGate`'s `ALL` lost its `as const`: nothing read it in a type position,
and the readonly tuple could not be the mutable `string[]` the resolved
participation set arrives as.
Also `shareDocumentTool.test.ts`, new this morning, through the same two helpers
the rest of the server already uses — `toolOutput()` for the `ValidationError`
arm and one named boundary for Mastra's request context.
apps/mail 19 -> 0, apps/server 11 -> 0. Both `types` stay clean.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 9, 2026, 9:39 PMdraftSlice.test.tstest: add 253 tests across frontend slices and server utilities
- Fix Jest 30 config (extract babel-plugin-import-meta.cjs, remove duplicate jest.config.js)
- Add @vitest/coverage-v8 to server for coverage reporting
- Add server tests: concurrentMap, message-utils, utils (cleanEmailAddress, truncateFileName, etc.)
- Add frontend tests: mailSlice, searchSlice, draftSlice, navigationSlice, aopSlice, uxSlice, threadSlice
- Add component tests: ProposalsReviewPanel, diffUtils, stylingUtils
Co-Authored-By: Claude Sonnet 4.6 (1M context) <<email>>Apr 27, 2026, 11:08 PMhtmlDiffBlocks.test.tsxfix(mail): render the composer's draft diff with real blocks and bullets
The diff overlay flattened both bodies to plain text, so lists lost their
markup entirely (any list not a direct child of the root vanished into a
single mashed line) and the pre-wrapped text no longer matched the editor's
spacing. Diff the parsed block structure instead and rebuild real list,
paragraph and blockquote markup under the editor's own classes.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 12, 2026, 9:56 PMrecipientClipboard.test.tsfeat(tasks): give a task with no output a place to be worked
Clicking a task that has produced nothing had nowhere to land: the centre column
became a read-only panel with no Execute, so the one action the task existed for
was missing. It now opens the task's conversation on its Inbox tab — the mail
behind the task, which is what you read to do it — with the task itself pinned
above the rows as OpenTaskExecutionCard, carrying its Execute with it. The panel
keeps an Execute too, for the deep link or collapsed rail where no list is on
screen to defer to.
A reload or a pasted link ran none of that: nothing called openTask, so the URL
sync claimed the bare `task` artifact and you got the read-only card whatever
kind of task it was. TaskExecutionUrlSync closes that — when `?task=` is the only
artifact in the URL it resolves the task and runs the same openTask a click runs.
It is a sibling of LayoutUrlSync rather than part of it because it fetches, and
folding it in would make a pure URL-store adapter require a QueryClient.
Execution mode also outlived its own centre column: backing out of the thread
landed on an entry still carrying `?task=`/`?group=`, leaving the task rail
beside a task list, the same tasks twice. useExitTaskExecutionOnClose clears both
on the close transition — a transition, not the bare state, because opening
writes the params first and the artifact a beat later.
Alongside, on the surfaces that list tasks:
- The chat empty state gains a Tasks card. Tasks are per-conversation but the
empty chat is per-thread, so a deal with three follow-ups showed only the one
you opened. TaskBlock now routes actions against the task's own conversation
rather than the active one, and takes `isActive` so the open task is tinted the
same way it is on the board.
- The agenda's action slot is a fixed h-6 w-28 box whether or not a button is in
it — without the height, hovering a row grew it and walked the page. A finished
task keeps a disabled Execute instead of an empty box. The hover scrim is opaque
bg-surface rather than a translucent backdrop-blur, whose filter region clamped
at the edges and drew a visible rectangle.
- The chat queue card is a flow sibling above the composer instead of floating
over the transcript, so it pushes the transcript up and its bottom edge runs
under the input.
- Copying a recipient pill writes the bare address to the system clipboard as
well as the in-app one, and selection compares cleaned addresses so a
"Name <email>" entry shows as selected.
- The composer's autofocus timer outlived the editor on a fast close and threw;
it now clears on destroy.
- Meeting detail sizes its video and transcript with min-h-0 flex-1: a percentage
height resolves to auto against an auto-height flex panel, so h-full let both
overflow the dialog.
Also adds the MCP tool-allowlist design doc.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 13, 2026, 6:19 PMsaveDraftError.test.tsfix(drafts): four different failures shared one sentence
Every draft-save failure in prod said "Failed to save draft". Among
them was invalid_grant — a dead Google token, where retrying is the one
thing that cannot work. The message sent the user back to the keyboard
to do it anyway.
Classify the cause and say it. A dead token names itself and carries a
Reconnect action; a provider fault says the text is safe rather than
implying the user should retype; a transport failure says so. The toast
also carries a short trace ref, so "it said X" becomes a lookup in
Axiom instead of a search across four identical strings.
(design: bug-draft-saving-duplicates-2026-09-01 phase 4)
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 1, 2026, 11:45 PMundoSendErrorPaths.test.tsfix: close the six audit findings on the send/telemetry work
A — the relay trusted the client to redact. It now enforces its own denylist on
normalized keys and drops email-shaped strings, so a mistake in one browser build
cannot put a recipient or a subject into Axiom.
B — teardown and keepalive were one flag. A send carrying attachments is too large
for keepalive, so it fell through to awaiting the session gate while the page was
going away, which is exactly how the request got lost. They are now separate:
every teardown commit skips the gate, keepalive stays size-limited.
C — the volume caps could silence the one event an incident needs. The sampler
already refused to drop a send or a failure, but the session cap and the buffer
eviction knew nothing about that, so a long-lived tab went quiet for everything
after 2,000 events and overflow evicted oldest-first regardless. Sends and failures
are now exempt from the session cap and evicted last. sanitizeEvent carries the
flag onto the buffered copy — without that the eviction loop read undefined on
every entry and the exemption did nothing, which the new test is what caught.
D — findOrCreateEmailEvent (singular) never wrote rfc_message_id, so events created
through that path stayed invisible to the RFC dedupe added in 51bc228ab.
E — the client request id was a timestamp plus a counter that resets on page load.
Two tabs, or a reload inside the same millisecond, minted identical ids and the
join then matched unrelated requests. It is a uuid now.
F — residue from the 22-row phantom cleanup. Two analytics_draft_actions rows still
pointed at messages Gmail 404s; the table has no FK, so nothing cascaded and nothing
dangled visibly, but attribution-gap.ts matches drafts to sends by message_id and
the fallback only runs when that column is NULL. Both real Cedar-drafted sends were
being reported as unattributed. Repointed to the surviving message, verified against
live Gmail, before-state kept. The one agent_executions row with a NULL event_id is
deliberately left alone: the run was triggered by the phantom, and repointing it at
a different message would invent causation.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 14, 2026, 6:39 AMundoSendRestore.test.tsfix: close the six audit findings on the send/telemetry work
A — the relay trusted the client to redact. It now enforces its own denylist on
normalized keys and drops email-shaped strings, so a mistake in one browser build
cannot put a recipient or a subject into Axiom.
B — teardown and keepalive were one flag. A send carrying attachments is too large
for keepalive, so it fell through to awaiting the session gate while the page was
going away, which is exactly how the request got lost. They are now separate:
every teardown commit skips the gate, keepalive stays size-limited.
C — the volume caps could silence the one event an incident needs. The sampler
already refused to drop a send or a failure, but the session cap and the buffer
eviction knew nothing about that, so a long-lived tab went quiet for everything
after 2,000 events and overflow evicted oldest-first regardless. Sends and failures
are now exempt from the session cap and evicted last. sanitizeEvent carries the
flag onto the buffered copy — without that the eviction loop read undefined on
every entry and the exemption did nothing, which the new test is what caught.
D — findOrCreateEmailEvent (singular) never wrote rfc_message_id, so events created
through that path stayed invisible to the RFC dedupe added in 51bc228ab.
E — the client request id was a timestamp plus a counter that resets on page load.
Two tabs, or a reload inside the same millisecond, minted identical ids and the
join then matched unrelated requests. It is a uuid now.
F — residue from the 22-row phantom cleanup. Two analytics_draft_actions rows still
pointed at messages Gmail 404s; the table has no FK, so nothing cascaded and nothing
dangled visibly, but attribution-gap.ts matches drafts to sends by message_id and
the fallback only runs when that column is NULL. Both real Cedar-drafted sends were
being reported as unattributed. Repointed to the surviving message, verified against
live Gmail, before-state kept. The one agent_executions row with a NULL event_id is
deliberately left alone: the run was triggered by the phantom, and repointing it at
a different message would invent causation.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 14, 2026, 6:39 AMundoSendUnloadGuard.test.tsfix: close the six audit findings on the send/telemetry work
A — the relay trusted the client to redact. It now enforces its own denylist on
normalized keys and drops email-shaped strings, so a mistake in one browser build
cannot put a recipient or a subject into Axiom.
B — teardown and keepalive were one flag. A send carrying attachments is too large
for keepalive, so it fell through to awaiting the session gate while the page was
going away, which is exactly how the request got lost. They are now separate:
every teardown commit skips the gate, keepalive stays size-limited.
C — the volume caps could silence the one event an incident needs. The sampler
already refused to drop a send or a failure, but the session cap and the buffer
eviction knew nothing about that, so a long-lived tab went quiet for everything
after 2,000 events and overflow evicted oldest-first regardless. Sends and failures
are now exempt from the session cap and evicted last. sanitizeEvent carries the
flag onto the buffered copy — without that the eviction loop read undefined on
every entry and the exemption did nothing, which the new test is what caught.
D — findOrCreateEmailEvent (singular) never wrote rfc_message_id, so events created
through that path stayed invisible to the RFC dedupe added in 51bc228ab.
E — the client request id was a timestamp plus a counter that resets on page load.
Two tabs, or a reload inside the same millisecond, minted identical ids and the
join then matched unrelated requests. It is a uuid now.
F — residue from the 22-row phantom cleanup. Two analytics_draft_actions rows still
pointed at messages Gmail 404s; the table has no FK, so nothing cascaded and nothing
dangled visibly, but attribution-gap.ts matches drafts to sends by message_id and
the fallback only runs when that column is NULL. Both real Cedar-drafted sends were
being reported as unattributed. Repointed to the surviving message, verified against
live Gmail, before-state kept. The one agent_executions row with a NULL event_id is
deliberately left alone: the run was triggered by the phantom, and repointing it at
a different message would invent causation.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 14, 2026, 6:39 AM