"""Guard the distinctions that make evolution reporting honest.""" from copy import deepcopy import importlib.util import json from pathlib import Path import subprocess import pytest ROOT = Path(__file__).resolve().parents[1] spec = importlib.util.spec_from_file_location('evolution', ROOT / 'scripts/research_evolution.py') evolution = importlib.util.module_from_spec(spec) spec.loader.exec_module(evolution) @pytest.fixture def data(): return evolution.load() def test_current_records_and_report_are_consistent(data): evolution.validate(data) baseline = json.loads((ROOT / evolution.DATA / 'snapshots/baseline-2026-09-26.json').read_text()) evolution.validate_snapshot(baseline) assert evolution.render(data, baseline) == (ROOT / evolution.DATA / 'EvolutionReport.md').read_text() @pytest.mark.parametrize('mutation,match', [ (lambda d: d['profiles']['profiles'][0].update(sha256='0'*64), 'Stale profile'), (lambda d: d['profiles']['profiles'].pop(), 'inventory mismatch'), (lambda d: d['profiles']['profiles'][1].update(id='CAND-0001'), 'Duplicate candidate'), (lambda d: d['profiles']['profiles'][0]['review'].update(independent_human=True), 'human validation'), (lambda d: d['profiles']['profiles'][0]['evidence'][0].update(ref='../missing.md'), 'Missing/unsafe'), (lambda d: d['ledger']['events'][0].update(affected_patterns=['FEP-9999']), 'Unknown affected'), (lambda d: d['ledger']['events'][0].update(supersedes=['LEARN-9999']), 'Supersession'), (lambda d: d['ledger']['events'][0]['effort'].update(research_minutes=-1), 'Invalid effort'), (lambda d: d['ledger']['events'][0].update(recording='prospective'), 'preregistered protocol'), ]) def test_rejects_misleading_or_untraceable_records(data, mutation, match): mutation(data) with pytest.raises(ValueError, match=match): evolution.validate(data) def test_unknown_effort_and_retrospective_sample_cannot_create_a_trend(data): for entry in data['ledger']['events']: entry['effort']['research_minutes'] = None report = evolution.render(data) assert 'Learning rate and diminishing returns: **not measurable' in report assert '0/10' in report assert 'historical only' in report assert 'no revised numeric count' in report assert 'Consequential outcome/hour' not in report # Even retrospectively supplied time cannot enter a prospective comparison group. for entry in data['ledger']['events']: entry['effort']['research_minutes'] = 10 entry['comparison_group'] = 'same-label' assert 'Consequential outcome/hour' not in evolution.render(data) def test_rates_separate_corroboration_unknown_effort_and_streams(data): events = data['ledger']['events'][:4] data['ledger']['events'] = events for entry in events: entry.update(recording='prospective', comparison_group='controlled-mechanism', stream='D', protocol_ref='templates/LearningCycle.md') entry['effort']['research_minutes'] = 30 events[1]['disposition'] = 'corroboration' events[2]['effort']['research_minutes'] = None events[3]['stream'] = 'F' evolution.validate(data) report = evolution.render(data) assert '| LEARN-0001 | revision | 30 | 2.00 |' in report assert '| LEARN-0002 | corroboration | 30 | 0.00 |' in report assert '| LEARN-0003 | revision | unknown | unknown |' in report assert '### Stream F / controlled-mechanism' in report assert 'Fewer than three comparable cycles' in report assert 'does not demonstrate a plateau' in report events[0]['effort']['research_minutes'] = 0 assert '| LEARN-0001 | revision | 0 | unknown |' in evolution.render(data) def test_snapshot_delta_does_not_count_additions_as_maturity_changes(data): old = deepcopy(data) old['profiles']['profiles'].pop() old['profiles']['profiles'][0]['sha256'] = 'old-content' baseline = {'id': 'test', 'source_revision': 'test-revision', 'assessment': old} report = evolution.render(data, baseline) assert 'Added: 1; removed: 0; shared candidates with changed content: 1' in report assert 'maturity transitions: 0' in report def test_snapshot_pins_committed_candidates_and_refuses_overwrite(tmp_path, data): subprocess.run(['git', 'init', '-q', str(tmp_path)], check=True) for p in data['profiles']['profiles']: path = tmp_path / p['path'] path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes((ROOT / p['path']).read_bytes()) subprocess.run(['git', 'add', '.'], cwd=tmp_path, check=True) subprocess.run(['git', '-c', 'user.name=Test', '-c', 'user.email=test@example.invalid', 'commit', '-qm', 'fixture'], cwd=tmp_path, check=True) (tmp_path / evolution.DATA / 'snapshots').mkdir(parents=True) first = evolution.snapshot(data, 'test-baseline', tmp_path) assert json.loads(first.read_text())['assessment'] == data with pytest.raises(FileExistsError): evolution.snapshot(data, 'test-baseline', tmp_path) data['profiles']['profiles'][0]['sha256'] = 'uncommitted' with pytest.raises(ValueError, match='Commit candidate changes'): evolution.snapshot(data, 'second', tmp_path) def test_corrupt_baseline_cannot_masquerade_as_pinned_history(): baseline = json.loads((ROOT / evolution.DATA / 'snapshots/baseline-2026-09-26.json').read_text()) baseline['assessment']['profiles']['profiles'][0]['sha256'] = '0'*64 with pytest.raises(ValueError, match='pinned source'): evolution.validate_snapshot(baseline)