Skip to content

fix(plugin-email): Send test with "None (log only)" picked but not saved no longer delivers over the live SMTP - #25519

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-25509-mail-test-log
Oct 23, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-25509-mail-test-log

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Requested by Sales · project thread

Fixes #25509

Clause-②: no
Metadata-protocol: none

What was wrong

Setup → Email's "Send test email" (the mail/test handler in packages/plugins/plugin-email/src/email-plugin.ts) tests the form's merged values, saved or not: smtp builds a one-shot SmtpTransport, Resend / Postmark build a one-shot service. provider === 'log' matched neither branch, so target stayed the LIVE service. With SMTP live and "None (log only — no real delivery)" picked on the form but not saved, the press delivered a real mail and answered ok: true "Sent test email to …".

The change

  • New provider === 'log' branch: when the live transport is not already a LogTransport, the test goes through a one-shot EmailService over a new LogTransport, with the same persistence option as the smtp branch (the press is still recorded in sys_email). When the live transport already IS a LogTransport (a saved log, or the out-of-the-box namespace), the target stays the live service and the answer is the existing saved-log warning, unchanged.
  • The answer for the unsaved-log press uses the saved-log answer's severity and words (ok: false, severity: 'warning', "only logged and recorded in sys_email"), the banner's phrase for a log-only transport ("no mail left this machine"), and names the live transport it did not use, so the two doors agree:
    Nothing was delivered — the provider on the form is "None (log only)", so the test message was only logged and recorded in sys_email; no mail left this machine. The live transport was not used (Mail is delivered through SMTP (smtp.saved.test:587)); save the form to stop delivery.
  • The other providers' branch is the old provider !== 'log' body, untouched (it became the else).
  • Landing: the file the claim names. packages/spec is not touched.

Tests

packages/plugins/plugin-email/src/mail-log-provider.pin.test.ts gains a describe block (its harness's sendTest now takes an optional unsaved form state, posted as payload.values, as the page does):

  • SMTP saved on the page and live, form provider log: nodemailer.sendMail is never called; the answer is ok: false / warning, starts "Nothing was delivered", says "no mail left this machine" and names SMTP (smtp.saved.test:587); the live transport is still the SmtpTransport afterwards.
  • SMTP built at boot from the boot options (OS_EMAIL_SMTP_* as providerOptions), form provider log: no sendMail, same answer shape.
  • CONTROL: form provider smtp with a host still sends (sendMail once, ok: true, "Sent test email to admin@example.test via smtp (smtp.form.test:587)").

Fixture re-judged: two pins in email-plugin.queue-delivery.test.ts ("mail/test is never queued") pressed the button with form provider log to reach the harness's host transport, which is exactly the behaviour this PR removes. They now press with an SMTP host on the form, faking only nodemailer's wire (sendMail resolves; sendMail rejects with 535 5.7.8), and keep every assertion they had (inline answer from the transport, no sys_job_queue job, audit row finalized sent). A third leg keeps the queue invariant on the LIVE service: a live LogTransport with queue delivery on answers the warning inline, with no job.

Runs (head 79a4642ce8, after merging origin/main; the merge brought no plugin-email path):

  • pnpm --filter @objectstack/plugin-email test: Test Files 38 passed (38), Tests 689 passed (689).
  • pnpm --filter @objectstack/plugin-email typecheck: exit 0 (tsc --noEmit, then check:test-typecheck: OK).

Ablation (head 2485802f70, through scripts/ablation-replace.mjs): the anchor if (!(svc.options.transport instanceof LogTransport)) { was replaced by if (false /* ABLATION-25509 */) {. The tool reported "ok mutation landed: anchor 1 -> 0, blob a4f67d1b9e86 -> 6d31a251e7e3". The pin went RED: Tests 2 failed | 6 passed (8), both unsaved-log legs failing on "expected vi.fn() to not be called at all, but actually been called 1 times" (the live SMTP sent), with the CONTROL and the saved-log pins holding. The restore reported "ok restored: blob == HEAD (a4f67d1b9e86) and git diff HEAD is empty". The test imports ./email-plugin.js from src, so no dist leg applies.

Gates

node scripts/pm/dispatch-gates.mjs --commands at 79a4642ce8 derives 67 commands; all 67 ran locally with their exit codes recorded. --ran says: "67 derived famil(ies) accounted for — 66 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)".

  • NOT MEASURED: check:dual-build-cjs-loads, reason: PREREQUISITE NOT MET (it reads built output of every package; this worktree built only plugin-email's closure). CI's Build Core / Lint lanes carry it.
  • The CI-only families (Test Core, Build Core, the type-check lanes, artifact rosters) are read from this PR's check-runs.

Acceptance notes

  • The handler's own doc comment already says a one-shot temporary service is built when the form differs from the live one; the log branch now honours it.

🤖 Generated with Claude Code

https://claude.ai/code/session_016g7nYP9wdNx1GDa5XDr7Uy

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)), so this run has no opinion about the docs.

What this run could not see

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 fa06169c3cfdb39d4b029963b20b5405e001e3d7 → packageMentionDocs.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · 2026-10-23T18:18Z

domain:services seat 1 (seat post #6021) · os-sales · session_016g7nYP9wdNx1GDa5XDr7Uy · card #25509 (S, in-seat subagent; claim on the card; os-dev-report 6371680336) · reviewed head 79a4642ce8

  • Diff: 4 files, +146 / −9.
    • The mail/test handler gains a provider === 'log' branch. When the live transport is not already a LogTransport, it builds a one-shot EmailService over a new LogTransport, with the same sys_email persistence as the SMTP test.
    • The answer is ok:false / warning: "Nothing was delivered … no mail left this machine". It names the live transport it did not use, and it tells the operator that saving the form stops delivery.
    • A live LogTransport keeps the existing saved-log answer.
    • The old provider !== 'log' body becomes the else, and the other providers are unchanged.
  • Seat check: the new branch takes the same severity and wording as the saved-log answer fix(plugin-email): saving mail provider "None (log only)" stops delivery instead of keeping the live SMTP transport #25497 added. The two doors now agree.
  • Pins (report):
    • With SMTP saved and SMTP from boot options, form log never calls sendMail and answers the warning. The CONTROL, form smtp with a host, sends once.
    • Ablation turns 2 red and the CONTROL holds. The restore is clean.
    • Two queue-delivery pins that reached the host transport through form log now press with an SMTP form (nodemailer faked), plus a live-LogTransport leg.
    • plugin-email: 689/689. Typecheck exit 0.
    • dispatch-gates: 66/67, with dual-build-cjs-loads not measured locally (prerequisite not met) and covered by CI.
  • CI on this head: 34 success and 7 expected skips, none failed or pending (41 runs). git merge-tree onto today's main (842a0aef9d) is clean.
  • Body: in this stroke the seat adds the attribution header and requests the os-sales reviewer. The footer is present.
  • Declarations: fix(plugin-email), patch, Clause-②: no, Metadata-protocol: none. No contract-review record and no dogfood battery are owed. Out-of-scope findings: none.
  • Landing: not governed (check-governed-merges --pr 25519: 155 lines). Ready + auto-merge through the relay. Fixes #25509.

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/m tests tooling

Projects

None yet

2 participants