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

This commit is contained in:
Agent Control Runner
2026-10-14 03:22:38 +09:00
parent 12430ba899
commit 7dd450c64c
22 changed files with 7543 additions and 2 deletions
+295
View File
@@ -0,0 +1,295 @@
"""Synthetic checker specimens only. No native/OS execution or acceptance claim.
The JSON, Variant stand-ins, executable and lifecycle records here are deliberately
fabricated unit-test data, never collector output. Semantic corruption tests repin
their bytes, so they cannot pass merely by detecting a stale artifact checksum.
"""
import copy
import hashlib
import json
from pathlib import Path
import shutil
import struct
import zlib
import subprocess
def put(path, data):
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(data, ensure_ascii=False, indent=2)+'\n')
def typed(value):
if isinstance(value, dict):
return {'type': 'Dictionary', 'entries': [[typed(k), typed(v)] for k, v in value.items()]}
if isinstance(value, list):
return {'type': 'Array', 'element_type': 0, 'element_class': '', 'items': [typed(v) for v in value]}
name = 'bool' if isinstance(value, bool) else 'int' if isinstance(value, int) else 'float' if isinstance(value, float) else 'Nil' if value is None else 'String'
return {'type': name, 'text': json.dumps(value, ensure_ascii=False)}
def owner_proof(folder, pid, checker):
"""Fabricated weak-reference records, only for the whole-parser control."""
directory = folder/'teardown'; audio = directory/'owned-audio'
audio.mkdir(parents=True)
before = {'owner_ids':[1], 'owner_meta_present':True, 'closing':False,
'clock_process':True,'clock_physics_process':False,'clock_process_mode':0,
'root_gui_disable_input':False,'auto_accept_quit':False,'original_auto_accept_quit':True}
after = dict(before, owner_ids=[], owner_meta_present=False, auto_accept_quit=True)
control = {'owned_engine_pid':pid,'owner_instance_id':1,'passed':True,
'maximum_wait_us':500000,'whole_Runtime_field_deletions':0,
'before':before,'after':after,'expected_remaining_owner_ids':[]}
for key in ('ordinary_owner_release_verified','clock_process_flags_unchanged',
'root_gui_input_flag_unchanged','audio_teardown_passed'): control[key] = True
for key in ('gameplay_expectations_changed','clock_flags_intentionally_modified','application_close_called',
'SceneTree_quit_overridden','WindowQuit_verified','formal_acceptance'): control[key] = False
put(directory/'control.json', control)
targets = [{'kind':kind,'role':role,'instance_id':i*4+j+1,'alive':True}
for i,kind in enumerate(('stream','packet_sequence')) for j,role in enumerate(('click','success','refusal','notification'))]
proof = {'owned_engine_pid':pid,'owner_instance_id':1,'passed':True,'maximum_wait_us':500000,
'waited_actual_us':100,'deadline_reached':False,'initial_targets':targets,
'final_targets':[dict(t,alive=False) for t in targets], 'full_typed_snapshots':{}}
for key in ('owner_freed','all_owned_targets_released','whole_runtime_unchanged'): proof[key] = True
for key in ('bus_deleted','cues_muted_for_test','actual_human_hearing','WindowQuit_verified','formal_acceptance'): proof[key] = False
for part in ('before','after'):
path = audio/('owner_1-'+part+'.varbytes')
path.write_bytes(b'SYNTHETIC typed bytes; not engine proof')
proof['full_typed_snapshots'][part] = {'path':str(path),'field_deletions':0,'bytes':path.stat().st_size,'sha256':checker.sha(path)}
put(audio/'owner_1.json', proof)
owners = checker.module('synthetic_owner_parser', checker.HERE/'regression/ordinary_proof_gate.py')
return owners.ordinary_gate(directory,pid,folder,['single'])
def make(base, source, checker):
root, out = base/'source', base/'p08-normal-placement-evidence'
for folder in ('autoload', 'scripts', 'scenes', 'data', 'tests', 'assets', 'tools'):
(root/folder).mkdir(parents=True, exist_ok=True)
for name in ('project.godot', 'export_presets.cfg', 'Makefile', 'AGENTS.md', checker.SPEC,
checker.CONTRACT, checker.TEST, checker.RECORDER, checker.RECORDER+".uid",
"tools/run_p08_normal_placement.py",
'docs/plans/original-completion-2026-10-02/IMPLEMENTATION_PLAN.md',
'docs/traceability/original/BASELINE.txt', 'data/facilities/reception.tres',
'data/facilities/standard_room.tres',
'data/facilities/infrastructure/guest_stairs.tres'):
target = root/name
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(source/name, target)
(root/'tests/fixtures/p08-03').mkdir(parents=True)
out.mkdir()
engine = base/'synthetic-engine-not-executable'
engine.write_text('SYNTHETIC TEST DATA; NEVER EXECUTE\n')
sha = checker.sha
source_map = {n: v['sha256'] for n, v in checker.runner.source_refs(root).items()}
index = {'schema': 'p08-normal-placement-v1', 'commit': subprocess.check_output(['git','-C',str(source),'rev-parse','HEAD'],text=True).strip(), 'source_map': source_map,
'inputs': checker.input_map(root), 'spec_sha256': checker.SPEC_SHA,
'marker_contract_sha256': sha(root/checker.CONTRACT), 'conditions': checker.CONDITIONS,
'collection_error': None, 'acceptance': 'pending', 'regression': None}
put(out/'runner-input.json', {'selected_source_map': source_map, 'commit': index['commit'],
'engine': str(engine), 'engine_sha256': sha(engine)})
put(out/'runner-ended.json', {'error': None, 'cleanup_errors': [], 'synthetic': True})
def phase(label, pid):
prefix = 'runs/'+label+'/'
command = [str(engine), '--headless', '--path', str(root), '--script', label]
raw = {'actual_pid': pid, 'command': command, 'live_identity_exact': True,
'engine_ended': True, 'exit_code': 0, 'synthetic': True}
for name in ('started.json', 'ended.json'):
put(out/(prefix+label+'/'+name), raw)
put(out/(prefix+label+'/outer-owned-finally.json'), {'actual_captured_pids': [pid],
'all_captured_ended': True, 'outer_cleanup_errors': [], 'foreign_process_or_group_signals': 0})
(out/(prefix+'stdout.log')).write_text('Godot Engine v4.7.2.stable.official.ed1daf0bf - https://godotengine.org\nP08 normal placement: 100 checks, 0 failures\n')
receipt = {'actual_pid': pid, 'command': command, 'phase': label, 'test': checker.TEST,
'source_sha256': source_map[checker.TEST], 'log': 'stdout.log',
'exit_code': 0, 'engine_ended': True, 'failed': False,
'native_process': {'exit_code': 0, 'helper_exit_code': 0, 'timed_out': False, 'lifecycle_error': None},
'actual_private_profile': {'report': {'fresh_before_parent_initialize': True,
'profile_writes_before_guard': 0, 'actual_user_data_dir': label, 'expected_user_data_dir': label}}}
name = prefix+label+'-receipt.json'
put(out/name, receipt)
return name
pair_phases = [phase('writer', 101), phase('reader', 102)]
os_phases = []
index['cases'] = [{'directory': 'pair', 'phases': pair_phases}]
index['phases'] = pair_phases
index['os_proof'] = 'os.json'
put(out/'os.json', {'source_map': source_map, 'inputs': index['inputs'], 'input_method': 'native_os',
'observer_mutations': [], 'phases': os_phases, 'directory': 'os-pair', 'synthetic': True})
def pair(directory, pids, method):
folder = out/directory
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': []},
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}}
target = [-8, 0, 0]
cost = {'reception': {'gold': 500, 'materials': 8}, 'guest_stairs': {'gold': 40, 'materials': 0}, 'standard_room':{'gold':350,'materials':5}}
rooms = 0
def snap(name):
put(folder/(name+'.json'), typed(model))
(folder/(name+'.var')).write_bytes(json.dumps(model, sort_keys=True).encode())
return name
def position(cell):
return {'world_cell': cell[:], 'selected_cell': cell[:], 'bounds': {'min_x': -10, 'max_y': 10},
'map_position': [0, 0], 'cell_dimensions': [10, 10],
'local': [(cell[0]+10.5)*10, (10.5-cell[1])*10],
'viewport_transform':[1,0,0,1,0,0], 'screen_transform':[1,0,0,1,0,0],
'screen':[(cell[0]+10.5)*10, (10.5-cell[1])*10],
'viewport': [(cell[0]+10.5)*10, (10.5-cell[1])*10], 'zoom': 1.0, 'pan': [0, 0]}
def result(code, details=None, message=''):
row = {'code': code, 'details': details or {}, 'message': message,
'command_id': runtime['next_command_id'], 'target_id': '', 'is_replay': False, 'resolution_hints': []}
runtime['next_command_id'] += 1
runtime['command_queue']['processed'].append(copy.deepcopy(row))
return row
def os_meta(row, pid):
row['input_method'] = method
if method != 'native_os': return
requests = [{'action':'cell', 'world_cell':row['position']['world_cell'],
'viewport':row['position']['viewport'], 'screen':row['position']['screen']}]
if row['id'] in ('unsupported','resume_overlap'):
requests.append({'action':'load_menu'})
if row['id'] == 'pan_zoom':
requests.append({'action':'button','node':'/root/Main/ManualSaveButton'})
requests += [{'action':'wheel','ctrl':False,'move_mode':True}]
requests += [{'action':'wheel','ctrl':False,'move_mode':False} for _ in range(18)]
row['requests'] = []
for sequence, request in enumerate(requests, 1):
prefix = directory+'/'+row['id']+'-'+str(sequence)+'/'
request.update(target_pid=pid, sequence=sequence)
put(out/(prefix+'request.json'), request)
put(out/(prefix+'observed.json'), {'pid':pid,'sequence':sequence,'events':[{'synthetic':True}]})
receipt = directory+'/'+row['id']+'-os.json'
put(out/receipt, {'method':'native_os','target_pid':pid,'timestamp_ns':10000,
'collector_pid':999,'tool_name':'mock CUA','tool_call_id':'synthetic-only',
'events':[{'synthetic':True}]})
put(out/(prefix+'ack.json'), {'target_pid':pid,'sequence':sequence,
'request_sha256':sha(out/(prefix+'request.json')), 'input_receipt':receipt,
'screen_before':directory+'/screen.png','screen_after':directory+'/screen.png'})
row['requests'].append(prefix+'ack.json')
def chunk(kind, body):
return struct.pack('>I',len(body))+kind+body+struct.pack('>I',zlib.crc32(kind+body))
(folder/'screen.png').write_bytes(b'\x89PNG\r\n\x1a\n'+
chunk(b'IHDR',struct.pack('>IIBBBBB',1,1,8,2,0,0,0))+
chunk(b'IDAT',zlib.compress(b'\0\0\0\0'))+chunk(b'IEND',b''))
actions = []
for label in checker.WRITER:
cell = [4, 1, 0] if label in ('lower_stair', 'refused_demolition') else [4, 2, 0] if label == 'upper_stair' else target[:]
definition = 'guest_stairs' if label in ('lower_stair', 'upper_stair', 'missing_connection') else 'reception'
if label == 'unsupported': definition = 'standard_room'
row = {'id': label, 'position': position(cell), 'expected': '', 'results': [], 'status': '',
'kind': 'build', 'definition': definition, 'before': snap('writer-'+label+'-before')}
report = {'can_build': True, 'error_codes': [], 'total_cost': cost[definition], 'connection': {'status': 'CONNECTED'}}
row['report'] = report
if label in checker.REASONS:
code, message = checker.REASONS[label]
report.update(can_build=False, error_codes=[code])
row.update(expected=code, status=message)
elif label == 'missing_connection':
row['expected'] = 'STAIR_LANDING_MISSING'
row['results'] = [result('VALIDATION_FAILED', {'reason': row['expected']}, 'missing landing')]
row['status'] = '操作に失敗しました: missing landing'
elif label == 'cancel_demolition':
row.update(kind='cancel', status='撤去を取り消しました。')
elif label in ('demolition', 'refused_demolition'):
fid = 'f1' if label == 'demolition' else 'f2'
row.update(kind='demolish', instance_id=fid)
if label == 'refused_demolition':
row['expected'] = 'STAIR_LANDING_IN_USE'
row['results'] = [result('VALIDATION_FAILED', {'reason': row['expected']}, 'dependent stair')]
else:
row['results'] = [result('OK', {'refund': {'materials': 2}, 'instance_id': fid})]
model['game_state']['resources']['materials'] += 2
runtime['construction']['facilities'] = [v for v in runtime['construction']['facilities'] if v['instance_id'] != fid]
runtime['construction']['jobs'] = [v for v in runtime['construction']['jobs'] if v.get('facility_instance_id') != fid]
runtime['facilities']['instances'] = [v for v in runtime['facilities']['instances'] if v['instance_id'] != fid]
runtime['world']['occupancies'] = [v for v in runtime['world']['occupancies'] if v['entity_id'] != fid]
r = row['results'][0]
row['status'] = '%s | %s | %s' % (r['code'], r['details'].get('reason', ''), r['message'])
elif label == 'pan_zoom':
row.update(kind='camera', original_local=[1, 2], ctrl_pressed=False,
pan_before=[0,0], pan_after=[0,10],
steps=[0.5]*9+[1, 1.5, 2, 2.5, 3, 3.5, 4, 4, 4])
else:
rooms += 1
fid, jid = 'f'+str(rooms), 'j'+str(rooms)
row['results'] = [result('OK', {'job_id': jid})]
for k, v in cost[definition].items(): model['game_state']['resources'][k] -= v
cells = [[cell[0]+dx,cell[1]+dy,cell[2]] for dx in range(4 if definition == 'reception' else 1) for dy in range(2 if definition == 'reception' else 1)]
job = {'job_id': jid, 'state': 'PLANNED', 'reserved_cells': cells}
runtime['construction']['jobs'].append(job)
runtime['world']['occupancies'].append({'entity_id': jid, 'cells': cells})
row['after'] = snap('writer-'+label+'-after')
job.update(state='COMPLETED', facility_instance_id=fid)
facility = {'instance_id': fid, 'definition_id': definition, 'occupied_cells': cells, 'origin': cell[:], 'source_job_id': jid}
runtime['construction']['facilities'].append(facility)
runtime['facilities']['instances'].append(copy.deepcopy(facility))
runtime['world']['occupancies'][-1]['entity_id'] = fid
row['completed'] = snap('writer-'+label+'-completed')
if 'after' not in row: row['after'] = snap('writer-'+label+'-after')
os_meta(row, pids[0])
actions.append(row)
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')
formal = folder/'formal'
formal.mkdir()
for member in checker.MEMBERS: (formal/member).write_text('synthetic '+member+'\n')
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),
'definition':'reception',
'before': snap('reader-resume-before'), 'after': snap('reader-resume-after'),
'results': [], 'report': {'can_build': False, 'error_codes': ['OVERLAP']},
'status': checker.REASONS['overlap'][1]}
os_meta(row, pids[1])
put(folder/'reader-actions.json', {'setup': 'diagnostic', 'actions': [row]})
if method == 'native_os':
put(folder/'writer-requests.json', {'pid':pids[0],
'requests':[name for action in actions for name in action['requests']]})
put(folder/'reader-requests.json', {'pid':pids[1],'requests':row['requests']})
for role, pid in zip(('writer', 'reader'), pids):
put(folder/(role+'-process.json'), {'pid': pid, 'engine': {'hash': 'synthetic', 'string': 'synthetic'}, 'checks': 100, 'failures': 0})
pair('pair', [101, 102], 'handler')
pair('os-pair', [201, 202], 'native_os')
shutil.copytree(out/'pair/formal', out/'pair/initial-formal')
for role, pid in (('writer',201),('reader',202)):
prefix = 'os-runs/'+role+'/'
folder = out/prefix
folder.mkdir(parents=True)
shutil.copyfile(source/checker.RECORDER, folder/'passive_os.gd')
profile = base/('synthetic-profile-'+role)
command = [str(engine),'--path',str(root),'--script',str(folder/'passive_os.gd')]
formal = out/('pair/initial-formal' if role == 'writer' else 'os-pair/formal')
members = {n:{'bytes':(formal/n).stat().st_size,'sha256':sha(formal/n)} for n in checker.MEMBERS}
record = {'protocol':'p08-placement-native-os-v1','role':role,'commit':index['commit'],
'source_map':source_map,'stage':str(root),'profile':str(profile),'command':command,
'engine_sha256':sha(engine),'controller_sha256':source_map['tools/run_p08_normal_placement.py'],
'recorder_sha256':sha(source/checker.RECORDER),'formal_input':members,
'actual_pid':pid,'monotonic_ns':1,'exit_code':0,'engine_ended':True,'passed':True,
'error':None,'guard_errors':[],'cleanup_errors':[],
'owned_cleanup':[{'pid':pid,'engine_ended':True,'outer_cleanup_errors':[],'foreign_process_or_group_signals':0}],
'stage_before':{},'stage_after':{},'default_before':{},'default_after':{},
'source_after':{n:{'sha256':v} for n,v in source_map.items()},
'profile_before':{'existed':False,'files':{}},'ordinary_owner_proof':owner_proof(folder,pid,checker)}
for name in ('started.json','process-started.json','ended.json'): put(folder/name, record)
(folder/'engine-native.log').write_text('P08 passive OS placement: 100 checks, 0 failures\n')
put(out/('os-pair/'+role+'-profile.json'), {'pid':pid,'user_data_dir':str(profile),'display':'synthetic'})
os_phases.append(prefix+'ended.json')
put(out/'os.json', {'source_map':source_map,'inputs':index['inputs'],'input_method':'native_os',
'observer_mutations':[],'phases':os_phases,'directory':'os-pair','synthetic':True})
put(out/'index.json', index)
repin(out)
return root, out
def repin(out):
index = json.loads((out/'index.json').read_text())
index['artifacts'] = {p.relative_to(out).as_posix(): {'bytes': p.stat().st_size,
'sha256': hashlib.sha256(p.read_bytes()).hexdigest()} for p in out.rglob('*') if p.is_file() and p != out/'index.json'}
put(out/'index.json', index)
+165
View File
@@ -0,0 +1,165 @@
extends "res://tests/fixtures/p08-03/placement.gd"
## Passive endpoint implementation. All gameplay/input is performed by Root CUA.
## Shared parent contains the handler endpoints, but every such endpoint below
## is overridden; this class never emits UI signals or invokes product handlers.
var receipts: Array = []
var all_requests: Array = []
var input_observer: Node
class InputObserver extends Node:
var events: Array = []
func _input(event: InputEvent) -> void:
if event is InputEventMouseButton or event is InputEventKey:
var row := {"ticks_usec": Time.get_ticks_usec(), "text": event.as_text(), "pressed": event.is_pressed()}
if event is InputEventMouseButton:
row["button"] = event.button_index
row["position"] = [event.position.x, event.position.y]
row["ctrl"] = event.ctrl_pressed
events.append(row)
func request(action: Dictionary) -> void:
serial += 1
var stem := "%s-request-%04d" % [role, serial]
var folder := directory.path_join(stem)
DirAccess.make_dir_recursive_absolute(folder)
var window: Window = main.get_window()
action.merge({"sequence": serial, "target_pid": OS.get_process_id(), "role": role,
"state": "waiting_for_root_cua", "window_title": window.title,
"window_position": [window.position.x, window.position.y],
"window_size": [window.size.x, window.size.y], "screen_scale": DisplayServer.screen_get_scale(window.current_screen),
"content_scale_factor": window.content_scale_factor, "setup": "diagnostic"})
json_file(stem + "/request.json", action)
var path := folder.path_join("ack.json")
var first_event: int = input_observer.events.size()
var deadline := Time.get_ticks_msec() + 300000
print("P08_ROOT_CUA_WAIT " + folder.path_join("request.json"))
while not FileAccess.file_exists(path) and Time.get_ticks_msec() < deadline:
await tree.process_frame
if not FileAccess.file_exists(path):
expect(false, "Root CUA acknowledgement timeout")
tree.quit(1)
return
var ack: Variant = JSON.parse_string(FileAccess.get_file_as_string(path))
if not ack is Dictionary or ack.get("target_pid") != OS.get_process_id() or ack.get("sequence") != serial or ack.get("request_sha256") != FileAccess.get_sha256(folder.path_join("request.json")):
expect(false, "Root CUA acknowledgement identity/hash")
tree.quit(1)
return
await frames()
json_file(stem + "/observed.json", {"pid": OS.get_process_id(), "sequence": serial,
"events": input_observer.events.slice(first_event), "status": main.build_status_label.text})
receipts.append("os-pair/" + stem + "/ack.json")
all_requests.append("os-pair/" + stem + "/ack.json")
func annotate(row: Dictionary) -> void:
row["input_method"] = "native_os"
row["requests"] = receipts.duplicate()
receipts.clear()
func point(node: Control) -> Dictionary:
var viewport: Vector2 = node.get_global_transform_with_canvas() * (node.size * 0.5)
var screen: Vector2 = node.get_screen_transform() * (node.size * 0.5)
return {"node": String(node.get_path()), "viewport": [viewport.x, viewport.y], "screen": [screen.x, screen.y], "window_id": node.get_window().get_window_id()}
func setup() -> void:
input_method = "native_os"
expect(DisplayServer.get_name() != "headless", "OS recorder requires a real display")
main = load("res://scenes/main/main.tscn").instantiate()
tree.root.add_child(main)
tree.root.title = "The Tree P08-03 " + role + " PID " + str(OS.get_process_id())
input_observer = InputObserver.new()
tree.root.add_child(input_observer)
await frames()
json_file(role + "-profile.json", {"pid": OS.get_process_id(), "user_data_dir": OS.get_user_data_dir(), "engine": Engine.get_version_info(), "display": DisplayServer.get_name()})
if role == "writer":
await load_menu(main.find_child("SaveLoadMenu", true, false))
var stored: Dictionary = SaveService.inspect_snapshot(ProjectSettings.globalize_path(String(main.manual_save_path)))
expect(bool(stored.get("ok", false)), "read-only initial formal inspection")
expect(runtime.create_snapshot() == stored.get("snapshot", {}), "OS initial formal model exact")
expect(clock.paused, "diagnostic formal fixture starts paused")
func diagnostic_budget() -> void:
budget = JSON.parse_string(FileAccess.get_file_as_string(directory.path_join("diagnostic-budget.json")))
func save_initial_fixture() -> void:
# The controller copied the handler's SHA-pinned formal diagnostic fixture.
# Root loaded it using the normal menu. No model/setup mutation here.
pass
func reveal(node: Control) -> void:
await request({"action": "reveal_palette", "node": String(node.get_path()),
"instructions": "Scroll the normal palette until this button is visible. Do not activate it yet."})
func button(node: BaseButton) -> void:
expect(node != null and not node.disabled, "available production button")
if node == null or node.disabled:
return
# Native modal windows own a separate input viewport.
var old_observer: Node = input_observer
if node.get_window() != main.get_window():
input_observer = InputObserver.new()
node.get_window().add_child(input_observer)
var action := point(node)
action.merge({"action": "button", "instructions": "Click the named button through CUA; capture before/after screenshots."})
await request(action)
if input_observer != old_observer:
input_observer.queue_free()
input_observer = old_observer
func click(cell: Vector3i) -> Dictionary:
var view: WorldPreview = main.world_preview
for attempt: int in range(8):
if Rect2(Vector2.ZERO, view.size).has_point(view.cell_to_canvas(cell)):
break
var framing := point(view)
framing.merge({"action": "framing", "world_cell": vector(cell), "instructions": "Use ordinary zoom-out at the preview center until the requested world cell is visible; coordinates will be recomputed after ack."})
await request(framing)
expect(Rect2(Vector2.ZERO, view.size).has_point(view.cell_to_canvas(cell)), "OS target visible after normal framing")
var local := view.cell_to_canvas(cell)
var viewport := view.get_global_transform_with_canvas() * local
var screen := view.get_screen_transform() * local
observed_cell = null
await request({"action": "cell", "world_cell": vector(cell), "viewport": [viewport.x, viewport.y],
"screen": [screen.x, screen.y], "instructions": "Click this transformed world-cell center. If clipped, use normal zoom/pan first and request a new collection; never substitute fixed pixels."})
var selected: Variant = observed_cell
if not selected is Vector3i:
# With no active build tool, the normal preview selects a facility.
var occupant: StringName = runtime.world_service.get_occupant(cell)
expect(not occupant.is_empty() and view.selected_entity_id == occupant, "actual OS facility selection")
selected = view.canvas_to_cell(view.get_local_mouse_position())
expect(selected is Vector3i and selected == cell, "observed OS selected cell")
return {"world_cell": vector(cell), "selected_cell": vector(selected) if selected is Vector3i else [],
"local": [local.x, local.y], "viewport": [viewport.x, viewport.y],
"viewport_transform": affine(view.get_global_transform_with_canvas()),
"screen": [screen.x, screen.y], "screen_transform": affine(view.get_screen_transform()),
"map_position": [view.get_map_rect().position.x, view.get_map_rect().position.y],
"cell_dimensions": [view.get_cell_rect(cell).size.x, view.get_cell_rect(cell).size.y],
"bounds": runtime.world_service.get_bounds(), "zoom": view.zoom_level,
"pan": [view.pan_offset.x, view.pan_offset.y]}
func wheel(event: InputEventMouseButton, move_mode: bool) -> void:
var action := point(main.world_preview)
action.merge({"action": "wheel", "direction": "up" if event.button_index == MOUSE_BUTTON_WHEEL_UP else "down",
"ctrl": false, "move_mode": move_mode, "instructions": "Send one ordinary wheel step at the preview center without Ctrl; retain the CUA receipt."})
await request(action)
func complete_build(id: StringName) -> void:
await request({"action": "complete_construction", "definition": String(id),
"minutes": runtime.definition_registry.get_facility(id).build_minutes,
"instructions": "Use normal speed/play controls, wait for construction completion, then pause. No injected clock or model changes."})
expect(clock.paused, "Root paused after ordinary construction progression")
func load_menu(menu: MenuButton) -> void:
var action := point(menu)
action.merge({"action": "load_menu", "item_id": 0, "instructions": "Open this normal menu and choose manual load (item 0)."})
await request(action)
func archive_formal() -> void:
DirAccess.make_dir_recursive_absolute(directory.path_join("formal"))
for member: String in MEMBERS:
var source := ProjectSettings.globalize_path(String(main.manual_save_path)).path_join(member)
var destination := directory.path_join("formal/" + member)
expect(DirAccess.copy_absolute(source, destination) == OK, "archive formal " + member)
expect(FileAccess.get_sha256(source) == FileAccess.get_sha256(destination), "archive formal SHA " + member)
func finish_observations() -> void:
json_file(role + "-requests.json", {"pid": OS.get_process_id(), "requests": all_requests})
+1
View File
@@ -0,0 +1 @@
uid://b08passivep03
+313
View File
@@ -0,0 +1,313 @@
extends RefCounted
## Diagnostic setup, never natural-play or OS-input proof.
const Cleanup := preload("res://tests/helpers/ui_audio_fixture_cleanup.gd")
const Codec := preload("res://tests/fixtures/p08-02/mixed_world.gd")
const MEMBERS := ["world.bin", "manifest.json", "thumbnail.png", "journal.log"]
var checks := 0
var failures := 0
var tree: SceneTree
var runtime: Variant
var clock: Variant
var main: Variant
var directory: String
var role: String
var actions: Array = []
var results: Array = []
var serial := 0
var target := Vector3i(-8, 0, 0)
var budget: Dictionary = {}
var input_method := "handler"
var observed_cell: Variant = null
func annotate(row: Dictionary) -> void:
row["input_method"] = input_method
func selected_cell(cell: Vector3i) -> void:
observed_cell = cell
func expect(ok: bool, message: String) -> void:
checks += 1
if not ok:
failures += 1
push_error("P08 placement: " + message)
func json_file(name: String, data: Variant) -> void:
var file := FileAccess.open(directory.path_join(name), FileAccess.WRITE)
expect(file != null, "write " + name)
if file != null:
file.store_string(JSON.stringify(data, "\t") + "\n")
file.close()
func capture(label: String) -> 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:
file.store_buffer(var_to_bytes(snapshot))
file.close()
else:
expect(false, "raw Variant " + name)
return name
func vector(cell: Vector3i) -> Array:
return [cell.x, cell.y, cell.z]
func affine(value: Transform2D) -> Array:
return [value.x.x, value.x.y, value.y.x, value.y.y, value.origin.x, value.origin.y]
func observe(result: CommandResult) -> void:
results.append(result.to_dictionary())
func frames() -> void:
await tree.process_frame
await tree.process_frame
func button(node: BaseButton) -> void:
# Actual production button signal/connection, explicitly not native OS.
expect(node != null and not node.disabled, "available production button")
if node == null or node.disabled:
return
node.pressed.emit()
await frames()
func choose(id: StringName) -> void:
if not main.palette_scroll.visible:
await button(main.palette_toggle_button)
var node: Button = main.build_buttons[id]
await reveal(node)
await button(node)
func click(cell: Vector3i) -> Dictionary:
var view: WorldPreview = main.world_preview
var local := view.cell_to_canvas(cell)
var viewport := view.get_global_transform_with_canvas() * local
var event := InputEventMouseButton.new()
event.button_index = MOUSE_BUTTON_LEFT
event.pressed = true
event.position = local
# This is a handler test through the real WorldPreview, not an OS receipt.
observed_cell = null
view._gui_input(event)
await frames()
return {"world_cell": vector(cell), "selected_cell": vector(observed_cell if observed_cell is Vector3i else view.canvas_to_cell(local)),
"local": [local.x, local.y], "viewport": [viewport.x, viewport.y],
"viewport_transform": affine(view.get_global_transform_with_canvas()),
"map_position": [view.get_map_rect().position.x, view.get_map_rect().position.y],
"cell_dimensions": [view.get_cell_rect(cell).size.x, view.get_cell_rect(cell).size.y],
"bounds": runtime.world_service.get_bounds(), "zoom": view.zoom_level,
"pan": [view.pan_offset.x, view.pan_offset.y]}
func build(id: StringName, cell: Vector3i, label: String, expected: String = "") -> void:
await choose(id)
var report: Dictionary = main.world_preview.get_build_preview_report(cell)
var before := capture(label + "-before")
results.clear()
var position := await click(cell)
var status: String = main.build_status_label.text
var after := capture(label + "-after")
if expected.is_empty():
expect(not results.is_empty() and results.back().code == "OK", label + " accepted")
elif expected == "STAIR_LANDING_MISSING":
expect(not results.is_empty() and results.back().details.get("reason") == expected, label + " domain refusal")
else:
expect(expected in report.error_codes and results.is_empty(), label + " preview refusal")
var row := {"id": label, "kind": "build", "definition": String(id), "expected": expected,
"report": report, "before": before, "after": after, "position": position,
"results": results.duplicate(true), "status": status, "input_method": "handler"}
annotate(row)
actions.append(row)
if expected.is_empty():
await complete_build(id)
row["completed"] = capture(label + "-completed")
func demolish(cell: Vector3i, label: String, cancel := false, expected := "") -> void:
if not main.palette_scroll.visible:
await button(main.palette_toggle_button)
if not main._demolition_mode:
await button(main.demolition_button)
var before := capture(label + "-before")
results.clear()
var position := await click(cell)
var instance_id: String = String(main._demolition_target)
expect(main.demolition_dialog.visible, "normal confirmation visible")
if cancel:
await button(main.demolition_dialog.get_cancel_button())
else:
await button(main.demolition_dialog.get_ok_button())
await frames()
if cancel:
expect(results.is_empty(), "cancel submits no command")
else:
expect(not results.is_empty(), "confirmation submitted Runtime command")
if not results.is_empty():
expect(results.back().code == ("OK" if expected.is_empty() else "VALIDATION_FAILED"), label + " result code")
expect(String(results.back().details.get("reason", "")) == expected, label + " result reason")
var row := {"id": label, "kind": "cancel" if cancel else "demolish", "expected": expected,
"instance_id": instance_id, "position": position, "before": before, "after": capture(label + "-after"),
"results": results.duplicate(true), "status": main.build_status_label.text, "input_method": input_method}
annotate(row)
actions.append(row)
func camera() -> void:
await button(main.pan_button)
var view: WorldPreview = main.world_preview
var before := capture("pan-before")
var original := view.cell_to_canvas(target)
var event := InputEventMouseButton.new()
event.button_index = MOUSE_BUTTON_WHEEL_UP
event.pressed = true
event.factor = 1.0
event.position = view.size * 0.5
event.ctrl_pressed = false
var zoom: float = view.zoom_level
var original_pan: Vector2 = view.pan_offset
await wheel(event, true)
expect(view.zoom_level == zoom, "move-mode wheel pans without Ctrl")
var moved_pan: Vector2 = view.pan_offset
expect(moved_pan != original_pan, "move-mode wheel actually moves the camera")
await button(main.pan_button)
var steps: Array = []
for direction: int in [MOUSE_BUTTON_WHEEL_DOWN, MOUSE_BUTTON_WHEEL_UP]:
for unused: int in range(9):
event.button_index = direction
await wheel(event, false)
steps.append(view.zoom_level)
expect(view.zoom_level == (0.5 if direction == MOUSE_BUTTON_WHEEL_DOWN else 4.0), "existing zoom clamp")
# Use transformed pixels; camera state is not required to survive save/load.
var position := await click(target)
expect(position.world_cell == position.selected_cell, "camera world identity")
expect(view.cell_to_canvas(target) != original, "pixels transform")
var row := {"id": "pan_zoom", "kind": "camera", "before": before, "after": capture("pan-after"),
"position": position, "original_local": [original.x, original.y], "steps": steps,
"pan_before": [original_pan.x, original_pan.y], "pan_after": [moved_pan.x, moved_pan.y],
"ctrl_pressed": false, "input_method": input_method}
annotate(row)
actions.append(row)
func diagnostic_budget() -> void:
var state: Variant = tree.root.get_node("GameState")
var needed := {&"gold": 0, &"materials": 0}
var costs: Array = []
for id: StringName in [&"reception", &"reception", &"guest_stairs", &"guest_stairs"]:
var definition: FacilityDefinition = runtime.definition_registry.get_facility(id)
costs.append({"definition": String(id), "cost": definition.construction_cost.duplicate(true)})
for key: StringName in needed:
needed[key] += int(definition.construction_cost.get(key, 0))
var deltas := {}
for key: StringName in needed:
if needed[key] > state.get_resource(key):
deltas[key] = needed[key] - state.get_resource(key)
budget = {"setup": "diagnostic", "costs": costs, "needed": needed, "initial": state.resources.duplicate(true), "deltas": deltas}
if not deltas.is_empty():
expect(runtime.submit_command(&"change_resources", {&"deltas": deltas}).is_success(), "definition-derived diagnostic budget")
func writer() -> void:
diagnostic_budget()
await save_initial_fixture()
await build(&"standard_room", Vector3i(-8, 4, 0), "unsupported", "UNSUPPORTED")
await build(&"reception", Vector3i(runtime.world_service.get_bounds()[&"max_x"], 0, 0), "out_of_bounds", "OUT_OF_BOUNDS")
await build(&"reception", target, "supported_connected")
await build(&"reception", target, "overlap", "OVERLAP")
# Existing reception supports this upper cell, but provides no stair landing.
await build(&"guest_stairs", Vector3i(-8, 2, 0), "missing_connection", "STAIR_LANDING_MISSING")
await build(&"guest_stairs", Vector3i(4, 1, 0), "lower_stair")
await build(&"guest_stairs", Vector3i(4, 2, 0), "upper_stair")
await demolish(Vector3i(4, 1, 0), "refused_demolition", false, "STAIR_LANDING_IN_USE")
await demolish(target, "cancel_demolition", true)
await demolish(target, "demolition")
await build(&"reception", target, "rebuild")
await camera()
await button(main.find_child("ManualSaveButton", true, false))
expect(main.build_status_label.text == "手動セーブを保存しました。", "normal save")
capture("checkpoint")
await archive_formal()
var members := {}
for member: String in MEMBERS:
members[member] = FileAccess.get_sha256(directory.path_join("formal/" + member))
json_file("save.json", {"members": members, "instance_id": String(runtime.world_service.get_occupant(target)), "cell": vector(target), "pid": OS.get_process_id()})
func reader(distinct: bool) -> void:
var saved: Dictionary = JSON.parse_string(FileAccess.get_file_as_string(directory.path_join("save.json")))
if distinct:
expect(saved.pid != OS.get_process_id(), "separate actual writer/reader PID")
# Real load menu callback, no direct restore_snapshot or model mutation.
var menu: MenuButton = main.find_child("SaveLoadMenu", true, false)
await load_menu(menu)
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")
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)
await build(&"reception", target, "resume_overlap", "OVERLAP")
func run(p_tree: SceneTree, output: String, requested_role: String) -> Dictionary:
# Handler entry is never an OS evidence route. Passive subclass overrides endpoints.
tree = p_tree
directory = output
role = "reader" if requested_role == "reader" else "writer"
expect(DirAccess.make_dir_recursive_absolute(directory) == OK, "diagnostic directory")
runtime = tree.root.get_node("SimulationRuntime")
clock = tree.root.get_node("GameClock")
await setup()
runtime.command_completed.connect(observe)
main.world_preview.cell_clicked.connect(selected_cell)
await frames()
if role == "writer":
await writer()
else:
await reader(true)
json_file(role + "-actions.json", {"setup": "diagnostic", "input_method": input_method, "budget": budget, "actions": actions})
if requested_role == "standalone":
role = "reader"
actions = []
await reader(false)
json_file(role + "-actions.json", {"setup": "diagnostic", "input_method": input_method, "actions": actions})
finish_observations()
runtime.command_completed.disconnect(observe)
var cleanup: Dictionary = await Cleanup.free_owner_and_wait(tree, main)
expect(cleanup.get(&"passed", false), "ordinary Main/audio cleanup")
json_file(role + "-process.json", {"pid": OS.get_process_id(), "engine": Engine.get_version_info(), "checks": checks, "failures": failures, "role": requested_role, "setup": "diagnostic"})
return {"checks": checks, "failures": failures}
func reveal(node: Control) -> void:
main.palette_scroll.ensure_control_visible(node)
await frames()
func wheel(event: InputEventMouseButton, _move_mode: bool) -> void:
main.world_preview._gui_input(event)
await frames()
func complete_build(id: StringName) -> void:
clock.advance_minutes(runtime.definition_registry.get_facility(id).build_minutes)
await frames()
func load_menu(menu: MenuButton) -> void:
menu.get_popup().id_pressed.emit(0)
func setup() -> void:
clock.reset(1, 6, 0)
tree.root.get_node("GameState").reset()
runtime.initialize(clock, tree.root.get_node("GameState"), 42533)
clock.set_paused(true)
tree.root.size = Vector2i(1440, 810)
main = load("res://scenes/main/main.tscn").instantiate()
main.manual_save_path = directory.path_join("formal")
tree.root.add_child(main)
runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
func save_initial_fixture() -> void:
# Explicit diagnostic input, not an OS action or natural-play save.
var saved: Dictionary = SaveService.save_snapshot(directory.path_join("initial-formal"), runtime.create_snapshot())
expect(bool(saved.get("ok", false)), "diagnostic initial formal save")
json_file("diagnostic-budget.json", budget)
func archive_formal() -> void:
pass
func finish_observations() -> void:
pass
+1
View File
@@ -0,0 +1 @@
uid://dc83placement