notes
All repositories: gitoria
13.6 KB
// tests/realdata-baseline.mjs (notes mission 002, code order; after gitoria's): every read the app answers + a fixed write// sequence, on a COPY of the LIVE storage, saved for diffing two code trees (a cleanup must answer the same). Compare two// outputs with tests/realdata-compare.py. README "Test" → "Same output".// node tests/realdata-baseline.mjs <tree> <port> <outdir> (kills only the server it started)// The live copy: NOTES_REALDATA (default .scratch/realdata) must hold storage/mpackdb, e.g. from Byrodin:// tar -C /CONTAINERS/projects/notes.worldapi.org -cf - storage/mpackdb | ssh loreana tar -C <repo>/.scratch/realdata -xf -// Each run works on a fresh copy of it (<outdir>.run, removed after). Signed in as the creator (identity az5b2 → users @id// 0muh6o5a3kkd) through a session file written before the start; every page and API read is fetched signed in AND out.// ident is a FAKE on <port>+5 (POST /api/exchange: code abcdef01 → az5b2, abcdef02 → an invalid id, abcdef03 → 403,// else 400); the login routes and faces answer what they answer for that. The public URL is the same for every port// (http://notes.test), so two runs on two ports can be compared.import { spawn } from 'node:child_process';import { request, createServer } from 'node:http';import { writeFileSync, mkdirSync, rmSync, cpSync } from 'node:fs';import { createHash, randomBytes } from 'node:crypto';import { join, resolve } from 'node:path';const [tree, portArg, outArg] = process.argv.slice(2);const PORT = Number(portArg);const W = resolve(process.env.NOTES_REALDATA || '.scratch/realdata'), OUT = resolve(outArg), RUN = OUT + '.run';const USER = '0muh6o5a3kkd';rmSync(RUN, { recursive: true, force: true }); rmSync(OUT, { recursive: true, force: true });mkdirSync(join(RUN, 'sessions'), { recursive: true }); mkdirSync(OUT, { recursive: true });cpSync(join(W, 'storage'), join(RUN, 'storage'), { recursive: true });// ---- the sessions: signed in, and a signed-out one (for the failed-login reason) -----------------------------------const now = Date.now();const session = (user, tag) => {const sid = randomBytes(16).toString('hex');writeFileSync(join(RUN, 'sessions', createHash('sha256').update(sid).digest('hex')),JSON.stringify({ created: now, data: { tag }, handled: [], id: sid, seen: now, user }), { mode: 0o600 });return 'notessid=' + sid;};const COOKIE = session({ id: USER }, 'rdtag1'), C2 = session(null, 'rdtag2');const sleep = ms => new Promise(r => setTimeout(r, ms));const HOST = `notes.test:${PORT}`;const IDENT = `http://127.0.0.1:${PORT + 5}`;const fakeIdent = createServer((req, res) => {let body = ''; req.on('data', d => body += d); req.on('end', () => {let j = {}; try { j = JSON.parse(body); } catch {}const answer = (status, v) => { res.writeHead(status, { 'content-type': 'application/json' }); res.end(JSON.stringify(v)); };if (req.method !== 'POST' || req.url !== '/api/exchange') return answer(404, { error: 'fake ident: not here' });if (j.key !== 'pk_rd' || j.secret !== 'sk_rd') return answer(403, { error: 'bad key' });if (j.code === 'abcdef01') return answer(200, { identity: 'az5b2' });if (j.code === 'abcdef02') return answer(200, { identity: 'Not Valid!' });if (j.code === 'abcdef03') return answer(403, { error: 'code expired' });return answer(400, { error: 'unknown code' });});});fakeIdent.listen(PORT + 5, '127.0.0.1');const srv = spawn(resolve(tree, 'bin/hybriel'), ['project.hl'], { cwd: resolve(tree), stdio: ['ignore', 'pipe', 'pipe'],env: { ...process.env, NOTES_PORT: String(PORT), NOTES_PUBLIC_URL: 'http://notes.test', NOTES_STORAGE: join(RUN, 'storage/mpackdb'),NOTES_SESSIONS: join(RUN, 'sessions') + '/', NOTES_WATCH: '0', IDENT_URL: 'http://ident.test', IDENT_EXCHANGE_URL: IDENT,IDENT_API_KEY: 'pk_rd', IDENT_API_SECRET: 'sk_rd' } });let log = ''; srv.stdout.on('data', d => log += d); srv.stderr.on('data', d => log += d);const hreq = (method, path, headers = {}, body = null) => new Promise((res) => {const rq = request({ host: '127.0.0.1', port: PORT, path, method, headers: { host: HOST, ...headers } }, (r) => {const parts = []; r.on('data', d => parts.push(d)); r.on('end', () => res({ status: r.statusCode, headers: r.headers, body: Buffer.concat(parts).toString('utf8') }));});rq.on('error', (e) => res({ status: 0, headers: {}, body: 'ERROR ' + e.message }));if (body != null) { rq.setHeader('content-length', Buffer.byteLength(body)); rq.write(body); }rq.end();});let n = 0;const file = (name, text) => writeFileSync(join(OUT, String(++n).padStart(4, '0') + '-' + name.replace(/[^a-zA-Z0-9._-]+/g, '_').slice(0, 120)), text);const save = async (name, method, path, headers = {}, body = null) => {const r = await hreq(method, path, headers, body);const head = [r.status, r.headers['content-type'] || '', r.headers['location'] || '', r.headers['cache-control'] || '', r.headers['set-cookie'] ? 'set-cookie' : ''].join(' | ');file(name, head + '\n' + r.body);return r;};let ei = 0;const emitF = async (name, event, payload, cookie = COOKIE) => {const r = await hreq('POST', '/__hl/emit', { 'content-type': 'application/json', cookie }, JSON.stringify({ t: 'emit', i: ++ei, event, payload }));file('w-face-' + name, r.status + '\n' + r.body);try { return JSON.parse(r.body).value; } catch { return null; }};const JH = { 'content-type': 'application/json', cookie: COOKIE };const post = async (name, body, cookie = COOKIE) => {const r = await save(name, 'POST', '/api/notes', { 'content-type': 'application/json', cookie }, typeof body === 'string' ? body : JSON.stringify(body));try { return JSON.parse(r.body); } catch { return {}; }};try {for (let i = 0; i < 240; i++) { if ((await hreq('GET', '/api/me')).status === 200) break; await sleep(500); }const list = JSON.parse((await save('api-notes', 'GET', '/api/notes', { cookie: COOKIE })).body).notes;// ---- READS ------------------------------------------------------------------------------------------------------const pages = ['/', '/note/nosuch', '/note/', '/login/failed', '/nope', '/login.js', '/sw.js', '/notes-offline.js', '/md-editor.js','/favicon.ico', '/icons/icon-192.png', '/icons/icon-512.png', '/icons/apple-touch-icon.png', '/icons/favicon.svg','/__hl/manifest.webmanifest', '/__hl/sw.js'];for (const x of list) pages.push('/note/' + x.id);const modules = new Set();for (const p of pages) {const r = await save('in-' + p, 'GET', p, { cookie: COOKIE }); await save('out-' + p, 'GET', p);for (const m of r.body.matchAll(/(\/(?:components|__hl)\/[\w./-]+\.(?:hl|js|css))\?v=[0-9a-f]+/g)) modules.add(m[0]);}for (const m of [...modules].sort()) await save('module-' + m.replace(/\?v=.*/, ''), 'GET', m);const apis = ['/api/me', '/api/notes', '/api/notes?id=nosuch', '/api/notes?id='];for (const x of list) apis.push('/api/notes?id=' + x.id);for (const u of apis) { await save('api-in' + u, 'GET', u, { cookie: COOKIE }); await save('api-out' + u, 'GET', u); }// the refusals of POST /api/notesfor (const [name, body] of [['empty', ''], ['badjson', '{"op":'], ['surrogate', '{"op":"save","text":"\\ud83d\\ude00"}'], ['array', '[1,2]'],['string', '"x"'], ['null', 'null'], ['noop', '{}'], ['badop', '{"op":"rename"}'],['save-nofields', '{"op":"save"}'], ['save-numid', '{"op":"save","id":1,"local":"","text":"","base":0,"at":0}'],['save-negbase', '{"op":"save","id":"","local":"","text":"","base":-1,"at":0}'],['save-longid', JSON.stringify({ op: 'save', id: 'x'.repeat(65), local: '', text: 'a', base: 0, at: 0 })],['save-longlocal', JSON.stringify({ op: 'save', id: '', local: 'l'.repeat(65), text: 'a', base: 0, at: 0 })],['save-toolong', JSON.stringify({ op: 'save', id: '', local: 'lrdlong', text: 'x'.repeat(200001), base: 0, at: 0 })],['delete-nofields', '{"op":"delete"}'], ['delete-strbase', '{"op":"delete","id":"x","base":"1"}']])await post('post-' + name, body);await save('post-out', 'POST', '/api/notes', { 'content-type': 'application/json' }, '{"op":"save"}');await save('put', 'PUT', '/api/notes', JH, '{}');await save('delete-method', 'DELETE', '/api/notes', JH);await save('me-post', 'POST', '/api/me', JH, '{}');// the login's function route (the fake ident)for (const q of ['', '?ident_code=zz', '?ident_code=abc123', '?ident_code=abcdef02', '?ident_code=abcdef03', '?ident_code=abc&next=/note/x']) await save('callback' + q, 'GET', '/login/callback' + q);await save('callback-post', 'POST', '/login/callback', {}, 'x');await save('page-loginfailed-after', 'GET', '/login/failed');await save('callback-cookie', 'GET', '/login/callback?ident_code=abc123', { cookie: C2 });await save('page-loginfailed-cookie', 'GET', '/login/failed', { cookie: C2 });await save('callback-cookie-nocode', 'GET', '/login/callback', { cookie: C2 });await save('page-loginfailed-cookie2', 'GET', '/login/failed', { cookie: C2 });await save('callback-cookie-get-only', 'POST', '/login/callback', { cookie: C2 }, 'x');await save('page-loginfailed-cookie3', 'GET', '/login/failed', { cookie: C2 });await save('callback-cookie-expired', 'GET', '/login/callback?ident_code=abcdef03', { cookie: C2 });await save('page-loginfailed-cookie4', 'GET', '/login/failed', { cookie: C2 });// a login that works (the fake ident knows abcdef01 = az5b2): fresh browser → a new session; `next` → back to that pathfor (const q of ['', '&next=/note/x', '&next=//evil', '&next=/login/failed', '&next=/a%20b', '&next=/a<b', '&next=x']) {const r = await save('w-callback-ok' + q, 'GET', '/login/callback?ident_code=abcdef01' + q);const ck = (r.headers['set-cookie'] || [''])[0].split(';')[0];if (q === '') { await save('w-callback-ok-page', 'GET', '/', { cookie: ck }); await save('w-callback-ok-me', 'GET', '/api/me', { cookie: ck }); }}// the same in a browser that has a signed-out session: that session is signed in (no new cookie)await save('w-callback-ok-cookie', 'GET', '/login/callback?ident_code=abcdef01&next=/note/y', { cookie: C2 });await save('w-callback-ok-cookie-me', 'GET', '/api/me', { cookie: C2 });await save('w-callback-ok-cookie-failed', 'GET', '/login/failed', { cookie: C2 });// ---- WRITES ('w-' files; realdata-compare.py masks the new ids and the times) ---------------------------------------const a = await post('w-save-new', { op: 'save', id: '', local: 'lrd1', text: '# rd one\n\nbody', base: 0, at: 1700000000000 });await post('w-save-retry', { op: 'save', id: '', local: 'lrd1', text: '# rd one\n\nbody', base: 0, at: 1700000000000 });await post('w-save-same', { op: 'save', id: a.id, local: '', text: '# rd one\n\nbody', base: a.edited, at: 1700000001000 });const b = await post('w-save-edit', { op: 'save', id: a.id, local: '', text: '# rd one\n\nbody 2', base: a.edited, at: 1700000002000 });// conflict: base is old; the push is LATER than the server's version → the push wins, the server's version is a copyconst c = await post('w-save-conflict-later', { op: 'save', id: a.id, local: '', text: 'rd later', base: a.edited, at: Date.now() - 1000 });// conflict: the push is OLDER → the server keeps its text, the push becomes a copyawait post('w-save-conflict-older', { op: 'save', id: a.id, local: '', text: 'rd older', base: a.edited, at: 1600000000000 });await post('w-save-unknownid', { op: 'save', id: 'nosuch', local: '', text: 'rd unknown id', base: 5, at: 0 });await post('w-save-real', { op: 'save', id: list.find(x => !x.gone)?.id || 'none', local: '', text: 'rd overwrite?', base: 1, at: 1 });await post('w-save-out', { op: 'save', id: '', local: 'lrd2', text: 'x', base: 0, at: 0 }, 'notessid=none');await post('w-delete-wrongbase', { op: 'delete', id: a.id, base: 1 });await post('w-delete', { op: 'delete', id: a.id, base: c.edited ?? b.edited });await post('w-delete-again', { op: 'delete', id: a.id, base: 0 });await post('w-delete-nosuch', { op: 'delete', id: 'nosuch', base: 0 });await post('w-delete-empty', { op: 'delete', id: '', base: 0 });await post('w-save-after-delete', { op: 'save', id: a.id, local: '', text: 'rd back', base: 0, at: 0 });await post('w-save-oldlocal-again', { op: 'save', id: '', local: 'lrd1', text: 'rd local again', base: 0, at: 0 });await save('w-api-notes', 'GET', '/api/notes', { cookie: COOKIE });await save('w-api-one', 'GET', '/api/notes?id=' + a.id, { cookie: COOKIE });await save('w-page-main', 'GET', '/', { cookie: COOKIE });await save('w-page-note', 'GET', '/note/' + a.id, { cookie: COOKIE });// the faces: login with a code (ident down), logout, then the pages againawait emitF('login-bad', 'notesLogin', ['zz']);await emitF('login-hex', 'notesLogin', ['abc123']);await emitF('login-invalid', 'notesLogin', ['abcdef02']);await emitF('login-out', 'notesLogin', ['abc123'], 'notessid=none');const C3 = session(null, 'rdtag3');await emitF('login-ok', 'notesLogin', ['abcdef01'], C3);await save('w-login-ok-me', 'GET', '/api/me', { cookie: C3 });await save('w-login-ok-page', 'GET', '/', { cookie: C3 });await emitF('logout', 'notesLogOut', []);await save('w-page-main-loggedout', 'GET', '/', { cookie: COOKIE });await save('w-api-me-loggedout', 'GET', '/api/me', { cookie: COOKIE });await save('w-api-notes-loggedout', 'GET', '/api/notes', { cookie: COOKIE });console.log(`${n} responses saved to ${OUT} (${pages.length} pages x2, ${apis.length} api urls x2, + writes)`);} finally {fakeIdent.close();srv.kill('SIGTERM'); await sleep(1000); try { process.kill(srv.pid, 'SIGKILL'); } catch {}}writeFileSync(join(OUT, '0000-server-log'), log.split('\n').filter(l => !/listening|watch/i.test(l)).join('\n'));rmSync(RUN, { recursive: true, force: true });
Branches
- mainmain branch
Latest commits
- 2149e902notes mission 002 (4/4): code order — README file map + same-output test, STATUS, LOG, report; tests/letcount.py, tests/realdata-baseline.mjs, tests/realdata-compare.pymre
- 47db4fc1notes mission 002 (3/4): code order — let only where reassigned (58 dropped; 34 left: 19 reassigned, 15 loop-bound); gates 50/0 + 18/0, live-data run = step 2mre
- 3f8cb383notes mission 002 (2/4): code order — topics, map, thin wrappers: lib/util.hl, lib/notes(-helpers).hl, lib/users.hl (+userOfLoginCode, tagOf), lib/api(-helpers).hl; project.hl = map; login routes take &req/&sessions (failed-login reason now kept); dead notes#1 functions removed; gates 50/0 + 18/0mre
- 8cda5412notes mission 002 (1/4): code order — files moved: lib/notes.hl, lib/users.hl, lib/jsoncheck.hl, components/styles.hl (imports only); gates 50/0 + 18/0, live-data run identicalmre
- 47dad68bnotes: Hybriel master 06617221 (plugin allocators 3a781359 + 413f60e4, mpackdb 2cb7ae5e, http1 773de63e); gates 50/0 + 18/0mre
- a4a2b2aeantcolony#40: tracker missions moved too — references to them in missions/reports/LOG.md updatedmre
- 3dea0ef2notes: Hybriel master 190aa11d (fc838894 GC correctness, #126 closure scopes, #127); gate 50/0mre
- e27c7d71notes: Hybriel master 8efba065 (#126 GC by bytes, #48 lambda params copy; audit: nothing to fix; gate 50/0)mre
- 124613b6antcolony#40: mission references point to the moved missionsmre
- 1ca2f34dantcolony#40: history (LOG.md), worker briefs (missions/) and reports moved here from antcolony, numbered per project; old numbers in antcolony docs/mission-map.mdmre
- 2bebebdanotes: Hybriel master ff51cf46 (re-vendor round)mre
- 3eff126dnotes#3: installable app (manifest + own icon/favicon; notes' own sw.js kept)mre
- 9883c540deploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
- eee693b8deploy.sh: never send .git or .gitignore to Byrodinmre
- c8904061State of 2026-09-27, before the move to gitoriamre