gitoriaLog in with ident

tickets

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commit7538b0347538b034mission 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 differmre7538b034/tests/shorturls-realdata.mjs

4.2 KB

  1. // tests/shorturls-realdata.mjs — ticket #25 on REAL data: every project and N tickets (default 20, spread over the
  2. // projects) answer on the short URLs AND on every old one (301 → the short one → 200, the right page).
  3. // Run against a server on a COPY of the live tables (README "Test" → "Short URLs on real data"):
  4. // node tests/shorturls-realdata.mjs http://127.0.0.1:8763 [N]
  5. // Also lists every slug and old slug that collides with a reserved name (lib/projects.hl reservedSlugs).
  6. import { readFileSync } from 'node:fs';
  7. import { join, dirname } from 'node:path';
  8. import { fileURLToPath } from 'node:url';
  9. const BASE = process.argv[2] || 'http://127.0.0.1:8763';
  10. const N = Number(process.argv[3] || 20);
  11. const APP = join(dirname(fileURLToPath(import.meta.url)), '..');
  12. let passes = 0, failures = 0;
  13. const check = (label, ok, detail = '') => { if (ok) passes++; else failures++; console.log(`${ok ? 'ok ' : 'FAIL'} ${label}${ok ? '' : ' — ' + detail}`); };
  14. const get = (path) => fetch(BASE + path, { redirect: 'manual' });
  15. const page = async (path) => { const r = await get(path); return { status: r.status, type: r.headers.get('content-type') || '', body: await r.text() }; };
  16. // an old URL: 301 to `to`, and `to` is the page
  17. const moves = async (from, to, want) => {
  18. const r = await get(from);
  19. check(`old ${from} → 301 ${to}`, r.status === 301 && r.headers.get('location') === to, r.status + ' ' + r.headers.get('location'));
  20. const f = await fetch(BASE + from); // followed
  21. const body = await f.text();
  22. check(`old ${from} followed → 200 ${to}, the page`, f.status === 200 && new URL(f.url).pathname === to && body.includes(want), f.status + ' ' + f.url);
  23. };
  24. const esc = (s) => s.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
  25. const reservedSrc = readFileSync(join(APP, 'lib/projects.hl'), 'utf8').match(/static reservedSlugs = \[([^\]]*)\]/);
  26. const reserved = [...reservedSrc[1].matchAll(/'([^']*)'/g)].map(m => m[1]);
  27. const { details } = await (await fetch(BASE + '/api/projects')).json();
  28. const slugs = [];
  29. for (const p of details) slugs.push(p.slug);
  30. const collisions = slugs.filter(s => reserved.includes(s.toLowerCase()));
  31. console.log(`projects: ${details.length} — ${slugs.join(', ')}`);
  32. check(`no project slug is a reserved name (${reserved.length} reserved)`, collisions.length === 0, collisions.join(', '));
  33. // every project: /<slug> is its page; /projects/<slug> → 301 /<slug>
  34. for (const p of details) {
  35. const r = await page('/' + p.slug);
  36. check(`/${p.slug} → 200, the project page (heading "${p.title}")`, r.status === 200 && r.type.startsWith('text/html') && r.body.includes('id="heading">' + esc(p.title) + '<'), r.status + ' ' + r.body.slice(0, 120));
  37. check(`api: ${p.slug} href = /${p.slug}`, p.href === '/' + p.slug, p.href);
  38. await moves('/projects/' + p.slug, '/' + p.slug, 'id="heading">' + esc(p.title) + '<');
  39. }
  40. // N tickets, round robin over the projects (newest first in each)
  41. const per = {};
  42. for (const p of details) per[p.slug] = (await (await fetch(BASE + '/api/projects/' + p.slug + '/tickets')).json()).tickets;
  43. const picked = [];
  44. for (let i = 0; picked.length < N; i++) {
  45. let added = false;
  46. for (const s of slugs) { if (per[s][i] && picked.length < N) { picked.push(per[s][i]); added = true; } }
  47. if (!added) break;
  48. }
  49. for (const t of picked) {
  50. const short = '/' + t.project + '/' + t.number;
  51. check(`api: ${t.project} #${t.number} href = ${short}`, t.href === short, t.href);
  52. const r = await page(short);
  53. check(`${short} → 200, the ticket page`, r.status === 200 && r.body.includes('id="subject"') && r.body.includes(esc(t.subject)), r.status + ' ' + r.body.slice(0, 120));
  54. await moves('/projects/' + t.project + '/' + t.number, short, esc(t.subject));
  55. await moves('/projects/' + t.project + '/tickets/' + t.number, short, esc(t.subject));
  56. if (t.oldNumber) await moves('/tickets/' + t.oldNumber, short, esc(t.subject));
  57. const md = await (await fetch(BASE + t.apiHref, { headers: { accept: 'text/markdown' } })).text();
  58. check(`api ${t.apiHref} (unchanged path) answers, Markdown URL line is the short URL`, md.includes(short + '\n'), md.slice(0, 200));
  59. }
  60. console.log(`tickets checked: ${picked.length}`);
  61. console.log(`\n${passes} passed, ${failures} failed`);
  62. process.exit(failures ? 1 : 0);

Branches

Latest commits

  • 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