Test: TS-07 returning user with existing display_name skips set-name

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, <name>" 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) <noreply@anthropic.com>
This commit is contained in:
2026-05-17 20:50:40 +08:00
parent e09f76ceb6
commit 93fa5f113a
4 changed files with 198 additions and 1 deletions

View File

@@ -306,6 +306,29 @@ export const internalTestRoutes = async (fastify) => {
return { ok: true, payment_id: row.id, ...row } 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 // 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 // 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). // (e.g. force-mitra-offline leaving the dev DB with no online mitras).

View File

@@ -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, <name>" 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.*"

View File

@@ -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

View File

@@ -608,7 +608,7 @@ Manual reproduction checklists for Phase 4 customer flows. Tick boxes as
verified. Cluster tag `[C]` = client_app, `[BE]` = backend setup. verified. Cluster tag `[C]` = client_app, `[BE]` = backend setup.
> **Coverage map** — these scenarios collectively exercise every branching > **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) | > | 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 | > | 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 | > | 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 | > | 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) ## 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 paid, so this is effectively abandoning a paid session. Worth confirming
the UX (probably a confirmation prompt) and whether the payment is the UX (probably a confirmation prompt) and whether the payment is
refunded / converted to credit. 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).