Skip to content

AL-2114 — [P28-M03] Send an authorized real newsletter test safely #2114

Open

Description

@cobmojo

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

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.

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

  1. changed the title [-][P28-M03] Send an authorized real newsletter test safely[/-] [+]AL-2114 — [P28-M03] Send an authorized real newsletter test safely[/+] on Oct 19, 2026
  2. II-ricky-bobby-II commented on Oct 25, 2026

    @II-ricky-bobby-II
    Contributor

    Spec r1, Scope, Oct 24, 2026, pending Samson approval

    Status: r1 for #2114 (P28 M03), base develop e483673. 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 are unavailable in 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

    §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.

    1. Entry. A Send real test action in M01's editor, for a saved revision only. The requester needs newsletters.edit on the issue in a current AL-2069 — [P28-W01] Enter and switch the exact authorized workspace context #2069 context.
    2. Fixed request shape. The route accepts only { 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.
    3. Qualified reviewer. reviewerRef must resolve to a verified sign-in user (the canonical human) who currently holds newsletters.read on 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.
    4. Render. Through 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.
    5. Prepare and submit. M02's native test source records the run and prepares the envelope; TestSubmitPort ([Phase 17] Enforce the provider-boundary state machine and exhaustive Resend failure taxonomy #897) submits it. Production adapters are unavailable, so the action shows "Real tests aren't available yet" and preview stays usable.
    6. Evidence. newsletter_test_sends records 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.
    7. Outcome. The result names the tested revision, the reviewer and one of: Prepared, Submission may have begun, Accepted by provider, Rejected by provider, Recovery unknown. A later provider observation is shown only if it's a signed observation matching this envelope's owner ([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). The copy says a successful test doesn't show the issue is ready for its audience or delivered to anyone else.
    8. Retry. The same idempotency key returns the same test. Retry on a definite rejection makes a new test of the same revision and reviewer. While the original is Submission may have begun or Recovery unknown, a replacement is refused with "We can't tell yet whether the first test went out".
    9. Outage. A provider or render outage fails only the test action; preview and drafting stay usable.

    Threat model

    Threat Control Criteria
    Draft disclosed to an arbitrary address Server-resolved verified sign-in address; fixed request shape A1, S1
    Reviewer without current access gets the draft newsletters.read checked at request and at admission A1
    Caller-supplied From or API key Rejected at schema; no parameter S1
    Subscriber private values in a test Safe sample personalization only A2
    Recipient uses a live unsubscribe or action Inert actions in test render A2
    Test counted as a send or engagement Operational evidence only; no Party event A3
    Forged provider callback changes the outcome Signed matching-owner observations only A3
    Duplicate test from retry Idempotency key; replacement refused while uncertain A4
    Outage blocks drafting Test action isolated A4

    Non-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_sends table and function, the render and submit ports with unavailable and test adapters, the dialog and outcome panel in M01's editor, docs and tests.

    §3 Out of scope

    §4 Criteria

    Tags: [A] Acceptance-owned test, [B] Builder test, [S] static, [R] real PostgreSQL (#2250's step).

    • A1 [A][R] Reviewer qualification (AC28-M16-02).
      • A reviewer who is verified and holds newsletters.read gets one test at their verified sign-in address.
      • An unverified user, a user without newsletters.read, a revoked holder and an other-tenant user are each denied, with no render, run, envelope or test row.
      • Revoking the reviewer's access after the request and before admission stops it at the fence.
      • A requester without newsletters.edit, or in a stale AL-2069 — [P28-W01] Enter and switch the exact authorized workspace context #2069 context, is denied.
    • A2 [A] Test content (AC28-M16-02). The rendered test (test adapter) is revision 5 exactly, has the "[Test]" marking, uses sample personalization only (a census finds no real supporter field), and its unsubscribe and protected actions make no request when activated.
    • A3 [A][R] Outcome (AC28-M16-03).
      • A provider-accepted test shows revision 5, Lee and "Accepted by provider", with the no-readiness copy.
      • The test writes one newsletter_test_sends row and no Party, business-send or engagement event (census of those tables).
      • An unsigned or other-owner observation doesn't change the outcome; a signed matching one does.
    • A4 [A][R] Retry and outage.
      • The same key returns the same test; a definite rejection allows Retry; Submission may have begun and Recovery unknown refuse a replacement.
      • With the submit adapter unavailable, the action says real tests aren't available yet, and preview and save still work.
    • B1 [B][R] record_newsletter_test_send replays on the same key and conflicts on the same key with another revision or reviewer.
    • S1 [S] The route schema is exactly the four fields (strict); no M03 module imports packages/email/resend/* or packages/api/src/email/*test-send*; the new function is in GUARDED_RPCS.
    • C1 [A] Journey. On a real stack with test adapters, at desktop, keyboard-only and with screen-reader names checked: Maya opens a saved revision, sends a real test to Lee, sees the accepted outcome with the revision; tries a reviewer without access and is denied; with the provider unavailable, still previews and saves. Axe clean.

    Handback

    §5 Mapping

    Ticket requirement Criteria
    P17 test-purpose binding, P6 sole spine; reviewer, sender/reply, tested revision A1, A2, A3
    Inert test actions; safe sample personalization A2
    Resend sandbox path, signed observations, exact safe retry, refusal of uncertain replacement A3, A4, handback gate
    No real supporters; no caller From/API key/destination A1, S1
    Outage leaves preview and drafting usable A4, C1
    AC28-M16-02 unauthorized reviewer denied; marked test with no private values or live unsubscribe A1, A2
    AC28-M16-03 revision and outcome identified; no Party event; no readiness claim A3

    §6 Declared edits

    File Change Owner
    supabase/migrations/<VERSION>_newsletter_test_sends.sql + rollback Table and function Builder
    packages/api/src/newsletters/real-tests/** (new) Request service, reviewer qualification, render and submit ports with unavailable and test adapters Builder
    apps/missionary/app/api/newsletters/[issueId]/real-tests/route.ts (new) Test request route Builder
    apps/missionary/app/feed/newsletters/** Send real test dialog and outcome panel Builder
    packages/api/src/identity/tenant-guard.ts New GUARDED_RPCS entry only Builder
    docs/features/native-newsletters/real-tests.md (new) Reviewer, content, evidence and retry rules Builder
    tooling/eslint-config/suppressions.json, tooling/shadscan/findings.json Refresh only (§7) Builder
    tests/unit/acceptance/issue-2114/** A1–A4, S1 Acceptance
    tests/e2e/newsletter-real-test.spec.ts (new) C1 Acceptance

    §7 Pins this change touches

    Pin Where In or out Criterion
    Email send seam and template test-send routes packages/email/resend/*, packages/api/src/email/* Out: unchanged and not imported S1
    M01 editor; M02 test source #2112's apps/missionary/app/feed/newsletters/**; #2113's native_issue_test_runs In: one dialog and panel; M02 used through its service A3, C1
    Missionary navigation list tests/unit/apps/missionary/missionary-navigation-search.test.tsx:67-75 Out (no nav item) —
    Donor detail suppressions tooling/eslint-config/suppressions.json:1181-1188 Out: the donor detail isn't touched —
    Shadscan app floors scripts/verify/shadscan.mjs (ci.yml "Enforce complete Shadscan app floors and reviewed findings") In: no new unreviewed finding H4
    #2250 step and order issue-2250/static.acceptance.test.ts Out (no step) H1
    Workflow fingerprints issue-2155/…:257-280 (bc8d57a2be564d61), issue-2201/…:166-178 (1078832efaff84e6) Out under the OQ5 default —
    docs/ci.md byte pin issue-2250 prefix pin and #2304 Out: not edited —
    Phase 28 PRD archive verify:phase28-spec (ci.yml :91), 911 files under docs/features/missionary-workspace-depth/grill/** Out: none edited —

    §8 CI

    No new CI step and no docs/ci.md edit under the OQ5 default. Real-DB tests run in "Verify tenant isolation (real stack)" (bun x vitest run realstack.acceptance, env TENANT_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

    • New table: newsletter_test_sends (tenant-scoped, same-tenant foreign keys to the issue, revision and M02 run; append-only outcome history; RLS and FORCE RLS; no anon/authenticated/service_role table privileges). It stores the reviewer's user id and no address.
    • Changed tables: none.
    • Grants: 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.
    • person_id: the reviewer is identified by verified sign-in user id. M03 reads and writes neither profiles.person_id nor missionaries.person_id. No [Security] missionaries.person_id column-grant fix: anon cross-tenant read + select("*") routes (follow-up from #2256) #2268 block is needed.
    • Donor spine: not touched. Tests use sample data; no donor or donation row is read.

    §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

    §12 Open Questions (proposed defaults in bold)

    1. Owners unbuilt. [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 (and [Phase 17] Tenant-owned Resend connection setup — one proved account, encrypted revisions, no shared fallback #893, [Phase 17] Per-connection Resend webhook authority — raw-body verification and monotonic outcome reduction #894, [Phase 17] Seal contract-owned headers, Sender Profiles, and human-reply routing #895 behind them) are open. Default: M03 merges the real route, qualification, evidence table and UI now, on test adapters, with render and submit unavailable in 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.
    2. Who counts as a reviewer. Default: a verified sign-in user who currently holds newsletters.read on this issue; the destination is always their verified sign-in email. No external or typed address. Flagged for Samson.
    3. Who can request a test. Default: newsletters.edit holders on the issue; newsletters.send isn't needed, since a test isn't an audience send. Flagged for Samson.
    4. Signals. M02's body-free signals cover the test path. Default: M03 adds no new signal; M02's signals name <P6_DELIVERY_ONCALL> (new in AL-2113 — [P28-M02] Prepare one native test message through the missing P6 sealed-submission producer #2113, activation only).
    5. Journey in CI. Default: no CI step; local run recorded. The alternative is a test-e2e step "Newsletter real test journeys" with run text node 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-pins docs/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

    §14 Flags

  3. II-ricky-bobby-II commented on Oct 25, 2026

    @II-ricky-bobby-II
    Contributor

    Approval (Samson, factory lead, Oct 24, 2026, 10:34 PM PT), relayed by Scope:

    All five r1s are approved. M03 approved, including OQ2 (the reviewer is a verified sign-in user holding newsletters.read on the issue; the test goes only to their sign-in email) and OQ3 (requesters need newsletters.edit, not newsletters.send). Version (prefix 20261026): M03 20261026156000. <P6_DELIVERY_ONCALL> is pending; it holds activation only. Defaults accepted, recorded as departures where they narrow. CI-step OQs closed: no.

    No other open questions.

  4. II-ricky-bobby-II commented on Oct 25, 2026

    @II-ricky-bobby-II
    Contributor

    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.
    • Activation still waits on the wiring gates in the approval; the build isn't held.
  5. II-ricky-bobby-II commented on Oct 25, 2026

    @II-ricky-bobby-II
    Contributor

    Pins addendum (Samson, factory lead, Oct 24, 2026, 11:01 PM PT), relayed by Scope:

    This amends Samson's approval (#2114 (comment)).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    ReadyBlake-approved for the agent factory to pick up (oldest first)complexity:hardHigh complexity or scopetype:featureNew functionality

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions