P08-03: 配置の通常操作受入

This commit is contained in:
Agent Control Runner
2026-10-14 16:08:32 +09:00
parent 7e2368f5c4
commit 47750010b8
9 changed files with 498 additions and 56 deletions
+20 -2
View File
@@ -559,11 +559,20 @@ def action_gate(row, typed, root=HERE.parent):
expected_reason = REASONS[label][0] if label in REASONS else {'missing_connection':'STAIR_LANDING_MISSING', 'refused_demolition':'STAIR_LANDING_IN_USE'}.get(label, '')
require(row.get('expected', expected_reason) == expected_reason, 'required refusal reason changed')
results = row.get('results', [])
if kind == 'build' and results and results[0]['code'] != 'OK':
reason = results[0]['details'].get('reason', results[0]['code'])
require(not row['report']['can_build'], 'preview/command disagreement')
require(reason in row['report']['error_codes'], 'preview reason differs from Runtime reason')
if label in REASONS:
code, message = REASONS[label]
require(row['report']['error_codes'][0] == code and row['status'] == message, 'reason mismatch')
require(not row['report']['can_build'] and not results, 'preview rejection bypassed')
preserve(before, after, results)
elif label == 'missing_connection' and not results:
require(not row['report']['can_build'], 'preview/command disagreement')
require('STAIR_LANDING_MISSING' in row['report']['error_codes'], 'preview reason differs from Runtime reason')
require(row['report']['reasons'] and row['status'] == row['report']['reasons'][0]['message'], 'connection reason mismatch')
preserve(before, after, [])
elif kind == 'cancel':
require(not results and row['status'] == '撤去を取り消しました。', 'cancel submitted command')
preserve(before, after, results)
@@ -584,7 +593,16 @@ def action_gate(row, typed, root=HERE.parent):
else:
require(result['code'] == 'OK', 'positive rejected')
if kind == 'demolish':
require(row['status'] == '%s | %s | %s' % (result['code'], result['details'].get('reason', ''), result['message']), 'displayed domain reason mismatch')
tokens = (result['code'], result['details'].get('reason', ''), ' | ')
require(not any(token and token in row['status'] for token in tokens), 'raw demolition token in status')
if row['expected']:
require(row['status'] == '撤去できません: '+result['message'], 'demolition refusal message mismatch')
else:
titles = {'gold':'所持金', 'mana':'マナ', 'materials':'資材', 'faith':'信仰'}
refund = result['details']['refund']
amounts = ['%s %d' % (titles.get(k, k), int(refund[k])) for k in sorted(refund) if int(refund[k]) != 0]
expected_status = '撤去しました(返金: '+ '、'.join(amounts)+')' if amounts else '撤去しました(返金なし)'
require(row['status'] == expected_status, 'demolition refund display mismatch')
if not row['expected']:
fid = row['instance_id']
old = keyed(a['systems']['runtime']['construction']['facilities'], 'instance_id')
@@ -606,7 +624,7 @@ def action_gate(row, typed, root=HERE.parent):
require(row['report']['connection']['status'] == 'CONNECTED', 'positive connection absent')
if label == 'missing_connection':
require(row['status'] == '操作に失敗しました: '+result['message'], 'connection reason mismatch')
require(row['report']['can_build'], 'connection refusal did not reach Runtime')
require(not row['report']['can_build'], 'preview/command disagreement')
def check(project_root, evidence_dir):
root, evidence = Path(project_root).resolve(), Path(evidence_dir).resolve()
@@ -98,6 +98,83 @@ class EvidenceTests(unittest.TestCase):
def test_positive_whole_checker(self):
self.assertEqual([], C.check(self.root, self.out))
def action_row(self, label):
rows = C.load(self.out/'pair/writer-actions.json')['actions']
return copy.deepcopy(next(row for row in rows if row['id'] == label))
def action_check(self, row, message=None, mutate_state=None):
def typed(name):
node = C.load(self.out/('pair/'+name+'.json'))
if mutate_state and name == row['after']:
mutate_state(node)
return node
if message:
with self.assertRaisesRegex(ValueError, message):
C.action_gate(row, typed, self.root)
else:
C.action_gate(row, typed, self.root)
def test_agreeing_preview_refusal(self):
row = self.action_row('missing_connection')
self.assertEqual(row['status'], row['report']['reasons'][0]['message'])
self.action_check(row)
def test_preview_command_disagreement(self):
row = self.action_row('missing_connection')
row['report']['can_build'] = True
row['results'] = [{'code':'VALIDATION_FAILED', 'details':{'reason':'STAIR_LANDING_MISSING'}, 'message':row['status']}]
self.action_check(row, 'preview/command disagreement')
def test_preview_runtime_reason_difference(self):
row = self.action_row('missing_connection')
row['report']['error_codes'] = ['OVERLAP']
row['results'] = [{'code':'VALIDATION_FAILED', 'details':{'reason':'STAIR_LANDING_MISSING'}, 'message':row['status']}]
self.action_check(row, 'preview reason differs from Runtime reason')
def test_preview_partial_state_change(self):
self.action_check(self.action_row('missing_connection'), 'cancel/preview partially applied',
lambda node: C.typed_member(node, 'game_state', 'resources', 'materials').update(text='999'))
def test_readable_demolition_success(self):
row = self.action_row('demolition')
self.assertEqual('撤去しました(返金: 資材 2)', row['status'])
self.action_check(row)
def test_demolition_missing_refund_amount(self):
row = self.action_row('demolition'); row['status'] = '撤去しました(返金: 資材)'
self.action_check(row, 'demolition refund display mismatch')
def test_demolition_wrong_refund_amount(self):
row = self.action_row('demolition'); row['status'] = '撤去しました(返金: 資材 3)'
self.action_check(row, 'demolition refund display mismatch')
def test_demolition_raw_enum_status(self):
row = self.action_row('demolition'); row['status'] = 'OK | |'
self.action_check(row, 'raw demolition token in status')
def test_readable_demolition_refusal(self):
row = self.action_row('refused_demolition')
self.assertEqual('STAIR_LANDING_IN_USE', row['results'][0]['details']['reason'])
self.assertEqual('撤去できません: '+row['results'][0]['message'], row['status'])
self.action_check(row)
def test_demolition_raw_reason_status(self):
row = self.action_row('refused_demolition'); row['status'] += ' STAIR_LANDING_IN_USE'
self.action_check(row, 'raw demolition token in status')
def test_demolition_altered_refusal_message(self):
for status in ('撤去できません', '撤去できません: altered'):
row = self.action_row('refused_demolition'); row['status'] = status
self.action_check(row, 'demolition refusal message mismatch')
def test_demolition_raw_reason_mismatch(self):
row = self.action_row('refused_demolition')
row['results'][0]['details']['reason'] = 'OTHER'
def mutate(node):
items = C.typed_member(node, 'systems', 'runtime', 'command_queue', 'processed')['items']
items[-1] = F.typed(row['results'][0])
self.action_check(row, 'reason mismatch', mutate)
def test_verify_only_cli_uses_build_output_directory(self):
output = io.StringIO()
with patch.dict(os.environ, {'BUILD_OUTPUT_DIR':str(self.out.parent)}), contextlib.redirect_stdout(output):