From f2a1249440d6ce8ccfbbe7e451ab9f7b1332cd35 Mon Sep 17 00:00:00 2001 From: Agent Control Runner Date: Wed, 21 Oct 2026 01:51:44 +0900 Subject: [PATCH] =?UTF-8?q?P28-01-RESTART:=20=E7=8F=BEHEAD=E3=81=AE?= =?UTF-8?q?=E6=80=A7=E8=83=BD=E5=9F=BA=E6=BA=96=EF=BC=9A=E9=80=94=E4=B8=AD?= =?UTF-8?q?=E4=BF=9D=E5=AD=98=E3=83=BB=E5=88=A5PID=E5=86=8D=E9=96=8B?= =?UTF-8?q?=E3=81=AE=E5=86=8D=E7=8F=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../p28-01/restart/probe/check_restart.py | 473 ++++++++++++++++++ .../p28-01/restart/probe/restart_pair.py | 310 ++++++++++++ .../p28-01/restart/probe/restart_probe.gd | 448 +++++++++++++++++ .../p28-01/restart/probe/test_restart.py | 466 +++++++++++++++++ 4 files changed, 1697 insertions(+) create mode 100644 docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/check_restart.py create mode 100644 docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/restart_pair.py create mode 100644 docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/restart_probe.gd create mode 100644 docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/test_restart.py diff --git a/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/check_restart.py b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/check_restart.py new file mode 100644 index 0000000..04e8371 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/check_restart.py @@ -0,0 +1,473 @@ +#!/usr/bin/env python3 +"""RESTART evidence validation; typed state comparisons execute in Godot.""" +import ast +import copy +import hashlib +import importlib.util +import json +from pathlib import Path +import re +import sys +import sysconfig + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parents[6] +BASE = "docs/plans/original-completion-2026-10-02/execution/p28-01/" +DEPENDENCIES = { + "tools/run_all_tests.py": "320ca668732b4ff72fc4449998af2327d4e5c527b06896740806aaf0487282b4", + "tools/regression/owned_outer.py": "c2e3483b76db1a3ff940f23f88ea0083fc78736e874ebcdb385d766dde8866e7", + "tools/regression/owned_diagnostic_process.py": "e31e93c6e2c5ea46a7d954ef8bc0b4caec1c08b86b8aef19e93b52ef24993087", + "tools/regression/p09_owned_engine_process.py": "c007e712c281573254faa1233c2ac92e100ab87393256a6dc096b35283e43037", + BASE + "collector/collect_p28_01.py": "a43e1f963ef8505ddd5acf8bf747d9bfac22a028cbb805c0b964ee11b9b40a14", + BASE + "collector/check_p28_01_report.py": "6c0851d498ca80bdc120fad047b072255f48713f63114521f3d4dfc902b2512b", +} +LOCAL = {BASE + "restart/probe/" + n for n in + ("restart_pair.py", "check_restart.py", "test_restart.py", "restart_probe.gd")} +PHASES = ["import", "parse-base", "parse-wrapper", "R-writer", "R-reader", "O-writer", "O-reader"] +CROSS = [ + {"condition": "原本条件とUI操作が一致", "applicable": False, "reference": "collector C1/C2", + "reason": "既存collectorのUI経路受入を参照。このtaskは再開の比較を担当する。"}, + {"condition": "Runtimeの実利用・会計・行動へ接続", "applicable": False, "reference": "collector C2/C3", + "reason": "既存collectorの実Runtime観測を参照。製品の接続は変更しない。"}, + {"condition": "途中保存・再起動で再現", "applicable": True, "reference": "RS1/RS2/RS3/RS4/RS5", + "reason": "別PIDのR/O writerとreaderで適用する。"}, + {"condition": "異常/境界/中断を検証", "applicable": False, "reference": "collector C5/C6/C7", + "reason": "横断受入は既存collectorを参照。再開固有の異常と境界はRS5で検証する。"}, + {"condition": "関連回帰PASS", "applicable": False, "reference": "collector C8", + "reason": "既存collectorの正式回帰証拠を参照。workerは全回帰を実行しない。"}, + {"condition": "要件別ログとソースハッシュを保存", "applicable": False, "reference": "collector C6/C7/C9", + "reason": "既存collectorの証拠契約を参照。再開のraw結合はRS7で保存する。"}, +] + + +def require(condition, message): + if not condition: + raise ValueError(message) + + +def sha(path): + return hashlib.sha256(Path(path).read_bytes()).hexdigest() + + +def pin(path): + path = Path(path) + require(path.is_file() and not path.is_symlink(), "missing/symlink evidence") + return {"sha256": sha(path), "bytes": path.stat().st_size} + + +def check_dependencies(root=ROOT): + for name, expected in DEPENDENCIES.items(): + require(sha(root / name) == expected, "dependency SHA: " + name) + + +def check_imports(text, external=None): + tree = ast.parse(text) + if external is not None: + require(external in DEPENDENCIES, "external AST closure") + # The explicitly forbidden run_all_tests.main imports are not executed. + tree.body = [node for node in tree.body if not ( + isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == "main")] + allowed = {Path(p).stem for p in set(DEPENDENCIES) | LOCAL} + for node in ast.walk(tree): + if isinstance(node, (ast.Import, ast.ImportFrom)): + names = [node.module] if isinstance(node, ast.ImportFrom) else [a.name for a in node.names] + for name in names: + require(name and (standard_library(name.split(".")[0]) or name in allowed), + "import outside closure") + if isinstance(node, ast.Call): + name = node.func.id if isinstance(node.func, ast.Name) else getattr(node.func, "attr", "") + require(name not in ("__import__", "eval", "import_module"), "dynamic code forbidden") + require(name != "exec" or external == "tools/regression/owned_diagnostic_process.py", + "unapproved exec") + if name == "spec_from_file_location": + for sub in ast.walk(node): + if isinstance(sub, ast.Constant) and isinstance(sub.value, str): + for path in re.findall(r"[\w./-]+\.py", sub.value): + require(path in DEPENDENCIES or path in LOCAL or + path in {Path(p).name for p in DEPENDENCIES}, "loader outside closure") + if external and isinstance(node, ast.BinOp): + for sub in ast.walk(node): + if isinstance(sub, ast.Constant) and isinstance(sub.value, str) and sub.value.endswith(".py"): + require(sub.value in DEPENDENCIES or sub.value in {Path(p).name for p in DEPENDENCIES}, + "external loader path outside closure") + + +def standard_library(name): + # Python 3.9 on the native host has no sys.stdlib_module_names. + spec = importlib.util.find_spec(name) + if spec is None: + return False + if spec.origin in ("built-in", "frozen"): + return True + if not spec.origin: + return False + path = Path(spec.origin).resolve() + return path.is_relative_to(Path(sysconfig.get_path("stdlib")).resolve()) and not any( + part in ("site-packages", "dist-packages") for part in path.parts) + + +def check_modules(modules=None, root=ROOT): + for module in (sys.modules if modules is None else modules).values(): + name = getattr(module, "__file__", None) + if name: + path = Path(name).resolve() + if path.is_relative_to(root): + require(path.relative_to(root).as_posix() in set(DEPENDENCIES) | LOCAL, + "loaded project module outside closure") + + +def check_probe(text): + function = None + blocks = [] + pauses = 0 + for raw in text.splitlines(): + line = raw.split("#", 1)[0] + match = re.match(r"^func (\w+)\(", line) + if match: + function = match[1] + if function == "deterministic_step": + blocks.append([]) + if function == "deterministic_step": + blocks[-1].append(line) + require(not re.search(r"set_paused\s*\(\s*false|\bclear\s*\(|spawn_guest|SimulationRuntime\.new|\binitialize\s*\(|enable_production_|_toggle_pause", line), + "forbidden probe API") + if re.search(r"\badvance_", line): + require(function == "deterministic_step" and line.strip() == "GameClock.advance_minutes(1)", + "advance outside deterministic_step") + if "set_paused(" in line: + require(function == "on_tick" and line.strip() == "GameClock.set_paused(true)", "pause outside tick") + pauses += 1 + require(not re.search(r"Time\.get_ticks_usec|\b\w+_us\b|Performance\.|get_performance_report", line), + "performance instrumentation") + require(len(blocks) == 1 and pauses == 1, "deterministic/tick functions required") + block = "\n".join(blocks[0]) + require(block.count("GameClock.advance_minutes(1)") == 1 and + "io_ready" in block and "GameClock.paused" in block, "step admission") + + +def event_id(counter): + require(type(counter) is int and counter >= 0, "counter type") + return "domain_event.%012d" % (counter + 1) + + +def transform(witness, event): + """Consistency oracle for recorded IO paths, not a Godot state decoder.""" + result = copy.deepcopy(witness) + eid = event_id(witness["counter"]) + ids, recent = witness["published_ids"], witness["recent_events"] + require(event["event_id"] == eid, "IO event ID") + require(ids == sorted(set(ids)) and eid not in ids and len(recent) <= 256, "event history") + require(len({x["event_id"] for x in recent}) == len(recent) and + all(x["event_id"] in ids for x in recent), "recent IDs") + result["counter"] += 1 + result["published_ids"] = sorted(ids + [eid]) + result["recent_events"] = (recent[1:] if len(recent) == 256 else recent) + [event] + return result + + +def exact(row): + require(row["method"] == "var_to_bytes" and row["equal"] is True and row["diff_paths"] == [], + "typed bytes differ") + require(row["excluded_paths"] == [], "state path exclusion") + require(re.fullmatch("[0-9a-f]{64}", row["expected_sha256"]) and + row["actual_sha256"] == row["expected_sha256"], "typed comparison contradiction") + + +def check_io(row, kind): + require(row["kind"] == kind and row["signal_count"] == 1, "one IO signal required") + signal, latest, ev = row["signal_payload"], row["latest"], row["event"] + require(signal == latest == ev["payload"], "signal/latest/event mismatch") + require(row["signal_projection"] == "_save_event_payload" and row["payload_bytes_equal"] is True, + "typed payload comparison") + success = kind != "tamper" + require(type(signal["ok"]) is bool and signal["ok"] is success and + ((signal["code"] == "OK") is success), "IO result") + require(signal["operation"] == ("save" if kind == "save" else "load"), "operation") + require(signal["save_path"].startswith("user://p28_restart/") and ".." not in signal["save_path"], + "private IO path") + require(row["return_kind"] == ("bool" if kind == "save" else "void"), "return kind") + require(row["return_value"] is signal["ok"] if kind == "save" else row["return_value"] is None, + "return/signal mismatch") + expected_type = {"save": "SAVE_CHECKPOINT_SUCCEEDED", "load": "SAVE_LOADED", + "tamper": "SAVE_LOAD_FAILED"}[kind] + require(ev["event_type"] == expected_type and ev["source_service"] == "save_service" and + ev["entity_ids"] == [] and ev["causation_command_id"] == 0, "event shape") + require(ev["absolute_minute"] == row["minute_after"] == row["minute_before"], "IO minute") + require(row["after"] == transform(row["before"], ev), "not X(P,event)") + require(row["dropped"] == (row["before"]["recent_events"][0] + if len(row["before"]["recent_events"]) == 256 else None), "wrong dropped head") + exact(row["comparison"]) + if success: + require(signal["saved_absolute_minute"] == row["minute_after"] and bool(signal["save_id"]), + "save identity") + require(row["paused_after"] is True, "IO clock not paused") + else: + failure = row["failure"] + require(failure["preload_pause_restored"] is True and + failure["loaded_clock_paused"] is row["paused_before"] and + row["paused_after"] is row["paused_before"], "tamper pause") + if failure["recovery_report"] is not None: + require(failure["recovery_report"]["restore_committed"] is False, "tamper restore") + require(type(failure["migration_attempted"]) is bool, "migration observation") + + +def check_report(report): + check_no_metrics(report) + require(report["schema"] == "p28-restart-v1" and report["stop_reason"] == "complete", "incomplete probe") + require(report["role"] in PHASES[3:] and type(report["pid"]) is int and report["pid"] > 0, "role/PID") + require(report["main_added"] is True and report["k_target"] == 30 and report["actual_k"] == 30, "main/K") + require(report["comparison_scope"] == "complete_typed_var_to_bytes" and report["excluded_paths"] == [], + "partial digest/path exclusion") + require(report["transfer_before_guard_writes"] == 0 and report["probe_deadline_seconds"] == 1740, + "profile/deadline") + writer = report["role"].endswith("writer") + kinds = ["save"] if writer else ["tamper", "load"] + require(len(report["io"]) == len(kinds), "IO sequence") + for row, kind in zip(report["io"], kinds): + check_io(row, kind) + if writer: + exact(report["package_matches_before"]) + require(report["package_matches_before"]["actual_sha256"] == report["states"]["P"]["sha256"], + "P not package snapshot") + else: + require(report["failure_event_absent"] is True and report["transfer_verified"] is True, + "failed event survived/transfer not verified") + io = report["io"][-1] + require(report["event_id"] == io["event"]["event_id"] and report["saved_minute"] == io["minute_after"], + "continuation IO identity") + require(report["paused_idle"]["before"] == report["paused_idle"]["after"], "paused clock consumed backlog") + require(report["paused_idle"]["before"]["paused"] is True, "paused observation") + exact(report["paused_idle"]["comparison"]) + rows = report["minutes"] + require(len(rows) == 30, "actual K not 30") + previous, lost = report["saved_minute"], False + for row in rows: + require(row["before"] == previous and row["after"] == previous + 1, "K counts actual minute advances") + require(row["paused"] is True and row["backlog_before"] == row["backlog_after"], "step consumed backlog") + require(row["scope"] == "complete_typed_var_to_bytes" and row["excluded_paths"] == [], "partial state") + require(re.fullmatch("[0-9a-f]{64}", row["sha256"]), "minute SHA") + if row["replacement"] is not None: + require(not lost and row["replacement"] == io["event"] and row["replacement_count"] == 1, + "replacement must be unchanged in-window E") + else: + lost = True + require(row["replacement_count"] == 0, "replacement after E left window") + require(0 <= row["recent_count"] <= 256, "recent limit") + require(len(row["recent_ids"]) == len(set(row["recent_ids"])) == row["recent_count"] and + row["recent_ids"].count(report["event_id"]) == row["replacement_count"], + "replacement does not match actual window") + previous = row["after"] + require(report["final_minute"] == previous, "final minute") + for name, entry in report["states"].items(): + require(re.fullmatch("[0-9a-f]{64}", entry["sha256"]) and entry["bytes"] > 0 and + entry["path"] == "p28_restart/evidence/" + name + ".bin", "state binding") + required = {"P", "after_save", "final"} if writer else {"P", "B", "after_tamper", "after_load", "final"} + require(set(report["states"]) == required, "missing complete state files") + require(report["states"]["after_save" if writer else "after_load"]["sha256"] == + io["comparison"]["actual_sha256"], "IO state file differs") + if not writer: + require(report["states"]["after_tamper"]["sha256"] == report["io"][0]["comparison"]["actual_sha256"], + "tamper state file differs") + if report["role"].startswith("O-"): + boundary = report["boundary"] + require(boundary is not None and boundary["guest_auto_count"] > 0, "arrival not processed") + require(len(boundary["guest_auto_ids"]) == len(set(boundary["guest_auto_ids"])) == + boundary["guest_auto_count"], "arrival IDs") + require(all(x.startswith("guest-auto-") for x in boundary["guest_auto_ids"]), "arrival identity") + require(boundary["clock"] == report["saved_minute"] and boundary["processed_arrival_minutes"], + "arrival boundary not processed") + if writer: + require(0 < report["saved_minute"] - report["start_minute"] <= 120, "arrival cap") + ticks = report["ui_ticks"] + require(ticks and ticks[-1] == boundary and all(t["guest_auto_count"] == 0 for t in ticks[:-1]), + "not first arrival tick") + next_boundary = report["next_boundary"] + added = [m for m in next_boundary["after"] if m not in next_boundary["before"]] + require(next_boundary["before"] == boundary["processed_arrival_minutes"] and + added == [next_boundary["minute"]] and next_boundary["processed_once"] is True and + next_boundary["minute"] > boundary["clock"] and + next_boundary["minute"] <= report["final_minute"], "next arrival missing/duplicate") + minutes = next_boundary["after"] + require(minutes == sorted(set(minutes)), "duplicate processed arrivals") + require([r["minute"] for r in next_boundary["arrival_reports"]] == minutes, + "arrival report duplicates/omissions") + else: + require(report["boundary"] is None and report["next_boundary"] is None, "R Operations boundary") + return True + + +def check_no_metrics(value): + if isinstance(value, dict): + for key, child in value.items(): + # Mandatory verbatim domain/IO payloads contain product diagnostics. + # They are evidence, never performance measurements or verdicts. + if key in ("payload", "signal_payload", "latest"): + continue + require(not key.endswith(("_us", "_ms")) and key not in + ("performance", "statistics", "fps", "budget_verdict"), "performance field forbidden") + check_no_metrics(child) + elif isinstance(value, list): + for child in value: + check_no_metrics(child) + + +def check_pair(writer, reader): + check_report(writer) + check_report(reader) + require(writer["role"].replace("writer", "reader") == reader["role"] and writer["pid"] != reader["pid"], + "pair roles/PID") + require(writer["event_id"] == reader["event_id"] and writer["saved_minute"] == reader["saved_minute"], + "pair IO identity") + require(writer["states"]["P"] == reader["states"]["P"], "package P differs") + require(writer["io"][0]["before"] == reader["io"][1]["before"], "package history differs") + for left, right in zip(writer["minutes"], reader["minutes"]): + require(all(left[k] == right[k] for k in + ("before", "after", "sha256", "scope", "replacement_count", "recent_count")), + "cross-PID complete typed state mismatch") + require(writer["boundary"] == reader["boundary"] and writer["next_boundary"] == reader["next_boundary"], + "Operations boundary differs") + + +def bound_raw(folder, row): + folder = Path(folder) + require(set(row["raw"]) == {"stdout.log", "stderr.log", "engine-native.log"}, "all raw required") + for name, expected in {**row["raw"], **row["owned_files"]}.items(): + require(Path(name).name == name and pin(folder / name) == expected, "raw/owned SHA mismatch") + require(set(row["owned_files"]) == {"process.json", "started.json", "ended.json", + "adapter-owned-cleanup.json", "outer-owned-finally.json"}, "owned receipts missing") + require(json.loads((folder / "process.json").read_text()) == row["process"], "process receipt mismatch") + process, capture = row["process"], row["capture"] + require(row["passed"] is True and process["exit_code"] == 0 and process["helper_exit_code"] == 0 and + process["timed_out"] is False and process["error_lines"] == [] and process["lifecycle_error"] is None and + process["external_process_or_group_signals"] == 0 and process["owned_popen_ended"] is True and + process["old_helper_engine_ended"] is True and process["start_receipt_written"] is True and + process["end_receipt_written"] is True, "owned phase failed") + require(capture["passed"] is True and capture["all_captured_ended"] is True and + capture["outer_cleanup_errors"] == [] and capture["adapter_exception"] is None and + capture["foreign_process_or_group_signals"] == 0 and + capture["all_cleanup_before_outer_receipt"] is True and capture["outer_receipt_written"] is True and + capture["outer_receipt_error"] is None and len(capture["actual_captured_pids"]) == 1, "outer capture") + actual = json.loads((folder / "outer-owned-finally.json").read_text()) + require(actual == {k: v for k, v in capture.items() if k not in + ("passed", "outer_receipt_written", "outer_receipt_error")}, "outer raw differs") + pid = capture["actual_captured_pids"][0] + require(process["pid"] == pid and process["argv"] == row["argv"], "process PID/argv") + for filename, key in (("started.json", "actual_start_binding"), ("ended.json", "actual_end_binding")): + binding = json.loads((folder / filename).read_text()) + require(binding == process[key] and binding["pid"] == binding["actual_pid"] == pid and + binding["argv"] == binding["command"] == row["argv"] and binding["owner_pid"] > 0, + "START/END binding") + require(process["actual_start_binding"]["owner_pid"] == process["actual_end_binding"]["owner_pid"], + "START/END owners differ") + require(json.loads((folder / "adapter-owned-cleanup.json").read_text()) == process["adapter_owned_cleanup"] + and process["adapter_owned_cleanup"]["engine_ended"] is True and + process["adapter_owned_cleanup"]["errors"] == [], "adapter cleanup binding") + require(process["stdout_sha256"] == row["raw"]["stdout.log"]["sha256"] and + process["stderr_sha256"] == row["raw"]["stderr.log"]["sha256"], "process raw binding") + require(row["timeout_seconds"] == 1800 and row["diagnostics"] == [], "phase timeout/diagnostics") + raw = (folder / "stdout.log").read_text() + for name in ("stdout.log", "stderr.log", "engine-native.log"): + require(not re.search(r"SCRIPT ERROR|Parse Error|ERROR:", (folder / name).read_text()), + "raw engine error") + if row["phase"] not in PHASES[3:]: + require("P28_RESTART_REPORT=" not in raw, "parse/import report") + if row["phase"].startswith("parse"): + require("--check-only" in row["argv"], "parse not check-only") + return None + reports = [s.partition("=")[2] for s in raw.splitlines() if s.startswith("P28_RESTART_REPORT=")] + closes = [s.partition("=")[2] for s in raw.splitlines() if s.startswith("P28_RESTART_AUDIO_CLOSE=")] + require(len(reports) == len(closes) == 1, "report/audio close count") + report, close = json.loads(reports[0]), json.loads(closes[0]) + require(report == row["report"] and report["role"] == row["phase"] and + capture["actual_captured_pids"] == [report["pid"]], "raw report/PID binding") + require(close.get("coordinator") is True and close.get("passed") is True and + close.get("owned_engine_pid") == report["pid"] and close.get("all_owned_targets_released") is True and + close.get("deadline_reached") is False and close.get("clock_process_held_stopped") is True and + close.get("window_input_held_disabled") is True and close.get("restored_before_quit") is False and + close.get("shared_bus_deleted") is False and close.get("settings_mutated") is False, "audio close") + check_report(report) + return report + + +def check_receipt(receipt, out): + from restart_pair import COLLECTOR_PINS, check_transfer + out = Path(out) + require(receipt["passed"] is True and receipt["error"] is None and receipt["cleanup_errors"] == [], + "execution failed") + require(receipt["source_before"] == receipt["source_after"] and len(receipt["source_before"]) == 1407, + "source map mismatch") + require(receipt["default_player_before"] == receipt["default_player_after"], "ordinary player changed") + require(receipt["dependencies_before"] == receipt["dependencies_after"] == DEPENDENCIES, "dependency pins") + require(receipt["collector_before"] == receipt["collector_after"] == COLLECTOR_PINS, + "collector changed") + require(receipt["worker_before"] == receipt["worker_after"] and set(receipt["worker_before"]) == + {"restart_pair.py", "restart_probe.gd", "check_restart.py", "test_restart.py"}, "worker changed") + require(receipt["cross_cutting"] == CROSS, "cross-cutting record") + require(receipt["original_credit"] == 0 and receipt["windows_verified"] is False and + receipt["operations_200_guests_verified"] is False, "scope expanded") + rows = receipt["phases"] + require([r["phase"] for r in rows] == PHASES, "phase order/incomplete execution") + pids = [] + for row in rows: + report = bound_raw(out / row["phase"], row) + pids.extend(row["capture"]["actual_captured_pids"]) + require(row["stage_before"] == row["stage_after"] if row["phase"] != "import" + else row["stage_preserved"] is True, "stage changed") + require(row["source_preserved"] is True and row["player_preserved"] is True, "phase preservation") + startup = row["startup_project"] + require(startup["fresh_before"] is True and sha(startup["path"]) == startup["sha256"], "startup profile") + if report: + guard = row["guard"] + expected = {"verified": True, "conditions": [True] * 5, "conditions_checked": 5, + "actual_user_data_dir": row["runtime_profile"], + "expected_user_data_dir": row["runtime_profile"], + "globalized_probe": str(Path(row["runtime_profile"]) / "guard-probe"), + "custom_user_dir_name": row["runtime_name"], "fresh_before_parent_initialize": True, + "profile_writes_before_guard": 0, "project_settings_persisted": False} + require(all(type(guard.get(k)) is type(v) and guard[k] == v for k, v in expected.items()), + "guard mismatch") + guard_path = out / (row["phase"] + "-guard.json") + require(sha(guard_path) == row["guard_sha256"] and json.loads(guard_path.read_text()) == guard, + "guard raw binding") + for name, evidence in report["states"].items(): + require(pin(out / (row["phase"] + "-states") / (name + ".bin")) == + {k: evidence[k] for k in ("sha256", "bytes")}, "state bytes SHA") + require(len(set(pids)) == len(pids), "reused PID") + require({r["pid"] for r in receipt["cleanup"]} == set(pids) and all( + r["engine_ended"] is True and r["outer_cleanup_errors"] == [] and + r["foreign_process_or_group_signals"] == 0 for r in receipt["cleanup"]), "cleanup mismatch") + for start in (3, 5): + check_pair(rows[start]["report"], rows[start + 1]["report"]) + transfer = out / (rows[start]["phase"][0] + "-transfer") + manifest_path = transfer / "manifest.json" + manifest = json.loads(manifest_path.read_text()) + check_transfer(transfer, manifest) + require(rows[start + 1]["report"]["transfer_manifest_sha256"] == sha(manifest_path), + "reader manifest SHA") + for phase_index, kinds in ((start, ("good",)), (start + 1, ("good", "tampered"))): + profile = rows[phase_index]["runtime_profile"] + for kind in kinds: + for name, expected in manifest["files"][kind].items(): + relative = "p28_restart/" + kind + "/" + name + require(receipt["profiles"][profile].get(relative) == expected, + "private package differs from transfer") + for path, expected in receipt["script_pins"].items(): + require(sha(path) == expected, "executed script changed") + check_probe((out / "restart_probe.gd").read_text()) + require(sha(out / "restart_probe.gd") == receipt["worker_before"]["restart_probe.gd"], + "executed probe differs from submitted source") + require(receipt["owned_writes"] == [], "probe/driver wrote owned folder") + return True + + +def main(): + import argparse + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("execution", type=Path) + args = parser.parse_args() + check_receipt(json.loads(args.execution.read_text()), args.execution.parent) + print("P28_RESTART_CHECK=PASS") + + +if __name__ == "__main__": + main() diff --git a/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/restart_pair.py b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/restart_pair.py new file mode 100644 index 0000000..b565c03 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/restart_pair.py @@ -0,0 +1,310 @@ +#!/usr/bin/env python3 +"""Root-only four-PID RESTART driver. --check performs no writes or launches.""" +import argparse +import importlib.util +import json +import os +from pathlib import Path +import shutil +import subprocess +import sys +import uuid + +sys.dont_write_bytecode = True +import check_restart as check + +HERE, ROOT = check.HERE, check.ROOT +COLLECTOR = check.BASE + "collector/collect_p28_01.py" +COLLECTOR_PINS = { + "collect_p28_01.py": check.DEPENDENCIES[COLLECTOR], + "check_p28_01_report.py": check.DEPENDENCIES[check.BASE + "collector/check_p28_01_report.py"], + "measure_p28_01.gd": "4acef88bce4ac77f3f5d2659d766afcc5fd139cee381118839012022772946bb", + "test_collect_p28_01.py": "99d86ea723c976c95dbd283acbb85981eabf427b30994e652002ec0e76dc4ab0", +} + + +def load_dependencies(): + check.check_dependencies() + for name in check.DEPENDENCIES: + check.check_imports((ROOT / name).read_text(), external=name) + for name in ("restart_pair.py", "check_restart.py", "test_restart.py"): + check.check_imports((HERE / name).read_text()) + check.check_modules() + sys.path.insert(0, str((ROOT / COLLECTOR).parent)) + spec = importlib.util.spec_from_file_location("restart_collector", ROOT / COLLECTOR) + collector = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = collector + spec.loader.exec_module(collector) + standard = collector.load_tool(collector.EXTERNAL_IMPORTS[0]) + outer = collector.load_tool(collector.EXTERNAL_IMPORTS[1]) + check.check_modules() + check.check_modules({"collector": collector, "standard": standard, "outer": outer, "native": outer.native}) + check.require(Path(outer.native.helper["__file__"]).resolve() == + ROOT / "tools/regression/p09_owned_engine_process.py", "exec helper closure") + return collector, standard, outer + + +def collector_pins(): + actual = {name: check.sha((ROOT / COLLECTOR).parent / name) for name in COLLECTOR_PINS} + check.require(actual == COLLECTOR_PINS, "collector differs from Q") + return actual + + +def plan(collector, engine, out): + stage, wrapper = out / "stage", out / "guarded.gd" + return [collector.argv_for(engine, stage, wrapper, True), + collector.parse_argv(engine, stage, out / "restart_probe.gd"), + collector.parse_argv(engine, stage, wrapper)] + [ + collector.argv_for(engine, stage, wrapper) for _ in range(4)] + + +def schedule(rows): + for index, label in enumerate(check.PHASES): + check.require(all(row["passed"] is True for row in rows), "previous phase failed") + if label.endswith("reader"): + check.require(rows and rows[-1]["phase"] == label.replace("reader", "writer") and + rows[-1].get("report", {}).get("stop_reason") == "complete", + "reader requires successful writer") + yield index, label + check.require(len(rows) == index + 1 and rows[-1]["phase"] == label and rows[-1]["passed"] is True, + "phase did not complete") + + +def safe_inventory(directory): + directory = Path(directory) + check.require(directory.is_dir() and not directory.is_symlink(), "package missing/symlink") + result = {} + for path in sorted(directory.rglob("*")): + check.require(not path.is_symlink(), "package symlink") + if path.is_file(): + result[path.relative_to(directory).as_posix()] = check.pin(path) + check.require(result, "empty package") + return result + + +def create_transfer(source, destination, write): + """Copy only after writer exit; never pre-populate the reader profile.""" + before = safe_inventory(source) + check.require("world.bin" in before and not destination.exists(), "transfer admission") + destination.mkdir() + shutil.copytree(source, destination / "good") + check.require(safe_inventory(destination / "good") == before, "good copy differs") + shutil.copytree(destination / "good", destination / "tampered") + world = destination / "tampered/world.bin" + data = bytearray(world.read_bytes()) + check.require(data, "empty world") + offset = len(data) // 2 + data[offset] ^= 1 + world.write_bytes(data) + manifest = {"files": {"good": before, "tampered": safe_inventory(destination / "tampered")}, + "tamper": {"file": "world.bin", "offset": offset, "xor": 1, + "before_sha256": before["world.bin"]["sha256"], "after_sha256": check.sha(world)}} + write(destination / "manifest.json", manifest) + check_transfer(destination, manifest) + check.require(safe_inventory(source) == before, "writer package changed") + return manifest + + +def check_transfer(directory, manifest): + check.require(set(manifest["files"]) == {"good", "tampered"}, "transfer kinds") + for kind in ("good", "tampered"): + check.require(safe_inventory(directory / kind) == manifest["files"][kind], "transfer SHA mismatch") + good = (directory / "good/world.bin").read_bytes() + bad = (directory / "tampered/world.bin").read_bytes() + tamper = manifest["tamper"] + check.require(len(good) == len(bad) > 0 and tamper["offset"] == len(good) // 2 and tamper["xor"] == 1, + "tamper location") + expected = bytearray(good) + expected[len(good) // 2] ^= 1 + check.require(bytes(expected) == bad and tamper["before_sha256"] == check.sha(directory / "good/world.bin") + and tamper["after_sha256"] == check.sha(directory / "tampered/world.bin"), "tamper bytes") + for name, value in manifest["files"]["good"].items(): + if name != "world.bin": + check.require(manifest["files"]["tampered"].get(name) == value, "extra tamper difference") + check.require(set(manifest["files"]["good"]) == set(manifest["files"]["tampered"]), "tamper inventory") + + +def fresh_profile(path): + check.require(not path.exists(), "profile must be fresh; pre-guard writes forbidden") + + +def committed_source(source, commit): + """Read-only Git operations: the 1407 files must be the actual HEAD bytes.""" + tracked = subprocess.check_output(["git", "-C", str(ROOT), "ls-tree", "-r", commit], text=True) + selected = {} + for line in tracked.splitlines(): + metadata, name = line.split("\t", 1) + if name.split("/", 1)[0] in ("autoload", "scripts", "scenes", "data", "tests", "assets", "tools") or \ + name in ("project.godot", "export_presets.cfg", "Makefile", "AGENTS.md"): + if Path(name).name != ".DS_Store" and "__pycache__" not in Path(name).parts: + selected[name] = metadata.split()[2] + check.require(set(selected) == set(source), "source map differs from HEAD") + for name, expected in selected.items(): + actual = subprocess.check_output(["git", "-C", str(ROOT), "hash-object", str(ROOT / name)], text=True).strip() + check.require(actual == expected, "dirty product source: " + name) + + +def bind_report(out, label, row): + text = (out / label / "stdout.log").read_text() + reports = [s.partition("=")[2] for s in text.splitlines() if s.startswith("P28_RESTART_REPORT=")] + check.require(len(reports) == 1, "one raw report required") + row["report"] = json.loads(reports[0]) + check.bound_raw(out / label, row) + return row["report"] + + +def copy_states(out, row): + folder = out / (row["phase"] + "-states") + folder.mkdir() + profile = Path(row["runtime_profile"]) + for name, expected in row["report"]["states"].items(): + source = profile / expected["path"] + check.require(source.resolve().is_relative_to(profile.resolve()) and + check.pin(source) == {k: expected[k] for k in ("bytes", "sha256")}, "state binding") + shutil.copyfile(source, folder / (name + ".bin")) + check.require(check.pin(source) == check.pin(folder / (name + ".bin")), "state copy differs") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + mode = parser.add_mutually_exclusive_group(required=True) + mode.add_argument("--check", action="store_true") + mode.add_argument("--run", action="store_true") + for name in ("readonly", "fixture", "engine", "output"): + parser.add_argument("--" + name, type=Path, required=True) + args = parser.parse_args() + collector, standard, outer = load_dependencies() + pinned = collector_pins() + readonly, fixture, engine, out = (p.resolve() for p in (args.readonly, args.fixture, args.engine, args.output)) + inputs = collector.inputs(readonly) + check.require(check.sha(fixture) == collector.FIXTURE_SHA and check.sha(engine) == collector.ENGINE_SHA, + "fixture/engine SHA") + check.check_probe((HERE / "restart_probe.gd").read_text()) + source = collector.source_map(standard, ROOT) + check.require(len(source) == 1407, "source count") + commit = subprocess.check_output(["git", "-C", str(ROOT), "rev-parse", "HEAD"], text=True).strip() + committed_source(source, commit) + worker_pins = {name: check.sha(HERE / name) for name in + ("restart_pair.py", "restart_probe.gd", "check_restart.py", "test_restart.py")} + commands = plan(collector, engine, out) + admission = {"commit": commit, "source_map": source, "readonly": inputs, "planned_argv": commands, + "engine_sha256": check.sha(engine), "fixture_sha256": check.sha(fixture), + "dependencies": check.DEPENDENCIES, "collector_Q": pinned, + "worker_pins": worker_pins, + "phases": check.PHASES, "engines_started": 0} + check.require(not out.exists(), "new output required") + if args.check: + print(json.dumps(admission, ensure_ascii=False, indent=2)) + return 0 + check.require(sys.platform == "darwin", "reviewed native platform is macOS") + collector.no_live_engine() + check.require(shutil.disk_usage(out.parent).free >= 1200000000, "disk space") + player = standard.default_player(ROOT) + out.mkdir(parents=True) + collector.write(out / "admission.json", admission) + rows, profiles = [], [] + error = None + scripts = {} + receipt = {} + try: + stage = out / "stage" + stage.mkdir() + for name in source: + target = stage / name + target.parent.mkdir(parents=True, exist_ok=True) + standard.copy_source(ROOT / name, target) + expected = standard.inventory(stage, stage=True) + check.require(expected == source, "stage source differs") + probe, guard, wrapper = (out / n for n in ("restart_probe.gd", "profile_guard.gd", "guarded.gd")) + probe.write_bytes((HERE / probe.name).read_bytes()) + guard.write_bytes((readonly / "run-r02/profile_guard.gd").read_bytes()) + wrapper.write_text("extends " + json.dumps(str(probe)) + "\nconst Guard = preload(" + + json.dumps(str(guard)) + ")\nfunc _initialize() -> void:\n" + "\tif not Guard.activate():\n\t\tquit(97)\n\t\treturn\n\tsuper._initialize()\n") + scripts = {str(p): check.sha(p) for p in (probe, guard, wrapper)} + public = (stage / "project.godot").read_bytes() + for index, label in schedule(rows): + check.check_dependencies() + collector_pins() + check.check_modules() + check.require(collector.source_map(standard, ROOT) == source and standard.default_player(ROOT) == player, + "source/player changed before phase") + check.require(standard.inventory(stage, stage=True) == expected and + all(check.sha(p) == value for p, value in scripts.items()) and + all(check.sha(HERE / p) == value for p, value in worker_pins.items()), + "stage/scripts changed") + startup_name = "TheTree-PCK-tests-" + uuid.uuid4().hex + "-restart-startup" + runtime_name = "TheTree-PCK-tests-" + uuid.uuid4().hex + "-restart-runtime" + startup, runtime = (standard.profile_path(n) for n in (startup_name, runtime_name)) + fresh_profile(startup) + fresh_profile(runtime) + profiles.extend([startup, runtime]) + transfer = out / (label[0] + "-transfer") + env = {"THE_TREE_PACK_PROFILE_NAME": runtime_name, "THE_TREE_PACK_PROFILE_EXPECTED": str(runtime), + "THE_TREE_PACK_PROFILE_REPORT": str(out / (label + "-guard.json")), + "THE_TREE_RESTART_ROLE": label, "THE_TREE_RESTART_K": "30", + "THE_TREE_RESTART_TRANSFER": str(transfer), + "THE_TREE_RESTART_TRANSFER_MANIFEST": str(transfer / "manifest.json"), + "THE_TREE_P28_FIXTURE": str(fixture), "THE_TREE_P28_FIXTURE_SHA": collector.FIXTURE_SHA} + if label.endswith("reader"): + check_transfer(transfer, json.loads((transfer / "manifest.json").read_text())) + before_stage = collector.digest(expected) + row = collector.run_private_engine(standard, outer, commands[index], out / label, env, + stage, public, startup_name) + row["phase"] = label + rows.append(row) + check.require(row["passed"], "owned engine failure: " + label) + after_stage = standard.inventory(stage, stage=True) + collector.stage_gate(expected, after_stage, importing=label == "import") + row.update(stage_before=before_stage, stage_after=collector.digest(after_stage), stage_preserved=True, + source_preserved=collector.source_map(standard, ROOT) == source, + player_preserved=standard.default_player(ROOT) == player) + expected = after_stage + if label in check.PHASES[3:]: + guard_path = out / (label + "-guard.json") + observation = json.loads(guard_path.read_text()) + collector.guard_gate(observation, runtime, runtime_name) + row.update(runtime_profile=str(runtime), runtime_name=runtime_name, + guard=observation, guard_sha256=check.sha(guard_path)) + report = bind_report(out, label, row) + copy_states(out, row) + collector.write(out / (label + "-report.json"), report) + if label.endswith("writer"): + create_transfer(runtime / "p28_restart/good", transfer, collector.write) + else: + check.require(report["transfer_manifest_sha256"] == check.sha(transfer / "manifest.json"), + "reader transfer manifest binding") + check.check_pair(rows[-2]["report"], report) + else: + check.bound_raw(out / label, row) + check.require(row["source_preserved"] and row["player_preserved"] and + check.sha(fixture) == collector.FIXTURE_SHA and + all(check.sha(p) == value for p, value in scripts.items()), "post-phase preservation") + except BaseException as caught: + error = repr(caught) + finally: + cleanup, cleanup_errors = outer.finish_captured(outer.OWNED) + receipt = {"admission": admission, "phases": rows, "error": error, "passed": error is None, + "source_before": source, "source_after": collector.source_map(standard, ROOT), + "default_player_before": player, "default_player_after": standard.default_player(ROOT), + "dependencies_before": check.DEPENDENCIES, + "dependencies_after": {p: check.sha(ROOT / p) for p in check.DEPENDENCIES}, + "collector_before": pinned, + "collector_after": {p: check.sha((ROOT / COLLECTOR).parent / p) for p in pinned}, + "script_pins": scripts, "cleanup": cleanup, "cleanup_errors": cleanup_errors, + "worker_before": worker_pins, + "worker_after": {p: check.sha(HERE / p) for p in worker_pins}, + "profiles": {str(p): collector.profile_files(p) for p in profiles}, "owned_writes": [], + "cross_cutting": check.CROSS, "original_credit": 0, + "windows_verified": False, "operations_200_guests_verified": False} + if receipt["passed"]: + try: + check.check_receipt(receipt, out) + except (ValueError, KeyError, TypeError, OSError) as caught: + receipt.update(passed=False, error="checker: " + repr(caught)) + collector.write(out / "execution.json", receipt) + return 0 if receipt["passed"] else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/restart_probe.gd b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/restart_probe.gd new file mode 100644 index 0000000..1d62300 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/restart_probe.gd @@ -0,0 +1,448 @@ +extends SceneTree +## Root-owned execution only. All files are written after Guard.activate(). +var GameClock: Variant +var runtime: Variant +var main: Variant +var report: Dictionary = {} +var signals: Array = [] +var key_forms: Dictionary = {} +var role := "" +var start_minute := 0 +var selected_tick := false +var overloaded := false +var io_ready := false +var finished := false +var E := "" +var package: Dictionary = {} +var failure_event: Dictionary = {} +var boundary: Variant = null + +func field(d: Variant, name: String) -> Variant: + if d is Dictionary: + if d.has(StringName(name)): + key_forms[name] = "StringName" + return d[StringName(name)] + if d.has(String(name)): + key_forms[name] = "String" + return d[String(name)] + key_forms[name] = "missing_product_field" + return null + +func _initialize() -> void: + call_deferred("begin") + +func bytes_hash(data: PackedByteArray) -> String: + var context := HashingContext.new() + context.start(HashingContext.HASH_SHA256) + context.update(data) + return context.finish().hex_encode() + +func runtime_part(snapshot: Dictionary) -> Dictionary: + return field(field(snapshot, "systems"), "runtime") + +func witness(snapshot: Dictionary) -> Dictionary: + var part := runtime_part(snapshot) + var events: Dictionary = field(part, "domain_events") + return {"counter": field(field(field(part, "id_generator"), "counters"), "domain_event"), + "published_ids": field(events, "published_ids").duplicate(true), + "recent_events": field(events, "recent_events").duplicate(true)} + +func different_paths(a: Variant, b: Variant, path := "$", depth := 0) -> Array: + var result: Array = [] + if var_to_bytes(a) == var_to_bytes(b): + return result + if depth >= 4 or typeof(a) != typeof(b): + return [path] + if a is Dictionary: + for key: Variant in a: + if not b.has(key): + result.append(path + "." + str(key)) + else: + result.append_array(different_paths(a[key], b[key], path + "." + str(key), depth + 1)) + for key: Variant in b: + if not a.has(key): + result.append(path + "." + str(key)) + elif a is Array and a.size() == b.size(): + for index in range(a.size()): + result.append_array(different_paths(a[index], b[index], path + "[%d]" % index, depth + 1)) + else: + result.append(path) + # Typed container/key/order differences can exist without value differences. + if result.is_empty(): + result.append(path) + return result + +func compare(expected: Dictionary, actual: Dictionary) -> Dictionary: + var a := var_to_bytes(expected) + var b := var_to_bytes(actual) + return {"method": "var_to_bytes", "equal": a == b, "expected_sha256": bytes_hash(a), + "actual_sha256": bytes_hash(b), "excluded_paths": [], + "diff_paths": different_paths(expected, actual)} + +func record_state(name: String, value: Dictionary) -> bool: + var path := "user://p28_restart/evidence/" + name + ".bin" + if DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(path).get_base_dir()) != OK: + return false + var data := var_to_bytes(value) + var file := FileAccess.open(path, FileAccess.WRITE) + if file == null: + return false + file.store_buffer(data) + file.close() + report.states[name] = {"path": path.trim_prefix("user://"), "bytes": data.size(), + "sha256": bytes_hash(data)} + return FileAccess.get_sha256(path) == report.states[name].sha256 + +func on_save(result: Dictionary) -> void: + signals.append(result.duplicate(true)) + +func auto_ids() -> Array: + var ids: Array = [] + for guest: Variant in runtime.guest_service.get_guests(): + if str(guest.guest_id).begins_with("guest-auto-"): + ids.append(str(guest.guest_id)) + ids.sort() + return ids + +func operations_observation() -> Dictionary: + var ids := auto_ids() + var processed: Array = [] + var arrivals: Array = [] + if runtime.operations_coordinator != null: + var state: Dictionary = runtime.operations_coordinator.create_snapshot() + processed = field(state, "processed_arrival_minutes").duplicate() + arrivals = runtime.operations_coordinator.get_arrival_reports().duplicate(true) + return {"clock": GameClock.absolute_minute, "guest_auto_ids": ids, "guest_auto_count": ids.size(), + "operations": runtime.get_operations_report(), "processed_arrival_minutes": processed, + "arrival_reports": arrivals} + +func on_tick(minute: int) -> void: + if io_ready or finished or selected_tick: + return + var observed := operations_observation() + if role == "O-writer": + report.ui_ticks.append(observed) + if minute == 390: + report["at_390"] = observed.duplicate(true) + report.at_390["domain_events"] = field(runtime_part(runtime.create_snapshot()), "domain_events") + var stop: bool = (role == "R-writer" and minute - start_minute >= 10) or \ + (role == "O-writer" and (observed.guest_auto_count > 0 or minute - start_minute >= 120)) + if stop: + GameClock.set_paused(true) + selected_tick = true + +func on_overload(backlog: int) -> void: + overloaded = true + report.overloads.append({"minute": GameClock.absolute_minute, "backlog": backlog}) + +func clock_observation() -> Dictionary: + return {"minute": GameClock.absolute_minute, "paused": GameClock.paused, + "backlog": GameClock.get_backlog_minutes(), + "accumulator_hex": PackedFloat64Array([GameClock.get("_accumulator")]).to_byte_array().hex_encode()} + +func begin() -> void: + GameClock = root.get_node("GameClock") + runtime = root.get_node("SimulationRuntime") + role = OS.get_environment("THE_TREE_RESTART_ROLE") + report = {"schema": "p28-restart-v1", "role": role, "pid": OS.get_process_id(), + "main_added": false, "k_target": 30, "actual_k": 0, "io": [], "states": {}, + "minutes": [], "ui_ticks": [], "overloads": [], "boundary": null, "next_boundary": null, + "comparison_scope": "complete_typed_var_to_bytes", "excluded_paths": [], + "transfer_before_guard_writes": 0, "transfer_verified": false, + "probe_deadline_seconds": 1740} + create_timer(1740.0).timeout.connect(func() -> void: finish("probe_deadline")) + if OS.get_environment("THE_TREE_RESTART_K") != "30" or role not in ["R-writer", "R-reader", "O-writer", "O-reader"]: + finish("invalid_environment") + return + # No await before main: the default autoload clock cannot process here. + if role == "R-writer": + var fixture := OS.get_environment("THE_TREE_P28_FIXTURE") + if FileAccess.get_sha256(fixture) != OS.get_environment("THE_TREE_P28_FIXTURE_SHA"): + finish("fixture_sha") + return + var file := FileAccess.open(fixture, FileAccess.READ) + if file == null: + finish("fixture_missing") + return + var value: Variant = file.get_var(false) + file.close() + if not value is Dictionary or not runtime.restore_snapshot(value): + finish("fixture_restore") + return + main = load("res://scenes/main/main.tscn").instantiate() + root.add_child(main) + report.main_added = true + report["startup"] = {"start_report": main.get("_operations_start_report").duplicate(true), + "operations": runtime.get_operations_report(), "clock": clock_observation()} + if role != "R-writer" and not GameClock.paused: + finish("main_did_not_pause") + return + var start_result: Dictionary = report.startup.start_report + var operations_enabled: Variant = field(report.startup.operations, "enabled") + if role == "R-writer": + if operations_enabled != false or field(start_result, "code") != "INVALID_START_TIME": + finish("unexpected_R_start") + return + elif operations_enabled != true or field(start_result, "ok") != true or GameClock.absolute_minute != 360: + finish("unexpected_fresh_start") + return + runtime.save_operation_completed.connect(on_save) + GameClock.tick_completed.connect(on_tick) + GameClock.simulation_overloaded.connect(on_overload) + start_minute = GameClock.absolute_minute + report["start_minute"] = start_minute + if role.ends_with("writer"): + main.call("_set_speed", 1) + while not GameClock.paused and not finished: + await process_frame + if finished: + return + if not selected_tick and not overloaded: + finish("unexpected_ui_stop") + return + if role == "O-writer" and auto_ids().is_empty(): + report["boundary"] = null + report["boundary_reason"] = "no_arrival_within_cap" if GameClock.absolute_minute - start_minute >= 120 else "stopped_before_arrival" + finish(report.boundary_reason) + return + if not writer_save(): + finish("save_comparison_failed") + return + else: + if not import_transfer(): + finish("transfer_failed") + return + if not reader_load(): + finish("load_comparison_failed") + return + report["event_id"] = E + report["saved_minute"] = GameClock.absolute_minute + boundary = operations_observation() if role.begins_with("O-") else null + report["boundary"] = boundary + var idle_before: Dictionary = runtime.create_snapshot() + var clock_before := clock_observation() + await process_frame + await process_frame + if finished: + return + var idle_after: Dictionary = runtime.create_snapshot() + report["paused_idle"] = {"before": clock_before, "after": clock_observation(), + "comparison": compare(idle_before, idle_after)} + if not report.paused_idle.comparison.equal or report.paused_idle.before != report.paused_idle.after: + finish("paused_backlog_consumed") + return + io_ready = true + for _index in range(30): + if not deterministic_step(): + finish("no_tick_advance_or_invalid_state") + return + # Keep the event loop responsive to deadline/audio while remaining paused. + await process_frame + if finished: + return + if not record_state("final", runtime.create_snapshot()): + finish("state_write_failed") + return + report["final_minute"] = GameClock.absolute_minute + if role.begins_with("O-"): + var last := operations_observation() + var added: Array = [] + for minute: Variant in last.processed_arrival_minutes: + if minute not in boundary.processed_arrival_minutes: + added.append(minute) + report["next_boundary"] = {"minute": added[0] if added.size() == 1 else null, + "processed_once": added.size() == 1, "before": boundary.processed_arrival_minutes, + "after": last.processed_arrival_minutes, "arrival_reports": last.arrival_reports, + "guest_auto_ids": last.guest_auto_ids} + if added.size() != 1: + finish("next_arrival_boundary_missing_or_duplicate") + return + finish("complete") + +func io_proof(base: Dictionary, after: Dictionary, kind: String, returned: Variant) -> bool: + var row := {"kind": kind, "signal_count": signals.size(), + "return_kind": "bool" if kind == "save" else "void", "return_value": returned} + report.io.append(row) + if signals.size() != 1: + return false + var signal_result: Dictionary = signals[0] + var payload: Dictionary = runtime.call("_save_event_payload", signal_result) + var latest: Dictionary = runtime.latest_save_operation_result.duplicate(true) + row["signal_projection"] = "_save_event_payload" + row["signal_payload"] = payload + row["latest"] = latest + row["payload_bytes_equal"] = var_to_bytes(payload) == var_to_bytes(latest) + var before := witness(base) + var after_witness := witness(after) + var eid := "domain_event.%012d" % (int(before.counter) + 1) + var found: Array = [] + for event: Dictionary in after_witness.recent_events: + if field(event, "event_id") == eid: + found.append(event) + if found.size() != 1 or before.published_ids.has(eid): + return false + var event: Dictionary = found[0] + row["event"] = event + row["before"] = before + row["after"] = after_witness + row["minute_before"] = field(field(base, "clock"), "absolute_minute") + row["minute_after"] = field(field(after, "clock"), "absolute_minute") + row["paused_before"] = field(field(base, "clock"), "paused") + row["paused_after"] = field(field(after, "clock"), "paused") + row["dropped"] = before.recent_events[0] if before.recent_events.size() == 256 else null + var expected := base.duplicate(true) + var part := runtime_part(expected) + var counters: Dictionary = field(field(part, "id_generator"), "counters") + counters["domain_event"] = int(before.counter) + 1 + var events: Dictionary = field(part, "domain_events") + var ids: Array = field(events, "published_ids") + ids.append(eid) + ids.sort() + var recent: Array = field(events, "recent_events") + if recent.size() > 256: + return false + if recent.size() == 256: + recent.pop_front() + recent.append(event.duplicate(true)) + row["comparison"] = compare(expected, after) + var expected_type := {"save": "SAVE_CHECKPOINT_SUCCEEDED", "load": "SAVE_LOADED", "tamper": "SAVE_LOAD_FAILED"} + var ok: bool = row.payload_bytes_equal and row.comparison.equal and \ + var_to_bytes(field(event, "payload")) == var_to_bytes(payload) and \ + field(event, "event_type") == expected_type[kind] and field(event, "source_service") == "save_service" and \ + field(event, "entity_ids").is_empty() and field(event, "causation_command_id") == 0 and \ + row.minute_before == row.minute_after and field(event, "absolute_minute") == row.minute_after + if kind == "tamper": + var recovery: Variant = field(signal_result, "recovery_report") + var migration: Variant = field(signal_result, "migration_attempted") + row["failure"] = {"preload_pause_restored": field(signal_result, "preload_pause_restored"), + "loaded_clock_paused": field(signal_result, "loaded_clock_paused"), "recovery_report": recovery, + "migration_attempted": migration == true} + failure_event = event.duplicate(true) + return ok and field(payload, "ok") == false and field(payload, "code") != "OK" and \ + row.failure.preload_pause_restored == true and row.failure.loaded_clock_paused == row.paused_before and \ + row.paused_before == row.paused_after and (recovery == null or field(recovery, "restore_committed") == false) + E = eid + return ok and field(payload, "ok") == true and field(payload, "code") == "OK" and \ + (returned == true if kind == "save" else returned == null) and row.paused_after == true and \ + field(payload, "saved_absolute_minute") == row.minute_after + +func writer_save() -> bool: + var before: Dictionary = runtime.create_snapshot() + signals = [] + var returned: bool = main.call("_save_manual_at_path", "user://p28_restart/good", "P28 RESTART") + var after: Dictionary = runtime.create_snapshot() + var loaded := SaveService.load_snapshot("user://p28_restart/good") + if field(loaded, "ok") != true: + return false + package = field(loaded, "snapshot") + report["package_matches_before"] = compare(before, package) + if not record_state("P", package) or not record_state("after_save", after): + return false + return report.package_matches_before.equal and io_proof(package, after, "save", returned) + +func import_transfer() -> bool: + var transfer := OS.get_environment("THE_TREE_RESTART_TRANSFER") + var manifest_path := OS.get_environment("THE_TREE_RESTART_TRANSFER_MANIFEST") + if not transfer.is_absolute_path() or not manifest_path.is_absolute_path(): + return false + var manifest: Variant = JSON.parse_string(FileAccess.get_file_as_string(manifest_path)) + if not manifest is Dictionary: + return false + for kind: String in ["good", "tampered"]: + var target := "user://p28_restart/" + kind + if DirAccess.dir_exists_absolute(target): + return false + for relative: String in manifest.files[kind]: + if relative.is_absolute_path() or ".." in relative.split("/") or "\\" in relative: + return false + var source := transfer.path_join(kind).path_join(relative) + var expected: Dictionary = manifest.files[kind][relative] + if FileAccess.get_sha256(source) != expected.sha256: + return false + var data := FileAccess.get_file_as_bytes(source) + if data.size() != int(expected.bytes): + return false + var destination := target.path_join(relative) + if DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(destination).get_base_dir()) != OK: + return false + var file := FileAccess.open(destination, FileAccess.WRITE) + if file == null: + return false + file.store_buffer(data) + file.close() + if FileAccess.get_sha256(destination) != expected.sha256: + return false + report["transfer_manifest_sha256"] = FileAccess.get_sha256(manifest_path) + report["transfer_verified"] = true + return true + +func reader_load() -> bool: + var loaded := SaveService.load_snapshot("user://p28_restart/good") + if field(loaded, "ok") != true: + return false + package = field(loaded, "snapshot") + var before: Dictionary = runtime.create_snapshot() + if not record_state("P", package) or not record_state("B", before): + return false + signals = [] + main.call("_load_manual_at_path", "user://p28_restart/tampered", true) + var failed: Dictionary = runtime.create_snapshot() + if not record_state("after_tamper", failed) or not io_proof(before, failed, "tamper", null): + return false + signals = [] + main.call("_load_manual_at_path", "user://p28_restart/good", true) + var after: Dictionary = runtime.create_snapshot() + if not record_state("after_load", after) or not io_proof(package, after, "load", null): + return false + report["failure_event_absent"] = true + for event: Dictionary in witness(after).recent_events: + if var_to_bytes(event) == var_to_bytes(failure_event): + report.failure_event_absent = false + return report.failure_event_absent + +func deterministic_step() -> bool: + if not io_ready or not GameClock.paused: + return false + var before: int = GameClock.absolute_minute + var backlog: int = GameClock.get_backlog_minutes() + GameClock.advance_minutes(1) + var after: int = GameClock.absolute_minute + if after != before + 1: + return false + var state: Dictionary = runtime.create_snapshot() + var normalized := state.duplicate(true) + var recent: Array = field(field(runtime_part(normalized), "domain_events"), "recent_events") + var replacement: Variant = null + var count := 0 + var recent_ids: Array = [] + for index in range(recent.size()): + recent_ids.append(field(recent[index], "event_id")) + if field(recent[index], "event_id") == E: + replacement = recent[index].duplicate(true) + recent[index] = {"event_id": E, "replaced_for_cross_pid_compare": true} + count += 1 + if count > 1: + return false + report.minutes.append({"before": before, "after": after, "paused": GameClock.paused, + "backlog_before": backlog, "backlog_after": GameClock.get_backlog_minutes(), + "sha256": bytes_hash(var_to_bytes(normalized)), "scope": "complete_typed_var_to_bytes", + "excluded_paths": [], "replacement": replacement, "replacement_count": count, + "recent_count": recent.size(), "recent_ids": recent_ids}) + report.actual_k += after - before + return GameClock.paused and backlog == GameClock.get_backlog_minutes() + +func finish(reason: String) -> void: + if finished: + return + finished = true + report["stop_reason"] = reason + report["key_forms"] = key_forms + print("P28_RESTART_REPORT=" + JSON.stringify(report)) + if is_instance_valid(main) and main.is_inside_tree(): + main.ui_audio_window_close_drained.connect(emit_audio_close, CONNECT_ONE_SHOT) + get_root().close_requested.emit() + else: + quit(1) + +func emit_audio_close(result: Dictionary) -> void: + print("P28_RESTART_AUDIO_CLOSE=" + JSON.stringify(result)) diff --git a/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/test_restart.py b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/test_restart.py new file mode 100644 index 0000000..cf16653 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/test_restart.py @@ -0,0 +1,466 @@ +#!/usr/bin/env python3 +"""Engine-free positive and adversarial controls. No native acceptance claimed.""" +import copy +import hashlib +import json +import os +from pathlib import Path +import sys +import tempfile +import time +from types import SimpleNamespace +import unittest +from unittest.mock import patch + +sys.dont_write_bytecode = True +import check_restart as check +import restart_pair as driver + +DATA = b"synthetic typed bytes fixture (not engine evidence)" +HASH = hashlib.sha256(DATA).hexdigest() + + +def comparison(): + return {"method": "var_to_bytes", "equal": True, "diff_paths": [], "excluded_paths": [], + "expected_sha256": HASH, "actual_sha256": HASH} + + +def history(count=256): + ids = ["domain_event.%012d" % n for n in range(1, count + 1)] + return {"counter": count, "published_ids": ids, + "recent_events": [{"event_id": eid, "event_type": "OLD", "payload": {"n": n}} + for n, eid in enumerate(ids)]} + + +def io(kind, minute=612, count=256): + before = history(count) + payload = {"ok": kind != "tamper", "code": "OK" if kind != "tamper" else "CORRUPT_WORLD", + "operation": "save" if kind == "save" else "load", + "save_path": "user://p28_restart/" + ("tampered" if kind == "tamper" else "good"), + "save_id": "synthetic", "saved_absolute_minute": minute} + event = {"event_id": check.event_id(count), + "event_type": {"save": "SAVE_CHECKPOINT_SUCCEEDED", "load": "SAVE_LOADED", + "tamper": "SAVE_LOAD_FAILED"}[kind], + "source_service": "save_service", "entity_ids": [], "causation_command_id": 0, + "absolute_minute": minute, "payload": copy.deepcopy(payload)} + return {"kind": kind, "signal_count": 1, "signal_payload": payload, "latest": copy.deepcopy(payload), + "event": event, "signal_projection": "_save_event_payload", "payload_bytes_equal": True, + "return_kind": "bool" if kind == "save" else "void", + "return_value": True if kind == "save" else None, + "before": before, "after": check.transform(before, event), + "dropped": before["recent_events"][0] if count == 256 else None, + "comparison": comparison(), "minute_before": minute, "minute_after": minute, + "paused_before": True, "paused_after": True, + "failure": {"preload_pause_restored": True, "loaded_clock_paused": True, + "migration_attempted": False, "recovery_report": {"restore_committed": False}}} + + +def report(role="R-writer", pid=100): + writer, ops = role.endswith("writer"), role.startswith("O-") + minute = 391 if ops else 612 + rows = [io("save", minute)] if writer else [io("tamper", 360, 3), io("load", minute)] + names = ["P", "after_save", "final"] if writer else ["P", "B", "after_tamper", "after_load", "final"] + event = rows[-1]["event"] + boundary = {"clock": minute, "guest_auto_ids": ["guest-auto-00000001"], "guest_auto_count": 1, + "operations": {"enabled": True}, "processed_arrival_minutes": [390], + "arrival_reports": [{"minute": 390}]} if ops else None + next_boundary = {"minute": 420, "processed_once": True, "before": [390], "after": [390, 420], + "arrival_reports": [{"minute": 390}, {"minute": 420}], + "guest_auto_ids": ["guest-auto-00000001", "guest-auto-00000002"]} if ops else None + clock = {"minute": minute, "paused": True, "backlog": 121, "accumulator_hex": "00"} + return {"schema": "p28-restart-v1", "role": role, "pid": pid, "stop_reason": "complete", + "main_added": True, "k_target": 30, "actual_k": 30, "comparison_scope": "complete_typed_var_to_bytes", + "excluded_paths": [], "transfer_before_guard_writes": 0, "probe_deadline_seconds": 1740, + "io": rows, "package_matches_before": comparison(), "failure_event_absent": True, + "transfer_verified": True, "event_id": event["event_id"], "saved_minute": minute, + "start_minute": 360 if ops else 602, "final_minute": minute + 30, + "states": {n: {"path": "p28_restart/evidence/" + n + ".bin", "bytes": len(DATA), "sha256": HASH} + for n in names}, + "paused_idle": {"before": clock, "after": copy.deepcopy(clock), "comparison": comparison()}, + "boundary": boundary, "next_boundary": next_boundary, "ui_ticks": [copy.deepcopy(boundary)] if ops else [], + "minutes": [{"before": minute + n, "after": minute + n + 1, "paused": True, + "backlog_before": 121, "backlog_after": 121, "sha256": HASH, + "scope": "complete_typed_var_to_bytes", "excluded_paths": [], + "replacement": copy.deepcopy(event) if n < 5 else None, + "replacement_count": 1 if n < 5 else 0, "recent_count": 256, + "recent_ids": history(255)["published_ids"] + + ([event["event_id"]] if n < 5 else ["domain_event.000000000256"])} + for n in range(30)]} + + +def write(path, value): + with Path(path).open("x") as stream: + json.dump(value, stream) + + +def audio(pid): + return {"coordinator": True, "passed": True, "owned_engine_pid": pid, + "all_owned_targets_released": True, "deadline_reached": False, + "clock_process_held_stopped": True, "window_input_held_disabled": True, + "restored_before_quit": False, "shared_bus_deleted": False, "settings_mutated": False} + + +def synthetic_receipt(out): + rows = [] + profiles = {} + source_package = out / "synthetic-package" + source_package.mkdir() + (source_package / "world.bin").write_bytes(DATA) + (source_package / "manifest.json").write_text("{}") + transfers = {kind: driver.create_transfer(source_package, out / (kind + "-transfer"), write) + for kind in ("R", "O")} + for index, label in enumerate(check.PHASES): + folder = out / label + folder.mkdir() + pid = 100 + index + process = {"exit_code": 0, "helper_exit_code": 0, "timed_out": False, "error_lines": [], + "lifecycle_error": None, "external_process_or_group_signals": 0, + "owned_popen_ended": True, "old_helper_engine_ended": True, + "start_receipt_written": True, "end_receipt_written": True} + capture = {"passed": True, "all_captured_ended": True, "actual_captured_pids": [pid], + "outer_cleanup_errors": [], "adapter_exception": None, "foreign_process_or_group_signals": 0, + "all_cleanup_before_outer_receipt": True, "outer_receipt_written": True, + "outer_receipt_error": None, "cleanup": []} + write(folder / "outer-owned-finally.json", {k: v for k, v in capture.items() if k not in + ("passed", "outer_receipt_written", "outer_receipt_error")}) + for filename, key in (("started.json", "actual_start_binding"), ("ended.json", "actual_end_binding")): + binding = {"pid": pid, "actual_pid": pid, "owner_pid": 99, + "argv": ["fake", "--check-only"], "command": ["fake", "--check-only"]} + write(folder / filename, binding) + process[key] = binding + process["adapter_owned_cleanup"] = {"engine_ended": True, "errors": []} + write(folder / "adapter-owned-cleanup.json", process["adapter_owned_cleanup"]) + process.update(pid=pid, argv=["fake", "--check-only"]) + raw = "" + row = {"phase": label, "passed": True, "process": process, "capture": capture, + "timeout_seconds": 1800, "diagnostics": [], "argv": ["fake", "--check-only"], + "stage_before": HASH, "stage_after": HASH, "stage_preserved": True, + "source_preserved": True, "player_preserved": True} + startup = out / (label + "-startup") + startup.write_bytes(DATA) + row["startup_project"] = {"fresh_before": True, "path": str(startup), "sha256": HASH} + if index >= 3: + observed = report(label, pid) + if label.endswith("reader"): + observed["transfer_manifest_sha256"] = check.sha(out / (label[0] + "-transfer/manifest.json")) + row["report"] = observed + raw = "P28_RESTART_REPORT=" + json.dumps(observed) + "\nP28_RESTART_AUDIO_CLOSE=" + json.dumps(audio(pid)) + "\n" + profile = str(out / (label + "-private")) + row.update(runtime_profile=profile, runtime_name=label) + profiles[profile] = {} + for kind in (("good",) if label.endswith("writer") else ("good", "tampered")): + for name, value in transfers[label[0]]["files"][kind].items(): + profiles[profile]["p28_restart/" + kind + "/" + name] = value + guard = {"verified": True, "conditions": [True] * 5, "conditions_checked": 5, + "actual_user_data_dir": profile, "expected_user_data_dir": profile, + "globalized_probe": str(Path(profile) / "guard-probe"), "custom_user_dir_name": label, + "fresh_before_parent_initialize": True, "profile_writes_before_guard": 0, + "project_settings_persisted": False} + guard_path = out / (label + "-guard.json") + write(guard_path, guard) + row.update(guard=guard, guard_sha256=check.sha(guard_path)) + states = out / (label + "-states") + states.mkdir() + for name in observed["states"]: + (states / (name + ".bin")).write_bytes(DATA) + for name, content in (("stdout.log", raw), ("stderr.log", ""), ("engine-native.log", "native\n")): + (folder / name).write_text(content) + row["raw"] = {n: check.pin(folder / n) for n in ("stdout.log", "stderr.log", "engine-native.log")} + process.update(stdout_sha256=row["raw"]["stdout.log"]["sha256"], + stderr_sha256=row["raw"]["stderr.log"]["sha256"]) + write(folder / "process.json", process) + row["owned_files"] = {n: check.pin(folder / n) for n in + ("process.json", "started.json", "ended.json", "adapter-owned-cleanup.json", "outer-owned-finally.json")} + rows.append(row) + probe = out / "restart_probe.gd" + probe.write_bytes((check.HERE / probe.name).read_bytes()) + source = {str(i): {"bytes": 1, "sha256": HASH} for i in range(1407)} + worker = {n: check.sha(check.HERE / n) for n in + ("restart_pair.py", "restart_probe.gd", "check_restart.py", "test_restart.py")} + return {"passed": True, "error": None, "cleanup_errors": [], "phases": rows, + "source_before": source, "source_after": copy.deepcopy(source), + "default_player_before": {}, "default_player_after": {}, + "dependencies_before": check.DEPENDENCIES.copy(), "dependencies_after": check.DEPENDENCIES.copy(), + "collector_before": driver.COLLECTOR_PINS.copy(), "collector_after": driver.COLLECTOR_PINS.copy(), + "worker_before": worker, "worker_after": worker.copy(), "profiles": profiles, + "cross_cutting": copy.deepcopy(check.CROSS), "original_credit": 0, + "windows_verified": False, "operations_200_guests_verified": False, + "script_pins": {str(probe): check.sha(probe)}, "owned_writes": [], + "cleanup": [{"pid": 100 + n, "engine_ended": True, "outer_cleanup_errors": [], + "foreign_process_or_group_signals": 0} for n in range(7)]} + + +class RestartTests(unittest.TestCase): + def rejects(self, original, mutations, validator): + for name, mutation in mutations.items(): + with self.subTest(rejection=name): + value = copy.deepcopy(original) + mutation(value) + with self.assertRaises((ValueError, KeyError, TypeError)): + validator(value) + + def test_rs1_signal_bool_void(self): + for kind in ("save", "load", "tamper"): + check.check_io(io(kind), kind) + self.rejects(io("save"), { + "true only": lambda x: x.pop("signal_count"), + "no signal": lambda x: x.update(signal_count=0), + "duplicate signal": lambda x: x.update(signal_count=2), + "bool differs": lambda x: x.update(return_value=False), + "latest differs": lambda x: x["latest"].update(code="FAILED"), + "payload typed mismatch": lambda x: x.update(payload_bytes_equal=False), + "state mismatch": lambda x: x["comparison"].update(equal=False), + }, lambda x: check.check_io(x, "save")) + self.rejects(io("load"), { + "void success": lambda x: x.update(return_value=True), + "void coerced bool": lambda x: x.update(return_kind="bool"), + "missing signal": lambda x: x.update(signal_count=0), + }, lambda x: check.check_io(x, "load")) + + def test_rs2_exact_event_transform(self): + for kind in ("save", "load", "tamper"): + for count in (0, 255, 256): + check.check_io(io(kind, count=count), kind) + self.rejects(io(kind), { + "two events": lambda x: x["after"]["recent_events"].append(copy.deepcopy(x["event"])), + "different E": lambda x: x["event"].update(event_id="domain_event.000009999999"), + "counter": lambda x: x["after"].update(counter=x["before"]["counter"] + 2), + "missing ID": lambda x: x["after"]["published_ids"].pop(0), + "duplicate ID": lambda x: x["after"]["published_ids"].append(x["event"]["event_id"]), + "unsorted IDs": lambda x: x["after"]["published_ids"].reverse(), + "drop tail": lambda x: x["after"].update(recent_events=x["before"]["recent_events"][:-1] + [x["event"]]), + "wrong dropped record": lambda x: x.update(dropped=x["before"]["recent_events"][-1]), + "excluded path": lambda x: x["comparison"].update(excluded_paths=["systems.runtime.economy"]), + "diff hidden": lambda x: x["comparison"].update(diff_paths=["$.clock"]), + "digest only": lambda x: x["comparison"].update(method="SimulationDigest"), + "bytes SHA disagreement": lambda x: x["comparison"].update(actual_sha256="f" * 64), + }, lambda x: check.check_io(x, kind)) + + def test_clarification01_tamper(self): + self.rejects(io("tamper"), { + "failure event ignored": lambda x: x.update(after=x["before"]), + "failure ok": lambda x: x["signal_payload"].update(ok=True), + "failure OK code": lambda x: x["signal_payload"].update(code="OK"), + "not restored": lambda x: x["failure"].update(preload_pause_restored=False), + "paused differs": lambda x: x.update(paused_after=False), + "loaded pause differs": lambda x: x["failure"].update(loaded_clock_paused=False), + "restore committed": lambda x: x["failure"]["recovery_report"].update(restore_committed=True), + "migration unrecorded": lambda x: x["failure"].pop("migration_attempted"), + }, lambda x: check.check_io(x, "tamper")) + self.rejects(report("R-reader"), { + "failed event retained": lambda x: x.update(failure_event_absent=False), + "backlog consumed": lambda x: x["paused_idle"]["after"].update(backlog=120), + "reader not paused": lambda x: x["io"][1].update(paused_after=False), + }, check.check_report) + + def test_rs3_actual_k_full_typed_cross_pid(self): + writer, reader = report(), report("R-reader", 101) + check.check_pair(writer, reader) + self.rejects(reader, { + "K call count": lambda x: x["minutes"][0].update(after=x["saved_minute"]), + "different K": lambda x: x.update(actual_k=29), + "missing minute": lambda x: x["minutes"].pop(), + "partial digest": lambda x: x["minutes"][0].update(scope="SimulationDigest"), + "minute SHA": lambda x: x["minutes"][0].update(sha256="f" * 64), + "wrong replacement": lambda x: x["minutes"][0]["replacement"].update(event_id="other"), + "two replacements": lambda x: x["minutes"][0].update(replacement_count=2), + "after window": lambda x: x["minutes"][6].update(replacement=x["io"][-1]["event"], replacement_count=1), + "excluded path": lambda x: x["minutes"][0].update(excluded_paths=["clock"]), + "same PID": lambda x: x.update(pid=100), + }, lambda x: check.check_pair(writer, x)) + + def test_rs4_rs5_arrival_boundary(self): + writer, reader = report("O-writer"), report("O-reader", 101) + check.check_pair(writer, reader) + self.rejects(writer, { + "boundary only": lambda x: x["boundary"].update(guest_auto_count=0), + "null arrival": lambda x: x.update(boundary=None), + "unprocessed": lambda x: x["boundary"].update(processed_arrival_minutes=[]), + "not first tick": lambda x: x["ui_ticks"].insert(0, copy.deepcopy(x["boundary"])), + "duplicate guest": lambda x: x["boundary"]["guest_auto_ids"].append("guest-auto-00000001"), + "next missing": lambda x: x["next_boundary"].update(processed_once=False), + "next duplicate": lambda x: x["next_boundary"]["after"].append(420), + "cap exceeded": lambda x: x.update(start_minute=200), + }, check.check_report) + + def test_d1_import_closure(self): + check.check_dependencies() + check.check_modules() + for name in ("restart_pair.py", "check_restart.py", "test_restart.py"): + check.check_imports((check.HERE / name).read_text()) + for text in ("import forbidden_project_module", "from tools.strict_test_markers import main", + "importlib.util.spec_from_file_location('x', 'tools/evil.py')", + "__import__('tools.evil')", "exec('import tools.evil')"): + with self.subTest(text=text), self.assertRaises(ValueError): + check.check_imports(text) + with self.assertRaises(ValueError): + check.check_modules({"bad": SimpleNamespace(__file__=str(check.ROOT / "tools/evil.py"))}) + for path in check.DEPENDENCIES: + original = check.sha + with self.subTest(pin=path), patch.object(check, "sha", side_effect=lambda p: + "0" * 64 if Path(p) == check.ROOT / path else original(p)), self.assertRaises(ValueError): + check.check_dependencies() + + def test_d2_order_and_check_plan(self): + collector, _, _ = driver.load_dependencies() + commands = driver.plan(collector, Path("/engine"), Path("/out")) + self.assertEqual(len(commands), 7) + self.assertIn("--import", commands[0]) + self.assertIn("--check-only", commands[1]) + self.assertIn("--check-only", commands[2]) + rows = [] + for _, label in driver.schedule(rows): + rows.append({"phase": label, "passed": True, "report": {"stop_reason": "complete"}}) + self.assertEqual([r["phase"] for r in rows], check.PHASES) + rows = [] + iterator = driver.schedule(rows) + for _ in range(4): + _, label = next(iterator) + rows.append({"phase": label, "passed": True, "report": {"stop_reason": "complete"}}) + rows[-1]["passed"] = False + with self.assertRaises(ValueError): + next(iterator) + rows[-1]["passed"] = True + rows[-1]["report"]["stop_reason"] = "failed" + iterator = driver.schedule([]) + next(iterator) + with self.assertRaises(ValueError): + next(iterator) + + def test_d3_transfer_and_fresh_profiles(self): + with tempfile.TemporaryDirectory(dir=check.HERE) as temporary: + base = Path(temporary) + source = base / "source" + source.mkdir() + (source / "world.bin").write_bytes(b"0123456789") + (source / "manifest.json").write_text("{}") + target = base / "transfer" + manifest = driver.create_transfer(source, target, write) + driver.check_transfer(target, manifest) + self.assertEqual((target / "tampered/world.bin").read_bytes(), b"0123446789") + driver.fresh_profile(base / "new-profile") + with self.assertRaises(ValueError): + driver.fresh_profile(source) + self.rejects(manifest, { + "SHA": lambda x: x["files"]["good"]["world.bin"].update(sha256="f" * 64), + "offset": lambda x: x["tamper"].update(offset=0), + "xor": lambda x: x["tamper"].update(xor=2), + }, lambda x: driver.check_transfer(target, x)) + (target / "tampered/manifest.json").write_text("changed") + with self.assertRaises(ValueError): + driver.check_transfer(target, manifest) + + def test_d4_d5_d7_probe_static(self): + text = (check.HERE / "restart_probe.gd").read_text() + check.check_probe(text) + snippets = ["GameClock.advance_minutes(1)", "GameClock.set_paused(false)", + "x.clear()", "spawn_guest()", "SimulationRuntime.new()", "initialize()", + "enable_production_checkpoint()", "main._toggle_pause()", "GameClock.set_paused(true)", + "var elapsed_us = 1", "Time.get_ticks_usec()"] + for snippet in snippets: + with self.subTest(snippet=snippet), self.assertRaises(ValueError): + check.check_probe(text + "\nfunc bad():\n\t" + snippet + "\n") + for replacement in ("GameClock.advance_minutes(2)", "Time.get_ticks_usec()", + "GameClock.advance_one_minute()"): + with self.subTest(step=replacement), self.assertRaises(ValueError): + check.check_probe(text.replace("GameClock.advance_minutes(1)", replacement)) + + def test_rs7_rs8_full_receipt_rejections(self): + with tempfile.TemporaryDirectory(dir=check.HERE) as temporary: + out = Path(temporary) + receipt = synthetic_receipt(out) + check.check_receipt(receipt, out) + self.rejects(receipt, { + "source map": lambda x: x["source_after"].pop("0"), + "ordinary player": lambda x: x["default_player_after"].update(changed=True), + "guard": lambda x: x["phases"][3]["guard"].update(profile_writes_before_guard=1), + "raw SHA": lambda x: x["phases"][3]["raw"]["stdout.log"].update(sha256="f" * 64), + "PID": lambda x: x["phases"][3]["capture"].update(actual_captured_pids=[999]), + "owned write": lambda x: x["owned_writes"].append("R-writer/report.json"), + "cleanup": lambda x: x["cleanup"][0].update(engine_ended=False), + "stage": lambda x: x["phases"][1].update(stage_after="f" * 64), + "collector": lambda x: x["collector_after"].pop("measure_p28_01.gd"), + "dependency": lambda x: x["dependencies_after"].update({"tools/run_all_tests.py": "f" * 64}), + "cross-cutting": lambda x: x["cross_cutting"].pop(), + "source count": lambda x: (x["source_after"].pop("1"), x["source_before"].pop("1")), + "phase order": lambda x: x["phases"].reverse(), + "foreign signal": lambda x: x["phases"][0]["capture"].update(foreign_process_or_group_signals=1), + }, lambda x: check.check_receipt(x, out)) + (out / "R-reader-states/B.bin").write_bytes(b"changed") + with self.assertRaises(ValueError): + check.check_receipt(receipt, out) + + def test_d6_raw_report_and_audio(self): + with tempfile.TemporaryDirectory(dir=check.HERE) as temporary: + out = Path(temporary) + receipt = synthetic_receipt(out) + row = receipt["phases"][3] + folder = out / "R-writer" + original = (folder / "stdout.log").read_text() + for text in ("", original + original, original.split("P28_RESTART_AUDIO_CLOSE=")[0], + original.replace('"owned_engine_pid": 103', '"owned_engine_pid": 999')): + (folder / "stdout.log").write_text(text) + row["raw"]["stdout.log"] = check.pin(folder / "stdout.log") + with self.assertRaises(ValueError): + check.bound_raw(folder, row) + + def test_d2_d6_real_owned_outer_fake_child(self): + collector, _, outer = driver.load_dependencies() + child = ( + "import os,sys,json,time,pathlib\n" + "mode=sys.argv[1]\n" + "identity={'pid':os.getpid(),'ppid':os.getppid(),'pgid':os.getpgrp()," + "'args':' '.join([sys.executable]+sys.argv)}\n" + "path=pathlib.Path(sys.argv[0]).parent/(mode+'-identity')\n" + "tmp=path.with_suffix('.tmp'); tmp.write_text(json.dumps(identity)); tmp.replace(path)\n" + "pathlib.Path(sys.argv[sys.argv.index('--log-file')+1]).write_text('native raw\\n')\n" + "print('Godot Engine v4.7.2.stable.official.ed1daf0bf - https://godotengine.org',flush=True)\n" + "time.sleep(0.15)\n" + "if mode=='timeout': time.sleep(5)\n" + "sys.exit(3 if mode=='exit' else 0)\n") + with tempfile.TemporaryDirectory(dir=check.HERE) as temporary: + base = Path(temporary) + script = base / "fake-child" + script.write_text(child) + def diagnostic(command, folder, timeout, env, **kwargs): + def probe(pid): + path = base / (command[2] + "-identity") + deadline = time.monotonic() + 2 + while time.monotonic() < deadline: + if path.exists(): + identity = json.loads(path.read_text()) + if identity["pid"] == pid: + return identity + time.sleep(.005) + raise RuntimeError("fake child identity unavailable") + return outer.native.run_owned_diagnostic(command, folder, timeout, env, probe_fn=probe, **kwargs) + for mode in ("ok", "exit", "timeout"): + folder = base / mode + def run_phase(command, destination, limit, env, **kwargs): + self.assertEqual(limit, 1800) + return outer.run_phase(command, destination, .3 if mode == "timeout" else 5, + env, diagnostic_fn=diagnostic, **kwargs) + def guarded_write(path, value): + self.assertFalse(Path(path).is_relative_to(folder), "driver writes owned folder") + write(path, value) + def real_phase(*args, **kwargs): + kwargs["dump_fn"] = write + return run_phase(*args, **kwargs) + with patch.object(collector, "live_engines", return_value=[]), \ + patch.object(collector, "write", side_effect=guarded_write): + row = collector.run_engine(SimpleNamespace(run_phase=real_phase), + [sys.executable, str(script), mode], folder, {}) + row["phase"] = "import" + self.assertEqual(row["passed"], mode == "ok", row) + self.assertTrue(row["capture"]["all_captured_ended"]) + if mode == "ok": + check.bound_raw(folder, row) + else: + with self.assertRaises(ValueError): + check.bound_raw(folder, row) + _, errors = outer.finish_captured(outer.OWNED) + self.assertEqual(errors, []) + + +if __name__ == "__main__": + unittest.main(verbosity=2)