eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1# πŸ€– verify β€” drive the web app end-to-end against a local build
2
3How to observe a web/chat change in a real browser, fast (no fixtures in the
4repo; everything is provisioned over the real protocol).
5
6## Build + run
7
8```sh
9go build -o /tmp/lchat .
10LCHAT_ADMIN_TOKEN=verifytok LCHAT_DB=/tmp/verify.db ADDR=127.0.0.1:18711 /tmp/lchat &
11```
12
13## Provision a member + thread from Node (no UI clicking)
14
15`web/static/crypto.js` runs under Node. (Node β‰₯20 already HAS `globalThis.crypto`
16and it's a getter-only property β€” assigning `webcrypto` to it throws
17`TypeError: Cannot set property crypto`. Just import crypto.js by ABSOLUTE path
18and use it.)
19
201. `randomSecret()` β†’ `deriveInvite(T)` β†’ `{id, claimHash, claimSecret}`.
212. `POST /api/admin/key-invites` (Bearer admin) `{label, id, wrapped:"", claim_hash}`.
223. `POST /api/key-invite/{id}/claim` `{claim_secret}` β†’ `{token}`.
234. `POST /api/pubkey` `{token, public_key}` β€” `generateUserKeypair()` returns
24 **`{publicKey, privateKey}`** (base64url), not `{pub, priv}`.
255. `GET /api/roster?token=` β†’ **`{roster: [...]}`** β€” find yourself by `public_key`.
266. `generateKey()` β†’ `{key, b64}`; `wrapThreadKey(b64, myPub)`;
27 `POST /api/threads` `{token, kind:"group", name_blob: encrypt(key,{name}), members:[{user_id, wrapped}]}` β€” a group of one is valid and mounts the real chat engine (a `self` thread mounts a mini-app instead).
28
29⚠️ **The thread-id field names differ by direction, and getting one wrong fails
30SILENTLY as an empty result, not an error:**
31
32- `POST /api/threads` returns **`{thread_id}`** (not `id`).
33- `GET /api/messages?token=…&thread=…` takes **`thread`** (not `thread_id`);
34 a wrong/absent one 403s `{"error":"not a thread member"}`, so a naive
35 `(r.messages || []).length` reads as **0 messages** and looks like your feature
36 failed to post. Assert on the count of `messages` only after checking the
37 response isn't an error β€” or read the DB (`sqlite3 /tmp/verify.db "select
38 thread_id, count(*) from messages group by thread_id;"`), which never lies.
39
40## Drive it in Chromium (Playwright)
41
42Seed the session *before* page scripts run, then assert on the live DOM:
43
44```js
45await ctx.addInitScript(([sess, tid]) => {
46 localStorage.setItem("lchat_session", JSON.stringify(sess)); // {token,name,userPub,userPriv}
47 localStorage.setItem("lchat_active", tid);
48}, [session, threadId]);
49```
50
51- Live selectors: `.chatapp-root .composer .composer-input`, `.chatapp-root .send-btn`,
52 `.chatapp-root .composer input[type=file]`, `.chatapp-feed .msg`,
53 `.composer-preview .preview-chip`. (The id-based `#feed`/`#text` skeleton is the
54 retired classic chat β€” always empty.)
55- **The composer is a ProseMirror contenteditable, not a `<textarea>`** β€” so
56 `.fill()` doesn't work (the locator never resolves and you get a 30s timeout).
57 Click it and type: `await page.locator(".composer .composer-input").click();
58 await page.keyboard.type("/gif cats"); await page.keyboard.press("Enter");`
59- **Panels are dismissed, not removed**: after `panel.close()` the `.cmd-panel`
60 element survives as an empty container β€” assert `isVisible() === false` (or that
61 its body is gone), never `count() === 0`.
62- **Reset the DB between runs.** State persists, so a second run of a
63 "first-launch asks for X" test finds X already set and the form never appears.
64- External APIs: `ctx.route("**://*.giphy.com/**", …)` to fulfil a fake page +
65 bytes β€” lets the whole tap-to-post path run with no real key, and the route
66 handler can record the `api_key` each call used (which is how you prove *which*
67 key a search read).
68- Slow/failing uploads: `page.route("**/api/upload*", …)` with a delay or a 500 β€”
69 perfect for in-flight-upload races.
70- `alert()` paths: register `page.on("dialog", …)` or the page wedges.
71- Test users render as "Someone" unless they publish encrypted labels β€” harmless.
72- A known Playwright install lives at
73 `~/.npm/_npx/e41f203b7505f1fb/node_modules/playwright/index.mjs`
74 (browsers in `~/Library/Caches/ms-playwright`); `npx playwright` works too.
75
76Worked example (provision + 14-assertion drive): the scripts from the
77send-waits-for-upload verification β€” re-create from this recipe.
78
79## ⚠️ Fixtures must never be a fresh space's first boot
80
81The FIRST member to boot a fresh instance mints its shared instance key
82(#785). A throwaway harness member doing that β€” then vanishing β€” orphans the
83key: real members who join later stay keyless forever (invites break, names
84don't decrypt). So when a canary will be handed to a person afterwards, let
85THEM claim first; or reset the instance DB before handover; or check
86`GET /api/instance-key` shows your fixture isn't the sole holder you're about
87to destroy.
88