From 93fa5f113a5b95d26e2735497ee4d5e5f9af9585 Mon Sep 17 00:00:00 2001 From: Ramadhan Sjamsani Date: Sun, 17 May 2026 20:50:40 +0800 Subject: [PATCH] Test: TS-07 returning user with existing display_name skips set-name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inverse coverage for the auth path: TS-01..TS-06 all wipe the customer row (drop_customer=true) so every OTP path lands on the new-user set-name branch. TS-07 instead seeds an existing identified customer (phone + display_name + is_anonymous=false) and verifies the OTP sign-in returns the existing row unchanged via resolveCustomerForIdentity branch 1, so /auth/set-name is never shown. Adds: * /internal/_test/seed-customer endpoint — upserts a customer with phone + display_name + is_anonymous=false. * client_app/.maestro/scripts/seed_customer.js helper. * client_app/.maestro/flows/ts-07_returning_existing_name_skips_setname.yaml. * TS-07 scenario doc + coverage-map row in requirement/phase4-customer-flow.md. The flow asserts the "halo, " greeting on the returning-user home variant (identified users always land on _SHomeReturningView regardless of chat history) plus an explicit notVisible on "Siapa namamu" as a belt-and-braces check. Co-Authored-By: Claude Opus 4.7 (1M context) --- backend/src/routes/internal/_test.routes.js | 23 ++++ ...returning_existing_name_skips_setname.yaml | 101 ++++++++++++++++++ client_app/.maestro/scripts/seed_customer.js | 20 ++++ requirement/phase4-customer-flow.md | 55 +++++++++- 4 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 client_app/.maestro/flows/ts-07_returning_existing_name_skips_setname.yaml create mode 100644 client_app/.maestro/scripts/seed_customer.js diff --git a/backend/src/routes/internal/_test.routes.js b/backend/src/routes/internal/_test.routes.js index 5a37131..8682734 100644 --- a/backend/src/routes/internal/_test.routes.js +++ b/backend/src/routes/internal/_test.routes.js @@ -306,6 +306,29 @@ export const internalTestRoutes = async (fastify) => { return { ok: true, payment_id: row.id, ...row } }) + // Upsert a customer row with phone + display_name (is_anonymous=false). + // Used by Maestro TS-07 to set up the "returning user already has a name" + // precondition: a real returning OTP sign-in must skip the set-name screen + // because resolveCustomerForIdentity returns the existing row unchanged. + // + // Body: { phone, display_name } + fastify.post('/seed-customer', async (request, reply) => { + const phone = request.body?.phone + const display_name = request.body?.display_name + if (!phone || !display_name) { + return reply.code(400).send({ error: 'phone and display_name required in body' }) + } + const [row] = await sql` + INSERT INTO customers (phone, display_name, is_anonymous) + VALUES (${phone}, ${display_name}, false) + ON CONFLICT (phone) DO UPDATE + SET display_name = EXCLUDED.display_name, + is_anonymous = false + RETURNING id, phone, display_name, is_anonymous + ` + return { ok: true, ...row } + }) + // Mark EVERY mitra row online. Used by Maestro flows as a setup step to // ensure a clean known-good state regardless of what previous tests did // (e.g. force-mitra-offline leaving the dev DB with no online mitras). diff --git a/client_app/.maestro/flows/ts-07_returning_existing_name_skips_setname.yaml b/client_app/.maestro/flows/ts-07_returning_existing_name_skips_setname.yaml new file mode 100644 index 0000000..16b07bf --- /dev/null +++ b/client_app/.maestro/flows/ts-07_returning_existing_name_skips_setname.yaml @@ -0,0 +1,101 @@ +# TS-07 — Returning user with existing display_name skips set-name screen +# (requirement/phase4-customer-flow.md → Test Scenarios → TS-07). +# +# Inverse of TS-01..TS-06: those flows wipe the customer (drop_customer=true) +# so every OTP path hits the new-user set-name branch. TS-07 instead seeds +# an EXISTING customer row with phone + display_name, then verifies the +# OTP sign-in returns the existing row unchanged (via +# resolveCustomerForIdentity branch 1) and the client routes directly to +# /home without showing /auth/set-name. +# +# Pre-reqs: +# - Backend reachable, NODE_ENV != 'production'. +# - (No mitra requirement — flow stops at /home.) +# +# Run: +# maestro test client_app/.maestro/flows/ts-07_returning_existing_name_skips_setname.yaml +appId: com.halobestie.client.client_app +env: + TEST_PHONE: "+6281234567890" + EXISTING_NAME: "Returning User" + BACKEND_INTERNAL_URL: http://localhost:3001 +--- +# --- Setup: wipe the phone, then re-seed an identified customer with name --- +- runScript: + file: ../scripts/reset_phone.js + env: + TEST_PHONE: ${TEST_PHONE} + BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL} +- runScript: + file: ../scripts/seed_customer.js + env: + TEST_PHONE: ${TEST_PHONE} + DISPLAY_NAME: ${EXISTING_NAME} + BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL} +- launchApp: + clearState: true + +# --- Welcome carousel → home (anon) --- +- extendedWaitUntil: + visible: + text: "Mulai" + timeout: 15000 +- tapOn: + text: "Mulai" + retryTapIfNoChange: true + +# Home anon view shows the `masuk →` banner. +- extendedWaitUntil: + visible: + text: "(?s).*udah pernah pakai HaloBestie.*" + timeout: 30000 + +# --- Tap masuk → register → phone → OTP --- +- tapOn: + text: "(?s).*masuk →.*" + +- extendedWaitUntil: + visible: + text: "(?s).*nomor wa-mu.*" + timeout: 10000 +- tapOn: + point: "60%, 47%" +- inputText: "81234567890" +- hideKeyboard +- tapOn: + text: "(?s).*kirim kode.*" + +- extendedWaitUntil: + visible: + text: "Masukkan OTP" + timeout: 15000 +- runScript: + file: ../scripts/peek_otp.js + env: + TEST_PHONE: ${TEST_PHONE} + BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL} +- inputText: ${output.OTP} + +# --- KEY ASSERTIONS --- +# 1. OTP entry should disappear (auto-submit on 6th digit). +- extendedWaitUntil: + notVisible: + text: "Masukkan OTP" + timeout: 15000 + +# 2. Home renders directly. Identified (verified) users land on the +# `_SHomeReturningView` regardless of chat history: greeting becomes +# "halo, " and CTA flips to "curhat sama bestie baru". The +# 1st-time view ("aku mau curhat") is the anon-user variant only. +- extendedWaitUntil: + visible: + text: "(?s).*halo, ${EXISTING_NAME}.*" + timeout: 20000 +- assertVisible: "(?s).*curhat sama bestie baru.*" + +# 3. The "Siapa namamu?" set-name screen must NOT have been shown — +# if it had, the assertion above would have failed at the set-name +# intermediate. This belt-and-braces assert catches the case where +# the set-name screen briefly flashes then auto-redirects. +- assertNotVisible: + text: "(?s).*Siapa namamu.*" diff --git a/client_app/.maestro/scripts/seed_customer.js b/client_app/.maestro/scripts/seed_customer.js new file mode 100644 index 0000000..aa7e662 --- /dev/null +++ b/client_app/.maestro/scripts/seed_customer.js @@ -0,0 +1,20 @@ +// Upsert a customer row with TEST_PHONE + DISPLAY_NAME via the dev-only +// /internal/_test/seed-customer endpoint. Used by TS-07 to set up the +// "returning user already has a name" precondition, so the OTP sign-in +// path can verify the set-name screen is skipped for existing identified +// customers. +const phone = TEST_PHONE +const displayName = DISPLAY_NAME +const url = BACKEND_INTERNAL_URL || 'http://localhost:3001' +if (!phone) throw new Error('TEST_PHONE env not set') +if (!displayName) throw new Error('DISPLAY_NAME env not set') +const resp = http.post(`${url}/internal/_test/seed-customer`, { + body: JSON.stringify({ phone, display_name: displayName }), + headers: { 'Content-Type': 'application/json' }, +}) +if (resp.status !== 200) { + throw new Error(`seed-customer failed (${resp.status}): ${resp.body}`) +} +const data = json(resp.body) +output.CUSTOMER_ID = data.id +output.CUSTOMER_DISPLAY_NAME = data.display_name diff --git a/requirement/phase4-customer-flow.md b/requirement/phase4-customer-flow.md index c2fd1ea..fc426e2 100644 --- a/requirement/phase4-customer-flow.md +++ b/requirement/phase4-customer-flow.md @@ -608,7 +608,7 @@ Manual reproduction checklists for Phase 4 customer flows. Tick boxes as verified. Cluster tag `[C]` = client_app, `[BE]` = backend setup. > **Coverage map** — these scenarios collectively exercise every branching -> point in §4 of `flow_customer.mermaid.md`: +> point in §4 of `flow_customer.mermaid.md`, plus one §2 (auth) edge: > > | Branching point | Scenario(s) | > |---|---| @@ -618,6 +618,7 @@ verified. Cluster tag `[C]` = client_app, `[BE]` = backend setup. > | PayStat: `paid` vs `timeout 20 min` | TS-01/02/04/06 vs TS-05 | > | PairRoute: `lama (Targeted)` vs `baru / cari lain (BlastFlow)` | TS-01/05/06 vs TS-02/04 | > | TargetedRes: `accept` vs `reject/timeout` | TS-01/05 vs TS-06 | +> | §2 post-OTP: new user (set-name) vs existing user with name (skip) | TS-01..06 vs TS-07 | ## TS-01 — Returning user re-pays an online bestie (lama happy path) @@ -878,3 +879,55 @@ escape (same shape as TS-03), but post-payment — the customer has already paid, so this is effectively abandoning a paid session. Worth confirming the UX (probably a confirmation prompt) and whether the payment is refunded / converted to credit. + +--- + +## TS-07 — Returning user with existing display_name skips set-name screen + +**Flow:** §2 (verified path) `Choice → "verif WA" → OTP → user lookup → existing account (display_name set, has_transacted=false) → /home`. Verifies the existing-user-with-name branch of `resolveCustomerForIdentity`. + +**Affects:** `client_app`, `backend`. + +**Goal:** Confirm a phone-OTP sign-in for a customer who already has a +non-empty `display_name` in `customers` does NOT re-show the +"Siapa namamu?" set-name screen. Routes directly from OTP success +to /home with the stored display_name. This is the inverse of TS-01..TS-06, +all of which use `drop_customer:true` (wiping the row) and therefore always +land on the new-user set-name branch. + +**Pre-reqs** +- [ ] **[BE]** Backend reachable; NODE_ENV != 'production'. + +**Steps** +1. [ ] **[BE]** Wipe phone state via `/internal/_test/reset-phone` + `{ phone, drop_customer: true }` — clears any prior customer row. +2. [ ] **[BE]** Seed an identified customer via + `/internal/_test/seed-customer` `{ phone, display_name }` — + inserts a row with `is_anonymous=false` and the chosen display_name. +3. [ ] **[C]** Cold-launch `client_app` with clearState → welcome + carousel → tap `Mulai` → home (anonymous view, shows `masuk →` banner). +4. [ ] **[C]** Tap `masuk →` → `/auth/register` → input phone digits + (after the `+62` chip) → tap `kirim kode` → OTP screen. +5. [ ] **[C]** Peek OTP from the stub, input it — auto-submits on the + 6th digit. + +**Expected result** +- [ ] **[C]** App routes directly to `/home`, CTA `aku mau curhat` + visible (the `_SHome1stView` no-history variant). The customer's + stored display_name is loaded into the profile state. +- [ ] **[C]** The `Siapa namamu?` set-name screen is **never shown**. + An `assertNotVisible` for the set-name title at the home-arrival point + acts as a belt-and-braces check against a brief flash-then-redirect. +- [ ] **[BE]** No new `customers` row created — the seeded row is the + same one returned by `getCustomerByPhone` → `resolveCustomerForIdentity` + branch 1 (existing identity, no anon prefix). `customers.id` after the + flow equals the seeded `CUSTOMER_ID`. + +**Why this needs its own test:** TS-01..TS-06 all begin with +`reset_phone` `drop_customer:true`, which makes every OTP path land in +`resolveCustomerForIdentity` branch 4 (no existing + no anon → create +new with display_name=null → client routes to set-name). That covers +the new-user surface but never exercises the "existing user with name" +path. TS-07 is the symmetric coverage for the same auth code, ensuring +the set-name screen isn't accidentally re-shown for known users (which +would be a real UX regression — name re-entry every login).