tickets
All repositories: gitoria
11.8 KB
// tests/realdata-baseline.mjs (mission 010): 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 (reads) and tests/realdata-compare-writes.py (the 'w-' files). README "Test".// node tests/realdata-baseline.mjs <tree> <port> <outdir> (run in tickets.worldapi.org; kills only the server it started)// The live copy: TICKETS_REALDATA (default .scratch/realdata) must hold storage/mpackdb, e.g. from Byrodin:// tar -C /CONTAINERS/projects/tickets.worldapi.org -cf - storage/mpackdb | ssh loreana tar -C <repo>/.scratch/realdata -xf -// Signed in as the creator (identity az5b2 → users @id 0mufikk6hj1u) through a session file written before the start;// every HTML page is fetched signed in AND signed out. Writes nothing to the live data (a fresh copy per run).import { spawn, execSync } from 'node:child_process';import { writeFileSync, mkdirSync, rmSync } from 'node:fs';import { createHash, randomBytes } from 'node:crypto';import { join, resolve } from 'node:path';const [tree, port, outArg] = process.argv.slice(2);const W = resolve(process.env.TICKETS_REALDATA || '.scratch/realdata'), OUT = resolve(outArg), RUN = OUT + '.run';rmSync(RUN, { recursive: true, force: true }); rmSync(OUT, { recursive: true, force: true });mkdirSync(join(RUN, 'sessions'), { recursive: true }); mkdirSync(OUT, { recursive: true });execSync(`cp -a ${W}/storage ${RUN}/storage`);const sid = randomBytes(16).toString('hex'), now = Date.now();writeFileSync(join(RUN, 'sessions', createHash('sha256').update(sid).digest('hex')),JSON.stringify({ created: now, data: { tag: 'w080tag' }, handled: [], id: sid, seen: now, user: { id: '0mufikk6hj1u' } }), { mode: 0o600 });const COOKIE = 'hlsid=' + sid;const sleep = ms => new Promise(r => setTimeout(r, ms));const BASE = `http://127.0.0.1:${port}`;const srv = spawn(resolve(tree, 'bin/hybriel'), ['project.hl'], { cwd: resolve(tree), stdio: ['ignore', 'pipe', 'pipe'],env: { ...process.env, TICKETS_PORT: port, TICKETS_STORAGE: join(RUN, 'storage/mpackdb'), TICKETS_SESSIONS: join(RUN, 'sessions'),IDENT_URL: 'http://127.0.0.1:9', IDENT_EXCHANGE_URL: 'http://127.0.0.1:9', TICKETS_PUBLIC_URL: BASE, TICKETS_CREATOR_IDENTITY: '', TICKETS_WATCH: '0', IDENT_API_KEY: 'pk_w080', IDENT_API_SECRET: '' } });let log = ''; srv.stdout.on('data', d => log += d); srv.stderr.on('data', d => log += d);let n = 0;const save = async (name, url, opts = {}) => {const r = await fetch(BASE + url, { redirect: 'manual', ...opts });const body = await r.text();const head = [r.status, r.headers.get('content-type'), r.headers.get('location') || ''].join(' | ');// the session's own cookie / the server's hashed asset urls are the same in both runs; nothing is maskedwriteFileSync(join(OUT, String(++n).padStart(4, '0') + '-' + name.replace(/[^a-zA-Z0-9._-]+/g, '_').slice(0, 120)), head + '\n' + body);return { status: r.status, body };};try {for (let i = 0; i < 240; i++) { try { if ((await fetch(BASE + '/api/projects')).ok) break; } catch {} await sleep(500); }const projects = JSON.parse((await save('api-projects', '/api/projects')).body).details;const pages = ['/', '/inbox', '/you', '/new-project', '/state/open', '/state/review', '/state/done', '/nope', '/connect/ffff', '/projects/nosuch', '/projects/nosuch/1'];const apis = ['/api/tickets', '/api/tickets?state=open', '/api/tickets?state=awaiting-creator', '/api/tickets?state=bogus', '/api/inbox','/api/projects/nosuch', '/api/tickets/99999', '/api/tickets/zzzz', '/connect', '/connect?app=x&return=https://evil.example/&state=s', '/login/callback', '/login/callback?ident_code=zz', '/tickets/1', '/tickets/38', '/tickets/9999'];for (const p of projects) {pages.push('/projects/' + p.slug, '/project/' + p.slug, '/projects/' + p.slug + '/settings', '/project/' + p.slug + '/state/open');apis.push('/api/projects/' + p.slug, '/api/projects/' + p.slug + '/tickets', '/api/projects/' + p.slug + '/tickets?state=done', '/api/projects/' + p.slug + '/connections', '/api/tickets?project=' + p.slug);const ts = JSON.parse((await save('list-' + p.slug, '/api/projects/' + p.slug + '/tickets')).body).tickets || [];for (const t of ts) { pages.push(t.href); apis.push(t.apiHref, '/api/tickets/' + t.id); if (t.oldNumber != null) apis.push('/api/tickets/' + t.oldNumber); }}for (const u of pages) { await save('in' + u, u, { headers: { cookie: COOKIE } }); await save('out' + u, u); }for (const u of apis) { await save('json' + u, u); await save('md' + u, u, { headers: { accept: 'text/markdown' } }); }// refusals that write nothing: no token, a bad token, bad JSON with a bad token (auth first), wrong methodconst posts = ['/api/tickets', '/api/projects', '/api/projects/' + projects[0].slug + '/members', '/api/connect/exchange', '/api/tickets/' + 'x/comments'];for (const u of posts) {await save('post-none' + u, u, { method: 'POST', body: '{"text":"x"}' });await save('post-badtok' + u, u, { method: 'POST', body: '{nope', headers: { authorization: 'Bearer tkt_' + '0'.repeat(48) } });await save('put' + u, u, { method: 'PUT', body: '{}' });}await save('exchange-badjson', '/api/connect/exchange', { method: 'POST', body: '{"code":' });await save('exchange-unknown', '/api/connect/exchange', { method: 'POST', body: '{"code":"abcdef"}' });// WRITES (same sequence on old and new code, each on its own fresh copy): a token over the face, then API writes and// faces as the creator; every answer and the pages / reads of what changed are saved ('w-' files; compare-writes masks// the new ids and times)let ei = 0;const emitF = async (name, event, payload) => {const r = await fetch(BASE + '/__hl/emit', { method: 'POST', headers: { 'content-type': 'application/json', cookie: COOKIE }, body: JSON.stringify({ t: 'emit', i: ++ei, event, payload }) });const body = await r.text();writeFileSync(join(OUT, String(++n).padStart(4, '0') + '-w-face-' + name), r.status + '\n' + body);try { return JSON.parse(body).value; } catch { return null; }};const tok = (await emitF('tokenCreate', 'tokenCreate', ['w080'])).token;const P = '/api/projects/tickets.worldapi.org';const send = (name, method, url, body) => save('w-' + name, url, { method, headers: { authorization: 'Bearer ' + tok, 'content-type': 'application/json' }, body: body == null ? undefined : JSON.stringify(body) });const made = JSON.parse((await send('create', 'POST', P + '/tickets', { subject: 'W080 write check', summary: '**bold** and `code`\n\n- a\n- b' })).body);const T = P + '/tickets/' + made.ticket.number, U = '/api/tickets/' + made.ticket.id;await send('create-again-source', 'POST', '/api/tickets', { project: 'tickets.worldapi.org', subject: 'W080 sourced', source: 'w080-source.md' });await send('create-again-source2', 'POST', '/api/tickets', { project: 'tickets.worldapi.org', subject: 'W080 sourced', source: 'w080-source.md' });await send('comment', 'POST', T + '/comments', { text: 'a *comment*' });await send('comment-empty', 'POST', T + '/comments', { text: ' ' });await send('comment-author', 'POST', T + '/comments', { text: 'x', author: 'me' });await send('state-progress', 'POST', T + '/state', { state: 'in progress', text: 'note' });await send('state-same', 'POST', T + '/state', { state: 'progress' });await send('state-bad', 'POST', T + '/state', { state: 'nope' });await send('state-review', 'POST', U + '/state', { state: 'awaiting creator' });await send('assign-nobody', 'POST', T + '/assign', { assignee: '' });await send('assign-again', 'POST', T + '/assign', { assignee: '' });await send('assign-unknown', 'POST', T + '/assign', { assignee: 'nobody-here' });await send('assign-me', 'POST', T + '/assign', { assignee: 'Caramboleyo' });await send('edit', 'POST', T + '/edit', { subject: 'W080 write check (edited)', summary: 'new *summary*' });await send('edit-same', 'POST', T + '/edit', { subject: 'W080 write check (edited)' });await send('parent', 'POST', T + '/parent', { parent: 'tickets.worldapi.org#1' });await send('parent-self', 'POST', T + '/parent', { parent: 'tickets.worldapi.org#' + made.ticket.number });await send('blocker-add', 'POST', U + '/blocked-by', { add: 'tickets.worldapi.org # 2' });await send('blocker-twice', 'POST', T + '/blocked-by', { add: 'tickets.worldapi.org#2' });await send('blocker-both', 'POST', T + '/blocked-by', { add: 'x', remove: 'y' });await send('blocker-remove', 'POST', T + '/blocked-by', { remove: 'tickets.worldapi.org#2' });await send('parent-remove', 'POST', T + '/parent', { parent: '' });await send('newproject', 'POST', '/api/projects', { title: 'W080 Project!', description: 'd' });await send('newproject-dup', 'POST', '/api/projects', { title: 'x', slug: 'w080-project' });await send('project-update', 'POST', '/api/projects/w080-project', { slug: 'w080-renamed', description: 'changed' });await send('project-old-slug', 'GET', '/api/projects/w080-project');await send('members-add-unknown', 'POST', '/api/projects/w080-renamed/members', { user: 'nobody-here', role: 'use' });await send('members-bad-role', 'POST', '/api/projects/w080-renamed/members', { user: 'Caramboleyo', role: 'king' });await send('members-last-admin', 'POST', '/api/projects/w080-renamed/members', { user: 'Caramboleyo', role: 'use' });await send('members-remove-last', 'POST', '/api/projects/w080-renamed/members/remove', { user: 'Caramboleyo' });await send('invite-bad-role', 'POST', '/api/projects/w080-renamed/invites', { role: 'king' });await send('invite-bad-uses', 'POST', '/api/projects/w080-renamed/invites', { role: 'use', uses: '0' });await send('invite-no-ident', 'POST', '/api/projects/w080-renamed/invites', { role: 'use' });await send('disconnect-unknown', 'POST', '/api/projects/w080-renamed/connections/remove', { id: 'zzz' });await send('inbox', 'GET', '/api/inbox');await emitF('commentOn', 'commentOn', [made.ticket.id, 'face comment']);await emitF('setTicketState', 'setTicketState', [made.ticket.id, 'done', 'closing']);await emitF('ticketRights', 'ticketRights', [made.ticket.id]);await emitF('ticketEdit-bad', 'ticketEdit', [made.ticket.id, 5, 'x']);await emitF('ticketAddBlocker', 'ticketAddBlocker', [made.ticket.id, 'tickets.worldapi.org#3']);await emitF('whatICanOpen', 'whatICanOpen', ['tickets.worldapi.org']);await emitF('myInbox', 'myInbox', []);await emitF('myInboxCount', 'myInboxCount', []);await emitF('youData', 'youData', []);await emitF('proposeSlug', 'proposeSlug', ['W080 Project!']);await emitF('settingsInvite-role', 'settingsInvite', [made.ticket.projectId, 'king', '1', '7', '']);await emitF('settingsInvite-uses', 'settingsInvite', [made.ticket.projectId, 'use', '0', '7', '']);await emitF('settingsRemove-last', 'settingsRemove', [made.ticket.projectId, '0mufikk6hj1u']);await emitF('connectChoices', 'connectChoices', []);for (const u of [T, U, '/api/tickets/' + made.ticket.id, '/api/projects/tickets.worldapi.org/tickets/1', '/api/projects/tickets.worldapi.org/tickets/2', '/api/projects/w080-renamed', '/api/projects']) {const nm = u.replace(made.ticket.id, 'NEWID'); await save('w-read' + nm, u); await save('w-md' + nm, u, { headers: { accept: 'text/markdown' } });}for (const u of [made.ticket.href, '/projects/tickets.worldapi.org/1', '/projects/tickets.worldapi.org', '/inbox', '/you', '/projects/w080-renamed/settings', '/projects/w080-project']) {await save('w-page' + u, u, { headers: { cookie: COOKIE } });}console.log(`${n} responses saved to ${OUT} (${pages.length} pages ×2, ${apis.length} api urls ×2, + writes)`);} finally { 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
- bb64d57cmission 012 (ticket #25 API part): every /api/projects/<x>/… route takes the project's id (12-char record id) as well as its slug, same answers byte for byte; tickets stay under their project, no global /api/tickets/<x>. gate 315/0 (old code: the 13 id checks fail), connect 60/0, live-data copy old vs new 1,484 identical, slug vs id 772 identicalmre
- 7538b034mission 011 (ticket #25 web part): short page URLs /<slug> and /<slug>/<number>; /projects/<slug>, /projects/<slug>/<n>, /projects/<slug>/tickets/<n> and /tickets/<ref> answer 301 (current slug); slug pages moved behind hl:web's own routes after construction (they answered /__hl/* and /components/*.hl); reserved slugs refused; API paths unchanged. gate 293/0, connect 60/0, live-data copy 187/0, API old vs new: only page links differmre
- 0369106emission 010 (code order) 4/4: README file map + import order + 'Same output' test, STATUS (entry, lessons, how to verify), LOG, report; tests/realdata-baseline.mjs + realdata-compare*.py (a cleanup answers the same on live data), tests/letcount.pymre
- a75e0279mission 010 (code order) 3/4: let only where a variable is reassigned or re-bound in a loop body (456 lets → plain declarations; Hybriel refuses a plain declaration inside a loop on its 2nd pass). gate 249/0, connect 60/0, real-data reads identical, a 50-step write sequence (API + faces) identical to the old codemre
- e9d5c618mission 010 (code order) 2/4: one lib/ file per topic — store.hl split into projects / tickets (+ relations) / events / tickets-helpers, util.hl shared helpers (env, storage dir, URLs, sorts, Vienna time), the function routes out of project.hl into lib/api.hl (thin; auth/filters/Accept in api-helpers.hl), invite + member-removal logic out of the faces/routes into invites.hl / tickets.hl; project.hl is the map. /login/callback gets req + the session store by reference. gate 249/0, connect 60/0, real-data reads identicalmre
- 97e269b5mission 010 (code order) 1/4: .hl files out of the root — lib/ (store, users, connections, invites, migrate, markdown, mdview, import = ticketfile, util = localtime, jsoncheck, api-helpers = api), tools/import.hl, components/styles.hl; import paths only. gate 249/0, connect 60/0, real-data reads identicalmre
- 38f9d10ftickets: Hybriel master 06617221 (plugin allocators 3a781359 + 413f60e4, mpackdb 2cb7ae5e, http1 773de63e); gate 249/0, connect 60/0mre
- d3db6139tickets: Hybriel master 190aa11d (fc838894 GC correctness, #126 closure scopes, #127); gate 249/0, connect 60/0mre
- bce182e3tickets: Hybriel master 7eea0d32 (#126 memory, #48 lambda copies its argument); migrate.hl lambdas take &logmre
- 4137be0fantcolony#40: mission references point to the moved missionsmre
- 9bfba36aantcolony#40: history (LOG.md), worker briefs (missions/) and reports moved here from antcolony, numbered per project; old numbers in antcolony docs/mission-map.mdmre
- c7bd2645tickets: Hybriel master 73267707 (#122 fixed); compactNow workaround removed (#110 covered)mre
- 2ab91ee9tickets: gate checks rows appear once (session sync); re-vendor to ff51cf46 stopped on hybriel#122, stays 837fe120mre
- e01c2b1dtickets#24: installable app (manifest, service worker, offline list), own icon; gate waits for the hello's pongmre
- 752fbb7fdeploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
- 38bdd5e4deploy.sh: never send .git or .gitignore to Byrodinmre
- f12fa1bcState of 2026-09-27, before the move to gitoriamre