frontend-patterns/tests/test_research_evolution.py
tegwick 7295172f1e Close local review loose ends and record blocked workplan gates
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a0e241-8285-7a63-8c0c-51c9cb824dc3
2026-09-27 17:20:43 +02:00

118 lines
5.9 KiB
Python

"""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]['review'].update(last_substantive_review_date='2099-01-01'), 'Review date exceeds'),
(lambda d: d['profiles']['profiles'][0]['review'].update(substantive_review_ref=None), 'needs provenance'),
(lambda d: d['profiles']['profiles'][0]['review'].update(substantive_review_scope=''), 'needs provenance'),
(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):
data['ledger']['events'] = [e for e in data['ledger']['events'] if e['recording'] == 'retrospective']
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 f"0/{len(data['ledger']['events'])}" 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)