gitoriaLog in with ident

calendar

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Main branchmain2881119fcalendar 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.pymremain/tests/realdata-baseline.mjs

14.6 KB

  1. // tests/realdata-baseline.mjs (calendar mission 001, code order; after notes 002 / gitoria 002): every read the app answers
  2. // + a fixed write sequence through the faces, on a COPY of the LIVE storage, saved for diffing two code trees (a cleanup must
  3. // answer the same). Compare two outputs with tests/realdata-compare.py. README "Test" → "Same output".
  4. // node tests/realdata-baseline.mjs <tree> <port> <outdir> (kills only the server it started)
  5. // The live copy: CALENDAR_REALDATA (default .scratch/realdata) must hold storage/mpackdb, e.g. from Byrodin:
  6. // tar -C /CONTAINERS/projects/calendar.worldapi.org -cf - storage/mpackdb | ssh loreana tar -C <repo>/.scratch/realdata -xf -
  7. // Each run works on a fresh copy of it (<outdir>.run, removed after). Signed in as the creator (identity az5b2 → users @id
  8. // 0muh8y8myb5z) through a session file written before the start; every page and load is fetched signed in AND out.
  9. // ident is a FAKE on <port>+5 (POST /api/exchange: code abcdef01 → az5b2, abcdef02 → an invalid id, abcdef03 → 403,
  10. // else 400). The public URL is the same for every port (http://calendar.test), so two runs on two ports can be compared.
  11. import { spawn } from 'node:child_process';
  12. import { request, createServer } from 'node:http';
  13. import { writeFileSync, mkdirSync, rmSync, cpSync } from 'node:fs';
  14. import { createHash, randomBytes } from 'node:crypto';
  15. import { join, resolve } from 'node:path';
  16. const [tree, portArg, outArg] = process.argv.slice(2);
  17. const PORT = Number(portArg);
  18. const W = resolve(process.env.CALENDAR_REALDATA || '.scratch/realdata'), OUT = resolve(outArg), RUN = OUT + '.run';
  19. const USER = '0muh8y8myb5z';
  20. rmSync(RUN, { recursive: true, force: true }); rmSync(OUT, { recursive: true, force: true });
  21. mkdirSync(join(RUN, 'sessions'), { recursive: true }); mkdirSync(OUT, { recursive: true });
  22. cpSync(join(W, 'storage'), join(RUN, 'storage'), { recursive: true });
  23. const now = Date.now();
  24. const session = (user, tag) => {
  25. const sid = randomBytes(16).toString('hex');
  26. writeFileSync(join(RUN, 'sessions', createHash('sha256').update(sid).digest('hex')),
  27. JSON.stringify({ created: now, data: { tag }, handled: [], id: sid, seen: now, user }), { mode: 0o600 });
  28. return 'calendarsid=' + sid;
  29. };
  30. const COOKIE = session({ id: USER }, 'rdtag1'), C2 = session(null, 'rdtag2');
  31. const sleep = ms => new Promise(r => setTimeout(r, ms));
  32. const HOST = `calendar.test:${PORT}`;
  33. const IDENT = `http://127.0.0.1:${PORT + 5}`;
  34. const fakeIdent = createServer((req, res) => {
  35. let body = ''; req.on('data', d => body += d); req.on('end', () => {
  36. let j = {}; try { j = JSON.parse(body); } catch {}
  37. const answer = (status, v) => { res.writeHead(status, { 'content-type': 'application/json' }); res.end(JSON.stringify(v)); };
  38. if (req.method !== 'POST' || req.url !== '/api/exchange') return answer(404, { error: 'fake ident: not here' });
  39. if (j.key !== 'pk_rd' || j.secret !== 'sk_rd') return answer(403, { error: 'bad key' });
  40. if (j.code === 'abcdef01') return answer(200, { identity: 'az5b2' });
  41. if (j.code === 'abcdef02') return answer(200, { identity: 'Not Valid!' });
  42. if (j.code === 'abcdef03') return answer(403, { error: 'code expired' });
  43. return answer(400, { error: 'unknown code' });
  44. });
  45. });
  46. fakeIdent.listen(PORT + 5, '127.0.0.1');
  47. const srv = spawn(resolve(tree, 'bin/hybriel'), ['project.hl'], { cwd: resolve(tree), stdio: ['ignore', 'pipe', 'pipe'],
  48. env: { ...process.env, CALENDAR_PORT: String(PORT), CALENDAR_PUBLIC_URL: 'http://calendar.test', CALENDAR_STORAGE: join(RUN, 'storage/mpackdb'),
  49. CALENDAR_SESSIONS: join(RUN, 'sessions') + '/', CALENDAR_WATCH: '0', IDENT_URL: 'http://ident.test', IDENT_EXCHANGE_URL: IDENT,
  50. IDENT_API_KEY: 'pk_rd', IDENT_API_SECRET: 'sk_rd' } });
  51. let log = ''; srv.stdout.on('data', d => log += d); srv.stderr.on('data', d => log += d);
  52. const hreq = (method, path, headers = {}, body = null) => new Promise((res) => {
  53. const rq = request({ host: '127.0.0.1', port: PORT, path, method, headers: { host: HOST, ...headers } }, (r) => {
  54. 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') }));
  55. });
  56. rq.on('error', (e) => res({ status: 0, headers: {}, body: 'ERROR ' + e.message }));
  57. if (body != null) { rq.setHeader('content-length', Buffer.byteLength(body)); rq.write(body); }
  58. rq.end();
  59. });
  60. let n = 0;
  61. const file = (name, text) => writeFileSync(join(OUT, String(++n).padStart(4, '0') + '-' + name.replace(/[^a-zA-Z0-9._-]+/g, '_').slice(0, 120)), text);
  62. const save = async (name, method, path, headers = {}, body = null) => {
  63. const r = await hreq(method, path, headers, body);
  64. const head = [r.status, r.headers['content-type'] || '', r.headers['location'] || '', r.headers['cache-control'] || '', r.headers['set-cookie'] ? 'set-cookie' : ''].join(' | ');
  65. file(name, head + '\n' + r.body);
  66. return r;
  67. };
  68. let ei = 0;
  69. const emitF = async (name, event, payload, cookie = COOKIE) => {
  70. const r = await hreq('POST', '/__hl/emit', { 'content-type': 'application/json', cookie }, JSON.stringify({ t: 'emit', i: ++ei, event, payload }));
  71. file(name, r.status + '\n' + r.body);
  72. try { return JSON.parse(r.body).value; } catch { return null; }
  73. };
  74. const ev = (o) => ({ title: 'rd', allDay: false, startDate: '2026-10-15', startTime: '09:00', endDate: '', endTime: '', place: '', notes: '', zone: 'Europe/Vienna', ...o });
  75. try {
  76. for (let i = 0; i < 240; i++) { if ((await hreq('GET', '/login.js')).status === 200) break; await sleep(500); }
  77. // ---- READS ------------------------------------------------------------------------------------------------------
  78. const pages = ['/', '/login/failed', '/nope', '/login.js', '/favicon.ico', '/icons/icon-192.png', '/icons/icon-512.png',
  79. '/__hl/manifest.webmanifest', '/__hl/sw.js'];
  80. const modules = new Set();
  81. for (const p of pages) {
  82. const r = await save('in-' + p, 'GET', p, { cookie: COOKIE }); await save('out-' + p, 'GET', p);
  83. for (const m of r.body.matchAll(/(\/(?:components|__hl)\/[\w./-]+\.(?:hl|js|css))\?v=[0-9a-f]+/g)) modules.add(m[0]);
  84. }
  85. await save('in-de', 'GET', '/', { cookie: COOKIE, 'accept-language': 'de-AT,de;q=0.9' });
  86. await save('in-fr', 'GET', '/', { cookie: COOKIE, 'accept-language': 'fr' });
  87. for (const m of [...modules].sort()) await save('module-' + m.replace(/\?v=.*/, ''), 'GET', m);
  88. // the faces' reads: every window that holds a live event (all of them first), the defaults, refusals, signed out
  89. const all = await emitF('load-all', 'calLoad', ['2000-01-01', '2100-12-31']);
  90. const months = [...new Set(((all && all.events) || []).map(e => e.first.slice(0, 7)))].sort();
  91. for (const m of months) await emitF('load-' + m, 'calLoad', [m + '-01', m + '-28']);
  92. 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']])
  93. await emitF(`load-${a}-${b}`, 'calLoad', [a, b]);
  94. await emitF('load-out', 'calLoad', ['2026-10-01', '2026-10-31'], 'calendarsid=none');
  95. await emitF('settings-load', 'calSettingsLoad', []);
  96. await emitF('settings-load-out', 'calSettingsLoad', [], 'calendarsid=none');
  97. // the login's function route (the fake ident)
  98. 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);
  99. await save('callback-post', 'POST', '/login/callback', {}, 'x');
  100. await save('page-loginfailed-after', 'GET', '/login/failed');
  101. await save('callback-cookie', 'GET', '/login/callback?ident_code=abc123', { cookie: C2 });
  102. await save('page-loginfailed-cookie', 'GET', '/login/failed', { cookie: C2 });
  103. await save('callback-cookie-nocode', 'GET', '/login/callback', { cookie: C2 });
  104. await save('page-loginfailed-cookie2', 'GET', '/login/failed', { cookie: C2 });
  105. await save('callback-cookie-expired', 'GET', '/login/callback?ident_code=abcdef03', { cookie: C2 });
  106. await save('page-loginfailed-cookie3', 'GET', '/login/failed', { cookie: C2 });
  107. // ---- WRITES ('w-' files; realdata-compare.py masks the new ids and the times) ---------------------------------------
  108. const W1 = ['2026-09-28', '2026-11-08'];
  109. const a = await emitF('w-save-appt', 'calSave', [ev({ title: ' rd appointment ', place: ' Wien ', notes: 'line\nline 2', endTime: '10:30' }), ...W1]);
  110. await emitF('w-save-allday', 'calSave', [ev({ title: 'rd allday', allDay: true, startTime: '', endDate: '2026-10-17' }), ...W1]);
  111. await emitF('w-save-allday-one', 'calSave', [ev({ title: 'rd one day', allDay: true, startDate: '2026-10-20', startTime: '', endDate: '2026-10-20' }), ...W1]);
  112. 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]);
  113. 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]);
  114. await emitF('w-save-custom-none', 'calSave', [ev({ title: 'rd no reminders', startDate: '2026-10-06', custom: true, reminders: [] }), ...W1]);
  115. for (const [name, o] of [['notitle', { title: ' ' }], ['longtitle', { title: 'x'.repeat(201) }], ['longplace', { place: 'x'.repeat(201) }],
  116. ['longnotes', { notes: 'x'.repeat(5001) }], ['badallday', { allDay: 'yes' }], ['baddate', { startDate: '2026-02-30x' }], ['nozone', { zone: '' }],
  117. ['badtime', { startTime: '25:00' }], ['endbefore', { endDate: '2026-10-14', endTime: '09:00' }], ['endnotime', { endDate: '2026-10-16', endTime: '' }],
  118. ['alldayendbefore', { allDay: true, endDate: '2026-10-01' }], ['alldaybadend', { allDay: true, endDate: 'x' }],
  119. ['badrem', { custom: true, reminders: [{ unit: 'weeks', n: '1' }] }], ['remnum', { custom: true, reminders: [{ unit: 'minutes', n: '-1' }] }],
  120. ['remmany', { custom: true, reminders: Array(9).fill({ unit: 'minutes', n: '5' }) }], ['remnotlist', { custom: true, reminders: 'x' }],
  121. ['remdaytime', { custom: true, reminders: [{ unit: 'days', n: '1', time: 'x' }] }], ['nosuchid', { id: 'nosuch' }]])
  122. await emitF('w-save-' + name, 'calSave', [ev(o), ...W1]);
  123. await emitF('w-save-notobject', 'calSave', ['x', ...W1]);
  124. await emitF('w-save-badwindow', 'calSave', [ev({}), 'x', 'y']);
  125. await emitF('w-save-out', 'calSave', [ev({}), ...W1], 'calendarsid=none');
  126. const aid = a && a.events ? (a.events.find(e => e.title === 'rd appointment') || {}).id : 'none';
  127. await emitF('w-edit', 'calSave', [ev({ id: aid, title: 'rd appointment changed', startTime: '11:00' }), ...W1]);
  128. // offline: a queued save with a temporary id, then an edit and a delete with the same id
  129. await emitF('w-queued', 'calSaveQueued', [ev({ id: 'local-rd1', title: 'rd queued', startDate: '2026-10-25' }), ...W1]);
  130. await emitF('w-queued-again', 'calSaveQueued', [ev({ id: 'local-rd1', title: 'rd queued 2', startDate: '2026-10-25' }), ...W1]);
  131. await emitF('w-queued-bad', 'calSaveQueued', [ev({ title: '' }), ...W1]);
  132. await emitF('w-queued-notobject', 'calSaveQueued', [null, ...W1]);
  133. await emitF('w-queued-out', 'calSaveQueued', [ev({}), ...W1], 'calendarsid=none');
  134. await emitF('w-edit-local', 'calSave', [ev({ id: 'local-rd1', title: 'rd queued 3', startDate: '2026-10-26' }), ...W1]);
  135. await emitF('w-delete-local', 'calDelete', ['local-rd1', ...W1]);
  136. await emitF('w-delete-local-again', 'calDelete', ['local-rd1', ...W1]);
  137. await emitF('w-delete', 'calDelete', [aid, ...W1]);
  138. await emitF('w-delete-again', 'calDelete', [aid, ...W1]);
  139. await emitF('w-delete-nosuch', 'calDelete', ['nosuch', ...W1]);
  140. await emitF('w-delete-null', 'calDelete', [null, ...W1]);
  141. await emitF('w-delete-badwindow', 'calDelete', [aid, 'x', null]);
  142. await emitF('w-delete-out', 'calDelete', [aid, ...W1], 'calendarsid=none');
  143. // a live event: changed, then deleted
  144. const live = ((all && all.events) || [])[0];
  145. if (live) {
  146. 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]);
  147. await emitF('w-delete-live', 'calDelete', [live.id, live.first, live.last]);
  148. }
  149. // the defaults
  150. await emitF('w-settings-save', 'calSettingsSave', [[{ unit: 'minutes', n: '15', time: '09:00' }, { unit: 'days', n: '0', time: '07:30' }]]);
  151. await emitF('w-settings-save-again', 'calSettingsSave', [[{ unit: 'hours', n: '2', time: '' }]]);
  152. await emitF('w-settings-save-bad', 'calSettingsSave', [[{ unit: 'hours', n: '999' }]]);
  153. await emitF('w-settings-save-notlist', 'calSettingsSave', ['x']);
  154. await emitF('w-settings-save-out', 'calSettingsSave', [[]], 'calendarsid=none');
  155. await emitF('w-settings-load', 'calSettingsLoad', []);
  156. await emitF('w-load-all', 'calLoad', ['2000-01-01', '2100-12-31']);
  157. await save('w-page-main', 'GET', '/', { cookie: COOKIE });
  158. // logins that work: the button's return (fresh browser, with next; a signed-out session) and the selector's face
  159. for (const q of ['', '&next=/x', '&next=//evil', '&next=/login/failed', '&next=/a<b']) {
  160. const r = await save('w-callback-ok' + q, 'GET', '/login/callback?ident_code=abcdef01' + q);
  161. const ck = (r.headers['set-cookie'] || [''])[0].split(';')[0];
  162. 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); }
  163. }
  164. await save('w-callback-ok-cookie', 'GET', '/login/callback?ident_code=abcdef01&next=/y', { cookie: C2 });
  165. await emitF('w-callback-ok-cookie-load', 'calLoad', ['2026-10-01', '2026-10-31'], C2);
  166. await emitF('w-face-login-bad', 'calendarLogin', ['zz']);
  167. await emitF('w-face-login-hex', 'calendarLogin', ['abc123']);
  168. await emitF('w-face-login-invalid', 'calendarLogin', ['abcdef02']);
  169. const C3 = session(null, 'rdtag3');
  170. await emitF('w-face-login-ok', 'calendarLogin', ['abcdef01'], C3);
  171. await emitF('w-face-login-ok-load', 'calLoad', ['2026-10-01', '2026-10-31'], C3);
  172. await emitF('w-face-logout', 'calendarLogOut', []);
  173. await save('w-page-main-loggedout', 'GET', '/', { cookie: COOKIE });
  174. await emitF('w-load-loggedout', 'calLoad', ['2026-10-01', '2026-10-31']);
  175. console.log(`${n} responses saved to ${OUT} (${pages.length} pages x2, ${months.length} event months, + writes)`);
  176. } finally {
  177. fakeIdent.close();
  178. srv.kill('SIGTERM'); await sleep(1000); try { process.kill(srv.pid, 'SIGKILL'); } catch {}
  179. }
  180. writeFileSync(join(OUT, '0000-server-log'), log.split('\n').filter(l => !/listening|watch/i.test(l)).join('\n'));
  181. rmSync(RUN, { recursive: true, force: true });

Branches

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