fix(cli): config.email.persist 接线到 EmailServicePlugin,补上 OS_EMAIL_PERSIST_ENABLED (#5447) - #5470
Merged
Merged
Conversation
`EmailServiceConfigSchema` declares `persist` and the generated reference
documents it as "Persist to sys_email (default true)".
`EmailServicePlugin` honours the constructor option — it builds no
`EmailPersistence` when `persist === false`. The segment between them did
not exist: `resolveEmailCapabilityArg` is the only reader `config.email`
has in the repo, and it read every declared key except this one.
A deployment that wrote `email: { persist: false }` to keep message bodies
out of the database type-checked, parsed and read as configured, and went
on writing every subject, body and recipient to `sys_email` — Prime
Directive #10's declared != enforced, with a PII blast radius. ADR-0049
enforce-or-remove, answered "enforce".
Resolution order, per setting, as the rest of this resolver:
OS_EMAIL_PERSIST_ENABLED > config.email.persist > default (persist ON).
The new env var reads the same truth table as OS_EMAIL_QUEUE_ENABLED, now
a shared `envBooleanFlag` helper rather than two copies of the list. It is
tri-state so an unset variable falls through to config instead of reading
as false. Declaring neither source leaves the key out of the constructor
options entirely, so the plugin default decides and existing deployments
are unchanged.
Tests boot the real plugin over the real resolver output and read back
whether an EmailPersistence was built, rather than re-pinning the key
name against itself. Reverse verification: deleting the wiring turns 8 of
the 13 red; the 5 that stay green are the default-behaviour pins plus two
completeness cases whose expectation coincides with the plugin default —
annotated in the file so they are not misread as discriminating.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016FNvXhtSdnEGEfLEsMmvxh
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 21 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
baozhoutao
marked this pull request as ready for review
August 5, 2026 13:16
baozhoutao
enabled auto-merge
August 5, 2026 13:16
os-zhuang
pushed a commit
that referenced
this pull request
Aug 5, 2026
`serve-email-config-parity.contract.test.ts` 曾注册唯一一条 `DECLARED_BUT_UNREAD` 豁免:`persist` 被 schema 声明却无人读取(#5447)。cli 车道的 PR #5470 (`cd2efe62a`)已合入 main —— `resolveEmailCapabilityArg` 现在经由新的 `envBooleanFlag` / `OS_EMAIL_PERSIST_ENABLED`(三态,默认仍为 ON)读取 `cfgEmail.persist`。 于是本分支的断言在 merge queue 里必然变红:它 expect `unread == ['persist']`, 而合并后的 main 上 `unread == []`。跨车道协议(#5447 评论)约定后落地方对齐, 即本分支。 对齐做法:删除豁免数组,而非留一个空数组 —— 空注册表是一种邀请,下一个 declared-but-unread 键会被直接追加进去而不必辩论,正是该条目当初要防止的 「静默豁免」。断言随之收紧为两个方向都为空,即 declared 集与 read 集相等, 也就是文件注释当初许诺的 plain set equality。豁免的来龙去脉保留在注释中。 反向验证(方向预先判定为 red,结果一致):保留旧豁免数组对合并后的 main 运行, `AssertionError: expected [] to deeply equal [ 'persist' ]` —— 这正是本次预先 规避的队列失败;撤销豁免后该文件 6 个用例全绿。 另:changeset 里「它读八个键」是 #5470 之前的读侧计数(现为九个),补时间 限定词「本次改动时」,以免这段 CHANGELOG 文案落地后失真。schema 中 `persist` 的 TSDoc「Persist to sys_email (default true)」经核对在 #5470 之后依然成立 (默认仍为 ON),故不改。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #5447
问题
EmailServiceConfigSchema一直声明着persist,生成的参考文档正面写着 “Persist to sys_email (default true)”;插件侧也一直是活的 ——EmailServicePlugin在persist === false时不构造EmailPersistence,并且在队列投递诊断里引用这个键。缺的只是两者之间那一段路:config.email在全仓唯一的读者是resolveEmailCapabilityArg,而它读遍了每一个声明键,独独没读这一个。后果是 Prime Directive #10 的 declared ≠ enforced,且带 PII 半径:作者在
objectstack.config.ts写email: { persist: false }想把邮件正文挡在库外 —— 类型通过、schema 通过、文档确认,运行时照旧把每一封邮件的收件人 / 主题 / 正文写进sys_email。按 ADR-0049 enforce-or-remove 取 enforce(分诊裁定)。改动
packages/cli/src/commands/serve.ts一处接线 + 一处提取:resolveEmailCapabilityArg读cfgEmail.persist,并作为插件构造参数传出。OS_EMAIL_PERSIST_ENABLED。优先级按本函数自述的「env 逐项覆盖」:OS_EMAIL_PERSIST_ENABLED优先于config.email.persist,再退到默认(persist 开)。OS_EMAIL_QUEUE_ENABLED原本内联了1/true/yes/on这张表,现提取为共用的envBooleanFlag,两个 flag 同一张表。它是三态的 —— 未设置返回undefined从而落到 config,而不是被读成false去压掉一个写了true的 config。命名说明:
_ENABLED是 PD #9 的布尔开关形状,但与队列 flag 不同,它默认开。因为它并非启用一个新能力,而是一个一直开着的能力的关闭开关;两侧都不声明时,该键根本不出现在构造参数里,由插件自身默认决定 —— 存量部署逐字不变。测试
packages/cli/src/commands/serve-email-persist.test.ts,13 例全绿(pnpm --filter @objectstack/cli test全量 80 文件 / 780 例全绿,typecheck干净)。端到端那一组是本文件的重点:只断言 resolver 吐出
persist: false等于拿键名去钉键名,且完全无法回答「插件到底读不读它」—— 而这正是本单要关的那类缺口。所以它用 resolver 的真实输出去构造真实的EmailServicePlugin,跑到kernel:ready,再回读EmailPersistence究竟有没有被构造。反向验证(方向先判后跑,结论与预判一致):删掉接线那一行,13 例中恰好 8 例转红。留绿的 5 例里,3 例是「两侧都不声明 → 行为与现状逐字一致」的兼容锚点(本就该绿),另 2 例(显式
persist: true、OS_EMAIL_PERSIST_ENABLED=true压 configfalse)是因为期望值恰好等于插件默认值而绿的,并不具备判别力。这一点已写进测试文件的注释,避免后来者把它们误读成接线存在的证据;真正判别接线的是persist:false与OS_EMAIL_PERSIST_ENABLED=false那两例。issue 与派单都要求「把
serve-email-config-parity.contract.test.ts的DECLARED_BUT_UNREAD豁免数组清空、断言收紧为集合相等」。该文件当前不在main上 —— #5307 仍是 open,其 PR 未合并,文件只存在于在飞分支claude/issue-5307-email-config-keys。因此本 PR 无法完成这一项,它是一个跨 PR 的合并次序问题:DECLARED_BUT_UNREAD由['persist']改为[],并把该断言收紧为集合相等 —— 否则它的reads every key it declares一例会红(unread实际为[],期望['persist'])。两个分支的文件面不相交(本 PR 只动
serve.ts+ 新增测试 + changeset;#5307 动packages/spec+ 新增该 parity 测试),不会产生文本冲突,只有这一处语义耦合。范围
严格按派单收口:未动
packages/spec的 schema 本体(键已声明,无需改),未动packages/plugins/plugin-email(插件侧本就完整),未碰defaultTemplateContext/appName相关行(#5448 同函数待派)。changeset 为@objectstack/clipatch,正文写明 PII 动机并提示:曾声明persist: false并据此做过审计的部署,那些行其实存在,值得复查。🤖 Generated with Claude Code
https://claude.ai/code/session_016FNvXhtSdnEGEfLEsMmvxh
Generated by Claude Code