20261016 修复 Manifest failed-fetch 缓存与子树恢复
Some checks failed
ci / rust (push) Failing after 4s
ci / docker (push) Failing after 1m27s
ci / audit (push) Failing after 3s

This commit is contained in:
2026-10-16 15:53:40 +08:00
parent 5f46837b61
commit 2d7501db08
28 changed files with 1565 additions and 143 deletions

View File

@@ -0,0 +1,72 @@
"""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)))