Repository navigation
AL-2114 — [P28-M03] Send an authorized real newsletter test safely #2114
Description
AL-2114 — [P28-M03] Send an authorized real newsletter test safely
Approved implementation ticket. Plan key: P28-M03. Work only after its real blockers and exact qualification gates are satisfied. Publication does not establish implementation, qualification or activation.
Parent
Phase 28 implementation specification #2064. Canonical specification commit e93e2e9958f32380e28ec0e005d7ef3ee24f7e7a; documentation PR #2065. The full published spec and comments were verified against the canonical source. Preserve the parent’s body, title, labels, state and comments; new native child/dependency relationships are metadata only.
What to build
An author can send the saved revision as clearly marked real test mail to one current access-confirmed reviewer, then see the exact operational outcome without subscribing anyone.
Source of truth and acceptance ownership
- Communications: part 1 · part 2 · part 3 · part 4
- Common safeguards and proof: part 1 · part 2
- Owner reconciliation and later-decision precedence: part 1
- Evidence and existing seam limits: part 1
Primary requirement references: REQ28-M16. Stories: US28-M16.
The checked criteria below are this ticket’s primary trace ownership, not a relaxation of the full normative contract. For a requirement split across tickets, this ticket delivers its named behavior and enforces applicable invariants immediately; the other ticket’s full scenario is not claimed complete early. All source-specific state/control, numeric-boundary, owner, monitoring and exclusion sections in the linked volume remain binding. Preserve settled D1–D50 and C01–C20; do not reopen them or import contradictory vendor conventions.
Acceptance criteria
- AC28-M16-02 — Given a real test request to an unconfirmed or unauthorized reviewer, when it is accepted for dispatch, then disclosure is denied; a qualified reviewer receives a clearly marked test with no subscriber-specific private values or usable production unsubscribe action.
- AC28-M16-03 — Given a test is provider-accepted, when its result is shown, then the tested revision and operational outcome are identified without creating a Party business event or claiming audience readiness or universal delivery.
Implementation decisions and slice boundaries
- Use P17 native authored test-purpose binding and the P6 sole delivery spine; review exact qualified reviewer, sender/reply and tested content revision. Test actions remain inert and recipient personalization uses safe sample data.
- Qualify the complete supported Resend sandbox path, matching-owner signed observations, exact safe retry and refusal of uncertain replacement. No real supporters or arbitrary caller From/API-key/destination override.
- Provider/test outage leaves inert preview and manual drafting usable.
Binding normative decisions
REQ28-M16 — Preview and test safely
Story US28-M16: As an author, I want accurate desktop/mobile preview and optional real tests, so that I can assess presentation without sending the issue accidentally.
Decision basis: D7 C08; Phase 17 preview/test source qualification and Phase 6 operational evidence.
The following behavior is required.
- Inert preview uses actual saved structure, sender/reply, subject/footer/link
representation and safe sample personalization, with plain text available. - Real test is explicitly labeled as real mail to an access-confirmed qualified
reviewer whose current authority permits the actual draft disclosure. - No arbitrary destination receives live subscriber fields; production
unsubscribe/protected response actions are inert in tests. Test creates
operational evidence, not a Party/business send or engagement event. - Identify tested content revision. A successful test does not prove audience
readiness or delivery to every subscriber; preview remains useful if tests fail.
Testing, migration and operating obligations
Every reached layer belongs to this slice: actual source persistence and upgrade behavior, canonical command/query, permitted UI, and executed proof. Reuse native owners, shared Base UI/base-maia components and the established architecture. No parallel CRM, Tasks, consent, sender, permission or retention engine.
Before affected activation, prove current trusted tenant/actor/subject/role and fine-grained purpose/field authority, effective old-row/new-row RLS and grants, same-tenant relationships, applicable uniqueness/history/delete constraints, and every reached view/RPC/storage/service path. Caller identifiers and metadata are never authority. Source revision and privacy checks precede disclosure and are repeated after ordering waits where required.
Implement semantic idempotency, required source history/receipt and durable secondary intent, truthful partial/unknown/lost-response recovery, context-switch/revocation fencing, temporal boundaries and correction lineage. Preserve each adopted default and exact Off/new-work/accepted-work/reenable behavior. Never infer consent, money, completion or actual interaction from a presentation action.
Test through existing Playwright whole-user journeys as the primary boundary; use canonical API Vitest tests, real PostgreSQL roles and at least two concurrent sessions, qualified Auth and existing provider-contract seams where the browser cannot prove the guarantee. Reuse existing account-frame/session-guard, task-recovery, roster/redaction, operation-handler, dispatch/restriction, field-policy SQL and replay proof patterns while retaining their demo/mock limits. No new test framework and no skipped mandatory fixture may count as a pass.
Use persistent truthful outcomes, clear context and next steps, keyboard/screen-reader/touch/list equivalents, narrow-screen reflow, localization, safe international data and honest interrupted-connection recovery. Inventory and fence reached legacy writers/readers, preserve lawful history and unresolved obligations, and qualify mixed versions, restore, retention and containment before activation. Adopt existing source-specific signals/thresholds/owners/responses and measured cohort budgets; do not invent a numeric SLO or defer correctness to monitoring.
Shared requirements Q01–Q27 apply where relevant to every ticket. R43, F41–F44, M41–M42 and C49 apply to their affected lanes. W05/W06 aggregate composed evidence; they are not permission to defer any slice’s safeguards.
Related source owners (references are not automatically blockers): #879, #893, #894, #895, #897, #903. Current source contract takes precedence over superseded issue wording.
Blocked by
A blocker’s closure is not runtime qualification: verify the exact consumed revision and executed proof. Keep independently qualified capabilities useful while an affected effect is unavailable. Integrate the approved specification into the implementation base; the open documentation PR is not evidence that these features exist.
Related approved slices
Short ticket keys in implementation notes resolve here. Published requirement, acceptance, story, decision and completeness codes retain their original meanings.
- P28-M01 / #2112 — Prepare and recover a native newsletter draft
- P28-W05 / #2151 — Recover and contain the composed workspace after restore
- P28-W06 / #2152 — Qualify whole journeys and the exact Phase 28 release cohort
The full execution index and primary-acceptance ownership map are published with the Phase 28 release-qualification ticket. Every shared safeguard remains mandatory within its implementing slice.
Activity
[-][P28-M03] Send an authorized real newsletter test safely[/-][+]AL-2114 — [P28-M03] Send an authorized real newsletter test safely[/+]II-ricky-bobby-II commentedon Oct 25, 2026
Spec r1, Scope, Oct 24, 2026, pending Samson approval
Status: r1 for #2114 (P28 M03), base
develope483673. The issue body is the source of intent; this spec is the build contract. M03 adds a real test send of a saved newsletter revision to one current, access-confirmed reviewer. The server picks the reviewer's own verified sign-in address, renders a clearly marked test with safe sample data and inert actions, prepares it through M02's P6 producer, submits it through #897's provider boundary and shows the tested revision with its exact operational outcome. The test isn't a Party business event and makes no audience-readiness claim. It fits both caps, so there's no split. Natively blocked by #2112, #897 and #903, which matches the body's "Blocked by" list. #2113 (M02) is upstream of #897, so it's mentioned and not linked. The body's related owners #879, #893, #894 and #895 are reached through #897 and #903 and aren't linked. #897, #903 and the P17/P6 owners behind them are open and unbuilt, so the render and submit adapters areunavailablein production and the editor says real tests aren't available yet (OQ1). Context read: #2064 (communications, common safeguards, owner reconciliation and evidence comments), the #879, #893, #894, #895, #897 and #903 bodies, the #2112 and #2113 r1 specs, and the communications volume (REQ28-M16, M41–M42).§0 What I checked
e4836737(Oct 24, 10:21 PM PT; fix(donations): read donors through the real FK and stop swallowing donations errors (#2269) #2319 merged, no migration or cited file changed). AL-2114 — [P28-M03] Send an authorized real newsletter test safely #2114's body, its 2 checked criteria (AC28-M16-02, AC28-M16-03) and REQ28-M16. The issue has 0 comments, so this is r1.newsletters.*atoms and the inert preview (AC28-M16-01 is M01's).packages/email/resend/send.ts. The existing admin template test-send routes (packages/api/src/email/template-test-send.ts,test-send.ts) serve system email templates; M03 doesn't reuse or change them (S1). The missionary editor and newsletter routes don't exist yet (M01).§1 Intended behavior
Maya wants to see her saved revision 5 in a real inbox. In the editor she chooses Send real test and picks Lee, a reviewer who can already read this issue. The dialog says it's real email to Lee's own sign-in address and that it won't reach supporters. Lee gets a message marked [Test] with sample names, and its buttons and unsubscribe link do nothing. Maya sees "Test of version 5 accepted by the email provider for Lee", not "Delivered to your audience". If she picks someone without access, nothing is sent and she's told why. If the provider is down, preview and drafting still work.
newsletters.editon the issue in a current AL-2069 — [P28-W01] Enter and switch the exact authorized workspace context #2069 context.{ issueId, revisionId, reviewerRef, idempotencyKey }. Any From, Reply-To, API key, destination address, subject, HTML or personalization field is rejected at the schema, and no service parameter accepts one.reviewerRefmust resolve to a verified sign-in user (the canonical human) who currently holdsnewsletters.readon this issue. The destination is that user's verified sign-in email, resolved on the server. An unverified, unknown, revoked or other-tenant reviewer is denied before anything is rendered (AC28-M16-02). The check repeats at M02's admission fence, so authority revoked while the test waits stops it.TestRenderPort([Phase 17] Render complete accessible recipient email and plain text from one frozen message #903's test-purpose binding): the exact saved revision, sender and reply identity from [Phase 17] Seal contract-owned headers, Sender Profiles, and human-reply routing #895 through that binding, a "[Test]" subject marking and banner, safe sample personalization, and inert unsubscribe and protected actions. No subscriber-specific private value is rendered.TestSubmitPort([Phase 17] Enforce the provider-boundary state machine and exhaustive Resend failure taxonomy #897) submits it. Production adapters areunavailable, so the action shows "Real tests aren't available yet" and preview stays usable.newsletter_test_sendsrecords the issue, revision, requester, reviewer, envelope ref, idempotency key and outcome. It's operational evidence only: no Party, business-send or engagement event is written.Threat model
newsletters.readchecked at request and at admissionNon-goals: the inert preview (M01); the P6 producer (M02); the #897 provider boundary, #894 observation reduction, #895 sender identity, #893 sandbox and #903 binding; audience send; changes to the admin template test-send routes.
§2 In scope
The test request route, reviewer qualification, the
newsletter_test_sendstable and function, the render and submit ports withunavailableand test adapters, the dialog and outcome panel in M01's editor, docs and tests.§3 Out of scope
packages/email/resend/*and the existing email test-send routes.docs/ci.mdand the byte-pinned Phase 28 PRD archive.§4 Criteria
Tags: [A] Acceptance-owned test, [B] Builder test, [S] static, [R] real PostgreSQL (#2250's step).
newsletters.readgets one test at their verified sign-in address.newsletters.read, a revoked holder and an other-tenant user are each denied, with no render, run, envelope or test row.newsletters.edit, or in a stale AL-2069 — [P28-W01] Enter and switch the exact authorized workspace context #2069 context, is denied.newsletter_test_sendsrow and no Party, business-send or engagement event (census of those tables).unavailable, the action says real tests aren't available yet, and preview and save still work.record_newsletter_test_sendreplays on the same key and conflicts on the same key with another revision or reviewer.packages/email/resend/*orpackages/api/src/email/*test-send*; the new function is inGUARDED_RPCS.Handback
tenant_guardschema and role ([Phase 4] #505 slice 1/4: withTenant tenant guard (non-owner, non-BYPASSRLS role, fail-closed) #2250 live), M01's and M02's tables, and thatnewsletter_test_sendsdoesn't exist. Any mismatch goes to Samson. No writes.bun run verify:shadscanpasses.§5 Mapping
§6 Declared edits
supabase/migrations/<VERSION>_newsletter_test_sends.sql+ rollbackpackages/api/src/newsletters/real-tests/**(new)unavailableand test adaptersapps/missionary/app/api/newsletters/[issueId]/real-tests/route.ts(new)apps/missionary/app/feed/newsletters/**packages/api/src/identity/tenant-guard.tsGUARDED_RPCSentry onlydocs/features/native-newsletters/real-tests.md(new)tooling/eslint-config/suppressions.json,tooling/shadscan/findings.jsontests/unit/acceptance/issue-2114/**tests/e2e/newsletter-real-test.spec.ts(new)§7 Pins this change touches
packages/email/resend/*,packages/api/src/email/*apps/missionary/app/feed/newsletters/**; #2113'snative_issue_test_runstests/unit/apps/missionary/missionary-navigation-search.test.tsx:67-75tooling/eslint-config/suppressions.json:1181-1188scripts/verify/shadscan.mjs(ci.yml "Enforce complete Shadscan app floors and reviewed findings")issue-2250/static.acceptance.test.tsissue-2155/…:257-280(bc8d57a2be564d61),issue-2201/…:166-178(1078832efaff84e6)docs/ci.mdbyte pinissue-2250prefix pin and #2304verify:phase28-spec(ci.yml :91), 911 files underdocs/features/missionary-workspace-depth/grill/**§8 CI
No new CI step and no
docs/ci.mdedit under the OQ5 default. Real-DB tests run in "Verify tenant isolation (real stack)" (bun x vitest run realstack.acceptance, envTENANT_GUARD_IT_ADMIN_DATABASE_URL), which is #2250's step and isn't on develop yet. Everything else runs in existing ci.yml steps:bun run format:check, "Run affected lint on pull requests", "Run affected typecheck on pull requests", "Run unit tests", "Run acceptance tests", Shadscan. Playwright C1 runs locally and is recorded (H3).§9 Data, grants, person_id and donor spine
newsletter_test_sends(tenant-scoped, same-tenant foreign keys to the issue, revision and M02 run; append-only outcome history; RLS and FORCE RLS; noanon/authenticated/service_roletable privileges). It stores the reviewer's user id and no address.record_newsletter_test_send(...)is reachable only through the tenant guard. No RLS policy on an existing table is dropped or recreated, so no live policy-name check is needed.profiles.person_idnormissionaries.person_id. No [Security] missionaries.person_id column-grant fix: anon cross-tenant read + select("*") routes (follow-up from #2256) #2268 block is needed.§10 Size
~760 lines of code and docs (migration and function ~140, service and ports ~260, route ~60, dialog and panel ~220, docs ~80) and ~850 lines of tests (A1–A4, B1, S1 ~620; C1 ~230). Both are inside their caps, so there's no split.
§11 Dependencies and migration
<VERSION>_newsletter_test_sends.sql, prefix 20261026.20261024120000), the 42 remote branches after a full fetch (only20261024140000,20261024141000and20261024150000are added off develop), open PRs (feat(identity): normalize identity emails and donor keys (#2254) #2291:20261024140000,20261024141000; Modernize Sentry, content, email and browser payment SDKs #2214 adds none), the reserved list (20261024: 130000 … 235900; 20261025: 010000 … 235700 as listed in the AL-2095 — [P28-F05] Copy a reviewed approved Support Plan into a goal #2095–AL-2099 — [P28-F09] Enable follow-up for a confirmed specific one-time need #2099 r1 specs; 20261026: 010000 through 141000, including 105000, 110000, 120000, 130000, 140000 and 141000), or the other requests in this batch (AL-2110 — [P28-F19] Correct campaign attribution without falsifying source or money #2110–AL-2114 — [P28-M03] Send an authorized real newsletter test safely #2114).20261026156000, after M02-c (20261026155000). Version is reserved; Samson may renumber at handback to keep merge order.§12 Open Questions (proposed defaults in bold)
unavailablein production, so no real test is sent and the action says so. Qualifying the Resend sandbox path and signed observations through [Phase 17] Enforce the provider-boundary state machine and exhaustive Resend failure taxonomy #897, and wiring [Phase 17] Render complete accessible recipient email and plain text from one frozen message #903, is a handback gate. This is a departure that narrows. The alternative, holding M03 until they merge, needs Samson's call.newsletters.readon this issue; the destination is always their verified sign-in email. No external or typed address. Flagged for Samson.newsletters.editholders on the issue;newsletters.sendisn't needed, since a test isn't an audience send. Flagged for Samson.<P6_DELIVERY_ONCALL>(new in AL-2113 — [P28-M02] Prepare one native test message through the missing P6 sealed-submission producer #2113, activation only).test-e2estep "Newsletter real test journeys" with run textnode scripts/run-with-ci-env.mjs -- node node_modules/@playwright/test/cli.js test -c playwright.missionary.config.ts tests/e2e/newsletter-real-test.spec.ts --workers=1, which re-pins both fingerprints and byte-pinsdocs/ci.md(with docs/ci.md:35 and :366 still call test-e2e informational on develop; update with the #2180 byte pin in one change (follow-up to #2161) #2304).§13 Deploy
20260515025814–20261024120000(25 applied, newest20260513173938). The dev project is 55 behind, with four20261001*collisions and an extra20260519233845. Develop's catalog is not assumed live anywhere.unavailable; (4) the real adapters after [Phase 17] Enforce the provider-boundary state machine and exhaustive Resend failure taxonomy #897 and [Phase 17] Render complete accessible recipient email and plain text from one frozen message #903 qualify; activation after<P6_DELIVERY_ONCALL>is set.newsletter_test_sendsrow exists.§14 Flags
<P6_DELIVERY_ONCALL>from AL-2113 — [P28-M02] Prepare one native test message through the missing P6 sealed-submission producer #2113; no new placeholder here.II-ricky-bobby-II commentedon Oct 25, 2026
Approval (Samson, factory lead, Oct 24, 2026, 10:34 PM PT), relayed by Scope:
20261026156000_newsletter_test_sends.sql, after M02-c's20261026155000. Version is reserved; Samson may renumber at handback to keep merge order.unavailablein production, so no real test is sent and the action says so. Qualifying the Resend sandbox path ([Phase 17] Tenant-owned Resend connection setup — one proved account, encrypted revisions, no shared fallback #893) and signed observations ([Phase 17] Per-connection Resend webhook authority — raw-body verification and monotonic outcome reduction #894) through [Phase 17] Enforce the provider-boundary state machine and exhaustive Resend failure taxonomy #897, and wiring [Phase 17] Render complete accessible recipient email and plain text from one frozen message #903's binding, plus A1–A4 rerun against them, is the gate before activation that Samson checks.newsletters.readon the issue; the test goes only to that user's verified sign-in email, resolved on the server. No external or typed address (A1, S1).newsletters.editon the issue, notnewsletters.send; a test isn't an audience send.<P6_DELIVERY_ONCALL>, which is pending with Chad for Blake. It holds activation only, not the build.docs/ci.mdedit; the Playwright run is local and recorded (H3).No other open questions.
II-ricky-bobby-II commentedon Oct 25, 2026
Pins addendum (Chad for Blake, Oct 24, 2026, 10:38 PM PT), relayed by Scope via Samson:
This amends Samson's approval (#2114 (comment)).
<P6_DELIVERY_ONCALL>(decided): Blake, until a GF operator is assigned. Code and docs use a configurable role or constant, not a handle.II-ricky-bobby-II commentedon Oct 25, 2026
Pins addendum (Samson, factory lead, Oct 24, 2026, 11:01 PM PT), relayed by Scope:
This amends Samson's approval (#2114 (comment)).
20261026155500_newsletter_test_sends.sql(was20261026156000), so the version is a valid HHMMSS clock time. It still sorts after M02-c's20261026155000([P28-M02c] #2113 follow-up: Prepared-submission restart and version proof, restore quarantine, purge fences and body-free signals #2374). Version is reserved; Samson may renumber at handback to keep merge order.