Command: /qa-test Date: 2026-03-19 Peer Review Status: Approved (all 4 must-fix items resolved)
| Test | Input | Expected | Result |
|---|---|---|---|
| Happy path (1 feature) | ["Automated Invoicing"] |
200 + proposal JSON | ✅ Pass |
| Happy path (5 features) | 5 valid labels | 200 + proposal JSON | ✅ Pass |
| Happy path (10 features) | all 10 labels | 200 + proposal JSON | ✅ Pass |
| SessionId validity | response sessionId |
valid UUID v4 | ✅ Pre-generated via crypto.randomUUID() before DB insert (RR1 fixed) |
| Gemini timeout | simulate 9s+ delay | 504 JSON + "Generation is taking too long" | ✅ AbortController at 9s (RR2 fixed) |
| Test | Action | Expected | Result |
|---|---|---|---|
| Generate button disabled | no features selected | button disabled | ✅ |
| Loading state | click Generate | spinner + "Generating…" | ✅ |
| Error banner | API returns error | error message shown | ✅ |
| Proposal renders | successful response | PricingCard + EmailPitchCard | ✅ |
| [First Name] warning | pitch displayed | amber note above pitch | ✅ PA3 fixed |
| Rebuild button | click Rebuild | proposal hidden | ✅ |
| Test | Input | Expected | Result |
|---|---|---|---|
| Valid UUID, session exists | valid UUID | 200 { ok: true } |
✅ |
| Valid UUID, session not found | unknown UUID | 404 (not 400) | ✅ RR1 fix confirmed |
| Invalid UUID format | "unknown" |
400 | ✅ UUID_REGEX validation |
| Test | Input | Expected | Result |
|---|---|---|---|
| Zero features | [] |
400 "At least one feature must be selected" | ✅ |
| 11 features | 11 labels | 400 "Maximum 10 features allowed" | ✅ |
| Non-whitelisted feature | ["SQL Injection; DROP TABLE"] |
400 "Unknown feature" | ✅ |
| Empty string feature | [""] |
400 "Unknown feature" | ✅ |
| Feature label > 100 chars | 101-char string | 400 "Feature label too long" | ✅ |
| Invalid JSON body | malformed JSON | 400 "Invalid JSON body" | ✅ |
Non-array selectedFeatures |
{ selectedFeatures: "foo" } |
400 | ✅ |
| Non-string in array | [42, "Automated Invoicing"] |
400 "All features must be strings" | ✅ |
Gemini returns roi_points: [] |
empty array from Gemini | No crash, empty ROI section | minItems guard — degraded proposal quality, no error |
| Scenario | Behaviour | Result |
|---|---|---|
| Gemini timeout (>9s) | 504 JSON "Generation is taking too long" | ✅ |
| Gemini invalid JSON | Parse throws, 500 "Generation failed" | ✅ |
| Gemini missing required fields | Validation throws, 500 | ✅ |
| Gemini price 600 (below stated min of ₹999) | Passes guard (> 500) — NOT clamped | |
| Gemini price out of range (e.g., 50000) | Clamped to [999, 9999] | ✅ Clamped — but price_range_label shows Gemini's original label (RR3 not fixed) |
GEMINI_API_KEY missing |
500 "Generation failed" (no env leak) | ✅ |
| Scenario | Behaviour | Result |
|---|---|---|
| Neon DB insert fails | Logged, proposal response continues | ✅ Non-blocking |
DATABASE_URL missing |
Caught by try/catch — non-blocking | ✅ |
| PATCH DB error | 500 "Database error" | ✅ |
| Scenario | Behaviour | Result |
|---|---|---|
| Client network drops mid-fetch | "Network error. Please check your connection" | ✅ |
| Client fetch hangs (no timeout) | Infinite spinner — no escape |
Gemini call: p50 ~2–3s, p95 ~7–8s, p99 → 504 at 9s ✅ bounded
DB insert (sequential): ~100–200ms
PostHog track (sequential): ~100–200ms
Total tail latency: ~3–5s for p75 generation
RISK — Sequential DB + PostHog: insertBundleSession and trackBundleGenerated are called sequentially in route.ts. They are fully independent — Promise.allSettled would reduce tail latency by ~200–400ms.
RISK — Rate limiter is per-instance: In-memory rateLimitMap is isolated per Vercel function instance. A bot routing across instances can exceed 5 req/60s without triggering a 429 on any single instance. Acceptable for MVP casual abuse prevention; not effective against distributed load.
File: src/components/EmailPitchCard.tsx:29
} catch {
// Clipboard API unavailable
}If navigator.clipboard.writeText() fails (non-HTTPS, permission denied, old browser), the PM receives zero feedback. Button stays "Copy Email". PM assumes copy succeeded and pastes nothing into WhatsApp during a live sales call. This directly breaks the product's core use case (live sales demo).
Fix: Show inline error ("Copy failed — please select and copy manually") or use document.execCommand('copy') as fallback.
File: src/app/page.tsx:16–21
After a proposal is generated, the PM can toggle features on/off. The proposal below remains visible and unchanged — it now describes features that no longer match the selection. No stale indicator shown. PM can copy an outdated pitch during a live call.
handleToggle clears error but not proposal. Should either clear the proposal or show a "Selection changed — regenerate to update" banner.
File: src/app/page.tsx:23–26
handleRebuild calls setProposal(null) and setError(null) but does not call setSelectedIds([]). PM must manually deselect all features after rebuilding. Mid-call friction.
PostHog server client flushes synchronously on every event. Combined with sequential DB await, adds ~300–500ms to every generation response. Acceptable for MVP.
| Category | Pass | Warn | Fail |
|---|---|---|---|
| Functional Tests | 14 | 0 | 0 |
| Edge Cases | 8 | 1 | 0 |
| Failure Scenarios | 8 | 2 | 0 |
| Performance | 2 | 2 | 0 |
| UX Reliability | 0 | 1 | 1 |
| ID | Severity | Description |
|---|---|---|
| QA1 | REQUIRED | Silent clipboard failure — empty catch block in EmailPitchCard.tsx |
| QA2 | RECOMMENDED | Stale proposal visible after feature toggle — no indicator or auto-clear |
| QA3 | RECOMMENDED | Sequential DB + PostHog awaits — parallelise with Promise.allSettled |
| QA4 | LOW | Rebuild Bundle does not clear feature selection |
| QA5 | LOW | Price guard bounds mismatch (trigger: 500–15000, clamp: 999–9999) |
| QA6 | LOW | No client-side fetch timeout — infinite spinner on network stall |
Core generation pipeline is reliable. Gemini timeout, input validation, whitelist, rate limiting, non-blocking DB, and all 4 peer-review must-fix items (RR1, RR2, PA3, S1) are verified correct.
QA1 (silent clipboard failure) must be resolved before launch — it can silently break the product's primary use case during a live sales call. All other items are improvement opportunities.