fix(inbox): disable the inbox atomically and simplify its webhook tests - #6441
Conversation
disableInbox deleted the webhook row and cleared the workspace columns as two independent statements. Now that inbox_provider_id is uniquely indexed, a half-applied disable strands the id of an AgentMail inbox that no longer exists, and the next workspace to claim that address cannot enable at all. Wrap both writes in one transaction. The receiver's tests also hand-rolled a table fixture that schemaMock already provides and queued rows the shared mock returns by default. Drop both, and assert the routed inbox id on the unknown-inbox case so it fails against a revert.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview The AgentMail webhook/inbox API deletes still run before the transaction, so external calls are not held inside it. Webhook route tests drop a local table fixture and use shared Reviewed by Cursor Bugbot for commit a1cef31. Configure here. |
Greptile SummaryThe PR makes the database portion of inbox disablement atomic and simplifies AgentMail webhook tests while strengthening the unknown-inbox routing assertion.
Confidence Score: 5/5The PR appears safe to merge, with no actionable correctness, security, or test-quality regressions identified. The transaction uses the repository’s supported callback pattern and transaction handle for both database writes, while the simplified tests preserve the required queued results and rely on the shared mock’s documented empty-array default.
|
| Filename | Overview |
|---|---|
| apps/sim/lib/mothership/inbox/lifecycle.ts | Correctly groups the two related database mutations into one transaction without placing external API calls inside it. |
| apps/sim/app/api/webhooks/agentmail/route.test.ts | Correctly adopts shared database schema mocks, removes redundant queued results, and adds a discriminating lookup assertion. |
Sequence Diagram
sequenceDiagram
participant D as disableInbox
participant A as AgentMail
participant DB as Postgres
D->>A: Delete remote webhook and inbox
A-->>D: Deletion attempts settle
D->>DB: Begin transaction
D->>DB: Delete webhook row
D->>DB: Clear workspace inbox fields
alt Both writes succeed
DB-->>D: Commit
else Either write fails
DB-->>D: Roll back both writes
end
Reviews (1): Last reviewed commit: "fix(inbox): disable the inbox atomically..." | Re-trigger Greptile
Follow-up audit of #6431 and #6436. Two findings, one behavioral and one test-quality.
Atomic disable
disableInboxdeleted the webhook row and cleared the workspace inbox columns as two independent statements in aPromise.all. That was tolerable before, butworkspace.inbox_provider_idis now uniquely indexed, which turns a half-applied disable into a fatal state: the workspace keeps the id of an AgentMail inbox that has already been deleted, that address becomes free at the provider, and the next workspace to claim the same username gets the same id back — so itsenableInboxfails on the unique constraint and surfaces a raw Postgres error to the admin.Narrow (needs a partial failure plus username reuse) and self-inflicted, but it is a state the index newly makes unrecoverable, so both writes now run in one transaction. The AgentMail API deletes still happen before it, so no external I/O is held inside the transaction.
Test simplification
schemaMockfrom@sim/testingalready provides; the file now uses the repo idiomvi.mock('@sim/db', () => ({ ...dbChainMock, ...schemaMock })).queueTableRowscalls were three-quarters no-ops — the shared chain mock already resolves unqueued chains to an empty array — along with its TSDoc, which was wrong about why the calls were needed.Verified by checking out the pre-fix route from
mainand running the suite against it: 6 of 7 tests fail. The seventh is the oversized-body test, which guards the shared body cap that predates all of this and correctly passes on both.Audit findings that needed no change
Recorded so they are on file rather than re-derived later:
inboxProviderIdmismatch check — same 401, same provider retry.JSON.parse, so the routing id and the message id are the same object property.inbox_provider_idaudited: onlyenableInboxanddisableInbox. Workspace creation and forking both omit the column, and no script-migration touches it.