gitoriaLog in with ident

antcolony

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Main branchmain3a4d0324antcolony#37: a too-long report gets up to 3 fix tries, finished work is never thrown away for lengthmremain/tests/e2e-sandbox.mjs

6.5 KB

  1. // tests/e2e-sandbox.mjs — antcolony#18 "each worker in its own sealed box": what a session sees inside the bubblewrap box.
  2. // No tickets, no Claude: `./colony box <project>` prints the box's argv prefix, the test runs commands inside it.
  3. // COLONY_E2E_SANDBOX_DIR (default .scratch/e2e-sandbox)
  4. import { spawnSync } from 'node:child_process';
  5. import { mkdirSync, rmSync, writeFileSync, existsSync } from 'node:fs';
  6. import { dirname, join, resolve } from 'node:path';
  7. import { fileURLToPath } from 'node:url';
  8. const APP = resolve(dirname(fileURLToPath(import.meta.url)), '..');
  9. const W = process.env.COLONY_E2E_SANDBOX_DIR || join(APP, '.scratch', 'e2e-sandbox');
  10. let passes = 0, failures = 0;
  11. const check = (name, ok, detail = '') => { if (ok) { passes++; console.log('ok ' + name); } else { failures++; console.log('FAIL ' + name + (detail ? ' — ' + detail : '')); } };
  12. rmSync(W, { recursive: true, force: true });
  13. const P = join(W, 'projects'), APPD = join(W, 'apps', 'app'), LIB = join(W, 'apps', 'lib'), OTHER = join(W, 'apps', 'other'), LIVE = join(W, 'live', 'app');
  14. for (const d of [P, APPD, LIB, OTHER, LIVE]) mkdirSync(d, { recursive: true });
  15. const reg = (name, o) => writeFileSync(join(P, name + '.json'), JSON.stringify({ name, concept: '', dev: { host: 'e2e-host', folder: o.dev }, dependsOn: o.deps || [], ...(o.live ? { live: { host: 'e2e-host', folder: o.live, url: 'x' } } : {}) }));
  16. reg('app', { dev: APPD, deps: ['lib'], live: LIVE });
  17. reg('lib', { dev: LIB });
  18. reg('other', { dev: OTHER });
  19. for (const [d, t] of [[APPD, 'app'], [LIB, 'lib'], [OTHER, 'other'], [LIVE, 'live']]) writeFileSync(join(d, 'file.txt'), t);
  20. writeFileSync(join(APPD, '.env'), 'SECRET=1');
  21. writeFileSync(join(LIB, '.env'), 'SECRET=2');
  22. writeFileSync(join(APPD, '.env.example'), 'SECRET=');
  23. const env = { ...process.env, COLONY_PROJECTS_DIR: P, COLONY_HOST: 'e2e-host', COLONY_SANDBOX: 'on', COLONY_SANDBOX_RO: '/nonexistent', COLONY_TOKEN_FILE: '/tmp/none', COLONY_CREATOR_TOKEN_FILE: '/tmp/none2' };
  24. const box = (project, extra = [], e = env) => {
  25. const r = spawnSync(join(APP, 'colony'), ['box', project, ...extra], { env: e, encoding: 'utf8' });
  26. return { code: r.status, args: r.stdout.split('\n').filter(Boolean), out: r.stdout + r.stderr };
  27. };
  28. const inside = (b, script) => spawnSync(b.args[0], [...b.args.slice(1), 'sh', '-c', script], { env, encoding: 'utf8', timeout: 30000 });
  29. const HOME = process.env.HOME;
  30. const b = box('app');
  31. check('box prints a bwrap command', b.args[0] === '/usr/bin/bwrap' && b.args.includes('--die-with-parent'), b.out);
  32. let r = inside(b, `cat ${APPD}/file.txt; echo; cat ${LIB}/file.txt`);
  33. check('sees its own folder and the folder of a project it depends on', r.stdout.split('\n').filter(Boolean).join(',') === 'app,lib', r.stdout + r.stderr);
  34. r = inside(b, `echo x > ${APPD}/new.txt && echo wrote`);
  35. check('writes into its own folder', r.stdout.trim() === 'wrote' && existsSync(join(APPD, 'new.txt')), r.stderr);
  36. r = inside(b, `echo x > ${LIB}/new.txt; echo rc=$?`);
  37. check('cannot write into a dependency folder (read-only)', /rc=[1-9]/.test(r.stdout) && !existsSync(join(LIB, 'new.txt')), r.stdout + r.stderr);
  38. r = inside(b, `ls ${OTHER} 2>&1; echo rc=$?`);
  39. check('does not see a project it does not depend on', /rc=[1-9]/.test(r.stdout) && !/file.txt/.test(r.stdout), r.stdout);
  40. r = inside(b, `ls ${W}/apps 2>&1`);
  41. check('the folder beside its own lists only what is bound', r.stdout.split('\n').filter(Boolean).sort().join(',') === 'app,lib', r.stdout);
  42. r = inside(b, `cat ${APPD}/.env | wc -c; cat ${LIB}/.env | wc -c; cat ${APPD}/.env.example`);
  43. check('.env files are empty inside, examples stay', r.stdout.trim().split('\n').join(',') === '0,0,SECRET=', r.stdout);
  44. r = inside(b, `ls -A ${HOME}`);
  45. check('home holds only the claude login + toolchain (no .config, .ssh, .bash_history)', !/\.config|\.ssh|bash_history|Downloads|Dokumente/.test(r.stdout), r.stdout);
  46. r = inside(b, `ls ${HOME}/.ssh ${LIVE} 2>&1; echo rc=$?`);
  47. check('no ssh keys and no live folder without a deploy mark', /rc=[1-9]/.test(r.stdout) && !/file.txt/.test(r.stdout), r.stdout);
  48. r = inside(b, `echo "t=\${COLONY_TOKEN_FILE:-unset} c=\${COLONY_CREATOR_TOKEN_FILE:-unset}"`);
  49. check('the tickets tokens are not in the environment', r.stdout.trim() === 't=unset c=unset', r.stdout);
  50. r = inside(b, `ls /media/STORAGE/projects 2>/dev/null ; ls /mnt /root /srv 2>&1 | wc -l`);
  51. check('the rest of the disk is gone (/media/STORAGE/projects holds only the bound folders)', r.stdout.split('\n').filter(l => /^[a-z]/.test(l)).join(',') === 'antcolony-scheduler' && r.stdout.trim().endsWith('3'), r.stdout);
  52. r = inside(b, `ps -e | wc -l`);
  53. check('own process namespace (only its own processes)', Number(r.stdout.trim()) < 12, r.stdout);
  54. r = inside(b, `node -e "require('http').get('http://127.0.0.1:1',()=>{}).on('error',e=>console.log(e.code))"; getent hosts localhost | head -1`);
  55. check('tools and the network stack work (node, name lookup)', /ECONNREFUSED/.test(r.stdout) && /localhost/.test(r.stdout), r.stdout + r.stderr);
  56. const d = box('app', ['--deploy']);
  57. r = inside(d, `echo x > ${LIVE}/deployed.txt && echo wrote; ls -d ${HOME}/.ssh 2>&1 | head -1`);
  58. check('a deploy ticket may write the live folder', /wrote/.test(r.stdout) && existsSync(join(LIVE, 'deployed.txt')), r.stdout + r.stderr);
  59. check('a deploy ticket gets the ssh keys read-only (when the user has some)', !existsSync(HOME + '/.ssh') || /\.ssh/.test(r.stdout), r.stdout);
  60. r = inside(d, `ls ${OTHER} 2>&1; echo rc=$?`);
  61. check('a deploy ticket still does not see other projects', /rc=[1-9]/.test(r.stdout), r.stdout);
  62. const off = box('app', [], { ...env, COLONY_SANDBOX: 'off' });
  63. check('COLONY_SANDBOX=off → no prefix (host access, only for tests)', off.args.length === 0, off.out);
  64. // the deploy mark
  65. const m = spawnSync(join(APP, 'bin', 'hybriel'), [join(APP, 'tests', 'sandbox-mark.hl')], { env, encoding: 'utf8', cwd: APP });
  66. const want = ['summary-mark=true', 'no-mark=false', 'null-events=false', 'non-creator-comment=false'];
  67. check('deploy mark: only the exact line `colony-deploy: yes` in the ticket text (or a creator comment) counts', want.every(x => m.stdout.includes(x)), m.stdout + m.stderr);
  68. // the scheduler working on itself: the runs of all sessions are not visible
  69. reg('antcolony', { dev: APP });
  70. const sb = box('antcolony', [], { ...env, COLONY_HOME: APP });
  71. r = inside(sb, `ls ${APP}/runs | wc -l; ls ${APP}/lib | head -1`);
  72. check('the scheduler folder: its own code is there, the runs of all sessions are not', r.stdout.split('\n')[0].trim() === '0' && /\.hl$/.test(r.stdout.split('\n')[1] || ''), r.stdout + r.stderr);
  73. console.log(`\n${passes} passed, ${failures} failed`);
  74. process.exit(failures ? 1 : 0);

Branches

Latest commits

  • 3a4d0324antcolony#37: a too-long report gets up to 3 fix tries, finished work is never thrown away for lengthmre
  • a6af7883tracker: worker box sees calendar.worldapi.org (login to copy)mre
  • c613d26btemplates: bridges to external components (login.js for ident's selector) are allowed (creator 2026-09-27)mre
  • 9062978ctracker: worker box sees /media/STORAGE/projects/old-tracker read-only (tracker#2 source data)mre
  • 7f9660eeState of 2026-09-27, before the move to gitoriamre