# RM Circle QA ## Optional member profile Since 2026-09-17 contact details are **optional** (Marty + Manson). Nothing about holding a position, getting paid, reading the org, the training or the tools may depend on a username or an email. There is no onboarding gate. The dashboard shows a dismissable invitation, and the inbox asks only because a message cannot be delivered to someone who left no way to reach them. If a change to this area makes any of those suites fail on "NO modal is forced", stop. That assertion is the product decision, not a test detail. ``` # unit: profiles.js in isolation (seeding, username rules, code flow, persistence) node qa/profiles-unit.mjs # sign-in fallback: real secp256k1 signatures, including the abuse cases node qa/signin-fallback.mjs # end to end: the real UI with real inbox sessions. # reseed.sh builds a throwaway data dir and starts the server on 3399. # NOT idempotent: the suite completes a profile for 21, so reseed before every run. bash qa/reseed.sh D=/d/tmp/rmc-e2e-data LOCAL=http://127.0.0.1:3399 TOKEN=$(sed -n 1p $D/tokens.txt) TOKEN2=$(sed -n 2p $D/tokens.txt) node qa/gate-e2e.mjs ``` Covers: no automatic modal on the owner's own page or on a phone, the dismissable invitation and its 7-day snooze, opting in end to end (including the regression that saving a username must ADVANCE to the email step rather than close), abandoning the dialog at any point, the profile card and in-place editing, the seeded email pre-filling at step 2, the inbox's reason-led ask, visitors on every ID-keyed shared link seeing no prompt and no 401, and phone-width layout. Two traps that cost real time, both now guarded in `reseed.sh`: * **Never call a shell variable `TMP`, `TEMP` or `TMPDIR`.** Windows already sets them to the system temp directory, so `${TMP:-default}` silently inherits it and a following `rm -rf "$TMP"` wipes the machine's temp folder, including the tooling's own scratch files. * **Never add `pkill`/`taskkill`.** A broad pattern kills the tooling running the script. Stop the old server through the pid file. The event flyer and the upgrade promo are full-screen overlays that legitimately cover `/my` once per browser/session and swallow clicks. Both E2E suites mark them already-seen via an init script rather than racing their fade-out. `devCode` is returned by `/api/public/profile/email-start` only when `NODE_ENV !== 'production'`, which is what lets the test read the code. The live container runs with NODE_ENV=production. ## Join flow (the money path) Drives the REAL /join-now page with a fake wallet that produces genuine secp256k1 signatures, against a server whose chain reads are stubbed by `qa/harness-server.js`. `COLD=1` reproduces the state that left #787 without a profile: the cached index does not yet know the brand-new position. ``` node -e "const c=require('crypto'),s=require('./vendor/secp256k1.js'),{keccak256}=require('./vendor/sha3.js');s.utils.hmacSha256Sync=(k,...m)=>{const h=c.createHmac('sha256',Buffer.from(k));m.forEach(x=>h.update(Buffer.from(x)));return Uint8Array.from(h.digest())};const p=c.randomBytes(32),pub=s.getPublicKey(p,false);require('fs').writeFileSync('D:/tmp/rmc-qa-wallet.json',JSON.stringify({priv:p.toString('hex'),addr:'0x'+keccak256(Buffer.from(pub.slice(1))).slice(-40)}))" J=/d/tmp/rmc-jd && rm -rf $J && mkdir -p $J && echo '[]' > $J/sponsors.json # never name it TMP, see the trap above ssh root@coolify.saasy.top "docker exec \$(docker ps -q --filter name=kr445fqc) cat /app/data/config.json" > $J/config.json # cold index (the state that broke #787) on 3400, warm index on 3402 TEST_ADDR= TEST_ID=9001 COLD=1 PORT=3400 DATA_DIR=$J ADMIN_PASSWORD=localtest node qa/harness-server.js & TEST_ADDR= TEST_ID=9003 COLD=0 PORT=3402 DATA_DIR=$J-w ADMIN_PASSWORD=localtest node qa/harness-server.js & LOCAL=http://127.0.0.1:3400 TEST_ADDR= TEST_PRIV= TEST_ID=9001 SCENARIO=sign node qa/join-flow-e2e.mjs LOCAL=http://127.0.0.1:3400 TEST_ADDR= TEST_PRIV= TEST_ID=9002 SCENARIO=refuse node qa/join-flow-e2e.mjs LOCAL=http://127.0.0.1:3402 TEST_ADDR= TEST_PRIV= TEST_ID=9003 SCENARIO=sign node qa/join-flow-e2e.mjs ``` `COLD` defaults to ON: only `COLD=0` gives a warm index, so a missing `COLD` var does not silently run the cold case twice. `SCENARIO=refuse` is the regression that matters most: a member who declines the signature must still complete the join and reach their dashboard. Never ship a join-flow change without it passing. Gotchas: seed `sponsors.json` as `[]` (an object 500s), and the fake wallet auto-connects so `#connectBtn` is hidden. Counters live in sessionStorage because the page redirects.