483 lines
37 KiB
GDScript
483 lines
37 KiB
GDScript
extends SceneTree
|
|
|
|
## Controlled public Runtime/UI fixture, not natural guest/destruction acceptance.
|
|
## The legacy fixture MUST be a pinned genuine prepatch14 formal capture from r08.
|
|
## It is never reconstructed by changing current facility cached fields.
|
|
const LEGACY := "res://tests/fixtures/required_kitchen_legacy14"
|
|
const PACKAGE_FILES: Array[String] = ["world.bin", "manifest.json", "journal.log", "thumbnail.png"]
|
|
var checks := 0
|
|
var failures := 0
|
|
var runtime: Variant
|
|
var clock: Variant
|
|
var state: Variant
|
|
var panel: Variant
|
|
var ids: Dictionary = {}
|
|
var output := ""
|
|
var base_slot := ""
|
|
var checkpoint_minute := -1
|
|
var journals: Array[Dictionary] = []
|
|
var _journal_stream: Variant
|
|
|
|
func _initialize() -> void: call_deferred("run")
|
|
|
|
func expect(ok: bool, label: String) -> void:
|
|
checks += 1
|
|
if not ok: failures += 1; push_error("Required kitchen: " + label)
|
|
else: print(" ✓ ", label)
|
|
|
|
func _guard() -> bool:
|
|
if not OS.get_cmdline_user_args().is_empty(): return false
|
|
var any_external := false
|
|
for key: String in ["THE_TREE_REQUIRED_KITCHEN_EXPECTED_RES_DIR", "THE_TREE_REQUIRED_KITCHEN_EXPECTED_USER_DIR", "THE_TREE_REQUIRED_KITCHEN_EXPECTED_PROFILE"]:
|
|
any_external = any_external or OS.has_environment(key)
|
|
if any_external:
|
|
var res := OS.get_environment("THE_TREE_REQUIRED_KITCHEN_EXPECTED_RES_DIR")
|
|
var user := OS.get_environment("THE_TREE_REQUIRED_KITCHEN_EXPECTED_USER_DIR")
|
|
var profile := OS.get_environment("THE_TREE_REQUIRED_KITCHEN_EXPECTED_PROFILE")
|
|
if not res.is_absolute_path() or not user.is_absolute_path() or profile.is_empty(): return false
|
|
if ProjectSettings.globalize_path("res://").simplify_path().trim_suffix("/") != res.simplify_path().trim_suffix("/") or OS.get_user_data_dir() != user: return false
|
|
if not bool(ProjectSettings.get_setting("application/config/use_custom_user_dir", false)) or ProjectSettings.get_setting("application/config/custom_user_dir_name", "") != profile: return false
|
|
base_slot = "user://tests/required_kitchen_%d_%d" % [OS.get_process_id(), Time.get_ticks_usec()]
|
|
output = ProjectSettings.globalize_path(base_slot.path_join("proofs"))
|
|
return not DirAccess.dir_exists_absolute(ProjectSettings.globalize_path(base_slot)) and DirAccess.make_dir_recursive_absolute(output) == OK
|
|
|
|
func _proof(label: String, value: Variant) -> void:
|
|
var path := output.path_join(label + ".var")
|
|
var file := FileAccess.open(path, FileAccess.WRITE) if not FileAccess.file_exists(path) else null
|
|
expect(file != null, "fresh full typed proof " + label)
|
|
if file != null: file.store_buffer(var_to_bytes(value)); file.close()
|
|
|
|
func _read(path: String) -> Variant:
|
|
var file := FileAccess.open(path, FileAccess.READ)
|
|
expect(file != null, "pinned genuine typed fixture opens " + path.get_file())
|
|
if file == null: return null
|
|
var value: Variant = bytes_to_var(file.get_buffer(file.get_length()))
|
|
file.close(); return value
|
|
|
|
func _setup() -> bool:
|
|
clock = load("res://autoload/game_clock.gd").new()
|
|
state = load("res://autoload/game_state.gd").new()
|
|
runtime = load("res://autoload/simulation_runtime.gd").new()
|
|
clock.reset(1, 6, 0); state.reset()
|
|
expect(runtime.initialize(clock, state, 42163).get(&"valid", false), "canonical resources/world, disclosed seed, Operations off")
|
|
clock.set_paused(true)
|
|
runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
|
|
_listen()
|
|
expect(runtime.create_snapshot()[&"systems"][&"runtime"][&"runtime_schema_version"] == 14 and runtime.facility_service.has_method(&"get_required_kitchen_report"), "actual schema14 and required-kitchen implementation")
|
|
return failures == 0
|
|
|
|
func _listen(include_load_event := false) -> void:
|
|
if _journal_stream == runtime.domain_events: return
|
|
_journal_stream = runtime.domain_events
|
|
var events: Array[Dictionary] = []
|
|
# Load replaces the stream before subscribing. Preserve its actual just-issued
|
|
# full IO event explicitly; no capped-history reconstruction or deletion.
|
|
if include_load_event:
|
|
var recent: Array = runtime.domain_events.recent_events()
|
|
if not recent.is_empty(): events.append(recent.back().to_dictionary().duplicate(true))
|
|
journals.append({"epoch": journals.size(), "absolute_minute": clock.absolute_minute, "events": events, "load_event_explicitly_retained": include_load_event})
|
|
expect(runtime.domain_events.subscribe(&"*", &"qa.required_kitchen.uncapped", _capture_event), "uncapped actual domain journal subscribed without altering old subscribers")
|
|
|
|
func _capture_event(event: SimulationDomainEvent) -> void:
|
|
journals.back()["events"].append(event.to_dictionary().duplicate(true))
|
|
|
|
|
|
func _command(kind: StringName, payload: Dictionary, target: StringName = &"") -> CommandResult:
|
|
return runtime.submit_command(kind, payload, &"player", target, 75)
|
|
|
|
func _prepare() -> void:
|
|
for row: Array in [[&"cafeteria", Vector3i(2, 0, 0)], [&"kitchen", Vector3i(2, 0, 1)], [&"kitchen", Vector3i(-6, 0, 1)], [&"staff_door", Vector3i(0, 0, 1)]]:
|
|
expect(_command(&"build_facility", {&"definition_id": row[0], &"origin": row[1]}).is_success(), "real paid construction " + String(row[0]))
|
|
clock.advance_minutes(180)
|
|
for facility: FacilityInstance in runtime.facility_service.get_instances():
|
|
if facility.definition_id == &"cafeteria": ids["cafe"] = facility.instance_id
|
|
elif facility.definition_id == &"kitchen": ids["a" if facility.origin.x == 2 else "b"] = facility.instance_id
|
|
expect(ids.has("cafe") and ids.has("a") and ids.has("b"), "actual dedicated cafe and two BACK kitchen IDs")
|
|
if failures: return
|
|
for row: Array in [[&"staff_chika", ids["a"]], [&"staff_doran", ids["b"]], [&"staff_iris", ids["cafe"]], [&"staff_juno", ids["cafe"]]]:
|
|
expect(_command(&"hire_staff_candidate", {&"candidate_id": row[0]}).is_success(), "catalog hire " + String(row[0]))
|
|
expect(_command(&"set_staff_work_assignment", {&"staff_id": row[0], &"facility_id": row[1], &"template_id": &"DAY"}, row[1]).is_success(), "public DAY assignment " + String(row[0]))
|
|
clock.advance_minutes(30)
|
|
var cafe: FacilityInstance = runtime.facility_service.get_instance(ids["cafe"])
|
|
expect(cafe.active_staff_ids == [&"staff_iris", &"staff_juno"] and runtime.facility_service.get_instance(ids["a"]).active_staff_ids == [&"staff_chika"] and runtime.facility_service.get_instance(ids["b"]).active_staff_ids == [&"staff_doran"], "genuine actual attendance, no staffing-cache injection")
|
|
expect(_command(&"add_inventory_stack", {&"owner_inventory_id": cafe.local_inventory_id, &"item_id": SupplyCatalog.BASIC_MEAL, &"quantity": 4}).is_success(), "disclosed finite4 stock public fixture")
|
|
expect(_command(&"register_guest_wallet", {&"guest_id": &"kitchen_payer", &"gold": 100}).is_success(), "disclosed100G payer public fixture")
|
|
expect(not cafe.related_facility_operating and cafe.operation_state == FacilityInstance.OperationState.INACTIVE, "unlinked dedicated cafe cannot operate despite real staff")
|
|
var before: Dictionary = runtime.create_snapshot()
|
|
var rejected := _command(&"create_service_request", {&"request_id": &"unlinked_rejected", &"facility_id": ids["cafe"], &"guest_ids": [&"kitchen_payer"]}, ids["cafe"])
|
|
var after: Dictionary = runtime.create_snapshot()
|
|
var kept := true
|
|
for key: StringName in [&"facilities", &"inventory", &"economy"]:
|
|
kept = kept and var_to_bytes(before[&"systems"][&"runtime"][key]) == var_to_bytes(after[&"systems"][&"runtime"][key])
|
|
expect(not rejected.is_success() and rejected.details.get(&"reason") == &"FACILITY_CLOSED" and kept, "unlinked request refuses before owned queue/stock/ledger mutation; outer command journal is retained")
|
|
_proof("unlinked-request-before", before); _proof("unlinked-request-after", after)
|
|
await _ui()
|
|
expect("25G" in panel.quote_label.text and "厨房" in panel.kitchen_status_label.text, "ordinary UI retains quote and required dependency explanation")
|
|
await _select(ids["a"])
|
|
expect(cafe.related_facility_id == ids["a"] and cafe.related_facility_operating, "ordinary UI explicitly binds actual BACK kitchen A")
|
|
expect(_command(&"create_service_request", {&"request_id": &"kitchen_once", &"facility_id": ids["cafe"], &"guest_ids": [&"kitchen_payer"]}, ids["cafe"]).is_success(), "normal meal request after explicit link")
|
|
clock.advance_one_minute(); clock.advance_minutes(5)
|
|
var active: Array = runtime.facility_service.get_active_services(ids["cafe"])
|
|
expect(active.size() == 1 and active[0].state == ServiceInstance.State.ACTIVE and active[0].elapsed_minutes == 5 and active[0].duration_minutes == 45, "real ACTIVE5/45 meal midpoint")
|
|
if active.size() != 1: return
|
|
ids["service"] = active[0].service_id
|
|
expect(_command(&"set_facility_policy", {&"facility_id": ids["a"], &"patch": {&"business_open": false}}, ids["a"]).is_success(), "public kitchen close")
|
|
clock.advance_one_minute(); _paused(5, "closed kitchen")
|
|
expect(_command(&"set_facility_policy", {&"facility_id": ids["a"], &"patch": {&"business_open": true}}, ids["a"]).is_success(), "public non-suspended kitchen reopen")
|
|
clock.advance_one_minute()
|
|
expect(active[0].state == ServiceInstance.State.ACTIVE and active[0].elapsed_minutes == 6, "ordinary close/reopen retains existing resume contract")
|
|
expect(_command(&"set_facility_conditions", {&"facility_id": ids["a"], &"conditions": {&"durability": 0}}, ids["a"]).is_success(), "disclosed public durability0 fixture, not natural attack")
|
|
clock.advance_one_minute(); _paused(6, "destroyed kitchen")
|
|
expect(_command(&"set_facility_conditions", {&"facility_id": ids["a"], &"conditions": {&"durability": 10000, &"power_available": false, &"operator_open": true}}, ids["a"]).is_success(), "public repair/power-off fixture")
|
|
expect(_command(&"set_facility_conditions", {&"facility_id": ids["a"], &"conditions": {&"power_available": true}}, ids["a"]).is_success(), "public power restoration fixture")
|
|
clock.advance_one_minute(); _paused(6, "persistent suspension after repair/power restoration")
|
|
expect(runtime.facility_service.get_instance(ids["a"]).suspension_reason == &"DESTROYED" and cafe.related_facility_id == ids["a"], "selected destroyed ID retained without automatic relink/recovery")
|
|
|
|
func _ui() -> void:
|
|
if panel != null: panel.free()
|
|
panel = BusinessManagementPanel.new(); panel.configure(runtime); root.add_child(panel)
|
|
await process_frame
|
|
panel.select_facility(ids["cafe"])
|
|
expect(panel.kitchen_option.visible and panel.kitchen_apply_button.visible, "ordinary explicit kitchen selector is available")
|
|
|
|
func _select(id: StringName) -> void:
|
|
panel.select_facility(ids["cafe"])
|
|
var index := -1
|
|
for position: int in panel.kitchen_option.item_count:
|
|
if StringName(panel.kitchen_option.get_item_metadata(position)) == id: index = position
|
|
expect(index >= 0, "selector contains exact kitchen ID " + String(id))
|
|
if index < 0: return
|
|
panel.kitchen_option.select(index); panel.kitchen_option.item_selected.emit(index)
|
|
expect(not panel.kitchen_apply_button.disabled, "changed explicit kitchen draft can apply")
|
|
panel.kitchen_apply_button.pressed.emit(); await process_frame
|
|
|
|
func _paused(elapsed: int, label: String) -> void:
|
|
var service: ServiceInstance = runtime.facility_service.get_service(ids["service"])
|
|
var cafe: FacilityInstance = runtime.facility_service.get_instance(ids["cafe"])
|
|
expect(service != null and service.state == ServiceInstance.State.PAUSED and service.interruption_reason == &"RELATED_FACILITY_INACTIVE" and service.elapsed_minutes == elapsed and not cafe.related_facility_operating, "same actual service pauses with exact progress: " + label)
|
|
expect(service.price_snapshot == 25 and runtime.economy_service.get_balance(&"GUEST_WALLET:kitchen_payer") == 100 and not runtime.facility_service.get_request(&"kitchen_once").reserved_inventory_ids.is_empty(), "pause retains real price, reservation and uncharged wallet")
|
|
|
|
func _fixture_source(file: String) -> String:
|
|
return file + ".bytes" if file == "thumbnail.png" else file
|
|
|
|
func _fixture_package() -> Dictionary:
|
|
var result := {}
|
|
for file: String in PACKAGE_FILES:
|
|
result[file] = FileAccess.get_sha256(LEGACY.path_join("formal-package").path_join(_fixture_source(file)))
|
|
return result
|
|
|
|
func _package(path: String) -> Dictionary:
|
|
var result := {}
|
|
var directory := DirAccess.open(path)
|
|
if directory == null: return result
|
|
for file: String in directory.get_files(): result[file] = FileAccess.get_sha256(path.path_join(file))
|
|
return result
|
|
|
|
func _copy_fixture_file(source: String, destination: String) -> bool:
|
|
if FileAccess.file_exists(destination): return false
|
|
var input := FileAccess.open(source, FileAccess.READ)
|
|
if input == null: return false
|
|
var data := input.get_buffer(input.get_length())
|
|
input.close()
|
|
var file := FileAccess.open(destination, FileAccess.WRITE)
|
|
if file == null: return false
|
|
file.store_buffer(data); file.close()
|
|
return FileAccess.get_sha256(source) == FileAccess.get_sha256(destination)
|
|
|
|
func _io_valid(event: Dictionary, kind: String, operation: StringName, path: String) -> bool:
|
|
var payload: Dictionary = event.get("payload", {})
|
|
return event.get("event_type") == kind and event.get("source_service") == "save_service" and event.get("causation_command_id") == 0 and event.get("entity_ids") == [] and not String(event.get("event_id", "")).is_empty() and payload.get(&"operation") == operation and typeof(payload.get(&"ok")) == TYPE_BOOL and payload[&"ok"] and payload.get(&"code") == &"OK" and payload.get(&"save_path") == path and payload.get(&"checkpoint_absolute_minute") == checkpoint_minute and payload.get(&"saved_absolute_minute") == checkpoint_minute and event.get("absolute_minute") == checkpoint_minute and not String(payload.get(&"save_id", "")).is_empty() and payload.get(&"request_id") == 0 and payload.get(&"reason") == (&"manual" if operation == &"save" else &"") and (operation != &"load" or typeof(payload.get(&"duration_us")) == TYPE_INT and payload[&"duration_us"] >= 0 and typeof(payload.get(&"runtime_duration_us")) == TYPE_INT and payload[&"runtime_duration_us"] >= 0)
|
|
|
|
func _typed_sha(value: Variant) -> String:
|
|
var context := HashingContext.new()
|
|
context.start(HashingContext.HASH_SHA256)
|
|
context.update(var_to_bytes(value))
|
|
return context.finish().hex_encode()
|
|
|
|
func _legacy_relationship_v1_to_v2(expected_subtree: Variant, actual_subtree: Variant) -> Dictionary:
|
|
var rejected := {"ok": false, "expected": null, "proof": {}}
|
|
if not expected_subtree is Dictionary or not actual_subtree is Dictionary: return rejected
|
|
var old: Dictionary = expected_subtree
|
|
var actual: Dictionary = actual_subtree
|
|
# The genuine fixture uses StringName keys, in this exact serialization order.
|
|
if var_to_bytes(old.keys()) != var_to_bytes([&"version", &"relationships", &"threshold_candidates", &"next_candidate_sequence"]): return rejected
|
|
if typeof(old[&"version"]) != TYPE_INT or old[&"version"] != 1: return rejected
|
|
if not old[&"relationships"] is Array or not old[&"threshold_candidates"] is Array or typeof(old[&"next_candidate_sequence"]) != TYPE_INT: return rejected
|
|
if var_to_bytes(actual.keys()) != var_to_bytes([&"version", &"legacy", &"relationships", &"original_relationships"]): return rejected
|
|
if typeof(actual[&"version"]) != TYPE_INT or actual[&"version"] != 2: return rejected
|
|
if not actual[&"legacy"] is Dictionary or var_to_bytes(actual[&"legacy"]) != var_to_bytes(old): return rejected
|
|
if var_to_bytes(actual[&"legacy"].get(&"next_candidate_sequence")) != var_to_bytes(old[&"next_candidate_sequence"]): return rejected
|
|
if not actual[&"original_relationships"] is Array or not actual[&"original_relationships"].is_empty(): return rejected
|
|
# create_snapshot uses a typed compatibility collection even for empty v1 input.
|
|
var projection: Array[Dictionary] = []
|
|
for entry: Variant in old[&"relationships"]:
|
|
if not entry is Dictionary: return rejected
|
|
projection.append(entry.duplicate(true))
|
|
if var_to_bytes(actual[&"relationships"]) != var_to_bytes(projection): return rejected
|
|
var originals: Array[Dictionary] = []
|
|
var migrated := {&"version": 2, &"legacy": old.duplicate(true), &"relationships": projection.duplicate(true), &"original_relationships": originals}
|
|
if var_to_bytes(migrated) != var_to_bytes(actual): return rejected
|
|
return {"ok": true, "expected": migrated, "proof": {"old_v1_bytes": var_to_bytes(old).size(), "old_v1_sha256": _typed_sha(old), "replaced_subtree_sha256": _typed_sha(migrated)}}
|
|
|
|
func _legacy_checkpoint(expected: Dictionary, reader: Dictionary) -> Dictionary:
|
|
var migration := _legacy_relationship_v1_to_v2(expected[&"systems"][&"runtime"][&"relationships"], reader[&"systems"][&"runtime"][&"relationships"])
|
|
if not migration["ok"]: return migration
|
|
# Reverse just this subtree in a copy of the reader: every other byte must
|
|
# already match the writer after the separately validated IO1 replacement.
|
|
var outside := reader.duplicate(true)
|
|
outside[&"systems"][&"runtime"][&"relationships"] = expected[&"systems"][&"runtime"][&"relationships"].duplicate(true)
|
|
if var_to_bytes(outside) != var_to_bytes(expected): return {"ok": false}
|
|
var result := expected.duplicate(true)
|
|
result[&"systems"][&"runtime"][&"relationships"] = migration["expected"].duplicate(true)
|
|
if var_to_bytes(result) != var_to_bytes(reader) or result != reader or SimulationDigest.compute(result) != SimulationDigest.compute(reader): return {"ok": false}
|
|
return {"ok": true, "expected": result, "proof": migration["proof"]}
|
|
|
|
func _legacy_control(writer: Dictionary, reader: Dictionary, label: String, kind: String, field: StringName = &"", value: Variant = null) -> void:
|
|
var writer_bytes := var_to_bytes(writer); var reader_bytes := var_to_bytes(reader)
|
|
var candidate := writer.duplicate(true); var observed := reader.duplicate(true)
|
|
var old: Dictionary = candidate[&"systems"][&"runtime"][&"relationships"]
|
|
var actual: Dictionary = observed[&"systems"][&"runtime"][&"relationships"]
|
|
match kind:
|
|
"outside": candidate[&"legacy_control_outside"] = true
|
|
"delete": old.erase(field)
|
|
"change": old[field] = value
|
|
"sequence": actual[&"legacy"][&"next_candidate_sequence"] += 1
|
|
"originals": actual[&"original_relationships"].append({&"unapproved": true})
|
|
"version": actual[&"version"] = value
|
|
"reorder":
|
|
var reordered := {}
|
|
var keys := old.keys(); keys.reverse()
|
|
for key: Variant in keys: reordered[key] = old[key]
|
|
candidate[&"systems"][&"runtime"][&"relationships"] = reordered
|
|
"reencode":
|
|
var reencoded := {}
|
|
for key: Variant in old: reencoded[String(key)] = old[key]
|
|
candidate[&"systems"][&"runtime"][&"relationships"] = reencoded
|
|
var before_candidate := var_to_bytes(candidate); var before_observed := var_to_bytes(observed)
|
|
var encoding_control := true
|
|
if kind in ["reorder", "reencode"]:
|
|
encoding_control = candidate == writer and before_candidate != writer_bytes
|
|
var result := _legacy_checkpoint(candidate, observed)
|
|
expect(encoding_control and result["ok"] == (kind == "copy") and var_to_bytes(writer) == writer_bytes and var_to_bytes(reader) == reader_bytes and var_to_bytes(candidate) == before_candidate and var_to_bytes(observed) == before_observed, "legacy migration control immutable " + label)
|
|
|
|
func _legacy_controls(writer: Dictionary, reader: Dictionary) -> void:
|
|
_legacy_control(writer, reader, "outside", "outside")
|
|
for field: StringName in [&"version", &"relationships", &"threshold_candidates", &"next_candidate_sequence"]:
|
|
_legacy_control(writer, reader, "delete " + String(field), "delete", field)
|
|
_legacy_control(writer, reader, "change " + String(field), "change", field, null)
|
|
_legacy_control(writer, reader, "sequence +1", "sequence")
|
|
_legacy_control(writer, reader, "nonempty originals", "originals")
|
|
for version: Variant in [0, 3, "2"]:
|
|
_legacy_control(writer, reader, "version " + str(version), "version", &"", version)
|
|
_legacy_control(writer, reader, "equal reordered keys", "reorder")
|
|
_legacy_control(writer, reader, "equal String reencoding", "reencode")
|
|
_legacy_control(writer, reader, "byte-identical deep copy", "copy")
|
|
_proof("legacy14-migration-controls", {"attempts": 17, "writer_sha256": _typed_sha(writer), "reader_sha256": _typed_sha(reader), "failures": failures})
|
|
|
|
func _compare_loaded(writer: Dictionary, write_path: String, read_path: String, label: String, migrate_relationships_v1: bool = false) -> void:
|
|
var reader: Dictionary = runtime.create_snapshot()
|
|
var old: Dictionary = writer[&"systems"][&"runtime"][&"domain_events"]["recent_events"].back().duplicate(true)
|
|
var actual: Dictionary = reader[&"systems"][&"runtime"][&"domain_events"]["recent_events"].back().duplicate(true)
|
|
var envelope := old.duplicate(true); envelope["event_type"] = actual.get("event_type"); envelope["payload"] = actual.get("payload")
|
|
var valid: bool = _io_valid(old, "SAVE_CHECKPOINT_SUCCEEDED", &"save", write_path) and _io_valid(actual, "SAVE_LOADED", &"load", read_path) and old["payload"].get(&"save_id") == actual["payload"].get(&"save_id") and var_to_bytes(envelope) == var_to_bytes(actual)
|
|
var expected := writer.duplicate(true); var replaced := 0
|
|
for event: Dictionary in expected[&"systems"][&"runtime"][&"domain_events"]["recent_events"]:
|
|
if event.get("event_id") == old.get("event_id"):
|
|
valid = valid and var_to_bytes(event) == var_to_bytes(old)
|
|
event["event_type"] = actual["event_type"]; event["payload"] = actual["payload"].duplicate(true); replaced += 1
|
|
var migration_proof := {}
|
|
if migrate_relationships_v1:
|
|
_legacy_controls(expected, reader)
|
|
var migration := _legacy_checkpoint(expected, reader)
|
|
valid = valid and migration["ok"]
|
|
if migration["ok"]:
|
|
expected = migration["expected"]
|
|
migration_proof = migration["proof"]
|
|
migration_proof["write_checkpoint_sha256"] = FileAccess.get_sha256(LEGACY.path_join("write-checkpoint.var"))
|
|
valid = valid and replaced == 1 and expected[&"systems"][&"runtime"][&"domain_events"]["published_ids"].has(actual.get("event_id")) and var_to_bytes(expected) == var_to_bytes(reader) and expected == reader and SimulationDigest.compute(expected) == SimulationDigest.compute(reader)
|
|
_proof(label + "-expected-io1", expected); _proof(label + "-actual", reader)
|
|
var binding := {"whole_typed_bytes_equal": valid, "writer_io": old, "reader_io": actual, "mapped_fields": ["event_type", "payload"], "event_deletion_count": 0, "write_path": write_path, "read_path": read_path, "copied_legacy_package": write_path != read_path}
|
|
if migrate_relationships_v1:
|
|
binding["mapped_fields"].append("relationships_v1_to_v2")
|
|
binding["relationships_v1_to_v2"] = migration_proof
|
|
_proof(label + "-io1-binding", binding)
|
|
expect(valid, "whole typed loaded state unchanged except complete identity/saveID-bound IO1: " + label)
|
|
|
|
func _new_saved_boundary() -> void:
|
|
var path := base_slot.path_join("new14")
|
|
checkpoint_minute = clock.absolute_minute
|
|
expect(runtime.save_to_path(path).get(&"ok", false), "normal strict14 persistent-pause Save")
|
|
var expected: Dictionary = runtime.create_snapshot(); var hashes := _package(path)
|
|
_proof("new14-checkpoint", expected)
|
|
_cleanup()
|
|
if not _setup(): return
|
|
expect(runtime.load_from_path(path).get(&"ok", false), "normal strict14 persistent-pause Load into fresh live authorities")
|
|
_listen(true)
|
|
_compare_loaded(expected, path, path, "new14-load")
|
|
expect(_package(path) == hashes and hashes.size() == 4, "new14 Load preserves every formal package file SHA")
|
|
_paused(6, "immediate restored persistent pause")
|
|
clock.advance_one_minute(); _paused(6, "first restored tick cannot resume destroyed dependency")
|
|
await _ui()
|
|
await _reject_remote_kitchen()
|
|
if failures: return
|
|
await _rebuild_behind_kitchen()
|
|
if failures: return
|
|
await _select(ids["replacement"])
|
|
clock.advance_one_minute()
|
|
var service: ServiceInstance = runtime.facility_service.get_service(ids["service"])
|
|
expect(runtime.facility_service.get_instance(ids["cafe"]).related_facility_id == ids["replacement"] and service.state == ServiceInstance.State.ACTIVE and service.elapsed_minutes == 7, "explicit valid rebuilt B relink alone resumes exact progress")
|
|
clock.advance_minutes(38)
|
|
expect(service.state == ServiceInstance.State.COMPLETED and service.elapsed_minutes == 45 and runtime.economy_service.get_balance(&"GUEST_WALLET:kitchen_payer") == 75, "real45th meal minute charges exact25G once")
|
|
clock.advance_minutes(5)
|
|
var sales := 0
|
|
for entry: LedgerEntry in runtime.economy_service.get_entries():
|
|
if entry.category == &"SERVICE_SALE" and entry.source_entity_id == ids["cafe"]: sales += 1
|
|
expect(sales == 1 and runtime.economy_service.get_balance(&"GUEST_WALLET:kitchen_payer") == 75 and runtime.economy_service.validate_ledger().get(&"ok", false), "tail5 preserves single real balanced cafe sale")
|
|
_proof("new14-final-tail5", runtime.create_snapshot())
|
|
|
|
func _reject_remote_kitchen() -> void:
|
|
var before: Dictionary = runtime.create_snapshot()
|
|
var placement: Dictionary = runtime.facility_service.get_kitchen_placement_report(ids["cafe"], ids["b"])
|
|
expect(placement.get(&"valid_kind", false) and not placement.get(&"valid_placement", true) and placement.get(&"reason") == &"INVALID_RELATED_KITCHEN_PLACEMENT" and var_to_bytes(before) == var_to_bytes(runtime.create_snapshot()), "remote BACK report keeps every Runtime byte and exposes XY rejection")
|
|
# Public FacilityService boundary proves all Runtime typed state retention.
|
|
# The separate UI command below legitimately advances command IDs/journal.
|
|
var direct: CommandResult = runtime.facility_service.set_operational_status(ids["cafe"], {&"related_facility_id": ids["b"]})
|
|
var handler_after: Dictionary = runtime.create_snapshot()
|
|
expect(not direct.is_success() and direct.details.get(&"reason") == &"INVALID_RELATED_KITCHEN_PLACEMENT" and var_to_bytes(before) == var_to_bytes(handler_after), "public service remote refusal retains literal whole Runtime typed state")
|
|
_proof("remote-refusal-handler-before", before); _proof("remote-refusal-handler-after", handler_after)
|
|
var index := -1
|
|
for position: int in panel.kitchen_option.item_count:
|
|
if StringName(panel.kitchen_option.get_item_metadata(position)) == ids["b"]: index = position
|
|
expect(index >= 0 and "裏に収まりません" in panel.kitchen_option.get_item_text(index), "normal selector identifies remote kitchen position before applying")
|
|
if index < 0: return
|
|
var journal_before_size: int = journals.back()["events"].size()
|
|
await _select(ids["b"])
|
|
var after: Dictionary = runtime.create_snapshot()
|
|
var kept := true
|
|
for key: StringName in [&"facilities", &"inventory", &"economy"]:
|
|
kept = kept and var_to_bytes(before[&"systems"][&"runtime"][key]) == var_to_bytes(after[&"systems"][&"runtime"][key])
|
|
expect(kept and runtime.facility_service.get_instance(ids["cafe"]).related_facility_id == ids["a"] and "裏に収まりません" in panel.result_label.text, "ordinary remote apply rejects with reason and retains handler-owned facilities/stock/ledger/link; full legitimate outer journal remains")
|
|
var failed_event: Dictionary = after[&"systems"][&"runtime"][&"domain_events"]["recent_events"].back()
|
|
var result: CommandResult = runtime.command_queue.get_processed_result(int(failed_event.get("causation_command_id", 0)))
|
|
# Runtime constructs entities from returned result.target_id; this canonical
|
|
# refused set_facility_conditions result intentionally retains empty target_id.
|
|
var expected_entity_ids: Array[String] = []
|
|
if result != null and not result.target_id.is_empty(): expected_entity_ids.append(String(result.target_id))
|
|
expect(failed_event.get("event_type") == "COMMAND_FAILED" and failed_event.get("source_service") == "simulation_runtime" and var_to_bytes(failed_event.get("entity_ids")) == var_to_bytes(expected_entity_ids) and result != null and result.target_id.is_empty() and result.command_id > 0 and result.command_id == int(failed_event.get("causation_command_id", 0)) and result != null and not result.is_success() and result.details.get(&"reason") == &"INVALID_RELATED_KITCHEN_PLACEMENT" and var_to_bytes(failed_event.get("payload")) == var_to_bytes(result.to_dictionary()) and journals.back()["events"].size() == journal_before_size + 1 and var_to_bytes(journals.back()["events"].back()) == var_to_bytes(failed_event), "full legitimate public COMMAND_FAILED/ID/result envelope remains exactly once in uncapped journal")
|
|
_proof("remote-refusal-ui-before", before); _proof("remote-refusal-ui-after", after)
|
|
_proof("remote-refusal-ui-uncapped", journals)
|
|
_paused(6, "remote link refused")
|
|
|
|
func _rebuild_behind_kitchen() -> void:
|
|
var old: FacilityInstance = runtime.facility_service.get_instance(ids["a"])
|
|
var origin: Vector3i = old.origin
|
|
var definition: FacilityDefinition = runtime.definition_registry.get_facility(&"kitchen")
|
|
var before: Dictionary = runtime.create_snapshot()
|
|
expect(_command(&"set_staff_work_assignment", {&"staff_id": &"staff_chika", &"facility_id": &"", &"template_id": &"DAY"}).is_success(), "publicly detach actual Chika DAY assignment before demolition")
|
|
expect(String(runtime.staff_service.get_work_assignment(&"staff_chika").get(&"facility_id", "")).is_empty() and runtime.facility_service.get_instance(ids["a"]).active_staff_ids.is_empty(), "actual staff assignment and projected kitchen attendance are cleared")
|
|
if failures: return
|
|
expect(_command(&"demolish_facility", {&"instance_id": ids["a"]}, ids["a"]).is_success(), "public demolition commits real destroyed A occupancy/material refund")
|
|
expect(runtime.facility_service.get_instance(ids["a"]) == null and runtime.facility_service.get_instance(ids["cafe"]).related_facility_id == ids["a"], "demolition keeps explicit missing related ID without automatic relink")
|
|
_proof("before-public-A-demolition", before); _proof("after-public-A-demolition", runtime.create_snapshot())
|
|
if failures: return
|
|
var cash: int = runtime.economy_service.get_balance(&"PLAYER_CASH")
|
|
var materials: int = state.get_resource(&"materials")
|
|
var started: CommandResult = _command(&"build_facility", {&"definition_id": &"kitchen", &"origin": origin})
|
|
expect(started.is_success(), "public paid replacement B builds at old A exact behind footprint")
|
|
expect(runtime.economy_service.get_balance(&"PLAYER_CASH") == cash - int(definition.construction_cost.get(&"gold", 0)) and state.get_resource(&"materials") == materials - int(definition.construction_cost.get(&"materials", 0)), "replacement pays unchanged original definition gold/material cost without free funding")
|
|
if failures: return
|
|
clock.advance_minutes(definition.build_minutes)
|
|
_paused(6, "same meal stays paused through full original replacement construction time")
|
|
var replacement: FacilityInstance
|
|
for facility: FacilityInstance in runtime.facility_service.get_instances():
|
|
if facility.definition_id == &"kitchen" and facility.origin == origin: replacement = facility
|
|
expect(replacement != null and replacement.instance_id != ids["a"] and replacement.instance_id != ids["b"], "real completed replacement owns distinct new ID at exact same XY/floor/BACK")
|
|
if replacement == null: return
|
|
ids["replacement"] = replacement.instance_id
|
|
expect(runtime.facility_service.get_kitchen_placement_report(ids["cafe"], ids["replacement"]).get(&"valid_placement", false), "real replacement geometry is eligible without linking")
|
|
expect(_command(&"set_staff_work_assignment", {&"staff_id": &"staff_chika", &"facility_id": ids["replacement"], &"template_id": &"DAY"}, ids["replacement"]).is_success(), "public DAY reassignment of real hired Chika to replacement B")
|
|
if failures: return
|
|
# QA safety bound only; no new commute rate or instant attendance is injected.
|
|
var waited := 0
|
|
while replacement.active_staff_ids != [&"staff_chika"] and waited < 60:
|
|
clock.advance_one_minute(); waited += 1
|
|
expect(replacement.active_staff_ids == [&"staff_chika"] and runtime.staff_service.get_staff(&"staff_chika").assigned_facility_id == ids["replacement"], "actual commute and projected attendance reach replacement within disclosed QA bound")
|
|
_paused(6, "replacement staffing cannot relink missing A automatically")
|
|
_proof("replacement-before-explicit-link", runtime.create_snapshot())
|
|
_proof("replacement-build-reassign-uncapped", journals)
|
|
await _ui()
|
|
|
|
|
|
func _legacy_saved_boundary() -> void:
|
|
var provenance: Variant = JSON.parse_string(FileAccess.get_file_as_string(LEGACY.path_join("provenance.json")))
|
|
expect(provenance is Dictionary and provenance.get("genuine_prepatch_runtime14", false) and provenance.get("strict_actual_capture_passed", false), "legacy fixture has successful genuine old14 capture provenance, not synthetic state")
|
|
if not provenance is Dictionary or not provenance.get("strict_actual_capture_passed", false): return
|
|
var expected_files: Array[String] = ["formal-package/world.bin", "formal-package/manifest.json", "formal-package/journal.log", "formal-package/thumbnail.png.bytes", "write-contract.var", "write-checkpoint.var", "write-persisted-before-io.var"]
|
|
var observed_files: Array[String] = []
|
|
for entry: Dictionary in provenance.get("files", []):
|
|
observed_files.append(String(entry.get("path", "")))
|
|
if not String(entry.get("path", "")) in expected_files: expect(false, "unknown legacy fixture path refused"); return
|
|
expect(FileAccess.get_sha256(LEGACY.path_join(entry["path"])) == entry["sha256"], "every legacy fixture byte matches capture pin " + entry["path"])
|
|
expected_files.sort(); observed_files.sort()
|
|
expect(observed_files == expected_files, "legacy provenance has every exact finite fixture path once")
|
|
var contract: Variant = _read(LEGACY.path_join("write-contract.var"))
|
|
var expected: Variant = _read(LEGACY.path_join("write-checkpoint.var"))
|
|
expect(contract is Dictionary and expected is Dictionary and contract.get("source_baseline_legacy_capture", false), "genuine typed contract and whole old14 checkpoint")
|
|
if not contract is Dictionary or not expected is Dictionary: return
|
|
ids = contract["ids"]; checkpoint_minute = contract["checkpoint_minute"]
|
|
var path := base_slot.path_join("legacy14")
|
|
expect(DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(path)) == OK, "fresh user-only legacy formal destination")
|
|
for file: String in PACKAGE_FILES:
|
|
expect(_copy_fixture_file(LEGACY.path_join("formal-package").path_join(_fixture_source(file)), path.path_join(file)), "FileAccess copies genuine sealed legacy package unchanged, including packed resource input " + file)
|
|
var hashes := _package(path)
|
|
expect(hashes == _fixture_package() and hashes.size() == 4, "all four genuine copied formal SHA match before Load")
|
|
_cleanup()
|
|
if not _setup(): return
|
|
var live: Dictionary = runtime.create_snapshot()
|
|
var inspected: Dictionary = SaveService.inspect_snapshot(path)
|
|
var persisted: Variant = _read(LEGACY.path_join("write-persisted-before-io.var"))
|
|
expect(persisted is Dictionary and inspected.get(&"ok", false) and inspected.get(&"read_only", false) and var_to_bytes(inspected.get(&"snapshot", {})) == var_to_bytes(persisted) and var_to_bytes(live) == var_to_bytes(runtime.create_snapshot()) and _package(path) == hashes, "old14 inspection exact complete typed persisted state and read-only for every live byte/package")
|
|
expect(runtime.load_from_path(path).get(&"ok", false), "ordinary strict old14 formal Load, schema stays14")
|
|
_listen(true)
|
|
_compare_loaded(expected, contract["slot"], path, "legacy14-load", true)
|
|
var cafe: FacilityInstance = runtime.facility_service.get_instance(ids["cafe"])
|
|
var service: ServiceInstance = runtime.facility_service.get_service(ids["service"])
|
|
expect(cafe.related_facility_id.is_empty() and service.state == ServiceInstance.State.ACTIVE and service.elapsed_minutes == 5 and cafe.state == FacilityInstance.State.OPEN and not cafe.related_facility_operating and cafe.operation_state == FacilityInstance.OperationState.INACTIVE, "old raw OPEN/ACTIVE5 remains serialized immediately; derived missing kitchen false")
|
|
var before: Dictionary = runtime.create_snapshot()
|
|
await _ui()
|
|
var report: Dictionary = runtime.get_business_management_report()
|
|
expect("関連厨房の稼働待ち" in panel.facility_status_label.text and "厨房" in panel.kitchen_status_label.text and report.has(&"kitchens") and var_to_bytes(before) == var_to_bytes(runtime.create_snapshot()), "read model/UI expose kitchen wait without rewriting a single old typed byte")
|
|
_proof("legacy14-read-only-before-first-tick", runtime.create_snapshot())
|
|
clock.advance_one_minute(); _paused(5, "old14 first actual tick, separate from read-only Load")
|
|
var request: ServiceRequest = runtime.facility_service.get_request(&"kitchen_once")
|
|
expect(service.service_id == ids["service"] and service.request_id == request.request_id and request.request_id == &"kitchen_once" and request.facility_id == ids["cafe"] and cafe.related_facility_id.is_empty() and _package(path) == hashes, "firsttick keeps original service/request IDs, explicit-empty link and formal bytes")
|
|
_proof("legacy14-first-tick-paused", runtime.create_snapshot())
|
|
await _select(ids["a"]); clock.advance_one_minute()
|
|
expect(service.state == ServiceInstance.State.ACTIVE and service.elapsed_minutes == 6 and cafe.related_facility_id == ids["a"], "old service resumes only on explicit valid kitchen selection and real tick")
|
|
_proof("legacy14-explicit-relinked-next1", runtime.create_snapshot())
|
|
|
|
func run() -> void:
|
|
root.get_node("GameClock").set_paused(true)
|
|
if not _guard(): push_error("Required kitchen exact mode/profile/fresh-user guards failed"); quit(91); return
|
|
if not _setup(): finish(); return
|
|
await _prepare()
|
|
if failures == 0: await _new_saved_boundary()
|
|
if failures == 0: await _legacy_saved_boundary()
|
|
finish()
|
|
|
|
func _cleanup() -> void:
|
|
if panel != null: panel.free()
|
|
if runtime != null: runtime.free()
|
|
if clock != null: clock.free()
|
|
if state != null: state.free()
|
|
expect(not is_instance_valid(panel) and not is_instance_valid(runtime) and not is_instance_valid(clock) and not is_instance_valid(state), "all owned UI/runtime/clock/state Nodes actually freed")
|
|
panel = null; runtime = null; clock = null; state = null
|
|
|
|
func finish() -> void:
|
|
_proof("uncapped-domain-journals", journals)
|
|
_cleanup()
|
|
_journal_stream = null
|
|
_proof("result", {"checks": checks + 1, "failures": failures, "same_process_standard_regression": true, "different_pid_restart_in_this_test": false, "natural_guests_or_attack_accepted": false, "whole_game_accepted": false})
|
|
print("Required kitchen: %d checks, %d failures" % [checks, failures])
|
|
quit(1 if failures else 0)
|