Skip to main content

Knowledge > Products > WiseAI Realtor > QA Coverage Matrix

WiseAI Realtor — QA Coverage Matrix

Purpose. This is the founder's "assure every screen, tab, and section is present and working — not hope" artifact. It enumerates the full app surface, states the expected content + working behaviour for each, links the acceptance spec that governs it, and records the automated test that assures it today (COVERED / GAP / ADDED). The end of the doc defines the authenticated E2E plan that codifies presence + function + no-mis-route + saves-persist into a CI suite.

How to read a row. Expected state = what a human must see/be able to do for the screen to count as "working". Acceptance = the knowledge/acceptance/realtor-*.md spec (the prose contract). Assurance = the test(s) that lock it. A surface with no assurance is flagged.

Verification ladder (CLAUDE.md). Unit/contract tests are rung 1 (necessary, never sufficient). Behavioural proof on the deployed URL is the bar for "done" (rung 2). This matrix's unit column is rung 1; the E2E plan is rung 2.

Source of truth for the surface inventory: the actual App-Router tree under churchwiseai-web/src/app/realtor/ + src/components/realtor/**, verified 2026-06-29. Nav model: src/components/realtor/shell/nav-model.ts.


0. Status legend

TokenMeaning
LIVEFully built screen wired to real data/APIs.
STUBIntentional RealtorStub placeholder (workstream deferred). Must render the stub copy, never a crash.
REDIRECTServer redirect() to a real sub-screen (no standalone UI yet).
COVEREDAn automated test asserts this surface's logic/shape today.
GAPNo automated test asserts this surface (presence or function).
ADDEDNew test added in this pass (chore/re-qa-coverage).

1. Surface inventory (master list)

1.1 Authentication surfaces (/realtor/*, outside the app shell)

RouteComponentStatusGate / behaviour
/realtor/loginRealtorSigninFormLIVEEmail → magic-link/OTP or password (re-signin-* testids); Google + Microsoft OAuth; redirects to /realtor/app on success.
/realtor/mfaRealtorMfaChallenge / RealtorMfaEnrollLIVETOTP enroll (QR) + challenge; AAL2 step-up.
/realtor/forgot-passwordreset requestLIVESends reset email.
/realtor/update-passwordreset completionLIVESets new password (post-reset / invited user).
/realtor/accept-inviteinvite acceptanceLIVEEmail-bound invite (R6) → creates membership within inviter ceiling.

1.2 Command-center shell chrome (every /realtor/app/* screen)

ElementComponentStatusExpected state
Left rail (nav)RealtorRail + nav-model + nav-visibilityLIVECapability-gated destination set, resolved server-side; active-route highlight; account chip + signed-in user footer; collapses to 64px (desktop) / off-canvas drawer (mobile, Esc/backdrop/nav-click closes).
Top barRealtorTopBarLIVEScreen title (longest-prefix from nav-model) + date/greeting; Setup N% pill (links /setup, hidden when allDone); Aria launcher chip; notification bell (live WS6 feed); Search (⌘K) + New are intentionally disabled scaffolding.
Notification bellNotificationBellLIVEReads per-user feed for the active accountId; unread count; mark-read.
Ask-Aria panelAskAriaPanelLIVEMounted only with an active workspace; mobile FAB when closed; modal dialog (Esc/backdrop/focus-trap) when open; loads contextual opening (GET) then streams (SSE); quick replies; deep-links are Next-routed buttons. Read-only — Aria never writes settings.

1.3 Backend screens (/realtor/app/*)

RouteComponentStatusNav capability gate
/realtor/appRealtorDashboardBodyLIVEalways (Home)
/realtor/app/setupSetupChecklistLIVEnone (first-run hub)
/realtor/app/leadsLeadsScreenLIVEleads:lead:read:own
/realtor/app/inboxInboxScreenLIVEinbox:conversation:read:own
/realtor/app/contactsContactsScreenLIVEcontacts:contact:read:own
/realtor/app/contacts/[id]ContactRecordScreenLIVEcontacts:contact:read:own
/realtor/app/pipelineDealsBoardLIVEdeals:deal:read:own
/realtor/app/listingsListingsManagerBodyLIVElistings:manual:read:own
/realtor/app/qr-campaignsQrCampaignsBodyLIVEanalytics:report:view:own
/realtor/app/websiteWebsiteBuilderBodyLIVEsettings:branding:edit
/realtor/app/studioRealtorStubSTUBcontent:item:create
/realtor/app/calendarCalendarTasksBodyLIVEtasks:task:read:own
/realtor/app/analyticsRealtorAnalyticsBodyLIVEanalytics:report:view:own
/realtor/app/reviewsReviewsManagerBodyLIVEreviews:testimonial:read
/realtor/app/settings/agentsAgentConfigClientLIVEai:config:read
/realtor/app/integrationsRealtorStubSTUBintegrations:crm:edit
/realtor/app/settingsredirect → /settings/agentsREDIRECTsettings:profile:read
/realtor/app/settings/notificationsNotificationPrefsScreenLIVE(settings child)

1.4 Public site (/s/[slug] — the RealEstate Pro Website template)

Rendered by RealEstateSiteTemplate via src/app/s/[slug]/page.tsx + fixed sub-routes (do-not-reinvent §"Public site sub-pages"): /about, /reviews, /partners, /tools (mortgage/LTT/CMHC/property-tax/rent-vs-buy/house-hack), /guides (+/guides/[guide]), /blog (+/blog/[postSlug]), /featured/top-agent-magazine, /listing/[mls] (detail + maps), /scan/[mls] (QR → lead), /privacy, /terms, /[page] (generic). Plus /lr/[token] (public "leave a review" hub). These are the LIVE shipped surfaces (PR #1017 + follow-ons) — the backend's Website builder manages them; it does not re-implement them.


2. Per-screen expected-state specs

Each screen lists: tabs, sections (exact headings), the must-work actions (with the API/route each hits), the empty/loading/error states, the acceptance criteria, and the assurance (test files). Brand: navy chrome / teal action / gold accent (design-direction.md).


2.1 Dashboard — /realtor/app (bento command center)

Acceptance: realtor-dashboard-mvp.md, realtor-dashboard-shell-mvp.md.

Section / tileExpected stateAssurance
First-run entry bannerRendered above the bento until setup complete (OnboardingEntryBanner).onboarding-model.test.ts (logic) · GAP (component render)
Hero — "New leads today"The one daily number from the REAL CRM book (deriveDashboardHero); supporting line always surfaces the open book so a 0-today day never reads "no leads"; links to leads.dashboard-hero.test.ts (COVERED)
KPI strip (4)AI conversations · Calls answered by AI · QR sign scans · Showings booked. Show + "AI front office not active yet" until provisioned (never fabricated).dashboard-hero.test.ts (aiActive)
Aria proof tile"Aria — your AI front office" + Autopilot badge; honest empty ("not active yet") + "Set up your AI front office" CTA → /settings/agents.route-integrity (ADDED, link)
Your access (live)Role / scope / effectivePermissions count / MFA (aal) per membership; MFA CTA → /realtor/mfa when not aal2.rbac.contract.test.ts (caps)
Finish setting upReal onboarding %, each step + StatusPill, "Open the full setup guide" → /setup. Falls back to static steps if no onboarding prop.onboarding-model.test.ts
Pipeline snapshot6 stages (colour+label, colourblind-safe), honest 0s, "Open board →" → /pipeline.route-integrity (ADDED)
Needs a humanHonest empty queue, "Open the inbox →" → /inbox.route-integrity (ADDED)
Fail-closed stateSigned in, no active membership → "Almost there" card naming the user email; zero capabilities.rbac.contract.test.ts (fail-closed R1)

Acceptance criteria: no fabricated metric ever (honesty contract); every tile CTA resolves to a real screen; fail-closed card shows for a membership-less user.


2.2 Setup hub — /realtor/app/setup (the onboarding CENTERPIECE)

Acceptance: realtor-onboarding-mvp.md.

  • Welcome header: workspace name + "Welcome, {first}" (or "You're all set up" when allDone); progress bar; "X of Y required · A of B recommended"; "Ready to launch" chip when launchReady; Ask-Aria invite.
  • 9 step cards in 5 groups (catalogue is the single source of truth — onboarding-model.ts): Foundations (brokerage_identity, profile_team), AI front office (ai_agent), Online presence (website, listings), Growth (notifications, reviews, qr_campaigns), Optional (vow_sold_dataVowOptInCard). * = required.
  • Each card: title + Required badge + "Start here" on the first unfinished required step; WhyThisMatters (open by default); estimate + honest detail line; CTA deep-link (Confirm/Set up/Build…/Review when done).
  • Honesty: a step shows "Done" only when the backing data is real; unknown ("Can't check yet") when a table/column isn't provisioned — never a fabricated Done.

Must work: every step cta routes to its real screen; progress math (computeProgress) and startHere reflect live signals.

Assurance: onboarding-model.test.ts (catalogue invariants, per-step derivation, progress, startHere, groupSteps) — COVERED. Step CTA routes → route-integrity.contract.test.ts — ADDED. Server gather (computeOnboardingStatus) → aria-onboarding-context.int.test.ts (DB) partial; GAP on the gather's per-signal SQL.


2.3 Leads — /realtor/app/leads

Acceptance: realtor-leads-mvp.md. Data: GET /api/real-estate/crm/contacts?account_id&limit=200.

  • Smart-list tabs (live counts): "All" + universal (Hot leads / New this week / Needs follow-up…) + per-language lists (roster ∩ rows, never hardcoded).
  • Toolbar: search "Search leads…"; Status filter (new→lost); Source filter (ai_voice/ai_chat/web_form/qr_scan/referral/portal); Language filter; Density (Compact/Comfortable); View toggle List ⇄ Pipeline.
  • List view (ContactsTable): Name·Status·Source·Lang·Score·Last touch·Next action·(Agent if management); sortable Name + Last touch; persists sort per smart-list; mobile = card list.
  • Pipeline view (KanbanBoard): 8 stages; drag → PATCH …/contacts/{id} {pipeline_status}; keyboard "Move to stage" alternative; per-column count + value.
  • Lead drawer: chips (Status/Source/Lang/Score); Call/Text/Email quick actions disabled when consent-suppressed (reason shown); Aria summary; key facts; activity timeline (loads …/contacts/{id}/timeline); "Open record →".
  • States: loading skeleton(8); NotProvisioned ("Your leads" copy); error+retry; empty ("No leads yet"); no-match ("Nothing in '{list}' right now").

Assurance: presenters.test.ts (chips, language pill, smart-lists, filterByView, consentView suppression, nextActionFor, sort) — COVERED. Loader/intake → crm-pure + crm-spine.int.test.ts — COVERED (DB-gated). Component render + drag-persist → GAP (E2E).


2.4 Inbox — /realtor/app/inbox

Acceptance: realtor-inbox-mvp.md. Data: GET /api/real-estate/crm/conversations?account_id.

  • 3-pane: (1) Views (needs_human / unassigned / ai_handled / assigned_to_me / all — needs_human urgent-red badge) + Channel + Language filters; (2) conversation list (avatar/name/time/snippet + chips: Channel/Lang/Escalated/AI/"You're in control"); (3) thread (message log, system escalation rows, contact/agent/AI bubbles).
  • Take-over / Resume: POST …/contacts/{id}/handoff {action:'take_over'|'resume'} (gated canTakeOver).
  • Composer: present but Send is disabled with title "Replies are sent by you — message sending is founder-gated in this build" (CASL).
  • Context rail (xl+): Aria summary + contact facts + "Open full record →".
  • States: 3-pane skeleton; NotProvisioned ("Your inbox"); error+retry; empty ("No conversations yet"); "Select a conversation to read it."

Assurance: presenters.test.ts (rollupConversations, filterByView) + conversations.int.test.ts (inbox rollup, DB) — COVERED. Handoff POST + render → GAP (E2E). The founder-gated no-send is a hard invariant → casl-no-cron-send.contract.test.ts — COVERED.


2.5 Contacts — /realtor/app/contacts & Contact record — /realtor/app/contacts/[id]

Acceptance: realtor-contact-record-mvp.md.

  • Contacts list: search "Search contacts…" + lifecycle filter (lead/active_client/under_contract/past_client/sphere) + count; row → /contacts/{id}. States mirror Leads (NotProvisioned "Your contacts", empty "No contacts yet", no-match).
  • Contact record (3-col): Identity card (avatar/name/lifecycle/score/lang/phone/email/location) · Relationship memory (birthday/anniversary/family/mortgage/referral + tags) · Consent (per-channel allowed/blocked, red when suppressed) · Aria summary · Activity timeline (filter: All/Calls/Messages/Notes) · Note composer (POST …/contacts/{id}/timeline, gated canUpdate) · Next best actions · deferred Tasks/Listings link cards · Deal card (commission row only if canSeeCommissions).
  • Merge: "Merge duplicate…" (gated canMerge) → MergeDialog search (…/contacts?search=) → two-stage confirm → POST …/crm/merge (strictest consent, reversible tombstone).
  • States: 3 skeletons; NotProvisioned ("This contact"); forbidden ("No access"); error+retry.

Assurance: crm-pure.test.ts (identity, consent reconcile, merge-strictest) + crm-spine.int.test.ts (create/get/update/merge/scoping) + presenters.test.ts (chips, relativeTime, scoreTone) — COVERED. Note POST + merge UI + commission redaction render → GAP (E2E); commission gate logic → rbac.contract.test.ts (COVERED).


2.6 Pipeline / Deals — /realtor/app/pipeline

Acceptance: realtor-deals-mvp.md. Data: GET /api/real-estate/crm/deals?account_id.

  • Header: "{active} active · {pipeline} in pipeline · {closed} closed"; Board ⇄ Table toggle; "+ New deal" (gated canEdit) → dialog (Side Buy/Sell, Address, Price) → POST …/deals.
  • Board: 6 columns (New/In progress/Offer/Accepted/Close to follow/Closed); drag → PATCH …/deals/{id} {status}; keyboard move alternative; cards show address/side/client/price/milestone/days-in-stage/lang.
  • Table: Address·Source·Status·Price·Beds·DOM·Leads·Actions, sortable.
  • Deal drawer: Summary · Parties · Key dates & reminders (offer/conditions/waived/closing/possession) · conditions checklist · linked listing/contact · Compliance (FINTRAC status flag + "documents in your brokerage system" + brokerage link) · activity. No GCI/commission row. Actions (gated canEdit): Log update (POST …/deals/{id}/activity), Advance → next stage, Add reminder (disabled → "set in Calendar & Tasks").
  • States: column skeletons; error "Couldn't load your deals."; NotProvisioned banner; empty nudge ("Won leads become deals here…").

Assurance: deals.int.test.ts (pure derivations no commission A2, CRUD, status transition + stage_since, parties, ownership scoping, archive/tenant scoping) — COVERED. Drag-persist + drawer render → GAP (E2E).


2.7 Calendar & Tasks — /realtor/app/calendar

Acceptance: realtor-tasks-calendar-mvp.md.

  • Calendar: week/day/list views; prev/today/next; sync badge (connected vs "request-capture still works"); legend (Showing/Open house/Listing pres./Call-back/Closing/Condition) + "Aria captures showing REQUESTS; you confirm. No AI auto-booking."
  • Follow-Up Coach rail: speed-to-lead widget (median first response + "N unactioned > threshold"); task groups Overdue/Today/Upcoming; task card checkbox (PATCH …/tasks/{id} {status:'completed'}); showing-request Confirm ({confirm_showing:true}) / Decline ({decline_showing:true}); Call/Text/Email quick actions (consent-gated); Action plans section (template badge, steps, channels, "SMS off by default", "no cron auto-sends" banner).
  • States: analytics skeletons; graceful empty workspace when tasks/deals tables unprovisioned; "No follow-ups yet"; "No events this week."

Assurance: ws3-pure.test.ts (day-bounds TZ, bucketing Overdue/Today/Upcoming, actionSuppressed, computeSpeedToLead, computeHotLead, routeLead, buildTasksFromSteps SMS-off/consent/idempotent) + ws3-spine.int.test.ts (CRUD, ownership, enroll spawns tasks, routing persists) — COVERED. Confirm/decline + complete render → GAP (E2E).


2.8 Listings — /realtor/app/listings

Acceptance: realtor-listings-mvp.md. Data: getUnifiedListings + DDF status from metadata.ddf.authorized.

  • Header: "{active} active · {manual} manual · {ddf} from MLS"; FeedPill (CREA DDF live vs not connected); Gallery/Table/Map toggle; "+ Add listing" (gated canEditManual).
  • Filter bar: search (location/address/MLS#) + min/max price + Beds + Baths + Type + status pills (all/active/pending/sold).
  • Gallery card: hero+status+source badge; feature star (PATCH …/crm/listings {action:'feature'}) + reorder ↑↓ ({action:'reorder'}) — works for feed AND manual (beats SellingToolz); compliance line ("MLS® … Courtesy of the listing brokerage via REALTOR.ca" for DDF / "Pre-market… not on REALTOR.ca" for manual); DOM·leads; Media modal; Edit (manual only) / "Read-only" (feed); "Kit ✦" → /studio?listing=; "QR sign"/qr-campaigns?listing={id} (added 2026-07-01, PR #1091).
  • Agent tag (added 2026-07-01, PR #1095): a select per card/row ({action:'assign_agent', listing_agent_id}) backed by the local_business_agents roster, for the team's OWN internal viewing — manual only, because CREA's DDF feed does not expose per-listing agent identity (confirmed live: ListAgentFullName returns null on every synced row). Needed because an office-wide feed (e.g. Beckett's Sutton office, ListOfficeKey scope) syncs every agent's listings at that office, not just the tenant's own — there is no way to auto-detect "which are mine." Writes directly to the existing listing_agent_id column, which the DDF sync already protects from being overwritten (see ddf/map.ts's feed-derived-columns list) — survives every resync exactly like featured/sort_order do via the overlay.
  • Listing form modal: pre-market banner; address(req)/city/neighbourhood/price/beds/baths/sqft/type/status/banner + "Show on public website"; POST/PATCH …/crm/listings(/{id}).
  • Media modal: photos + alt-text (AODA, required) ({action:'alt_text'} works for feed too); reorder/add/remove (manual); video/3D/floorplan URLs.
  • States: EmptyState ("No listings yet" + Add / Connect DDF); NoMatches; map "no coordinates"; not-provisioned stub ("…WS4").

Assurance: listings-overlay-pure.test.ts (source/editable, DOM, overlay merge, featuring survives DDF sync, counts, alt-text) + listings-overlay-server.test.ts (feature INSERT/UPDATE fail-closed on foreign, reorder, alt-text on overlay, lead attribution, getUnified) + listing-attribution.test.ts (MLS®/courtesy/trademark hygiene) — COVERED, 21/21 still green after the agent-tag addition. Form/media modal save render + agent-tag select render → GAP (E2E).


2.9 QR Campaigns — /realtor/app/qr-campaigns

Acceptance: realtor-qr-campaigns-mvp.md. Data: getCampaignsAnalytics.

  • Header: "{n} campaigns · {scans} scans · {leads} leads · {range}"; differentiator badge "Only on WiseAI: scan → live AI chat"; range All/7d/30d/90d; Export CSV; "+ Create QR campaign" (gated canCreate).
  • KPIs (4): Total scans · Unique visitors · Leads captured · Scan→lead rate ($0 CPL).
  • Charts: Scans over time (+ "view as table") · Scan funnel (Scans→Unique→Opened chat→Captured→Showing) · Top campaigns.
  • Campaigns table: type chips + search; Campaign·Type·Linked to·Scans·Leads·Conv.·Status·Created; status badges Active/Paused/Draft/Archived.
  • Create drawer (single-page form, not a stepper): type → connect content (listing/page) → landing template → capture fields (Name locked) → generate QR (style/logo/QrCodePreview PNG·SVG) → print assets (yard-sign/window card). "Save draft" (status:draft, generate_qr:false) / "Activate" (status:active then PATCH generate_qr:true). The "connect content → Listing" dropdown sources getUnifiedListings (the SAME real DB listings the Listings screen shows, manual+DDF) — fixed 2026-07-01 (PR #1091); it previously read a hardcoded site-config.ts fixture with non-UUID ids, so picking a listing silently no-opped and the campaign never actually linked to it. A listing's real mls_number now flows through to the destination path (/s/{slug}/scan/{mls}), fulfilling the "pre-loaded with the listing" claim. A per-listing "QR sign" quick action on the Listings screen deep-links ?listing={id} into this drawer, pre-selected.
  • A "QR sign" deep-link (?listing=) reads the query param in a useEffect (client-only, post-mount), never at render time — deriving initial state from useSearchParams() is a hydration-mismatch risk (see the fmtDate finding below).
  • States: ProvisioningEmpty; EmptyCampaigns; "No campaigns match this filter."

Assurance: qr-campaigns-pure.test.ts (slug, destination path, buildTrackedUrl, scan funnel, scan→lead, bucketByDay, range) + qr-campaigns-server.test.ts (not-provisioned, create, ownership filter, recordScan, analytics) — COVERED. Create drawer render + QR render → GAP (E2E). The QR-create-500 prod bug (task #67 / PR #1077) was fixed but never actually re-verified live until 2026-07-01, when a real create was triggered on prod and succeeded (201).

Hydration gotcha (found + fixed 2026-07-01, PR #1094): fmtDate() in QrCampaignsBody.tsx called toLocaleDateString() with no explicit timeZone — the Vercel server (UTC) and a visitor's browser (local zone) can format the same timestamp as a different calendar day, causing a real React #418 hydration error once a campaign row existed to render through it (0 campaigns = 0 errors, which is why it wasn't caught until this session's own test campaigns triggered it). Reproduces ONLY on real cross-machine infra — a local next build && next start shares one machine's timezone for both "server" and "client" and will never show it; verify any date-formatting fix against a live preview/prod deploy, never a local build. The same unpinned-timezone pattern was found and fixed in 5 more files: ContactRecordScreen.tsx, TeamManagerClient.tsx, NotificationBell.tsx, the shared presenters.ts, and reviews/manager-model.ts — all latent until real timestamped rows render on those screens. RealtorTopBar's date/greeting already guards this correctly via useSyncExternalStore (renders empty server-side, fills in client-side post-mount) — confirmed NOT a bug.


2.10 Website builder — /realtor/app/website (5 tabs)

Acceptance: realtor-website-builder-mvp.md. Loader: loadWebsiteBuilderState. Gate: settings:branding:edit (else stub); publish needs content:item:publish.

TabExpected stateAssurance
Templates6 families (Local Authority/Luxury/Signature Agent/Modern Team/Multilingual Community/Brokerage Pro); "Current" badge; full editor opens from account editor.GAP (static, presentational)
PagesNav pages (status + lang chips + SEO badge) + utility pages; nested area guides; reorder ↑↓PUT …/website/nav-order; "N pages need an SEO meta description" warning; Edit (manual) / View (feed).pages-model.test.ts (buildPagesModel, status/SEO from real inputs, languages tenant-driven, applyNavOrder) — COVERED
Brand kitBrand name + languages; colours/fonts/logo in full editor; AA contrast preserved.GAP (static)
DomainHosting (Canada region · SSL); custom domain (verified/DNS pending); bring-your-own embed snippet.brokerage-footer/status (partial) · GAP (embed render)
Tracking & Compliance (the publish GATE)Readiness summary (province regulator + "X of Y required checks"); Brokerage identity form = the HARD gate (legal name, descriptor word, office address, registered agents — Publish blocked client+server until complete; live footer preview); PUT …/website/brokerage-identity; POST …/website/publish. Trademark & DDF · Analytics (Resend click-tracking OFF by design) · Cookie consent (PIPEDA/Law 25) · Accessibility (AODA) · Hosting.brokerage-identity.test.ts (regulator resolution province-aware, the hard publish gate, footer line, defaults overlay) + brokerage-footer.test.ts + status.test.ts (requiresFrench, compliance status) — COVERED

GAP: loadWebsiteBuilderState assembler (DB loader) has no direct test; constituent models (pages, identity, footer, compliance status) are covered.


2.11 Analytics — /realtor/app/analytics

Acceptance: realtor-analytics-mvp.md. Pure body from AnalyticsResult; controls are URL-param links (no client JS).

  • Controls: date range 7d/30d/90d/MTD/YTD; first⇄last touch; compare prev period; Export CSV/api/real-estate/crm/analytics?format=csv.
  • KPI strip (5): Leads · Lead→client% · Showing requests · Deals firm/closed · Transaction volume (not GCI) [gold accent]. Each with delta vs prev.
  • Reports: Lead source attribution & ROI (first/last touch, $0 owned, "—" until ad spend) · Leads by language · Conversion funnel (scan→…→close, biggest-drop flag) · Speed-to-lead (median AI vs human + unactioned>24h) · What's creating your leads (top listings / top QR signs scan→lead / top content) · Team performance (management-only; per-agent volume, never commission).
  • Honesty: every number a real first-party count; GCI never appears (A2); honest "No data for this period" per report + a spine-empty card.

Assurance: analytics-compute.test.ts (helpers, KPI strip, source attribution & ROI, funnel, speed-to-lead, language, what's-creating-leads, team performance) + analytics.int.test.ts (team scope, own-scope isolation, empty honest empties — DB) — COVERED. Render + CSV export + URL-param controls → GAP (E2E).


2.12 Reviews — /realtor/app/reviews

Acceptance: realtor-reviews-manager-mvp.md. Loader: loadReviewsManagerState.

  • Header + dry-run banner: "Owner-pressed · dry-run on · no cron auto-send"; "nothing is sent automatically."
  • Review funnel: Requests sent → Clicked the link (real engagement) → ★ Public review / Private feedback; "Honest metrics — clicks, not opens"; same public ask to every eligible client (FTC 16 CFR 465 — no happy→public routing).
  • Platform targets (Google/RankMyAgent…); Review requests table (Client·Consent(EBR)·Status·Last touch·Actions): Send (gated canSend, consent re-checked), Reviewed, Opt-out, Read & reply (private feedback); batch send of approved.
  • CSV import (last 24mo only, transaction date required, staged as drafts); Testimonials gallery (republish only with consent + RECO ID + CREA disclosure).
  • CASL footer: sender name/address/unsubscribe/EBR windows.

Assurance: manager-model.test.ts (funnel honest clicks-not-opens, rowEligibilityForSend mirrors engine gate, presentReviewRow owner actions + terminal state, consent/EBR labels, approved batch count, latest-import) + casl-no-cron-send.contract.test.ts (reviews send owner-pressed behind consent gate) — COVERED. Send/opt-out POST + render → GAP (E2E).


2.13 AI Agents — /realtor/app/settings/agents (Voice + Chat tabs)

Acceptance: realtor-agent-config-mvp.md. Data: GET /api/realtor/ai-config?account_id (account_id in query, not just header — middleware strips the header). Save: PATCH same. Test: POST /api/realtor/ai-config/test.

  • Header: Aria live indicator; Discard/Save (gated canEditChat); preview-only banner if config not persisted; view-only banner if !canEditChat; first-run "Chat" discoverability nudge (reduced-motion-safe).
  • Voice tab: Working mode (Co-pilot/Autopilot) · Guardrails (RE_GUARDRAIL_CHIPS shown LOCKED — never toggles) · Premium Voice Line (DID/voice/language/hours/greeting/capture/escalation/summary) · Community Language Lines (your differentiator; "Native review required" flag for non-EN/FR).
  • Chat tab: Knowledge sources (brokerage policies Required/locked) + multilingual replies · Conversation rules (tone) · Website placement (3 placements + embed snippet) · Lead-capture fields (Name/contact Required) · Test Aria sandbox ("no lead is saved").

Assurance: ai-guardrails.test.ts (valuation/financing/fair-housing/confidentiality belts, safe deferral, bridge frame, guardrail block, church/funeral/vet unaffected, facts-used receipt, safety floor) — COVERED. agent-config-shared.test.ts — ADDED (catalogs' required-locked invariants, resolveAgentConfig defaulting + required-can't-toggle-off, mode sanitize, voice-line classifiers, languageLabel). /api/realtor/ai-config GET/PATCH/test route handlers → GAP (E2E — note the account_id-in-query 401 fix, task #65).


2.14 Notifications — /realtor/app/settings/notifications

Acceptance: realtor-notifications-mvp.md. Data: GET/PATCH /api/real-estate/crm/notifications/prefs.

  • Event grid (Event·Push·Email·SMS·Timing); urgent events ("Always urgent" — hot lead/handoff/unactioned) always instant + never digested; transport-gate banner (push/email/SMS "ready, switching on gradually" — in-app feed always on); quiet hours; daily digest; Save (409 → "personal login required").
  • Hard invariant: notifications address staff only, never a lead/prospect (CASL).

Assurance: notifications-pure.test.ts (staff-vs-lead boundary, taxonomy, in-app floor, urgent-never-digested, founder transport gate, quiet hours, deep-links, default copy) + notifications-server.test.ts (assertStaffRecipient, recipient fan-out, CRUD, visibility scope, prefs) — COVERED. Save round-trip render → GAP (E2E).


2.15 Stubs & redirects (must render, never crash)

ScreenExpectedAssurance
Content Studio /studioRealtorStub "Content Studio" + 3-pane editor description (WS9 deferred).route-integrity (ADDED — page exists)
Integrations /integrationsRealtorStub "Integrations" + CRM/DDF/calendar/Zapier description (Connect deferred).route-integrity (ADDED)
Settings /settingsredirect()/settings/agents.nav-visibility (title) + route-integrity (target exists)

2.16 Ask-Aria concierge panel (cross-cutting)

Acceptance: aria-onboarding-concierge-mvp.md. Opens from top-bar chip, setup/dashboard cards, step cards, mobile FAB.

  • Contextual opening (GET) → streamed replies (SSE); quick replies; read-only ("she points you to the right screen, she doesn't change settings"); honest fallback when status unreadable; deep-links are real routes.

Assurance: aria-onboarding.test.ts (prompt grounding, honest metrics in SETUP STATE, deep-link accuracy never-invented-route, no-write floor, contextual opening) + aria-onboarding-context.int.test.ts (live DB) — COVERED. Panel component render/stream → GAP (E2E). Element-registry/highlight (Aria 1.5) → its own node --test coverage (tasks #68–71).


3. Test-coverage summary (per surface)

SurfaceUnit/ContractIntegration (DB)Component renderE2E (authed)
Shell nav + title✅ nav-visibilityGAPPLAN
Route wiring (nav+onboarding→screen)route-integrity (ADDED)n/aPLAN
Dashboard✅ dashboard-heroGAPPLAN
Setup hub✅ onboarding-modelpartial (context.int)GAPPLAN
Leads✅ presenters✅ crm-spineGAPPLAN
Inbox✅ presenters✅ conversationsGAPPLAN
Contacts / record✅ crm-pure/presenters✅ crm-spineGAPPLAN
Pipeline/Deals✅ deals (pure)✅ deals.intGAPPLAN
Calendar/Tasks✅ ws3-pure✅ ws3-spineGAPPLAN
Listings✅ overlay-pure/attribution✅ overlay-serverGAPPLAN
QR Campaigns✅ qr-pure✅ qr-serverGAPPLAN
Website · Pages✅ pages-modelGAPPLAN
Website · Compliance gate✅ brokerage-identity/footer/statusGAPPLAN
Website · Templates/Brand/DomainGAP (static)GAP (loader)GAPPLAN
Analytics✅ analytics-compute✅ analytics.intGAPPLAN
Reviews✅ manager-modelGAPPLAN
AI Agents config modelagent-config-shared (ADDED) + ai-guardrailsGAPPLAN
Notifications✅ notifications-pure✅ notifications-serverGAPPLAN
RBAC / capability gating✅ rbac.contractn/aPLAN
Compliance (CASL/VOW/attribution)✅ casl/vow-gate/listing-attr/statusn/aPLAN
Auth bridge / invites✅ auth-bridge.contractpartialGAPPLAN
Ask-Aria concierge✅ aria-onboarding✅ context.intGAPPLAN
Stubs (Studio/Integrations)✅ route-integrity (ADDED)n/asmoke

Top surfaces with the LEAST assurance (priority for E2E):

  1. Every LIVE screen's React render + interaction — no component-render tests exist; logic is well-covered but "does it paint + can the user act" is only assured by the authed walk. (Highest-value gap; the E2E plan §4 closes it.)
  2. API route handlers (/api/real-estate/crm/*, /api/realtor/ai-config*, /api/real-estate/website/*) — covered indirectly via server-fn int tests, but the HTTP layer (auth, account_id resolution, status codes) has no route-handler test. Two prod 401/500 bugs (#65, #67) slipped exactly here.
  3. Website Templates/Brand/Domain tabs + loadWebsiteBuilderState — presentational/loader, no test.
  4. Server gather computeOnboardingStatus — the per-signal SQL that drives the whole setup %/Start-here.

4. Authenticated E2E plan (to codify into CI)

Why E2E is mandatory here. Unit tests prove the logic; they cannot prove a screen renders, a save persists, a route doesn't mis-navigate, or a capability actually hides a tab. CLAUDE.md's verification ladder requires deployed-URL behavioural proof for "done". The existing qa-teamtest walk (scripts/qa-walk.mjs, qa-final-walk.mjs, seeded by qa-teamtest-seed.mjs) is the seed of this — it logs in as the TeamTest fixture and visits every screen, asserting HTTP status, no page/console errors, no stub/error-boundary leak, and one expected-text hit. This plan deepens that from "loads without error" to "present + functional + persists".

4.1 Harness & credentials (secure-creds requirement)

  • Auth: password sign-in as seeded TeamTest users (owner + a scoped agent) via the re-signin-* testids (already in qa-walk.mjs). Credentials (WALK_EMAIL / WALK_PASSWORD) must come from CI secrets / env, never committed. The seed user is the canonical TeamTest/teamtest-realty fixture (and TeamMoelker).
  • Target: the deployed app (preview or prod host). Realtor backend is not host-aliased, so a *.vercel.app preview is acceptable for /realtor/app/*; the public /s/[slug] site must be checked on the real Pro Website host (middleware rewrites). Read the preview URL fresh per HEAD sha.
  • Realtime check: real desktop Chrome UA (the walk already sets one) to avoid bot-gating.
  • Isolation: each run uses the seeded fixture account; writes go to demo/test rows only (never a customer account).

4.2 Per-screen E2E cases (presence + function + no-mis-route + saves-persist)

For every LIVE screen in §1.3, the suite asserts:

  • Presence: the screen's H1/title matches navTitleForPath; each documented tab label and section heading from §2 is in the DOM; no stub/error-boundary marker (STUB_MARKERS in qa-walk.mjs).
  • No mis-route: clicking each rail item lands on the matching route (URL + active highlight); each in-screen CTA (e.g. dashboard "Open board →", setup step CTAs, "Open record →") navigates to the documented target (this E2E mirrors what route-integrity proves statically).
  • Function (sampled, behaviour not DOM-presence — verify-behavior rule):
    • Leads: toggle List⇄Pipeline (assert view change at 2 timepoints); apply a status filter (row count changes); drag/keyboard-move a kanban card → reload → status persisted.
    • Contacts record: add a note → it appears in the timeline → reload → still there.
    • Pipeline: create a deal → appears in column; advance stage → persisted.
    • Calendar: complete a task → it leaves the Today group; confirm a showing-request → becomes a showing.
    • Listings: feature-star a feed listing → reload → still featured (overlay survives); save alt-text → persisted.
    • QR: run the create drawer to "Save draft" → row appears as Draft (regression for the #67 500).
    • Website: save brokerage identity incomplete → Publish disabled; complete it → Publish enabled; nav-order reorder persists.
    • AI Agents: load (no 401 — #65 regression); toggle a non-required knowledge source + Save → reload → persisted; required source/field cannot be turned off; Test Aria returns a reply and writes no lead.
    • Notifications: change a channel toggle + Save → reload → persisted (or the documented 409 for a shared-login session).
    • Analytics: switch date range (URL param) → KPIs update; Export CSV returns a CSV content-type.
  • States: for at least one screen, force the empty/not-provisioned path and assert the honest copy (not a blank or a crash).
  • RBAC: as the scoped agent user, assert Website + Integrations are absent from the rail and the routes fail-closed (stub), while Leads/Contacts/etc. are present (mirrors nav-visibility.contract at runtime).
  • Public site: /s/teamtest-realty renders (title), chat CTA present, a listing detail loads, a /scan/[mls] capture works.

4.3 Suite shape & registry

  • Author as Playwright specs under churchwiseai-web/tests/e2e/realtor/ (one spec per nav area + a shell/RBAC spec + a public-site spec), reusing the qa-walk.mjs login + error-capture scaffolding.
  • Register in knowledge/tests/registry.yaml as a realtor suite (today it has zero realtor entries — itself a gap); mark the AI-config / publish-gate / lead-persist cases critical_path: true so the CI gate requires a saved Playwright artifact.
  • Cross-reference the qa-teamtest live coverage-walk scorecard (the qa/teamtest-e2e worktree) as the running baseline; this plan is its acceptance-criteria upgrade.

5. What changed in this pass (chore/re-qa-coverage)

  • ADDED src/components/realtor/shell/__tests__/route-integrity.contract.test.ts — every nav destination + every onboarding CTA resolves to a real page.tsx; no orphan screens (any new screen must be wired to the rail or allowlisted). This is the "every screen is present" assurance, encoded.
  • ADDED src/lib/real-estate/__tests__/agent-config-shared.test.ts — the AI Agents config model (previously zero coverage): catalog required-locked invariants, resolveAgentConfig defaulting + the can't-toggle-off-a-required-field rule, mode sanitize, voice-line classifiers, languageLabel.
  • Both pass; full realtor pure+contract suite (with the server-only shim) = 543/543 green.

Runner (no vitest):

node --require ./src/lib/real-estate/crm/__tests__/setup-server-only.cjs \
--import tsx --test <spec> # files that transitively import server-only
node --import tsx --test <spec> # pure files (route-integrity, agent-config-shared, …)