gitoriaLog in with ident

gitoria

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commite85eaf01e85eaf01gitoria: 069 round 2 — hybriel 1a096ad3 not adopted (Markdown SSR still grows); browser gate waits for the server-side logout before restartmree85eaf01/tests/push.mjs

22.2 KB

  1. // tests/push.mjs — THE GATE OF PUSH AND PULL (ticket gitoria#7). Its own gitoria server + its own ident (copy, mail sink),
  2. // a real Chrome for everything visible (create a repo, the "add code" box, access tokens), and the REAL `git` binary for
  3. // clone / push / fetch over HTTP against the repo address <slug>.gitoria.test (git's http.curloptResolve maps it to 127.0.0.1).
  4. // node tests/push.mjs (GITORIA_GATE_PORT 8700, GITORIA_GATE_IDENT_PORT 8701, GITORIA_GATE_CHROME "8703-8707")
  5. import { spawn } from 'node:child_process';
  6. import { request as httpRequest, createServer } from 'node:http';
  7. import { rmSync, mkdirSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
  8. import { dirname, join, resolve } from 'node:path';
  9. import { fileURLToPath } from 'node:url';
  10. import { randomBytes } from 'node:crypto';
  11. import { launchBrowser } from './cdp.mjs';
  12. import { startIdent } from './identkit.mjs';
  13. if (!process.env.HL_CHROME && existsSync('/opt/google/chrome/chrome')) process.env.HL_CHROME = '/opt/google/chrome/chrome';
  14. const HERE = dirname(fileURLToPath(import.meta.url));
  15. const APP = resolve(HERE, '..');
  16. const BIN = join(APP, 'bin/hybriel');
  17. const PORT = Number(process.env.GITORIA_GATE_PORT || 8700);
  18. const IDENT_PORT = Number(process.env.GITORIA_GATE_IDENT_PORT || 8701);
  19. const PROXY = Number(process.env.GITORIA_GATE_PROXY || 8702); // stands in for nginx: buffers a chunked request body and sends it with a length
  20. const [CH_FROM, CH_TO] = (process.env.GITORIA_GATE_CHROME || '8703-8707').split('-').map(Number);
  21. const API = `http://127.0.0.1:${PORT}`;
  22. const BASE = `http://gitoria.test:${PORT}`;
  23. const IDENT_B = `http://ident.gitoria.test:${IDENT_PORT}`;
  24. const REPO = (slug) => `http://${slug}.gitoria.test:${PORT}`;
  25. const hreq = (method, path, host, headers = {}, body = null) => new Promise((res, rej) => {
  26. const rq = httpRequest({ host: '127.0.0.1', port: PORT, path, method, headers: { host, ...headers } }, (r) => { let b = ''; r.setEncoding('utf8'); r.on('data', d => b += d); r.on('end', () => res({ status: r.statusCode, headers: r.headers, body: b })); });
  27. rq.on('error', rej); if (body) { rq.setHeader('content-length', Buffer.byteLength(body)); rq.write(body); } rq.end();
  28. });
  29. process.env.HL_CHROME_ARGS = '--host-resolver-rules=MAP *.gitoria.test 127.0.0.1, MAP gitoria.test 127.0.0.1';
  30. const SCRATCH = join(APP, '.scratch');
  31. const STORE = join(SCRATCH, 'push-store');
  32. const WORK = join(STORE, 'work');
  33. const IDENT_DIR = process.env.GITORIA_GATE_IDENT_DIR || [resolve(APP, '../ident.worldapi.org'), '/media/STORAGE/projects/ident.worldapi.org'].find(d => existsSync(join(d, 'project.hl')));
  34. rmSync(STORE, { recursive: true, force: true });
  35. mkdirSync(WORK, { recursive: true });
  36. let failures = 0, passes = 0;
  37. function check(label, ok, detail = '') {
  38. console.log(`${ok ? 'ok ' : 'FAIL'} ${label}${ok ? '' : ' — ' + detail}`);
  39. if (ok) passes++; else failures++;
  40. }
  41. const sleep = (ms) => new Promise(r => setTimeout(r, ms));
  42. const J = JSON.stringify;
  43. let pageA = null;
  44. let log = '', server = null, ident = null, proxy = null;
  45. function startProxy() {
  46. proxy = createServer((req, res) => {
  47. const parts = []; req.on('data', d => parts.push(d));
  48. req.on('end', () => {
  49. const body = Buffer.concat(parts);
  50. const headers = { ...req.headers }; delete headers['transfer-encoding']; headers['content-length'] = String(body.length);
  51. const up = httpRequest({ host: '127.0.0.1', port: PORT, path: req.url, method: req.method, headers }, (r) => { res.writeHead(r.statusCode, r.headers); r.pipe(res); });
  52. up.on('error', () => { res.writeHead(502); res.end(); });
  53. up.end(body);
  54. });
  55. });
  56. proxy.listen(PROXY, '127.0.0.1');
  57. }
  58. const browsers = [];
  59. function startServer(extraEnv = {}) {
  60. server = spawn(BIN, ['project.hl'], {
  61. cwd: APP,
  62. env: { ...process.env, GITORIA_PORT: String(PORT), GITORIA_STORAGE: join(STORE, 'mpackdb'), GITORIA_SESSIONS: join(STORE, 'sessions') + '/', GITORIA_WATCH: '0', GITORIA_GIT: join(STORE, 'git'),
  63. GITORIA_PUBLIC_URL: BASE, IDENT_URL: IDENT_B, IDENT_EXCHANGE_URL: ident.base, GITORIA_TICKETS_URL: 'http://127.0.0.1:1', ...extraEnv },
  64. stdio: ['ignore', 'pipe', 'pipe'],
  65. });
  66. server.stdout.on('data', d => log += d); server.stderr.on('data', d => log += d);
  67. }
  68. async function serverUp() {
  69. for (let i = 0; i < 80; i++) { try { const r = await fetch(API + '/api/repos'); if (r.ok) return; } catch {} await sleep(250); }
  70. throw new Error('gitoria did not come up\n' + log);
  71. }
  72. async function stopServer() {
  73. if (!server) return;
  74. try { server.kill('SIGTERM'); } catch {}
  75. await new Promise(r => { if (server.exitCode !== null || server.signalCode !== null) return r(); server.once('exit', r); setTimeout(r, 3000); });
  76. server = null;
  77. }
  78. function patient(page) {
  79. const waitFor = page.waitFor.bind(page);
  80. page.waitFor = (expr, o = {}) => waitFor(expr, { ...o, timeout: (o.timeout || 10000) * 3 });
  81. const goto = page.goto.bind(page);
  82. page.goto = (url, o = {}) => goto(url, { ...o, timeout: (o.timeout || 15000) * 3 });
  83. return page;
  84. }
  85. const hydrated = (page) => page.waitFor('!!window.__hl && window.__hl.socket && window.__hl.socket.readyState === 1', { label: 'page hydrated' });
  86. const txt = (page, sel) => page.evaluate(`(document.querySelector(${J(sel)}) || {}).textContent || null`);
  87. const has = (page, sel) => page.evaluate(`!!document.querySelector(${J(sel)})`);
  88. const noOverflow = (page) => page.evaluate('document.documentElement.scrollWidth <= window.innerWidth');
  89. // the real git client. HOST = the repo address mapped to 127.0.0.1; credentials in the URL when given
  90. const GIT_ENV = { ...process.env, GIT_TERMINAL_PROMPT: '0', GIT_CONFIG_NOSYSTEM: '1', HOME: WORK, LC_ALL: 'C' };
  91. function git(cwd, args, slug, extra = {}) {
  92. const pre = slug ? ['-c', `http.curloptResolve=${slug}.gitoria.test:${extra.port || PROXY}:127.0.0.1`] : [];
  93. const { port: _p, ...envExtra } = extra;
  94. // async: the proxy of this gate lives in this process, a blocking spawnSync would starve it
  95. return new Promise((res) => {
  96. const c = spawn('git', [...pre, '-c', 'user.name=Gate', '-c', '[email protected]', '-c', 'init.defaultBranch=main', ...args], { cwd, env: { ...GIT_ENV, ...envExtra } });
  97. let out = ''; c.stdout.on('data', d => out += d); c.stderr.on('data', d => out += d);
  98. const t = setTimeout(() => c.kill('SIGKILL'), 120000);
  99. c.on('close', (code) => { clearTimeout(t); res({ code, out }); });
  100. });
  101. }
  102. const urlOf = (slug, user, pass, port = PROXY) => `http://${user ? `${user}:${pass}@` : ''}${slug}.gitoria.test:${port}/${slug}.git`;
  103. try {
  104. ident = await startIdent({ identDir: IDENT_DIR, workDir: join(STORE, 'ident'), port: IDENT_PORT });
  105. const alice = await ident.signIn('[email protected]');
  106. const bob = await ident.signIn('[email protected]');
  107. const APPKEY = await ident.registerApp(alice, 'gitoria (push gate)', [BASE]);
  108. startServer({ IDENT_API_KEY: APPKEY.key, IDENT_API_SECRET: APPKEY.secret });
  109. await serverUp();
  110. startProxy();
  111. // ---- alice in Chrome: log in, name, create an empty repo --------------------------------------------------
  112. const b = await launchBrowser({ debugPortRange: [CH_FROM, CH_TO] });
  113. browsers.push(b);
  114. const A = patient(await b.newPage());
  115. pageA = A;
  116. const [ck, cv] = alice.cookie.split('=');
  117. await A.send('Network.enable');
  118. await A.send('Network.setCookie', { name: ck, value: cv, url: IDENT_B + '/' });
  119. await A.goto(BASE + '/');
  120. await A.waitForSelector('#loginbutton');
  121. await hydrated(A);
  122. check('tokens: signed out, the main page offers no token form', !(await has(A, '#tokenform')));
  123. await A.click('#loginbutton');
  124. await A.waitForSelector('#chooselist', { timeout: 10000 });
  125. await hydrated(A);
  126. await A.click('#chooselist li:nth-child(1) .choose');
  127. await A.waitForSelector('#nameform');
  128. await hydrated(A);
  129. await A.type('#displayname', 'alice');
  130. await A.click('#namesave');
  131. await A.waitFor('!document.querySelector("#nameform") && !!document.querySelector("#createform")', { label: 'named' });
  132. await A.type('#slug', 'gamma');
  133. await A.type('#description', 'pushed to');
  134. await A.click('#createsave');
  135. await A.waitForSelector('#created');
  136. // ---- the "add code" box of an EMPTY repo (gitoria#20: plain text, only on the Code page) ---------------------
  137. await A.goto(REPO('gamma') + '/code');
  138. await A.waitFor('!!document.querySelector("#addcode")', { label: 'add code box' });
  139. const url = urlOf('gamma', null, null, PORT);
  140. check('empty repo: the "add code" box is plain text (no <details>/<summary>)', await A.evaluate('document.querySelector("#addcode").tagName !== "DETAILS" && !document.querySelector("#addcode summary")'));
  141. check('empty repo: the clone command names the repo address', (await txt(A, '#clonecommand')) === 'git clone ' + url, await txt(A, '#clonecommand'));
  142. const nc = await txt(A, '#newcommands');
  143. check('empty repo: the new-project commands are complete', nc.split('\n').length === 6 && /^git init -b main$/m.test(nc) && nc.includes(`git remote add origin ${url}`) && /git push -u origin main$/.test(nc), nc);
  144. check('empty repo: the existing-project commands are complete', (await txt(A, '#existingcommands')) === `git remote add origin ${url}\ngit branch -M main\ngit push -u origin main`, await txt(A, '#existingcommands'));
  145. check('empty repo: the box links to the access tokens on the main address', (await A.evaluate('document.querySelector("#totokens").getAttribute("href")')) === BASE + '/#tokens');
  146. const first = (await hreq('GET', '/code', `gamma.gitoria.test:${PORT}`)).body;
  147. check('empty repo: the box is in the FIRST html of /code (no script needed)', /id="addcode"/.test(first) && first.includes(`git clone ${url}`), first.slice(first.indexOf('id="addcode"'), first.indexOf('id="addcode"') + 200));
  148. const other = await A.evaluate('1'); // keep the page alive
  149. await A.goto(REPO('nothere') + '/');
  150. await A.waitFor('!!document.querySelector("#missing")');
  151. check('unknown address: no "add code" box', !(await has(A, '#addcode')));
  152. // ---- the token, made in the browser -------------------------------------------------------------------------
  153. await A.goto(BASE + '/');
  154. await A.waitFor('!!document.querySelector("#tokenform")', { label: 'token form' });
  155. await hydrated(A);
  156. check('tokens: logged in, "You have no tokens yet"', await has(A, '#notokens'));
  157. await A.click('#tokensave');
  158. await sleep(300);
  159. await A.type('#tokenname', 'laptop');
  160. await A.click('#tokensave');
  161. await A.waitFor('!!document.querySelector("#tokensecret")', { label: 'token shown' });
  162. const TOKEN = (await txt(A, '#tokensecret')).trim();
  163. check('tokens: a token is shown once (gtr_ + 40 hex), listed by its name', /^gtr_[0-9a-f]{40}$/.test(TOKEN) && (await txt(A, '#tokenlist')).includes('laptop') && !(await has(A, '#notokens')), TOKEN);
  164. const stored = readFileSync(join(STORE, 'mpackdb', readdirOf(join(STORE, 'mpackdb')).find(n => n.startsWith('tokens.'))), 'latin1');
  165. // hybriel 64527baa: the face's ack syncs `tokens` from the server; the new row must not be listed twice
  166. check('tokens: the first token is listed ONCE', (await A.evaluate('document.querySelectorAll("#tokenlist li").length')) === 1, await txt(A, '#tokenlist'));
  167. check('tokens: only the hash is stored, never the token', !stored.includes(TOKEN.slice(4)), 'the token text is in the table file');
  168. await A.type('#tokenname', 'second');
  169. await A.click('#tokensave');
  170. await A.waitFor('document.querySelectorAll("#tokenlist li").length === 2', { label: 'two tokens' });
  171. const SECOND = (await txt(A, '#tokensecret')).trim();
  172. check('tokens: a second token replaces the shown secret', /^gtr_[0-9a-f]{40}$/.test(SECOND) && SECOND !== TOKEN, J({ TOKEN, SECOND }));
  173. // ---- real git: clone the empty repo, push, clone, fetch, push again ---------------------------------------------
  174. const W = (n) => join(WORK, n);
  175. let g = await git(WORK, ['clone', urlOf('gamma'), 'first'], 'gamma');
  176. check('git: cloning the empty repo works (an empty repository)', g.code === 0 && /empty repository/.test(g.out), J(g));
  177. writeFileSync(join(W('first'), 'README.md'), '# gamma\n\nPushed with **git**.\n');
  178. writeFileSync(join(W('first'), 'big.bin'), randomBytes(4000000));
  179. await git(W('first'), ['add', '.']); await git(W('first'), ['commit', '-qm', 'first commit']);
  180. g = await git(W('first'), ['push', 'origin', 'main'], 'gamma');
  181. check('git: a push without credentials is refused and says how to get a token', g.code !== 0 && /terminal prompts disabled|could not read Username|401/.test(g.out) && g.out.length > 0, g.out);
  182. const rawNo = await hreq('POST', '/gamma.git/git-receive-pack', `gamma.gitoria.test:${PORT}`, {}, '0000');
  183. check('git: the push endpoint answers 401 with a Basic challenge and the hint', rawNo.status === 401 && /Basic/.test(rawNo.headers['www-authenticate'] || '') && /token/.test(rawNo.body), J(rawNo));
  184. g = await git(W('first'), ['push', urlOf('gamma', 'alice', 'gtr_' + '0'.repeat(40)), 'main'], 'gamma');
  185. check('git: a token nobody made is refused', g.code !== 0 && /Authentication failed|not valid|401/.test(g.out), g.out);
  186. // bob logs in through his own ident identity: another user's token must not push to alice's repo
  187. const bobCode = await ident.selectorCode(bob, APPKEY, BASE);
  188. const bl = await fetch(API + '/login/callback?ident_code=' + bobCode, { redirect: 'manual' });
  189. const bobCookie = (bl.headers.get('set-cookie') || '').split(';')[0];
  190. const bobName = await fetch(API + '/__hl/emit', { method: 'POST', headers: { 'content-type': 'application/json', cookie: bobCookie }, body: J({ t: 'emit', i: 1, event: 'gitoriaSaveName', payload: ['bob'] }) });
  191. const bt = await fetch(API + '/__hl/emit', { method: 'POST', headers: { 'content-type': 'application/json', cookie: bobCookie }, body: J({ t: 'emit', i: 2, event: 'gitoriaMakeToken', payload: ['bobs'] }) });
  192. const BOBTOKEN = (JSON.parse(await bt.text()).value || {}).token;
  193. check('tokens: bob makes his own token (a face, own session)', /^gtr_[0-9a-f]{40}$/.test(BOBTOKEN || ''), String(BOBTOKEN));
  194. g = await git(W('first'), ['push', urlOf('gamma', 'bob', BOBTOKEN), 'main'], 'gamma');
  195. check('git: another user\'s token may not push — only the owner may', g.code !== 0 && /only the owner|403/.test(g.out), g.out);
  196. g = await git(W('first'), ['push', urlOf('gamma', 'alice', TOKEN), 'main'], 'gamma');
  197. check('git: the owner pushes with the token (a 4 MB pack too)', g.code === 0 && /main -> main/.test(g.out), g.out);
  198. g = await git(WORK, ['clone', '-q', urlOf('gamma'), 'second'], 'gamma');
  199. check('git: a clone needs no login (public read) and has the pushed commit and file', g.code === 0 && readFileSync(join(W('second'), 'README.md'), 'utf8').includes('Pushed with') && readFileSync(join(W('second'), 'big.bin')).equals(readFileSync(join(W('first'), 'big.bin'))), g.out);
  200. check('git: the bare repo on the server holds the commit', await (await git(W('first'), ['--git-dir', join(STORE, 'git', 'gamma.git'), 'log', '--format=%s'])).out.trim() === 'first commit');
  201. // hybriel#89: hl:http1 reads a chunked request body — a big push straight to the server (no proxy) works (was a 411 before)
  202. writeFileSync(join(W('first'), 'direct.bin'), randomBytes(4000000));
  203. await git(W('first'), ['add', '.']); await git(W('first'), ['commit', '-qm', 'direct push']);
  204. g = await git(W('first'), ['-c', 'http.postBuffer=65536', 'push', urlOf('gamma', 'alice', TOKEN, PORT), 'main'], 'gamma', { port: PORT });
  205. check('git: a big push straight to hl:http1 (chunked, no proxy) works (hybriel#89)', g.code === 0 && /main -> main/.test(g.out) && (await git(W('first'), ['--git-dir', join(STORE, 'git', 'gamma.git'), 'log', '-1', '--format=%s'])).out.trim() === 'direct push', g.out);
  206. // a fetch with many local commits sends a gzip-compressed request
  207. for (let i = 0; i < 300; i++) { await git(W('second'), ['commit', '-q', '--allow-empty', '-m', 'local ' + i]); }
  208. writeFileSync(join(W('first'), 'more.txt'), 'more\n'); await git(W('first'), ['add', '.']); await git(W('first'), ['commit', '-qm', 'second commit']);
  209. g = await git(W('first'), ['push', urlOf('gamma', 'alice', TOKEN), 'main'], 'gamma');
  210. check('git: a second push (fast-forward) works', g.code === 0, g.out);
  211. g = await git(W('second'), ['fetch', 'origin'], 'gamma');
  212. check('git: a fetch with 300 unpushed local commits (gzip request) works', g.code === 0, g.out);
  213. g = await git(W('second'), ['pull', '--no-rebase', '--no-edit', 'origin', 'main'], 'gamma');
  214. check('git: pull merges the new commit', g.code === 0 && existsSync(join(W('second'), 'more.txt')), g.out);
  215. g = await git(W('first'), ['-c', 'protocol.version=0', 'ls-remote', urlOf('gamma')], 'gamma');
  216. check('git: protocol v0 works too', g.code === 0 && /refs\/heads\/main/.test(g.out), g.out);
  217. g = await git(W('first'), ['-c', 'protocol.version=2', 'ls-remote', urlOf('gamma')], 'gamma');
  218. check('git: protocol v2 works', g.code === 0 && /refs\/heads\/main/.test(g.out), g.out);
  219. // ---- odd requests ---------------------------------------------------------------------------------------------
  220. const st = async (path, o = {}) => (await hreq(o.method || 'GET', path, `gamma.gitoria.test:${PORT}`, o.headers || {}, o.body)).status;
  221. check('transport: dumb-protocol / unknown service → 403', (await st('/gamma.git/info/refs')) === 403 && (await st('/gamma.git/info/refs?service=git-evil')) === 403);
  222. check('transport: another repo name in the path / an unknown repo → 404', (await st('/beta.git/info/refs?service=git-upload-pack')) === 404 && (await st('/gamma/info/refs?service=git-upload-pack')) === 404);
  223. check('transport: wrong methods → 405', (await st('/gamma.git/git-upload-pack')) === 405 && (await st('/gamma.git/info/refs?service=git-upload-pack', { method: 'POST' })) === 405);
  224. check('transport: the main address has no git', (await hreq('GET', '/gamma.git/info/refs?service=git-upload-pack', `gitoria.test:${PORT}`)).status === 404);
  225. check('transport: a Basic header that is not base64 is just "no credentials" → 401', (await st('/gamma.git/git-receive-pack', { method: 'POST', body: '0000', headers: { authorization: 'Basic $(touch /tmp/pwned)' } })) === 401 && !existsSync('/tmp/pwned'));
  226. check('transport: no temp files left behind', !existsSync(join(STORE, 'git', '.tmp')) || readdirOf(join(STORE, 'git', '.tmp')).length === 0, J(readdirOf(join(STORE, 'git', '.tmp'))));
  227. // ---- the box on a repo with code; the pushed code shows; remove a token -----------------------------------------
  228. await A.goto(REPO('gamma') + '/');
  229. await A.waitFor('!!document.querySelector("#homepage")', { label: 'README of the pushed code' });
  230. check('pushed code: the Readme page shows the pushed README', /Pushed with/.test(await txt(A, '#homepage')));
  231. check('pushed code: no "add code" box on the Readme', !(await has(A, '#addcode')));
  232. await A.goto(REPO('gamma') + '/code');
  233. await A.waitFor('!!document.querySelector("#crumbs")', { label: '/code' });
  234. check('pushed code: /code has no "add code" box once there is a commit (gitoria#20)', !(await has(A, '#addcode')));
  235. await A.goto(BASE + '/');
  236. await A.waitFor('document.querySelectorAll("#tokenlist li").length === 2', { label: 'tokens after reload' });
  237. await hydrated(A);
  238. check('tokens: the list survives a reload; the secret is not shown again', !(await has(A, '#tokensecret')));
  239. await A.evaluate('document.querySelector("#tokenlist li .removetoken").click()');
  240. await A.waitFor('document.querySelectorAll("#tokenlist li").length === 1', { label: 'one token removed' });
  241. const gone = (await txt(A, '#tokenlist')).includes('laptop') ? 'laptop' : 'second';
  242. // removed one is the newest = "second"
  243. g = await git(W('first'), ['push', urlOf('gamma', 'alice', SECOND), 'main'], 'gamma');
  244. check('tokens: a removed token stops working at once', g.code !== 0 && /not valid|Authentication failed|401/.test(g.out) && gone === 'laptop', g.out + ' / left: ' + gone);
  245. g = await git(W('first'), ['push', urlOf('gamma', 'alice', TOKEN), 'main'], 'gamma');
  246. check('tokens: the other token still works', g.code === 0, g.out);
  247. await A.evaluate('document.querySelector("#tokenlist li .removetoken").click()');
  248. await A.waitFor('!document.querySelector("#tokenlist li") && !!document.querySelector("#notokens")', { label: 'all removed' });
  249. await A.click('#logout');
  250. await A.waitFor('!document.querySelector("#tokenform")', { label: 'token form gone at logout' });
  251. check('tokens: logout hides the token form', !(await has(A, '#tokenform')) && !(await has(A, '#tokensecret')));
  252. // face forgery
  253. const fk = await fetch(API + '/__hl/emit', { method: 'POST', headers: { 'content-type': 'application/json' }, body: J({ t: 'emit', i: 3, event: 'gitoriaMakeToken', payload: ['x', { user: { id: 'y' } }] }) });
  254. check('tokens: a forged session argument makes no token', !/gtr_/.test(await fk.text()));
  255. // layout
  256. for (const [w, name] of [[390, 'phone'], [1280, 'wide']]) {
  257. await A.send('Emulation.setDeviceMetricsOverride', { width: w, height: 900, deviceScaleFactor: 1, mobile: w < 600 });
  258. await A.goto(REPO('gamma') + '/code');
  259. await A.waitFor('!!document.querySelector("#crumbs")');
  260. check(`layout ${w}px: the pushed repo's code page has no horizontal overflow`, await noOverflow(A));
  261. const { data } = await A.send('Page.captureScreenshot', { format: 'png', captureBeyondViewport: true });
  262. writeFileSync(join(SCRATCH, `push-${name}.png`), Buffer.from(data, 'base64'));
  263. }
  264. const problems = A.problems().filter(m => !/favicon|ERR_|Failed to load resource/.test(m.text));
  265. check('browser: no console errors', problems.length === 0, problems.map(m => m.text).join(' | '));
  266. check('server log: no error other than absorbed ones', !/error(?!: absorbed)/i.test(log.replace(/error absorbed[^\n]*/g, '')), log.split('\n').filter(l => /error/i.test(l)).slice(0, 5).join(' | '));
  267. } catch (e) {
  268. failures++;
  269. console.log('FAIL gate crashed — ' + (e && e.stack || e));
  270. if (pageA) { try { console.log('DOM: ' + (await pageA.evaluate('document.querySelector("#tokens") ? document.querySelector("#tokens").outerHTML.slice(0, 1500) : location.href'))); } catch {} }
  271. if (pageA) console.log('page problems: ' + J(pageA.problems().map(m => m.text.slice(0, 300))));
  272. } finally {
  273. for (const b of browsers) { try { await b.close(); } catch {} }
  274. if (proxy) proxy.close();
  275. await stopServer();
  276. if (ident) await ident.stop();
  277. writeFileSync(join(SCRATCH, 'push-server.log'), log);
  278. console.log(`${passes} passed, ${failures} failed`);
  279. process.exit(failures ? 1 : 0);
  280. }
  281. function readdirOf(d) { try { return require_readdir(d); } catch { return []; } }
  282. import { readdirSync as require_readdir } from 'node:fs';

Branches

Latest commits

  • 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