Skip to content

fix(plugin-email): the Send test email refusals name what the admin does on the page, not a request body - #25453

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-25424-email-test-copy
Oct 23, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-25424-email-test-copy

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Sales · project thread

Fixes #25424

Clause-②: no
Metadata-protocol: none

What

Setup → Email → Send test email sends to the signed-in admin's own address. When it cannot, three refusals in resolveMailTestRecipient (packages/plugins/plugin-email/src/email-plugin.ts) told the admin to pass "to" in the request body. The mail settings page has no recipient field, so the admin could not follow that advice from the page.

Each refusal now keeps its opening and its "nothing was sent", and names what the admin does on the page. The wording follows the SMS twin in #25421 (resolveSmsTestRecipient):

Case Opening (unchanged) New remedy
no signed-in user (in-process call) No recipient — nothing was sent. Sign in as an admin whose account has an email address, then press Send test email again.
account read fails Could not read your account's email address … press Send test email again once your account can be read.
account has no email, or no row Your account has no email address … Press Send test email while signed in as an admin whose account has an email address.

A to in the request body still wins, as before. The override is now documented only in the function's TSDoc, for API callers.

No route, response shape, status or code changes. Only the message text changes.

Landing

  • The landing is the file the claim names. The refusal strings and their TSDoc are the only lines touched in email-plugin.ts. The live-status registration (open PR feat(settings,plugin-email): state the live mail transport on Setup → Email #25448) and the provider === 'log' branch are untouched.
  • Repo-wide grep for the old strings (pass "to", in the request body, to name a recipient, Add an email address to your account, carries no signed-in): the only hits were the three refusals themselves. No doc, pin or other package quoted them. content/docs has no copy of them. packages/*/CHANGELOG.md is release-owned and was not searched for edits.

Tests

  • New describe('served mail/test — the recipient refusals are admin copy') in mail-test-recipient.pin.test.ts. It drives the served POST /api/settings/mail/test door (real SettingsService, real routes, real handler). Each case asserts the following:
    • the 400 + SETTINGS_ACTION_FAILED envelope;
    • the message opening;
    • the remedy;
    • nothing was sent and Send test email are present;
    • there is no /api/, no request body and no pass "to";
    • sendMail was never called.
  • The cases are: no email address, missing account row, failed account read, and the in-process call with no user.
  • pnpm --filter @objectstack/plugin-email test: Test Files 36 passed (36), Tests 675 passed (675). pnpm --filter @objectstack/plugin-email typecheck (tsc + check:test-typecheck): exit 0. Lock VERDICT command-exit 0 for both.
  • Ablation (one-shot, scripts/ablation-replace.mjs). The failed-read remedy was put back to the old Pass "to" in the request body to name a recipient. text, and the mutation landed (anchor 1 -> 0). Result: a failed account read went red with Tests 1 failed | 9 passed (10). The file was restored (blob == HEAD, git diff HEAD empty).

Acceptance notes

  • None.

🤖 Generated with Claude Code

https://claude.ai/code/session_016g7nYP9wdNx1GDa5XDr7Uy

…p, not a REST body

The three mail/test recipient refusals told the admin to pass "to" in the
request body; the mail settings page has no recipient field. Each now says
nothing was sent and what to do on the page. The `to` override is documented
in resolveMailTestRecipient's TSDoc for API callers. Pins added per refusal.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016g7nYP9wdNx1GDa5XDr7Uy
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation tests tooling labels Oct 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json b9c7a8f541a0af1ec82cabad8c9aaab65de895c1 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from af714fb869449ace29788f1c83bb11355770d182 — the merge of head 7d5d64976d52e2ab85888d5bd42ca5abe49fb5ed into base b9c7a8f541a0af1ec82cabad8c9aaab65de895c1, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin af714fb869449ace29788f1c83bb11355770d182 && git checkout af714fb869449ace29788f1c83bb11355770d182
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b9c7a8f541a0af1ec82cabad8c9aaab65de895c1 7d5d64976d52e2ab85888d5bd42ca5abe49fb5ed && git checkout -B drift-repro b9c7a8f541a0af1ec82cabad8c9aaab65de895c1 && git merge --no-ff 7d5d64976d52e2ab85888d5bd42ca5abe49fb5ed

node scripts/docs-audit/affected-docs.mjs --json b9c7a8f541a0af1ec82cabad8c9aaab65de895c1

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet
objectstack-fleet Bot requested a review from os-sales October 23, 2026 14:55
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · 2026-10-23T14:55Z

domain:services seat 1 (seat post #6021) · os-sales · session_016g7nYP9wdNx1GDa5XDr7Uy · card #25424 (S, in-seat subagent; os-dev-report on the card) · reviewed head 7d5d64976d

  • Diff, read file by file against the claim's surface: 3 files, +75 / −5.
  • Pins (report): a new describe in mail-test-recipient.pin.test.ts with four cases. Each asserts the 400 SETTINGS_ACTION_FAILED envelope, the opening, the remedy and the absence of /api/, "request body" and pass "to", and that sendMail is never called.
    • The ablation (old failed-read remedy restored) turns its case red, and the restore is byte-identical to HEAD.
    • plugin-email: 36 files / 675 tests pass, and typecheck exits 0.
    • dispatch-gates --ran: 67 derived, 65 run, 2 unrun. check:dual-build-cjs-loads needs a full-workspace dist. check:query-options-erasure hit the local timeout during its whole-tree scan. CI's Lint · scoped · query-options erasure is green on this head.
  • CI on this head: 41 check-runs, 34 success and 7 expected skips, none failed. git merge-tree onto today's main is clean.
  • Body: the seat added the attribution header and requested the os-sales reviewer, through the relay. The closing footer is present.
  • Declarations: Clause-②: no, Metadata-protocol: none. Only message text changes; no route, status, code or shape does. No contract-review record or dogfood battery is owed.
  • Commit trailers: model-free Co-authored-by: Claude + Claude-Session.
  • Landing: not governed (check-governed-merges --pr 25453). Ready + auto-merge through the relay. Fixes #25424 closes the card on merge.

Generated by Claude Code

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

Labels

documentation Improvements or additions to documentation size/s tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plugin-email: the Send test email refusals tell the admin to pass "to" in the request body — name the page's control instead (same family as #25345)

2 participants