antcolony
All repositories: gitoria
6.5 KB
// tests/e2e-sandbox.mjs — antcolony#18 "each worker in its own sealed box": what a session sees inside the bubblewrap box.// No tickets, no Claude: `./colony box <project>` prints the box's argv prefix, the test runs commands inside it.// COLONY_E2E_SANDBOX_DIR (default .scratch/e2e-sandbox)import { spawnSync } from 'node:child_process';import { mkdirSync, rmSync, writeFileSync, existsSync } from 'node:fs';import { dirname, join, resolve } from 'node:path';import { fileURLToPath } from 'node:url';const APP = resolve(dirname(fileURLToPath(import.meta.url)), '..');const W = process.env.COLONY_E2E_SANDBOX_DIR || join(APP, '.scratch', 'e2e-sandbox');let passes = 0, failures = 0;const check = (name, ok, detail = '') => { if (ok) { passes++; console.log('ok ' + name); } else { failures++; console.log('FAIL ' + name + (detail ? ' — ' + detail : '')); } };rmSync(W, { recursive: true, force: true });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');for (const d of [P, APPD, LIB, OTHER, LIVE]) mkdirSync(d, { recursive: true });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' } } : {}) }));reg('app', { dev: APPD, deps: ['lib'], live: LIVE });reg('lib', { dev: LIB });reg('other', { dev: OTHER });for (const [d, t] of [[APPD, 'app'], [LIB, 'lib'], [OTHER, 'other'], [LIVE, 'live']]) writeFileSync(join(d, 'file.txt'), t);writeFileSync(join(APPD, '.env'), 'SECRET=1');writeFileSync(join(LIB, '.env'), 'SECRET=2');writeFileSync(join(APPD, '.env.example'), 'SECRET=');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' };const box = (project, extra = [], e = env) => {const r = spawnSync(join(APP, 'colony'), ['box', project, ...extra], { env: e, encoding: 'utf8' });return { code: r.status, args: r.stdout.split('\n').filter(Boolean), out: r.stdout + r.stderr };};const inside = (b, script) => spawnSync(b.args[0], [...b.args.slice(1), 'sh', '-c', script], { env, encoding: 'utf8', timeout: 30000 });const HOME = process.env.HOME;const b = box('app');check('box prints a bwrap command', b.args[0] === '/usr/bin/bwrap' && b.args.includes('--die-with-parent'), b.out);let r = inside(b, `cat ${APPD}/file.txt; echo; cat ${LIB}/file.txt`);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);r = inside(b, `echo x > ${APPD}/new.txt && echo wrote`);check('writes into its own folder', r.stdout.trim() === 'wrote' && existsSync(join(APPD, 'new.txt')), r.stderr);r = inside(b, `echo x > ${LIB}/new.txt; echo rc=$?`);check('cannot write into a dependency folder (read-only)', /rc=[1-9]/.test(r.stdout) && !existsSync(join(LIB, 'new.txt')), r.stdout + r.stderr);r = inside(b, `ls ${OTHER} 2>&1; echo rc=$?`);check('does not see a project it does not depend on', /rc=[1-9]/.test(r.stdout) && !/file.txt/.test(r.stdout), r.stdout);r = inside(b, `ls ${W}/apps 2>&1`);check('the folder beside its own lists only what is bound', r.stdout.split('\n').filter(Boolean).sort().join(',') === 'app,lib', r.stdout);r = inside(b, `cat ${APPD}/.env | wc -c; cat ${LIB}/.env | wc -c; cat ${APPD}/.env.example`);check('.env files are empty inside, examples stay', r.stdout.trim().split('\n').join(',') === '0,0,SECRET=', r.stdout);r = inside(b, `ls -A ${HOME}`);check('home holds only the claude login + toolchain (no .config, .ssh, .bash_history)', !/\.config|\.ssh|bash_history|Downloads|Dokumente/.test(r.stdout), r.stdout);r = inside(b, `ls ${HOME}/.ssh ${LIVE} 2>&1; echo rc=$?`);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);r = inside(b, `echo "t=\${COLONY_TOKEN_FILE:-unset} c=\${COLONY_CREATOR_TOKEN_FILE:-unset}"`);check('the tickets tokens are not in the environment', r.stdout.trim() === 't=unset c=unset', r.stdout);r = inside(b, `ls /media/STORAGE/projects 2>/dev/null ; ls /mnt /root /srv 2>&1 | wc -l`);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);r = inside(b, `ps -e | wc -l`);check('own process namespace (only its own processes)', Number(r.stdout.trim()) < 12, r.stdout);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`);check('tools and the network stack work (node, name lookup)', /ECONNREFUSED/.test(r.stdout) && /localhost/.test(r.stdout), r.stdout + r.stderr);const d = box('app', ['--deploy']);r = inside(d, `echo x > ${LIVE}/deployed.txt && echo wrote; ls -d ${HOME}/.ssh 2>&1 | head -1`);check('a deploy ticket may write the live folder', /wrote/.test(r.stdout) && existsSync(join(LIVE, 'deployed.txt')), r.stdout + r.stderr);check('a deploy ticket gets the ssh keys read-only (when the user has some)', !existsSync(HOME + '/.ssh') || /\.ssh/.test(r.stdout), r.stdout);r = inside(d, `ls ${OTHER} 2>&1; echo rc=$?`);check('a deploy ticket still does not see other projects', /rc=[1-9]/.test(r.stdout), r.stdout);const off = box('app', [], { ...env, COLONY_SANDBOX: 'off' });check('COLONY_SANDBOX=off → no prefix (host access, only for tests)', off.args.length === 0, off.out);// the deploy markconst m = spawnSync(join(APP, 'bin', 'hybriel'), [join(APP, 'tests', 'sandbox-mark.hl')], { env, encoding: 'utf8', cwd: APP });const want = ['summary-mark=true', 'no-mark=false', 'null-events=false', 'non-creator-comment=false'];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);// the scheduler working on itself: the runs of all sessions are not visiblereg('antcolony', { dev: APP });const sb = box('antcolony', [], { ...env, COLONY_HOME: APP });r = inside(sb, `ls ${APP}/runs | wc -l; ls ${APP}/lib | head -1`);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);console.log(`\n${passes} passed, ${failures} failed`);process.exit(failures ? 1 : 0);
Branches
- mainmain branch
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