Bandhu — QA

Manual test cases

Concrete, reproducible cases for exercising the real flow end to end — backend + frontend, no mocks. Written from the actual bugs found wiring this up (see frontend/README.md's "Known gaps" for the ones that are permanent, not fixable by retesting).

Status column is meant to be hand-updated as you actually run these locally — not a claim about current state. expected fail means the case documents a known, deliberate gap, not a bug to chase.

Crisis detection

safety_gate.py — requires scripts/seed_safety_patterns.py run locally first

Matching is plain substring, case-insensitive, against safety_patterns — seeded from the draft, self-vetted list only. Anything not a literal phrase in that list will not trigger, by design, until a professional review happens.

#StepsExpectedStatus
C1 On Home, type I want to die and tap Share. Navigates to Crisis Support. Real helpline cards render (Vandrevala ×2, iCall, KIRAN, Dial 112) — not the hardcoded frontend fallback. todo
C2 Type I don't want to be here anymore. Same as C1 — direct-statement pattern match. todo
C3 Type I've been hurting myself. Same as C1 — self-harm pattern match, distinct pattern_type from C1/C2 but same routing. todo
C4 Type I feel like dying (a natural phrasing, not a literal seeded string). crisis: false. Stays on Response with a normal warm reply. This is the known keyword-matching gap — confirmed 2026-07-14, see conversation log — not a bug to re-report without new information. expected fail
C5 Trigger C1 twice within the same session, a few minutes apart. Second time: crisis: true still (detection never skips), but check whether the card content differs — should_display/suppression logic (SUPPRESSION_WINDOW, 2h) should show the softer "I'm still here with you" card, not re-render the full card. todo
C6 Trigger a crisis phrase from Thinking Trap's composer (after selecting a pattern) instead of Home. Same crisis routing fires from that entry point too — ThinkingTrap.tsx has its own result.crisis check. todo

Response — multi-turn conversation

Response.tsx
#StepsExpectedStatus
R1 From Home, send a first message. On Response, send 2-3 more messages in a row. Each reply appends as a new bubble (companion left, you right). Composer stays pinned to the bottom of the screen; message list scrolls internally, page itself never grows taller than the viewport. todo
R2 Reference something you said 2 messages ago (e.g. "the exam thing I mentioned"). Reply shows awareness of it — same-session memory (read_recent_turns, 2h/12-turn window) working, not just single-shot. todo
R3 Send a message likely to produce a thinking-trap nudge, e.g. I always mess everything up, I'm a failure at everything. Retry with a fresh session 2-3 times if it doesn't fire the first time.Confirmed 2026-07-14: this is a genuine Orchestrator judgment call, not a rule — same exact message fired ~50% of the time across 4 fresh sessions. One silent reply isn't a bug; only report if it never fires across several tries with category=thinking-trap confirmed in the Langfuse trace. When it fires: reply renders normally, plus a small muted line "Want to look at it together?" beneath that specific bubble only (not every bubble). Tapping it opens Thinking Trap. todo
R4 Stop the backend (kill the uvicorn process), then send a message on Response. Inline error banner with a Retry button — no crash, no stuck spinner. Restart the backend, tap Retry, message sends successfully. todo
R5 Refresh the page directly on /app/response (no router state). Doesn't crash — shows the fallback acknowledgment ("I hear you. That sounds like a lot to carry.") as a valid, complete screen on its own. todo

Thinking Trap

ThinkingTrap.tsx — requires scripts/ingest_content.py run first
#StepsExpectedStatus
T1 Reach Thinking Trap via a "Want to look at it together?" line (see R3), select "Fortune Telling", tap Continue. Sends a bridged message and lands on Response with a reply that's actually about predicting bad outcomes without evidence — not a generic reply. Confirms real retrieval against the seeded content_entries rows, not a canned string. todo
T2 On Thinking Trap, tap Continue with nothing selected. Continue button stays disabled — no request sent. todo
T3 Select a pattern, but the selection itself happens to read as crisis language (unlikely but worth checking once). Still routes to Crisis Support correctly — ThinkingTrap.tsx's own result.crisis branch, independent of Response's. todo

Breathing

Breathing.tsx + POST /breathe
#StepsExpectedStatus
B1 On Home, tap "Breathe" in the secondary actions row. Full-screen breathing view opens immediately (no loading spinner blocking it — the log call is fire-and-forget). Circle animates through a 4-4-4-4 in/hold/out/hold cycle, phase label updates each step. todo
B2 Leave the breathing screen open through 2-3 full cycles, then tap the ✕. Returns to the previous screen. No error, no stuck state. Check GET /looking-back afterward — a theme: "breathing" checkin should be logged. todo

Looking Back

LookingBack.tsx + GET /looking-back
#StepsExpectedStatus
L1 On a brand-new session (no prior check-ins), open Looking Back via the header icon. Cold-start copy renders ("Nothing to look back on yet…"), not a blank screen or error — summary_text is genuinely null until the nightly Summarizer job runs at least once. todo
L2 After a few real check-ins in one session, open Looking Back. Daily timeline shows each checkin's mood/theme, most recent date first, grouped correctly by day. Still no summary_text unless the Summarizer job has actually run (nightly, not per-message) — that's expected, not a bug. todo

Settings

Settings.tsx + DELETE /session
#StepsExpectedStatus
S1 Toggle language to हिन्दी, reload the page, revisit Settings. Selection persisted (reads from localStorage). Nothing else in the app actually translates — that's the documented gap, not a bug. todo
S2 Have a few check-ins logged. Tap "Delete my data" once, then tap it again to confirm. Second tap actually deletes — first tap only arms the confirm state, doesn't delete. Navigates back to Home. Immediately after, GET /looking-back returns an empty checkins list — a fresh session, cascade delete worked. todo
S3 Tap "Delete my data" once, then navigate away without confirming. Nothing deleted. Returning to Settings later shows the button back in its normal (non-confirming) state. todo
S4 Tap the Privacy Policy button. Opens /privacy/ with the real contact email, not a placeholder. todo

Home — mood, loading, error states

Home.tsx
#StepsExpectedStatus
H1 Tap a mood (e.g. "Anxious") with the text box empty, then tap Share. Sends successfully — MOOD_ONLY_TEXT bridges the tap into a plain sentence server-side sees as normal text. Share button is enabled the moment a mood is tapped, even with no typed text. todo
H2 Tap Share with both text empty and no mood selected. Share button stays disabled — no request sent. todo
H3 Type a message, tap Share, and watch the button while the request is in flight. Button shows a spinner and is disabled — can't double-submit by tapping again. todo
H4 Stop the backend, send a message from Home. Inline error banner with Retry, same pattern as R4. Spinner clears, button re-enables. todo

Cross-cutting / infra

session, CORS, telemetry
#StepsExpectedStatus
X1 Close the browser tab entirely, reopen localhost:5173, go straight to Home (skip Welcome — bandhu_visited already set). bandhu_sid cookie persists across the restart (14-day expiry, not session-only) — a message sent still has access to the same conversation history as before closing. todo
X2 After any real /message call, check the Langfuse dashboard for that trace. Every stage span (pipeline.ingest, pipeline.classify, pipeline.retrieval, pipeline.generate, etc.) shows real Input/Output, not blank — requires TELEMETRY_LOG_MESSAGE_CONTENT=true in local .env (already set for local dev, see backend/.env's comment — flip back to false before anything shared). todo
X3 Fire 5+ messages in quick succession (well under a minute). All succeed under 30s each. If one times out, it should fail once (~30s), not 3× (~90-112s) — confirms max_retries=0 is actually in effect on both NVIDIA clients. todo