frontend-patterns/tests/test_research_evolution.py

116 lines
5.6 KiB
Python
Raw Normal View History

"""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):
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)