gitoriaLog in with ident

gitoria

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commit8660544686605446mission 002 (code order) 4/4: README file map + import order + 'Same output' test + gate run with a tickets HEAD copy, STATUS (entry, lessons), LOG, report; tests/realdata-baseline.mjs + realdata-compare.py (a cleanup answers the same on live data: pages, modules, API, git over HTTPS and SSH, faces), tests/letcount.pymre86605446/tests/realdata-baseline.mjs

25.2 KB

  1. // tests/realdata-baseline.mjs (gitoria mission 002, code order): every read the app answers + a fixed write sequence, on a
  2. // COPY of the LIVE storage, saved for diffing two code trees (a cleanup must answer the same). Compare two outputs with
  3. // tests/realdata-compare.py. README "Test" → "Same output".
  4. // node tests/realdata-baseline.mjs <tree> <port> <outdir> (kills only the server and the sshd container it started)
  5. // The live copy: GITORIA_REALDATA (default .scratch/realdata) must hold storage/mpackdb + storage/git, e.g. from Byrodin:
  6. // tar -C /CONTAINERS/projects/gitoria.worldapi.org -cf - storage/mpackdb storage/git | 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. // 0mugibaf6fds) through a session file written before the start; every page is fetched signed in AND signed out.
  9. // Ports: <port> = the app, <port>+8 = the sshd container (docker, image built from docker/sshd, name gitoria-sshd-rd<port>).
  10. // Git commits are made with fixed dates (client and server: GIT_AUTHOR_DATE / GIT_COMMITTER_DATE), so their ids match
  11. // between runs; the ssh keys are made once in <GITORIA_REALDATA>/keys and reused (same fingerprints).
  12. // Tickets: the connected repo's tickets API is a SNAPSHOT of the live answer (<GITORIA_REALDATA>/tickets-snapshot.json,
  13. // fetched once), served on <port>+9; the copy's stored `tktApi` is rewritten to it (same length, the msgpack stays valid).
  14. // So nothing reaches the live tickets, and a change on live tickets between two runs does not show up as a difference.
  15. import { spawn, spawnSync, execSync, execFileSync } from 'node:child_process';
  16. import { request, createServer } from 'node:http';
  17. import { writeFileSync, mkdirSync, rmSync, existsSync, readFileSync } from 'node:fs';
  18. import { createHash, randomBytes } from 'node:crypto';
  19. import { join, resolve } from 'node:path';
  20. const [tree, portArg, outArg] = process.argv.slice(2);
  21. const PORT = Number(portArg), SSHPORT = PORT + 8;
  22. const W = resolve(process.env.GITORIA_REALDATA || '.scratch/realdata'), OUT = resolve(outArg), RUN = OUT + '.run';
  23. const USER = '0mugibaf6fds', SECRET = 'rd-secret-fixed', CONTAINER = 'gitoria-sshd-rd' + PORT;
  24. rmSync(RUN, { recursive: true, force: true }); rmSync(OUT, { recursive: true, force: true });
  25. mkdirSync(join(RUN, 'sessions'), { recursive: true }); mkdirSync(join(RUN, 'home'), { recursive: true }); mkdirSync(OUT, { recursive: true });
  26. execSync(`cp -a ${W}/storage ${RUN}/storage`);
  27. const GIT = join(RUN, 'storage/git');
  28. const LIVE_TICKETS = 'https://tickets.worldapi.org', FAKE_TICKETS = `http://127.0.0.1:${PORT + 9}/fake00`; // the same length
  29. if (LIVE_TICKETS.length !== FAKE_TICKETS.length) throw new Error('fake tickets URL must have the length of the live one');
  30. const reposFile = join(RUN, 'storage/mpackdb/repos.mpack');
  31. const rb = readFileSync(reposFile, 'latin1');
  32. writeFileSync(reposFile, rb.split(LIVE_TICKETS + '/api/projects/').join(FAKE_TICKETS + '/api/projects/'), 'latin1');
  33. const SNAP = join(W, 'tickets-snapshot.json');
  34. if (!existsSync(SNAP)) writeFileSync(SNAP, await (await fetch(LIVE_TICKETS + '/api/projects/gitoria.worldapi.org/tickets')).text());
  35. const fake = createServer((req, res) => {
  36. if (req.method === 'GET' && req.url === '/fake00/api/projects/gitoria.worldapi.org/tickets') { res.writeHead(200, { 'content-type': 'application/json' }); res.end(readFileSync(SNAP)); return; }
  37. res.writeHead(404, { 'content-type': 'application/json' }); res.end('{"error":"fake tickets: not here"}');
  38. });
  39. fake.listen(PORT + 9, '127.0.0.1');
  40. // ---- the keys (once) ---------------------------------------------------------------------------------------------
  41. const KEYS = join(W, 'keys');
  42. if (!existsSync(join(KEYS, 'ed'))) {
  43. mkdirSync(KEYS, { recursive: true });
  44. spawnSync('ssh-keygen', ['-q', '-t', 'ed25519', '-N', '', '-C', 'rd@w082', '-f', join(KEYS, 'ed')]);
  45. spawnSync('ssh-keygen', ['-q', '-t', 'rsa', '-b', '1024', '-N', '', '-C', 'small@w082', '-f', join(KEYS, 'rsa1024')]);
  46. }
  47. const edPub = readFileSync(join(KEYS, 'ed.pub'), 'utf8').trim(), rsaSmall = readFileSync(join(KEYS, 'rsa1024.pub'), 'utf8').trim();
  48. // ---- the session -------------------------------------------------------------------------------------------------
  49. const sid = randomBytes(16).toString('hex'), now = Date.now();
  50. writeFileSync(join(RUN, 'sessions', createHash('sha256').update(sid).digest('hex')),
  51. JSON.stringify({ created: now, data: { tag: 'w082tag' }, handled: [], id: sid, seen: now, user: { id: USER } }), { mode: 0o600 });
  52. const COOKIE = 'gitoriasid=' + sid;
  53. // a second, signed-out session (for the failed-login note)
  54. const sid2 = randomBytes(16).toString('hex');
  55. writeFileSync(join(RUN, 'sessions', createHash('sha256').update(sid2).digest('hex')),
  56. JSON.stringify({ created: now, data: { tag: 'w082tag2' }, handled: [], id: sid2, seen: now, user: null }), { mode: 0o600 });
  57. const sleep = ms => new Promise(r => setTimeout(r, ms));
  58. const MAIN = `gitoria.test:${PORT}`, HOST = (slug) => `${slug}.gitoria.test:${PORT}`;
  59. const DATES = { GIT_AUTHOR_DATE: '2026-10-03T12:00:00+0200', GIT_COMMITTER_DATE: '2026-10-03T12:00:00+0200' };
  60. const srv = spawn(resolve(tree, 'bin/hybriel'), ['project.hl'], { cwd: resolve(tree), stdio: ['ignore', 'pipe', 'pipe'],
  61. env: { ...process.env, ...DATES, GITORIA_PORT: String(PORT), GITORIA_PUBLIC_URL: `http://${MAIN}`, GITORIA_STORAGE: join(RUN, 'storage/mpackdb'),
  62. GITORIA_GIT: GIT, GITORIA_SESSIONS: join(RUN, 'sessions') + '/', GITORIA_WATCH: '0', IDENT_URL: 'http://127.0.0.1:9', IDENT_EXCHANGE_URL: 'http://127.0.0.1:9',
  63. IDENT_API_KEY: 'pk_w082', IDENT_API_SECRET: '', GITORIA_SSH_SECRET: SECRET, GITORIA_SSH_PORT: String(SSHPORT), GITORIA_TICKETS_URL: 'https://tickets.worldapi.org' } });
  64. let log = ''; srv.stdout.on('data', d => log += d); srv.stderr.on('data', d => log += d);
  65. const hreq = (method, host, path, headers = {}, body = null) => new Promise((res) => {
  66. const rq = request({ host: '127.0.0.1', port: PORT, path, method, headers: { host, ...headers } }, (r) => {
  67. 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') }));
  68. });
  69. rq.on('error', (e) => res({ status: 0, headers: {}, body: 'ERROR ' + e.message }));
  70. if (body != null) { rq.setHeader('content-length', Buffer.byteLength(body)); rq.write(body); }
  71. rq.end();
  72. });
  73. let n = 0;
  74. const file = (name, text) => writeFileSync(join(OUT, String(++n).padStart(4, '0') + '-' + name.replace(/[^a-zA-Z0-9._-]+/g, '_').slice(0, 120)), text);
  75. const save = async (name, method, host, path, headers = {}, body = null) => {
  76. const r = await hreq(method, host, path, headers, body);
  77. const head = [r.status, r.headers['content-type'] || '', r.headers['location'] || '', r.headers['www-authenticate'] || '', r.headers['cache-control'] || '', r.headers['set-cookie'] ? 'set-cookie' : ''].join(' | ');
  78. file(name, head + '\n' + r.body);
  79. return r;
  80. };
  81. let ei = 0;
  82. const emitF = async (name, event, payload, cookie = COOKIE) => {
  83. const r = await hreq('POST', MAIN, '/__hl/emit', { 'content-type': 'application/json', cookie }, JSON.stringify({ t: 'emit', i: ++ei, event, payload }));
  84. file('w-face-' + name, r.status + '\n' + r.body);
  85. try { return JSON.parse(r.body).value; } catch { return null; }
  86. };
  87. const gitIn = (dir, args) => execFileSync('git', args.split(' '), { cwd: dir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
  88. const GENV = { ...process.env, ...DATES, HOME: join(RUN, 'home'), GIT_CONFIG_NOSYSTEM: '1', GIT_TERMINAL_PROMPT: '0', LC_ALL: 'C' };
  89. const sshCmd = `ssh -i ${join(KEYS, 'ed')} -p ${SSHPORT} -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -o LogLevel=ERROR`;
  90. // the real git client; `slug` maps <slug>.gitoria.test to 127.0.0.1 for http
  91. const git = (cwd, args, slug = null, extra = {}) => new Promise((res) => {
  92. const pre = slug ? ['-c', `http.curloptResolve=${slug}.gitoria.test:${PORT}:127.0.0.1`] : [];
  93. const c = spawn('git', [...pre, '-c', 'user.name=W082', '-c', '[email protected]', '-c', 'init.defaultBranch=main', ...args], { cwd, env: { ...GENV, ...extra } });
  94. let out = ''; c.stdout.on('data', d => out += d); c.stderr.on('data', d => out += d);
  95. const t = setTimeout(() => c.kill('SIGKILL'), 180000);
  96. c.on('close', (code) => { clearTimeout(t); res({ code, out }); });
  97. });
  98. // what a git run says, without what differs between runs (temp paths, transfer speeds)
  99. const gitText = (r) => 'exit ' + r.code + '\n' + r.out.split(/\r|\n/).filter(l => l.trim() && !/Receiving objects|Resolving deltas|Counting objects|Compressing objects|Writing objects|Total \d|remote: (Enumerating|Counting|Compressing|Total)/.test(l)).map(l => l.split(RUN).join('<RUN>')).join('\n');
  100. try {
  101. for (let i = 0; i < 240; i++) { if ((await hreq('GET', MAIN, '/api/repos')).status === 200) break; await sleep(500); }
  102. const repos = JSON.parse((await save('api-repos', 'GET', MAIN, '/api/repos')).body).repos;
  103. // ---- READS ------------------------------------------------------------------------------------------------------
  104. const pages = [[MAIN, '/'], [MAIN, '/login/failed'], [MAIN, '/code'], [MAIN, '/pulls'], [MAIN, '/settings'], [MAIN, '/nope'],
  105. [HOST('nosuchrepo'), '/'], [HOST('nosuchrepo'), '/code'], [HOST('nosuchrepo'), '/pulls'], [HOST('x--y'), '/']];
  106. const apis = ['/api/repos', '/api/repos/nosuch', '/api/repos/x--y'];
  107. const advert = [];
  108. for (const r of repos) {
  109. const s = r.slug, H = HOST(s), dir = join(GIT, s + '.git');
  110. apis.push('/api/repos/' + s);
  111. for (const p of ['/', '/code', '/pulls', '/releases', '/tickets', '/settings', '/code/nosuch.txt', '/branch/nosuch', '/branch', '/commit/zzzz', '/commit/deadbeef', '/commit']) pages.push([H, p]);
  112. const branches = gitIn(dir, 'for-each-ref --format=%(refname:short) refs/heads').split('\n').filter(Boolean).slice(0, 6);
  113. for (const b of branches) pages.push([H, '/branch/' + encodeURIComponent(b).replaceAll('%2F', '/')]);
  114. if (branches.length) {
  115. const b0 = branches.includes(r.branch) ? r.branch : branches.includes('main') ? 'main' : branches[0];
  116. const all = gitIn(dir, `ls-tree -r --name-only ${b0}`).split('\n').filter(Boolean);
  117. const enc = (p) => p.split('/').map(encodeURIComponent).join('/');
  118. const pick = [all[0], all.find(p => /readme\.md$/i.test(p)), all.find(p => p.includes('/')), all.find(p => p.split('/').length > 2), all[all.length - 1], all.find(p => /\.(png|ico|jpg|gz|zip)$/i.test(p))].filter(Boolean);
  119. for (const p of [...new Set(pick)]) pages.push([H, '/code/' + enc(p)]);
  120. const dirs = [...new Set(all.filter(p => p.includes('/')).map(p => p.split('/')[0]))].slice(0, 2);
  121. for (const d of dirs) pages.push([H, '/code/' + enc(d)]);
  122. const sha = gitIn(dir, `rev-parse ${b0}`);
  123. pages.push([H, '/commit/' + sha], [H, '/commit/' + sha.slice(0, 8)]);
  124. if (dirs[0]) pages.push([H, '/commit/' + sha + '/' + enc(dirs[0])]);
  125. const old = gitIn(dir, `log --format=%H -1 --skip=5 ${b0}`);
  126. if (old) pages.push([H, '/commit/' + old]);
  127. }
  128. advert.push(s);
  129. }
  130. const modules = new Set();
  131. for (const [h, p] of pages) {
  132. const r = await save('in-' + h + p, 'GET', h, p, { cookie: COOKIE }); await save('out-' + h + p, 'GET', h, p);
  133. for (const m of r.body.matchAll(/(\/(?:components|__hl)\/[\w./-]+\.(?:hl|js|css))\?v=[0-9a-f]+/g)) modules.add(m[0]);
  134. }
  135. // the browser half: every component module, the runtime, the client, the stylesheet the pages load
  136. for (const m of [...modules].sort()) await save('module-' + m.replace(/\?v=.*/, ''), 'GET', MAIN, m);
  137. for (const u of apis) { await save('json' + u, 'GET', MAIN, u); await save('md' + u, 'GET', MAIN, u, { accept: 'text/markdown' }); }
  138. await save('api-post', 'POST', MAIN, '/api/repos', { 'content-type': 'application/json' }, '{}');
  139. await save('api-post-one', 'POST', MAIN, '/api/repos/tracker', { 'content-type': 'application/json' }, '{}');
  140. // the git protocol: the refs advertisement of every repo, and the refusals
  141. for (const s of advert) {
  142. await save('refs-upload-' + s, 'GET', HOST(s), `/${s}.git/info/refs?service=git-upload-pack`);
  143. await save('refs-upload-v2-' + s, 'GET', HOST(s), `/${s}.git/info/refs?service=git-upload-pack`, { 'git-protocol': 'version=2' });
  144. }
  145. await save('refs-receive-noauth', 'GET', HOST('email'), '/email.git/info/refs?service=git-receive-pack');
  146. await save('refs-receive-badtoken', 'GET', HOST('email'), '/email.git/info/refs?service=git-receive-pack', { authorization: 'Basic ' + Buffer.from('x:gtr_' + '0'.repeat(40)).toString('base64') });
  147. await save('refs-receive-junk', 'GET', HOST('email'), '/email.git/info/refs?service=git-receive-pack', { authorization: 'Basic !!!' });
  148. await save('refs-dumb', 'GET', HOST('email'), '/email.git/info/refs');
  149. await save('refs-wronghost', 'GET', HOST('tracker'), '/email.git/info/refs?service=git-upload-pack');
  150. await save('refs-main', 'GET', MAIN, '/email.git/info/refs?service=git-upload-pack');
  151. await save('refs-post', 'POST', HOST('email'), '/email.git/info/refs?service=git-upload-pack', {}, 'x');
  152. await save('upload-get', 'GET', HOST('email'), '/email.git/git-upload-pack');
  153. // the internal ssh endpoints
  154. const S = { 'x-gitoria-secret': SECRET };
  155. await save('keys-nosecret', 'GET', MAIN, '/__git/keys?type=ssh-ed25519&key=AAAAC3NzaC1lZDI1NTE5AAAAIx');
  156. await save('keys-wrong', 'GET', MAIN, '/__git/keys?type=ssh-ed25519&key=AAAAC3NzaC1lZDI1NTE5AAAAIx', { 'x-gitoria-secret': 'nope' });
  157. await save('keys-proxied', 'GET', MAIN, '/__git/keys?type=ssh-ed25519&key=AAAAC3NzaC1lZDI1NTE5AAAAIx', { ...S, 'x-forwarded-for': '1.2.3.4' });
  158. await save('keys-unknown', 'GET', MAIN, '/__git/keys?type=ssh-ed25519&key=AAAAC3NzaC1lZDI1NTE5AAAAIx', S);
  159. await save('keys-post', 'POST', MAIN, '/__git/keys', S, 'x');
  160. for (const q of [`user=${USER}&slug=tracker&write=0`, `user=${USER}&slug=tracker&write=1`, 'user=nobody&slug=tracker&write=1', 'user=nobody&slug=tracker&write=0', `user=${USER}&slug=nosuch&write=0`, `user=${USER}&slug=X..&write=0`, 'slug=tracker'])
  161. await save('access-' + q, 'GET', MAIN, '/__git/access?' + q, S);
  162. // the login's and the tickets connection's function routes (ident is down on purpose: port 9)
  163. for (const q of ['', '?ident_code=zz', '?ident_code=abc123', '?ident_code=abc&next=/code']) await save('callback' + q, 'GET', MAIN, '/login/callback' + q);
  164. await save('callback-post', 'POST', MAIN, '/login/callback', {}, 'x');
  165. await save('page-loginfailed-after', 'GET', MAIN, '/login/failed');
  166. // a failed login in a browser that has a session: the reason is parked in THAT session (the route writes it by reference)
  167. const C2 = 'gitoriasid=' + sid2;
  168. await save('callback-cookie', 'GET', MAIN, '/login/callback?ident_code=abc123', { cookie: C2 });
  169. await save('page-loginfailed-cookie', 'GET', MAIN, '/login/failed', { cookie: C2 });
  170. await save('callback-cookie-nocode', 'GET', MAIN, '/login/callback', { cookie: C2 });
  171. await save('page-loginfailed-cookie2', 'GET', MAIN, '/login/failed', { cookie: C2 });
  172. await save('connect-out', 'GET', HOST('gitoria'), '/settings/connect');
  173. await save('connect-main', 'GET', MAIN, '/settings/connect', { cookie: COOKIE });
  174. await save('connect-nosuch', 'GET', HOST('nosuchrepo'), '/settings/connect', { cookie: COOKIE });
  175. await save('connected-out', 'GET', HOST('email'), '/settings/connected?code=x&state=y');
  176. await save('connected-error', 'GET', HOST('email'), '/settings/connected?error=nope+said+tickets', { cookie: COOKIE });
  177. await save('settings-note1', 'GET', HOST('email'), '/settings', { cookie: COOKIE });
  178. await save('connected-nostate', 'GET', HOST('email'), '/settings/connected?code=x&state=y', { cookie: COOKIE });
  179. await save('settings-note2', 'GET', HOST('email'), '/settings', { cookie: COOKIE });
  180. await save('connect-start', 'GET', HOST('email'), '/settings/connect', { cookie: COOKIE });
  181. await save('connected-wrongstate', 'GET', HOST('email'), '/settings/connected?code=x&state=y', { cookie: COOKIE });
  182. await save('settings-note3', 'GET', HOST('email'), '/settings', { cookie: COOKIE });
  183. // ---- WRITES ('w-' files; realdata-compare.py masks the new ids, times and token secrets) ------------------------------
  184. await emitF('saveName-again', 'gitoriaSaveName', ['someone']);
  185. await emitF('create', 'gitoriaCreate', ['w082-new', 'W082 test repo']);
  186. await emitF('create-dup', 'gitoriaCreate', ['w082-new', '']);
  187. await emitF('create-bad', 'gitoriaCreate', ['Bad Slug', '']);
  188. await emitF('create-reserved', 'gitoriaCreate', ['www', '']);
  189. await emitF('create-longdesc', 'gitoriaCreate', ['w082-other', 'x'.repeat(201)]);
  190. await emitF('create-out', 'gitoriaCreate', ['w082-out', ''], 'gitoriasid=none');
  191. const tok = (await emitF('token', 'gitoriaMakeToken', ['w082'])) || {};
  192. await emitF('token-empty', 'gitoriaMakeToken', ['']);
  193. const key = (await emitF('key', 'gitoriaAddKey', ['w082 key', edPub])) || {};
  194. await emitF('key-dup', 'gitoriaAddKey', ['again', edPub]);
  195. await emitF('key-private', 'gitoriaAddKey', ['p', '-----BEGIN OPENSSH PRIVATE KEY-----']);
  196. await emitF('key-small', 'gitoriaAddKey', ['small', rsaSmall]);
  197. await emitF('key-junk', 'gitoriaAddKey', ['junk', 'ssh-ed25519 AAAAnotakeyatallxxxxxxxxx']);
  198. const [kt, kb] = edPub.split(' ');
  199. await save('w-keys-known', 'GET', MAIN, `/__git/keys?type=${kt}&key=${encodeURIComponent(kb)}`, S);
  200. await save('w-access-new', 'GET', MAIN, `/__git/access?user=${USER}&slug=w082-new&write=1`, S);
  201. for (const p of ['/', '/code', '/pulls', '/releases', '/settings']) await save('w-page-empty' + p, 'GET', HOST('w082-new'), p, { cookie: COOKIE });
  202. await save('w-page-main', 'GET', MAIN, '/', { cookie: COOKIE });
  203. // over HTTP: clone the empty repo, commit, push with the token (and refused without / with a wrong one)
  204. const W1 = join(RUN, 'home', 'w1'); mkdirSync(W1);
  205. const urlOf = (slug, pass) => `http://${pass ? 'x:' + pass + '@' : ''}${slug}.gitoria.test:${PORT}/${slug}.git`;
  206. file('w-git-clone-empty', gitText(await git(W1, ['clone', urlOf('w082-new'), 'r'], 'w082-new')));
  207. const R = join(W1, 'r');
  208. writeFileSync(join(R, 'README.md'), '# w082\n\nA test repo. fixes #1\n\n| a | b |\n|---|---|\n| 1 | 2 |\n');
  209. mkdirSync(join(R, 'src')); writeFileSync(join(R, 'src/a.txt'), 'line one\nline two\n');
  210. writeFileSync(join(R, 'src/bin.dat'), Buffer.from([0, 1, 2, 255, 254]));
  211. await git(R, ['add', '-A']); await git(R, ['commit', '-q', '-m', 'first commit #1']);
  212. writeFileSync(join(R, 'src/b.txt'), 'b\n'); await git(R, ['add', '-A']); await git(R, ['commit', '-q', '-m', '|||RL first release']);
  213. file('w-git-push-noauth', gitText(await git(R, ['push', urlOf('w082-new'), 'main'], 'w082-new')));
  214. file('w-git-push-badtoken', gitText(await git(R, ['push', urlOf('w082-new', 'gtr_' + '1'.repeat(40)), 'main'], 'w082-new')));
  215. file('w-git-push', gitText(await git(R, ['push', urlOf('w082-new', tok.token), 'main'], 'w082-new')));
  216. await git(R, ['checkout', '-q', '-b', 'feature']); writeFileSync(join(R, 'src/c.txt'), 'c\n'); await git(R, ['add', '-A']); await git(R, ['commit', '-q', '-m', '|||PR feature work, fixes #2']);
  217. await git(R, ['checkout', '-q', '-b', 'side', 'main']); writeFileSync(join(R, 'src/d.txt'), 'd\n'); await git(R, ['add', '-A']); await git(R, ['commit', '-q', '-m', '|||PR|nosuch] bad target']);
  218. await git(R, ['checkout', '-q', 'main']); writeFileSync(join(R, 'src/b.txt'), 'b2\n'); await git(R, ['add', '-A']); await git(R, ['commit', '-q', '-m', '|||RL|med a minor one']);
  219. file('w-git-push-branches', gitText(await git(R, ['push', urlOf('w082-new', tok.token), 'main', 'feature', 'side'], 'w082-new')));
  220. const prSha = gitIn(R, 'rev-parse feature');
  221. for (const p of ['/', '/code', '/code/src', '/code/src/a.txt', '/code/src/bin.dat', '/pulls', '/releases', '/branch/feature', '/branch/side/src', '/commit/' + prSha])
  222. await save('w-page' + p.replace(prSha, 'PR'), 'GET', HOST('w082-new'), p, { cookie: COOKIE });
  223. await emitF('setBranch', 'gitoriaSetBranch', ['w082-new', 'feature', 'code', '']);
  224. await save('w-page-code-feature', 'GET', HOST('w082-new'), '/code', { cookie: COOKIE });
  225. await emitF('setBranch-back', 'gitoriaSetBranch', ['w082-new', 'main', 'code', 'src']);
  226. await emitF('setBranch-nosuch', 'gitoriaSetBranch', ['w082-new', 'nosuch', 'code', '']);
  227. await emitF('setBranch-badkind', 'gitoriaSetBranch', ['w082-new', 'main', 'x', '']);
  228. await emitF('setBranch-out', 'gitoriaSetBranch', ['w082-new', 'main', 'code', ''], 'gitoriasid=none');
  229. await emitF('merge', 'gitoriaMergePull', ['w082-new', prSha]);
  230. await emitF('merge-again', 'gitoriaMergePull', ['w082-new', prSha]);
  231. await emitF('merge-bad', 'gitoriaMergePull', ['w082-new', 'zz']);
  232. await emitF('merge-target-missing', 'gitoriaMergePull', ['w082-new', gitIn(R, 'rev-parse side')]);
  233. await emitF('openTicket', 'gitoriaOpenTicket', ['w082-new', 'subject', 'text']);
  234. await emitF('openTicket-nosuch', 'gitoriaOpenTicket', ['nosuchrepo', 'subject', 'text']);
  235. await emitF('disconnect', 'gitoriaDisconnectTickets', ['w082-new']);
  236. await emitF('disconnect-out', 'gitoriaDisconnectTickets', ['w082-new'], 'gitoriasid=none');
  237. for (const p of ['/', '/code', '/pulls', '/releases', '/settings']) await save('w-page-after' + p, 'GET', HOST('w082-new'), p, { cookie: COOKIE });
  238. await save('w-api-new', 'GET', MAIN, '/api/repos/w082-new'); await save('w-api-new-md', 'GET', MAIN, '/api/repos/w082-new', { accept: 'text/markdown' });
  239. // a real repo: clone over HTTP, push a branch with the token
  240. file('w-git-clone-tracker', gitText(await git(W1, ['clone', '-q', urlOf('tracker'), 'tracker'], 'tracker')) + '\n' + gitIn(join(W1, 'tracker'), 'log --format=%H -3') + '\n' + gitIn(join(W1, 'tracker'), 'ls-tree -r HEAD').split('\n').length);
  241. file('w-git-clone-email', gitText(await git(W1, ['clone', '-q', urlOf('email'), 'email'], 'email')));
  242. const E = join(W1, 'email');
  243. await git(E, ['checkout', '-q', '-b', 'w082http']); writeFileSync(join(E, 'w082.txt'), 'http\n'); await git(E, ['add', '-A']); await git(E, ['commit', '-q', '-m', 'w082 over http']);
  244. file('w-git-push-email', gitText(await git(E, ['push', urlOf('email', tok.token), 'w082http'], 'email')));
  245. await save('w-page-email-branch', 'GET', HOST('email'), '/branch/w082http', { cookie: COOKIE });
  246. // over SSH: the real sshd container (docker/sshd) on a copy of the repos
  247. spawnSync('docker', ['rm', '-f', CONTAINER]);
  248. const built = spawnSync('docker', ['build', '-q', '-t', 'gitoria-sshd-gate', resolve(tree, 'docker/sshd')], { encoding: 'utf8' });
  249. const started = spawnSync('docker', ['run', '-d', '--rm', '--name', CONTAINER, '--network', 'host', '-e', 'GITORIA_SSH_SECRET=' + SECRET, '-e', 'GITORIA_SSH_PORT=' + SSHPORT, '-e', `GITORIA_URL=http://127.0.0.1:${PORT}`, '-v', GIT + ':/repos', 'gitoria-sshd-gate'], { encoding: 'utf8' });
  250. file('w-ssh-container', `build ${built.status} run ${started.status}`);
  251. for (let i = 0; i < 60; i++) { const r = spawnSync('sh', ['-c', `ssh-keyscan -p ${SSHPORT} 127.0.0.1 2>/dev/null | grep -c ssh-`], { encoding: 'utf8' }); if (Number(r.stdout) > 0) break; await sleep(250); }
  252. const SURL = (slug) => `ssh://[email protected]:${SSHPORT}/${slug}.git`;
  253. const W2 = join(RUN, 'home', 'w2'); mkdirSync(W2);
  254. file('w-ssh-clone-tracker', gitText(await git(W2, ['clone', '-q', SURL('tracker'), 'tracker'], null, { GIT_SSH_COMMAND: sshCmd })) + '\n' + gitIn(join(W2, 'tracker'), 'log --format=%H -3'));
  255. file('w-ssh-clone-email', gitText(await git(W2, ['clone', '-q', SURL('email'), 'email'], null, { GIT_SSH_COMMAND: sshCmd })));
  256. const E2 = join(W2, 'email');
  257. await git(E2, ['checkout', '-q', '-b', 'w082ssh']); writeFileSync(join(E2, 'w082ssh.txt'), 'ssh\n'); await git(E2, ['add', '-A']); await git(E2, ['commit', '-q', '-m', 'w082 over ssh #3']);
  258. file('w-ssh-push-email', gitText(await git(E2, ['push', SURL('email'), 'w082ssh'], null, { GIT_SSH_COMMAND: sshCmd })));
  259. file('w-ssh-push-nosuch', gitText(await git(E2, ['push', SURL('nosuchrepo'), 'w082ssh'], null, { GIT_SSH_COMMAND: sshCmd })));
  260. await save('w-page-email-ssh', 'GET', HOST('email'), '/branch/w082ssh', { cookie: COOKIE });
  261. await save('w-page-email-code', 'GET', HOST('email'), '/code', { cookie: COOKIE });
  262. // removing: the key stops at once, the token too
  263. await emitF('removeKey', 'gitoriaRemoveKey', [key.row ? key.row.id : 'none']);
  264. file('w-ssh-after-remove', gitText(await git(W2, ['clone', '-q', SURL('email'), 'email2'], null, { GIT_SSH_COMMAND: sshCmd })));
  265. await emitF('removeKey-again', 'gitoriaRemoveKey', [key.row ? key.row.id : 'none']);
  266. await emitF('removeToken', 'gitoriaRemoveToken', [tok.row ? tok.row.id : 'none']);
  267. file('w-git-push-removed', gitText(await git(E, ['push', urlOf('email', tok.token), 'w082http'], 'email')));
  268. await emitF('removeToken-again', 'gitoriaRemoveToken', [tok.row ? tok.row.id : 'none']);
  269. await save('w-page-main-after', 'GET', MAIN, '/', { cookie: COOKIE });
  270. await save('w-api-repos', 'GET', MAIN, '/api/repos');
  271. // the login faces (ident down) and the logout
  272. await emitF('login-bad', 'gitoriaLogin', ['zz']);
  273. await emitF('logout', 'gitoriaLogOut', []);
  274. await save('w-page-main-loggedout', 'GET', MAIN, '/', { cookie: COOKIE });
  275. console.log(`${n} responses saved to ${OUT} (${pages.length} pages x2, ${apis.length} api urls x2, + writes)`);
  276. } finally {
  277. fake.close();
  278. spawnSync('docker', ['rm', '-f', CONTAINER]);
  279. srv.kill('SIGTERM'); await sleep(1000); try { process.kill(srv.pid, 'SIGKILL'); } catch {}
  280. }
  281. writeFileSync(join(OUT, '0000-server-log'), log.split('\n').filter(l => !/listening|watch/i.test(l)).join('\n'));
  282. rmSync(RUN, { recursive: true, force: true });

Branches

Latest commits

  • 86605446mission 002 (code order) 4/4: README file map + import order + 'Same output' test + gate run with a tickets HEAD copy, STATUS (entry, lessons), LOG, report; tests/realdata-baseline.mjs + realdata-compare.py (a cleanup answers the same on live data: pages, modules, API, git over HTTPS and SSH, faces), tests/letcount.pymre
  • cc7bf7bamission 002 (code order) 3/4: let only where a variable is reassigned or re-bound in a loop body (289 lets → plain declarations; 213 left: 125 reassigned, 88 loop-bound; no member/import/param clash). gates 200/0, 46/0, 44/0; real-data reads + writes identical (browser modules: var → const only)mre
  • 090a20c6mission 002 (code order) 2/4: one lib/ file per topic — git.hl split into git (calls, branches, init, temp folder) / homepage / code / pulls / releases (+ git-helpers: paths, ids, |||PR/|||RL markers); repos-helpers, tickets-helpers, transport-helpers; util.hl = localtime + env, storage dir, addresses, lists, text checks, one newest-first sort (was 3 copies); the function routes out of project.hl into lib/api.hl (thin; plumbing in api-helpers.hl), sshgate.hl folded into api.hl + sshkeys.hl keyLine + repos.hl mayPush; 'Make main' and the merge answer out of the faces (code.hl makeMain, pulls.hl pullsView), one login helper (users.hl userOfLoginCode); project.hl is the map. Session-writing routes get &req + &server.sessions. gates 200/0, 46/0, 44/0; real-data identical except /login/failed now shows the parked reason for a browser that already had a session (the old copy-of-req lost it)mre
  • 110c2799mission 002 (code order) 1/4: .hl files out of the root — lib/ (api, git, localtime, markdown, repos, sshgate, sshkeys, tickets, tokens, transport, users), components/styles.hl; jsoncheck.hl removed (imported nowhere); import paths only. gates 200/0, 46/0, 44/0; real-data reads + writes identicalmre
  • fdfb4b1bgitoria: Hybriel master 06617221 (plugin allocators 3a781359 + 413f60e4, mpackdb 2cb7ae5e, http1 773de63e); gates 200/0, 46/0, 44/0mre
  • 5b46ac84antcolony#40: LOG.md — missions 069/072 are antcolony missions (report paths on Byrodin)mre
  • 5602ff41gitoria: Hybriel master 190aa11d (fc838894 GC correctness, #127 mountKids by reference, #126, #48) — tracker README flat; gates 200/0, 46/0, 44/0mre
  • e85eaf01gitoria: 069 round 2 — hybriel 1a096ad3 not adopted (Markdown SSR still grows); browser gate waits for the server-side logout before restartmre
  • 09ce4f3fgitoria: mission 069 re-vendor hybriel 8efba065 stopped (big SSR pages grow + slow down); lambda audit clean; old vendor keptmre
  • 3dc43108antcolony#40: mission references point to the moved missionsmre
  • 8d9450fdantcolony#40: history (LOG.md), worker briefs (missions/) and reports moved here from antcolony, numbered per project; old numbers in antcolony docs/mission-map.mdmre
  • 205d5fe4gitoria: Hybriel master ff51cf46; ssh keys/tokens no double rows (session sync); gates follow #20mre
  • 9b27cb26gitoria#21: installable app (manifest, service worker, offline start page), own iconmre
  • 68dcb603deploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
  • e2deed6dgitoria#20: "Add code" only on the Code page of an empty repository, no collapsiblemre
  • 8bb97ffddeploy.sh: never send .git or .gitignore to Byrodinmre
  • fd981932State of 2026-09-27; bin/ no longer tracked (Hybriel commit is in README)mre
  • 4a2d7125initial commitmre