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
140 lines
6.4 KiB
Python
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()
|