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

This commit is contained in:
Agent Control Runner
2026-10-14 04:21:06 +09:00
parent c2b59f04f9
commit 8e3d79d5f2
12 changed files with 543 additions and 54 deletions
+95 -2
View File
@@ -12,6 +12,7 @@ import sys
import struct
import zlib
import subprocess
import copy
sys.dont_write_bytecode = True
HERE = Path(__file__).resolve().parent
@@ -53,6 +54,68 @@ def load(path): return json.loads(Path(path).read_bytes())
def require(ok, message):
if not ok: raise ValueError(message)
def scalar(value, kind=None):
kind = kind or ('bool' if isinstance(value, bool) else 'int' if isinstance(value, int) else 'String')
return {'type':kind, 'text':('&' if kind == 'StringName' else '')+json.dumps(value, ensure_ascii=False)}
def dictionary(entries, key_type='String'):
return {'type':'Dictionary', 'entries':[[scalar(k, key_type), v] for k,v in entries.items()]}
def typed_member(node, *path):
for key in path:
matches = [v for k,v in node['entries'] if decode(k) == key]
require(len(matches) == 1, 'missing/duplicate typed model member '+repr(path))
node = matches[0]
return node
def expected_operation(formal, result, event_type):
"""Rebuild the full typed state from source rules, never from the actual delta.
Runtime 4680-4720, StableIdGenerator.next_id, SimulationDomainEvent.to_dictionary
and DomainEventStream.publish/create_snapshot define this single notification.
"""
saved = decode(formal)
minute = saved['clock']['absolute_minute']
values = {'ok':bool(result.get('ok',False)), 'code':str(result.get('code','UNKNOWN')),
'message':str(result.get('message','')), 'operation':str(result.get('operation','unknown')),
'save_path':str(result.get('save_path',result.get('path',''))), 'reason':str(result.get('reason','')),
'request_id':int(result.get('request_id',0)),
'checkpoint_absolute_minute':int(result.get('checkpoint_absolute_minute',minute)),
'duration_us':int(result.get('duration_us',0)), 'runtime_duration_us':int(result.get('runtime_duration_us',0))}
names = {'code','operation','reason','selected_candidate_id','selected_candidate_kind'}
if isinstance(result.get('selected'),dict):
selected = result['selected']
values.update(selected_candidate_id=str(selected.get('id','')),
selected_candidate_kind=str(selected.get('kind','')), selected_generation=int(selected.get('generation',-1)))
if isinstance(result.get('manifest'),dict):
manifest = result['manifest']
values.update(save_id=str(manifest.get('save_id','')), saved_absolute_minute=int(manifest.get('absolute_minute',-1)))
expected = copy.deepcopy(formal)
runtime = typed_member(expected,'systems','runtime')
counters = typed_member(runtime,'id_generator','counters')
count = decode(counters).get('domain_event',0)+1
require(0 < count <= 999999999999, 'invalid domain event counter')
for key,value in counters['entries']:
if decode(key) == 'domain_event':
value.update(scalar(count)); break
else:
counters['entries'].append([scalar('domain_event'),scalar(count)])
event_id = 'domain_event.%012d' % count
stream = typed_member(runtime,'domain_events')
published = typed_member(stream,'published_ids')
require(event_id not in decode(published), 'duplicate operation notification')
published['items'].append(scalar(event_id))
published['items'].sort(key=decode)
payload = dictionary({k:scalar(v,'StringName' if k in names else None) for k,v in values.items()},'StringName')
event = dictionary({'event_id':scalar(event_id), 'event_type':scalar(event_type),
'absolute_minute':scalar(minute), 'source_service':scalar('save_service'),
'entity_ids':{'type':'Array','element_type':4,'element_class':'','items':[]},
'payload':payload, 'causation_command_id':scalar(0)})
recent = typed_member(stream,'recent_events')
recent['items'].append(event)
recent['items'] = recent['items'][-256:]
return expected
def png(data):
require(data.startswith(b'\x89PNG\r\n\x1a\n'), 'missing OS screenshot')
offset = 8; chunks = []; payload = b''
@@ -312,6 +375,8 @@ def check(project_root, evidence_dir):
artifact(directory+'/'+name+'.var')
return document(directory+'/'+name+'.json')
rows = []
normal_results = {}
inspections = {}
for role, pid in zip(('writer', 'reader'), expected_pids):
process = document(directory+'/'+role+'-process.json')
require(process['pid'] == pid and process['failures'] == 0 and process['checks'] > 0, 'process attribution')
@@ -329,6 +394,10 @@ def check(project_root, evidence_dir):
require(isinstance(result, dict) and result.get('ok') is True, 'actual save/load failed')
if record['operation'] == 'runtime_operation':
operations.append(result.get('operation'))
normal_results[role] = result
if record['operation'] == 'formal_inspect':
require(result.get('read_only') is True and 'snapshot' in result, 'missing read-only formal model')
inspections[role] = typed_member(record['result'],'snapshot')
require(('save' in operations) if role == 'writer' else any(op in ('load','recovery','recovery_candidate') for op in operations), 'missing actual normal save/load result')
doc = document(directory+'/'+role+'-actions.json')
require(doc['setup'] == 'diagnostic', 'unlabelled diagnostic')
@@ -368,9 +437,33 @@ def check(project_root, evidence_dir):
require(len(matches) == 1 and {tuple(c) for c in matches[0]['occupied_cells']} == cells, 'production footprint mismatch')
rows += doc['actions']
checkpoint = typed('writer-checkpoint')
require(checkpoint == typed('reader-checkpoint'), 'wrong resume model')
state(checkpoint)
require(sha(artifact(directory+'/writer-checkpoint.var')) == sha(artifact(directory+'/reader-checkpoint.var')), 'wrong raw resume')
formal = typed('writer-formal-model')
require(not typed_differences(typed('writer-presave'),formal), 'wrong presave formal model')
require(sha(artifact(directory+'/writer-presave.var')) == sha(artifact(directory+'/writer-formal-model.var')), 'wrong raw presave formal model')
require(not typed_differences(formal,typed('reader-formal-model')), 'wrong formal resume model')
require(sha(artifact(directory+'/writer-formal-model.var')) == sha(artifact(directory+'/reader-formal-model.var')), 'wrong raw formal resume model')
for role in ('writer','reader'):
require(role in inspections and not typed_differences(formal,inspections[role]), 'wrong read-only formal model')
result = normal_results[role]
require(result['operation'] == ('save' if role == 'writer' else 'recovery'), 'wrong operation notification kind')
event_type = 'SAVE_CHECKPOINT_SUCCEEDED' if role == 'writer' else 'SAVE_RECOVERY_LOADED'
expected = expected_operation(formal,result,event_type)
differences = typed_differences(expected,typed(role+'-checkpoint'))
require(not differences, 'wrong resume model: source-defined operation drift '+role+' '+repr(differences))
require(not typed_differences(expected,typed(role+'-expected-operation')), 'wrong reconstructed operation model')
require(sha(artifact(directory+'/'+role+'-expected-operation.var')) == sha(artifact(directory+'/'+role+'-checkpoint.var')), 'wrong raw operation model')
require(not typed_differences(typed(role+'-checkpoint'),typed(role+'-operation-actual')), 'inspection changed live model')
require(sha(artifact(directory+'/'+role+'-operation-actual.var')) == sha(artifact(directory+'/'+role+'-checkpoint.var')), 'wrong raw inspection preservation')
comparison = document(directory+'/'+role+'-operation-comparison.json')
require(comparison == {'pid':expected_pids[0 if role == 'writer' else 1],
'delta_kind':'source_defined_notification','formal_model':role+'-formal-model',
'actual_model':role+'-checkpoint','expected_model':role+'-expected-operation',
'returned_result':comparison['returned_result'],
'saved_recent_count':len(decode(formal)['systems']['runtime']['domain_events']['recent_events']),
'expected_recent_count':len(decode(expected)['systems']['runtime']['domain_events']['recent_events']),
'actual_recent_count':len(decode(typed(role+'-checkpoint'))['systems']['runtime']['domain_events']['recent_events']),
'whole_raw_equal':True} and decode(comparison['returned_result']) == result, 'wrong operation comparison attribution')
saved = document(directory+'/save.json')
require(set(saved['members']) == set(MEMBERS) and saved['pid'] == expected_pids[0], 'missing formal members/writer')
for member in MEMBERS:
+109
View File
@@ -135,6 +135,115 @@ class EvidenceTests(unittest.TestCase):
self.edit('pair/reader-save-result-0001.json', lambda d: d.update(result=F.typed({'ok':True,'operation':'load'}), requested_path='/outside/formal'))
self.reject('save outside private user profile')
def test_notification_payload_is_recomputed_from_returned_dict(self):
path = 'pair/reader-checkpoint.json'
original = (self.out/path).read_bytes()
fields = ('ok','code','message','operation','save_path','reason','request_id',
'checkpoint_absolute_minute','duration_us','runtime_duration_us',
'selected_candidate_id','selected_candidate_kind','selected_generation',
'save_id','saved_absolute_minute')
for field in fields:
with self.subTest(field=field):
data = json.loads(original)
event = C.typed_member(data,'systems','runtime','domain_events','recent_events')['items'][-1]
value = C.typed_member(event,'payload',field)
decoded = C.decode(value)
changed = not decoded if isinstance(decoded,bool) else decoded+1 if isinstance(decoded,int) else decoded+'-drift'
value.update(C.scalar(changed,value['type']))
F.put(self.out/path,data); F.repin(self.out)
self.reject('source-defined operation drift')
(self.out/path).write_bytes(original); F.repin(self.out)
def test_notification_envelope_cannot_drift(self):
path = 'pair/writer-checkpoint.json'
original = (self.out/path).read_bytes()
for field in ('event_id','event_type','source_service','absolute_minute','causation_command_id','entity_ids'):
with self.subTest(field=field):
data = json.loads(original)
event = C.typed_member(data,'systems','runtime','domain_events','recent_events')['items'][-1]
value = C.typed_member(event,field)
if field == 'entity_ids': value['items'].append(C.scalar('unexpected-entity'))
else:
old = C.decode(value)
value.update(C.scalar(old+1 if isinstance(old,int) else old+'-drift',value['type']))
F.put(self.out/path,data); F.repin(self.out)
self.reject('source-defined operation drift')
(self.out/path).write_bytes(original); F.repin(self.out)
def test_notification_counter_must_increment_exactly_once(self):
def corrupt(data):
C.typed_member(data,'systems','runtime','id_generator','counters','domain_event')['text'] = '9'
self.edit('pair/reader-checkpoint.json',corrupt)
self.reject('source-defined operation drift')
def test_notification_published_ids_cannot_be_omitted(self):
self.edit('pair/writer-checkpoint.json',lambda d: C.typed_member(d,'systems','runtime','domain_events','published_ids')['items'].clear())
self.reject('source-defined operation drift')
def test_extra_notification_is_rejected(self):
def corrupt(data):
events = C.typed_member(data,'systems','runtime','domain_events','recent_events')['items']
events.append(copy.deepcopy(events[-1]))
self.edit('pair/reader-checkpoint.json',corrupt)
self.reject('source-defined operation drift')
def test_existing_event_history_cannot_be_rewritten(self):
def corrupt(data):
event = C.typed_member(data,'systems','runtime','domain_events','recent_events')['items'][0]
C.typed_member(event,'event_type').update(text='"REWRITTEN_HISTORY"')
self.edit('pair/reader-checkpoint.json',corrupt)
self.reject('source-defined operation drift')
def test_postsave_unrelated_resource_drift_is_rejected(self):
self.edit('pair/writer-checkpoint.json',lambda d: C.typed_member(d,'game_state','resources','gold').update(text='9999'))
self.reject('source-defined operation drift')
def test_postload_unknown_field_drift_is_rejected(self):
self.edit('pair/reader-checkpoint.json',lambda d: C.typed_member(d,'systems','runtime','inventory')['entries'].append([C.scalar('unknown'),C.scalar(1)]))
self.reject('source-defined operation drift')
def test_presave_must_match_read_only_formal_model(self):
self.edit('pair/writer-presave.json',lambda d: C.typed_member(d,'clock','absolute_minute').update(text='361'))
self.reject('wrong presave formal model')
def test_read_only_inspection_must_match_complete_formal_model(self):
self.edit('pair/reader-save-result-0002.json',lambda d: C.typed_member(d['result'],'snapshot','clock','absolute_minute').update(text='361'))
self.reject('wrong read-only formal model')
def test_inspection_cannot_lose_read_only_attribution(self):
self.edit('pair/writer-save-result-0002.json',lambda d: C.typed_member(d['result'],'read_only').update(text='false'))
self.reject('missing read-only formal model')
def test_returned_result_payload_tampering_is_rejected(self):
self.edit('pair/reader-save-result-0001.json',lambda d: C.typed_member(d['result'],'duration_us').update(text='999'))
self.reject('source-defined operation drift')
def test_notification_payload_type_drift_is_rejected(self):
def corrupt(data):
event = C.typed_member(data,'systems','runtime','domain_events','recent_events')['items'][-1]
C.typed_member(event,'payload','operation')['type'] = 'String'
C.typed_member(event,'payload','operation')['text'] = '"recovery"'
self.edit('pair/reader-checkpoint.json',corrupt)
self.reject('source-defined operation drift')
def test_operation_count_is_source_derived_with_recent_limit(self):
model = C.decode(json.loads((self.out/'pair/writer-formal-model.json').read_text()))
runtime = model['systems']['runtime']
runtime['id_generator']['counters']['domain_event'] = 900
stream = runtime['domain_events']
stream['published_ids'] = ['domain_event.%012d' % n for n in range(1,901)]
stream['recent_events'] = [{'ordinal':n} for n in range(645,901)]
expected = C.decode(C.expected_operation(F.typed(model),{'ok':True},'SAVE_CHECKPOINT_SUCCEEDED'))
new = expected['systems']['runtime']
self.assertEqual(901,new['id_generator']['counters']['domain_event'])
self.assertEqual(901,len(new['domain_events']['published_ids']))
self.assertEqual(256,len(new['domain_events']['recent_events']))
self.assertEqual(stream['recent_events'][1:],new['domain_events']['recent_events'][:-1])
self.assertEqual('domain_event.000000000901',new['domain_events']['recent_events'][-1]['event_id'])
del runtime['id_generator']['counters']
with self.assertRaisesRegex(ValueError,'missing/duplicate typed model member'):
C.expected_operation(F.typed(model),{'ok':True},'SAVE_CHECKPOINT_SUCCEEDED')
def test_missing_formal_member(self):
for name in ('formal','formal.bak'):
with self.subTest(package=name):