"""Real CLI restarts: failed-fetch must retain valid per-instance results.""" from pathlib import Path import argparse import json import shutil import subprocess import sys import tempfile from test_public_config import make_rsync_wrapper, read_json, run_checked, write_config def run(binary: str, evidence: Path | None = None, expect_no_cache: bool = False) -> dict: proof = {'runs': []} with tempfile.TemporaryDirectory(prefix='panda-failed-fetch-') as temp: base = Path(temp) fixture = base / 'fixture' run_checked([sys.executable, str(Path(__file__).with_name('generate_repository.py')), '--output', str(fixture), '--rsync-host', '127.0.0.1', '--strict-names']) repository = base / 'repository' shutil.copytree(fixture / 'cases/baseline-v1/repository', repository) wrapper = base / 'rsync.py' log = base / 'rsync.log' make_rsync_wrapper(wrapper, fixture, log) wrapper.write_text(wrapper.read_text().replace(str(fixture / 'cases/baseline-v1/repository'), str(repository))) wrapper.chmod(0o755) baseline = None for name in ('initial', 'missing-object', 'missing-parent', 'recovered'): if name == 'missing-object': (repository / 'child/valid.roa').write_bytes(b'corrupted downloaded ROA') if name == 'missing-parent': (repository / 'root/root.mft').write_bytes(b'corrupted downloaded manifest') if name == 'recovered': shutil.rmtree(repository) shutil.copytree(fixture / 'cases/baseline-v2/repository', repository) out = base / name config = base / 'validate.toml' write_config(config, fixture=fixture, output=out, state=base / 'state', ccr=out / 'result.ccr', wrapper=wrapper, mirror=base / 'mirror', sync_preference='rsync-only', rrdp_mode='auto', validation_time='2026-06-01T00:00:00Z', resource_mode='rfc6487', scope='publication-point') start = len(log.read_text().splitlines()) if log.exists() else 0 result = subprocess.run([binary, 'validate', '--config', str(config)], capture_output=True, text=True, timeout=120) summary = read_json(out / 'summary.json') expected = 2 if name == 'recovered' else (0 if expect_no_cache and name.startswith('missing') else 1) assert summary['vrps'] == expected, (name, summary, result.stderr[-4000:]) if name.startswith('missing'): assert summary['resultStatus'] == 'partial', summary assert result.returncode == 3, result else: assert summary['resultStatus'] == 'success' and result.returncode == 0, (summary, result.stderr[-3000:]) csv = (out / 'vrps.csv').read_bytes() if name == 'initial': baseline = csv elif name.startswith('missing') and not expect_no_cache: assert baseline == csv, 'failed-fetch changed still-valid payloads' requests = log.read_text().splitlines()[start:] if name == 'missing-parent': assert len(requests) == 1, ('cached subtree must not fetch children', requests) proof['runs'].append({'name': name, 'status': summary['resultStatus'], 'vrps': summary['vrps'], 'exit': result.returncode, 'rsync_requests': requests}) if evidence: evidence.write_text(json.dumps(proof, indent=2) + '\n') return proof if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('binary') parser.add_argument('--evidence', type=Path) parser.add_argument('--expect-no-cache', action='store_true') args = parser.parse_args() print(json.dumps(run(args.binary, args.evidence, args.expect_no_cache)))