test(09.1): persist human verification items as UAT

This commit is contained in:
lorentz 2026-05-11 06:50:10 -04:00
parent c4fb6e22a4
commit 1de6c02c3b
2 changed files with 149 additions and 0 deletions

View file

@ -0,0 +1,32 @@
---
status: partial
phase: 09.1-ntfy-backend-fix
source: [09.1-VERIFICATION.md]
started: 2026-05-11T06:48:00Z
updated: 2026-05-11T06:48:00Z
---
## Current Test
[awaiting human testing]
## Tests
### 1. End-to-end personal ntfy delivery
expected: On /mobile/profile open Channels section. Enable ntfy. Verify the minted topic shown begins with `pulse-me-`. Scan/open the QR code or subscribe link — it points at `https://ntfy.wulfconsulting.cloud/pulse-me-...`. Tap "Test now" and confirm a notification arrives on your ntfy app within a few seconds.
result: [pending]
### 2. ntfy custom-topic inline error rejects bad input
expected: Open the Channels section, expand 'Edit advanced' on ntfy, enter `bad-topic` (or any value not starting with `pulse-me-`). Save. Inline `text-xs text-destructive` error shows below the input reading "Topic must start with pulse-me-..." (or similar). No success toast fires.
result: [pending]
## Summary
total: 2
passed: 0
issues: 0
pending: 2
skipped: 0
blocked: 0
## Gaps

View file

@ -0,0 +1,117 @@
---
phase: 09.1-ntfy-backend-fix
verified: 2026-05-11T07:00:00Z
status: human_needed
score: 6/6 must-haves verified
re_verification: false
human_verification:
- test: "Personal ntfy channel end-to-end delivery on mobile"
expected: "Open /mobile/profile, enable mobile push, confirm minted topic starts with pulse-me-, confirm QR code value reads https://ntfy.wulfconsulting.cloud/pulse-me-XXXXXXXX, scan QR with ntfy app, tap 'Test now' — notification arrives on device with the company server bearer auth"
why_human: "Cannot verify actual ntfy delivery to a physical device programmatically; requires NTFY_PULSE_TOKEN to be set and the company ntfy server to be reachable"
- test: "Custom-topic inline error on 400"
expected: "Open 'Edit advanced', enter 'bad-topic' (no pulse-me- prefix), tap 'Save custom topic'. Server returns 400. The destructive error paragraph renders the server's message string below the help line."
why_human: "Requires a running server with a live session to trigger the 400 path and observe inline rendering"
---
# Phase 09.1: ntfy Backend Fix — Verification Report
**Phase Goal:** Close the major gap surfaced in Phase 09 UAT — personal ntfy channels must use the company server (https://ntfy.wulfconsulting.cloud) with bearer auth via NTFY_PULSE_TOKEN and the `pulse-me-` topic prefix, while global/admin ntfy channels continue to honor their per-channel server_url + auth_token config.
**Verified:** 2026-05-11T07:00:00Z
**Status:** human_needed
**Re-verification:** No — initial verification
## Goal Achievement
All 6 must-have truths are VERIFIED in code. Two human spot-checks remain for end-to-end delivery confirmation and inline-error UI rendering.
### Observable Truths
| # | Truth | Status | Evidence |
|---|-------|--------|----------|
| 1 | Personal ntfy channels mint topics with the `pulse-me-` prefix (8 hex chars of entropy) | VERIFIED | `personal-channels.ts` line 52: `NTFY_TOPIC_RE = /^pulse-me-[A-Za-z0-9-]{6,64}$/`; line 65: `return \`pulse-me-${id}\`` |
| 2 | Custom ntfy topics accepted only when they match `^pulse-me-[A-Za-z0-9-]{6,64}$``pulse-`, `noc-`, `soc-`, and arbitrary names rejected | VERIFIED | `isValidNtfyTopic` uses `NTFY_TOPIC_RE` unchanged; regex updated in place at line 52 |
| 3 | All four ntfy send paths (sendChannelTest, notify.ts sendNtfy, approval.ts ntfy branch, ticket-digest-service.ts ntfy branch) target the company server + Bearer NTFY_PULSE_TOKEN for personal channels | VERIFIED | All four files branch on `owner_user_id`/`isPersonal` and read `process.env.NTFY_BASE_URL \|\| 'https://ntfy.wulfconsulting.cloud'` and `process.env.NTFY_PULSE_TOKEN` (see Artifacts table) |
| 4 | QR code and subscribe link in /mobile/profile point at the company ntfy host (NEXT_PUBLIC_NTFY_BASE_URL) | VERIFIED | `ProfileChannelsSection.tsx` line 35: `const NTFY_BASE = process.env.NEXT_PUBLIC_NTFY_BASE_URL \|\| 'https://ntfy.wulfconsulting.cloud'`; used at lines 288, 293, 301; `ntfy.sh` not present in file |
| 5 | Inline error copy under the custom-topic Input reads 'Topic must start with pulse-me-' on 400 | VERIFIED (static help line) | `ProfileChannelsSection.tsx` line 324-326: `<p className="text-xs text-muted-foreground">Topic must start with <code>pulse-me-</code>.</p>` renders between Input and server-error paragraph; server-error paragraph unchanged |
| 6 | Global/admin ntfy rows (owner_user_id IS NULL) continue to use config.server_url + config.auth_token — only personal channels forced to company server + NTFY_PULSE_TOKEN | VERIFIED | All four send sites: `global` branch reads `channel.config.server_url \|\| 'https://ntfy.sh'` and `channel.config.auth_token`; `isPersonal = !!channel.owner_user_id` gates the two paths cleanly |
**Score:** 6/6 truths verified
### Required Artifacts
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `lib/services/personal-channels.ts` | Updated NTFY_TOPIC_RE, mintNtfyTopic, sendChannelTest forcing company server + bearer auth | VERIFIED | Line 52: `NTFY_TOPIC_RE = /^pulse-me-[A-Za-z0-9-]{6,64}$/`; line 65: `return \`pulse-me-${id}\``; lines 110-133: `sendChannelTest` ntfy case uses `NTFY_BASE_URL` + `NTFY_PULSE_TOKEN`, drops `channel.config.auth_token` path |
| `lib/services/pipeline-steps/notify.ts` | sendNtfy branches on owner_user_id; personal → company server + NTFY_PULSE_TOKEN | VERIFIED | Lines 368-396: `isPersonal = !!channel.owner_user_id`; personal branch reads `process.env.NTFY_BASE_URL` and `process.env.NTFY_PULSE_TOKEN`; global branch reads `channel.config.server_url` and `channel.config.auth_token` |
| `lib/services/pipeline-steps/approval.ts` | ntfy approval branch applies personal/global split | VERIFIED | Lines 113-141: `isPersonal = !!channel.owner_user_id`; personal → `NTFY_BASE_URL` + `NTFY_PULSE_TOKEN` (soft fallback when token missing, matching existing best-effort posture) |
| `lib/services/ticket-digest-service.ts` | deliver() ntfy branch applies personal/global split; both SELECTs include owner_user_id | VERIFIED | Line 159 (getAvailableChannels SELECT) and line 612 (deliver SELECT) both include `owner_user_id`; lines 648-665: ntfy branch reads `isPersonal` via type cast; personal → `NTFY_BASE_URL` + `NTFY_PULSE_TOKEN` |
| `components/mobile/profile/ProfileChannelsSection.tsx` | NTFY_BASE constant + QR + subscribe link target NEXT_PUBLIC_NTFY_BASE_URL; inline error copy updated | VERIFIED | Line 35: `NTFY_BASE` constant; lines 288, 293, 301: `NTFY_BASE` used for href, rendered text, QRCodeSVG value; line 324-326: help line added; `ntfy.sh` not present anywhere in file |
### Key Link Verification
| From | To | Via | Status | Details |
|------|----|-----|--------|---------|
| `personal-channels.ts mintNtfyTopic` | minted topic value returned to PUT /api/me/channels/ntfy | returns `pulse-me-${id}` | WIRED | Line 65: `return \`pulse-me-${id}\``; regex on line 52 validates the same prefix |
| `personal-channels.ts sendChannelTest (ntfy)` | `process.env.NTFY_BASE_URL` + `process.env.NTFY_PULSE_TOKEN` | `Authorization: Bearer ${token}` on POST | WIRED | Lines 114-126: `serverUrl` from `NTFY_BASE_URL`, `token` from `NTFY_PULSE_TOKEN`, header set at line 125 |
| `ProfileChannelsSection.tsx` | `process.env.NEXT_PUBLIC_NTFY_BASE_URL` | `QRCodeSVG value` prop + subscribe `href` | WIRED | Line 35: `NTFY_BASE` reads env var; lines 288 and 301 use `NTFY_BASE` in href and QRCodeSVG value |
| `notify.ts sendNtfy` | company ntfy server for personal channels | `isPersonal` branch on `channel.owner_user_id` | WIRED | Line 372: `isPersonal = !!channel.owner_user_id`; line 374: personal path reads `NTFY_BASE_URL`; line 389-393: token from `NTFY_PULSE_TOKEN` |
| `approval.ts ntfy block` | company ntfy server for personal channels | `isPersonal` branch | WIRED | Lines 116-130: identical personal/global split pattern |
| `ticket-digest-service.ts deliver() ntfy block` | company ntfy server for personal channels | `isPersonal` branch + owner_user_id in SELECT | WIRED | Line 652: `isPersonal` via type cast; line 612 SELECT includes `owner_user_id` |
### Data-Flow Trace (Level 4)
Not applicable — these are notification dispatch utilities, not data-rendering components. The "data" (env vars, channel config) is scalar and verified at the code level via grep.
### Behavioral Spot-Checks
| Behavior | Command | Result | Status |
|----------|---------|--------|--------|
| TypeScript type check (no new type errors) | `npx tsc --noEmit` | 0 lines output (exit 0) | PASS |
| notify.ts vitest (mute semantics not regressed) | `npx vitest run lib/services/pipeline-steps/notify.test.ts` | 1 test file, 1 test passed | PASS |
| `ntfy.sh` absent from ProfileChannelsSection | `grep -n "ntfy.sh" ProfileChannelsSection.tsx` | (no output) | PASS |
| `ntfy.sh` absent from personal-channels.ts | `grep -n "ntfy.sh" personal-channels.ts` | (no output) | PASS |
| owner_user_id in all three pipeline files | `grep -n "owner_user_id" notify.ts approval.ts ticket-digest-service.ts` | Multiple matches in each file | PASS |
| NEXT_PUBLIC_NTFY_BASE_URL in ProfileChannelsSection | `grep -n "NEXT_PUBLIC_NTFY_BASE_URL" ProfileChannelsSection.tsx` | Line 35: NTFY_BASE constant | PASS |
| pulse-me- help text rendered in advanced section | `grep -n "pulse-me-" ProfileChannelsSection.tsx` | Line 325: help paragraph found | PASS |
| ntfy.sh only appears in global channel fallback paths | `grep -n "ntfy.sh" notify.ts approval.ts ticket-digest-service.ts` | Present only as `channel.config.server_url \|\| 'https://ntfy.sh'` in the `!isPersonal` branch | PASS |
| Both ticket-digest SELECTs include owner_user_id | lines 159, 612 in ticket-digest-service.ts | Both SELECT strings include `owner_user_id` | PASS |
| Task commits exist | `git log --oneline` | 2ff2dc9 (backend 4 files), 985728a (ProfileChannelsSection) | PASS |
### Requirements Coverage
| Requirement | Source Plan | Description | Status | Evidence |
|-------------|------------|-------------|--------|----------|
| CHAN-03 | 09.1-01-PLAN.md | ntfy topic is Pulse-minted on first save with UUID-prefixed topic | SATISFIED | `mintNtfyTopic()` now returns `pulse-me-XXXXXXXX`; regex enforces that format |
| CHAN-05 | 09.1-01-PLAN.md | On save, API issues best-effort test send; test result surfaces inline | SATISFIED | `sendChannelTest` (ntfy case) rewired to company server + bearer auth; test result shape unchanged |
| CHAN-07 | 09.1-01-PLAN.md | /api/me/channels routes auth-gated to session.user.id | SATISFIED | Routes unchanged (Phase 09 wired this); bearer auth for ntfy test sends now uses NTFY_PULSE_TOKEN not channel.config |
| ROUTE-04 | 09.1-01-PLAN.md | When user route fails, fallback to global channel_id with user_route_fallback recorded | SATISFIED | Fallback logic in notify.ts unchanged; personal-channel ntfy sends now succeed where they previously 401'd (wrong server), reducing spurious fallbacks |
### Anti-Patterns Found
| File | Line | Pattern | Severity | Impact |
|------|------|---------|----------|--------|
| `lib/services/ticket-digest-service.ts` | 652 | Type cast `(ch as NotificationChannel & { owner_user_id?: string | null })` | INFO | Deliberate workaround documented in plan: local `NotificationChannel` interface not extended to avoid type churn; cast is safe because SELECT now includes the column. Not a blocker. |
| `lib/services/pipeline-steps/approval.ts` | 128-131 | Soft fallback: sends unauthenticated when `NTFY_PULSE_TOKEN` missing | INFO | Intentional design decision documented in plan and threat model (T-09.1-05). Matches approval.ts existing best-effort posture. notify.ts uses a loud error instead — appropriate distinction. |
### Human Verification Required
#### 1. Personal ntfy channel end-to-end delivery
**Test:** Sign into Pulse on mobile. Open `/mobile/profile`. Enable mobile push. Confirm the minted topic starts with `pulse-me-`. Confirm the subscribe link text and QR code URL both read `https://ntfy.wulfconsulting.cloud/pulse-me-XXXXXXXX`. Scan QR with the ntfy app to subscribe. Tap "Test now". Confirm a notification arrives.
**Expected:** The notification is delivered via `https://ntfy.wulfconsulting.cloud/{topic}` with bearer auth. The ntfy app (subscribed to that topic) receives the message "Pulse channel verified — you can ignore this message."
**Why human:** Requires `NTFY_PULSE_TOKEN` to be set in the runtime env, the company ntfy server to be reachable, and a physical device running the ntfy app. Cannot simulate network delivery programmatically.
#### 2. Custom-topic inline error on 400
**Test:** With an ntfy channel minted, open "Edit advanced" in the ntfy section of `/mobile/profile`. Enter `bad-topic` (no `pulse-me-` prefix). Tap "Save custom topic".
**Expected:** Server returns HTTP 400. The muted help line `Topic must start with pulse-me-` is visible above a new `text-destructive` paragraph containing the server's error message (e.g. "Topic must start with pulse-me-"). No success toast appears.
**Why human:** Requires a running Next.js dev or prod server with an authenticated session to trigger the PUT 400 path and observe the inline error rendering behavior.
### Gaps Summary
No gaps. All six must-have truths are satisfied by code that exists, is substantive, and is wired to the correct data sources. The two human verification items are confirmational (delivery and UI rendering) and do not represent structural gaps — all code paths are verified correct. The phase goal is achieved at the implementation level.
---
_Verified: 2026-05-11T07:00:00Z_
_Verifier: Claude (gsd-verifier)_