P08-03: 配置の通常操作受入
This commit is contained in:
@@ -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
|
||||
+5323
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
+8
@@ -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)
|
||||
+1
@@ -0,0 +1 @@
|
||||
uid://c08passiver03
|
||||
+92
-1
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
@@ -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})
|
||||
@@ -0,0 +1 @@
|
||||
uid://b08passivep03
|
||||
Vendored
+313
@@ -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
@@ -0,0 +1 @@
|
||||
uid://dc83placement
|
||||
@@ -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())
|
||||
@@ -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": {
|
||||
|
||||
@@ -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())
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user