#!/usr/bin/env python3 """Owned worktree gate, with a fresh process group and bounded cleanup per command.""" import json, os, signal, subprocess, sys, time from pathlib import Path root = Path(__file__).resolve().parents[4] scratch = Path(sys.argv[1]); scratch.mkdir(parents=True, exist_ok=True) env = {**os.environ, 'TMPDIR': str(scratch), 'MOSAIC_REQUIRE_EDITOR_BUILD': '1'} env.pop('MOSAIC_LIVE_PROVIDER', None) node = ['node', '--test', '--test-reporter=tap', '--test-concurrency=1', '--test-timeout=30000'] steps = [('editor-build', 120, ['npm', 'run', 'build:editor'], root / 'packages/webui'), ('webui-full', 900, node + ['packages/webui/tests/'], root), ('plugins-full', 300, node + ['packages/plugins/tests/'], root), ('bus-full', 1200, node + ['packages/bus/tests/'], root)] steps += [(p.stem, 1200, ['bash', str(p)], root) for p in sorted((root / 'scripts').glob('test-*.sh'))] if '--scripts-only' in sys.argv[2:]: steps = [step for step in steps if step[0].startswith('test-')] if '--t9-reruns' in sys.argv[2:]: steps = [(f't9-rerun-{i}', 120, node + ['packages/bus/tests/library-credential.test.mjs'], root) for i in range(1, 4)] + [('bus-rerun', 1200, node + ['packages/bus/tests/'], root)] results = [] summary = scratch / ('scripts-summary.json' if '--scripts-only' in sys.argv[2:] else 'rerun-summary.json' if '--t9-reruns' in sys.argv[2:] else 'gate-summary.json') for name, limit, command, cwd in steps: started = time.monotonic(); signals = [] with (scratch / (name + '.tap')).open('w') as log: child = subprocess.Popen(['timeout', '-k', '10', str(limit), *command], cwd=cwd, env=env, stdout=log, stderr=subprocess.STDOUT, start_new_session=True) try: code = child.wait(timeout=limit + 15) except subprocess.TimeoutExpired: os.killpg(child.pid, signal.SIGKILL); signals.append('KILL'); code = child.wait() finally: # This group was created for this command; never signal any other seat's group. try: os.killpg(child.pid, signal.SIGTERM); signals.append('TERM'); time.sleep(.2) try: os.killpg(child.pid, signal.SIGKILL); signals.append('KILL') except ProcessLookupError: pass except ProcessLookupError: pass entry = {'step': name, 'command': command, 'cwd': str(cwd), 'outerSeconds': limit, 'exit': code, 'seconds': round(time.monotonic()-started, 3), 'cleanupSignals': signals} results.append(entry); summary.write_text(json.dumps(results, indent=2) + '\n') print(json.dumps(entry), flush=True) if code: sys.exit(code if 0 < code < 126 else 1)