"""Offline source packaging guards; no image build, service or Docker calls.""" from html.parser import HTMLParser from pathlib import Path import re ROOT = Path(__file__).resolve().parents[2] class AssetReferences(HTMLParser): def __init__(self): super().__init__() self.paths = [] def handle_starttag(self, tag, attrs): attrs = dict(attrs) path = attrs.get('src') if tag == 'script' else attrs.get('href') if tag == 'link' and attrs.get('rel') == 'stylesheet' else None if path and not path.startswith(('http:', 'https:', '//', 'data:')): self.paths.append(path.split('?')[0]) def test_speech_image_packages_coordinator_and_all_entrypoint_assets(): dockerfile = (ROOT/'Dockerfile').read_text() for source, destination in ( ('agent/agent.py', '/opt/voice-agent/agent.py'), ('agent/hermes_llm.py', '/opt/voice-agent/hermes_llm.py'), ('agent/conversation_llm.py', '/opt/voice-agent/conversation_llm.py'), ): assert (ROOT/source).is_file() assert re.search(r'^COPY\s+'+re.escape(source)+r'\s+'+re.escape(destination)+r'\s*$', dockerfile, re.M), source references = AssetReferences() references.feed((ROOT/'web/index.html').read_text()) assert {'chat.js', 'features.js', 'hope-themes.css'} <= set(references.paths) for path in references.paths: assert (ROOT/'web'/path).is_file(), path assert re.search(r'^COPY\s+web/'+re.escape(path)+r'\s+/var/www/voice/\s*$', dockerfile, re.M), path assert 'chmod -R a+rX /var/www/voice' in dockerfile assert 'chmod 644 /opt/voice-agent/*.py /opt/voice/*.py' in dockerfile assert '.venv-tests' in (ROOT/'.dockerignore').read_text().splitlines() def test_control_packages_chat_imports_pdf_and_mounted_private_roots(): dockerfile = (ROOT/'control.Dockerfile').read_text() assert re.search(r'^COPY\s+control\s+/app/control\s*$', dockerfile, re.M) assert re.search(r'\bpypdf==6\.\d+\.\d+', dockerfile) for module in ('chat_api', 'chat_store', 'chat_runs', 'chat_features', 'chat_exports'): assert (ROOT/'control'/(module+'.py')).is_file() deploy = (ROOT/'scripts/deploy-unraid.sh').read_text() assert '-e CONTROL_DB_PATH=/var/lib/hope/accounts.sqlite3' in deploy assert '-e HOPE_HERMES_DATA_DIR="$ROOT/hermes"' in deploy assert '-v "$ROOT/accounts:/var/lib/hope"' in deploy assert '-v "$ROOT/hermes:$ROOT/hermes"' in deploy app = (ROOT/'control/app.py').read_text() assert "Path(settings.db_path).absolute().parent / 'chat-features'" in app def test_nginx_preserves_strict_auth_and_allows_declared_advanced_bodies(): nginx = (ROOT/'nginx.conf').read_text() assert re.search(r'client_max_body_size\s+16k;', nginx) for route, limit in ((r'/api/attachments', '20m'), (r'~ ^/api/(artifacts|memory)(/|$)', '1m'), (r'/api/tasks', '64k')): block = re.search(r'location\s+'+re.escape(route)+r'\s*\{([^}]+)\}', nginx) assert block is not None, route assert f'client_max_body_size {limit};' in block[1] assert 'proxy_pass http://127.0.0.1:8092;' in block[1] assert re.search(r'location\s+/internal/\s*\{\s*return 404;', nginx) api = re.search(r'location\s+/api/\s*\{([^}]+)\}', nginx)[1] assert 'proxy_buffering off;' in api and 'proxy_read_timeout 700s;' in api def test_ci_qualification_gates_images_and_deployment_without_live_conftest(): workflow = (ROOT/'.gitea/workflows/rebuild.yml').read_text() assert workflow.index('Qualify offline') < workflow.index('Build images') < workflow.index('Deploy using') assert '--noconftest -p no:cacheprovider' in workflow for name in ('test_chat_packaging.py', 'test_chat_exports.py', 'test_chat_cross_client.py', 'test_chat_security_review.py', 'test_deploy_rollback.py'): assert 'tests/unit/'+name in workflow assert 'pytest-asyncio -r control/requirements.txt' in workflow assert 'scripts/generate_hope_themes.py --check' in workflow assert 'git archive HEAD | docker run --rm -i' in workflow assert 'tar -xf - -C /tmp/src' in workflow assert '-v "$PWD:/src' not in workflow