dp-zp-agent/scripts/verify_project.py
2026-10-26 19:23:02 +01:00

174 lines
8.3 KiB
Python

#!/usr/bin/env python3
"""Verify local code, tests, databases and Docker without calling an external LLM."""
from __future__ import annotations
import argparse
import ast
import ipaddress
import json
import os
from pathlib import Path
import re
import shlex
import socket
import sqlite3
import subprocess
import sys
import time
import urllib.request
ROOT = Path(__file__).resolve().parents[1]
def local_test_worker() -> int:
"""Allow live local API/Neo4j tests, forbid outbound model calls."""
os.environ.pop('OPENWEBUI_API_KEY', None)
for line in (ROOT / '.env').read_text().splitlines():
key, separator, value = line.partition('=')
if separator and key in {'NEO4J_URI', 'NEO4J_USER', 'NEO4J_PASSWORD', 'NEO4J_DATABASE'}:
values = shlex.split(value)
if values:
os.environ[key] = values[0]
for key in ['RUN_NEO4J_TESTS', 'RUN_LIVE_TESTS', 'RUN_LIVE_RAG_E2E', 'HF_HUB_OFFLINE', 'TRANSFORMERS_OFFLINE']:
os.environ[key] = '1'
original_connect, original_connect_ex = socket.socket.connect, socket.socket.connect_ex
original_dns = socket.getaddrinfo
def permitted(address):
try:
return address[1] in {7687, 8000} and ipaddress.ip_address(address[0]).is_loopback
except (ValueError, TypeError, IndexError):
return False
def guarded_connect(self, address):
if self.family in {socket.AF_INET, socket.AF_INET6} and not permitted(address):
raise RuntimeError('External network disabled during local verification')
return original_connect(self, address)
def guarded_connect_ex(self, address):
if self.family in {socket.AF_INET, socket.AF_INET6} and not permitted(address):
raise RuntimeError('External network disabled during local verification')
return original_connect_ex(self, address)
def guarded_dns(host, port, *args, **kwargs):
if host not in {'localhost', '127.0.0.1', '::1'} or int(port) not in {7687, 8000}:
raise RuntimeError('External DNS disabled during local verification')
return original_dns(host, port, *args, **kwargs)
socket.socket.connect, socket.socket.connect_ex = guarded_connect, guarded_connect_ex
socket.getaddrinfo = guarded_dns
import pytest
return pytest.main(['-q', 'test', 'tools/rag/tests', 'tools/graphrag/tests'])
def verify_code() -> dict:
files = [path for name in ['app', 'tools', 'test', 'scripts'] for path in (ROOT / name).rglob('*.py')]
for path in files:
text = path.read_text()
compile(text, str(path), 'exec')
for node in ast.walk(ast.parse(text)):
modules = ([node.module] if isinstance(node, ast.ImportFrom) and node.module else
[item.name for item in node.names] if isinstance(node, ast.Import) else [])
for module in modules:
if module.split('.')[0] in {'tools', 'app'}:
target = ROOT.joinpath(*module.split('.'))
if not target.with_suffix('.py').is_file() and not (target / '__init__.py').is_file():
raise RuntimeError(f'Unresolved internal module {module} in {path.relative_to(ROOT)}')
shells = [path for name in ['scripts', 'tools'] for path in (ROOT / name).rglob('*.sh')]
for path in shells:
subprocess.run(['bash', '-n', str(path)], check=True)
return {'compiled_python_files': len(files), 'checked_shell_scripts': len(shells)}
def verify_database() -> dict:
path = ROOT / 'data/zp_index.sqlite'
with sqlite3.connect(path.resolve().as_uri() + '?mode=ro', uri=True) as conn:
assert conn.execute('PRAGMA quick_check').fetchone()[0] == 'ok', 'SQLite integrity failed'
assert not conn.execute('PRAGMA foreign_key_check').fetchall(), 'SQLite foreign keys failed'
counts = {table: conn.execute(f'SELECT count(*) FROM {table}').fetchone()[0]
for table in ['documents', 'chunks', 'chunks_fts', 'chunk_embeddings']}
assert counts['chunks'] == counts['chunks_fts'] and counts['chunks'] > 0, 'FTS count mismatch'
return counts
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--docker-context', default='default')
parser.add_argument('--git-network', action='store_true', help='Read-only probe of wiki origin over its configured SSH connection')
parser.add_argument('--output', type=Path, default=ROOT / 'data/project_verification.json')
parser.add_argument('--test-worker', action='store_true', help=argparse.SUPPRESS)
args = parser.parse_args()
os.chdir(ROOT)
sys.path.insert(0, str(ROOT))
if args.test_worker:
return local_test_worker()
report = {'checks': {}, 'external_llm_calls': False}
args.output.parent.mkdir(parents=True, exist_ok=True)
def check(name, action):
started = time.monotonic()
try:
result = action()
record = {'passed': True, 'details': result}
except Exception as error:
record = {'passed': False, 'error_type': type(error).__name__}
record['seconds'] = round(time.monotonic() - started, 2)
report['checks'][name] = record
args.output.write_text(json.dumps(report, ensure_ascii=False, indent=2) + '\n')
print(f'{name}: {"OK" if record["passed"] else "FAILED"}', flush=True)
def command(*argv):
result = subprocess.run(argv, text=True, capture_output=True, timeout=90)
if result.returncode:
raise RuntimeError('Verification command failed; inspect the service locally')
return result.stdout.strip()
docker = ['docker', '--context', args.docker_context]
compose = [*docker, 'compose', '--project-directory', str(ROOT)]
check('python_and_shell', verify_code)
check('git_diff', lambda: command('git', 'diff', '--check'))
check('git_integrity', lambda: command('git', 'fsck', '--connectivity-only'))
check('compose_configuration', lambda: command(*compose, 'config', '--quiet'))
def health():
with urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=15) as response:
value = json.load(response)
assert value['ready'] and all(tool['ready'] for tool in value['tools'].values())
assert value['tools']['rag']['index_version'] == value['tools']['graphrag']['index_version']
return {'ready': True, 'index_version': value['index_version'],
'tools': {name: item['ready'] for name, item in value['tools'].items()}}
check('api_and_both_indexes', health)
check('sqlite_integrity', verify_database)
check('container_readiness', lambda: command(*compose, 'exec', '-T', 'zp-agent-api',
'python', '-m', 'tools.runtime.runtime_health'))
if args.git_network:
probe = ("import os,subprocess; from tools.sync.wiki_git import expected_branch; "
"p=subprocess.run(['git','-C',os.environ['ZPWIKI_ROOT'],'ls-remote','--exit-code',"
"'origin','refs/heads/'+expected_branch()],capture_output=True,text=True,timeout=60); "
"assert p.returncode==0,'Wiki origin unavailable'; print(p.stdout.strip())")
check('wiki_git_ssh', lambda: command(*compose, 'exec', '-T', 'zp-agent-api', 'python', '-c', probe))
def tests():
log = args.output.with_suffix('.pytest.log')
environment = {**os.environ}
environment.pop('OPENWEBUI_API_KEY', None)
with log.open('w') as output:
result = subprocess.run([sys.executable, str(Path(__file__).resolve()), '--test-worker'],
env=environment, stdout=output, stderr=subprocess.STDOUT, timeout=900)
assert result.returncode == 0, 'Regression failed; see local pytest log'
text = log.read_text()
counts = {name: int(value) for value, name in re.findall(r'(\d+) (passed|failed|skipped)', text)}
assert counts.get('passed', 0) > 0 and counts.get('skipped', 0) == 0
return {**counts, 'log': str(log), 'external_network_blocked': True}
check('full_local_regression', tests)
report['passed'] = all(item['passed'] for item in report['checks'].values())
args.output.write_text(json.dumps(report, ensure_ascii=False, indent=2) + '\n')
print('Report: ' + str(args.output))
return 0 if report['passed'] else 1
if __name__ == '__main__':
raise SystemExit(main())