---
phase: 22
slug: approval-ui-livelink-addressable-campaign-review-and-approve
status: draft
nyquist_compliant: true
wave_0_complete: false
created: 2026-07-16
---

# Phase 22 — Validation Strategy

> Per-phase validation contract for feedback sampling during execution.

---

## Test Infrastructure

| Property | Value |
|----------|-------|
| **Framework** | vitest 4.1.5 |
| **Config file** | `/opt/stacks/pulse/vitest.config.ts` |
| **Quick run command** | `npm test -- <specific new lib/services test file>` |
| **Full suite command** | `npm test` |
| **Estimated runtime** | ~30 seconds |

**Confirmed constraint:** `vitest.config.ts`'s `test.include` is `['lib/**/*.test.ts']` only — it does NOT include `app/**` or `components/**`. React page/component code and Next.js API route handlers in this phase have **no automated test coverage under the current config**, consistent with CLAUDE.md's stated safety net (`npx tsc --noEmit --pretty`) for untested areas. Any genuinely new *pure logic* this phase introduces is extracted into a `lib/services/*.ts` file (plan 22-01) so it can be unit-tested; UI composition and route wiring fall back to type-check as the safety net.

---

## Sampling Rate

- **After every task commit:** `npx tsc --noEmit --pretty` + `npm test -- <newly-added lib/services test file>` if one exists for that task
- **After every plan wave:** `npm test` (full suite) + `npx tsc --noEmit --pretty`
- **Before `/gsd:verify-work`:** Full suite must be green + manual click-through of both new pages (list page + ticket-scoped review page) in all four states: full-campaign, D-07 not-yet-triaged, D-08 ungrouped-report, load-error
- **Max feedback latency:** ~30 seconds (full suite)

---

## Per-Task Verification Map

| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------|
| 22-01-T1 (resolver) | 22-01 | 1 | REVIEW-01 | T-22-01/02 (V5 Input Validation) | `resolveTicketToCampaign` returns correct `{found, reportId, campaignId, ticketNumber}` for no-report / ungrouped-report / grouped-campaign; parameterized query only | unit | `npx vitest run lib/services/phishing-ticket-resolver.test.ts` | ❌ Wave 0 — created in 22-01 T1 | ⬜ pending |
| 22-01-T2 (defaults) | 22-01 | 1 | REVIEW-04 | — | Default-param derivation (7 action types + unknown fallback) matches `ApproveActionInput` params shape exactly | unit | `npx vitest run lib/services/remediation-default-params.test.ts` | ❌ Wave 0 — created in 22-01 T2 | ⬜ pending |
| 22-01-T3 (timeline) | 22-01 | 1 | REVIEW-02 | — | `mergeTimeline` merges reports + classifications + audit_events into one ascending chronological array (server-merge approach chosen) | unit | `npx vitest run lib/services/phishing-timeline.test.ts` | ❌ Wave 0 — created in 22-01 T3 | ⬜ pending |
| 22-02-T1/T2/T3 (routes) | 22-02 | 2 | REVIEW-01, REVIEW-02, REVIEW-03, REVIEW-04 | T-22-03..06 (V4 Access Control, V5, Info Disclosure) | Resolver + extended detail + extended list routes; requirePermission gates; fresh blastRadius scoped to real sender/recipient (not empty-string); no `completed_at` column reference | type-check (no route test infra) | `npx tsc --noEmit --pretty` | N/A — no `app/**` test infra | ⬜ pending |
| 22-03-T1/T2 (evidence) | 22-03 | 1 | REVIEW-03 | T-22-07/08/09 (Tampering/XSS) | URLs inert copy-only (no `<a href>`, D-09); body preview plain `<pre>`, no `dangerouslySetInnerHTML`; attachment metadata only; blast-radius unavailable state explicit | manual + grep gates | `grep`-based DOM-safety assertions in plan acceptance_criteria | N/A — no `components/**` test infra | ⬜ pending |
| 22-04-T1/T2 (class/timeline UI) | 22-04 | 1 | REVIEW-02, REVIEW-04 | T-22-10/11 | ClassificationCard verdict/reasons/actions + approval warning; Reclassify gated on analyze; TimelineCard renders 3 merged sources | manual | — | N/A — no `components/**` test infra | ⬜ pending |
| 22-05-T1/T2 (action card) | 22-05 | 2 | REVIEW-05, REVIEW-06 | T-22-12..15 (Elevation, Tampering, CSRF) | Approve submits exact `ApproveActionInput[]`; remediate/mark-false-positive confirmed + refetch (D-04); all buttons gated by server-identical `hasPermission()`, disabled-with-tooltip (no bespoke `role === 'admin'`) | manual (+ grep gate on `hasPermission`) | `grep`-based permission-gate assertions in plan acceptance_criteria | N/A — no `components/**` test infra | ⬜ pending |
| 22-06-T1/T2 (pages+nav) | 22-06 | 3 | REVIEW-01, REVIEW-05, REVIEW-06 | T-22-16/17/18 (Spoofing/Elevation/Info Disclosure) | Ticket→campaign two-step fetch, 5-state machine, refetch after action, Better-Auth-session-only (no token/query-param auth); list-page row-click → `/phishing/tickets/{firstReportTicketId}`; nav entry | manual | — | N/A — no `app/**` test infra | ⬜ pending |

*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*

**Server-merge decision:** The planner chose the server-merge approach for the timeline (RESEARCH.md Alternatives Considered), so `lib/services/phishing-timeline.ts` + test IS created (22-01 T3) — the conditional Wave 0 item is now a firm requirement.

---

## Wave 0 Requirements

- [x] `lib/services/phishing-ticket-resolver.ts` + `.test.ts` — ticket→campaign resolution as a testable pure function (REVIEW-01) → **22-01 Task 1**
- [x] `lib/services/remediation-default-params.ts` + `.test.ts` — 7-action-type default-param derivation extracted from UI-SPEC's Action Area spec (REVIEW-04) → **22-01 Task 2**
- [x] `lib/services/phishing-timeline.ts` + `.test.ts` — server-merge timeline (REVIEW-02) chosen → **22-01 Task 3**
- [x] No framework install needed — vitest already configured and passing for the rest of the codebase

---

## Manual-Only Verifications

| Behavior | Requirement | Why Manual | Test Instructions |
|----------|-------------|------------|-------------------|
| Ticket-scoped review page renders timeline, evidence, classification correctly across all campaign states | REVIEW-02, REVIEW-03, REVIEW-04 | No `app/**`/`components/**` test infra (vitest.config.ts excludes these paths) | Click through `/phishing/tickets/{ticketId}` for: a fully-classified campaign, a D-07 not-yet-triaged ticket, a D-08 ungrouped report, and an invalid/missing ticket ID |
| Approve/remediate/mark-false-positive buttons call correct APIs and refetch-render (D-04) | REVIEW-05 | Same — no component test infra | Approve one action, confirm page refetches and shows updated state; repeat for remediate and mark-false-positive; confirm buttons disable post-resolution (D-05) with tooltip |
| Approve/remediate actions disabled/hidden for a non-privileged role, matching server-side `requirePermission` exactly | REVIEW-06 | Same — no component test infra; also requires a second test-role session | Sign in as a `user`-role account (no `phishing:approve`/`remediate`), confirm buttons are disabled/hidden and a direct API call still 403s |
| Extracted URLs render as inert copy-only text, never clickable (D-09) | REVIEW-03 | Visual/DOM inspection, not unit-testable without component infra | Inspect rendered evidence section — confirm no `<a href>` wraps any indicator URL, confirm copy-to-clipboard affordance works |
| Blast-radius `unavailable` state renders explicitly when Mimecast isn't configured | REVIEW-03 | Requires toggling integration config, not a pure-function concern | With `MIMECAST_*` env unset/disabled, load a campaign's review page and confirm an explicit "unavailable" state (not a blank/error) |

---

## Validation Sign-Off

- [x] All tasks have automated verify or Wave 0 dependencies
- [x] Sampling continuity: no 3 consecutive tasks without automated verify (Wave 0 unit tests + per-task type-check + grep DOM/permission gates)
- [x] Wave 0 covers all MISSING references
- [x] No watch-mode flags
- [x] Feedback latency < 30s
- [x] `nyquist_compliant: true` set in frontmatter

**Approval:** pending
