calendar
All repositories: gitoria
14.6 KB
// tests/realdata-baseline.mjs (calendar mission 001, code order; after notes 002 / gitoria 002): every read the app answers// + a fixed write sequence through the faces, 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: CALENDAR_REALDATA (default .scratch/realdata) must hold storage/mpackdb, e.g. from Byrodin:// tar -C /CONTAINERS/projects/calendar.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// 0muh8y8myb5z) through a session file written before the start; every page and load 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 public URL is the same for every port (http://calendar.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.CALENDAR_REALDATA || '.scratch/realdata'), OUT = resolve(outArg), RUN = OUT + '.run';const USER = '0muh8y8myb5z';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 });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 'calendarsid=' + sid;};const COOKIE = session({ id: USER }, 'rdtag1'), C2 = session(null, 'rdtag2');const sleep = ms => new Promise(r => setTimeout(r, ms));const HOST = `calendar.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, CALENDAR_PORT: String(PORT), CALENDAR_PUBLIC_URL: 'http://calendar.test', CALENDAR_STORAGE: join(RUN, 'storage/mpackdb'),CALENDAR_SESSIONS: join(RUN, 'sessions') + '/', CALENDAR_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(name, r.status + '\n' + r.body);try { return JSON.parse(r.body).value; } catch { return null; }};const ev = (o) => ({ title: 'rd', allDay: false, startDate: '2026-10-15', startTime: '09:00', endDate: '', endTime: '', place: '', notes: '', zone: 'Europe/Vienna', ...o });try {for (let i = 0; i < 240; i++) { if ((await hreq('GET', '/login.js')).status === 200) break; await sleep(500); }// ---- READS ------------------------------------------------------------------------------------------------------const pages = ['/', '/login/failed', '/nope', '/login.js', '/favicon.ico', '/icons/icon-192.png', '/icons/icon-512.png','/__hl/manifest.webmanifest', '/__hl/sw.js'];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]);}await save('in-de', 'GET', '/', { cookie: COOKIE, 'accept-language': 'de-AT,de;q=0.9' });await save('in-fr', 'GET', '/', { cookie: COOKIE, 'accept-language': 'fr' });for (const m of [...modules].sort()) await save('module-' + m.replace(/\?v=.*/, ''), 'GET', m);// the faces' reads: every window that holds a live event (all of them first), the defaults, refusals, signed outconst all = await emitF('load-all', 'calLoad', ['2000-01-01', '2100-12-31']);const months = [...new Set(((all && all.events) || []).map(e => e.first.slice(0, 7)))].sort();for (const m of months) await emitF('load-' + m, 'calLoad', [m + '-01', m + '-28']);for (const [a, b] of [['2026-09-28', '2026-11-08'], ['2026-10-03', '2026-10-03'], ['nope', '2026-10-01'], ['2026-10-01', null], ['2026-13-01', '2026-13-31']])await emitF(`load-${a}-${b}`, 'calLoad', [a, b]);await emitF('load-out', 'calLoad', ['2026-10-01', '2026-10-31'], 'calendarsid=none');await emitF('settings-load', 'calSettingsLoad', []);await emitF('settings-load-out', 'calSettingsLoad', [], 'calendarsid=none');// 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=/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-expired', 'GET', '/login/callback?ident_code=abcdef03', { cookie: C2 });await save('page-loginfailed-cookie3', 'GET', '/login/failed', { cookie: C2 });// ---- WRITES ('w-' files; realdata-compare.py masks the new ids and the times) ---------------------------------------const W1 = ['2026-09-28', '2026-11-08'];const a = await emitF('w-save-appt', 'calSave', [ev({ title: ' rd appointment ', place: ' Wien ', notes: 'line\nline 2', endTime: '10:30' }), ...W1]);await emitF('w-save-allday', 'calSave', [ev({ title: 'rd allday', allDay: true, startTime: '', endDate: '2026-10-17' }), ...W1]);await emitF('w-save-allday-one', 'calSave', [ev({ title: 'rd one day', allDay: true, startDate: '2026-10-20', startTime: '', endDate: '2026-10-20' }), ...W1]);await emitF('w-save-overnight', 'calSave', [ev({ title: 'rd overnight', startDate: '2026-10-21', startTime: '22:00', endDate: '2026-10-22', endTime: '02:00', zone: 'Asia/Tokyo' }), ...W1]);await emitF('w-save-custom', 'calSave', [ev({ title: 'rd custom', startDate: '2026-10-05', custom: true, reminders: [{ unit: 'minutes', n: '30', time: '09:00' }, { unit: 'days', n: '2', time: '18:00' }, { unit: 'hours', n: '3', time: '' }] }), ...W1]);await emitF('w-save-custom-none', 'calSave', [ev({ title: 'rd no reminders', startDate: '2026-10-06', custom: true, reminders: [] }), ...W1]);for (const [name, o] of [['notitle', { title: ' ' }], ['longtitle', { title: 'x'.repeat(201) }], ['longplace', { place: 'x'.repeat(201) }],['longnotes', { notes: 'x'.repeat(5001) }], ['badallday', { allDay: 'yes' }], ['baddate', { startDate: '2026-02-30x' }], ['nozone', { zone: '' }],['badtime', { startTime: '25:00' }], ['endbefore', { endDate: '2026-10-14', endTime: '09:00' }], ['endnotime', { endDate: '2026-10-16', endTime: '' }],['alldayendbefore', { allDay: true, endDate: '2026-10-01' }], ['alldaybadend', { allDay: true, endDate: 'x' }],['badrem', { custom: true, reminders: [{ unit: 'weeks', n: '1' }] }], ['remnum', { custom: true, reminders: [{ unit: 'minutes', n: '-1' }] }],['remmany', { custom: true, reminders: Array(9).fill({ unit: 'minutes', n: '5' }) }], ['remnotlist', { custom: true, reminders: 'x' }],['remdaytime', { custom: true, reminders: [{ unit: 'days', n: '1', time: 'x' }] }], ['nosuchid', { id: 'nosuch' }]])await emitF('w-save-' + name, 'calSave', [ev(o), ...W1]);await emitF('w-save-notobject', 'calSave', ['x', ...W1]);await emitF('w-save-badwindow', 'calSave', [ev({}), 'x', 'y']);await emitF('w-save-out', 'calSave', [ev({}), ...W1], 'calendarsid=none');const aid = a && a.events ? (a.events.find(e => e.title === 'rd appointment') || {}).id : 'none';await emitF('w-edit', 'calSave', [ev({ id: aid, title: 'rd appointment changed', startTime: '11:00' }), ...W1]);// offline: a queued save with a temporary id, then an edit and a delete with the same idawait emitF('w-queued', 'calSaveQueued', [ev({ id: 'local-rd1', title: 'rd queued', startDate: '2026-10-25' }), ...W1]);await emitF('w-queued-again', 'calSaveQueued', [ev({ id: 'local-rd1', title: 'rd queued 2', startDate: '2026-10-25' }), ...W1]);await emitF('w-queued-bad', 'calSaveQueued', [ev({ title: '' }), ...W1]);await emitF('w-queued-notobject', 'calSaveQueued', [null, ...W1]);await emitF('w-queued-out', 'calSaveQueued', [ev({}), ...W1], 'calendarsid=none');await emitF('w-edit-local', 'calSave', [ev({ id: 'local-rd1', title: 'rd queued 3', startDate: '2026-10-26' }), ...W1]);await emitF('w-delete-local', 'calDelete', ['local-rd1', ...W1]);await emitF('w-delete-local-again', 'calDelete', ['local-rd1', ...W1]);await emitF('w-delete', 'calDelete', [aid, ...W1]);await emitF('w-delete-again', 'calDelete', [aid, ...W1]);await emitF('w-delete-nosuch', 'calDelete', ['nosuch', ...W1]);await emitF('w-delete-null', 'calDelete', [null, ...W1]);await emitF('w-delete-badwindow', 'calDelete', [aid, 'x', null]);await emitF('w-delete-out', 'calDelete', [aid, ...W1], 'calendarsid=none');// a live event: changed, then deletedconst live = ((all && all.events) || [])[0];if (live) {await emitF('w-edit-live', 'calSave', [{ ...ev({}), id: live.id, title: live.title + ' (rd)', allDay: live.allDay, startDate: live.startDate, startTime: live.startTime, endDate: live.endDate, endTime: live.endTime, place: live.place, notes: live.notes, zone: live.zone || 'UTC', custom: live.custom, reminders: live.rows }, live.first, live.last]);await emitF('w-delete-live', 'calDelete', [live.id, live.first, live.last]);}// the defaultsawait emitF('w-settings-save', 'calSettingsSave', [[{ unit: 'minutes', n: '15', time: '09:00' }, { unit: 'days', n: '0', time: '07:30' }]]);await emitF('w-settings-save-again', 'calSettingsSave', [[{ unit: 'hours', n: '2', time: '' }]]);await emitF('w-settings-save-bad', 'calSettingsSave', [[{ unit: 'hours', n: '999' }]]);await emitF('w-settings-save-notlist', 'calSettingsSave', ['x']);await emitF('w-settings-save-out', 'calSettingsSave', [[]], 'calendarsid=none');await emitF('w-settings-load', 'calSettingsLoad', []);await emitF('w-load-all', 'calLoad', ['2000-01-01', '2100-12-31']);await save('w-page-main', 'GET', '/', { cookie: COOKIE });// logins that work: the button's return (fresh browser, with next; a signed-out session) and the selector's facefor (const q of ['', '&next=/x', '&next=//evil', '&next=/login/failed', '&next=/a<b']) {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 emitF('w-callback-ok-load', 'calLoad', ['2026-10-01', '2026-10-31'], ck); }}await save('w-callback-ok-cookie', 'GET', '/login/callback?ident_code=abcdef01&next=/y', { cookie: C2 });await emitF('w-callback-ok-cookie-load', 'calLoad', ['2026-10-01', '2026-10-31'], C2);await emitF('w-face-login-bad', 'calendarLogin', ['zz']);await emitF('w-face-login-hex', 'calendarLogin', ['abc123']);await emitF('w-face-login-invalid', 'calendarLogin', ['abcdef02']);const C3 = session(null, 'rdtag3');await emitF('w-face-login-ok', 'calendarLogin', ['abcdef01'], C3);await emitF('w-face-login-ok-load', 'calLoad', ['2026-10-01', '2026-10-31'], C3);await emitF('w-face-logout', 'calendarLogOut', []);await save('w-page-main-loggedout', 'GET', '/', { cookie: COOKIE });await emitF('w-load-loggedout', 'calLoad', ['2026-10-01', '2026-10-31']);console.log(`${n} responses saved to ${OUT} (${pages.length} pages x2, ${months.length} event months, + 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
- 2881119fcalendar mission 001 (4/4): code order — README layout + same-output test, STATUS, LOG, report; tests/letcount.py (sub-folders), tests/realdata-baseline.mjs, tests/realdata-compare.pymre
- 3b2a4cd0calendar mission 001 (3/4): code order — let only where reassigned (147 dropped incl. components/month-view; 110 left: 67 reassigned, 43 loop-bound); gates 78/0 (3 of 4 runs; 1 known 'next month' flake) + 18/0, live-data run = step 2mre
- f5ce6b1dcalendar mission 001 (2/4): code order — topics, map, thin faces: lib/util.hl, lib/events(-helpers).hl (+calendarView/settingsView/soonOf), lib/settings.hl, lib/users.hl (+userOfLoginCode, tagOf), lib/api(-helpers).hl; project.hl = map; login route takes &req/&sessions (failed-login reason now kept); gates 78/0 + 18/0mre
- c6fbd011calendar mission 001 (1/4): code order — files moved: lib/events.hl, lib/users.hl, components/styles.hl (imports only); gates 78/0 + 18/0, live-data run identicalmre
- d6c59290calendar: Hybriel master 06617221 (plugin allocators 3a781359 + 413f60e4, mpackdb 2cb7ae5e, http1 773de63e); gates 78/0 + 18/0mre
- 7bd0337ccalendar: Hybriel master 190aa11d (fc838894 GC correctness, #126 closure scopes, #127); gates 78/0 + 18/0mre
- ff41310ccalendar: Hybriel master 8efba065 (#126 memory, #48 lambda copy; audit: no & needed)mre
- 14ba08c7antcolony#40: mission references point to the moved missionsmre
- 99c73346antcolony#40: history (LOG.md), worker briefs (missions/) and reports moved here from antcolony, numbered per project; old numbers in antcolony docs/mission-map.mdmre
- 76edaa62calendar: Hybriel master ff51cf46 (re-vendor round, static workaround removed)mre
- 90a3fc2cdeploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
- 6722b72ddeploy.sh: never send .git or .gitignore to Byrodinmre
- be099807State of 2026-09-27, before the move to gitoriamre