Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0e241-8285-7a63-8c0c-51c9cb824dc3
118 lines
5.9 KiB
Python
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)
|