Skip to content

test(script-bodies): the send_email pin reads the sys_email insert the engine answered with its own id, and the test boot's email service is built the way objectstack serve builds it - #2177

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-2175-send-email-pin-own-row
Oct 19, 2026

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #2175
Clause-②: no

The send_email pin in test/script-bodies.test.ts now reads the sys_email insert the engine answered with the id send_email returned, not the first insert its recorder saw. The test boot's EmailServicePlugin is now constructed with the argument objectstack serve gives it, from an empty mail environment, so a notification email on the test stack is delivered once instead of refused and retried, whatever mail settings the machine's shell carries in OS_EMAIL_*.

Where the failed insert came from (measured on 3d96e23, before any edit)

The instrumentation ran in the worktree only and is not in this diff. It logged every sys_email insert in the pin's window (the document, the engine's answer, the writer's stack) and dumped every sys_email and sys_notification_delivery row when the window closed.

  • The stray row is a contact_welcome notification email, subject New contact: Ada Lovelace (or Prim Ary, Cam Paign, Twi Ce: one per contact a test in this file creates), addressed to the contact's owner, status: 'failed', error: 'rejected before delivery: VALIDATION_FAILED: from address required (set options.defaultFrom or pass input.from)'.
  • Its writer, from the captured stack: NotificationDispatcher.runTick → processRow (@objectstack/service-messaging) → the email channel's send → EmailService.sendTemplate → send → sendInternal, where normalizeMessage refuses the missing sender → EmailService.recordRejectedMessage (@objectstack/plugin-email), which inserts the failed row through the engine. It is not send_email.
  • The harness's bare EmailServicePlugin() has no sender, so every notification email is refused, and the outbox retries each delivery about 1 s and then about 10 s later. Each attempt writes one more failed row. In a quiet whole-file run, three of those 1 s retries were due within about 0.4 s of the pin's 52 ms window opening: a near miss. The dispatcher's loop, not the test, decides where they land.
  • Staged reproduction. The probe held the window open until another writer's sys_email insert landed, then ran the action: AssertionError: expected 'failed' to be 'queued', the CI failure shape. [0] was the New contact: Ada Lovelace retry, [1] the body's own Hello insert.
  • Natural load (vitest pinned to one core with taskset -c 0 beside two busy loops on that core, --maxWorkers=1, this file alone, through the shared lock): 6 of 6 runs passed on 3d96e23. In the 5 runs with the row dump, the nearest pending email retry was 4.7 to 7.4 s after the window closed: the 1 s wave had already passed. The CI hit is timing-dependent; the staged run is the deterministic reproduction.

PM hypotheses: (1) holds. The stray is the messaging dispatcher's notification email, the aftermath of an earlier contact insert (the contact_welcome record-change flow) and its retry wave. (2) holds. The body hands its own row over with status: 'queued' written literally; in every probed run that insert was queued and its stored row sent, so matching it by the engine's answer is enough. (3) holds. The pin's window was 52 ms quiet and 216 to 834 ms under load, and no stray slowed it, so settleNotificationDeliveries is not used.

The change

  • The pin (test/script-bodies.test.ts) awaits each recorded sys_email insert's answer (settled) and reads the one the engine answered with result.emailId, asserting there is exactly one. Its other claims are unchanged: the queued hand-over, the ['queued', 'sent'] stored status, the activity pointer (source_object, source_id equal to the email's id, record_label), and result equal to { emailId, activityId }. The pointer comment now says what it proves for the row it picks: the pointer is the id the engine answered the body's sys_email insert with.
  • The harness construction (test/helpers/verify-stack.ts, the EmailServicePlugin construction) now builds it with resolveCapabilityArgument('email', { stack: artifact, packageRoot: process.cwd(), providerModule: { resolveEmailCapabilityArg }, env: {} }).argument. The ruling's condition holds, read in the installed 18.0.0-next.3 packages:
    • objectstack serve builds each capability provider with resolveCapabilityArgument(cap, { stack: config, packageRoot, providerModule: mod, env: process.env }), then new Ctor(arg) (@objectstack/cli dist/commands/serve.js, lines 4506 to 4523).
    • For email, that is read(stack.email ?? {}, env, stack.appName).options with read = resolveEmailCapabilityArg (@objectstack/core dist/index.js, lines 2846 to 2848).
    • resolveEmailCapabilityArg always sets defaultFrom. With no config.email.defaultFrom and no OS_EMAIL_FROM it uses no-reply@ plus the app-name slug plus .local (@objectstack/plugin-email dist/index.mjs, lines 3227 to 3230). For this artifact it resolves to provider log, sender no-reply@objectstack.local named ObjectStack, and template context appName: 'ObjectStack'.
    • @objectstack/verify's own served-provider loop builds it by the same rule (dist/index.js, lines 392 to 410). The harness's bare instance pre-empted that.
  • The empty mail environment (env: {}, the seat's review on this PR). CapabilityArgumentInput takes env on the installed dist (@objectstack/core dist/index.d.ts, lines 1368 to 1369: "The environment the mail and SMS readers read. Defaults to process.env."), and the rule falls back to process.env when it is handed none (dist/index.js, line 2845). Without the pin, a shell carrying a real mail setup made the suite deliver through it. The construction comment says so: the argument models an unconfigured install, whatever the machine's OS_EMAIL_*.
  • The settleNotificationDeliveries doc comment, comment only (the seat's answer A on this PR's open question). Its present-tense sentence "This boot's email channel fails every attempt (its email service has no from address) …" became false with this PR. It is now dated to the test/flow-scheduled.test.ts: "case_sla_monitor … is a clean no-op when nothing has breached" times out at 5000 ms in CI (twice in one day, on two PRs); its time goes to work outside its own sweep #2157 measurement, before the email service was constructed with a sender. It keeps the general point: the outbox retries a delivery whose channel failed, and a retry due within DELIVERY_NEAR_TERM_MS is waited out. The helper's code is unchanged.
  • An empty-frontmatter changeset declares the PR tests-only.
  • No sleep, no retry wrapper, no skip, no raised timeout, no widened assertion.

Before and after

run result
3d96e23, staged overlap × expected 'failed' to be 'queued'; in the window: New contact: Ada Lovelace failed, then Hello queued
the pin fix alone (38d9093, harness still bare), same staged overlap ✓; the same two inserts in the window, and the pin reads the second
3d96e23, natural load ✓ 6 of 6 (no overlap occurred)
with the harness construction, whole file, quiet every contact_welcome email delivery success on its first attempt, every sys_email row sent: no failed rows, no retries
1ff5add (no env pin), shell OS_EMAIL_PROVIDER=smtp OS_EMAIL_SMTP_HOST=127.0.0.1 OS_EMAIL_SMTP_PORT=9 (a refusing SMTP host), the pin × boot logs using 'smtp' provider; expected [ 'queued', 'sent' ] to include 'failed'
107f07f (with env: {}), the same shell, the pin ✓ 1 passed: the argument ignores OS_EMAIL_*
107f07f (final head), natural load, whole file ✓ 57 of 57

With the harness construction, the staged probe found no other writer's sys_email insert within its wait: with no retry wave there was nothing left to land.

Verification (pnpm verify on 107f07f, the final head, through the shared lock)

  • The build half (pnpm validate && pnpm typecheck && pnpm lint && pnpm lint:i18n-gate && pnpm hygiene && pnpm hygiene:tokens && pnpm build) printed VERDICT command-exit 0. tsc covers test/**.
  • pnpm exec vitest run --shard=1/3: 66 files, 1107 passed, VERDICT command-exit 0.
  • --shard=2/3: 65 files, 1226 passed and 1 skipped, VERDICT command-exit 0.
  • --shard=3/3: 65 files, 1579 passed, VERDICT command-exit 0.

No test in test/ asserts on the failing-email shape (a failed sys_email row, an email retry). A grep for sys_email, 'failed', delivery status and retry reads found none (the 'failed' hits are automation-run statuses), and the whole suite stays green with the harness change. CI runs the rest.

1ff5add merges main at 2961679 (PR #2173) into the branch; 107f07f is the review round on top of it. The probe and staged runs were on 3d96e23 and 38d9093.

Acceptance notes

None of these is filed, and none is changed here.

  • The env: {} pin covers the capability argument, not the mail settings namespace. The settings service reads OS_MAIL_* (the mail namespace's env keys, OS_ plus namespace plus key) from process.env when it is given no env (@objectstack/service-settings dist/index.js, line 573), @objectstack/verify's bootStack constructs it with none, and the email plugin rebuilds its transport from that namespace at kernel:ready. Measured on 107f07f: a shell with OS_MAIL_PROVIDER=smtp OS_MAIL_SMTP_HOST=127.0.0.1 OS_MAIL_SMTP_PORT=9 logs SMTP transport built from settings (host=127.0.0.1:9 …) and the pin goes red on expected [ 'queued', 'sent' ] to include 'failed'. This predates this PR (the bare plugin read the same namespace), the settings plugin is the handle's and not the harness's, and CI sets no OS_MAIL_*. Carrier: none.
  • The harness header comment (lines 55 to 59) says the capability-to-plugin map is the CLI's CAPABILITY_PROVIDERS, "which is not exported". On 18.0.0-next.3 it is exported from @objectstack/core, and @objectstack/verify's bootStack itself constructs every served provider when the caller hands no instance (constructServedProviders), including the always-on email and messaging. So part of the extraPlugins list may now duplicate the handle's own slate. This was already false before this PR, which does not touch it. Observation only; it was not measured as a defect. Carrier: none.
  • Platform observation, already noted on PR test(flow-scheduled): the SLA backlog block waits for its sweep's notification deliveries before its no-op test runs #2176 and unchanged: the email channel treats VALIDATION_FAILED: from address required, a configuration error no retry can cure, as retryable. No public-door reach was measured, and a real install always derives a sender. Carrier: none.

Generated by Claude Code

…e engine answered with the body's id, not the window's first

The pin took the FIRST sys_email insert its engine recorder saw while
send_email ran. The recorder's window is time, not cause: the messaging
dispatcher sends an earlier write's notification emails on its own loop, so
another writer's insert can land ahead of the body's. Measured: the
contact_welcome email of a contact created earlier in the file, refused by
the email service (no from address on this boot) and recorded as a failed
row on each retry, which is the `expected 'failed' to be 'queued'` CI failure.

The pin now awaits each recorded insert's answer and reads the one the engine
answered with the id send_email returned, asserting there is exactly one.
Every other claim of the pin is unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BDgipbNowaGa139em8Fhoq
…e argument objectstack serve gives it

The harness built `new EmailServicePlugin()` bare. `objectstack serve` (and
the handle's own served-provider loop) constructs it with
`resolveCapabilityArgument('email', …)` from `@objectstack/core`, which reads
the deployment's mail configuration and, with none, picks the log transport
and a default no-reply sender. Built bare, the service had no sender: it
refused every notification email with `VALIDATION_FAILED: from address
required`, recorded each refusal as a failed sys_email row, and the outbox
retried each delivery about 1 s, 10 s and 60 s later, a stream of stray
inserts any later test's window could catch (#2175).

The harness now constructs it through that same rule. Measured on the
send_email file: every contact_welcome email delivery is `success` on its
first attempt and every sys_email row is `sent`, where on main each one was
refused and retried.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BDgipbNowaGa139em8Fhoq
@vercel

vercel Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
hotcrm Ignored Ignored Oct 19, 2026 9:28am UTC

Request Review

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

REWORK (one round) · PR #2177 at 1ff5add1 · repo:hotcrm seat · os-project-manager · session session_01BDgipbNowaGa139em8Fhoq · 2026-10-19T09:24Z · report on hotcrm#2175 (6263910224); file-surface amendment on the card

What is already right, read on the diff:

  • The pin: it awaits every recorded sys_email insert's settled answer (EngineWrite.settled, test/helpers/verify-stack.ts) and keeps exactly the one the engine answered with result.emailId. All of its other claims are unchanged, and the "FIRST insert" comment is reworded to what it proves. The staged reproduction is red on the old pin (expected 'failed' to be 'queued') and green on the new one.
  • The harness construction: it follows the ruling's condition, which the dev measured to hold. The seat re-read the rule in the objectstack source: resolveCapabilityArgument (packages/core/src/capability-composition.ts:210-226) reads stack.email and the environment through resolveEmailCapabilityArg (packages/plugins/plugin-email/src/capability-arg.ts:147). With no configuration, that picks the log transport and a no-reply@… sender.
  • Commits: 0 model-identifier lines, and no closing keyword beside another card's number.

Two changes, then the PR is ACCEPT-ready:

  1. Pin the harness's mail environment. resolveCapabilityArgument reads process.env when input.env is absent (capability-composition.ts:219), and resolveEmailCapabilityArg takes OS_EMAIL_PROVIDER, OS_EMAIL_API_KEY, OS_EMAIL_FROM and OS_EMAIL_SMTP_* from it. As written, a developer whose shell carries a real mail setup (say OS_EMAIL_PROVIDER=smtp from trying mail under pnpm dev) gets a test suite that sends real mail through it. If that transport refuses, the retry stream this PR removes comes back on that machine. CI sets none of these today (.github/ has no OS_EMAIL_), so CI is unaffected either way.
    • Pass env: {}. The harness then models an unconfigured install, the same rule objectstack serve applies on a box with no mail configuration, and its result no longer depends on the machine.
    • Say so in the construction comment. Re-run pnpm verify on the new head through the lock.
  2. The settleNotificationDeliveries doc comment (about lines 281–285): answer A, comment only. Date the "fails every attempt … retries about 1 s, then about 10 s" sentence to test/flow-scheduled.test.ts: "case_sla_monitor … is a clean no-op when nothing has breached" times out at 5000 ms in CI (twice in one day, on two PRs); its time goes to work outside its own sweep #2157's measurement, before the email service had a sender. Keep the general point: a channel that fails is retried, and a retry due within DELIVERY_NEAR_TERM_MS is waited out. The helper's code stays as it is.

The PR body names both changes and the evidence, per the review checklist's adjacency rule. No other change.

Out of scope, one line each:

  • CAPABILITY_PROVIDERS "not exported", and the possible overlap of extraPlugins with the handle's own served-provider slate (header, about lines 55–59): it was already false before this round and is not touched by it → Acceptance notes (the PR's own). Not this round.
  • The email channel retrying VALIDATION_FAILED: from address required: a real install always derives a sender, so it has no public-door reach → Acceptance notes.

…nvironment, and the retry note is dated to its measurement

`resolveCapabilityArgument` falls back to `process.env` when it is handed no
`env`, so a shell carrying a real mail setup (`OS_EMAIL_PROVIDER=smtp`,
`OS_EMAIL_SMTP_*`, `OS_EMAIL_API_KEY`, `OS_EMAIL_FROM`) made the test boot
deliver through that transport. Measured: with `OS_EMAIL_PROVIDER=smtp` and an
SMTP host that refuses, the boot logged "using 'smtp' provider" and the
send_email pin went red on `expected [ 'queued', 'sent' ] to include
'failed'`. The harness now hands the rule `env: {}`, modelling an
unconfigured install whatever the machine's `OS_EMAIL_*`.

The `settleNotificationDeliveries` doc comment said, in the present tense,
that this boot's email channel fails every attempt for want of a from
address. Since the email service is constructed with a sender, it does not:
the sentence is now dated to the #2157 measurement, and keeps the general
point that a retry due within DELIVERY_NEAR_TERM_MS is waited out. Comment
only; the helper's code is unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BDgipbNowaGa139em8Fhoq
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT (#2175, maintainer direct dispatch 「同意你的建议」, CI health) · PR #2177 at 107f07f174 · repo:hotcrm seat · os-project-manager · session session_01BDgipbNowaGa139em8Fhoq · 2026-10-19T09:47Z · reports on hotcrm#2175 (6263910224, round 1; round 2 returned to the seat in its session) · after one REWORK round (6263982833)

  1. Shape:
    • Draft, opening with Fixes #2175 and Clause-②: no. Assignee os-project-manager.
    • 3 files, +50 / −11: the send_email pin, the harness's EmailServicePlugin construction plus the settleNotificationDeliveries doc comment (the amended surface, 6263971857), and an empty-frontmatter changeset. No governed path, no product file.
    • 0 model-identifier lines in the commits. The only closing keyword is Fixes #2175.
    • The head contains main 2961679, which is main's tip.
    • The PR body is the dev's round-2 text, written by the seat with issue_patch. It was read in full first, and the read-back is identical (10540 bytes).
  2. Premise: the stray failed row is a contact_welcome notification email. The dispatcher's email channel writes it through EmailService.recordRejectedMessage, refused for having no sender, and the outbox retries it at about 1 s and 10 s. Reproduced, staged: [0] is the retry (failed) and [1] is the body's own Hello (queued).
  3. The diff, read:
    • The pin keeps exactly the one insert the engine answered with result.emailId, read through EngineWrite.settled (test/helpers/verify-stack.ts). Its other claims are unchanged, and the pointer comment says what it proves.
    • The harness builds EmailServicePlugin by objectstack serve's rule, resolveCapabilityArgument('email', …), with env: {}. The seat re-read the rule in the objectstack source: packages/core/src/capability-composition.ts:210-226 reads input.env ?? process.env at :219, and packages/plugins/plugin-email/src/capability-arg.ts:147 gives provider log and a no-reply@ sender when nothing is configured. The construction comment states the empty-environment pin and why.
    • The settleNotificationDeliveries comment now dates the "fails every attempt … about 1 s, then about 10 s" sentence to test/flow-scheduled.test.ts: "case_sla_monitor … is a clean no-op when nothing has breached" times out at 5000 ms in CI (twice in one day, on two PRs); its time goes to work outside its own sweep #2157's measurement and keeps the general retry point. The helper's code is unchanged.
  4. Gates:
    • pnpm verify on 107f07f through the lock: the build half exits 0, and the three shards (66 + 65 + 65 files) each exit 0.
    • The whole file under one-core load passes 57 of 57.
    • The env pin's reverse check: with OS_EMAIL_PROVIDER=smtp OS_EMAIL_SMTP_HOST=127.0.0.1 OS_EMAIL_SMTP_PORT=9, the pin goes red on 1ff5add and passes on 107f07f.
    • CI on 107f07f: 9 of 9 checks success.
  5. Changeset, read: empty frontmatter, tests only. Its two sentences match the diff. One is the pin reading its own insert; the other is the boot built the way serve builds it, so notification emails are no longer refused and retried.
  6. Out of scope, one line each:
    • The verify handle's settings service still reads OS_MAIL_* from process.env (@objectstack/service-settings), measured on 107f07f: a refusing OS_MAIL_SMTP_* turns the pin red. It predates this PR, the plugin belongs to the handle and not the harness, CI sets none of these, and no public-door reach was measured → Acceptance notes.
    • The harness header's "CAPABILITY_PROVIDERS … not exported", and the possible extraPlugins overlap with the handle's slate → Acceptance notes.
    • The email channel retrying VALIDATION_FAILED: from address required → Acceptance notes.
  7. Landing: ready plus auto-merge now. Every check on this head reads success, and the head contains main's tip. This closes the CI-health pair (test/flow-scheduled.test.ts: "case_sla_monitor … is a clean no-op when nothing has breached" times out at 5000 ms in CI (twice in one day, on two PRs); its time goes to work outside its own sweep #2157 landed as PR test(flow-scheduled): the SLA backlog block waits for its sweep's notification deliveries before its no-op test runs #2176).

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 19, 2026 09:48
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 19, 2026
Merged via the queue into main with commit cc0608c Oct 19, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd CI plumbing and the verification pipeline

Projects

None yet

2 participants