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
@@ -3679,6 +3679,17 @@
"path": "docs/spec/acceptance/P08-02.json",
"scope": "new standalone mixed suite; native execution pending"
}
},
{
"test": "tests/ui/test_p08_normal_placement_ops.gd",
"template": "P08 normal placement: <checks> checks, <failures> failures",
"required": true,
"fixed_check_count_used": false,
"source_sha256_at_schema_audit": "734f4f85d0b0fc45dc31fc23502425b911635667dabc0341ba0d88b0107f4405",
"authority": {
"path": "docs/spec/acceptance/P08-03.json",
"scope": "normal placement; native and OS pending"
}
}
],
"unknown_roster_suite_policy": "refuse, explicitly review its source/schema before accepting",
@@ -0,0 +1,67 @@
# P08-03 condition matrix — implementation submission, not acceptance
Immutable specification: `docs/spec/acceptance/P08-03.json`, normalized SHA-256
`7941da4edbd3c43a636a4a3d834d57c095b56da988fa3a056e55b22d3a5d50e9`.
Base HEAD: `12430ba89970bda349781e51157e72e48a4ac06f`. Worker made no commit.
This is one existing unit in 30 packages / 88 units, with no partial credit.
| Condition | Implemented paths / diagnostic cases | Current evidence state |
|---|---|---|
| support_connection | Normal palette: unsupported standard room; supported/connected reception; supported stair with missing landing; lower/upper stair construction. Real preview and Runtime command results are retained. | Source and synthetic checker controls only; real handler/native/OS pending. |
| invalid_position_reason | Reception footprint crosses the boundary from an in-bounds origin; existing reception overlap. Exact displayed Japanese reason and report codes; whole typed before/after preservation. | Source and semantic corruption controls only; actual refusals pending. |
| demolish_rebuild | Palette demolition toggle, cell selection, confirmation/cancel, Runtime `demolish_facility`, synchronous/queued results. Lower landing refusal, cancellation, reception removal, same-cell reconstruction. Occupancy, operational/construction instances, costs/refund checked. | Implemented; Godot parsing, actual UI, audio ownership, Runtime results pending. |
| pan_zoom_world_identity | Move mode + non-Ctrl wheel; normal wheel zoom, 0.5/4.0 clamps; recomputed local/viewport/screen coordinates and actual selected cell. OS recorder requests CUA only. | Mock bridge/checker controls only; actual OS wheel/screenshot provenance pending. |
| separate_pid_resume | Formal four-member save; separate native writer/reader; normal load menu; whole typed/raw checkpoint identity, instance ID, cell and overlap judgement. | Collector/source prepared; real private profiles, actual distinct PIDs and formal saves pending. |
## Original-matched diagnostic choices
`BASELINE.txt` §9.3 names reception/checkout in the lobby; current
`data/facilities/reception.tres` is FRONT 4×2, cost read directly from the
definition. §10.1 explicitly gives standard room 2×1 FRONT; current
`standard_room.tres` matches and is used for unsupported-placement rejection.
§8.4 names stairs; current `infrastructure/guest_stairs.tres` and
`tests/transport/test_runtime_stairs.gd` expose ordinary two-floor stair
construction and `STAIR_LANDING_MISSING` / `STAIR_LANDING_IN_USE`.
All three IDs are in Main's existing normal palette and Runtime registry.
These choices establish this diagnostic slice, never all30 acceptance.
The handler fixture computes any diagnostic resource top-up from the four
planned positive construction definitions. It emits an initial formal save
and a budget report explicitly tagged diagnostic. No production costs,
services, balance values or definitions are changed. New art is unnecessary;
the demolition button reuses Main's existing styling and audio.
## Evidence boundary
`placement.gd` contains the shared finite scenario and handler endpoints.
`passive_placement.gd` overrides **all** input/setup/progression endpoints:
button, reveal, click, wheel, load_menu, complete_build, setup,
diagnostic_budget, save_initial_fixture, archive_formal. Its reachable
scenario neither calls product handlers nor emits their signals, advances
the clock, submits commands, resets Runtime or restores models. It reads
snapshots/reports and archives evidence. Root performs all OS gameplay.
Constructing/freeing the Main scene uses the existing ordinary cleanup helper.
The passive executable has a separate completion line. Only the handler UI
suite adds one entry to the strict marker contract and one source-pinned
ordinary cleanup policy. All previous marker/policy entries are retained.
The native OS collector never routes its process through the headless gate.
Checker controls are synthetic, including fabricated engine, PID, typed-byte
stand-ins, screenshots and weak-reference records. A positive local test
validates the checker, not the game or OS. Controller tests replace Popen,
engine probes, profiles and owner gate with mocks and launch no engine.
See `local-validation.json` and `implementation.json` for the actual final
executed source/input hashes and results.
## Still required from Root / independent review
Freeze a clean exact candidate; collect guarded native handler and real CUA
OS writer/reader evidence; run verify-only against that candidate; perform
impact checks and final full regression after related sources are frozen;
obtain independent Opus review of all five conditions; integrate and verify
the current final source, then update the authoritative ledger.
No reused P08-02 PASS is claimed for changed sources. P08-01 and the whole
P08 package remain open. Windows same-distribution/device evidence remains
open; Mac/Linux results cannot substitute for it. No outside-scope service
defect or required permission change has been demonstrated by this worker.
@@ -0,0 +1,102 @@
# Root collection protocol (not executed by worker)
Read the fixed spec and condition matrix first. Freeze/commit through the
authorized Root workflow. The worker has not committed or run this procedure.
Use a clean checkout at the exact submitted HEAD, an installed native Godot,
and a new output directory **outside** the source checkout:
```sh
python3 tools/run_p08_normal_placement.py --collect \
--project-root /absolute/frozen/checkout \
--output /absolute/build/p08-normal-placement-evidence \
--commit EXACT_FROZEN_HEAD --godot /absolute/Godot \
--input-mode native_os
BUILD_OUTPUT_DIR=/absolute/build python3 tools/check_p08_normal_placement_evidence.py \
--verify-only --project-root /absolute/frozen/checkout
```
`--input-mode handler` collects only the strict headless pair. Its index has
no OS proof and intentionally cannot pass the whole checker. `native_os`
first collects that same headless pair, then separately starts the passive
nonheadless writer/reader. Import and handler phases use unchanged
`execute_phase` guards, markers, owner/audio proof. OS phases reuse the
P08-02 `collect_native_os.py` / passive recorder protocol, read-only as reuse
sources, with P08-03 arguments and paths; no new OS automation framework.
Before native launch the controller checks live engines, exact HEAD, clean
checkout, copied source inventory, disk admission and default player.
Each OS role gets a UUID private profile. Writer receives the handler's
`pair/initial-formal`; reader receives the OS writer's `os-pair/formal`.
All four formal members are copied and SHA-checked. The staged project gets
only private-profile settings and is restored in finally. Only captured
Popen children are cleaned by `owned_outer.finish_captured`.
Actual argv is `[engine, --path, stage, --script, passive_os.gd]`, with no
headless flag or headless proof gate. `started.json`, `process-started.json`,
`ended.json`, raw engine log, actual profile/engine report, source/stage/default
player inventories and ordinary owner/audio records remain in the evidence.
Controller and recorder SHA appear in start/end documents. Each OS child
has a 900-second bound and each Root acknowledgement a 300-second bound.
Timeout/error preserves failure records and cleans only owned children.
## CUA request / read-only acknowledgement bridge
The collector prints `waiting_for_root_cua`; the recorder prints each exact
request filename and writes:
`os-pair/{writer|reader}-request-NNNN/request.json`.
It contains PID, sequence, action, target window, instructions and applicable
expected world cell, transformed viewport/screen coordinate, window/screen
scale. The recorder waits; it never clicks or changes Accessibility settings.
Root uses its existing CUA tool on the indicated PID/window. A request may
ask for normal palette scrolling, a button/cell click, ordinary non-Ctrl
wheel, normal manual load, or normal play/speed/pause until construction
finishes. Modal buttons include their own window ID and screen transform.
If a cell is clipped, the recorder first asks for ordinary zoom-out framing,
then recomputes coordinates. Do not substitute old pixels or injected input.
Capture real CUA screenshots before and after each request. Retain the raw
CUA result and tool-call identity; never manufacture a receipt to advance.
Root writes its own receipt file within the evidence directory. The bridge
envelope references that raw tool call and records:
```json
{
"method": "native_os",
"target_pid": 123,
"timestamp_ns": 123456789,
"collector_pid": 456,
"tool_name": "actual CUA tool name",
"tool_call_id": "actual tool call identity",
"events": [{"type": "actual input event", "raw_receipt": "retained raw CUA result"}]
}
```
After completing the requested operation, Root writes `ack.json` beside
the request. All artifact references are relative to the evidence root:
```json
{
"target_pid": 123,
"sequence": 1,
"request_sha256": "actual request file SHA-256",
"input_receipt": "root-receipts/writer-0001.json",
"screen_before": "root-receipts/writer-0001-before.png",
"screen_after": "root-receipts/writer-0001-after.png"
}
```
These numbers/strings are format examples, never evidence. Recorder and
controller only **read** Root's acknowledgement/receipt files. The recorder
separately retains actual Godot input observations and world selection,
whole typed/Variant snapshots, displayed reasons and Runtime results.
The checker verifies request/ack hash and sequence/PID binding, raw input
presence, PNG structure/CRC, affine coordinate conversions, formal members,
model invariants, distinct processes, native lifecycle and real owner proof.
Independent review must match tool-call identities/raw CUA results and actual
screenshots; an integrity checker is not an OS attestation authority.
Keep final requests (including save/load) as well as row-specific requests.
Do not edit evidence after collection to force a pass. Correct source/input
defects through the fixed repair workflow. GDScript parsing, actual native
and OS success, full regression, review and integration are all pending.
@@ -0,0 +1,142 @@
{
"task_id": "P08-03",
"status": "submitted",
"acceptance_ids": [
"P08-03"
],
"base_commit": "12430ba89970bda349781e51157e72e48a4ac06f",
"actual_worktree_HEAD": "12430ba89970bda349781e51157e72e48a4ac06f",
"worker_committed": false,
"final_commit": null,
"final_commit_note": "Root must freeze and record the submitted candidate; worker git writes are prohibited.",
"spec_normalized_sha256": "7941da4edbd3c43a636a4a3d834d57c095b56da988fa3a056e55b22d3a5d50e9",
"counting_scope": {
"packages": 30,
"units": 88,
"additional_units": 0,
"partial_credit": false
},
"final_local_files_sha256": {
"docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json": "f59f01f65c795057117fda753706a6200928d4954af901fc965cf1a7c09232b5",
"docs/plans/original-completion-2026-10-02/execution/p08-03/CONDITION_MATRIX.md": "656400d0f9028ff7cf272b8e02be9c329f25adb6ff2039708ca04d0251be611c",
"docs/plans/original-completion-2026-10-02/execution/p08-03/ROOT_COLLECTION.md": "d2a0da74c3b469350e9130fbd8a024be49b4c8cc9ed09154c3966bf7642c38cc",
"docs/plans/original-completion-2026-10-02/execution/p08-03/passive_os_placement_recorder.gd": "21855e3b32263eb98127202467b770cb1334fa70c6b357825a122db385ffa8d6",
"docs/plans/original-completion-2026-10-02/execution/p08-03/passive_os_placement_recorder.gd.uid": "d7f97560c8bbdfdaed173b2b48261e65b92847bd0493dd1cc2ebb309d0622314",
"scenes/main/main.gd": "e015e18fee8c114ddce39e46a04be139e694a4e3569855065576c0c909b94fc6",
"scenes/main/world_preview.gd": "2f0e417ae2ff1745062bdbdcec74b3b058cfcffbbd9001ca3e6a62cd23efb47d",
"tests/fixtures/p08-03/checker_fixture.py": "56f80f0e8a44a707c2c59e385c2fa5bd3949f1dd95d4cbf40f480870aca08d45",
"tests/fixtures/p08-03/passive_placement.gd": "f070c93000d0eceef8cc9e518fefa2ba9c7608883ab5bf9198dff224b60d4da1",
"tests/fixtures/p08-03/passive_placement.gd.uid": "f75e3f2d9555c30cee3a1de882f32225fc624565a78a7ef3f5c604325c578ce3",
"tests/fixtures/p08-03/placement.gd": "95d46b329d5b08954915a4f80b06b7774eeaf4976ef9c687e8c61433bf0d14eb",
"tests/fixtures/p08-03/placement.gd.uid": "5caca08c1e627e0ca07e2ab7b13bd82b24a22f77513fe87e30b79023568e1987",
"tests/ui/test_p08_normal_placement_ops.gd": "734f4f85d0b0fc45dc31fc23502425b911635667dabc0341ba0d88b0107f4405",
"tests/ui/test_p08_normal_placement_ops.gd.uid": "7d168ac50ca82ae1979da31c467f9a5020ca5ec7616b5bf99294413d85b3b299",
"tools/check_p08_normal_placement_evidence.py": "d79df59d4434afd5219aca9aeb4581119859d08a71c9df73ae25f01e70ab59f5",
"tools/regression/suite-policy.json": "e875e6b05bbac8dfb98c2af007221436fa558ddb8806b6214689830295b32e8d",
"tools/run_p08_normal_placement.py": "191831b4f684bdd7e99b3be6038c15a6830f544d17c941869b1c2a8fbe4a2aa3",
"tools/test_p08_normal_placement_evidence.py": "5a4a859817b7764a07d215c4811d2912c7f502de7219d57b11c3e3158dc177eb"
},
"executed_source_manifest": {
"path": "local-source-manifest.json",
"sha256": "63cd7079a4e9fb4966d829b5edcccdaedd82154c3afa2ea715622cb3d0b437b5",
"source_file_count": 1326,
"contains": "Complete local execution source map and immutable/fixture input map; no material-size omission"
},
"final_input_sha256": {
"AGENTS.md": "8d8dc40211390275f858f322743309a0663c4f2d859780a8b8a08cea1450e8b3",
"docs/plans/original-completion-2026-10-02/IMPLEMENTATION_PLAN.md": "e3d5a83b65207a402f8b36a06f9893ab969adcbb5b97915930af1358b15749e1",
"docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json": "f59f01f65c795057117fda753706a6200928d4954af901fc965cf1a7c09232b5",
"docs/plans/original-completion-2026-10-02/execution/p08-03/passive_os_placement_recorder.gd": "21855e3b32263eb98127202467b770cb1334fa70c6b357825a122db385ffa8d6",
"docs/plans/original-completion-2026-10-02/execution/p08-03/passive_os_placement_recorder.gd.uid": "d7f97560c8bbdfdaed173b2b48261e65b92847bd0493dd1cc2ebb309d0622314",
"docs/spec/acceptance/P08-03.json": "37a6a581b5a9bbe03c07a3a266b03df1f740ff488da33ecd9bd60f5b32f1efe1",
"docs/traceability/original/BASELINE.txt": "75e4bdbdeb379c9269553f0c776bdb9cc96e01f2d6aa5ca650d132c63a8aeb1a",
"tests/fixtures/p08-03/checker_fixture.py": "56f80f0e8a44a707c2c59e385c2fa5bd3949f1dd95d4cbf40f480870aca08d45",
"tests/fixtures/p08-03/passive_placement.gd": "f070c93000d0eceef8cc9e518fefa2ba9c7608883ab5bf9198dff224b60d4da1",
"tests/fixtures/p08-03/passive_placement.gd.uid": "f75e3f2d9555c30cee3a1de882f32225fc624565a78a7ef3f5c604325c578ce3",
"tests/fixtures/p08-03/placement.gd": "95d46b329d5b08954915a4f80b06b7774eeaf4976ef9c687e8c61433bf0d14eb",
"tests/fixtures/p08-03/placement.gd.uid": "5caca08c1e627e0ca07e2ab7b13bd82b24a22f77513fe87e30b79023568e1987"
},
"actual_local_validation": {
"path": "local-validation.json",
"sha256": "09701dcb7ef6a6da695f1c9b1060107e64666a8f8f0256c3caf1da9cf2d8f487",
"python_syntax": "pass: four changed/new Python sources compiled in memory",
"checker_mock_tests": 35,
"checker_mock_exit_code": 0,
"log_sha256": "c542a3079ac31374dd3fce5bc96ccb44ca8ccfc765f78fe9f18413c259c5f67b",
"git_diff_check": "pass"
},
"conditions": {
"support_connection": {
"implementation": "prepared",
"local_checker_controls": "passed on recorded final Python sources with synthetic specimens",
"actual_Godot_handler": "pending",
"actual_native_OS": "pending",
"independent_review": "pending",
"accepted": false
},
"invalid_position_reason": {
"implementation": "prepared",
"local_checker_controls": "passed on recorded final Python sources with synthetic specimens",
"actual_Godot_handler": "pending",
"actual_native_OS": "pending",
"independent_review": "pending",
"accepted": false
},
"demolish_rebuild": {
"implementation": "prepared",
"local_checker_controls": "passed on recorded final Python sources with synthetic specimens",
"actual_Godot_handler": "pending",
"actual_native_OS": "pending",
"independent_review": "pending",
"accepted": false
},
"pan_zoom_world_identity": {
"implementation": "prepared",
"local_checker_controls": "passed on recorded final Python sources with synthetic specimens",
"actual_Godot_handler": "pending",
"actual_native_OS": "pending",
"independent_review": "pending",
"accepted": false
},
"separate_pid_resume": {
"implementation": "prepared",
"local_checker_controls": "passed on recorded final Python sources with synthetic specimens",
"actual_Godot_handler": "pending",
"actual_native_OS": "pending",
"independent_review": "pending",
"accepted": false
}
},
"pending": [
"Root clean exact candidate freeze",
"Godot parsing and actual handler execution",
"Root isolated native writer/reader and ordinary owner/audio proof",
"Root real CUA input/screenshots/receipts and passive observations",
"verify-only on actual collected evidence",
"impact checks and final full regression after source freeze",
"independent Opus review of all five conditions",
"integration/current final verification and ledger",
"Windows same-distribution/device global platform acceptance"
],
"not_executed": [
"Godot",
"OS gameplay",
"native process collection",
"full regression",
"Runner operations",
"independent review",
"integration"
],
"outside_scope_modifications": [],
"outside_scope_defects_demonstrated": [],
"specification_gaps": [],
"reuse": {
"collector": "P08-02 collect_native_os.py read-only protocol reuse; no inherited changed-source PASS",
"handler": "existing execute_phase/profile/owned_outer/strict marker gates retained",
"OS": "separate nonheadless Popen/owned_outer lifecycle; Root read-only request/ack bridge",
"assets": "existing Main styling/audio; no new artwork or asset pack"
},
"whole_P08_complete": false,
"P08_01_all30_claimed": false,
"formal_acceptance": false
}
@@ -0,0 +1,5 @@
...................................
----------------------------------------------------------------------
Ran 35 tests in 17.918s
OK
@@ -0,0 +1,34 @@
{
"task_id": "P08-03",
"started_at": "2026-10-13T18:21:21.344634+00:00",
"ended_at": "2026-10-13T18:21:42.345749+00:00",
"syntax": {
"method": "compile(source, filename, exec); no pyc",
"files": {
"tools/run_p08_normal_placement.py": "191831b4f684bdd7e99b3be6038c15a6830f544d17c941869b1c2a8fbe4a2aa3",
"tools/check_p08_normal_placement_evidence.py": "d79df59d4434afd5219aca9aeb4581119859d08a71c9df73ae25f01e70ab59f5",
"tools/test_p08_normal_placement_evidence.py": "5a4a859817b7764a07d215c4811d2912c7f502de7219d57b11c3e3158dc177eb",
"tests/fixtures/p08-03/checker_fixture.py": "56f80f0e8a44a707c2c59e385c2fa5bd3949f1dd95d4cbf40f480870aca08d45"
},
"passed": true
},
"checker_tests": {
"argv": [
"/Applications/Xcode.app/Contents/Developer/usr/bin/python3",
"-B",
"tools/test_p08_normal_placement_evidence.py"
],
"exit_code": 0,
"log": "local-checker-tests.log",
"log_sha256": "c542a3079ac31374dd3fce5bc96ccb44ca8ccfc765f78fe9f18413c259c5f67b",
"synthetic_only": true,
"test_count": 35
},
"local_source_manifest": "local-source-manifest.json",
"local_source_manifest_sha256": "63cd7079a4e9fb4966d829b5edcccdaedd82154c3afa2ea715622cb3d0b437b5",
"source_and_inputs_unchanged_during_tests": true,
"native_engines_started": 0,
"OS_operations": 0,
"full_regression_executed": false,
"acceptance": "pending"
}
@@ -0,0 +1,8 @@
extends SceneTree
const Passive := preload("res://tests/fixtures/p08-03/passive_placement.gd")
func _initialize() -> void:
call_deferred("run")
func run() -> void:
var result: Dictionary = await Passive.new().run(self, OS.get_environment("P08_PLACEMENT_ROOT"), OS.get_environment("P08_PLACEMENT_ROLE"))
print("P08 passive OS placement: %d checks, %d failures" % [result.checks, result.failures])
quit(0 if result.failures == 0 else 1)
+92 -1
View File
@@ -148,6 +148,11 @@ var world_preview: WorldPreview
var front_button: Button
var back_button: Button
var excavation_button: Button
var demolition_button: Button
var demolition_dialog: ConfirmationDialog
var _demolition_mode := false
var _demolition_target: StringName = &""
var _pending_demolition_commands: Dictionary = {}
var pan_button: Button
var pause_button: Button
var speed_buttons: Dictionary = {}
@@ -792,10 +797,14 @@ func _create_bottom_bar() -> Control:
column.add_child(palette_scroll)
var grid := GridContainer.new()
grid.name = "BuildPalette"
grid.columns = MVP_FACILITY_IDS.size()
grid.columns = MVP_FACILITY_IDS.size() + 1
grid.add_theme_constant_override("h_separation", 4)
grid.add_theme_constant_override("v_separation", 0)
palette_scroll.add_child(grid)
demolition_button = _button("撤去", _toggle_demolition_mode, 72)
demolition_button.name = "DemolitionButton"
demolition_button.toggle_mode = true
grid.add_child(demolition_button)
build_status_label = Label.new()
build_status_label.name = "BuildStatusLabel"
@@ -1241,6 +1250,7 @@ func _select_build_definition(definition_id: StringName) -> void:
if definition == null:
_set_status("選択した施設定義が見つかりません。", false)
return
_set_demolition_mode(false, false)
_set_excavation_mode(false, false)
_set_pan_mode(false)
_selected_definition_id = definition_id
@@ -1267,6 +1277,9 @@ func _clear_build_selection(announce := true) -> void:
func _clear_active_tool() -> void:
if _demolition_mode:
_set_demolition_mode(false)
return
if world_preview.pan_mode:
_set_pan_mode(false)
_set_status("画面移動を解除しました。施設をクリックすると情報を表示します。", true, true)
@@ -1299,6 +1312,7 @@ func _toggle_pan_mode() -> void:
func _set_pan_mode(enabled: bool) -> void:
if enabled:
_set_demolition_mode(false, false)
_clear_build_selection(false)
_set_excavation_mode(false, false)
_set_build_palette_open(false)
@@ -1309,6 +1323,7 @@ func _set_pan_mode(enabled: bool) -> void:
func _set_excavation_mode(enabled: bool, announce := true) -> void:
if enabled:
_set_demolition_mode(false, false)
_set_pan_mode(false)
_excavation_mode = enabled
if excavation_button != null:
@@ -1354,6 +1369,9 @@ func _rotate_build_preview() -> void:
func _on_map_cell_clicked(cell: Vector3i) -> void:
if _audio_feedback != null:
_audio_feedback.request_click()
if _demolition_mode:
_preview_demolition(cell)
return
if _excavation_mode:
_start_excavation(cell)
return
@@ -2331,6 +2349,8 @@ func _apply_loaded_interface(result: Dictionary) -> void:
if _audio_feedback != null:
_audio_feedback.begin_loaded_epoch(_current_audio_foreground())
_pending_build_commands.clear()
_pending_demolition_commands.clear()
_set_demolition_mode(false, false)
_pending_excavation_commands.clear()
_pending_hire_commands.clear()
_pending_elevator_commands.clear()
@@ -2570,6 +2590,10 @@ func _on_snapshot_published(_snapshot: Dictionary = {}, _digest: String = "") ->
func _on_command_completed(result: Variant) -> void:
if result == null:
return
if _pending_demolition_commands.has(int(result.command_id)):
_pending_demolition_commands.erase(int(result.command_id))
_show_demolition_result(result)
return
if _pending_related_commands.has(int(result.command_id)):
_pending_related_commands.erase(int(result.command_id))
_set_status("関連施設の設定を反映しました。" if result.is_success() else result.message, result.is_success())
@@ -3083,3 +3107,70 @@ func _submit_ui_command(
if _audio_feedback != null:
return _audio_feedback.submit_command(command_type, payload, actor_id, target_id, priority, execute_at, idempotency_key)
return _runtime.call(&"submit_command", command_type, payload, actor_id, target_id, priority, execute_at, idempotency_key)
func _toggle_demolition_mode() -> void:
_set_demolition_mode(not _demolition_mode)
func _set_demolition_mode(enabled: bool, announce := true) -> void:
if enabled:
_set_pan_mode(false)
_set_excavation_mode(false, false)
_clear_build_selection(false)
_demolition_mode = enabled
_demolition_target = &""
if demolition_dialog != null:
demolition_dialog.hide()
if demolition_button != null:
demolition_button.set_pressed_no_signal(enabled)
if world_preview != null:
world_preview.set_demolition_mode(enabled)
if announce:
_set_status("撤去する施設のマスを選択してください。確認画面で取り消せます。" if enabled else "撤去モードを解除しました。", true, true)
func _preview_demolition(cell: Vector3i) -> void:
# Selection only; Runtime owns eligibility, occupancy and refunds.
_demolition_target = world_preview.facility_at_canvas(world_preview.cell_to_canvas(cell))
world_preview.selected_entity_id = _demolition_target
if demolition_dialog == null:
demolition_dialog = ConfirmationDialog.new()
demolition_dialog.name = "DemolitionDialog"
demolition_dialog.title = "施設の撤去"
demolition_dialog.ok_button_text = "撤去する"
demolition_dialog.cancel_button_text = "戻る"
demolition_dialog.confirmed.connect(_confirm_demolition)
demolition_dialog.canceled.connect(_cancel_demolition)
add_child(demolition_dialog)
_apply_button_style(demolition_dialog.get_ok_button())
_apply_button_style(demolition_dialog.get_cancel_button())
demolition_dialog.dialog_text = "座標 (%d, %d / %d)\n施設: %s\n\n撤去を指示しますか?" % [cell.x, cell.y, cell.z, String(_demolition_target)]
demolition_dialog.popup_centered(Vector2i(420, 210))
func _cancel_demolition() -> void:
_demolition_target = &""
_set_status("撤去を取り消しました。", true, true)
func _confirm_demolition() -> void:
if not _demolition_mode:
return
if _runtime == null or not _runtime.has_method(&"submit_command"):
_set_status("Simulation Runtimeへ接続できません。", false)
return
var target := _demolition_target
_demolition_target = &""
var result: CommandResult = _runtime.submit_command(&"demolish_facility", {&"instance_id": target})
# Paused play returns execution synchronously; running play returns acceptance.
if result.is_success() and _runtime.command_queue.get_processed_result(result.command_id) == null:
_pending_demolition_commands[result.command_id] = target
_show_demolition_result(result)
func _show_demolition_result(result: CommandResult) -> void:
var code := String(CommandResult.Code.keys()[result.code])
var reason := String(result.details.get(&"reason", ""))
_set_status("%s | %s | %s" % [code, reason, result.message], result.is_success())
_refresh_selected_entity()
world_preview.queue_redraw()
+13 -1
View File
@@ -51,6 +51,7 @@ var selected_entity_id: StringName = &"":
var build_definition: Variant = null
var build_rotation := 0
var excavation_mode := false
var demolition_mode := false
var visual_theme: VisualThemeProfile = null
var zoom_level := 1.0
@@ -134,6 +135,8 @@ func set_visual_theme(profile: VisualThemeProfile) -> void:
func set_build_definition(definition: Variant, rotation := 0) -> void:
if definition != null:
demolition_mode = false
if definition != null and excavation_mode:
set_excavation_mode(false)
build_definition = definition
@@ -157,6 +160,8 @@ func set_build_rotation(value: int) -> void:
func set_excavation_mode(enabled: bool) -> void:
if enabled:
demolition_mode = false
if excavation_mode == enabled:
_update_excavation_preview()
return
@@ -461,7 +466,7 @@ func _gui_input(event: InputEvent) -> void:
var cell: Variant = canvas_to_cell(event.position)
if not cell is Vector3i:
return
if build_definition != null or excavation_mode:
if build_definition != null or excavation_mode or demolition_mode:
cell_clicked.emit(cell)
accept_event()
return
@@ -1685,3 +1690,10 @@ func _draw_build_connection(report: Dictionary) -> void:
var message := "入口への経路接続済" if status == &"CONNECTED" else "入口への経路未接続" if status == &"DISCONNECTED" else "入口・経路を確認できません"
draw_string(ThemeDB.fallback_font, target+Vector2(8,-12), message,
HORIZONTAL_ALIGNMENT_LEFT, -1, 13, color)
func set_demolition_mode(enabled: bool) -> void:
demolition_mode = enabled
if enabled:
clear_build_definition()
set_excavation_mode(false)
queue_redraw()
+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
+13
View File
@@ -0,0 +1,13 @@
extends SceneTree
const Placement := preload("res://tests/fixtures/p08-03/placement.gd")
func _initialize() -> void:
call_deferred("run")
func run() -> void:
var directory := OS.get_environment("P08_PLACEMENT_ROOT")
var role := OS.get_environment("P08_PLACEMENT_ROLE")
if directory.is_empty():
directory = ProjectSettings.globalize_path("user://p08-placement-%d" % Time.get_ticks_usec())
role = "standalone"
var result: Dictionary = await Placement.new().run(self, directory, role)
print("P08 normal placement: %d checks, %d failures" % [result.checks, result.failures])
quit(0 if result.failures == 0 else 1)
@@ -0,0 +1 @@
uid://ca83n0rmalp03
@@ -0,0 +1,440 @@
#!/usr/bin/env python3
"""Read-only P08-03 evidence gate. Integrity success is never unit acceptance."""
import argparse
import hashlib
import importlib.util
import json
import math
import os
from pathlib import Path
import re
import sys
import struct
import zlib
import subprocess
sys.dont_write_bytecode = True
HERE = Path(__file__).resolve().parent
SPEC = 'docs/spec/acceptance/P08-03.json'
SPEC_SHA = '7941da4edbd3c43a636a4a3d834d57c095b56da988fa3a056e55b22d3a5d50e9'
CONTRACT = 'docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json'
TEST = 'tests/ui/test_p08_normal_placement_ops.gd'
RECORDER = 'docs/plans/original-completion-2026-10-02/execution/p08-03/passive_os_placement_recorder.gd'
CONDITIONS = ['support_connection', 'invalid_position_reason', 'demolish_rebuild',
'pan_zoom_world_identity', 'separate_pid_resume']
MEMBERS = ['world.bin', 'manifest.json', 'thumbnail.png', 'journal.log']
WRITER = ['unsupported', 'out_of_bounds', 'supported_connected', 'overlap',
'missing_connection', 'lower_stair', 'upper_stair', 'refused_demolition',
'cancel_demolition', 'demolition', 'rebuild', 'pan_zoom']
RUNTIME_KEYS = set(('runtime_schema_version weather world_seed next_command_id '
'staffing_projection_enabled save_lifecycle command_queue id_generator domain_events rng_streams '
'world path movement_credits mana economy facility_upkeep upkeep_settlement inventory kitchen_production '
'auto_purchase_plans elevators legacy_stairs logistics_dispatcher logistics_requests facilities '
'facility_cleanliness guests staff catalog_manifest progression hired_staff_candidate_ids operations '
'relationships event_definitions events authority defense construction excavation spatial_subslots '
'route_projection facility_infrastructure').split())
REASONS = {'unsupported': ('UNSUPPORTED', '配置不可: 構造を支えられません'),
'out_of_bounds': ('OUT_OF_BOUNDS', '配置不可: 世界の範囲外です'),
'overlap': ('OVERLAP', '配置不可: ほかの施設または工事と重なります'),
'resume_overlap': ('OVERLAP', '配置不可: ほかの施設または工事と重なります')}
def module(name, path):
spec = importlib.util.spec_from_file_location(name, path)
obj = importlib.util.module_from_spec(spec)
spec.loader.exec_module(obj)
return obj
prior = module('p08_placement_typed_codec', HERE/'check_p08_migration_evidence.py')
runner = module('p08_placement_source_inventory', HERE/'run_all_tests.py')
decode = prior.decode
typed_differences = prior.typed_differences
def sha(path): return hashlib.sha256(Path(path).read_bytes()).hexdigest()
def load(path): return json.loads(Path(path).read_bytes())
def require(ok, message):
if not ok: raise ValueError(message)
def png(data):
require(data.startswith(b'\x89PNG\r\n\x1a\n'), 'missing OS screenshot')
offset = 8; chunks = []; payload = b''
while offset < len(data):
require(offset+12 <= len(data), 'invalid OS screenshot')
size = int.from_bytes(data[offset:offset+4], 'big')
kind = data[offset+4:offset+8]; body = data[offset+8:offset+8+size]
require(offset+12+size <= len(data), 'invalid OS screenshot')
crc = int.from_bytes(data[offset+8+size:offset+12+size], 'big')
require(zlib.crc32(kind+body) == crc, 'invalid OS screenshot CRC')
if kind == b'IHDR':
require(len(body) == 13 and all(v > 0 for v in struct.unpack('>II',body[:8])), 'invalid OS screenshot dimensions')
if kind == b'IDAT': payload += body
chunks.append(kind); offset += size+12
require(chunks and chunks[0] == b'IHDR' and chunks[-1] == b'IEND' and payload, 'invalid OS screenshot chunks')
try:
require(bool(zlib.decompress(payload)), 'empty OS screenshot')
except zlib.error as exc:
raise ValueError('invalid OS screenshot pixels') from exc
def input_map(root):
names = [SPEC, CONTRACT, 'AGENTS.md', RECORDER, RECORDER+'.uid',
'docs/plans/original-completion-2026-10-02/IMPLEMENTATION_PLAN.md',
'docs/traceability/original/BASELINE.txt']
names += [p.relative_to(root).as_posix() for p in (root/'tests/fixtures/p08-03').rglob('*')
if p.is_file() and '__pycache__' not in p.parts and '.scratch' not in p.parts]
return {name: sha(root/name) for name in sorted(names)}
def keyed(rows, key):
result = {r[key]: r for r in rows}
require(len(result) == len(rows), 'duplicate '+key)
return result
def state(node):
snap = decode(node)
require(set(snap) == {'schema_version', 'clock', 'game_state', 'systems'}, 'missing whole snapshot envelope')
r = snap['systems']['runtime']
require(set(r) == RUNTIME_KEYS, 'missing whole Runtime members')
facilities = keyed(r['construction']['facilities'], 'instance_id')
jobs = keyed(r['construction']['jobs'], 'job_id')
for job in jobs.values():
if job['state'] == 'COMPLETED':
require(job['facility_instance_id'] in facilities, 'orphan completed job')
owners = {k: v['occupied_cells'] for k, v in facilities.items()}
owners.update({k: v['reserved_cells'] for k, v in jobs.items() if v['state'] not in ('COMPLETED', 'CANCELLED')})
occupancy = keyed(r['world']['occupancies'], 'entity_id')
require(set(owners) == set(occupancy), 'orphan/lost occupancy')
seen = set()
for key, cells in owners.items():
actual = {tuple(c) for c in occupancy[key]['cells']}
require(actual == {tuple(c) for c in cells} and len(actual) == len(cells), 'partial occupancy')
require(not seen.intersection(actual), 'overlapping occupancy')
seen.update(actual)
operational = keyed(r['facilities']['instances'], 'instance_id')
require(set(operational) == set(facilities), 'partial demolition/completion')
for key, row in operational.items():
require(row['occupied_cells'] == facilities[key]['occupied_cells'], 'operational cells drift')
return snap
def coordinates(p):
cell = p['world_cell']
require(cell == p['selected_cell'] and len(cell) == 3, 'world-cell drift')
bounds, origin, dimensions = p['bounds'], p['map_position'], p['cell_dimensions']
require(len(dimensions) == 2 and all(math.isfinite(v) and v > 0 for v in dimensions), 'invalid cell dimensions')
wanted = [origin[0] + (cell[0]-bounds['min_x']+0.5)*dimensions[0],
origin[1] + (bounds['max_y']-cell[1]+0.5)*dimensions[1]]
require(all(math.isfinite(v) for v in p['viewport']+p['local']), 'nonfinite viewport')
require(all(abs(a-b) < 0.001 for a, b in zip(p['local'], wanted)), 'converted cell pixel mismatch')
require(0.5 <= p['zoom'] <= 4.0, 'zoom bounds')
for target in ('viewport','screen'):
if target == 'screen' and target not in p: continue
matrix = p[target+'_transform']
require(len(matrix) == 6 and all(math.isfinite(v) for v in matrix), 'invalid viewport transform')
x, y = p['local']
expected = [matrix[0]*x+matrix[2]*y+matrix[4], matrix[1]*x+matrix[3]*y+matrix[5]]
require(len(p[target]) == 2 and all(abs(a-b) < 0.001 for a,b in zip(expected,p[target])), 'transformed '+target+' mismatch')
def preserve(a, b, results):
differences = typed_differences(a, b)
if not results:
require(not differences, 'cancel/preview partially applied')
return
# Keep every typed field. Only the command envelope may grow on a refusal.
allowed = [('systems', 'runtime', key) for key in
('command_queue', 'next_command_id', 'domain_events', 'id_generator')]
require(all(any(p[:3] == prefix for prefix in allowed) for p in differences), 'refusal partially applied')
old, new = [decode(n)['systems']['runtime'] for n in (a, b)]
require(new['next_command_id'] == old['next_command_id'] + len(results), 'refusal command sequence')
oq, nq = old['command_queue'], new['command_queue']
require(nq['processed'] == oq['processed'] + results, 'refusal result/journal mismatch')
for key in ('pending', 'in_flight'):
require(nq[key] == oq[key], 'refusal queue loss')
def action_gate(row, typed):
coordinates(row['position'])
before, after = typed(row['before']), typed(row['after'])
a, b = state(before), state(after)
require(row['position']['bounds'] == a['systems']['runtime']['world']['bounds'], 'invented world bounds')
kind, label = row['kind'], row['id']
expected_kind = 'camera' if label == 'pan_zoom' else 'cancel' if label == 'cancel_demolition' else 'demolish' if label in ('demolition','refused_demolition') else 'build'
require(kind == expected_kind, 'operation kind mismatch')
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 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 kind == 'cancel':
require(not results and row['status'] == '撤去を取り消しました。', 'cancel submitted command')
preserve(before, after, results)
elif kind == 'camera':
preserve(before, after, [])
require(row['pan_before'] != row['pan_after'], 'move-mode wheel did not pan')
require(row['ctrl_pressed'] is False and row['steps'] == [max(0.5, row['steps'][0]-0.5*i) for i in range(9)] + [min(4.0, 1.0+0.5*i) for i in range(9)], 'wheel clamp sequence')
require(row['steps'][8] == 0.5 and row['steps'][-1] == 4.0, 'missing clamp boundaries')
require(row['original_local'] != row['position']['local'], 'fixed pixels substituted for world identity')
else:
require(len(results) == 1, 'one real Runtime result required')
result = results[0]
processed = b['systems']['runtime']['command_queue']['processed']
require(result in processed, 'result absent from actual model')
if row['expected']:
require(result['code'] == 'VALIDATION_FAILED' and result['details']['reason'] == row['expected'], 'reason mismatch')
preserve(before, after, results)
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')
if not row['expected']:
fid = row['instance_id']
old = keyed(a['systems']['runtime']['construction']['facilities'], 'instance_id')
new = keyed(b['systems']['runtime']['construction']['facilities'], 'instance_id')
require(fid in old and fid not in new and set(new) == set(old)-{fid}, 'partial demolition')
require(row['position']['world_cell'] in old[fid]['occupied_cells'], 'demolition wrong cell')
refund = result['details']['refund']
resources_a, resources_b = a['game_state']['resources'], b['game_state']['resources']
require(set(resources_a) == set(resources_b) and all(resources_b[k] == v + refund.get(k, 0) for k, v in resources_a.items()), 'demolition refund mismatch')
if kind == 'build' and not row['expected']:
completed = state(typed(row['completed']))
facilities = completed['systems']['runtime']['construction']['facilities']
require(any(f['definition_id'] == row['definition'] and f['origin'] == row['position']['world_cell'] for f in facilities), 'construction completion missing')
require(row['report']['can_build'], 'positive support invalid')
cost = row['report']['total_cost']
resources_a, resources_b = a['game_state']['resources'], b['game_state']['resources']
require(set(resources_a) == set(resources_b) and all(resources_b[k] == v - cost.get(k, 0) for k, v in resources_a.items()), 'build cost mismatch')
if label == 'supported_connected':
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')
def check(project_root, evidence_dir):
root, evidence = Path(project_root).resolve(), Path(evidence_dir).resolve()
try:
index = load(evidence/'index.json')
require(index['schema'] == 'p08-normal-placement-v1' and index['acceptance'] == 'pending', 'schema/acceptance')
require(index['collection_error'] is None, 'collection failed')
require(index['conditions'] == CONDITIONS, 'missing condition')
actual = {n: v['sha256'] for n, v in runner.source_refs(root).items()}
require(index['source_map'] == actual, 'stale source')
require(index['inputs'] == input_map(root), 'stale input')
require(index['spec_sha256'] == SPEC_SHA == hashlib.sha256((root/SPEC).read_text().strip().encode()).hexdigest(), 'immutable spec')
require(index['marker_contract_sha256'] == sha(root/CONTRACT), 'stale marker')
require(re.fullmatch('[0-9a-f]{40}', index['commit']) is not None, 'commit')
require(index['commit'] == subprocess.check_output(['git','-C',str(root),'rev-parse','HEAD'],text=True).strip(), 'stale commit')
def artifact(name):
require(isinstance(name, str) and name and not Path(name).is_absolute() and '..' not in Path(name).parts, 'unsafe/missing artifact')
p = evidence/name
require(not p.is_symlink() and p.resolve().is_relative_to(evidence), 'artifact escape')
pin = index['artifacts'][name]
require(p.stat().st_size == pin['bytes'] and sha(p) == pin['sha256'], 'artifact hash '+name)
return p
def document(name): return load(artifact(name))
for name in index['artifacts']: artifact(name)
run = document('runner-input.json')
require(run['selected_source_map'] == actual and run['commit'] == index['commit'], 'runner source')
require(sha(run['engine']) == run['engine_sha256'], 'engine hash')
ended = document('runner-ended.json')
require(ended['error'] is None and not ended['cleanup_errors'], 'outer failure')
markers = module('p08_placement_markers', HERE/'strict_test_markers.py')
contracts = markers.load_contracts(root/CONTRACT)
pids = set()
def phase(name):
receipt = document(name)
pid = receipt['actual_pid']
require(type(pid) is int and pid > 0 and pid not in pids, 'missing/duplicate actual PID')
pids.add(pid)
require('--headless' in receipt['command'] and '--script' in receipt['command']
and '--path' in receipt['command'], 'handler command contract')
prefix = str(Path(name).parent)+'/'
folder = prefix+receipt['phase']+'/'
start, end, outer = [document(folder+n) for n in ('started.json', 'ended.json', 'outer-owned-finally.json')]
require(all(v['actual_pid'] == pid and v['command'] == receipt['command'] for v in (start, end)), 'raw lifecycle identity')
require(start.get('live_identity_exact') is True or start.get('tracked_child_ended_before_initial_probe') is True, 'unobserved native PID')
require(end['engine_ended'] is True and end['exit_code'] == 0, 'unended PID')
require(outer['actual_captured_pids'] == [pid] and outer['all_captured_ended'] is True and not outer['outer_cleanup_errors'] and outer['foreign_process_or_group_signals'] == 0, 'owned cleanup')
native = receipt['native_process']
require(native['exit_code'] == native['helper_exit_code'] == 0 and native['timed_out'] is False and native['lifecycle_error'] is None, 'native failure')
require(receipt['exit_code'] == 0 and receipt['engine_ended'] is True and not receipt['failed'], 'phase failure')
profile = receipt['actual_private_profile']['report']
require(profile['fresh_before_parent_initialize'] is True and profile['profile_writes_before_guard'] == 0 and profile['actual_user_data_dir'] == profile['expected_user_data_dir'], 'profile guard')
require(receipt['test'] == TEST and receipt['source_sha256'] == actual[TEST], 'executed suite source')
require(not markers.parse_raw(TEST, artifact(prefix+receipt['log']).read_text(), contracts, actual[TEST])['errors'], 'strict completion')
return pid
def os_phase(name):
end = document(name)
prefix = str(Path(name).parent)+'/'
start = document(prefix+'started.json')
spawned = document(prefix+'process-started.json')
pid = end['actual_pid']
require(type(pid) is int and pid > 0 and pid not in pids, 'missing/duplicate actual PID')
pids.add(pid)
command = end['command']
require(command == [run['engine'], '--path', end['stage'], '--script', command[-1]], 'OS native argv')
require('--headless' not in command and len(command) == 5, 'OS headless masquerade')
require(spawned['actual_pid'] == pid and spawned['command'] == command and spawned['monotonic_ns'] > 0,
'OS raw process identity')
for doc in (start, spawned, end):
require(doc['protocol'] == 'p08-placement-native-os-v1' and doc['source_map'] == actual
and doc['commit'] == index['commit'], 'OS exact source/commit')
require(doc['controller_sha256'] == actual['tools/run_p08_normal_placement.py']
and doc['recorder_sha256'] == sha(root/RECORDER), 'OS controller/recorder SHA')
require(doc['command'] == command and doc['engine_sha256'] == run['engine_sha256'], 'OS command/engine drift')
require(sha(artifact(prefix+'passive_os.gd')) == sha(root/RECORDER), 'executed OS recorder SHA')
require(end['error'] is None and not end['guard_errors'] and not end['cleanup_errors']
and end['exit_code'] == 0 and end['engine_ended'] is True and end['passed'] is True, 'OS native failure')
cleanup = end['owned_cleanup']
require(len(cleanup) == 1 and cleanup[0]['pid'] == pid and cleanup[0]['engine_ended'] is True
and not cleanup[0]['outer_cleanup_errors'] and cleanup[0]['foreign_process_or_group_signals'] == 0,
'OS owned cleanup')
require(end['stage_before'] == end['stage_after'] and end['default_before'] == end['default_after'], 'OS protected state changed')
require({n:v['sha256'] for n,v in end['source_after'].items()} == actual, 'OS source changed')
require(end['profile_before'] == {'existed':False,'files':{}}, 'OS profile not fresh')
owners = module('p08_verify_os_owners', HERE/'regression/ordinary_proof_gate.py')
actual_owner_proof = owners.ordinary_gate(evidence/prefix/'teardown', pid, evidence/prefix, ['single'])
require(end['ordinary_owner_proof'] == actual_owner_proof, 'OS ordinary owner proof mismatch')
profile = document('os-pair/'+end['role']+'-profile.json')
require(profile['pid'] == pid and Path(profile['user_data_dir']).resolve() == Path(end['profile']).resolve()
and profile['display'] != 'headless', 'OS actual profile/display')
raw = artifact(prefix+'engine-native.log').read_text()
require('ERROR:' not in raw and re.findall(r'(?m)^P08 passive OS placement: ([0-9]+) checks, 0 failures$', raw)
and 'P08 normal placement:' not in raw, 'OS raw diagnostics/completion')
members = end['formal_input']
require(set(members) == set(MEMBERS), 'OS missing formal input')
source_dir = 'pair/initial-formal' if end['role'] == 'writer' else 'os-pair/formal'
for member, pin in members.items():
source = artifact(source_dir+'/'+member)
require(pin == {'bytes':source.stat().st_size,'sha256':sha(source)}, 'OS formal input SHA')
return pid
case = index['cases'][0]
require(len(index['cases']) == 1 and len(case['phases']) == 2, 'writer/reader pair')
pair_pids = [phase(n) for n in case['phases']]
def pair_gate(directory, expected_pids, native_os=False):
def typed(name):
artifact(directory+'/'+name+'.var')
return document(directory+'/'+name+'.json')
rows = []
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')
require(bool(process['engine']['hash']) and bool(process['engine']['string']), 'engine version')
doc = document(directory+'/'+role+'-actions.json')
require(doc['setup'] == 'diagnostic', 'unlabelled diagnostic')
expected = WRITER if role == 'writer' else ['resume_overlap']
require([r['id'] for r in doc['actions']] == expected, 'missing operations')
if role == 'writer':
budget = doc['budget']
require(budget['setup'] == 'diagnostic', 'budget setup')
costs = {'reception': root/'data/facilities/reception.tres', 'guest_stairs': root/'data/facilities/infrastructure/guest_stairs.tres'}
total = {'gold': 0, 'materials': 0}
require([r['definition'] for r in budget['costs']] == ['reception', 'reception', 'guest_stairs', 'guest_stairs'], 'diagnostic build roster')
for entry in budget['costs']:
text = costs[entry['definition']].read_text()
line = re.search(r'^construction_cost = (.+)$', text, re.M).group(1)
original = {k: int(v) for k, v in re.findall(r'&"([^"]+)":\s*(\d+)', line)}
require(entry['cost'] == original, 'invented diagnostic cost')
for k in total: total[k] += original.get(k, 0)
delta = {k: v-budget['initial'][k] for k, v in total.items() if v > budget['initial'][k]}
require(total == budget['needed'] and delta == budget['deltas'], 'diagnostic budget mismatch')
for row in doc['actions']:
require(row['input_method'] == ('native_os' if native_os else 'handler'), 'fake OS provenance')
action_gate(row, typed)
if row['kind'] == 'build':
wanted_definition = 'standard_room' if row['id'] == 'unsupported' else 'guest_stairs' if row['id'] in ('missing_connection','lower_stair','upper_stair') else 'reception'
require(row['definition'] == wanted_definition, 'diagnostic definition substituted')
if not row.get('expected', REASONS.get(row['id'], ('',))[0]):
path = root/('data/facilities/infrastructure/' if wanted_definition == 'guest_stairs' else 'data/facilities/')/(wanted_definition+'.tres')
text = path.read_text()
line = re.search(r'^construction_cost = (.+)$',text,re.M).group(1)
cost = {k:int(v) for k,v in re.findall(r'&"([^"]+)":\s*(\d+)',line)}
require(row['report']['total_cost'] == cost, 'invented production cost')
width,height = map(int,re.search(r'^footprint = Vector2i\((\d+), (\d+)\)$',text,re.M).groups())
x,y,z = row['position']['world_cell']
cells = {(x+dx,y+dy,z) for dx in range(width) for dy in range(height)}
completed = state(typed(row['completed']))['systems']['runtime']['construction']['facilities']
matches = [f for f in completed if f['origin'] == [x,y,z] and f['definition_id'] == wanted_definition]
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')
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:
require(saved['members'][member] == sha(artifact(directory+'/formal/'+member)), 'formal member changed')
facilities = keyed(decode(checkpoint)['systems']['runtime']['construction']['facilities'], 'instance_id')
require(saved['instance_id'] in facilities and facilities[saved['instance_id']]['origin'] == saved['cell'], 'wrong resume instance')
by_id = {r['id']: r for r in rows}
require(all(by_id[n]['position']['world_cell'] == saved['cell'] for n in ('supported_connected', 'demolition', 'rebuild', 'pan_zoom', 'resume_overlap')), 'target world-cell drift')
require(by_id['demolition']['instance_id'] != saved['instance_id'], 'rebuild did not create new instance')
return rows
pair_gate(case['directory'], pair_pids)
os_proof = document(index['os_proof'])
require(os_proof['source_map'] == actual and os_proof['inputs'] == index['inputs'], 'stale OS source/input')
require(os_proof['input_method'] == 'native_os' and os_proof['observer_mutations'] == [], 'fake OS provenance')
os_pids = [os_phase(n) for n in os_proof['phases']]
require(len(os_pids) == 2, 'OS separate PID pair')
os_rows = pair_gate(os_proof['directory'], os_pids, True)
lifecycle = []
for role, pid in zip(('writer','reader'),os_pids):
requests = document('os-pair/'+role+'-requests.json')
require(requests['pid'] == pid, 'OS lifecycle PID')
actions = [document(str(Path(name).parent)+'/request.json') for name in requests['requests']]
require(any(a['action'] == 'load_menu' for a in actions), 'missing OS normal load')
if role == 'writer':
require(any(a['action'] == 'button' and a.get('node','').endswith('/ManualSaveButton') for a in actions), 'missing OS normal save')
lifecycle.append({'id':role+'-lifecycle', 'requests':requests['requests']})
all_ack = {name for name in index['artifacts'] if name.startswith('os-pair/') and name.endswith('/ack.json')}
require(all_ack == {name for row in lifecycle for name in row['requests']}, 'omitted OS lifecycle requests')
require(all(set(row['requests']) <= all_ack for row in os_rows), 'OS action request missing')
os_rows += lifecycle
for row in os_rows:
requests = row['requests']
require(requests and len(requests) == len(set(requests)), 'missing/duplicate OS requests')
expected_pid = os_pids[1 if row['id'] in ('resume_overlap','reader-lifecycle') else 0]
wheel_requests = []
for name in requests:
ack = document(name)
folder = str(Path(name).parent)+'/'
request = document(folder+'request.json')
observed = document(folder+'observed.json')
require(ack['target_pid'] == request['target_pid'] == observed['pid'] == expected_pid
and ack['sequence'] == request['sequence'] == observed['sequence'], 'fake OS provenance')
require(ack['request_sha256'] == sha(artifact(folder+'request.json')), 'OS ack request mismatch')
raw = document(ack['input_receipt'])
require(raw['method'] == 'native_os' and raw['target_pid'] == expected_pid and raw['timestamp_ns'] > 0
and raw['tool_name'] and raw['tool_call_id'], 'fake OS provenance')
require(raw['events'] and observed['events'], 'missing OS events')
require(raw['collector_pid'] > 0 and raw['collector_pid'] not in os_pids, 'fake OS collector')
for key in ('screen_before', 'screen_after'):
png(artifact(ack[key]).read_bytes())
if request['action'] == 'cell' and 'position' in row:
require(request['world_cell'] == row['position']['world_cell']
and request['viewport'] == row['position']['viewport']
and request['screen'] == row['position']['screen'], 'OS transformed cell drift')
if request['action'] == 'wheel':
wheel_requests.append(request)
if row['id'] == 'pan_zoom':
require(any(e['move_mode'] and not e['ctrl'] for e in wheel_requests), 'missing OS pan')
require(sum(not e['move_mode'] and not e['ctrl'] for e in wheel_requests) == 18, 'missing OS zoom')
# Full regression remains a separately verified Root requirement; this
# finite gate cannot replace the standard runner or independent review.
return []
except (KeyError, ValueError, TypeError, OSError, IndexError, AttributeError, AssertionError, subprocess.SubprocessError) as exc:
return [str(exc)]
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--verify-only', action='store_true', required=True)
parser.add_argument('--project-root', type=Path, default=HERE.parent)
args = parser.parse_args(argv)
build = os.environ.get('BUILD_OUTPUT_DIR')
if not build:
print('BUILD_OUTPUT_DIR is required', file=sys.stderr)
return 2
deficits = check(args.project_root, Path(build)/'p08-normal-placement-evidence')
print(json.dumps({'task_id': 'P08-03', 'integrity': 'fail' if deficits else 'pass',
'deficits': deficits, 'acceptance': 'pending'}, ensure_ascii=False))
return 1 if deficits else 0
if __name__ == '__main__':
raise SystemExit(main())
+9
View File
@@ -391,6 +391,15 @@
],
"old_bounded_teardown": false,
"new_audio_QA": false
},
"tests/ui/test_p08_normal_placement_ops.gd": {
"source_sha256": "734f4f85d0b0fc45dc31fc23502425b911635667dabc0341ba0d88b0107f4405",
"ordinary_cleanup_prepared": true,
"ordinary_control_labels": [
"single"
],
"old_bounded_teardown": false,
"new_audio_QA": false
}
},
"outside_routes": {
+248
View File
@@ -0,0 +1,248 @@
#!/usr/bin/env python3
"""Root-only native collection. Does not run regression or claim acceptance."""
import argparse
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import time
import uuid
import re
sys.dont_write_bytecode = True
HERE = Path(__file__).resolve().parent
CONTRACT = 'docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json'
CONDITIONS = ('support_connection', 'invalid_position_reason', 'demolish_rebuild',
'pan_zoom_world_identity', 'separate_pid_resume')
RECORDER = 'docs/plans/original-completion-2026-10-02/execution/p08-03/passive_os_placement_recorder.gd'
def collect_os(r, root, out, stage, engine, commit, refs, protected, outer):
"""P08-02 collect_native_os protocol; deliberately not execute_phase/headless.
Root supplies CUA ack/receipt files in the requested directories. Neither
controller nor passive endpoints generate input or change permissions.
"""
r.require(not outer.native.helper['live_engines'](), 'external engine present')
before = r.inventory(stage, stage=True)
public = (stage/'project.godot').read_bytes()
pair = out/'os-pair'
pair.mkdir()
r.copy_source(out/'pair/diagnostic-budget.json', pair/'diagnostic-budget.json')
phases = []
for role in ('writer', 'reader'):
r.require(not outer.native.helper['live_engines'](), 'external engine present')
r.require(r.source_refs(root) == refs and r.default_player(root) == protected, 'OS before guards')
r.require(r.inventory(stage, stage=True) == before, 'OS stage before guard')
r.require(shutil.disk_usage(out).free >= 500*1024*1024, 'OS disk admission')
folder = out/'os-runs'/role
folder.mkdir(parents=True)
name = 'TheTree-PCK-tests-'+uuid.uuid4().hex+'-p08-placement-'+role
profile = r.profile_path(name)
r.require(not profile.exists(), 'fresh OS private profile')
formal_source = out/('pair/initial-formal' if role == 'writer' else 'os-pair/formal')
formal_before = r.inventory(formal_source)
r.require(set(formal_before) == {'world.bin','manifest.json','thumbnail.png','journal.log'}, 'all formal inputs')
formal = profile/'saves/manual_ui'
formal.mkdir(parents=True)
for member, pin in formal_before.items():
r.copy_source(formal_source/member, formal/member)
r.require(r.sha(formal/member) == pin['sha256'], 'formal fixture copy SHA')
recorder = folder/'passive_os.gd'
r.copy_source(root/RECORDER, recorder)
proof = folder/'teardown'
(proof/'owned-audio').mkdir(parents=True)
env = r.fresh_environment(dict(os.environ))
env.update(P08_PLACEMENT_ROOT=str(pair), P08_PLACEMENT_ROLE=role,
THE_TREE_AUDIO_TEARDOWN_PROOF_DIR=str(proof/'owned-audio'),
THE_TREE_AUDIO_FIXTURE_CONTROL_PROOF=str(proof/'control.json'))
command = [engine, '--path', str(stage), '--script', str(recorder)]
base = {'protocol':'p08-placement-native-os-v1', 'role':role, 'commit':commit,
'source_map':{n:v['sha256'] for n,v in refs.items()}, 'stage':str(stage),
'stage_before':before, 'profile':str(profile), 'profile_name':name,
'profile_before':{'existed':False, 'files':{}}, 'formal_input':formal_before,
'formal_source':str(formal_source), 'owner_pid':os.getpid(),
'default_before':protected, 'command':command, 'engine_sha256':r.sha(engine),
'controller_sha256':r.sha(root/'tools/run_p08_normal_placement.py'),
'recorder_sha256':r.sha(recorder), 'formal_acceptance':False,
'environment':{k:v for k,v in env.items() if k.startswith(('P08_', 'THE_TREE_'))},
'environment_sha256':hashlib.sha256(json.dumps(env,sort_keys=True).encode()).hexdigest()}
r.write(folder/'started.json', base)
error = None; children = []; guards = []; exit_code = None; owner_proof = None
try:
(stage/'project.godot').write_bytes(r.private_project(public, name))
with (folder/'engine-native.log').open('x') as log:
process = subprocess.Popen(command, cwd=stage, env=env, stdout=log, stderr=subprocess.STDOUT)
children.append(process)
r.write(folder/'process-started.json', dict(base, actual_pid=process.pid,
monotonic_ns=time.monotonic_ns(), state='waiting_for_root_cua_requests'))
print(json.dumps({'actual_pid':process.pid, 'request_directory':str(pair),
'state':'waiting_for_root_cua', 'role':role}), flush=True)
exit_code = process.wait(timeout=900)
r.require(exit_code == 0, 'ordinary native OS exit0')
raw = (folder/'engine-native.log').read_text()
r.require('ERROR:' not in raw and 'SCRIPT ERROR:' not in raw, 'OS native diagnostics')
matches = re.findall(r'(?m)^P08 passive OS placement: ([0-9]+) checks, 0 failures$', raw)
r.require(len(matches) == 1 and int(matches[0]) > 0, 'passive observer completion')
observed = r.read(pair/(role+'-profile.json'))
r.require(observed['pid'] == process.pid and Path(observed['user_data_dir']).resolve() == profile.resolve()
and observed['display'] != 'headless', 'actual OS private profile/display')
owners = r.module('p08_os_owner_gate', r.SUPPORT/'ordinary_proof_gate.py')
owner_proof = owners.ordinary_gate(proof, process.pid, folder, ['single'])
except BaseException as caught:
error = type(caught).__name__+': '+str(caught)
finally:
try:
cleanup, cleanup_errors = outer.finish_captured(children)
finally:
(stage/'project.godot').write_bytes(public)
try:
r.require(r.source_refs(root) == refs, 'OS source unchanged')
r.require(r.inventory(stage, stage=True) == before, 'OS stage unchanged')
r.require(r.default_player(root) == protected, 'OS default player unchanged')
r.require(r.inventory(formal_source) == formal_before, 'formal input unchanged')
r.require(r.sha(recorder) == base['recorder_sha256'], 'recorder unchanged')
except BaseException as caught:
guards.append(type(caught).__name__+': '+str(caught))
passed = not (error or guards or cleanup_errors) and len(children) == 1 and exit_code == 0
r.write(folder/'ended.json', dict(base, actual_pid=children[0].pid if children else None,
exit_code=exit_code, engine_ended=bool(cleanup) and all(v['engine_ended'] for v in cleanup),
error=error, guard_errors=guards, cleanup_errors=cleanup_errors, owned_cleanup=cleanup,
ordinary_owner_proof=owner_proof, stage_after=r.inventory(stage,stage=True),
default_after=r.default_player(root), source_after=r.source_refs(root),
profile_after=r.inventory(profile), passed=passed))
phases.append('os-runs/'+role+'/ended.json')
r.require(passed, 'native OS collection failed: '+str(error or guards or cleanup_errors))
return phases
def module(name, path):
spec = importlib.util.spec_from_file_location(name, path)
obj = importlib.util.module_from_spec(spec)
spec.loader.exec_module(obj)
return obj
def absolute(value):
path = Path(value)
if not path.is_absolute():
raise argparse.ArgumentTypeError('absolute path required')
return path.resolve()
def collect(args):
root, out = args.project_root, args.output
if out.is_relative_to(root):
raise ValueError('evidence output must be outside the frozen source checkout')
r = module('p08_standard_runner', root/'tools/run_all_tests.py')
commit = subprocess.check_output(['git', '-C', str(root), 'rev-parse', 'HEAD'], text=True).strip()
r.require(commit == args.commit, 'actual final submitted commit required')
r.require(not subprocess.check_output(['git','-C',str(root),'status','--porcelain'], text=True), 'frozen clean worktree required')
r.require(not out.exists(), 'fresh output; never overwrite earlier proof')
refs = r.source_refs(root)
checker = module('p08_initial_inputs', root/'tools/check_p08_normal_placement_evidence.py')
inputs_before = checker.input_map(root)
outer = r.module('p08_owned_outer', r.SUPPORT/'owned_outer.py')
r.require(not outer.native.helper['live_engines'](), 'one native engine')
engine = shutil.which(args.godot)
r.require(engine is not None, 'native engine exists')
engine = str(Path(engine).resolve())
out.mkdir(parents=True)
stage = out/'stage'; stage.mkdir()
(out/'starters').mkdir(); (out/'bootstrap').mkdir()
for name, pin in r.read(r.SUPPORT/'bootstrap-scripts.json').items():
data = pin['text'].encode()
r.require(hashlib.sha256(data).hexdigest() == pin['sha256'], 'bootstrap pin')
(out/'bootstrap'/name).write_bytes(data)
for name, pin in refs.items():
target = stage/name; target.parent.mkdir(parents=True, exist_ok=True)
r.copy_source(Path(pin['path']), target)
r.require(r.sha(target) == pin['sha256'], 'whole copied source '+name)
expected = {n: {'bytes':v['bytes'], 'sha256':v['sha256']} for n,v in refs.items()}
selected = {'root_before':refs, 'map':{n:v['sha256'] for n,v in refs.items()}, 'timeout':180}
r.require(r.inventory(stage, stage=True) == expected, 'complete private source')
protected = r.default_player(root); public = (stage/'project.godot').read_bytes()
paths = ['res://'+n for n in sorted(refs) if n.endswith('.gd')]
r.write(out/'parse-paths.json', paths)
markers = r.module('p08_strict_markers', root/'tools/strict_test_markers.py')
contracts = markers.load_contracts(root/CONTRACT)
policy = r.read(r.SUPPORT/'suite-policy.json')
diagnostics = r.module('p08_diagnostics', r.SUPPORT/'diagnostic_gate.py')
owners = r.module('p08_owners', r.SUPPORT/'ordinary_proof_gate.py')
audio = r.module('p08_audio', r.SUPPORT/'audio_evidence_gate.py')
r.write(out/'runner-input.json', {'protocol':'p08-normal-placement-v1', 'commit':commit,
'selected_source_map':selected['map'], 'engine':engine, 'engine_sha256':r.sha(engine),
'owner_pid':os.getpid(), 'default_player':protected, 'diagnostic_seed':42533, 'inputs':inputs_before})
phases = []; previous = {}; cases = []; error = None; os_proof = None
deadline = time.monotonic()+(7200 if args.input_mode == 'native_os' else 900)
saved_env = {k:os.environ.get(k) for k in ('P08_PLACEMENT_ROOT','P08_PLACEMENT_ROLE','P08_PLACEMENT_INPUT')}
def phase(label, test):
remaining = int(deadline-time.monotonic())
r.require(remaining >= (180 if label == 'import' else 1), '900s collection deadline')
selected['timeout'] = min(180, remaining)
phase_out = out/'runs'/label
phase_out.mkdir(parents=True)
(phase_out/'starters').mkdir(); (phase_out/'bootstrap').mkdir()
for original in (out/'bootstrap').iterdir():
r.copy_source(original,phase_out/'bootstrap'/original.name)
r.require(r.sha(original)==r.sha(phase_out/'bootstrap'/original.name),'bootstrap full copy')
r.write(phase_out/'parse-paths.json',paths)
row = r.execute_phase(engine, stage, phase_out, label, test, selected, expected, previous,
policy, contracts, paths, public, protected, root, outer, markers, diagnostics, owners, audio)
phases.append('runs/'+label+'/'+label+'-receipt.json')
r.require(not row['failed'], 'strict phase failed: '+label)
try:
phase('import', None); phase('parse', None)
pair = out/'pair'
preserved = {}
for role in ('writer', 'reader'):
os.environ.update(P08_PLACEMENT_ROOT=str(pair), P08_PLACEMENT_ROLE=role, P08_PLACEMENT_INPUT='handler')
phase(role, 'tests/ui/test_p08_normal_placement_ops.gd')
actual = r.inventory(pair)
r.require(all(actual.get(n) == pin for n, pin in preserved.items()),
'reader preserved every writer and formal byte')
preserved = actual
cases.append({'directory':'pair', 'phases':phases[-2:], 'files':preserved})
if args.input_mode == 'native_os':
os_phases = collect_os(r, root, out, stage, engine, commit, refs, protected, outer)
checker = module('p08_os_input_map', root/'tools/check_p08_normal_placement_evidence.py')
os_proof = 'os-proof.json'
r.write(out/os_proof, {'source_map':selected['map'], 'inputs':checker.input_map(root),
'input_method':'native_os', 'observer_mutations':[], 'phases':os_phases, 'directory':'os-pair'})
except BaseException as caught:
error = type(caught).__name__+': '+str(caught)
finally:
cleanup, cleanup_errors = outer.finish_captured(outer.OWNED)
for key,value in saved_env.items():
if value is None: os.environ.pop(key, None)
else: os.environ[key] = value
if r.source_refs(root) != refs or r.default_player(root) != protected:
error = (error+'; ' if error else '')+'protected root/profile changed'
if checker.input_map(root) != inputs_before:
error = (error+'; ' if error else '')+'spec/fixture/controller inputs changed'
r.write(out/'runner-ended.json', {'error':error, 'cleanup':cleanup, 'cleanup_errors':cleanup_errors,
'phases':phases, 'formal_acceptance':False, 'native_Windows':'pending'})
checker = module('p08_placement_checker', root/'tools/check_p08_normal_placement_evidence.py')
r.write(out/'index.json', {'schema':'p08-normal-placement-v1', 'commit':commit,
'source_map':selected['map'], 'inputs':inputs_before,
'marker_contract_sha256':r.sha(root/CONTRACT),
'spec_sha256':hashlib.sha256((root/'docs/spec/acceptance/P08-03.json').read_text().strip().encode()).hexdigest(),
'cases':cases, 'phases':phases, 'conditions':list(CONDITIONS),
'artifacts':{str(p.relative_to(out)):{'bytes':p.stat().st_size,'sha256':r.sha(p)}
for p in out.rglob('*') if p.is_file() and 'stage' not in p.relative_to(out).parts},
'os_proof':os_proof, 'regression':None,
'acceptance':'pending', 'collection_error':error})
return 1 if error or cleanup_errors else 0
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--project-root', type=absolute, required=True)
parser.add_argument('--output', type=absolute, required=True)
parser.add_argument('--commit', required=True)
parser.add_argument('--godot', default='godot')
parser.add_argument('--input-mode', choices=('handler', 'native_os'), default='handler')
parser.add_argument('--collect', action='store_true', required=True)
return collect(parser.parse_args(argv))
if __name__ == '__main__':
raise SystemExit(main())
+259
View File
@@ -0,0 +1,259 @@
#!/usr/bin/env python3
"""Whole-checker positive and semantic corruption tests; synthetic proof only."""
import copy
import importlib.util
import json
from pathlib import Path
import sys
import tempfile
import unittest
from unittest.mock import patch
from types import SimpleNamespace
import shutil
import contextlib
import io
import os
sys.dont_write_bytecode = True
ROOT = Path(__file__).resolve().parents[1]
def module(name, path):
spec = importlib.util.spec_from_file_location(name, path)
value = importlib.util.module_from_spec(spec)
spec.loader.exec_module(value)
return value
C = module('placement_checker_test', ROOT/'tools/check_p08_normal_placement_evidence.py')
F = module('placement_synthetic_fixture', ROOT/'tests/fixtures/p08-03/checker_fixture.py')
D = module('placement_collector_test', ROOT/'tools/run_p08_normal_placement.py')
class EvidenceTests(unittest.TestCase):
def setUp(self):
scratch = ROOT/'tests/fixtures/p08-03/.scratch'
scratch.mkdir(exist_ok=True)
self.temp = tempfile.TemporaryDirectory(dir=scratch)
self.addCleanup(self.temp.cleanup)
self.root, self.out = F.make(Path(self.temp.name), ROOT, C)
def edit(self, path, callback):
p = self.out/path
data = json.loads(p.read_text())
callback(data)
F.put(p, data)
F.repin(self.out)
def reject(self, text):
errors = C.check(self.root, self.out)
self.assertTrue(errors, 'corrupted proof accepted')
self.assertIn(text, ' '.join(errors))
def test_positive_whole_checker(self):
self.assertEqual([], C.check(self.root, self.out))
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):
self.assertEqual(0,C.main(['--verify-only','--project-root',str(self.root)]))
self.assertEqual('pending',json.loads(output.getvalue())['acceptance'])
def test_verify_only_cli_rejects_semantic_corruption(self):
self.edit('pair/writer-actions.json', lambda d: d['actions'][0].update(status='配置可能'))
output = io.StringIO()
with patch.dict(os.environ, {'BUILD_OUTPUT_DIR':str(self.out.parent)}), contextlib.redirect_stdout(output):
self.assertEqual(1,C.main(['--verify-only','--project-root',str(self.root)]))
self.assertIn('reason mismatch',json.loads(output.getvalue())['deficits'])
def test_reason_mismatch(self):
self.edit('pair/writer-actions.json', lambda d: d['actions'][0].update(status='配置可能'))
self.reject('reason mismatch')
def test_world_cell_drift(self):
self.edit('pair/writer-actions.json', lambda d: d['actions'][0]['position'].update(selected_cell=[9, 9, 0]))
self.reject('world-cell drift')
def test_transformed_viewport_corruption(self):
self.edit('pair/writer-actions.json', lambda d: d['actions'][0]['position'].update(viewport=[1,2]))
self.reject('transformed viewport mismatch')
def test_stale_source(self):
(self.root/C.TEST).write_text('changed')
self.reject('stale source')
def test_stale_commit(self):
self.edit('index.json', lambda d: d.update(commit='0'*40))
self.reject('stale commit')
def test_stale_input(self):
(self.root/'docs/traceability/original/BASELINE.txt').write_text('changed')
self.reject('stale input')
def test_missing_os(self):
self.edit('index.json', lambda d: d.update(os_proof=None))
self.reject('missing artifact')
def test_fake_os_method(self):
self.edit('os-pair/unsupported-os.json', lambda d: d.update(method='handler'))
self.reject('fake OS provenance')
def test_fake_os_pid(self):
self.edit('os-pair/unsupported-os.json', lambda d: d.update(target_pid=555))
self.reject('fake OS provenance')
def test_same_pid_resume(self):
self.edit('runs/reader/reader-receipt.json', lambda d: d.update(actual_pid=101))
self.reject('duplicate actual PID')
def test_orphan_occupancy(self):
path = 'pair/writer-demolition-after.json'
model = C.decode(json.loads((self.out/path).read_text()))
model['systems']['runtime']['world']['occupancies'].append({'entity_id': 'ghost', 'cells': [[-8, 0, 0]]})
F.put(self.out/path, F.typed(model)); F.repin(self.out)
self.reject('orphan/lost occupancy')
def test_partial_demolition(self):
path = 'pair/writer-demolition-after.json'
model = C.decode(json.loads((self.out/path).read_text()))
model['systems']['runtime']['facilities']['instances'].append({'instance_id': 'f1', 'occupied_cells': [[-8, 0, 0]]})
F.put(self.out/path, F.typed(model)); F.repin(self.out)
self.reject('partial demolition')
def test_refusal_partial_resources(self):
path = 'pair/writer-refused_demolition-after.json'
model = C.decode(json.loads((self.out/path).read_text()))
model['game_state']['resources']['gold'] -= 1
F.put(self.out/path, F.typed(model)); F.repin(self.out)
self.reject('refusal partially applied')
def test_wrong_resume(self):
path = 'pair/reader-checkpoint.json'
model = C.decode(json.loads((self.out/path).read_text()))
model['clock']['absolute_minute'] += 1
F.put(self.out/path, F.typed(model)); F.repin(self.out)
self.reject('wrong resume model')
def test_missing_formal_member(self):
(self.out/'pair/formal/journal.log').unlink()
F.repin(self.out)
self.reject('journal.log')
def test_missing_model_member(self):
path = 'pair/writer-unsupported-before.json'
model = C.decode(json.loads((self.out/path).read_text()))
del model['systems']['runtime']['inventory']
F.put(self.out/path, F.typed(model)); F.repin(self.out)
self.reject('missing whole Runtime members')
def test_missing_clamp_boundary(self):
self.edit('pair/writer-actions.json', lambda d: d['actions'][-1].update(steps=[1]*18))
self.reject('wheel clamp sequence')
def test_no_actual_pan(self):
self.edit('pair/writer-actions.json', lambda d: d['actions'][-1].update(pan_after=[0,0]))
self.reject('move-mode wheel did not pan')
def test_handler_command_must_remain_headless(self):
self.edit('runs/writer/writer-receipt.json', lambda d: d['command'].remove('--headless'))
self.reject('handler command contract')
def test_os_headless_masquerade(self):
self.edit('os-runs/writer/ended.json', lambda d: d['command'].insert(1, '--headless'))
self.reject('OS native argv')
def test_stale_os_recorder(self):
self.edit('os-runs/writer/ended.json', lambda d: d.update(recorder_sha256='0'*64))
self.reject('OS controller/recorder SHA')
def test_os_process_unended(self):
self.edit('os-runs/writer/ended.json', lambda d: d.update(engine_ended=False))
self.reject('OS native failure')
def test_os_default_player_changed(self):
self.edit('os-runs/writer/ended.json', lambda d: d.update(default_after={'changed':True}))
self.reject('OS protected state changed')
def test_os_actual_observation_missing(self):
self.edit('os-pair/unsupported-1/observed.json', lambda d: d.update(events=[]))
self.reject('missing OS events')
def test_os_ack_for_another_request(self):
self.edit('os-pair/unsupported-1/ack.json', lambda d: d.update(request_sha256='0'*64))
self.reject('OS ack request mismatch')
def test_refusal_cannot_be_relabelled(self):
self.edit('pair/writer-actions.json', lambda d: d['actions'][4].update(expected='OTHER_REASON'))
self.reject('required refusal reason changed')
def test_demolition_cannot_be_relabelled(self):
self.edit('pair/writer-actions.json', lambda d: d['actions'][9].update(kind='camera'))
self.reject('operation kind mismatch')
def test_png_signature_is_not_a_screenshot(self):
(self.out/'os-pair/screen.png').write_bytes(b'\x89PNG\r\n\x1a\nFAKE')
F.repin(self.out)
self.reject('invalid OS screenshot')
def test_omitted_os_lifecycle_request(self):
self.edit('os-pair/writer-requests.json', lambda d: d['requests'].pop())
self.reject('omitted OS lifecycle requests')
def test_os_audio_boolean_cannot_replace_owner_evidence(self):
self.edit('os-runs/writer/teardown/owned-audio/owner_1.json', lambda d: d['final_targets'][0].update(alive=True))
self.reject('real WeakRef release')
def collector_mock(self, fail_wait=False):
"""No native engine: Popen, profiles, live engine probes are all mocked."""
output = Path(self.temp.name)/'mock-collection'
stage = output/'stage'; stage.mkdir(parents=True)
shutil.copyfile(self.root/'project.godot',stage/'project.godot')
public = (stage/'project.godot').read_bytes()
(output/'pair').mkdir()
shutil.copytree(self.out/'pair/formal',output/'pair/initial-formal')
F.put(output/'pair/diagnostic-budget.json',{'setup':'synthetic mock'})
refs = C.runner.source_refs(self.root)
protected = {'mock_default_player':True}
captured = []; cleaned = []
def spawn(argv, **kw):
self.assertNotIn('--headless', argv)
self.assertEqual(['--path',str(stage),'--script'],argv[1:4])
role = kw['env']['P08_PLACEMENT_ROLE']
profile_text = (stage/'project.godot').read_text()
name = __import__('re').search(r'config/custom_user_dir_name="([^"]+)"',profile_text).group(1)
pid = 801+len(captured)
process = SimpleNamespace(pid=pid)
def wait(timeout):
self.assertEqual(900,timeout)
if fail_wait:
raise TimeoutError('mock Root acknowledgement timeout')
kw['stdout'].write('P08 passive OS placement: 2 checks, 0 failures\n')
F.put(output/('os-pair/'+role+'-profile.json'),{'pid':pid,'user_data_dir':str(Path(self.temp.name)/name),'display':'mock'})
if role == 'writer': shutil.copytree(output/'pair/initial-formal',output/'os-pair/formal')
return 0
process.wait = wait; captured.append(process)
return process
def cleanup(children):
cleaned.extend(p.pid for p in children)
return ([{'pid':p.pid,'engine_ended':True} for p in children], [])
r = SimpleNamespace(require=C.runner.require, inventory=C.runner.inventory,
sha=C.sha, read=C.load, write=C.runner.write, copy_source=shutil.copyfile,
source_refs=lambda root:refs, default_player=lambda root:protected,
profile_path=lambda name:Path(self.temp.name)/name,
private_project=C.runner.private_project, fresh_environment=C.runner.fresh_environment,
SUPPORT=ROOT/'tools/regression', module=lambda *a:SimpleNamespace(ordinary_gate=lambda *a:{'mock':True}))
outer = SimpleNamespace(native=SimpleNamespace(helper={'live_engines':lambda:[]}),finish_captured=cleanup)
with patch.object(D.subprocess,'Popen',side_effect=spawn), contextlib.redirect_stdout(io.StringIO()):
if fail_wait:
with self.assertRaisesRegex(AssertionError,'native OS collection failed'):
D.collect_os(r,self.root,output,stage,str(Path(self.temp.name)/'synthetic-engine-not-executable'),'1'*40,refs,protected,outer)
else:
phases = D.collect_os(r,self.root,output,stage,str(Path(self.temp.name)/'synthetic-engine-not-executable'),'1'*40,refs,protected,outer)
self.assertEqual(2,len(phases))
self.assertEqual([p.pid for p in captured],cleaned)
self.assertEqual(public,(stage/'project.godot').read_bytes())
self.assertTrue((output/'os-runs/writer/ended.json').exists())
def test_mock_native_controller_two_private_pids(self):
self.collector_mock()
def test_mock_native_timeout_restores_project_and_cleans_owned_child(self):
self.collector_mock(fail_wait=True)
if __name__ == '__main__':
unittest.main()