|
897 | 897 | "title": "Email templates resolve from the registry (no sys_email_template row since ADR-0131 D6/D10), resolve (name, locale) with en-US fallback, render {{path}} holes, gate on required variables and active:false, survive admin edits across redeploys — and the raw POST /api/v1/email/send door authenticates, requires manage_platform_settings, bounds sentBy, from, headers, organizationId and identityMail, refuses anonymous, and 400s malformed input", |
898 | 898 | "since": "v15", |
899 | 899 | "status": "active", |
900 | | - "revision": 12, |
| 900 | + "revision": 13, |
901 | 901 | "priority": "P2", |
902 | 902 | "surface": "mixed", |
903 | 903 | "fixtures": { |
|
914 | 914 | ] |
915 | 915 | }, |
916 | 916 | "variants": [ |
917 | | - "locale resolution: exact (name, locale) match / en-US fallback / two-locale bundle picking by recipient locale", |
| 917 | + "locale resolution: exact (name, locale) match / a named locale folded to a bundle member (zh → zh-CN) / the declared fallback locale, then en-US / two-locale bundle picking by recipient locale", |
918 | 918 | "body: bodyHtml + bodyText authored / bodyText omitted (service derives text from HTML — omitted from the ROW, not nulled)", |
919 | 919 | "gate: active:false → TEMPLATE_INACTIVE", |
920 | 920 | "gate: missing required variable → fail-fast (requireVars)", |
|
926 | 926 | "drive a send with full data ({ title, project }) through the scratch notify flow of knownGaps[1] (POST /api/v1/automation; channels ['email'], template, templateData; fired through its api trigger); capture the rendered subject/body from the sys_email row it lands (the dev log transport logs no body)", |
927 | 927 | "drive a send MISSING the required `title`; capture the fast failure", |
928 | 928 | "make the template inactive (a Studio save of the item with active:false, or a scratch template authored active:false) and send; capture the TEMPLATE_INACTIVE error", |
929 | | - "author the zh-CN twin; send to a zh-CN-locale recipient and to an unmatched-locale recipient; capture which row rendered each", |
| 929 | + "author the zh-CN twin; send to a zh-CN-locale recipient, to a zh-locale recipient (a tag the bundle folds) and to a recipient whose language the bundle lacks (e.g. ja-JP); capture which row rendered each. The showcase declares i18n fallbackLocale 'en' (examples/app-showcase/objectstack.config.ts), which folds to the en-US row", |
930 | 930 | "author a template with fromOverride + replyTo and verify both on the outbound message. fromOverride is an object { name?, address } with address an email (EmailAddressInlineSchema, packages/spec/src/system/email-template.zod.ts) and replyTo a plain address string; a string fromOverride is refused at save, 422 INVALID_METADATA 'expected object'", |
931 | 931 | "edit the template wording as an admin in Studio (an environment overlay over the declared item), redeploy/reboot, and re-read the served item over GET /api/v1/meta/email_template/showcase_task_done_email — the admin's wording must still be served", |
932 | 932 | "on the WRITABLE showcase copy (fixtures.requires[3]), edit the DECLARED source template and metadata-reload WITHOUT a reboot, then GET /api/v1/meta/email_template/<name> — the registry serves the edited item (email_template is allowRuntimeCreate:true; the plugin subscribes to metadata changes). Keep the reboot out of it: under os dev's default watcher a declared-source save recompiles AND restarts the server (packages/cli/src/commands/dev.ts, --restart defaults on), which proves nothing about a reload. Compile into the running copy's artifact with objectstack compile --output COPY/dist/objectstack.json, or boot the copy with os dev --no-restart (the watcher then only rebuilds the artifact), and read the listener's PID before and after (lsof -ti tcp:PORT): an unchanged PID is part of the evidence, and a changed one means this half was not driven. On the stock read-only showcase this half is not drivable: record it partial, as QA run #23166 did", |
|
954 | 954 | "evidence": "the error" |
955 | 955 | }, |
956 | 956 | { |
957 | | - "clause": "(name, locale) resolution is an EXACT match with no language-subtag folding — two rows with one name are an i18n bundle, each reachable ONLY from its own exact tag; a call naming a locale with no row retries the single literal rung en-US and then raises TEMPLATE_NOT_FOUND, and a call naming no locale starts at en-US", |
| 957 | + "clause": "(name, locale) resolution walks the ladder of record, SendTemplateInput.locale in packages/spec/src/contracts/email-service.ts: two rows with one name are an i18n bundle, the name is matched exactly and only the locale member is resolved, and every rung loads one row at one tag exactly, so en and en-US stay two distinct rows (folding chooses which row is read, it never merges two). A call that NAMES a locale, over the built-in registry template loader, walks: (1) this locale, folded against the bundle's locale tags by resolveBundleLocale — exact, then case-insensitive (zh-cn → zh-CN), then base language (en-GB → en), then variant expansion (zh and zh-TW → zh-CN), the first match in ascending tag order; (2) the stack's declared fallback locale (II18nService.getFallbackLocale(): i18n.fallbackLocale, else defaultLocale), folded the same way, skipped when none is declared; (3) en-US, exactly; (4) TEMPLATE_NOT_FOUND, which is permanent. A rung that resolves to a tag already tried is skipped; a replacement TemplateLoader keeps the exact ladder. A call that names no locale starts at en-US exactly, with no fold and no declared fallback, then takes the loader's own no-locale answer (the built-in loader: the bundle's lowest locale tag)", |
958 | 958 | "oracle": "log", |
959 | | - "verify": "the zh-CN recipient gets the zh-CN render; a recipient whose locale has no row (e.g. zh) gets the en-US row, NOT the zh-CN one", |
| 959 | + "verify": "the zh-CN recipient gets the zh-CN render; the zh recipient gets the zh-CN render too (variant expansion); a recipient whose language the bundle lacks (e.g. ja-JP) gets the en-US row, through the showcase's declared fallback en folding to en-US. A zh recipient getting the en-US row is the FAIL", |
960 | 960 | "evidence": "the two renders" |
961 | 961 | }, |
962 | 962 | { |
|
994 | 994 | ], |
995 | 995 | "automated": { |
996 | 996 | "kind": "e2e", |
997 | | - "ref": "packages/qa/dogfood/test/email-template-materialization.dogfood.test.ts — on its own fixture package (auth.password_reset), not the showcase template: acceptance[0] (the registry serves the authored item beside the built-in and sendTemplate renders it, the subject exact and the body by containment), and halves only of acceptance[3] (the exact zh-CN member is reachable; not the no-folding case or the TEMPLATE_NOT_FOUND end) and of acceptance[5] (a Studio save sends on the next mail with no restart; not redeploy survival, not the declared-source reload). + packages/qa/dogfood/test/email-template-overlay-survives-boot.dogfood.test.ts ('after a cold boot the metadata door and the rendered mail both carry the admin's wording') — acceptance[5]'s redeploy-survival half on the showcase. + packages/qa/dogfood/test/email-template-two-locales-survive-boot.dogfood.test.ts ('control: a locale nobody saved keeps the package ladder') — acceptance[3]'s en-US rung for a locale with no row (ja-JP). acceptance[6] rests on no pin; acceptance[7]'s capability, sentBy, sender, headers-shape, organization and identity-mail bounds are pinned below the e2e layer by packages/rest/src/email-send-door-bounds.test.ts (the real Hono transport over a contract-typed mail-service double — the dev-transport landing and the sys_email row stay hand-driven), and acceptance[1], [2] and [4] on one below their oracle's layer: packages/plugins/plugin-email/src/send-template.test.ts drives EmailService over a fake loader and a capture transport, asserting MISSING_VARIABLES (acceptance[1]), TEMPLATE_INACTIVE (acceptance[2]) and the fromOverride and derived text (acceptance[4]) at unit — whether the error names the variable, the registry-resolved inactive item, replyTo and the row stay hand-driven: drive them by hand" |
| 997 | + "ref": "packages/qa/dogfood/test/email-template-materialization.dogfood.test.ts — on its own fixture package (auth.password_reset), not the showcase template: acceptance[0] (the registry serves the authored item beside the built-in and sendTemplate renders it, the subject exact and the body by containment), and halves only of acceptance[3] (the exact zh-CN member is reachable; not the fold rungs, the declared-fallback rung or the TEMPLATE_NOT_FOUND end, which packages/plugins/plugin-email/src/template-locale-fold.test.ts pins at unit and template-locale-fold.integration.test.ts on a booted EmailServicePlugin, below this oracle's layer) and of acceptance[5] (a Studio save sends on the next mail with no restart; not redeploy survival, not the declared-source reload). + packages/qa/dogfood/test/email-template-overlay-survives-boot.dogfood.test.ts ('after a cold boot the metadata door and the rendered mail both carry the admin's wording') — acceptance[5]'s redeploy-survival half on the showcase. + packages/qa/dogfood/test/email-template-two-locales-survive-boot.dogfood.test.ts ('control: a locale nobody saved keeps the package ladder') — acceptance[3]'s en-US rung for a locale with no row (ja-JP). acceptance[6] rests on no pin; acceptance[7]'s capability, sentBy, sender, headers-shape, organization and identity-mail bounds are pinned below the e2e layer by packages/rest/src/email-send-door-bounds.test.ts (the real Hono transport over a contract-typed mail-service double — the dev-transport landing and the sys_email row stay hand-driven), and acceptance[1], [2] and [4] on one below their oracle's layer: packages/plugins/plugin-email/src/send-template.test.ts drives EmailService over a fake loader and a capture transport, asserting MISSING_VARIABLES (acceptance[1]), TEMPLATE_INACTIVE (acceptance[2]) and the fromOverride and derived text (acceptance[4]) at unit — whether the error names the variable, the registry-resolved inactive item, replyTo and the row stay hand-driven: drive them by hand" |
998 | 998 | }, |
999 | 999 | "source": [ |
1000 | 1000 | "packages/spec/src/system/email-template.zod.ts#fromOverride (requireds, dotted-name regex, aliases #5013, variables/required, fromOverride/replyTo/active)", |
|
1078 | 1078 | "date": "2026-11-01", |
1079 | 1079 | "change": "Two QA run #28656 (W3-RT) findings. knownGaps[1]'s recipe was incomplete. #28656 met three refusals on the way: an inline title/message beside template (400), type 'autolaunched' at POST /api/v1/automation/NAME/trigger (403), and type 'api' with no config.secret or secretRef (400). It now spells a type:'api' flow with start config {triggerType:'api', hookId, secret}, a notify node with channels ['email'], template and templateData and no inline title/message, fired as the admin. Source: NotifyConfigSchema's template/inline exclusivity (io-node-config.zod.ts) and validateApiTriggerSecret (flow-registration-checks.ts). The 403's rule is read from flow-start-admission.ts, which refuses a runAs: 'system' self-triggered type. That #28656's flow declared runAs: 'system' is not confirmed from source, and the 400 and 403 statuses are #28656's readings. steps[7] said 'metadata-reload WITHOUT a reboot' on a copy whose os dev watcher restarts on every save (dev.ts --restart, default on). It now names objectstack compile --output into the running copy's dist/objectstack.json or --no-restart, and requires an unchanged listener PID as evidence. No clause changed. Re-verified against source at base 1c5b1773ec. Wave anchor #27377, wave-3 accuracy slice S10 (#28701); findings parked in anchor comment 6557904764", |
1080 | 1080 | "ref": "#28656" |
| 1081 | + }, |
| 1082 | + { |
| 1083 | + "revision": 13, |
| 1084 | + "date": "2026-11-02", |
| 1085 | + "change": "CORRECTION: acceptance[3] said (name, locale) resolution is an EXACT match with no language-subtag folding, and its verify line scored a zh recipient getting the en-US row as the pass. Since #28880 a named locale, over the built-in registry template loader, is folded against the bundle's tags by resolveBundleLocale, then the stack's declared fallback locale is tried folded the same way, then en-US, then TEMPLATE_NOT_FOUND, so a run against main scored the clause wrong. The clause now copies the ladder of record, SendTemplateInput.locale in packages/spec/src/contracts/email-service.ts, and keeps en and en-US as two rows of one bundle; the verify line and steps[4] add a zh recipient (folds to zh-CN) and a recipient whose language the bundle lacks (declared fallback en folds to en-US); variants[0] names the fold and fallback rungs; automated.ref no longer calls the uncovered half 'the no-folding case' and names the plugin-email fold pins below the oracle's layer. Read from source at base 8ca9dd7b", |
| 1086 | + "ref": "#29032" |
1081 | 1087 | } |
1082 | 1088 | ] |
1083 | 1089 | }, |
|
0 commit comments