Files
rm-circle-team-router/qa
martbost b3385ca3f8 Captions: translated subtitles for the training videos, no re-render, no re-voicing
Manson asked whether the training videos could be in other languages. A real dub
means re-rendering every video per language: translated Romance-language speech runs
15-25% longer than English and these are slide videos with fixed beat timings, so
swapped audio drifts off what is on screen. That turns 15 files into 75 and makes
every future lesson edit a five-way job. Captions keep ONE video and add small text
tracks beside it, so editing a lesson re-captions that lesson only.

tools/captions.mjs: ffmpeg pulls the audio, ElevenLabs Scribe transcribes with word
timings (the same STT we already use to verify voiceovers), words are grouped into
SENTENCES, and only then translated. Translating cue-by-cue is why most auto-captions
read badly at cue boundaries. Translation goes through our own /api/public/translate,
so every phrase caches forever in translations.json and costs once across the site.
The STT response is cached on disk because it costs money; never pay for it twice.

Language choice is evidence, not instinct. The translation cache shows real member
demand: Italian and French far ahead, then Spanish, then GERMAN - which beats
Portuguese by more than double, the opposite of what we assumed. Proof of concept is
Italian on the 5-minute overview.

Captions are deliberately NOT on by default. An English reader does not want them
forced over the picture; someone who already switched the site to Italian almost
certainly does. public/vtt-lang.js shows the track matching their 🌐 choice and
leaves the player's CC button to do the rest.

Two bugs this caught in my own code, both found by reading the output:
- the line wrapper truncated each cue to two lines and SILENTLY DELETED the overflow,
  so "the whole plan fits in one sentence" shipped as "fits in" then "sentence". It
  now chunks by the real wrapped line count and never drops a word.
- a one or two word tail ("sentence." alone on screen) folds back into the previous
  cue.
The suite asserts all 780 transcript words survive into the English track.

Also: .vtt had no Content-Type mapping, so it served as octet-stream and browsers
silently ignore such a track. qa/captions-e2e.mjs (12 assertions) reads the parsed
cues back out of the player rather than trusting the markup, which is the only way to
catch that class of failure. profiles-unit 28 and gate-e2e 33 still green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 06:06:34 -05:00
..

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=<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.

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.