Files
TheLadder/tools/story-to-pack/probe/test_review.py
T
JesseMarkowitzandClaude Opus 5 fa3769d0fe Add story-to-pack research and a structured situation review
Research toward building a content pack from a story corpus, kept on its own
branch and independent of the game. Records the selection experiments against
blind labels, and settles selection as gate G2 followed by a human review:
review.py writes REVIEW.md and a review.json form, apply_review.py checks the
filled form and writes situations.json for the next stage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C6UDQ9o6L6Ey173U7XVou6
2026-09-15 06:59:35 -04:00

140 lines
6.4 KiB
Python

"""Tests for the review form's checks and conversion.
python3 -m unittest test_review
"""
import copy, json, pathlib, subprocess, sys, tempfile, unittest
import review_format as rf
def candidate(cid, kind, stories):
"""A synthetic candidate whose n-th scene comes from stories[n]."""
return {'kind': kind, 'stats': {}, 'gate_failures': [], 'most_similar': {'id': None, 'cosine': 0.0},
'members': [{'member': rf.member_id(cid, n + 1), 'chunk': hash((cid, n)) % 100000, 'story': s,
'title': f'STORY {s}', 'summary': f'{cid} scene {n + 1}'} for n, s in enumerate(stories)]}
def fixture():
return {'schema_version': rf.SCHEMA_VERSION,
'run': {'id': 'k60-s99'},
'candidates': {'C01': candidate('C01', 'accepted', [1, 2, 3, 4, 5, 6, 7, 8]),
'C02': candidate('C02', 'accepted', [9, 10, 11, 12, 13, 14]),
'B01': candidate('B01', 'borderline', [1, 3, 5, 7, 9, 11, 13])}}
def filled(cands):
form = rf.blank_form(cands)
e = {x['id']: x for x in form['entries']}
e['C01'].update(decision='accept', situation='A lodger wants more time from a landlady who wants the rent.',
actor='a lodger', counterpart='a landlady', stakes='the room', exclude=['C01.02'])
e['C02'].update(decision='merge', merge_into='C01')
return form, e
class ReviewFormTests(unittest.TestCase):
def test_blank_form_requires_decisions_only_for_accepted(self):
cands = fixture()
errors, _, result = rf.validate(cands, rf.blank_form(cands))
self.assertIsNone(result)
self.assertTrue(any(err.startswith('C01: no decision') for err in errors))
self.assertTrue(any(err.startswith('C02: no decision') for err in errors))
self.assertFalse(any(err.startswith('B01') for err in errors))
def test_valid_form_merges_and_excludes(self):
cands = fixture()
form, _ = filled(cands)
errors, warnings, result = rf.validate(cands, form)
self.assertEqual(errors, [])
self.assertEqual(len(result['situations']), 1)
s = result['situations'][0]
self.assertEqual(s['from'], ['C01', 'C02'])
self.assertEqual(s['size'], 8 - 1 + 6)
self.assertNotIn('C01.02', [m['member'] for m in s['members']])
self.assertEqual(result['skipped'], ['B01'])
self.assertEqual(result['counts']['merged'], 1)
def test_accept_needs_situation_and_roles(self):
cands = fixture()
form, e = filled(cands)
e['C01']['actor'] = ' '
errors, _, result = rf.validate(cands, form)
self.assertIsNone(result)
self.assertIn('C01: accepted, so "actor" must be filled in.', errors)
def test_unknown_excluded_scene_is_named(self):
cands = fixture()
form, e = filled(cands)
e['C01']['exclude'] = ['C01.02', 'C02.01', 'C01.99']
errors, _, _ = rf.validate(cands, form)
self.assertTrue(any('C02.01, C01.99' in err and 'C01.01 to C01.08' in err for err in errors), errors)
def test_merge_target_must_be_accepted(self):
cands = fixture()
form, e = filled(cands)
e['C01']['decision'] = 'reject'
errors, _, _ = rf.validate(cands, form)
self.assertTrue(any(err.startswith('C02: merges into C01') for err in errors), errors)
def test_exclusions_that_leave_too_few_stories_are_refused(self):
cands = fixture()
form, e = filled(cands)
e['C02'].update(decision='accept', merge_into=None, situation='A clerk wants a raise.',
actor='a clerk', counterpart='an employer',
exclude=['C02.01', 'C02.02', 'C02.03'])
errors, _, result = rf.validate(cands, form)
self.assertIsNone(result)
self.assertTrue(any(err.startswith('C02: after exclusions 3 scenes remain from 3 stories') for err in errors), errors)
def test_borderline_cannot_be_rejected_explicitly(self):
cands = fixture()
form, e = filled(cands)
e['B01']['decision'] = 'reject'
errors, _, _ = rf.validate(cands, form)
self.assertTrue(any(err.startswith('B01: "decision" is \'reject\'') for err in errors), errors)
def test_form_from_another_run_is_refused(self):
cands = fixture()
form, _ = filled(cands)
form['run'] = 'k60-s24'
errors, _, _ = rf.validate(cands, form)
self.assertTrue(any('This form is for run' in err for err in errors))
def test_every_problem_is_reported_at_once(self):
cands = fixture()
form, e = filled(cands)
e['C01']['situation'] = ''
e['C02']['merge_into'] = 'C77'
errors, _, _ = rf.validate(cands, form)
self.assertEqual(len(errors), 2, errors)
def test_apply_review_writes_nothing_until_the_form_passes(self):
cands = fixture()
with tempfile.TemporaryDirectory() as tmp:
run = pathlib.Path(tmp)
(run / 'candidates.json').write_text(json.dumps(cands))
(run / 'review.json').write_text(json.dumps(rf.blank_form(cands)))
here = pathlib.Path(__file__).resolve().parent
bad = subprocess.run([sys.executable, 'apply_review.py', str(run)], cwd=here, capture_output=True, text=True)
self.assertEqual(bad.returncode, 1)
self.assertFalse((run / 'situations.json').exists())
form, _ = filled(cands)
(run / 'review.json').write_text(json.dumps(form))
good = subprocess.run([sys.executable, 'apply_review.py', str(run)], cwd=here, capture_output=True, text=True)
self.assertEqual(good.returncode, 0, good.stdout + good.stderr)
written = json.loads((run / 'situations.json').read_text())
self.assertEqual(written['situations'][0]['from'], ['C01', 'C02'])
def test_malformed_json_is_explained(self):
cands = fixture()
with tempfile.TemporaryDirectory() as tmp:
run = pathlib.Path(tmp)
(run / 'candidates.json').write_text(json.dumps(cands))
(run / 'review.json').write_text('{"entries": [ {"id": "C01",} ]}')
here = pathlib.Path(__file__).resolve().parent
r = subprocess.run([sys.executable, 'apply_review.py', str(run)], cwd=here, capture_output=True, text=True)
self.assertEqual(r.returncode, 1)
self.assertIn('is not valid JSON: line 1', r.stdout)
if __name__ == '__main__':
unittest.main()