"""Check development startup/cleanup with stubs; never touch running services.""" import os from pathlib import Path import signal import subprocess import tempfile import time ROOT = Path(__file__).resolve().parents[1] with tempfile.TemporaryDirectory(prefix='detective-dev-') as directory: root = Path(directory) (root / 'scripts').mkdir() (root / 'scripts/dev.sh').write_text((ROOT / 'scripts/dev.sh').read_text()) (root / 'evening_detective_server').mkdir() (root / 'evening_detective_frontend/node_modules').mkdir(parents=True) bin_dir = root / 'bin' bin_dir.mkdir() log = root / 'commands.log' env = {**os.environ, 'PATH': str(bin_dir) + os.pathsep + os.environ['PATH'], 'TEST_LOG': str(log)} for name in ['docker', 'make', 'npm']: stub = bin_dir / name stub.write_text( '#!/usr/bin/env bash\n' f'echo "{name} $*" >> "$TEST_LOG"\n' + ('exit 0\n' if name == 'docker' else f'trap \'echo "{name} stopped" >> "$TEST_LOG"; exit 0\' TERM\n' 'sleep 60 &\n' 'wait\n') ) stub.chmod(0o755) script = str(root / 'scripts/dev.sh') result = subprocess.run(['bash', script], env=env, capture_output=True, text=True) assert result.returncode != 0 and '.env.example' in result.stderr assert not log.exists(), 'Missing config must fail before Docker' (root / 'evening_detective_server/.env').touch() def wait_for(text): deadline = time.monotonic() + 5 while time.monotonic() < deadline: if log.exists() and text in log.read_text(): return time.sleep(.05) raise AssertionError('Missing log entry: ' + text) for sig in [signal.SIGINT, signal.SIGTERM]: log.unlink(missing_ok=True) process = subprocess.Popen(['bash', script], env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE) try: wait_for('npm run dev') wait_for('make -C evening_detective_server run') process.send_signal(sig) process.communicate(timeout=5) assert process.returncode == 128 + sig commands = log.read_text() assert 'compose -f evening_detective_server/docker-compose-db.yml up -d --wait' in commands assert 'make stopped' in commands and 'npm stopped' in commands, commands finally: if process.poll() is None: process.send_signal(signal.SIGTERM) process.communicate(timeout=5) (bin_dir / 'make').write_text('#!/bin/sh\nsleep .2\nexit 7\n') log.unlink(missing_ok=True) result = subprocess.run(['bash', script], env=env, capture_output=True, timeout=5) assert result.returncode == 7 assert 'npm stopped' in log.read_text(), 'Backend failure must stop frontend' stop = subprocess.check_output(['make', '-n', 'stop'], cwd=ROOT, text=True) assert 'venue/docker-compose.yml' in stop and 'venue/docker-compose.https.yml' in stop assert '--env-file venue/.env down' in stop and '-v' not in stop.split() game = subprocess.check_output(['make', '-n', 'game'], cwd=ROOT, text=True) assert game.strip() == 'bash venue/deploy.sh' dev = subprocess.check_output(['make', '-n', 'dev'], cwd=ROOT, text=True) assert dev.strip() == 'bash scripts/dev.sh' print('PASS: command aliases, config preflight, startup, Ctrl+C/SIGTERM cleanup, backend failure; stop preserves volumes')