# RM Circle QA ## Member profile gate ``` # unit: profiles.js in isolation (seeding, username rules, code flow, persistence) node qa/profiles-unit.mjs # end to end: the real UI with real inbox sessions TMP=/d/tmp/rmc-e2e-data && rm -rf $TMP && mkdir -p $TMP ssh root@coolify.saasy.top "docker exec \$(docker ps -q --filter name=kr445fqc) cat /app/data/config.json" > $TMP/config.json echo '{}' > $TMP/sponsors.json echo '{"49":{"email":"seeded49@example.com","ts":"x"}}' > $TMP/member-alerts.json node -e "const m=require('./messages.js'); m.init({dataDir:process.argv[1],chain:require('./chain.js'),isProd:false}); require('fs').writeFileSync(process.argv[1]+'/tokens.txt', m.mintSession(21)+'\n'+m.mintSession(49));" $TMP PORT=3399 DATA_DIR=$TMP ADMIN_PASSWORD=localtest node server.js & LOCAL=http://127.0.0.1:3399 TOKEN=$(sed -n 1p $TMP/tokens.txt) TOKEN2=$(sed -n 2p $TMP/tokens.txt) node qa/gate-e2e.mjs ``` Covers: the gate fires on the owner's own page only, cannot be dismissed (Escape, backdrop, no close button), username and email validation, the code flow including a wrong code, completion landing on the dashboard tab, the profile card and in-place editing (dismissable), the seeded email pre-filling at step 2, visitors on every ID-keyed shared link seeing no gate and no 401, and phone-width layout. `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)}))" TMP=/d/tmp/rmc-jd && rm -rf $TMP && mkdir -p $TMP && echo '[]' > $TMP/sponsors.json ssh root@coolify.saasy.top "docker exec \$(docker ps -q --filter name=kr445fqc) cat /app/data/config.json" > $TMP/config.json TEST_ADDR= TEST_ID=9001 COLD=1 PORT=3399 DATA_DIR=$TMP ADMIN_PASSWORD=localtest node qa/harness-server.js & LOCAL=http://127.0.0.1:3399 TEST_ADDR= TEST_PRIV= TEST_ID=9001 SCENARIO=sign node qa/join-flow-e2e.mjs LOCAL=http://127.0.0.1:3399 TEST_ADDR= TEST_PRIV= TEST_ID=9002 SCENARIO=refuse node qa/join-flow-e2e.mjs ``` `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.