Member profiles are optional: an invitation on the dashboard, never a gate
Manson's objection was that requiring a username and a verified email pulls the
build back toward a centralized database of members. He is right, and the
communication gap is real too, so the answer is to ask well rather than to force.
Nothing about holding a position, getting paid, reading the org, the training or
the tools depends on contact details any more. There is no onboarding gate: a
brand-new member registers, lands on their page and is never stopped by a modal.
The dashboard offers a dismissable card ("Not now" snoozes it for a week) that
leads with the thing members actually want, a note the moment a payout lands in
their wallet, and says outright that everything works the same without it. The
inbox is the one place that asks, because a message cannot be delivered to
someone who left no way to reach them, and even there it is an invitation.
The card sits above the tab strip rather than inside the dashboard pane: the page
opens on the pitch tab, so an invitation parked in the dashboard would never be
seen by the new members it is aimed at.
For leaders, /api/public/reach answers "how many of my org can I reach off the
site", scoped by chain.isInTeam so it leaks nothing upward or sideways. That
makes coverage a leader's own problem to solve by asking, not a rule imposed on
members.
Fixes a real bug found by the rewritten suite: the dismissable flag double-booked
as "single-field edit", so saving a username in the opt-in flow closed the dialog
instead of advancing to the email step. Split into oneShot; the suite now asserts
the advance as a regression.
QA, all green: profiles-unit 28, signin-fallback 7, gate-e2e 33 (rewritten to
assert the opposite of what it used to: no forced modal, dismissable everywhere,
visitors unaffected), join-flow 12 cold / 11 refuse / 12 warm.
qa/reseed.sh carries two hard-won guards: never name a shell variable TMP on
Windows (it inherits the system temp dir and rm -rf wipes it), and never pkill.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+48
-18
@@ -1,25 +1,47 @@
|
||||
# RM Circle QA
|
||||
|
||||
## Member profile gate
|
||||
## 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
|
||||
|
||||
# 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
|
||||
# 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: 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.
|
||||
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.
|
||||
@@ -33,13 +55,21 @@ left #787 without a profile: the cached index does not yet know the brand-new po
|
||||
```
|
||||
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=<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=<addr> TEST_PRIV=<priv> TEST_ID=9001 SCENARIO=sign node qa/join-flow-e2e.mjs
|
||||
LOCAL=http://127.0.0.1:3399 TEST_ADDR=<addr> TEST_PRIV=<priv> TEST_ID=9002 SCENARIO=refuse node qa/join-flow-e2e.mjs
|
||||
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=<addr> TEST_ID=9001 COLD=1 PORT=3400 DATA_DIR=$J ADMIN_PASSWORD=localtest node qa/harness-server.js &
|
||||
TEST_ADDR=<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=<addr> TEST_PRIV=<priv> TEST_ID=9001 SCENARIO=sign node qa/join-flow-e2e.mjs
|
||||
LOCAL=http://127.0.0.1:3400 TEST_ADDR=<addr> TEST_PRIV=<priv> TEST_ID=9002 SCENARIO=refuse node qa/join-flow-e2e.mjs
|
||||
LOCAL=http://127.0.0.1:3402 TEST_ADDR=<addr> TEST_PRIV=<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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user