Test Specs
RankFlow AI — Admin Dashboard & Observability Test Specification
- [ ] AdminShell renders with sidebar, header, and content area for ADMIN role
docs/test-specs/TEST-admin-observability.mdOn this page
- 1. Admin Layout & Access Control Tests
- 1.1 Unit Tests — Layout Components
- 1.2 Integration Tests — Access Control & Role Enforcement
- 1.3 Success Criteria (Binary) — Layout & Access Control
- 1.4 Agent Context (Pre-conditions) — Layout & Access Control
- 1.5 Verification Commands — Layout & Access Control
- 2. KPI Dashboard (/admin) Tests
- 2.1 Unit Tests — KPI Page Components
- 2.2 Integration Tests — KPI Data Fetching
- 2.3 Success Criteria (Binary) — KPI Dashboard
- 2.4 Agent Context (Pre-conditions) — KPI Dashboard
- 2.5 Verification Commands — KPI Dashboard
- 3. Client Management Tests
- 3.1 Unit Tests — Client List (/admin/clients)
- 3.2 Unit Tests — Client Detail (/admin/clients/[id])
- 3.3 Integration Tests — Client API
- 3.4 Success Criteria (Binary) — Client Management
- 3.5 Agent Context (Pre-conditions) — Client Management
- 3.6 Verification Commands — Client Management
- 4. Infrastructure Tests
- 4.1 Unit Tests — Domain Management (/admin/directory)
- 4.2 Unit Tests — Website Management (/admin/directory-profiles)
- 4.3 Integration Tests — Infrastructure API
- 4.4 Success Criteria (Binary) — Infrastructure
- 4.5 Agent Context (Pre-conditions) — Infrastructure
- 4.6 Verification Commands — Infrastructure
- 5. Social Connections Tests
- 5.1 Unit Tests — Social Connections (/admin/social-connections)
- 5.2 Integration Tests — Social API
- 5.3 Success Criteria (Binary) — Social Connections
- 5.4 Agent Context (Pre-conditions) — Social Connections
- 5.5 Verification Commands — Social Connections
- 6. Job Monitor Tests
- 6.1 Unit Tests — Job Monitor (/admin/jobs)
- 6.2 Integration Tests — Job Monitor API
- 6.3 Success Criteria (Binary) — Job Monitor
- 6.4 Agent Context (Pre-conditions) — Job Monitor
- 6.5 Verification Commands — Job Monitor
- 7. Content Review Tests
- 7.1 Unit Tests — Content Review (/admin/content)
- 7.2 Integration Tests — Content API
- 7.3 Success Criteria (Binary) — Content Review
- 7.4 Agent Context (Pre-conditions) — Content Review
- 7.5 Verification Commands — Content Review
- 8. Billing Tests
- 8.1 Unit Tests — Billing (/admin/billing)
- 8.2 Integration Tests — Billing API
- 8.3 Success Criteria (Binary) — Billing
- 8.4 Agent Context (Pre-conditions) — Billing
- 8.5 Verification Commands — Billing
- 9. System Reports Tests
- 9.1 Unit Tests — System Reports (/admin/reports)
- 9.2 Integration Tests — Reports API
- 9.3 Success Criteria (Binary) — System Reports
- 9.4 Agent Context (Pre-conditions) — System Reports
- 9.5 Verification Commands — System Reports
- 10. Impersonation Feature Tests
- 10.1 Unit Tests — Impersonation
- 10.2 Integration Tests — Impersonation API & Flow
- 10.3 Success Criteria (Binary) — Impersonation
- 10.4 Agent Context (Pre-conditions) — Impersonation
- 10.5 Verification Commands — Impersonation
- 11. KPI Calculation Tests
- 11.1 Unit Tests — KPI Calculation Logic
- 11.2 Integration Tests — KPI Aggregation from DB
- 11.3 Success Criteria (Binary) — KPI Calculation
- 11.4 Agent Context (Pre-conditions) — KPI Calculation
- 11.5 Verification Commands — KPI Calculation
- 12. Job Monitoring & Alert Threshold Tests
- 12.1 Unit Tests — Alert Threshold Logic
- 12.2 Integration Tests — Alert Engine
- 12.3 Integration Tests — Job Monitoring Metrics
- 12.4 Success Criteria (Binary) — Job Monitoring & Alerts
- 12.5 Agent Context (Pre-conditions) — Job Monitoring & Alerts
- 12.6 Verification Commands — Job Monitoring & Alerts
- 13. Cross-Cutting Integration Tests
- 13.1 End-to-End Admin Flow Tests
- 13.2 API Router Protection Tests
- 13.3 Success Criteria (Binary) — Cross-Cutting
- 13.4 Agent Context (Pre-conditions) — Cross-Cutting
- 13.5 Verification Commands — Cross-Cutting
- 14. Execution Plan & Agent Assignments
- Phase 1: Component Infrastructure (Day 1)
- Phase 2: Page Unit Tests (Days 2-3)
- Phase 3: API Integration Tests (Days 4-5)
- Phase 4: E2E & Cross-Cutting (Day 6)
- Phase 5: Verification & Compliance (Day 7)
- Appendix A: Test Data Constants
- Appendix B: Admin tRPC Procedure Registry
Version: 1.0.0
Date: 2026-06-13
Target Specs: docs/specs/admin-dashboard-ui.md, docs/business_flow_map.md Section 6
Systems Under Test: Next.js Admin Shell, tRPC Admin Router, BullMQ Job Monitor, KPI Aggregations, Alert Engine
Test Tooling: Vitest, React Testing Library, msw, ioredis-mock, @testing-library/user-event, bullmq test helpers
1. Admin Layout & Access Control Tests#
1.1 Unit Tests — Layout Components#
| Test | Input | Expected Output | File |
|---|---|---|---|
admin-shell.render |
role: "ADMIN", pathname: "/admin" |
Renders AdminSidebar, AdminHeader, and PageContent without error |
src/__tests__/unit/admin/layout.test.tsx |
admin-sidebar.navigation |
currentRoute: "/admin/clients" |
All 9 nav items render; "All Clients" has aria-current="page" |
src/__tests__/unit/admin/Sidebar.test.tsx |
admin-sidebar.collapsible |
isCollapsed: true |
Sidebar width = 64px; nav labels hidden; icons visible | src/__tests__/unit/admin/Sidebar.test.tsx |
admin-header.breadcrumbs |
pathname: "/admin/clients/prac_123" |
Breadcrumbs: "Dashboard > Clients > Dr. Smith Dental" | src/__tests__/unit/admin/Header.test.tsx |
admin-header.notifications |
unreadCount: 3 |
Notification bell shows badge with "3"; dropdown opens on click | src/__tests__/unit/admin/Header.test.tsx |
admin-header.usermenu |
user: { name: "Admin", email: "admin@rankflow.in" } |
Dropdown shows user name, email, "Sign out" link | src/__tests__/unit/admin/Header.test.tsx |
stat-card.render |
{ label: "MRR", value: 480000, prefix: "₹" } |
Displays "₹4,80,000" with "MRR" label; color variant applied | src/__tests__/unit/admin/StatCard.test.tsx |
status-badge.active |
status: "ACTIVE" |
Green badge with "Active" text | src/__tests__/unit/admin/StatusBadge.test.tsx |
status-badge.trial |
status: "TRIAL" |
Amber badge with "Trial" text | src/__tests__/unit/admin/StatusBadge.test.tsx |
status-badge.churned |
status: "CHURNED" |
Red badge with "Churned" text | src/__tests__/unit/admin/StatusBadge.test.tsx |
data-table.sort |
Click column header "Name" | Table rows re-sort ascending; click again = descending | src/__tests__/unit/admin/DataTable.test.tsx |
data-table.filter |
Type "dental" in search filter | Only rows with "dental" in name remain visible; count = 2 | src/__tests__/unit/admin/DataTable.test.tsx |
data-table.pagination |
totalRows: 50, pageSize: 10 |
Page 1 shows rows 1-10; "Next" enabled; page indicator "1 / 5" | src/__tests__/unit/admin/DataTable.test.tsx |
confirm-dialog.render |
isOpen: true, title: "Suspend Client", description: "..." |
Dialog renders with title, description, "Cancel" and "Confirm" buttons | src/__tests__/unit/admin/ConfirmDialog.test.tsx |
confirm-dialog.confirm |
Click "Confirm" button | onConfirm callback fired; dialog closes |
src/__tests__/unit/admin/ConfirmDialog.test.tsx |
filter-bar.date-range |
Select "Last 7 days" | DateRangePicker shows start/end dates; onFilterChange called with range |
src/__tests__/unit/admin/FilterBar.test.tsx |
practice-select.render |
practices: [{ id: "prac_1", name: "Dr. A" }, { id: "prac_2", name: "Dr. B" }] |
Dropdown renders practice names; searchable | src/__tests__/unit/admin/PracticeSelect.test.tsx |
json-viewer.render |
data: { jobId: "job_123", payload: { practiceId: "prac_1" } } |
Syntax-highlighted JSON with collapsible nodes | src/__tests__/unit/admin/JsonViewer.test.tsx |
1.2 Integration Tests — Access Control & Role Enforcement#
| Test | Setup | Action | Assertion | File |
|---|---|---|---|---|
access.admin-allowed |
Session with role: "ADMIN" |
Navigate to /admin |
Page renders 200; AdminShell mounts; no redirect |
src/__tests__/integration/admin/access.test.ts |
access.client-blocked |
Session with role: "CLIENT" |
Navigate to /admin |
Redirects to /dashboard with 302; AdminShell never mounts |
src/__tests__/integration/admin/access.test.ts |
access.editor-blocked |
Session with role: "EDITOR" |
Navigate to /admin |
Redirects to /dashboard with 302 |
src/__tests__/integration/admin/access.test.ts |
access.unauthenticated |
No session | Navigate to /admin |
Redirects to /login?redirect=/admin |
src/__tests__/integration/admin/access.test.ts |
api.admin-procedure-allowed |
x-practice-id: admin, authenticated admin user |
Call admin.getKPIs tRPC query |
Returns AdminKPIs object with all fields populated |
src/__tests__/integration/admin/api.access.test.ts |
api.admin-procedure-blocked |
Authenticated client user | Call admin.getKPIs tRPC query |
Returns TRPCError with code FORBIDDEN (403) |
src/__tests__/integration/admin/api.access.test.ts |
api.admin-procedure-unauthenticated |
No session | Call admin.listClients tRPC query |
Returns TRPCError with code UNAUTHORIZED (401) |
src/__tests__/integration/admin/api.access.test.ts |
layout.mobile-drawer |
Viewport 375px width |
Render /admin |
Sidebar hidden; hamburger menu visible; drawer opens on click | src/__tests__/integration/admin/responsive.test.tsx |
layout.tablet-collapsible |
Viewport 800px width |
Render /admin |
Sidebar collapsible; toggle button visible; table shows reduced columns | src/__tests__/integration/admin/responsive.test.tsx |
layout.desktop-full |
Viewport 1440px width |
Render /admin |
Sidebar fixed 240px; full table columns; charts side-by-side | src/__tests__/integration/admin/responsive.test.tsx |
1.3 Success Criteria (Binary) — Layout & Access Control#
-
AdminShellrenders with sidebar, header, and content area for ADMIN role -
AdminSidebardisplays all 9 navigation routes with correct icons and labels - Active nav item has
aria-current="page"and visual highlight -
AdminHeaderrenders breadcrumbs matching current route hierarchy - Notification bell displays unread count badge; dropdown opens on click
- User menu dropdown shows admin user details and sign-out option
-
DataTablesupports sorting, filtering, and pagination -
StatCardrenders label, formatted value, and color variant correctly -
StatusBadgemaps all statuses (ACTIVE, TRIAL, CHURNED, SUSPENDED, PENDING) to correct colors -
ConfirmDialogrenders title, description, cancel, and confirm buttons; fires callbacks correctly -
FilterBarapplies date range and dropdown filters; callsonFilterChangewith correct shape -
PracticeSelectrenders searchable dropdown of all practices -
JsonViewerrenders structured JSON with syntax highlighting and collapsible nodes -
/admin/*routes redirect non-ADMIN users to/dashboardwith 302 - Unauthenticated users are redirected to
/loginwithredirectquery param - All
admin.*tRPC procedures return 403 FORBIDDEN for non-admin roles - All
admin.*tRPC procedures return 401 UNAUTHORIZED for unauthenticated requests - Mobile (<768px) renders sidebar as drawer; tables as card lists; charts stacked
- Tablet (768-1024px) renders collapsible sidebar; reduced table columns
- Desktop (>1024px) renders fixed 240px sidebar; full table columns; charts side-by-side
1.4 Agent Context (Pre-conditions) — Layout & Access Control#
- Required DB state:
Usertable with at least 3 records:role: "ADMIN",role: "CLIENT",role: "EDITOR";ClientProfilewith at least 1 record - Required env vars:
NEXTAUTH_SECRET(mocked);ADMIN_ROLE="ADMIN" - Required mocks:
next-auth/reactgetServerSessionmocked for role-based tests;next/navigationuseRoutermocked for redirect assertions - Session state: Admin session with
user.id,user.email,user.role: "ADMIN"; client session withuser.role: "CLIENT" - Viewport mocks:
@testing-library/reactrenderwithwindow.innerWidthmocked for responsive tests
1.5 Verification Commands — Layout & Access Control#
# Unit: layout components
pnpm test:unit -- src/__tests__/unit/admin/
# Integration: access control + responsive
pnpm test:integration -- src/__tests__/integration/admin/access.test.ts
pnpm test:integration -- src/__tests__/integration/admin/responsive.test.tsx
# API access control
pnpm test:integration -- src/__tests__/integration/admin/api.access.test.ts
# Full layout suite
pnpm test:unit -- src/__tests__/unit/admin/
2. KPI Dashboard (/admin) Tests#
2.1 Unit Tests — KPI Page Components#
| Test | Input | Expected Output | File |
|---|---|---|---|
kpi-page.render |
AdminKPIs mock data |
All 6 sections render: Stat Row, Revenue Chart, Client Status, Job Health, API Costs, Activity Feed | src/__tests__/unit/admin/kpi/page.test.tsx |
kpi-cards.counts |
{ totalClients: 100, activeClients: 80, trialClients: 15, churnedClients: 5 } |
4 stat cards render with correct counts and labels | src/__tests__/unit/admin/kpi/KPICards.test.tsx |
kpi-cards.mrr |
{ mrr: 480000, arr: 5760000 } |
MRR card shows "₹4,80,000"; ARR card shows "₹57,60,000" | src/__tests__/unit/admin/kpi/KPICards.test.tsx |
kpi-cards.citation-health |
{ citationHealth: 87.5 } |
Card shows "87.5%" with green/amber color based on threshold (>=90% green, <80% red) | src/__tests__/unit/admin/kpi/KPICards.test.tsx |
revenue-chart.render |
12 months of MRR data | Recharts AreaChart renders with X-axis labels, Y-axis (INR), tooltip on hover |
src/__tests__/unit/admin/kpi/RevenueChart.test.tsx |
client-status-chart.render |
{ trial: 15, active: 80, churned: 5 } |
Recharts PieChart renders with 3 segments; legend shows labels and percentages |
src/__tests__/unit/admin/kpi/ClientStatusChart.test.tsx |
job-health-chart.render |
{ completed: 1200, failed: 30, running: 5 } |
Recharts BarChart renders with 3 bars; failed bar is red |
src/__tests__/unit/admin/kpi/JobHealthChart.test.tsx |
api-cost-chart.render |
{ claude: 150, openai: 50, dataforseo: 100, serpapi: 50, hyperbrowser: 50, firecrawl: 10 } |
Stacked bar chart renders 6 provider segments; total = ₹410 | src/__tests__/unit/admin/kpi/ApiCostChart.test.tsx |
activity-feed.render |
recentActivity: [20 AuditLogEntry items] |
List renders 20 items; newest first; "View all" link to /admin/reports |
src/__tests__/unit/admin/kpi/ActivityFeed.test.tsx |
activity-feed.empty |
recentActivity: [] |
Empty state message: "No recent activity" | src/__tests__/unit/admin/kpi/ActivityFeed.test.tsx |
2.2 Integration Tests — KPI Data Fetching#
| Test | Setup | Action | Assertion | File |
|---|---|---|---|---|
kpi.data-loading |
admin.getKPIs tRPC query mocked with 1s delay |
Render /admin |
Skeleton loaders shown during fetch; replaced by data after resolve | src/__tests__/integration/admin/kpi/data.test.tsx |
kpi.data-error |
admin.getKPIs throws TRPCError |
Render /admin |
Error boundary or toast shows "Failed to load KPIs"; retry button visible | src/__tests__/integration/admin/kpi/data.test.tsx |
kpi.auto-refresh |
Mock admin.getKPIs returning activeJobs: 5 |
Wait 60s (mock timers) | Query re-fetches; if activeJobs changes to 7, stat card updates |
src/__tests__/integration/admin/kpi/refresh.test.tsx |
kpi.citation-health-calculation |
Citation table: 26 MATCHED, 4 FAILED out of 30 |
Call admin.getKPIs |
citationHealth = 86.67% (rounded to 86.7% or 87% depending on rounding rule) |
src/__tests__/integration/admin/kpi/calculation.test.ts |
kpi.api-cost-aggregation |
AiUsage table: 100 records across 6 providers over 7 days |
Call admin.getKPIs |
apiCosts7d sums match DB aggregation exactly (to 2 decimal places) |
src/__tests__/integration/admin/kpi/calculation.test.ts |
kpi.recent-activity-limit |
AuditLog table: 50 records |
Call admin.getKPIs |
recentActivity array has exactly 20 items; ordered by createdAt DESC |
src/__tests__/integration/admin/kpi/calculation.test.ts |
2.3 Success Criteria (Binary) — KPI Dashboard#
-
/adminpage renders all 6 sections: Stat Row, Revenue Chart, Client Status, Job Health, API Costs, Activity Feed - Stat Row shows Total Clients, Active Clients, Trial Clients, Churned Clients, MRR, ARR, Active Jobs, Failed Jobs (24h), Citation Health
- MRR and ARR values are formatted in INR with proper comma separators (e.g., ₹4,80,000)
- Citation Health card color-codes: >=90% green, 80-89% amber, <80% red
- Revenue Chart renders 12-month MRR trend with correct X/Y axes and tooltips
- Client Status Pie Chart shows Trial vs Active vs Churned with percentages
- Job Health Bar Chart shows Completed, Failed, Running counts; failed bar is red
- API Cost Chart shows 7-day breakdown by provider (Claude, OpenAI, DataForSEO, SerpAPI, Hyperbrowser, Firecrawl)
- Activity Feed displays last 20 audit log entries in reverse chronological order
- Activity Feed shows empty state when no entries exist
- Skeleton loaders appear during data fetching; real data replaces them on success
- Error state displays friendly message and retry button on tRPC failure
- KPI data auto-refreshes every 60 seconds (or on window focus)
-
citationHealthis calculated as(MATCHED citations / total attempted) * 100with correct rounding -
apiCosts7daggregates per-provider spend fromAiUsagetable over rolling 7-day window -
recentActivityis limited to exactly 20 entries, newest first
2.4 Agent Context (Pre-conditions) — KPI Dashboard#
- Required DB state:
User(100 records),ClientProfile(80 ACTIVE, 15 TRIAL, 5 CHURNED),Subscription(withstatusandamount),Citation(26 MATCHED, 4 FAILED),AiUsage(100 records across 6 providers),AuditLog(50+ records),JobLog(1200 completed, 30 failed, 5 running) - Required env vars:
ADMIN_ROLE="ADMIN";CURRENCY_CODE="INR" - Required mocks:
rechartscomponents can be shallow-rendered or mocked if causing issues in JSDOM - tRPC mocks:
admin.getKPIsandadmin.getSystemHealthreturn deterministic mock data - Timer control:
vi.useFakeTimers()for auto-refresh tests
2.5 Verification Commands — KPI Dashboard#
# Unit: KPI components
pnpm test:unit -- src/__tests__/unit/admin/kpi/
# Integration: KPI data + calculations
pnpm test:integration -- src/__tests__/integration/admin/kpi/
# Smoke: admin KPI page render
pnpm test:integration -- src/__tests__/integration/admin/kpi/data.test.tsx
3. Client Management Tests#
3.1 Unit Tests — Client List (/admin/clients)#
| Test | Input | Expected Output | File |
|---|---|---|---|
client-list.render |
clients: [10 mock ClientProfile rows] |
Table renders 10 rows with Name, Slug, Type, Tier, Status, Locations, Created, Trial Ends columns | src/__tests__/unit/admin/clients/ClientListPage.test.tsx |
client-list.sort-name |
Click "Name" header | Rows sorted A-Z; click again = Z-A | src/__tests__/unit/admin/clients/ClientListPage.test.tsx |
client-list.sort-tier |
Click "Tier" header | Rows sorted by tier enum order (STARTER < STANDARD < PREMIUM < ENTERPRISE) | src/__tests__/unit/admin/clients/ClientListPage.test.tsx |
client-list.filter-type |
Select "CLINIC" from Type filter | Only clinic rows visible; count updates | src/__tests__/unit/admin/clients/ClientListPage.test.tsx |
client-list.filter-status |
Select "TRIAL" from Status filter | Only trial rows visible; count updates | src/__tests__/unit/admin/clients/ClientListPage.test.tsx |
client-list.filter-search |
Type "dental" in search | Only rows with "dental" in name or slug visible | src/__tests__/unit/admin/clients/ClientListPage.test.tsx |
client-list.filter-date |
Select date range "Last 30 days" | Only clients created within range visible | src/__tests__/unit/admin/clients/ClientListPage.test.tsx |
client-list.action-view |
Click "View" on row 1 | Navigates to /admin/clients/{id} |
src/__tests__/unit/admin/clients/ClientListPage.test.tsx |
client-list.action-edit |
Click "Edit" on row 1 | Inline modal opens with ClientEditForm; pre-filled with row data |
src/__tests__/unit/admin/clients/ClientListPage.test.tsx |
client-list.action-suspend |
Click "Suspend" on row 1 | ConfirmDialog opens with "Suspend Client" title; on confirm, admin.updateClient called with status: "SUSPENDED" |
src/__tests__/unit/admin/clients/ClientListPage.test.tsx |
client-list.action-delete |
Click "Delete" on row 1 | ConfirmDialog opens with "Delete Client" title; on confirm, soft delete called (not hard delete) |
src/__tests__/unit/admin/clients/ClientListPage.test.tsx |
client-list.bulk-change-tier |
Select 3 rows; choose "Change tier to Standard" from bulk actions | admin.updateClient called 3 times with tier: "STANDARD"; toast confirmation |
src/__tests__/unit/admin/clients/ClientListPage.test.tsx |
client-list.bulk-export |
Select 5 rows; click "Export CSV" | CSV blob downloaded with correct headers and 5 data rows | src/__tests__/unit/admin/clients/ClientListPage.test.tsx |
client-list.bulk-email |
Select 2 rows; click "Send email" | Email modal opens with pre-filled recipient list; email.send called on submit |
src/__tests__/unit/admin/clients/ClientListPage.test.tsx |
client-list.empty |
clients: [] |
Empty state with "No clients found" and "Add client" button | src/__tests__/unit/admin/clients/ClientListPage.test.tsx |
client-list.pagination |
totalClients: 150, pageSize: 25 |
6 pages shown; page 1 has rows 1-25; "Next" advances to page 2 | src/__tests__/unit/admin/clients/ClientListPage.test.tsx |
3.2 Unit Tests — Client Detail (/admin/clients/[id])#
| Test | Input | Expected Output | File |
|---|---|---|---|
client-detail.render |
practiceId: "prac_123", all 9 tabs data |
9 tabs render: Overview, Locations, GBP, Social, Citations, Site, Jobs, Billing, Audit Log | src/__tests__/unit/admin/clients/ClientDetailPage.test.tsx |
client-detail.overview-card |
practice: { name: "Dr. Smith", type: "DENTIST", tier: "PREMIUM", status: "ACTIVE", members: 3, locations: 2 } |
Profile card shows logo, name, type, tier, status, member count, location count | src/__tests__/unit/admin/clients/ClientDetailPage.test.tsx |
client-detail.overview-actions |
Practice with status: "ACTIVE" |
"Edit", "Impersonate", "Suspend" buttons visible | src/__tests__/unit/admin/clients/ClientDetailPage.test.tsx |
client-detail.locations-tab |
locations: [2 Location rows], gbpLocations: [1 GbpLocation] |
Location cards render with address; GBP status badge shows "Connected — 1 location" | src/__tests__/unit/admin/clients/ClientDetailPage.test.tsx |
client-detail.gbp-tab |
gbpAccount: { isActive: true }, gbpPosts: [5], reviews: [10] |
Account status green; posts table with 5 rows; reviews table with 10 rows; insights charts render | src/__tests__/unit/admin/clients/ClientDetailPage.test.tsx |
client-detail.social-tab |
socialAccounts: [4 SocialAccount rows] |
Table shows platform, name, status, follower count, last sync for each account | src/__tests__/unit/admin/clients/ClientDetailPage.test.tsx |
client-detail.citations-tab |
citations: [30 Citation rows] |
Directory list with status badges; NAP status (MATCHED/MISMATCHED); screenshot thumbnails clickable | src/__tests__/unit/admin/clients/ClientDetailPage.test.tsx |
client-detail.site-tab |
profileSections: [7 SiteSection rows], template: "dental-clean" |
Template name shown; section list with visibility toggles; preview iframe renders | src/__tests__/unit/admin/clients/ClientDetailPage.test.tsx |
client-detail.jobs-tab |
jobs: [20 JobLog rows] |
Job table with ID, skill, status, duration, cost; pagination if >20 | src/__tests__/unit/admin/clients/ClientDetailPage.test.tsx |
client-detail.billing-tab |
invoices: [12 Invoice rows], subscription: { status: "ACTIVE" } |
Invoice table with ID, amount, status, paid date; subscription card shows plan and next billing | src/__tests__/unit/admin/clients/ClientDetailPage.test.tsx |
client-detail.audit-log-tab |
auditLogs: [50 AuditLog rows] |
Audit log table with action, actor, timestamp, IP; filterable by action type | src/__tests__/unit/admin/clients/ClientDetailPage.test.tsx |
client-detail.not-found |
practiceId: "prac_nonexistent" |
"Client not found" error page; 404 status; link back to /admin/clients |
src/__tests__/unit/admin/clients/ClientDetailPage.test.tsx |
client-detail.suspend-action |
Click "Suspend" in overview | ConfirmDialog opens; on confirm, admin.updateClient called; status changes to SUSPENDED |
src/__tests__/unit/admin/clients/ClientDetailPage.test.tsx |
client-detail.edit-action |
Click "Edit" in overview | Inline modal opens with ClientEditForm; save calls admin.updateClient |
src/__tests__/unit/admin/clients/ClientDetailPage.test.tsx |
3.3 Integration Tests — Client API#
| Test | Setup | Action | Assertion | File |
|---|---|---|---|---|
api.list-clients |
DB with 50 ClientProfile records |
Call admin.listClients |
Returns array of 50; each has id, name, slug, type, tier, status, locations, createdAt, trialEndsAt |
src/__tests__/integration/admin/clients/api.test.ts |
api.list-clients-filter |
DB with mixed types and statuses | Call admin.listClients with { type: "CLINIC", status: "ACTIVE" } |
Returns only clinic + active records; count matches DB query | src/__tests__/integration/admin/clients/api.test.ts |
api.list-clients-search |
DB with 10 records containing "dental" | Call admin.listClients with { search: "dental" } |
Returns exactly 10 records; all have "dental" in name or slug | src/__tests__/integration/admin/clients/api.test.ts |
api.list-clients-pagination |
DB with 100 records | Call admin.listClients with { page: 2, pageSize: 25 } |
Returns records 26-50; totalCount: 100, totalPages: 4 |
src/__tests__/integration/admin/clients/api.test.ts |
api.get-client |
DB with practiceId: "prac_123" and related records |
Call admin.getClient({ id: "prac_123" }) |
Returns full practice object with all 9 tab relations populated | src/__tests__/integration/admin/clients/api.test.ts |
api.get-client-not-found |
No practice with id: "prac_missing" |
Call admin.getClient({ id: "prac_missing" }) |
Throws TRPCError with code NOT_FOUND |
src/__tests__/integration/admin/clients/api.test.ts |
api.update-client |
Existing practice | Call admin.updateClient({ id: "prac_123", tier: "PREMIUM" }) |
DB updated; returns updated practice; auditLog entry created |
src/__tests__/integration/admin/clients/api.test.ts |
api.update-client-suspend |
Active practice | Call admin.updateClient({ id: "prac_123", status: "SUSPENDED" }) |
DB updated to SUSPENDED; recurring jobs paused; auditLog entry created |
src/__tests__/integration/admin/clients/api.test.ts |
api.update-client-soft-delete |
Active practice | Call admin.updateClient({ id: "prac_123", deletedAt: <now> }) |
deletedAt set; data retained; login blocked; auditLog entry created |
src/__tests__/integration/admin/clients/api.test.ts |
api.update-client-bulk-tier |
3 practices with tier STARTER | Call admin.updateClient 3 times with tier: "STANDARD" |
All 3 updated; no partial updates; transactions rollback on failure | src/__tests__/integration/admin/clients/api.test.ts |
3.4 Success Criteria (Binary) — Client Management#
-
/admin/clientsrenders data table with all 9 columns (Name, Slug, Type, Tier, Status, Locations, Created, Trial Ends, Actions) - All sortable columns (Name, Slug, Type, Tier, Status, Locations, Created, Trial Ends) support ascending/descending sort
- All filterable columns (Name search, Type dropdown, Tier dropdown, Status dropdown, Created date) filter rows correctly
- Search filter matches against name and slug fields
- Date range filter matches
createdAtandtrialEndsAtfields - Row actions: "View" navigates to
/admin/clients/{id}; "Edit" opens inline modal; "Suspend" shows confirmation; "Delete" shows confirmation (soft delete) - Bulk actions: "Change tier" updates all selected rows; "Export CSV" downloads correct data; "Send email" opens email modal
- Pagination works with configurable page size; total count and page count accurate
- Empty state renders when no clients match filters
-
/admin/clients/[id]renders 9 tabs: Overview, Locations, GBP, Social, Citations, Site, Jobs, Billing, Audit Log - Overview tab shows profile card with logo, name, type, tier, status, members, locations, and action buttons (Edit, Impersonate, Suspend)
- Locations tab shows location cards with GBP connection status per location
- GBP tab shows account status, posts table, reviews table, and insights charts
- Social tab shows connected accounts with platform, name, status, followers, last sync
- Citations tab shows all 30 directory submissions with status, NAP match, and screenshot
- Site tab shows template name, section list with visibility toggles, and live preview
- Jobs tab shows all jobs for this practice with pagination and status badges
- Billing tab shows invoices and subscription status
- Audit Log tab shows all actions with actor, timestamp, IP, and action type filter
- Non-existent client ID renders 404 error page with link back to list
-
admin.listClientsreturns paginated list with correct filtering, sorting, and search -
admin.getClientreturns full practice with all 9 tab relations populated -
admin.updateClientupdates DB, returns updated record, and createsauditLogentry - Suspend action updates status to SUSPENDED and pauses recurring jobs
- Soft delete sets
deletedAtwithout removing data; blocks login - Bulk tier changes apply atomically per client; no partial updates
3.5 Agent Context (Pre-conditions) — Client Management#
- Required DB state:
ClientProfile(50+ records across all types, tiers, statuses),Location(2 per practice),GbpAccount(1 per practice),GbpLocation(1 per location),SocialAccount(0-4 per practice),Citation(30 per practice),DirectoryProfileSection(7 per practice),JobLog(20 per practice),Invoice(12 per practice),AuditLog(50 per practice),User(members per practice) - Required env vars:
ADMIN_ROLE="ADMIN" - Required mocks:
next/navigationuseRouterandredirectmocked;next-auth/reactuseSessionmocked for admin role - tRPC mocks:
admin.listClients,admin.getClient,admin.updateClientreturn deterministic data - File mocks:
URL.createObjectURLandBlobmocked for CSV export tests
3.6 Verification Commands — Client Management#
# Unit: client list + detail pages
pnpm test:unit -- src/__tests__/unit/admin/clients/
# Integration: client API
pnpm test:integration -- src/__tests__/integration/admin/clients/api.test.ts
# Full client management suite
pnpm test:unit -- src/__tests__/unit/admin/clients/
pnpm test:integration -- src/__tests__/integration/admin/clients/
4. Infrastructure Tests#
4.1 Unit Tests — Domain Management (/admin/directory)#
| Test | Input | Expected Output | File |
|---|---|---|---|
domain-list.render |
domains: [20 Domain rows] |
Table shows Practice, DirectorySlug, Custom Domain, DNS Status, SSL Status, CDN Status, Actions | src/__tests__/unit/admin/directory/DomainListPage.test.tsx |
domain-list.dns-propagated |
dnsStatus: "PROPAGATED" |
Green checkmark badge | src/__tests__/unit/admin/directory/DomainListPage.test.tsx |
domain-list.dns-pending |
dnsStatus: "PENDING" |
Yellow clock badge | src/__tests__/unit/admin/directory/DomainListPage.test.tsx |
domain-list.dns-error |
dnsStatus: "ERROR" |
Red error badge | src/__tests__/unit/admin/directory/DomainListPage.test.tsx |
domain-list.action-dns-check |
Click "DNS Check" on row | admin.checkDNS called with practiceId; status badge updates after refresh |
src/__tests__/unit/admin/directory/DomainListPage.test.tsx |
domain-list.action-revalidate |
Click "Force Revalidate" on row | admin.revalidateDomain called with practiceId; toast confirmation |
src/__tests__/unit/admin/directory/DomainListPage.test.tsx |
domain-list.filter-status |
Select "DNS Error" from filter | Only rows with dnsStatus: "ERROR" visible |
src/__tests__/unit/admin/directory/DomainListPage.test.tsx |
4.2 Unit Tests — Website Management (/admin/directory-profiles)#
| Test | Input | Expected Output | File |
|---|---|---|---|
website-list.render |
websites: [15 Website rows] |
Table shows Domain, Niche, Authority Score, Post Count, Linked Practice, Health, Last Published | src/__tests__/unit/admin/directory-profiles/WebsiteListPage.test.tsx |
website-list.health-active |
health: "ACTIVE" |
Green badge; uptime percentage shown | src/__tests__/unit/admin/directory-profiles/WebsiteListPage.test.tsx |
website-list.health-down |
health: "DOWN" |
Red badge; last error message shown | src/__tests__/unit/admin/directory-profiles/WebsiteListPage.test.tsx |
website-list.sort-authority |
Click "Authority Score" header | Rows sorted by DA score descending | src/__tests__/unit/admin/directory-profiles/WebsiteListPage.test.tsx |
website-list.linked-practice |
linkedPractice: "Dr. Smith Dental" |
Practice name shown as link to /admin/clients/{id} |
src/__tests__/unit/admin/directory-profiles/WebsiteListPage.test.tsx |
website-list.empty |
websites: [] |
Empty state with "No owned blog pages configured" | src/__tests__/unit/admin/directory-profiles/WebsiteListPage.test.tsx |
4.3 Integration Tests — Infrastructure API#
| Test | Setup | Action | Assertion | File |
|---|---|---|---|---|
api.list-domains |
DB with 20 Practice records with varied domain statuses |
Call admin.listDomains |
Returns array with practiceName, directorySlug, directoryProfileUrl, dnsStatus, sslStatus, cdnStatus |
src/__tests__/integration/admin/infrastructure/api.test.ts |
api.check-dns |
Practice with directorySlug: "dr-smith.rankflow.in" |
Call admin.checkDNS({ practiceId: "prac_123" }) |
Performs DNS lookup; returns PROPAGATED or PENDING based on mock resolver |
src/__tests__/integration/admin/infrastructure/api.test.ts |
api.revalidate-domain |
Practice with directorySlug: "dr-smith.rankflow.in" |
Call admin.revalidateDomain({ practiceId: "prac_123" }) |
Calls /api/revalidate/site with correct slug; returns { success: true } |
src/__tests__/integration/admin/infrastructure/api.test.ts |
api.list-websites |
DB with 15 BlogSite records |
Call admin.listWebsites |
Returns array with domain, niche, authorityScore, postCount, linkedPractice, health, lastPublished |
src/__tests__/integration/admin/infrastructure/api.test.ts |
api.website-health-check |
BlogSite with domain: "kerala-health.rankflow.in" |
Call admin.checkWebsiteHealth({ domain: "..." }) |
HTTP 200 check performed; returns { status: "ACTIVE" } or { status: "DOWN", error: "..." } |
src/__tests__/integration/admin/infrastructure/api.test.ts |
4.4 Success Criteria (Binary) — Infrastructure#
-
/admin/directorytable shows all 7 columns with correct data - DNS Status displays 3 states: Propagated (green), Pending (yellow), Error (red)
- SSL Status displays 2 states: Active (green), Expired (red)
- CDN Status displays 2 states: Cached (green), Bypass (amber)
- "DNS Check" action performs live DNS lookup and updates status badge
- "Force Revalidate" action triggers ISR revalidation for the domain
- Domain list supports filtering by DNS, SSL, and CDN status
-
/admin/directory-profilestable shows all 7 columns with correct data - Health status shows Active (green) or Down (red) with uptime/error details
- Authority Score sorts numerically; Post Count sorts numerically
- Linked Practice column links to client detail page
- Empty state renders when no blog sites exist
-
admin.listDomainsreturns all practice domains with correct status fields -
admin.checkDNSperforms DNS resolution and returns accurate status -
admin.revalidateDomaincalls revalidation endpoint and returns success -
admin.listWebsitesreturns all owned blog pages with health and metadata -
admin.checkWebsiteHealthperforms HTTP check and returns accurate status
4.5 Agent Context (Pre-conditions) — Infrastructure#
- Required DB state:
Practice(20 records withdirectorySlug,directoryProfileUrl,domainStatus,sslStatus,cdnStatus),BlogSite(15 records withdomain,niche,authorityScore,postCount,linkedPracticeId,health,lastPublished) - Required env vars:
BASE_DOMAIN="rankflow.in";API_REVALIDATE_SECRET(mocked) - Required mocks: DNS resolver mocked (
dns.resolveor equivalent); HTTP fetch mocked for health checks;next/navigationmocked - tRPC mocks:
admin.listDomains,admin.listWebsites,admin.checkDNS,admin.revalidateDomainreturn deterministic data
4.6 Verification Commands — Infrastructure#
# Unit: domain + website pages
pnpm test:unit -- src/__tests__/unit/admin/directory/
pnpm test:unit -- src/__tests__/unit/admin/directory-profiles/
# Integration: infrastructure API
pnpm test:integration -- src/__tests__/integration/admin/infrastructure/api.test.ts
5. Social Connections Tests#
5.1 Unit Tests — Social Connections (/admin/social-connections)#
| Test | Input | Expected Output | File |
|---|---|---|---|
social-list.render |
accounts: [25 SocialAccount rows] |
Table shows Platform, Account Name, Practice, Status, Followers, Last Sync, Actions | src/__tests__/unit/admin/social/SocialConnectionsPage.test.tsx |
social-list.platform-icons |
Platforms: Facebook, Instagram, LinkedIn, Twitter | Correct platform icons render (e.g., FB blue, IG gradient) | src/__tests__/unit/admin/social/SocialConnectionsPage.test.tsx |
social-list.status-active |
status: "ACTIVE" |
Green badge | src/__tests__/unit/admin/social/SocialConnectionsPage.test.tsx |
social-list.status-expired |
status: "EXPIRED" |
Red badge with "Reconnect" action | src/__tests__/unit/admin/social/SocialConnectionsPage.test.tsx |
social-list.status-pending |
status: "PENDING" |
Amber clock badge | src/__tests__/unit/admin/social/SocialConnectionsPage.test.tsx |
social-list.action-disconnect |
Click "Disconnect" on active account | ConfirmDialog opens; on confirm, social.disconnect called; row removed or status changes to DISCONNECTED |
src/__tests__/unit/admin/social/SocialConnectionsPage.test.tsx |
social-list.action-refresh |
Click "Refresh Token" on expired account | social.refreshToken called; status updates to ACTIVE if success |
src/__tests__/unit/admin/social/SocialConnectionsPage.test.tsx |
social-list.filter-platform |
Select "Instagram" from filter | Only Instagram accounts visible | src/__tests__/unit/admin/social/SocialConnectionsPage.test.tsx |
social-list.filter-practice |
Select practice from dropdown | Only accounts belonging to selected practice visible | src/__tests__/unit/admin/social/SocialConnectionsPage.test.tsx |
social-list.sort-followers |
Click "Followers" header | Rows sorted by follower count descending | src/__tests__/unit/admin/social/SocialConnectionsPage.test.tsx |
5.2 Integration Tests — Social API#
| Test | Setup | Action | Assertion | File |
|---|---|---|---|---|
api.list-social |
DB with 25 SocialAccount records across 4 platforms |
Call admin.listSocialConnections |
Returns array with all fields; grouped by platform optionally | src/__tests__/integration/admin/social/api.test.ts |
api.disconnect |
Active SocialAccount with composioConnectionId: "conn_123" |
Call social.disconnect({ id: "soc_123" }) |
Composio API disconnect called; DB isActive set to false; auditLog entry created |
src/__tests__/integration/admin/social/api.test.ts |
api.refresh-token |
SocialAccount with status: "EXPIRED", tokenExpiresAt: <past> |
Call social.refreshToken({ id: "soc_123" }) |
New token fetched from platform; DB updated; isActive set to true |
src/__tests__/integration/admin/social/api.test.ts |
api.refresh-token-fail |
SocialAccount with revoked OAuth |
Call social.refreshToken({ id: "soc_123" }) |
Returns { success: false, error: "Token revoked" }; status remains EXPIRED |
src/__tests__/integration/admin/social/api.test.ts |
5.3 Success Criteria (Binary) — Social Connections#
-
/admin/social-connectionstable shows all 7 columns with correct data - Platform icons render correctly for Facebook, Instagram, LinkedIn, Twitter
- Status badges show Active (green), Expired (red), Pending (amber)
- "Disconnect" action calls
social.disconnectand updates status - "Refresh Token" action calls
social.refreshTokenand updates status on success - Expired accounts show "Reconnect" action; pending accounts show clock icon
- Filter by platform and practice works correctly
- Sort by follower count works numerically descending
-
admin.listSocialConnectionsreturns all connected accounts with correct metadata -
social.disconnectcalls Composio API, updates DB, and creates audit log entry -
social.refreshTokenfetches new token, updates DB, and reactivates account - Token refresh failure returns clear error without crashing
5.4 Agent Context (Pre-conditions) — Social Connections#
- Required DB state:
SocialAccount(25 records across 4 platforms with varied statuses),Practice(linked to accounts),ClientProfile - Required env vars:
COMPOSIO_API_KEY(mocked) - Required mocks: Composio API mocked for disconnect and refresh;
next/navigationmocked - tRPC mocks:
admin.listSocialConnections,social.disconnect,social.refreshTokenreturn deterministic data
5.5 Verification Commands — Social Connections#
# Unit: social connections page
pnpm test:unit -- src/__tests__/unit/admin/social/
# Integration: social API
pnpm test:integration -- src/__tests__/integration/admin/social/api.test.ts
6. Job Monitor Tests#
6.1 Unit Tests — Job Monitor (/admin/jobs)#
| Test | Input | Expected Output | File |
|---|---|---|---|
job-monitor.render |
jobs: [50 JobLog rows] |
Stat cards (Pending, Running, Completed, Failed) + data table render | src/__tests__/unit/admin/jobs/JobMonitorPage.test.tsx |
job-monitor.stat-cards |
{ pending: 12, running: 5, completed: 1200, failed: 30 } |
Cards show correct counts; Pending=yellow, Running=blue, Completed=green, Failed=red | src/__tests__/unit/admin/jobs/JobMonitorPage.test.tsx |
job-monitor.table-columns |
Mixed job statuses | Table shows Job ID, Skill, Practice, Status, Duration, Cost, Started, Actions | src/__tests__/unit/admin/jobs/JobMonitorPage.test.tsx |
job-monitor.status-badge-pending |
status: "PENDING" |
Yellow "PENDING" badge | src/__tests__/unit/admin/jobs/JobMonitorPage.test.tsx |
job-monitor.status-badge-running |
status: "RUNNING" |
Blue "RUNNING" badge | src/__tests__/unit/admin/jobs/JobMonitorPage.test.tsx |
job-monitor.status-badge-completed |
status: "COMPLETED" |
Green "COMPLETED" badge | src/__tests__/unit/admin/jobs/JobMonitorPage.test.tsx |
job-monitor.status-badge-failed |
status: "FAILED" |
Red "FAILED" badge | src/__tests__/unit/admin/jobs/JobMonitorPage.test.tsx |
job-monitor.action-retry |
Click "Retry" on failed job row | admin.retryJob called with jobId; toast shows "Job queued for retry" |
src/__tests__/unit/admin/jobs/JobMonitorPage.test.tsx |
job-monitor.action-view-logs |
Click "View logs" on failed job | JsonViewer modal opens with job payload, error stack, and attempt history |
src/__tests__/unit/admin/jobs/JobMonitorPage.test.tsx |
job-monitor.filter-status |
Select "FAILED" from status multi-select | Only failed jobs visible; stat cards still show totals | src/__tests__/unit/admin/jobs/JobMonitorPage.test.tsx |
job-monitor.filter-practice |
Select practice from dropdown | Only jobs for selected practice visible | src/__tests__/unit/admin/jobs/JobMonitorPage.test.tsx |
job-monitor.filter-skill |
Select skill from dropdown | Only jobs for selected skill visible | src/__tests__/unit/admin/jobs/JobMonitorPage.test.tsx |
job-monitor.filter-date-range |
Select "Last 24 hours" | Only jobs with startedAt within range visible |
src/__tests__/unit/admin/jobs/JobMonitorPage.test.tsx |
job-monitor.auto-refresh |
jobs data changes after 10s |
Table updates automatically every 10 seconds; new counts reflected in stat cards | src/__tests__/unit/admin/jobs/JobMonitorPage.test.tsx |
job-monitor.duration-format |
duration: 12.3 |
Displays "12.3s" | src/__tests__/unit/admin/jobs/JobMonitorPage.test.tsx |
job-monitor.cost-format |
cost: 0.045 |
Displays "$0.05" (rounded to 2 decimals) | src/__tests__/unit/admin/jobs/JobMonitorPage.test.tsx |
job-monitor.empty |
jobs: [] |
Empty state with "No jobs in queue" | src/__tests__/unit/admin/jobs/JobMonitorPage.test.tsx |
job-monitor.pagination |
totalJobs: 200 |
Correct page count; page size selector works | src/__tests__/unit/admin/jobs/JobMonitorPage.test.tsx |
6.2 Integration Tests — Job Monitor API#
| Test | Setup | Action | Assertion | File |
|---|---|---|---|---|
api.list-jobs |
BullMQ queues with 200 jobs across all states | Call admin.listJobs |
Returns array with id, skill, practiceName, status, duration, cost, startedAt |
src/__tests__/integration/admin/jobs/api.test.ts |
api.list-jobs-filter-status |
50 pending, 30 running, 100 completed, 20 failed | Call admin.listJobs({ status: ["FAILED"] }) |
Returns exactly 20 jobs; all have status: "FAILED" |
src/__tests__/integration/admin/jobs/api.test.ts |
api.list-jobs-filter-practice |
Jobs from 10 different practices | Call admin.listJobs({ practiceId: "prac_123" }) |
Returns only jobs for that practice | src/__tests__/integration/admin/jobs/api.test.ts |
api.list-jobs-filter-skill |
Jobs from 15 different skills | Call admin.listJobs({ skill: "gbp-post-publish" }) |
Returns only jobs for that skill | src/__tests__/integration/admin/jobs/api.test.ts |
api.list-jobs-filter-date |
Jobs from last 7 days | Call admin.listJobs({ dateFrom: "2026-06-06", dateTo: "2026-06-13" }) |
Returns only jobs within date range | src/__tests__/integration/admin/jobs/api.test.ts |
api.retry-job |
Failed job in DLQ with id: "job_456" |
Call admin.retryJob({ id: "job_456" }) |
Job re-added to original queue; removed from DLQ; returns { success: true, newJobId: "..." } |
src/__tests__/integration/admin/jobs/api.test.ts |
api.retry-job-invalid |
Job with status: "COMPLETED" |
Call admin.retryJob({ id: "job_789" }) |
Returns { success: false, error: "Job is not in failed state" } |
src/__tests__/integration/admin/jobs/api.test.ts |
api.retry-job-missing |
jobId: "job_nonexistent" |
Call admin.retryJob({ id: "job_nonexistent" }) |
Returns { success: false, error: "Job not found" } |
src/__tests__/integration/admin/jobs/api.test.ts |
api.job-stats |
Queue with 12 pending, 5 running, 1200 completed, 30 failed | Call admin.getJobStats |
Returns { pending: 12, running: 5, completed: 1200, failed: 30 } |
src/__tests__/integration/admin/jobs/api.test.ts |
api.job-logs |
Job with 3 attempts | Call admin.getJobLogs({ id: "job_123" }) |
Returns array of 3 log entries with timestamps, error messages, and stack traces | src/__tests__/integration/admin/jobs/api.test.ts |
6.3 Success Criteria (Binary) — Job Monitor#
-
/admin/jobsrenders 4 stat cards (Pending, Running, Completed, Failed) with correct colors - Job table shows all 8 columns (Job ID, Skill, Practice, Status, Duration, Cost, Started, Actions)
- Status badges render correctly for PENDING, RUNNING, COMPLETED, FAILED
- "Retry" action on failed job calls
admin.retryJoband shows toast confirmation - "View logs" action opens modal with
JsonViewershowing payload, error, and attempt history - Status multi-select filter shows only jobs with selected statuses
- Practice dropdown filter shows only jobs for selected practice
- Skill dropdown filter shows only jobs for selected skill
- Date range filter shows only jobs within selected range
- Table auto-refreshes every 10 seconds with updated queue state
- Duration formatted as seconds with 1 decimal (e.g., "12.3s")
- Cost formatted as USD with 2 decimals (e.g., "$0.05")
- Pagination works with configurable page size
- Empty state renders when no jobs match filters
-
admin.listJobsreturns paginated, filtered job list with all metadata -
admin.retryJobre-queues failed job from DLQ to original queue -
admin.retryJobrejects non-failed jobs with clear error message -
admin.retryJobreturns clear error for non-existent job IDs -
admin.getJobStatsreturns accurate queue depth counts per status -
admin.getJobLogsreturns full attempt history with error details
6.4 Agent Context (Pre-conditions) — Job Monitor#
- Required DB state:
JobLog(200+ records across all statuses),Practice(linked to jobs), DLQ with 20 failed jobs - Required env vars:
REDIS_URLforioredis-mock;BULLMQ_PREFIX(optional) - Required mocks: BullMQ queue state mocked with known job counts;
next/navigationmocked - Mock Redis state: Fresh
ioredis-mockwith populated queues;flushallbetween suites - tRPC mocks:
admin.listJobs,admin.retryJob,admin.getJobStats,admin.getJobLogsreturn deterministic data - Timer control:
vi.useFakeTimers()for auto-refresh tests
6.5 Verification Commands — Job Monitor#
# Unit: job monitor page
pnpm test:unit -- src/__tests__/unit/admin/jobs/
# Integration: job API + retry
pnpm test:integration -- src/__tests__/integration/admin/jobs/api.test.ts
# Full job monitor suite
pnpm test:unit -- src/__tests__/unit/admin/jobs/
pnpm test:integration -- src/__tests__/integration/admin/jobs/
7. Content Review Tests#
7.1 Unit Tests — Content Review (/admin/content)#
| Test | Input | Expected Output | File |
|---|---|---|---|
content-review.render |
contentPieces: [40 ContentPiece rows] |
Split view renders: list left, preview right; Prompt Management tab visible | src/__tests__/unit/admin/content/ContentReviewPage.test.tsx |
content-review.list-columns |
Mixed content types | List shows Title/Type, Practice, Status, AI Provider, Cost, Created | src/__tests__/unit/admin/content/ContentReviewPage.test.tsx |
content-review.status-draft |
status: "DRAFT" |
Gray badge | src/__tests__/unit/admin/content/ContentReviewPage.test.tsx |
content-review.status-pending |
status: "PENDING_REVIEW" |
Yellow badge with countdown timer | src/__tests__/unit/admin/content/ContentReviewPage.test.tsx |
content-review.status-approved |
status: "APPROVED" |
Green badge | src/__tests__/unit/admin/content/ContentReviewPage.test.tsx |
content-review.status-published |
status: "PUBLISHED" |
Blue badge | src/__tests__/unit/admin/content/ContentReviewPage.test.tsx |
content-review.status-rejected |
status: "REJECTED" |
Red badge | src/__tests__/unit/admin/content/ContentReviewPage.test.tsx |
content-review.preview-render |
Select content piece with html: "<h1>Title</h1><p>Body</p>" |
Preview panel renders HTML with dangerouslySetInnerHTML; SEO score and readability score visible |
src/__tests__/unit/admin/content/ContentReviewPage.test.tsx |
content-review.action-approve |
Select PENDING_REVIEW piece; click "Approve" | admin.approveContent called with id; status changes to APPROVED; toast confirmation |
src/__tests__/unit/admin/content/ContentReviewPage.test.tsx |
content-review.action-reject |
Select PENDING_REVIEW piece; click "Reject" | admin.rejectContent called with id; status changes to REJECTED; toast confirmation |
src/__tests__/unit/admin/content/ContentReviewPage.test.tsx |
content-review.action-edit |
Select DRAFT piece; click "Edit" | Inline editor opens with content; save calls admin.updateContent |
src/__tests__/unit/admin/content/ContentReviewPage.test.tsx |
content-review.prompt-tab |
Click "Prompt Management" tab | Prompt template list renders; system prompt and user prompt templates visible | src/__tests__/unit/admin/content/ContentReviewPage.test.tsx |
content-review.prompt-edit |
Click "Edit" on system prompt | Inline editor opens; save calls admin.updatePrompt with new template |
src/__tests__/unit/admin/content/ContentReviewPage.test.tsx |
content-review.prompt-ab |
Select prompt with A/B variants | Performance scores (click-through, conversion) shown per variant; winner highlighted | src/__tests__/unit/admin/content/ContentReviewPage.test.tsx |
content-review.filter-status |
Select "PENDING_REVIEW" from filter | Only pending review pieces visible | src/__tests__/unit/admin/content/ContentReviewPage.test.tsx |
content-review.filter-practice |
Select practice from dropdown | Only content for selected practice visible | src/__tests__/unit/admin/content/ContentReviewPage.test.tsx |
content-review.filter-provider |
Select "Claude" from AI Provider filter | Only Claude-generated content visible | src/__tests__/unit/admin/content/ContentReviewPage.test.tsx |
content-review.empty |
contentPieces: [] |
Empty state with "No content awaiting review" | src/__tests__/unit/admin/content/ContentReviewPage.test.tsx |
7.2 Integration Tests — Content API#
| Test | Setup | Action | Assertion | File |
|---|---|---|---|---|
api.list-content |
DB with 40 ContentPiece records across all statuses |
Call admin.listContent |
Returns array with id, title, type, practiceName, status, aiProvider, cost, createdAt |
src/__tests__/integration/admin/content/api.test.ts |
api.list-content-filter |
Mixed statuses, providers, practices | Call admin.listContent with { status: "PENDING_REVIEW", aiProvider: "Claude" } |
Returns only matching records; count accurate | src/__tests__/integration/admin/content/api.test.ts |
api.approve-content |
ContentPiece with status: "PENDING_REVIEW", autoPublishAt: <future> |
Call admin.approveContent({ id: "content_123" }) |
DB status updated to "APPROVED"; publish scheduled immediately; auditLog entry created |
src/__tests__/integration/admin/content/api.test.ts |
api.reject-content |
ContentPiece with status: "PENDING_REVIEW" |
Call admin.rejectContent({ id: "content_123" }) |
DB status updated to "REJECTED"; auditLog entry created; triggers regeneration if configured |
src/__tests__/integration/admin/content/api.test.ts |
api.update-prompt |
Existing prompt template | Call admin.updatePrompt({ id: "prompt_1", systemPrompt: "New system prompt..." }) |
DB updated; version incremented; auditLog entry created; profile variant test flag preserved |
src/__tests__/integration/admin/content/api.test.ts |
api.content-preview |
ContentPiece with html and seoScore: 85, readabilityScore: 72 |
Call content.getPreview({ id: "content_123" }) |
Returns rendered HTML, SEO score, readability score, and suggestion list | src/__tests__/integration/admin/content/api.test.ts |
7.3 Success Criteria (Binary) — Content Review#
-
/admin/contentrenders split view: list left, preview right; Prompt Management tab available - Content list shows all 6 columns (Title/Type, Practice, Status, AI Provider, Cost, Created)
- Status badges render correctly for DRAFT, PENDING_REVIEW, APPROVED, PUBLISHED, REJECTED, FAILED
- PENDING_REVIEW badge shows 24-hour auto-publish countdown timer
- Preview panel renders HTML content safely; SEO score and readability score visible
- "Approve" action calls
admin.approveContent; status changes to APPROVED; toast confirms - "Reject" action calls
admin.rejectContent; status changes to REJECTED; toast confirms - "Edit" action opens inline editor; save calls
admin.updateContent - Prompt Management tab lists all prompt templates with system and user prompts
- Prompt edit opens inline editor; save increments version and creates audit log entry
- profile variant test variants show performance scores; winner highlighted
- Filter by status, practice, and AI provider works correctly
- Empty state renders when no content pieces match filters
-
admin.listContentreturns paginated, filtered content list with all metadata -
admin.approveContentupdates status, schedules publish, and creates audit log entry -
admin.rejectContentupdates status, creates audit log, and optionally triggers regeneration -
admin.updatePromptupdates template, increments version, and preserves profile variant test config -
content.getPreviewreturns rendered HTML with SEO and readability scores
7.4 Agent Context (Pre-conditions) — Content Review#
- Required DB state:
ContentPiece(40 records across all statuses, types, providers),Practice(linked to content),PromptTemplate(5 templates with A/B variants),AuditLog - Required env vars:
AI_MODELSconfig (mocked) - Required mocks:
next/navigationmocked; HTML sanitizer mocked if needed - tRPC mocks:
admin.listContent,admin.approveContent,admin.rejectContent,admin.updatePrompt,content.getPreviewreturn deterministic data
7.5 Verification Commands — Content Review#
# Unit: content review page
pnpm test:unit -- src/__tests__/unit/admin/content/
# Integration: content API
pnpm test:integration -- src/__tests__/integration/admin/content/api.test.ts
8. Billing Tests#
8.1 Unit Tests — Billing (/admin/billing)#
| Test | Input | Expected Output | File |
|---|---|---|---|
billing.render |
revenueData, subscriptions, invoices, failedCharges |
4 revenue cards + 3 tables render | src/__tests__/unit/admin/billing/BillingOverviewPage.test.tsx |
billing.revenue-cards |
{ mrr: 480000, arr: 5760000, activeSubscriptions: 80, failedPayments: 3 } |
Cards show MRR "₹4,80,000", ARR "₹57,60,000", 80 active, 3 failed; failed card is red | src/__tests__/unit/admin/billing/BillingOverviewPage.test.tsx |
billing.subscriptions-table |
subscriptions: [80 Subscription rows] |
Table shows Practice, Tier, Status, Next Billing, Amount; pagination if >25 | src/__tests__/unit/admin/billing/BillingOverviewPage.test.tsx |
billing.invoices-table |
invoices: [200 Invoice rows] |
Table shows Invoice ID, Practice, Amount, Status, Paid Date; sortable by date | src/__tests__/unit/admin/billing/BillingOverviewPage.test.tsx |
billing.failed-charges-table |
failedCharges: [5 FailedCharge rows] |
Table shows Practice, Amount, Failure Reason, Retry Count; red status badges | src/__tests__/unit/admin/billing/BillingOverviewPage.test.tsx |
billing.filter-tier |
Select "PREMIUM" from tier filter | Only premium subscriptions visible | src/__tests__/unit/admin/billing/BillingOverviewPage.test.tsx |
billing.filter-status |
Select "Past Due" from status filter | Only past due subscriptions visible | src/__tests__/unit/admin/billing/BillingOverviewPage.test.tsx |
billing.search-practice |
Type "dental" in search | Only subscriptions/invoices for matching practices visible | src/__tests__/unit/admin/billing/BillingOverviewPage.test.tsx |
billing.export-revenue |
Click "Export Revenue CSV" | CSV blob downloaded with revenue by month data | src/__tests__/unit/admin/billing/BillingOverviewPage.test.tsx |
billing.empty-invoices |
invoices: [] |
Empty state "No invoices found" | src/__tests__/unit/admin/billing/BillingOverviewPage.test.tsx |
8.2 Integration Tests — Billing API#
| Test | Setup | Action | Assertion | File |
|---|---|---|---|---|
api.get-revenue |
DB with 80 active subscriptions across 4 tiers | Call admin.getRevenue |
Returns { mrr, arr, activeSubscriptions, failedPayments, revenueByMonth: [{ month, amount }] } |
src/__tests__/integration/admin/billing/api.test.ts |
api.mrr-calculation |
Subscriptions: 20 Starter @ ₹4,000, 40 Standard @ ₹8,000, 15 Premium @ ₹12,000, 5 Enterprise @ ₹20,000 | Call admin.getRevenue |
MRR = (20×4000 + 40×8000 + 15×12000 + 5×20000) = ₹6,80,000; ARR = MRR × 12 | src/__tests__/integration/admin/billing/api.test.ts |
api.list-invoices |
DB with 200 Invoice records |
Call billing.listInvoices |
Returns paginated array with id, practiceName, amount, status, paidDate, pdfUrl |
src/__tests__/integration/admin/billing/api.test.ts |
api.list-invoices-filter |
Invoices from 10 practices | Call billing.listInvoices({ practiceId: "prac_123" }) |
Returns only invoices for that practice | src/__tests__/integration/admin/billing/api.test.ts |
api.failed-charges |
DB with 5 failed payments in last 7 days | Call admin.getFailedCharges |
Returns array with practiceName, amount, failureReason, retryCount, lastRetryAt |
src/__tests__/integration/admin/billing/api.test.ts |
api.subscription-detail |
Subscription with status: "trialing", trialEndsAt: <future> |
Call billing.getSubscription({ practiceId: "prac_123" }) |
Returns full subscription with plan, status, next billing, payment method, and trial info | src/__tests__/integration/admin/billing/api.test.ts |
8.3 Success Criteria (Binary) — Billing#
-
/admin/billingrenders 4 revenue cards (MRR, ARR, Active Subscriptions, Failed Payments) - MRR and ARR formatted in INR with comma separators
- Failed Payments card shows red color when > 0
- Subscriptions table shows 5 columns (Practice, Tier, Status, Next Billing, Amount)
- Invoices table shows 5 columns (Invoice ID, Practice, Amount, Status, Paid Date)
- Failed Charges table shows 4 columns (Practice, Amount, Failure Reason, Retry Count)
- Filter by tier, status, and practice search works correctly
- Revenue export CSV downloads correct monthly revenue data
- Empty state renders when no invoices or failed charges exist
-
admin.getRevenuecalculates MRR as sum of all active subscription amounts -
admin.getRevenuecalculates ARR as MRR × 12 -
admin.getRevenuereturns accurateactiveSubscriptionscount by tier -
admin.getRevenuereturns accuratefailedPaymentscount for last 7 days -
billing.listInvoicesreturns paginated, filtered invoice list with all metadata -
admin.getFailedChargesreturns all failed payments in last 7 days with retry info -
billing.getSubscriptionreturns full subscription details including trial status
8.4 Agent Context (Pre-conditions) — Billing#
- Required DB state:
Subscription(80 active across 4 tiers),Invoice(200 records),Billing(with failed charges),ClientProfile(linked to subscriptions),PaymentMethod(linked to billing) - Required env vars:
CURRENCY_CODE="INR";STRIPE_WEBHOOK_SECRET/RAZORPAY_WEBHOOK_SECRET(mocked) - Required mocks:
next/navigationmocked; CSV export utilities mocked - tRPC mocks:
admin.getRevenue,billing.listInvoices,admin.getFailedCharges,billing.getSubscriptionreturn deterministic data
8.5 Verification Commands — Billing#
# Unit: billing page
pnpm test:unit -- src/__tests__/unit/admin/billing/
# Integration: billing API + MRR calculation
pnpm test:integration -- src/__tests__/integration/admin/billing/api.test.ts
9. System Reports Tests#
9.1 Unit Tests — System Reports (/admin/reports)#
| Test | Input | Expected Output | File |
|---|---|---|---|
reports.render |
reports data loaded |
6 report cards render with title, period, and export buttons | src/__tests__/unit/admin/reports/SystemReportsPage.test.tsx |
reports.revenue-by-month |
Click "Revenue by Month" card | Table shows month, MRR, new clients, churned clients, net revenue; CSV export available | src/__tests__/unit/admin/reports/SystemReportsPage.test.tsx |
reports.client-churn |
Click "Client Churn" card | Table shows month, starting clients, churned, churn rate %, reason breakdown; CSV export available | src/__tests__/unit/admin/reports/SystemReportsPage.test.tsx |
reports.citation-success |
Click "Citation Success Rate" card | Table shows week, submitted count, failed count, success rate %, top failing directories; CSV export available | src/__tests__/unit/admin/reports/SystemReportsPage.test.tsx |
reports.api-cost-breakdown |
Click "API Cost Breakdown" card | Table shows day, provider, requests, cost; stacked bar chart by provider; CSV export available | src/__tests__/unit/admin/reports/SystemReportsPage.test.tsx |
reports.content-volume |
Click "Content Generation Volume" card | Table shows week, articles generated, GBP posts, social posts, total cost; CSV export available | src/__tests__/unit/admin/reports/SystemReportsPage.test.tsx |
reports.support-tickets |
Click "Support Ticket Summary" card | Table shows month, total tickets, resolved, avg resolution time, categories; CSV export available | src/__tests__/unit/admin/reports/SystemReportsPage.test.tsx |
reports.export-csv |
Click "Export CSV" on any report | CSV blob downloaded with correct headers and all data rows | src/__tests__/unit/admin/reports/SystemReportsPage.test.tsx |
reports.export-pdf |
Click "Export PDF" on "Revenue by Month" | PDF blob downloaded with branded report layout | src/__tests__/unit/admin/reports/SystemReportsPage.test.tsx |
reports.date-range-filter |
Select "Last 3 months" from global filter | All report cards update to show data for selected date range | src/__tests__/unit/admin/reports/SystemReportsPage.test.tsx |
reports.empty |
No data for selected range | Empty state with "No data for selected period" | src/__tests__/unit/admin/reports/SystemReportsPage.test.tsx |
9.2 Integration Tests — Reports API#
| Test | Setup | Action | Assertion | File |
|---|---|---|---|---|
api.revenue-by-month |
DB with 12 months of subscription data | Call admin.getRevenue({ period: "monthly", months: 12 }) |
Returns 12 rows with month, mrr, newClients, churnedClients, netRevenue |
src/__tests__/integration/admin/reports/api.test.ts |
api.churn-calculation |
DB: 100 clients at start of month, 5 churned during month | Call admin.getChurnReport({ month: "2026-05" }) |
churnRate = 5.0%; startingClients = 100; churned = 5 |
src/__tests__/integration/admin/reports/api.test.ts |
api.citation-success-rate |
DB: 30 citations attempted, 24 submitted, 6 failed | Call admin.getCitationSuccessReport({ week: "2026-W23" }) |
successRate = 80.0%; submitted = 24; failed = 6; failing directories listed |
src/__tests__/integration/admin/reports/api.test.ts |
api.api-cost-daily |
AiUsage table: 50 records across 6 providers over 7 days |
Call admin.getApiCostReport({ period: "daily", days: 7 }) |
Returns 7 rows with daily totals per provider; grand total matches sum of all records | src/__tests__/integration/admin/reports/api.test.ts |
api.content-volume |
ContentPiece table: 100 articles, 50 GBP posts, 40 social posts |
Call admin.getContentVolumeReport({ week: "2026-W23" }) |
articles = 100; gbpPosts = 50; socialPosts = 40; totalCost = sum of AiUsage for content generation |
src/__tests__/integration/admin/reports/api.test.ts |
api.support-tickets |
SupportTicket table: 20 tickets in month |
Call admin.getSupportTicketReport({ month: "2026-05" }) |
total = 20; resolved = 18; avgResolutionTime = calculated; categories breakdown |
src/__tests__/integration/admin/reports/api.test.ts |
api.gross-margin |
MRR = ₹6,80,000; COGS = ₹50,000 | Call admin.getGrossMarginReport({ month: "2026-05" }) |
grossMargin = ((6,80,000 - 50,000) / 6,80,000) × 100 = 92.65% |
src/__tests__/integration/admin/reports/api.test.ts |
9.3 Success Criteria (Binary) — System Reports#
-
/admin/reportsrenders 6 report cards: Revenue by Month, Client Churn, Citation Success Rate, API Cost Breakdown, Content Generation Volume, Support Ticket Summary - Each report card shows period label and export buttons (CSV, PDF where applicable)
- Revenue by Month table shows month, MRR, new clients, churned clients, net revenue
- Client Churn table shows month, starting clients, churned, churn rate %, reason breakdown
- Citation Success Rate table shows week, submitted, failed, success rate %, top failing directories
- API Cost Breakdown table shows day, provider, requests, cost; stacked bar chart renders
- Content Generation Volume table shows week, articles, GBP posts, social posts, total cost
- Support Ticket Summary table shows month, total, resolved, avg resolution time, categories
- CSV export downloads correct data with proper headers
- PDF export downloads branded report for Revenue by Month
- Global date range filter updates all report cards simultaneously
- Empty state renders when no data for selected period
-
admin.getRevenuereturns monthly revenue data with accurate MRR, new clients, churned clients - Churn rate calculated as
(churned / startingClients) * 100with correct rounding - Citation success rate calculated as
(submitted / attempted) * 100with correct rounding - API cost breakdown aggregates per provider per day with correct totals
- Content volume report counts all content types and sums generation costs
- Gross margin calculated as
((MRR - COGS) / MRR) * 100with correct rounding
9.4 Agent Context (Pre-conditions) — System Reports#
- Required DB state:
Subscription(12 months),ClientProfile(withchurnedAtdates),Citation(with weekly batch data),AiUsage(daily records),ContentPiece(weekly counts),SupportTicket(monthly records),Invoice(monthly) - Required env vars:
CURRENCY_CODE="INR" - Required mocks: CSV/PDF generation utilities mocked;
next/navigationmocked - tRPC mocks:
admin.getRevenue,admin.getChurnReport,admin.getCitationSuccessReport,admin.getApiCostReport,admin.getContentVolumeReport,admin.getSupportTicketReportreturn deterministic data
9.5 Verification Commands — System Reports#
# Unit: reports page
pnpm test:unit -- src/__tests__/unit/admin/reports/
# Integration: reports API + calculations
pnpm test:integration -- src/__tests__/integration/admin/reports/api.test.ts
10. Impersonation Feature Tests#
10.1 Unit Tests — Impersonation#
| Test | Input | Expected Output | File |
|---|---|---|---|
impersonate.button-render |
Admin viewing client detail page | "Impersonate" button visible in overview card actions | src/__tests__/unit/admin/impersonation/ImpersonateButton.test.tsx |
impersonate.button-hidden |
Non-admin viewing any page | "Impersonate" button not rendered | src/__tests__/unit/admin/impersonation/ImpersonateButton.test.tsx |
impersonate.modal-confirm |
Click "Impersonate" button | Modal opens with warning: "You are about to view this client's dashboard. All actions will be logged." | src/__tests__/unit/admin/impersonation/ImpersonateButton.test.tsx |
impersonate.badge-render |
Admin is impersonating practiceId: "prac_123" |
Red banner at top: "Impersonating Dr. Smith Dental — Exit Impersonation" | src/__tests__/unit/admin/impersonation/ImpersonationBanner.test.tsx |
impersonate.badge-exit |
Click "Exit Impersonation" in banner | Admin redirected back to /admin/clients/prac_123; x-practice-id header cleared |
src/__tests__/unit/admin/impersonation/ImpersonationBanner.test.tsx |
impersonate.session-indicator |
session.impersonating: true, session.impersonatedPracticeId: "prac_123" |
Session object includes impersonation flags; UI reflects impersonation state | src/__tests__/unit/admin/impersonation/ImpersonationBanner.test.tsx |
10.2 Integration Tests — Impersonation API & Flow#
| Test | Setup | Action | Assertion | File |
|---|---|---|---|---|
impersonate.start |
Admin session; practiceId: "prac_123" |
Call admin.impersonate({ practiceId: "prac_123" }) |
Session updated with impersonating: true, impersonatedPracticeId: "prac_123"; x-practice-id header set to "prac_123" |
src/__tests__/integration/admin/impersonation/api.test.ts |
impersonate.procedure-access |
Admin impersonating prac_123 |
Call practice.get({ id: "prac_123" }) via practiceProcedure |
Returns practice data successfully (admin temporarily has practiceProcedure access) |
src/__tests__/integration/admin/impersonation/api.test.ts |
impersonate.procedure-blocked-other |
Admin impersonating prac_123 |
Call practice.get({ id: "prac_456" }) via practiceProcedure |
Returns TRPCError with code FORBIDDEN (cannot access other practice while impersonating) |
src/__tests__/integration/admin/impersonation/api.test.ts |
impersonate.audit-log |
Admin impersonating prac_123 |
Call admin.impersonate({ practiceId: "prac_123" }) |
AuditLog entry created with action: "IMPERSONATE_START", actorId: <admin_id>, targetPracticeId: "prac_123", ipAddress, timestamp |
src/__tests__/integration/admin/impersonation/api.test.ts |
impersonate.audit-log-end |
Admin exits impersonation | Call admin.endImpersonate() |
AuditLog entry created with action: "IMPERSONATE_END", actorId: <admin_id>, targetPracticeId: "prac_123" |
src/__tests__/integration/admin/impersonation/api.test.ts |
impersonate.dashboard-view |
Admin impersonating prac_123 |
Navigate to /dashboard |
Client dashboard renders with prac_123 data; admin sidebar replaced by client sidebar; impersonation banner visible |
src/__tests__/integration/admin/impersonation/flow.test.tsx |
impersonate.action-audit |
Admin impersonating prac_123; clicks "Approve Content" on client dashboard |
Action executes with practiceId: "prac_123" |
AuditLog entry created with action: "CONTENT_APPROVED", actorId: <admin_id>, practiceId: "prac_123", impersonating: true |
src/__tests__/integration/admin/impersonation/flow.test.tsx |
impersonate.client-blocked |
Client session | Call admin.impersonate({ practiceId: "prac_123" }) |
Returns TRPCError with code FORBIDDEN (only ADMIN can impersonate) |
src/__tests__/integration/admin/impersonation/api.test.ts |
impersonate.nonexistent-practice |
Admin session; practiceId: "prac_missing" |
Call admin.impersonate({ practiceId: "prac_missing" }) |
Returns TRPCError with code NOT_FOUND |
src/__tests__/integration/admin/impersonation/api.test.ts |
impersonate.suspended-practice |
Admin session; practiceId: "prac_suspended" with status: "SUSPENDED" |
Call admin.impersonate({ practiceId: "prac_suspended" }) |
Returns TRPCError with code BAD_REQUEST and message "Cannot impersonate suspended client" |
src/__tests__/integration/admin/impersonation/api.test.ts |
impersonate.session-persistence |
Admin impersonating; refreshes page | Reload /dashboard |
Impersonation state preserved in session; x-practice-id header still set; banner still visible |
src/__tests__/integration/admin/impersonation/flow.test.tsx |
impersonate.timeout |
Admin impersonating for > 2 hours | Wait 2 hours (mock timers) | Impersonation auto-expires; session cleared; redirected to /admin/clients/{id} |
src/__tests__/integration/admin/impersonation/flow.test.tsx |
10.3 Success Criteria (Binary) — Impersonation#
- "Impersonate" button visible on client detail page for ADMIN role only
- Clicking "Impersonate" opens confirmation modal with warning message
- Confirming impersonation updates session with
impersonating: trueandimpersonatedPracticeId -
x-practice-idheader set to impersonated practice ID on all subsequent requests - Admin gains temporary
practiceProcedureaccess for the impersonated practice only - Access to other practices is blocked during impersonation (403 FORBIDDEN)
- Client dashboard renders correctly when accessed via impersonation
- Client sidebar replaces admin sidebar during impersonation
- Red impersonation banner shows practice name and "Exit Impersonation" button at top of page
- Clicking "Exit Impersonation" clears session flags, removes header, redirects to admin client detail
-
AuditLogentry created withIMPERSONATE_STARTon impersonation begin -
AuditLogentry created withIMPERSONATE_ENDon impersonation end - All actions performed during impersonation are logged with
impersonating: trueflag - Non-admin users cannot call
admin.impersonate(403 FORBIDDEN) - Impersonating non-existent practice returns 404 NOT FOUND
- Impersonating suspended practice returns 400 BAD REQUEST with clear message
- Impersonation state persists across page reloads (session-backed)
- Impersonation auto-expires after 2 hours of inactivity; session cleared
10.4 Agent Context (Pre-conditions) — Impersonation#
- Required DB state:
User(1 admin, 1 client),ClientProfile(2 practices: 1 active, 1 suspended),AuditLog(empty or seeded),ContentPiece(pending review for impersonated practice) - Required env vars:
IMPERSONATION_TIMEOUT_MINUTES=120;ADMIN_ROLE="ADMIN" - Required mocks:
next-auth/reactgetServerSessionmocked to return admin session with impersonation flags;next/navigationmocked for redirect assertions - Session state: Admin session must support
impersonatingandimpersonatedPracticeIdfields - tRPC mocks:
admin.impersonate,admin.endImpersonate,practice.getreturn deterministic data based on impersonation state - Timer control:
vi.useFakeTimers()for timeout tests
10.5 Verification Commands — Impersonation#
# Unit: impersonation components
pnpm test:unit -- src/__tests__/unit/admin/impersonation/
# Integration: impersonation API + flow
pnpm test:integration -- src/__tests__/integration/admin/impersonation/api.test.ts
pnpm test:integration -- src/__tests__/integration/admin/impersonation/flow.test.tsx
# Full impersonation suite
pnpm test:unit -- src/__tests__/unit/admin/impersonation/
pnpm test:integration -- src/__tests__/integration/admin/impersonation/
11. KPI Calculation Tests#
11.1 Unit Tests — KPI Calculation Logic#
| Test | Input | Expected Output | File |
|---|---|---|---|
kpi.mrr-simple |
Subscriptions: [Starter ₹4,000, Standard ₹8,000, Premium ₹12,000] | MRR = ₹24,000 | src/__tests__/unit/admin/kpi/calculations.test.ts |
kpi.mrr-trial-excluded |
Subscriptions: [Active ₹8,000, Trial ₹4,000, Churned ₹12,000] | MRR = ₹8,000 (trial and churned excluded) | src/__tests__/unit/admin/kpi/calculations.test.ts |
kpi.mrr-paused-excluded |
Subscriptions: [Active ₹8,000, Paused ₹8,000] | MRR = ₹8,000 (paused excluded) | src/__tests__/unit/admin/kpi/calculations.test.ts |
kpi.arr-calculation |
MRR = ₹4,80,000 | ARR = ₹57,60,000 | src/__tests__/unit/admin/kpi/calculations.test.ts |
kpi.citation-health-perfect |
30 citations, 30 MATCHED | 100.0% | src/__tests__/unit/admin/kpi/calculations.test.ts |
kpi.citation-health-partial |
30 citations, 24 MATCHED, 4 MISMATCHED, 2 FAILED | 80.0% (MATCHED / total attempted) | src/__tests__/unit/admin/kpi/calculations.test.ts |
kpi.citation-health-zero |
0 citations attempted | 0.0% (no division by zero) | src/__tests__/unit/admin/kpi/calculations.test.ts |
kpi.churn-rate |
100 clients start of month, 5 churned | 5.0% | src/__tests__/unit/admin/kpi/calculations.test.ts |
kpi.churn-rate-zero |
100 clients start, 0 churned | 0.0% | src/__tests__/unit/admin/kpi/calculations.test.ts |
kpi.api-cost-aggregation |
AiUsage: [{ provider: "claude", cost: 150 }, { provider: "claude", cost: 25 }, { provider: "openai", cost: 50 }] | { claude: 175, openai: 50 } |
src/__tests__/unit/admin/kpi/calculations.test.ts |
kpi.gross-margin |
MRR = ₹6,80,000, COGS = ₹50,000 | 92.65% (rounded to 2 decimals) | src/__tests__/unit/admin/kpi/calculations.test.ts |
kpi.gross-margin-zero-mrr |
MRR = 0, COGS = 0 | 0.0% (no division by zero) | src/__tests__/unit/admin/kpi/calculations.test.ts |
kpi.nap-consistency |
30 citations, 28 MATCHED, 2 MISMATCHED | 93.33% (MATCHED / total live) | src/__tests__/unit/admin/kpi/calculations.test.ts |
kpi.gbp-post-success |
15 scheduled, 14 PUBLISHED, 1 FAILED | 93.33% | src/__tests__/unit/admin/kpi/calculations.test.ts |
kpi.social-post-success |
20 scheduled, 18 PUBLISHED, 2 FAILED | 90.0% | src/__tests__/unit/admin/kpi/calculations.test.ts |
kpi.token-refresh-success |
50 tokens, 49 refreshed, 1 failed | 98.0% | src/__tests__/unit/admin/kpi/calculations.test.ts |
kpi.email-delivery-rate |
1000 sent, 950 delivered, 50 bounced | 95.0% | src/__tests__/unit/admin/kpi/calculations.test.ts |
kpi.failed-jobs-24h |
Jobs: 100 completed today, 5 failed today, 3 failed yesterday | failedJobs24h = 5 |
src/__tests__/unit/admin/kpi/calculations.test.ts |
kpi.active-jobs |
Jobs: 5 RUNNING, 12 PENDING, 100 COMPLETED, 20 FAILED | activeJobs = 17 (RUNNING + PENDING) |
src/__tests__/unit/admin/kpi/calculations.test.ts |
kpi.customer-engagement |
80 clients, 60 logged in >=2 times this week | 75.0% engagement rate | src/__tests__/unit/admin/kpi/calculations.test.ts |
11.2 Integration Tests — KPI Aggregation from DB#
| Test | Setup | Action | Assertion | File |
|---|---|---|---|---|
kpi.db-mrr-aggregation |
Subscription table: 80 active across 4 tiers |
Query SELECT SUM(amount) FROM Subscription WHERE status = 'active' |
Result equals calculated MRR from admin.getKPIs |
src/__tests__/integration/admin/kpi/db-aggregation.test.ts |
kpi.db-citation-health |
Citation table: 30 records, 26 MATCHED, 4 FAILED |
Query SELECT COUNT(*) FROM Citation WHERE matchStatus = 'MATCHED' |
citationHealth from API = (26 / 30) * 100 = 86.67% |
src/__tests__/integration/admin/kpi/db-aggregation.test.ts |
kpi.db-api-cost-7d |
AiUsage table: 100 records over 7 days |
Query SELECT provider, SUM(cost) FROM AiUsage WHERE createdAt >= now() - interval '7 days' GROUP BY provider |
Sums match apiCosts7d from API exactly |
src/__tests__/integration/admin/kpi/db-aggregation.test.ts |
kpi.db-failed-jobs-24h |
JobLog table: 5 failed in last 24h, 3 failed 48h ago |
Query SELECT COUNT(*) FROM JobLog WHERE status = 'FAILED' AND createdAt >= now() - interval '24 hours' |
Count matches failedJobs24h from API |
src/__tests__/integration/admin/kpi/db-aggregation.test.ts |
kpi.db-recent-activity |
AuditLog table: 50 records |
Query SELECT * FROM AuditLog ORDER BY createdAt DESC LIMIT 20 |
Result matches recentActivity from API exactly |
src/__tests__/integration/admin/kpi/db-aggregation.test.ts |
kpi.db-churn-count |
ClientProfile table: 5 with status: "CHURNED", churnedAt in current month |
Query SELECT COUNT(*) FROM ClientProfile WHERE status = 'CHURNED' AND churnedAt >= date_trunc('month', now()) |
Count matches churnedClients from API |
src/__tests__/integration/admin/kpi/db-aggregation.test.ts |
11.3 Success Criteria (Binary) — KPI Calculation#
- MRR calculated as sum of all active subscription amounts; trial, churned, and paused excluded
- ARR calculated as MRR × 12
- Citation health calculated as
(MATCHED citations / total attempted citations) * 100with no division by zero - Churn rate calculated as
(churned clients / starting clients) * 100with no division by zero - API cost 7-day aggregation sums per provider correctly from
AiUsagetable - Gross margin calculated as
((MRR - COGS) / MRR) * 100with no division by zero - NAP consistency calculated as
(MATCHED live citations / total live citations) * 100 - GBP post success rate calculated as
(PUBLISHED / total scheduled) * 100 - Social post success rate calculated as
(PUBLISHED / total scheduled) * 100 - Token refresh success rate calculated as
(successful refreshes / total attempts) * 100 - Email delivery rate calculated as
(DELIVERED / total sent) * 100 - Failed jobs 24h count only includes jobs failed in last 24 hours
- Active jobs count includes both RUNNING and PENDING statuses
- Customer engagement rate calculated as
(clients with >=2 logins this week / total active clients) * 100 - All KPI DB aggregations match direct SQL query results exactly
- All percentage calculations handle zero denominators gracefully (return 0.0%)
11.4 Agent Context (Pre-conditions) — KPI Calculation#
- Required DB state:
Subscription(80 active, 15 trial, 5 churned),ClientProfile(withchurnedAtdates),Citation(30 records with variedmatchStatus),AiUsage(100 records over 7 days across 6 providers),JobLog(mixed statuses and timestamps),AuditLog(50 records),EmailLog(1000 records with varied statuses),OauthToken(50 records with refresh history) - Required env vars:
CURRENCY_CODE="INR" - Required mocks: Prisma aggregation queries mocked if using mock DB; direct SQL assertions via test DB
- Calculation baseline: Pre-computed expected values for all KPIs to assert against
11.5 Verification Commands — KPI Calculation#
# Unit: KPI calculation logic
pnpm test:unit -- src/__tests__/unit/admin/kpi/calculations.test.ts
# Integration: DB aggregation accuracy
pnpm test:integration -- src/__tests__/integration/admin/kpi/db-aggregation.test.ts
# Full KPI suite
pnpm test:unit -- src/__tests__/unit/admin/kpi/
pnpm test:integration -- src/__tests__/integration/admin/kpi/
12. Job Monitoring & Alert Threshold Tests#
12.1 Unit Tests — Alert Threshold Logic#
| Test | Input | Expected Output | File |
|---|---|---|---|
alert.queue-depth-warning |
queueDepth: 150 |
alertTriggered: true, severity: "WARNING", alertType: "QUEUE_DEPTH_HIGH" |
src/__tests__/unit/admin/alerts/thresholds.test.ts |
alert.queue-depth-normal |
queueDepth: 50 |
alertTriggered: false |
src/__tests__/unit/admin/alerts/thresholds.test.ts |
alert.failure-rate-warning |
failed: 6, total: 100, window: "1h" |
alertTriggered: true, severity: "WARNING", alertType: "FAILURE_RATE_HIGH" |
src/__tests__/unit/admin/alerts/thresholds.test.ts |
alert.failure-rate-normal |
failed: 1, total: 100, window: "1h" |
alertTriggered: false |
src/__tests__/unit/admin/alerts/thresholds.test.ts |
alert.api-cost-warning |
dailyCost: 55, practiceId: "prac_123" |
alertTriggered: true, severity: "WARNING", alertType: "API_COST_HIGH", flagForReview: true |
src/__tests__/unit/admin/alerts/thresholds.test.ts |
alert.api-cost-normal |
dailyCost: 10, practiceId: "prac_123" |
alertTriggered: false |
src/__tests__/unit/admin/alerts/thresholds.test.ts |
alert.citation-success-alert |
successRate: 65, totalAttempted: 30 |
alertTriggered: true, severity: "ALERT", alertType: "CITATION_SUCCESS_LOW" |
src/__tests__/unit/admin/alerts/thresholds.test.ts |
alert.citation-success-normal |
successRate: 85, totalAttempted: 30 |
alertTriggered: false |
src/__tests__/unit/admin/alerts/thresholds.test.ts |
alert.nap-mismatch-alert |
mismatches: 4, practiceId: "prac_123" |
alertTriggered: true, severity: "ALERT", alertType: "NAP_MISMATCH_HIGH" |
src/__tests__/unit/admin/alerts/thresholds.test.ts |
alert.nap-mismatch-normal |
mismatches: 2, practiceId: "prac_123" |
alertTriggered: false |
src/__tests__/unit/admin/alerts/thresholds.test.ts |
alert.gbp-token-expiry |
expiresInDays: 5, practiceId: "prac_123" |
alertTriggered: true, severity: "ALERT", alertType: "GBP_TOKEN_EXPIRY" |
src/__tests__/unit/admin/alerts/thresholds.test.ts |
alert.gbp-token-ok |
expiresInDays: 15, practiceId: "prac_123" |
alertTriggered: false |
src/__tests__/unit/admin/alerts/thresholds.test.ts |
alert.negative-review |
review: { rating: 2, status: "NEW", createdAt: <25h ago> } |
alertTriggered: true, severity: "ALERT", alertType: "NEGATIVE_REVIEW_UNREPLIED" |
src/__tests__/unit/admin/alerts/thresholds.test.ts |
alert.payment-failed |
subscription: { status: "past_due" } |
alertTriggered: true, severity: "ALERT", alertType: "PAYMENT_FAILED" |
src/__tests__/unit/admin/alerts/thresholds.test.ts |
alert.system-health-critical |
/api/health returns status: "degraded", db: false |
alertTriggered: true, severity: "CRITICAL", alertType: "SYSTEM_HEALTH_CRITICAL" |
src/__tests__/unit/admin/alerts/thresholds.test.ts |
alert.disk-usage-critical |
diskUsage: 85% |
alertTriggered: true, severity: "CRITICAL", alertType: "DISK_USAGE_HIGH" |
src/__tests__/unit/admin/alerts/thresholds.test.ts |
alert.memory-usage-critical |
memoryUsage: 92% |
alertTriggered: true, severity: "CRITICAL", alertType: "MEMORY_USAGE_HIGH" |
src/__tests__/unit/admin/alerts/thresholds.test.ts |
alert.dlq-count-warning |
dlqCount: 12, window: "24h" |
alertTriggered: true, severity: "WARNING", alertType: "DLQ_HIGH" |
src/__tests__/unit/admin/alerts/thresholds.test.ts |
alert.worker-cpu-warning |
cpu: 85%, duration: 6 (minutes) |
alertTriggered: true, severity: "WARNING", alertType: "WORKER_CPU_HIGH" |
src/__tests__/unit/admin/alerts/thresholds.test.ts |
alert.duration-investigate |
baseline: 30s, current: 70s |
alertTriggered: true, severity: "WARNING", alertType: "DURATION_HIGH" |
src/__tests__/unit/admin/alerts/thresholds.test.ts |
alert.priority-colors |
severity: "CRITICAL" |
Color #dc2626 (red) |
src/__tests__/unit/admin/alerts/thresholds.test.ts |
alert.priority-colors |
severity: "WARNING" |
Color #f59e0b (amber) |
src/__tests__/unit/admin/alerts/thresholds.test.ts |
alert.priority-colors |
severity: "LOW" |
Color #2563eb (blue) |
src/__tests__/unit/admin/alerts/thresholds.test.ts |
alert.response-sla |
severity: "CRITICAL" |
responseSLA: "15 minutes" |
src/__tests__/unit/admin/alerts/thresholds.test.ts |
alert.response-sla |
severity: "WARNING" |
responseSLA: "2 hours" |
src/__tests__/unit/admin/alerts/thresholds.test.ts |
alert.response-sla |
severity: "LOW" |
responseSLA: "24 hours" |
src/__tests__/unit/admin/alerts/thresholds.test.ts |
12.2 Integration Tests — Alert Engine#
| Test | Setup | Action | Assertion | File |
|---|---|---|---|---|
alert.engine.queue-depth |
BullMQ queue with 150 waiting jobs | Poll alert engine | Alert sent to Slack #alerts with "QUEUE_DEPTH_HIGH"; auto-scaling triggered if configured | src/__tests__/integration/admin/alerts/engine.test.ts |
alert.engine.failure-rate |
6 failed out of 100 jobs in 1 hour | Poll alert engine | Alert sent to Slack #alerts with "FAILURE_RATE_HIGH"; new job ingestion paused | src/__tests__/integration/admin/alerts/engine.test.ts |
alert.engine.api-cost |
Practice prac_123 daily AI cost = $55 |
Daily cost rollup | Admin email sent with "API_COST_HIGH"; practice flagged for review; non-essential tasks throttled | src/__tests__/integration/admin/alerts/engine.test.ts |
alert.engine.citation-low |
Citation batch with 65% success rate | Batch completion | Admin + client email sent with "CITATION_SUCCESS_LOW"; manual review of failing directories triggered | src/__tests__/integration/admin/alerts/engine.test.ts |
alert.engine.nap-mismatch |
4 NAP mismatches for prac_123 |
Monthly NAP check | Admin Slack alert with "NAP_MISMATCH_HIGH"; citation update workflow scheduled | src/__tests__/integration/admin/alerts/engine.test.ts |
alert.engine.gbp-token |
GbpAccount with tokenExpiresAt: now + 5 days |
Daily token health check | Admin email with "GBP_TOKEN_EXPIRY"; token refresh job triggered | src/__tests__/integration/admin/alerts/engine.test.ts |
alert.engine.negative-review |
Review with rating: 2, status: "NEW", createdAt: 25h ago |
Review monitor run | Client + admin email with "NEGATIVE_REVIEW_UNREPLIED"; escalation to client | src/__tests__/integration/admin/alerts/engine.test.ts |
alert.engine.payment-failed |
Stripe webhook payment_failed |
Webhook received | Client + admin email with "PAYMENT_FAILED"; retry schedule initiated; churn risk flagged | src/__tests__/integration/admin/alerts/engine.test.ts |
alert.engine.system-health |
/api/health returns status: "degraded" |
Health check poll | PagerDuty + Slack alert with "SYSTEM_HEALTH_CRITICAL"; auto-restart triggered | src/__tests__/integration/admin/alerts/engine.test.ts |
alert.engine.disk-usage |
EC2 disk at 85% | CloudWatch poll | PagerDuty + Slack alert with "DISK_USAGE_HIGH"; auto-scale storage triggered | src/__tests__/integration/admin/alerts/engine.test.ts |
alert.engine.memory-usage |
Container memory at 92% | Container metrics poll | PagerDuty + Slack alert with "MEMORY_USAGE_HIGH"; container restart triggered | src/__tests__/integration/admin/alerts/engine.test.ts |
alert.engine.dlq-high |
12 jobs in DLQ within 24h | DLQ counter poll | Slack #alerts with "DLQ_HIGH"; admin email with job list | src/__tests__/integration/admin/alerts/engine.test.ts |
alert.engine.worker-cpu |
Worker CPU at 85% for 6 minutes | Metrics poll | Slack #alerts with "WORKER_CPU_HIGH"; scale to next tier triggered | src/__tests__/integration/admin/alerts/engine.test.ts |
alert.engine.inngest-latency |
Inngest step takes 6 minutes | Step completion | Alert with "INNGEST_LATENCY_HIGH"; engineering team notified | src/__tests__/integration/admin/alerts/engine.test.ts |
alert.notification-format |
Any alert triggered | Inspect notification | Contains alert type, severity, affected client (if applicable), recommended action, timestamp | src/__tests__/integration/admin/alerts/engine.test.ts |
alert.dedup |
Same alert condition persists for 5 minutes | Alert engine polls | Only 1 alert sent; no duplicate spam within 15-minute dedup window | src/__tests__/integration/admin/alerts/engine.test.ts |
alert.resolve |
Queue depth drops from 150 to 50 | Alert engine polls | "Resolved: QUEUE_DEPTH_HIGH" notification sent; severity cleared | src/__tests__/integration/admin/alerts/engine.test.ts |
12.3 Integration Tests — Job Monitoring Metrics#
| Test | Setup | Action | Assertion | File |
|---|---|---|---|---|
metric.queue-depth |
5 queues with known waiting counts | Call admin.getQueueMetrics |
Returns accurate waiting count per queue; total matches sum |
src/__tests__/integration/admin/jobs/metrics.test.ts |
metric.active-jobs |
5 queues with known active counts | Call admin.getQueueMetrics |
Returns accurate active count per queue |
src/__tests__/integration/admin/jobs/metrics.test.ts |
metric.completed-jobs |
5 queues with known completed counts | Call admin.getQueueMetrics |
Returns accurate completed count per queue |
src/__tests__/integration/admin/jobs/metrics.test.ts |
metric.failed-jobs |
5 queues with known failed counts | Call admin.getQueueMetrics |
Returns accurate failed count per queue |
src/__tests__/integration/admin/jobs/metrics.test.ts |
metric.failure-rate-calculation |
100 completed, 5 failed in 1 hour | Calculate rolling rate | failureRate = 4.76% (failed / (completed + failed)) |
src/__tests__/integration/admin/jobs/metrics.test.ts |
metric.average-duration |
10 jobs with durations [10, 20, 30, 40, 50, 60, 70, 80, 90, 100] | Calculate average | avgDuration = 55.0s |
src/__tests__/integration/admin/jobs/metrics.test.ts |
metric.duration-baseline |
gbp-post-publish baseline = 30s; current avg = 70s |
Compare to baseline | durationAlert = true (70 > 2×30) |
src/__tests__/integration/admin/jobs/metrics.test.ts |
metric.per-client-cost |
AiUsage for prac_123: 10 records today totaling $55 |
Daily rollup | perClientDailyCost = $55.00; alert triggered |
src/__tests__/integration/admin/jobs/metrics.test.ts |
metric.dashboard-refresh |
KPI dashboard loaded | Wait 60s (mock) | admin.getKPIs re-fetches; stat cards update with new data |
src/__tests__/integration/admin/jobs/metrics.test.ts |
metric.job-monitor-refresh |
Job monitor loaded | Wait 10s (mock) | admin.listJobs re-fetches; table updates with new queue state |
src/__tests__/integration/admin/jobs/metrics.test.ts |
metric.api-cost-refresh |
API cost chart loaded | Wait 5 minutes (mock) | admin.getKPIs re-fetches; chart updates with new cost data |
src/__tests__/integration/admin/jobs/metrics.test.ts |
metric.citation-health-refresh |
Citation health loaded | Wait 1 hour (mock) | admin.getKPIs re-fetches; citation health card updates |
src/__tests__/integration/admin/jobs/metrics.test.ts |
metric.revenue-refresh |
Revenue chart loaded | Wait 1 hour (mock) | admin.getKPIs re-fetches; revenue chart updates |
src/__tests__/integration/admin/jobs/metrics.test.ts |
12.4 Success Criteria (Binary) — Job Monitoring & Alerts#
- Queue depth alert triggers when any queue has > 100 waiting jobs (WARNING)
- Job failure rate alert triggers when > 5% of jobs fail within 1-hour rolling window (WARNING)
- API cost alert triggers when per-practice daily AI spend > $50 (WARNING); flags for review
- Citation success rate alert triggers when < 70% per batch (ALERT); notifies admin + client
- NAP mismatch alert triggers when > 3 mismatches per client (ALERT); schedules update workflow
- GBP token expiry alert triggers when < 7 days until expiry (ALERT); triggers refresh job
- Negative review alert triggers when 1-2 star review unreplied > 24h (ALERT); escalates to client
- Payment failed alert triggers on Stripe/Razorpay
payment_failedwebhook (ALERT); initiates retry - System health alert triggers when
/api/healthreturns non-200 (CRITICAL); PagerDuty + Slack - Disk usage alert triggers when > 80% (CRITICAL); auto-scales storage
- Memory usage alert triggers when > 90% (CRITICAL); restarts container
- DLQ count alert triggers when > 10 jobs in DLQ within 24h (WARNING); admin Slack + email
- Worker CPU alert triggers when > 80% for > 5 minutes (WARNING); scales to next tier
- Inngest latency alert triggers when any step takes > 5 minutes
- Duration alert triggers when average job duration exceeds 2× baseline per job type
- All alerts include: type, severity, affected entity, recommended action, timestamp
- Alert deduplication prevents duplicate notifications within 15-minute window
- Alert resolution notification sent when condition returns to normal
- Critical alerts have 15-minute response SLA; Warning = 2 hours; Low = 24 hours
- Job monitor dashboard refreshes every 10 seconds
- KPI stat cards refresh every 60 seconds
- API cost chart refreshes every 5 minutes
- Citation health refreshes every 1 hour
- Revenue chart refreshes every 1 hour
- Queue metrics accurately reflect BullMQ Redis state
- Failure rate calculation uses rolling 1-hour window
- Average duration calculation correctly computes mean per job type
- Per-client daily cost aggregation matches
AiUsagetable rollup
12.5 Agent Context (Pre-conditions) — Job Monitoring & Alerts#
- Required DB state: BullMQ queues with known job counts (waiting/active/completed/failed),
AiUsage(daily records per practice),Citation(batch data),GbpAccount(with variedtokenExpiresAt),Review(with negative unreplied),Subscription(with past due records),JobLog(with timestamps for rolling window),AlertLog(empty for dedup tests) - Required env vars:
ALERT_SLACK_WEBHOOK_URL(mocked);ALERT_PAGERDUTY_KEY(mocked);ADMIN_EMAIL=admin@rankflow.in;QUEUE_DEPTH_THRESHOLD=100;FAILURE_RATE_THRESHOLD=0.05;API_COST_DAILY_THRESHOLD=50;CITATION_SUCCESS_THRESHOLD=0.70;NAP_MISMATCH_THRESHOLD=3;TOKEN_EXPIRY_DAYS_THRESHOLD=7;DLQ_THRESHOLD=10;CPU_THRESHOLD=80;CPU_DURATION_MINUTES=5;MEMORY_THRESHOLD=90;DISK_THRESHOLD=80;INNGEST_LATENCY_THRESHOLD_MINUTES=5;DURATION_BASELINE_MULTIPLIER=2 - Required mocks: Slack webhook mocked; PagerDuty API mocked; email provider mocked;
next/navigationmocked - Mock Redis state: BullMQ queues with known counts for metric accuracy tests
- Alert engine state: Alert rules engine initialized with all threshold configs
12.6 Verification Commands — Job Monitoring & Alerts#
# Unit: alert threshold logic
pnpm test:unit -- src/__tests__/unit/admin/alerts/thresholds.test.ts
# Integration: alert engine
pnpm test:integration -- src/__tests__/integration/admin/alerts/engine.test.ts
# Integration: job metrics
pnpm test:integration -- src/__tests__/integration/admin/jobs/metrics.test.ts
# Full alert + monitoring suite
pnpm test:unit -- src/__tests__/unit/admin/alerts/
pnpm test:integration -- src/__tests__/integration/admin/alerts/
pnpm test:integration -- src/__tests__/integration/admin/jobs/metrics.test.ts
13. Cross-Cutting Integration Tests#
13.1 End-to-End Admin Flow Tests#
| Test | Setup | Action | Assertion | File |
|---|---|---|---|---|
e2e.admin-login-to-kpi |
Clean browser; admin credentials | Login → navigate to /admin |
KPI dashboard loads with all 6 sections; data visible within 3 seconds | e2e/admin/kpi.spec.ts |
e2e.client-crud |
Admin logged in | Navigate to /admin/clients → create client → edit → suspend → view detail → delete |
Each action succeeds; audit log reflects all 4 actions; no 500 errors | e2e/admin/client-crud.spec.ts |
e2e.job-retry-from-dlq |
Failed job in DLQ | Navigate to /admin/jobs → filter FAILED → click Retry → verify job re-queued |
Job status changes from FAILED to PENDING; DLQ count decreases by 1 | e2e/admin/job-retry.spec.ts |
e2e.content-approval |
Content in PENDING_REVIEW | Navigate to /admin/content → select piece → click Approve → verify status |
Content status changes to APPROVED; client dashboard shows published | e2e/admin/content-approval.spec.ts |
e2e.impersonation-flow |
Admin logged in; client with prac_123 |
Navigate to /admin/clients/prac_123 → click Impersonate → confirm → view client dashboard → exit |
Client dashboard renders correctly; impersonation banner visible; exit returns to admin | e2e/admin/impersonation.spec.ts |
e2e.billing-export |
Admin logged in | Navigate to /admin/billing → click Export Revenue CSV → verify download |
CSV file downloaded with correct headers and monthly data | e2e/admin/billing-export.spec.ts |
e2e.report-generation |
Admin logged in | Navigate to /admin/reports → select Citation Success Rate → export CSV |
CSV downloaded with weekly citation data | e2e/admin/report-export.spec.ts |
e2e.alert-notification |
Queue depth > 100 | Trigger alert condition | Slack webhook receives payload with correct alert type and severity | e2e/admin/alert-notification.spec.ts |
e2e.domain-revalidate |
Admin logged in; practice with directorySlug | Navigate to /admin/directory → click Force Revalidate → verify success |
Toast shows "Domain revalidated"; ISR cache purged | e2e/admin/directory-profile-revalidate.spec.ts |
e2e.social-disconnect |
Admin logged in; active social account | Navigate to /admin/social-connections → click Disconnect → confirm |
Account status changes to DISCONNECTED; Composio API called | e2e/admin/social-disconnect.spec.ts |
13.2 API Router Protection Tests#
| Test | Setup | Action | Assertion | File |
|---|---|---|---|---|
api.all-admin-procedures |
List of all 15+ admin tRPC procedures | Call each with client role | All return TRPCError with code FORBIDDEN |
src/__tests__/integration/admin/api.protection.test.ts |
api.admin-procedures-list |
admin.getKPIs, admin.listClients, admin.getClient, admin.updateClient, admin.listDomains, admin.listWebsites, admin.listSocialConnections, admin.listJobs, admin.retryJob, admin.listContent, admin.approveContent, admin.updatePrompt, admin.getRevenue, admin.getFailedCharges, admin.impersonate, admin.endImpersonate |
Call each with admin role | All return 200 with valid data shapes | src/__tests__/integration/admin/api.protection.test.ts |
api.mutation-protection |
All admin mutations | Call each without session | All return TRPCError with code UNAUTHORIZED |
src/__tests__/integration/admin/api.protection.test.ts |
13.3 Success Criteria (Binary) — Cross-Cutting#
- Admin login →
/adminKPI dashboard loads within 3 seconds with all sections visible - Full client CRUD flow (create → edit → suspend → view → delete) completes without errors
- Job retry from DLQ changes status from FAILED to PENDING; DLQ count decreases
- Content approval from admin dashboard updates status to APPROVED and reflects on client dashboard
- Impersonation flow: admin → client dashboard → exit → admin detail page works end-to-end
- Billing CSV export downloads file with correct revenue data
- Report CSV export downloads file with correct report data
- Alert notification sends Slack payload with correct type and severity when threshold breached
- Domain revalidate triggers ISR cache purge and shows success toast
- Social disconnect updates status and calls Composio API
- All 15+ admin tRPC procedures return 403 FORBIDDEN for non-admin roles
- All admin tRPC procedures return 401 UNAUTHORIZED for unauthenticated requests
- All admin tRPC procedures return 200 with valid data for authenticated admin
- No admin API endpoint leaks data across practice boundaries (except
listaggregated views)
13.4 Agent Context (Pre-conditions) — Cross-Cutting#
- Required DB state: Full production-like dataset: 100
ClientProfile, 100Location, 100GbpAccount, 400SocialAccount, 3000Citation, 700DirectoryProfileSection, 2000JobLog, 1200Invoice, 2000AuditLog, 400ContentPiece, 50PromptTemplate, 80Subscription, 500AiUsage, 1000EmailLog, 20SupportTicket - Required env vars: All production env vars set to test values;
NODE_ENV=test;E2E_BASE_URL=http://localhost:3000 - Required external mocks: All third-party APIs mocked (Stripe, Razorpay, Google, Composio, Resend, Slack, PagerDuty, DataForSEO, SerpAPI, Hyperbrowser, Firecrawl, Claude, OpenAI)
- Mock Redis state: Full BullMQ queue state with realistic job counts; DLQ populated with 10-20 failed jobs
- Browser state: Playwright or Cypress with admin session cookie pre-set
- Network mocks:
mswserver running for all external API calls
13.5 Verification Commands — Cross-Cutting#
# E2E: admin flows
pnpm test:e2e -- e2e/admin/kpi.spec.ts
pnpm test:e2e -- e2e/admin/client-crud.spec.ts
pnpm test:e2e -- e2e/admin/job-retry.spec.ts
pnpm test:e2e -- e2e/admin/content-approval.spec.ts
pnpm test:e2e -- e2e/admin/impersonation.spec.ts
pnpm test:e2e -- e2e/admin/billing-export.spec.ts
pnpm test:e2e -- e2e/admin/report-export.spec.ts
pnpm test:e2e -- e2e/admin/alert-notification.spec.ts
pnpm test:e2e -- e2e/admin/directory-profile-revalidate.spec.ts
pnpm test:e2e -- e2e/admin/social-disconnect.spec.ts
# Integration: API protection
pnpm test:integration -- src/__tests__/integration/admin/api.protection.test.ts
# Full E2E suite
pnpm test:e2e -- e2e/admin/
14. Execution Plan & Agent Assignments#
Phase 1: Component Infrastructure (Day 1)#
| Agent | Task | Test Files | Verification |
|---|---|---|---|
Admin_UI_Specialist |
Set up admin component test harness (RTL, mock tRPC, mock session) | src/__tests__/unit/admin/setup.ts |
pnpm test:unit -- admin passes with 0 tests |
Admin_UI_Specialist |
Unit tests for all reusable admin components | src/__tests__/unit/admin/Sidebar.test.tsx, Header.test.tsx, DataTable.test.tsx, StatCard.test.tsx, StatusBadge.test.tsx, ConfirmDialog.test.tsx, FilterBar.test.tsx, PracticeSelect.test.tsx, JsonViewer.test.tsx |
pnpm test:unit -- admin |
Access_Control_Specialist |
Integration tests for layout, access control, responsive behavior | src/__tests__/integration/admin/access.test.ts, responsive.test.tsx |
pnpm test:integration -- admin/access |
Phase 2: Page Unit Tests (Days 2-3)#
| Agent | Task | Test Files | Verification |
|---|---|---|---|
KPI_Dashboard_Specialist |
Unit tests for KPI page + all chart components | src/__tests__/unit/admin/kpi/ |
pnpm test:unit -- admin/kpi |
Client_Management_Specialist |
Unit tests for client list and client detail pages | src/__tests__/unit/admin/clients/ |
pnpm test:unit -- admin/clients |
Infrastructure_Specialist |
Unit tests for domain and website pages | src/__tests__/unit/admin/directory/, websites/ |
pnpm test:unit -- admin/domains, admin/websites |
Social_Content_Specialist |
Unit tests for social connections and content review pages | src/__tests__/unit/admin/social/, content/ |
pnpm test:unit -- admin/social, admin/content |
Job_Monitor_Specialist |
Unit tests for job monitor page + stat cards | src/__tests__/unit/admin/jobs/ |
pnpm test:unit -- admin/jobs |
Billing_Reports_Specialist |
Unit tests for billing and reports pages | src/__tests__/unit/admin/billing/, reports/ |
pnpm test:unit -- admin/billing, admin/reports |
Phase 3: API Integration Tests (Days 4-5)#
| Agent | Task | Test Files | Verification |
|---|---|---|---|
Admin_API_Specialist |
Integration tests for all admin tRPC procedures | src/__tests__/integration/admin/kpi/, clients/, infrastructure/, social/, jobs/, content/, billing/, reports/ |
pnpm test:integration -- admin/ |
KPI_Calculation_Specialist |
KPI calculation logic + DB aggregation accuracy | src/__tests__/unit/admin/kpi/calculations.test.ts, src/__tests__/integration/admin/kpi/db-aggregation.test.ts |
pnpm test:unit -- admin/kpi/calculations, pnpm test:integration -- admin/kpi/db-aggregation |
Impersonation_Specialist |
Impersonation feature unit + integration tests | src/__tests__/unit/admin/impersonation/, src/__tests__/integration/admin/impersonation/ |
pnpm test:unit -- admin/impersonation, pnpm test:integration -- admin/impersonation |
Alert_Engine_Specialist |
Alert threshold logic + alert engine integration | src/__tests__/unit/admin/alerts/thresholds.test.ts, src/__tests__/integration/admin/alerts/engine.test.ts |
pnpm test:unit -- admin/alerts, pnpm test:integration -- admin/alerts |
Job_Metrics_Specialist |
Job monitoring metrics + refresh intervals | src/__tests__/integration/admin/jobs/metrics.test.ts, api.test.ts |
pnpm test:integration -- admin/jobs |
Phase 4: E2E & Cross-Cutting (Day 6)#
| Agent | Task | Test Files | Verification |
|---|---|---|---|
E2E_Admin_Specialist |
End-to-end admin flows | e2e/admin/*.spec.ts |
pnpm test:e2e -- e2e/admin/ |
API_Protection_Specialist |
Router protection + role enforcement tests | src/__tests__/integration/admin/api.protection.test.ts, api.access.test.ts |
pnpm test:integration -- admin/api.protection |
Phase 5: Verification & Compliance (Day 7)#
| Agent | Task | Verification |
|---|---|---|
Test_Suite_Auditor |
Run full admin suite; verify all checkboxes pass | pnpm test:unit -- admin/, pnpm test:integration -- admin/, pnpm test:e2e -- e2e/admin/ |
Coverage_Auditor |
Verify coverage thresholds for admin code: lines > 80%, functions > 85%, branches > 75% | pnpm test:coverage -- src/app/(admin)/, src/components/admin/, src/server/api/routers/admin.ts |
Smoke_Test_Agent |
Run admin dashboard smoke test against local dev server | `curl -s http://localhost:3000/api/health/jobs |
Appendix A: Test Data Constants#
| Constant | Value | Usage |
|---|---|---|
MOCK_ADMIN_ID |
usr_admin_001 |
Deterministic admin user ID |
MOCK_CLIENT_ID |
usr_client_001 |
Deterministic client user ID |
MOCK_EDITOR_ID |
usr_editor_001 |
Deterministic editor user ID |
MOCK_PRACTICE_ID |
prac_test_001 |
Deterministic practice ID |
MOCK_LOCATION_ID |
loc_test_001 |
Deterministic location ID |
MOCK_DIRECTORY_SLUG |
dr-smith-dental.rankflow.in |
Deterministic directorySlug |
MOCK_CUSTOM_DOMAIN |
drsmithdental.com |
Deterministic directory profile URL |
MOCK_SOCIAL_ACCOUNT_ID |
soc_test_001 |
Deterministic social account ID |
MOCK_JOB_ID |
job_test_001 |
Deterministic job ID |
MOCK_CONTENT_ID |
content_test_001 |
Deterministic content piece ID |
MOCK_INVOICE_ID |
inv_test_001 |
Deterministic invoice ID |
CURRENCY_INR |
INR |
Default currency code |
IMPERSONATION_TIMEOUT |
120 |
Impersonation timeout in minutes |
QUEUE_DEPTH_THRESHOLD |
100 |
Queue depth warning threshold |
FAILURE_RATE_THRESHOLD |
0.05 |
Failure rate warning threshold (5%) |
API_COST_DAILY_THRESHOLD |
50 |
Per-practice daily AI cost threshold (USD) |
CITATION_SUCCESS_THRESHOLD |
0.70 |
Citation success rate alert threshold (70%) |
NAP_MISMATCH_THRESHOLD |
3 |
NAP mismatch alert threshold per client |
TOKEN_EXPIRY_DAYS_THRESHOLD |
7 |
GBP token expiry alert threshold (days) |
DLQ_THRESHOLD |
10 |
Dead letter queue warning threshold (24h) |
CPU_THRESHOLD |
80 |
Worker CPU warning threshold (%) |
CPU_DURATION_MINUTES |
5 |
CPU alert duration threshold |
MEMORY_THRESHOLD |
90 |
Memory usage critical threshold (%) |
DISK_THRESHOLD |
80 |
Disk usage critical threshold (%) |
INNGEST_LATENCY_THRESHOLD |
5 |
Inngest latency alert threshold (minutes) |
DURATION_BASELINE_MULTIPLIER |
2 |
Duration alert baseline multiplier |
KPI_REFRESH_INTERVAL |
60000 |
KPI dashboard refresh interval (ms) |
JOB_MONITOR_REFRESH_INTERVAL |
10000 |
Job monitor refresh interval (ms) |
API_COST_REFRESH_INTERVAL |
300000 |
API cost chart refresh interval (ms) |
CITATION_REFRESH_INTERVAL |
3600000 |
Citation health refresh interval (ms) |
REVENUE_REFRESH_INTERVAL |
3600000 |
Revenue chart refresh interval (ms) |
Appendix B: Admin tRPC Procedure Registry#
| Procedure | Type | Auth Required | Role Required | Returns |
|---|---|---|---|---|
admin.getKPIs |
Query | ✅ | ADMIN | AdminKPIs |
admin.getSystemHealth |
Query | ✅ | ADMIN | { status, checks } |
admin.listClients |
Query | ✅ | ADMIN | ClientProfile[] (paginated) |
admin.getClient |
Query | ✅ | ADMIN | ClientProfile + relations |
admin.updateClient |
Mutation | ✅ | ADMIN | ClientProfile |
admin.listDomains |
Query | ✅ | ADMIN | Domain[] |
admin.checkDNS |
Query | ✅ | ADMIN | DNSStatus |
admin.revalidateDomain |
Mutation | ✅ | ADMIN | { success } |
admin.listWebsites |
Query | ✅ | ADMIN | BlogSite[] |
admin.checkWebsiteHealth |
Query | ✅ | ADMIN | HealthStatus |
admin.listSocialConnections |
Query | ✅ | ADMIN | SocialAccount[] |
admin.listJobs |
Query | ✅ | ADMIN | JobLog[] (paginated) |
admin.retryJob |
Mutation | ✅ | ADMIN | { success, newJobId } |
admin.getJobStats |
Query | ✅ | ADMIN | JobStats |
admin.getJobLogs |
Query | ✅ | ADMIN | JobLogEntry[] |
admin.listContent |
Query | ✅ | ADMIN | ContentPiece[] |
admin.approveContent |
Mutation | ✅ | ADMIN | ContentPiece |
admin.rejectContent |
Mutation | ✅ | ADMIN | ContentPiece |
admin.updatePrompt |
Mutation | ✅ | ADMIN | PromptTemplate |
admin.getRevenue |
Query | ✅ | ADMIN | RevenueData |
admin.getFailedCharges |
Query | ✅ | ADMIN | FailedCharge[] |
admin.getChurnReport |
Query | ✅ | ADMIN | ChurnReport |
admin.getCitationSuccessReport |
Query | ✅ | ADMIN | CitationSuccessReport |
admin.getApiCostReport |
Query | ✅ | ADMIN | ApiCostReport |
admin.getContentVolumeReport |
Query | ✅ | ADMIN | ContentVolumeReport |
admin.getSupportTicketReport |
Query | ✅ | ADMIN | SupportTicketReport |
admin.getGrossMarginReport |
Query | ✅ | ADMIN | GrossMarginReport |
admin.impersonate |
Mutation | ✅ | ADMIN | Session |
admin.endImpersonate |
Mutation | ✅ | ADMIN | Session |
admin.getQueueMetrics |
Query | ✅ | ADMIN | QueueMetrics |
End of Admin Dashboard & Observability Test Specification — RankFlow AI v1.0.0