User-perspective end-to-end test scenarios for OffsideStudio — Agent Marketplace. Each case is written as steps a real user would perform, not as a unit test. IDs are stable (
TC-N); cross-references to PRD.md functional requirements are noted asFR-N.Hero-surface TCs for demo coverage: TC-29..TC-32 (Agent Design Studio, FR-12) and TC-92..TC-94 (Agents Marketplace, FR-26). The rest are foundational.
Purpose: acceptance gate for milestones, manual QA scripts, and the source list for automated E2E coverage (Playwright on web; XCUITest on iOS). Owner: Offside Labs. Last revised: 2026-05.
Each test case is structured as:
TC-NTitle Persona:P-x· Covers:FR-N,NFR-N· Surface: web / iOS / API Pre-conditions: what must be true before starting. Steps: numbered user actions. Expected: observable outcome. Edge: notable variations / failure modes.
Severity tags on edges: [blocking] = test fails; [degraded] = soft-failure / warning.
Persona: P-1 · Covers: FR-1, FR-2 · Surface: web Pre-conditions: Fresh email, no existing account. Steps:
- Navigate to
app.offside.ai/signup. - Enter email + password (≥10 chars, mixed case + digit).
- Submit form.
- Receive verification email; click link.
- Land on workspace-creation form; enter workspace name "Acme Sales".
- Submit; auto-navigate to workspace home.
Expected: Authenticated session; workspace created with the user as
owner; brand-tan welcome banner with the workspace name; activity log empty. Edge:[blocking]Reused email → 409 with link to login.[degraded]Verification link expired → resend flow.
Persona: P-2 · Covers: FR-2 · Surface: web Pre-conditions: Existing verified account. Steps:
- Navigate to
app.offside.ai/login. - Enter email + password.
- Submit.
Expected: Redirect to last-visited workspace + last-visited route. JWT stored in HTTP-only cookie + access in memory.
Edge:
[blocking]Wrong password → 401, no email enumeration; rate-limited after 5 attempts.
Persona: P-2 · Covers: FR-2 · Surface: web Pre-conditions: Existing account. Steps:
- Click "Forgot password" on login.
- Enter email; submit.
- Receive reset email; click link.
- Set new password; submit.
Expected: New password works; old refresh tokens revoked.
Edge:
[blocking]Reset token expired (>1h) → "Request a new link" message.
Persona: P-2 · Covers: FR-2 · Surface: web + iOS Pre-conditions: Logged in. Steps:
- Click avatar → "Log out".
Expected: Refresh token blacklisted; access token cleared; redirect to
/login. iOS Keychain entry cleared.
Persona: P-1 · Covers: FR-1, FR-24 · Surface: web Pre-conditions: Owner of a workspace; teammate has email but no account. Steps:
- Settings → Team → "Invite teammate".
- Enter teammate email; choose role "Rep".
- Submit.
- Teammate receives Resend magic-link email.
- Teammate clicks link → signup flow auto-pre-fills workspace.
Expected: Teammate lands in the workspace as
rep. Activity log shows "Teammate joined". Edge:[degraded]Teammate already exists in another workspace → invite still works; teammate can switch.
Persona: P-3 · Covers: FR-1 · Surface: web Steps:
- Settings → Team.
- Click rep's row; change role dropdown to "Manager".
- Confirm. Expected: Role updates immediately; rep sees expanded UI on next page load.
Persona: P-2 · Covers: FR-1, NFR-2 · Surface: web Pre-conditions: User has membership in two workspaces. Steps:
- Press
Cmd+K. - Type "switch".
- Select "Switch workspace → Acme Sales". Expected: Workspace context switches; URL updates; data view filters to the new workspace.
Persona: P-2 · Covers: FR-1, NFR-3 · Surface: API Steps:
- Authenticate as user-A active in workspace-A.
- Hit
GET /api/contacts/{contact_id_from_workspace_B}. Expected: HTTP 403 (or 404 to avoid enumeration). No payload leakage. Edge:[blocking]Any leak is a critical security defect.
Persona: P-1 · Covers: FR-1, NFR-8 · Surface: web Steps:
- Settings → Workspace → "Archive workspace" → confirm with workspace name. Expected: Workspace marked deleted; users see archive notice; data retained 30 days.
Persona: P-2 · Covers: FR-3 · Surface: web Steps:
- Press
n(new) on Contacts list, or click "+ New Contact". - Fill first/last name, primary email, phone, title.
- Optionally link to a company.
- Save.
Expected: Contact appears in list ≤100ms after save; activity row "created" written.
Edge:
[degraded]Duplicate email → toast "Contact already exists" + link to existing.
Persona: P-2 · Covers: FR-3 · Surface: web Steps:
- Open contact detail.
- Click title; edit; press Enter. Expected: Save indicator → success in ≤300ms; activity row "title changed" with diff.
Persona: P-2 · Covers: FR-3 · Surface: web Steps:
- Open contact detail → kebab → Archive. Expected: Contact disappears from default list; visible under "Archived" filter; recoverable for 90 days.
Persona: P-2 · Covers: FR-4 · Surface: web Steps:
- Open company detail.
- Click "Contacts (12)" tab; pick one.
- From contact, click "Deals (3)" → pick one. Expected: Smooth navigation; back button returns to company.
Persona: P-3 · Covers: FR-4, FR-8 · Surface: web Steps:
- Companies list → multi-select 50 rows.
- Bulk action → set Industry = "B2B SaaS". Expected: Operation runs as a workflow with progress; activity rows for each company.
Persona: P-1 · Covers: FR-5 · Surface: web Steps:
- Settings → Pipelines → "+ New".
- Name "Outbound Q3"; add stages "Prospect → Discovery → Demo → Negotiation → Closed Won → Closed Lost".
- Save. Expected: Pipeline appears in deals view; stages are drag-reorderable.
Persona: P-2 · Covers: FR-5 · Surface: web Steps:
- From a contact's "Deals" tab, click "+ New Deal".
- Title "Acme Q3 expansion"; value "$24,000"; pipeline "Outbound Q3"; stage "Discovery".
- Save. Expected: Deal appears in kanban under Discovery; activity logged.
Persona: P-2 · Covers: FR-5, FR-11 · Surface: web Steps:
- Pipeline kanban view.
- Drag deal "Acme Q3 expansion" from "Discovery" → "Demo". Expected: Deal moves; activity "stage changed Discovery → Demo"; if a "stage = Demo" automation exists, run starts (TC-29).
Persona: P-2 · Covers: FR-5 · Surface: web Steps:
- Move deal to "Closed Won". Expected: Deal updates; "Won" badge in tan-text; activity row.
Persona: P-2 · Covers: FR-6 · Surface: web Steps:
- Contact detail → "Tasks" → "+ New".
- Title "Send pricing"; due tomorrow 5pm; owner self. Expected: Task appears in "Today" smart view tomorrow; in contact's tasks list now.
Persona: P-2 · Covers: FR-6, NFR-2 · Surface: web Pre-conditions: Task whose due is in the past. Expected: Row uses tan-text accent + clear text label "Overdue" (color is not the sole signal).
Persona: P-2 · Covers: FR-7 · Surface: web Steps:
- Contact detail → Notes tab → "+ New".
- Body "Key: discount-blocked by procurement. Follow up Friday.".
- Save.
Expected: Note rendered with bold; activity row.
Edge:
[degraded]Edit beyond 24h triggers audit-trail row.
Persona: P-3 · Covers: FR-8 · Surface: web Steps:
- Settings → Custom fields → Contacts → "+ New".
- Type
number; keylead_score; label "Lead Score". - Save. Expected: Field appears on contact detail; column toggleable in list view; usable in filter DSL.
Persona: P-2 · Covers: FR-8, FR-19 · Surface: web Steps:
- Contacts list → filter →
lead_score > 70. Expected: Filter applies ≤200ms; result list updates with count.
Persona: P-3 · Covers: FR-8 · Surface: web
Steps: Add field type select; options "A, B, C".
Expected: Drop-down on contact detail; required-flag respected on create.
Persona: P-2 · Covers: FR-9 · Surface: web Steps:
- Settings → Integrations → Gmail → Connect.
- Complete Google OAuth (read + send + push scopes).
- Wait for initial sync banner. Expected: Banner reports "Synced last 90 days, 1,243 messages, 312 threads, 87 contacts touched". Each touched contact has email thread visible.
Persona: P-2 · Covers: FR-9, FR-13 · Surface: web Pre-conditions: Gmail connected. Steps:
- Contact detail → "Send email".
- AI suggests a draft based on last 3 messages + deal stage.
- Edit draft; click Send. Expected: Email sends as the connected user; thread updates in Gmail; activity row "email sent".
Persona: P-2 · Covers: FR-9, FR-11 · Surface: email + web
Pre-conditions: Gmail Pub/Sub push working.
Steps: External party replies to the connected user.
Expected: Within 30s, contact's activity feed shows "email received". If a workflow has an email_received trigger, run starts.
Persona: P-2 · Covers: FR-10 · Surface: web Steps: Connect GCal; open a deal that has an upcoming meeting. Expected: Meeting appears under "Upcoming" on the deal.
Persona: P-3 · Covers: FR-11, FR-12 · Surface: web Steps:
- Automations → "+ New".
- Trigger:
Deal stage changed to Negotiation. - Drag node:
Wait 1 day. - Drag node:
AI: Draft follow-up email(model: Sonnet; prompt: comms.draft_reply.v1). - Drag node:
Human-in-the-loop approval→ on approve →Send email. - Connect edges; Save as draft.
- Click "Publish". Expected: Workflow appears in the active list (version 1). Schema validation passes.
Persona: P-3 · Covers: FR-12 · Surface: web Steps:
- New automation → "Describe in English".
- Type "When a deal goes quiet 5 days, draft a re-engage email and ask me to approve".
- Click Generate. Expected: Canvas hydrates with: trigger (delay since last activity = 5d) → AI draft → HITL approval → send. User can edit before publishing.
Persona: P-3 · Covers: FR-12 · Surface: web Steps: Edit a published workflow; save as draft; later, restore version 1 from version history. Expected: Active version flips; runs already in flight finish on their original version (per FR-11).
Persona: P-3 · Covers: FR-12 · Surface: web Pre-conditions: Workflow with disconnected node. Steps: Click Publish. Expected: Inline error "Disconnected node 'AI Draft' has no inbound edge"; node highlighted; publish blocked.
Persona: P-1 · Covers: FR-11, NFR-7 · Surface: admin / web Steps:
- Trigger TC-29 by moving a deal to Negotiation.
- Open run inspector; see status
awaiting_delaywithresume_at~24h ahead. - Wait (or in dev: clock-skew the resume_at to now-1s).
Expected: Run advances to AI Draft step; result captured; status flips to
awaiting_approval.
Persona: engineering / P-1 · Covers: FR-11, NFR-7 · Surface: admin
Pre-conditions: Run in running after delay; AI step in flight.
Steps:
- Force-kill Celery worker mid-step.
- Worker restarts. Expected: Idempotency-key short-circuit prevents duplicate AI call; step output recovered; run continues.
Persona: P-1 · Covers: FR-17, FR-24 · Surface: web
Pre-conditions: Run in awaiting_approval from TC-33.
Steps:
- Open Agent inbox.
- See approval request with summary + full draft.
- Click "Approve". Expected: Run resumes; email sends; activity row records decider + decided_at.
Persona: P-1 · Covers: FR-17 · Surface: email + web
Pre-conditions: Run in awaiting_approval.
Steps: Click magic link in email; land on signed-token approval page; click Approve.
Expected: Run resumes. Token is single-use; second click shows "Already decided".
Edge: [blocking] Token expired (>7d) → "Decision window closed" message; run remains paused.
Persona: P-1 · Covers: FR-17, FR-23 · Surface: iOS push + iOS app
Pre-conditions: iOS app installed + push enabled; run in awaiting_approval.
Steps: Receive push; long-press → Approve action.
Expected: Approval registered without unlocking the app; activity row records "approved via iOS push".
Persona: P-2 · Covers: FR-17 · Surface: web
Pre-conditions: Workspace policy add_internal_note = autonomous.
Steps:
- Agent autonomously adds a note.
- Within 60s, click "Undo" toast. Expected: Note removed; activity row records autonomous-action + undo.
Persona: P-3 · Covers: FR-11, NFR-7 · Surface: admin Pre-conditions: HTTP step pointing at an endpoint that flaps. Steps: Trigger workflow; first request fails with 503; wait. Expected: Step retries with exponential backoff (3 attempts); final attempt logged either success or hard-fail with detail.
Persona: P-2 · Covers: FR-13, NFR-1 · Surface: web Steps: Contact view → "AI draft reply"; first token visible ≤1.5s P95. Expected: Streaming appears chunk-by-chunk; final draft editable.
Persona: P-2 · Covers: FR-13 · Surface: web
Pre-conditions: Gmail thread with ≥6 messages.
Steps: Open thread.
Expected: 3-bullet summary at top within 2s; cached for the thread's last_message_at.
Persona: P-2 · Covers: FR-13, FR-10 · Surface: web Steps: On a deal, paste a meeting transcript → "Summarize". Expected: 5-bullet summary written to the deal's activity log; cost displayed in admin telemetry.
Persona: P-1 · Covers: FR-14, FR-19, NFR-1 · Surface: web Steps:
Cmd+K.- Type "deals over $10k that went quiet last week".
- Press Enter. Expected: Filtered list ≤2s P95; "Save as view" affordance; AI summary above ("12 deals, 60% in Negotiation, average value $18k").
Persona: P-1 · Covers: FR-14, FR-19 · Surface: web Steps: Type "deals that smelled risky". Expected: Hybrid keyword + vector results; AI summary explains why each is flagged.
Persona: P-2 · Covers: FR-14 · Surface: web Steps: Save TC-43's view as "Quiet >$10k"; visible to teammates with appropriate role.
Persona: P-2 · Covers: FR-15 · Surface: web
Steps: Add contact john@stripe.com.
Expected: Within 60s, contact's company shows industry "Payments", size "1000+"; lead score populated as a number.
Persona: P-3 · Covers: FR-15, NFR-6 · Surface: admin Pre-conditions: Daily budget set very low. Steps: Add many contacts in a row. Expected: After budget hit, enrichment workers pause; admin gets in-app + email notice; queued work resumes next budget window.
Persona: P-2 · Covers: FR-15 · Surface: web Steps: Contact detail → "Re-score" button. Expected: Score recomputed within 5s; previous score recorded in activity diff.
Persona: P-2 · Covers: FR-16 · Surface: web Pre-conditions: Deal stage stable >5 days. Steps: Open deal. Expected: Suggestion strip shows "This deal hasn't moved in 5 days — draft a re-engage email?". Edge: Dismissed suggestion does not reappear for 7 days.
Persona: P-3 · Covers: FR-12, FR-16 · Surface: web Steps: Suggestion → "Make this a workflow". Expected: Canvas opens with a draft graph implementing the suggestion's intent.
Persona: P-3 · Covers: FR-17 · Surface: web
Steps: Settings → Agent → set send_email_to_contact = approve, post_slack = autonomous, update_note = autonomous.
Expected: Policy persists; visible in agent inbox legend.
Persona: P-2 · Covers: FR-17 · Surface: web Steps: Deal detail → "Agent assist" toggle. Expected: Agent runs a planning loop; proposes ≤N actions; per-action mode comes from the policy table.
Persona: P-1 · Covers: FR-17 · Surface: web + iOS
Pre-conditions: TC-52 active; agent proposes (1) Slack note (autonomous), (2) email to contact (approve), (3) calendar invite (approve).
Expected: Slack action runs immediately with undo affordance; emails and invites pause as awaiting_approval; user can approve from web inbox or iOS push.
Persona: P-3 · Covers: FR-17, FR-25 · Surface: admin
Steps: Audit log filter actor_kind = agent.
Expected: Each row shows prompt name, model, cost, output, decision, decider.
Persona: P-2 · Covers: FR-19, NFR-1 · Surface: web
Steps: Press Cmd+K; type "ac" (3 chars).
Expected: Open ≤80ms; first results ≤200ms; Acme Corp + Acme Ltd ranked above Carmackdyne.
Persona: P-2 · Covers: FR-19 · Surface: web Steps: Open cmd-K with empty input. Expected: Last 5 queries listed.
Persona: P-2 · Covers: FR-19 · Surface: web Steps: Search "procurement blocking". Expected: Hits in notes + emails; filters by entity type chips.
Persona: P-3 · Covers: FR-20, FR-11 · Surface: web Steps:
- Settings → Import → CSV.
- Drop file; AI suggests column → field mapping; user reviews.
- Click Import. Expected: Import runs as a workflow; progress visible; final report shows successes + skipped (with reasons).
Persona: P-3 · Covers: FR-20 · Surface: web Steps: Paste HubSpot export URL; pick objects; map; import. Expected: Cross-references preserved (deal → contact → company); custom HubSpot fields land in custom fields where types align.
Persona: P-3 · Covers: FR-20, FR-5 · Surface: web Expected: Pipeline stages from Pipedrive create matching pipeline; deals land in correct stages.
Persona: P-3 · Covers: FR-20, NFR-7 · Surface: admin Steps: Kill worker mid-import. Expected: On restart, idempotency keys prevent re-creating already-imported rows; remaining rows continue.
Persona: P-3 · Covers: FR-21 · Surface: web Steps: Settings → Integrations → Slack → OAuth. Expected: Channels list visible; can be picked in workflow Slack nodes.
Persona: P-3 · Covers: FR-11, FR-21 · Surface: web → Slack Pre-conditions: TC-62 done; workflow with Slack node. Steps: Trigger workflow. Expected: Message appears in target channel ≤5s; activity row logs.
Persona: P-3 · Covers: FR-22, FR-11 · Surface: HTTP → web
Steps: Copy webhook URL from a workflow's webhook trigger; POST a JSON payload from curl with HMAC header.
Expected: Workflow run starts ≤2s; payload visible in run inspector.
Persona: P-3 · Covers: FR-22 · Surface: web → HTTP Steps: Workflow with HTTP node calling external API with bearer token preset. Expected: Request body + headers correct; response captured in step output.
Persona: P-2 · Covers: FR-24 · Surface: web Steps: Settings → Notifications → uncheck email for task_due_soon. Expected: In-app + push still on; email skipped.
Persona: P-2 · Covers: FR-24 · Surface: iOS Pre-conditions: Quiet hours 9pm–7am set. Steps: Trigger a non-HITL push at 11pm. Expected: Push deferred to 7am; HITL approval push during same window arrives immediately.
Persona: P-2 · Covers: FR-23, NFR-1 · Surface: iOS Pre-conditions: Logged in. Steps: Cold-start the app. Expected: "Today" view rendered ≤2s P95 on a 3-year-old iPhone.
Persona: P-2 · Covers: FR-23, NFR-7 · Surface: iOS Steps:
- Toggle airplane mode.
- Edit deal value; save.
- Re-enable network. Expected: Edit queued + flagged "pending sync"; on reconnect, sync runs; activity row logs the edit time.
Persona: P-1 · Covers: FR-17, FR-23, FR-24 · Surface: iOS push Steps: Receive HITL push on lock screen; long-press → Approve. Expected: Approval succeeds without app unlock.
Persona: P-2 · Covers: FR-23 · Surface: iOS Expected: Last-updated timestamp + spinner; new records appear if any.
Persona: P-1 · Covers: FR-18, NFR-1 · Surface: web Steps: Land on workspace home. Expected: Cached aggregates load ≤500ms P95; deal funnel chart rendered; activity-over-time chart rendered.
Persona: P-4 · Covers: FR-18 · Surface: web Steps: Click counter "Deals in Negotiation: 12". Expected: Filtered list of those 12 deals.
Persona: P-4 · Covers: FR-18, NFR-2 · Surface: web Expected: Status badges include text labels in addition to color.
Persona: any · Covers: NFR-2 · Surface: web Steps: Disable mouse; navigate signup → workspace → contacts → deal → workflow author. Expected: Every interactive element reachable via Tab/Shift-Tab; cmd-K + j/k + slash work; focus-visible ring (tan, 2px, 2px offset) on every focused element.
Persona: any · Covers: NFR-2 · Surface: web + iOS Pre-conditions: OS reduced-motion enabled. Expected: Page transitions, skeleton shimmer, agent suggestion fade-in collapse to instant.
Persona: any · Covers: NFR-2 · Surface: web Steps: Load page; press Tab. Expected: "Skip to content" link appears; activating it focuses main.
Persona: engineering · Covers: NFR-2 · Surface: web Steps: Run axe-core against /, /contacts, /deals, /automations//edit, /brand. Expected: No violations of severity ≥serious.
Persona: any · Covers: NFR-2 · Surface: web Pre-conditions: VoiceOver or NVDA running. Expected: Deal name, stage, value, owner all announced; kanban drag has keyboard-equivalent (move-to dropdown).
Persona: engineering · Covers: NFR-1 · Surface: web Steps: RUM measurement on contacts → deal → company → contacts navigations. Expected: P95 ≤300ms.
Persona: P-2 · Covers: NFR-1 · Surface: web Steps: Scroll, filter change. Expected: No frame drops below 50fps; filter change ≤200ms.
Persona: P-3 · Covers: FR-12, NFR-1 · Surface: web Steps: Open a 100-node workflow; pan, zoom, drag. Expected: Canvas responsive ≥45fps.
Persona: engineering · Covers: FR-11, NFR-1 · Surface: admin Steps: Run a 10-step workflow without delays/AI. Expected: Per-step advancer latency P95 ≤500ms.
Persona: P-2 · Covers: FR-3, FR-20 · Surface: web Pre-conditions: New workspace with 0 contacts. Expected: Empty state shows "No contacts yet — import from CSV / HubSpot / Pipedrive, or create one." with CTAs.
Persona: P-3 · Covers: FR-12 · Surface: web Pre-conditions: Workflow with invalid node config. Steps: Click Save / Publish. Expected: Inline error highlights bad node; canvas state preserved; user can fix.
Persona: P-2 · Covers: FR-13, NFR-6 · Surface: web Pre-conditions: Anthropic returns 503. Steps: Request a draft. Expected: Router falls back to OpenAI; degraded notice in admin telemetry; user sees a draft (possibly with "fallback model" label).
Persona: any · Covers: NFR-1 · Surface: web + iOS Expected: Each route ships an idiomatic skeleton; never a blank flash.
Persona: P-2 · Covers: FR-23 · Surface: iOS Pre-conditions: Offline. Expected: Last-cached data visible with "Offline" badge; retry button.
Persona: P-3 · Covers: FR-25 · Surface: web
Steps: Audit log filter last 30 days, kind = field_changed, entity = deal.
Expected: Table of changes with actor + diff; CSV export available.
Persona: P-1 · Covers: NFR-8 · Surface: web + admin Steps: Owner requests workspace deletion. Expected: Workspace soft-deleted; 30-day countdown banner; after 30 days a job hard-deletes all workspace data including S3/Spaces objects and embedding rows.
Persona: engineering · Covers: NFR-3, NFR-8 · Surface: admin Steps: In a test, force two simultaneous AI calls from different workspaces with overlapping content. Expected: Prompt cache keys are workspace-scoped; one workspace's data never appears in another workspace's response context.
| Functional area | Web | iOS | API | Workflow |
|---|---|---|---|---|
| Auth & workspaces | ✓ | ✓ | ✓ | — |
| Contacts/Companies/Deals/Tasks | ✓ | ✓ | ✓ | ✓ (mutate) |
| Custom fields | ✓ | (read) | ✓ | ✓ (mutate) |
| Email/Calendar sync | ✓ | (read activity) | ✓ | ✓ (trigger + send) |
| Workflow authoring | ✓ | — | ✓ | — |
| Workflow execution | (run inspector) | (HITL) | ✓ | ✓ |
| AI: comms / NL / next-actions / scoring | ✓ | (next-actions read) | ✓ | ✓ |
| Agent autonomy + HITL | ✓ | ✓ (push + approve) | ✓ | ✓ |
| Reports | ✓ | (light) | ✓ | — |
| Search | ✓ | ✓ | ✓ | — |
| Import | ✓ | — | ✓ | ✓ |
| Slack | ✓ (configure) | — | ✓ | ✓ (action) |
| Webhooks / HTTP | ✓ (configure) | — | ✓ | ✓ |
| Audit | ✓ | — | ✓ | — |
- Smoke (≤10 min) — TC-1, TC-7, TC-10, TC-15, TC-25, TC-29, TC-43, TC-55, TC-68. Run before every deploy.
- Pre-release — every test case TC-1..TC-91. Run before each milestone-ending demo.
- Continuous (Playwright web) — auth, workspace switch, contact/deal CRUD, cmd-K, brand a11y. Runs on every PR.
- iOS regression (XCUITest) — login, list, detail, HITL push approve. Runs on every iOS build.
TESTING.md ends.