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:
@@ -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).
|
||||||
|
|||||||
@@ -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.*"
|
||||||
20
client_app/.maestro/scripts/seed_customer.js
Normal file
20
client_app/.maestro/scripts/seed_customer.js
Normal 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
|
||||||
@@ -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).
|
||||||
|
|||||||
Reference in New Issue
Block a user