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
+36 -5
View File
@@ -117,10 +117,17 @@ def make(base, source, checker):
folder.mkdir()
runtime = {k: {} for k in sorted(checker.RUNTIME_KEYS)}
runtime.update(world={'occupancies': [], 'bounds':{'min_x':-10,'max_y':10}}, construction={'facilities': [], 'jobs': []},
id_generator={'version':1,'counters':{'domain_event':7}},
domain_events={'version':1,'published_ids':[],'recent_events':[]},
facilities={'instances': []}, next_command_id=1,
command_queue={'processed': [], 'pending': [], 'in_flight': [], 'accepted': [], 'idempotency': {}})
model = {'schema_version': 1, 'clock': {'absolute_minute': 360, 'paused': True},
'game_state': {'resources': {'gold': 5000, 'materials': 50}}, 'systems': {'runtime': runtime}}
runtime['domain_events']['published_ids'] = ['domain_event.%012d' % n for n in range(1,8)]
runtime['domain_events']['recent_events'] = [
{'event_id':'domain_event.%012d' % n, 'event_type':'SYNTHETIC_HISTORY',
'absolute_minute':360, 'source_service':'synthetic', 'entity_ids':[],
'payload':{'ordinal':n}, 'causation_command_id':0} for n in range(1,8)]
target = [-8, 0, 0]
cost = {'reception': {'gold': 500, 'materials': 8}, 'guest_stairs': {'gold': 40, 'materials': 0}, 'standard_room':{'gold':350,'materials':5}}
rooms = 0
@@ -236,8 +243,10 @@ def make(base, source, checker):
budget = {'setup': 'diagnostic', 'costs': [{'definition': d, 'cost': cost[d]} for d in ('reception', 'reception', 'guest_stairs', 'guest_stairs')],
'needed': {'gold': 1080, 'materials': 16}, 'initial': {'gold': 5000, 'materials': 50}, 'deltas': {}}
put(folder/'writer-actions.json', {'setup': 'diagnostic', 'actions': actions, 'budget': budget})
snap('writer-checkpoint')
snap('reader-checkpoint')
snap('writer-presave')
snap('writer-formal-model')
snap('reader-formal-model')
formal_model = typed(model)
formal = folder/'formal'
formal.mkdir()
for member in checker.MEMBERS: (formal/member).write_text('synthetic '+member+'\n')
@@ -251,14 +260,36 @@ def make(base, source, checker):
'members':{n:{'sha256':sha(folder/name/n),'bytes':(folder/name/n).stat().st_size} for n in checker.MEMBERS}}
for name,suffix in [('formal',''),('formal.bak','.bak')]},
'slot_metadata':{'file':'formal-slot-metadata.json','sha256':sha(formal/'manifest.json')}})
for role,pid,op in [('writer',pids[0],'save'),('reader',pids[1],'load')]:
for role,pid,op in [('writer',pids[0],'save'),('reader',pids[1],'recovery')]:
returned = {'ok':True,'operation':op,'save_path':'user://saves/manual_ui',
'code':'OK','message':'synthetic result','duration_us':13,'runtime_duration_us':17,
'manifest':{'save_id':'synthetic-save','absolute_minute':360}}
if role == 'reader':
returned['selected'] = {'id':'formal','kind':'formal','generation':0}
expected = checker.expected_operation(formal_model,returned,
'SAVE_CHECKPOINT_SUCCEEDED' if role == 'writer' else 'SAVE_RECOVERY_LOADED')
for label in ('checkpoint','expected-operation','operation-actual'):
put(folder/(role+'-'+label+'.json'),expected)
(folder/(role+'-'+label+'.var')).write_bytes(json.dumps(expected,sort_keys=True).encode())
count = len(model['systems']['runtime']['domain_events']['recent_events'])
put(folder/(role+'-operation-comparison.json'), {'pid':pid,'delta_kind':'source_defined_notification',
'formal_model':role+'-formal-model','actual_model':role+'-checkpoint',
'expected_model':role+'-expected-operation','returned_result':typed(returned),
'saved_recent_count':count,'expected_recent_count':count+1,'actual_recent_count':count+1,
'whole_raw_equal':True})
stem = role+'-save-result-0001'
(folder/(stem+'.var')).write_bytes(b'synthetic raw Dictionary stand-in')
put(folder/(stem+'.json'), {'pid':pid,'operation':'runtime_operation',
'requested_path':'user://saves/manual_ui','absolute_path':absolute,'user_data_dir':profile,
'result':typed({'ok':True,'operation':op,'save_path':'user://saves/manual_ui'}),
'result':typed(returned),
'raw_sha256':sha(folder/(stem+'.var'))})
put(folder/(role+'-save-results.json'), {'pid':pid,'records':[stem]})
inspect_stem = role+'-save-result-0002'
(folder/(inspect_stem+'.var')).write_bytes(b'synthetic inspection Dictionary stand-in')
put(folder/(inspect_stem+'.json'), {'pid':pid,'operation':'formal_inspect',
'requested_path':'user://saves/manual_ui','absolute_path':absolute,'user_data_dir':profile,
'result':typed({'ok':True,'read_only':True,'snapshot':copy.deepcopy(model)}),
'raw_sha256':sha(folder/(inspect_stem+'.var'))})
put(folder/(role+'-save-results.json'), {'pid':pid,'records':[stem,inspect_stem]})
put(folder/'save.json', {'members': {n: sha(formal/n) for n in checker.MEMBERS},
'pid': pids[0], 'cell': target, 'instance_id': 'f4'})
row = {'id': 'resume_overlap', 'kind': 'build', 'position': position(target),
+74 -3
View File
@@ -20,6 +20,7 @@ var input_method := "handler"
var observed_cell: Variant = null
var save_serial := 0
var save_records: Array = []
var operation_results: Array[Dictionary] = []
const INITIAL_SAVE := "user://saves/p08_initial"
func annotate(row: Dictionary) -> void:
@@ -43,8 +44,10 @@ func json_file(name: String, data: Variant) -> void:
file.close()
func capture(label: String) -> String:
return capture_model(label, runtime.create_snapshot())
func capture_model(label: String, snapshot: Dictionary) -> String:
var name := role + "-" + label
var snapshot: Dictionary = runtime.create_snapshot()
json_file(name + ".json", Codec.new().typed(snapshot))
var file := FileAccess.open(directory.path_join(name + ".var"), FileAccess.WRITE)
if file != null:
@@ -226,10 +229,15 @@ func writer() -> void:
await demolish(target, "demolition")
await build(&"reception", target, "rebuild")
await camera()
capture("presave")
await button(main.find_child("ManualSaveButton", true, false))
expect(main.build_status_label.text == "手動セーブを保存しました。", "normal save")
capture("checkpoint")
await archive_formal()
var formal := inspect_formal()
var presave: Dictionary = bytes_to_var(FileAccess.get_file_as_bytes(directory.path_join("writer-presave.var")))
expect(var_to_bytes(presave) == var_to_bytes(formal), "whole presave formal model")
verify_operation(formal, operation_results.back(), &"SAVE_CHECKPOINT_SUCCEEDED")
var members := {}
for member: String in MEMBERS:
members[member] = FileAccess.get_sha256(directory.path_join("formal/" + member))
@@ -245,8 +253,10 @@ func reader(distinct: bool) -> void:
await frames()
expect(main.build_status_label.text == "手動セーブを読み込みました。", "normal resume")
capture("checkpoint")
var original := FileAccess.get_file_as_bytes(directory.path_join("writer-checkpoint.var"))
expect(original == var_to_bytes(runtime.create_snapshot()), "whole formal resume model")
var formal := inspect_formal()
var original := FileAccess.get_file_as_bytes(directory.path_join("writer-formal-model.var"))
expect(original == var_to_bytes(formal), "whole formal resume model")
verify_operation(formal, operation_results.back(), &"SAVE_RECOVERY_LOADED")
expect(String(runtime.world_service.get_occupant(target)) == saved.instance_id, "same resumed instance/cell")
for member: String in MEMBERS:
expect(FileAccess.get_sha256(directory.path_join("formal/" + member)) == saved.members[member], "reader preserves formal member " + member)
@@ -325,7 +335,68 @@ func save_initial_fixture() -> void:
func archive_formal() -> void:
export_package(String(main.manual_save_path), "formal")
func inspect_formal() -> Dictionary:
var stored := SaveService.inspect_snapshot(String(main.manual_save_path))
record_save_result("formal_inspect", String(main.manual_save_path), stored)
expect(bool(stored.get(&"ok", false)), "read-only formal inspection")
var formal: Dictionary = stored.get(&"snapshot", {})
capture_model("formal-model", formal)
return formal
func verify_operation(formal: Dictionary, result: Dictionary, event_type: StringName) -> void:
var expected := formal.duplicate(true)
var model: Dictionary = expected[&"systems"][&"runtime"]
var counters: Dictionary = model[&"id_generator"]["counters"]
var next: int = int(counters.get("domain_event", 0)) + 1
counters["domain_event"] = next
var event_id := "domain_event.%012d" % next
var payload := {
&"ok": bool(result.get(&"ok", false)),
&"code": StringName(result.get(&"code", &"UNKNOWN")),
&"message": str(result.get(&"message", "")),
&"operation": StringName(result.get(&"operation", &"unknown")),
&"save_path": str(result.get(&"save_path", result.get(&"path", ""))),
&"reason": StringName(result.get(&"reason", &"")),
&"request_id": int(result.get(&"request_id", 0)),
&"checkpoint_absolute_minute": int(result.get(&"checkpoint_absolute_minute", formal[&"clock"][&"absolute_minute"])),
&"duration_us": int(result.get(&"duration_us", 0)),
&"runtime_duration_us": int(result.get(&"runtime_duration_us", 0)),
}
if result.get(&"selected") is Dictionary:
var selected: Dictionary = result[&"selected"]
payload[&"selected_candidate_id"] = StringName(selected.get(&"id", &""))
payload[&"selected_candidate_kind"] = StringName(selected.get(&"kind", &""))
payload[&"selected_generation"] = int(selected.get(&"generation", -1))
if result.get(&"manifest") is Dictionary:
var manifest: Dictionary = result[&"manifest"]
payload[&"save_id"] = str(manifest.get("save_id", ""))
payload[&"saved_absolute_minute"] = int(manifest.get("absolute_minute", -1))
var stream: Dictionary = model[&"domain_events"]
stream["published_ids"].append(event_id)
stream["published_ids"].sort()
var notification := SimulationDomainEvent.new(StringName(event_id), event_type,
int(formal[&"clock"][&"absolute_minute"]), &"save_service", [], payload, 0)
stream["recent_events"].append(notification.to_dictionary())
if stream["recent_events"].size() > DomainEventStream.RECENT_EVENT_LIMIT:
stream["recent_events"].pop_front()
capture_model("expected-operation", expected)
capture("operation-actual")
var actual: Dictionary = bytes_to_var(FileAccess.get_file_as_bytes(directory.path_join(role + "-checkpoint.var")))
json_file(role + "-operation-comparison.json", {
"pid": OS.get_process_id(), "delta_kind": "source_defined_notification",
"formal_model": role + "-formal-model", "actual_model": role + "-checkpoint",
"expected_model": role + "-expected-operation", "returned_result": Codec.new().typed(result),
"saved_recent_count": formal[&"systems"][&"runtime"][&"domain_events"]["recent_events"].size(),
"expected_recent_count": stream["recent_events"].size(),
"actual_recent_count": actual[&"systems"][&"runtime"][&"domain_events"]["recent_events"].size(),
"whole_raw_equal": var_to_bytes(expected) == var_to_bytes(actual),
})
# Complete raw actual/expected snapshots remain available on unknown drift.
expect(var_to_bytes(expected) == var_to_bytes(actual), "whole source-defined save operation model; see " + role + "-operation-comparison.json")
expect(var_to_bytes(actual) == var_to_bytes(runtime.create_snapshot()), "read-only inspection preserves live model")
func record_save_operation(result: Dictionary) -> void:
operation_results.append(result.duplicate(true))
record_save_result("runtime_operation", String(main.manual_save_path), result)
func record_save_result(operation: String, path: String, result: Dictionary) -> void: