Files
TheTree_mock/tests/supply/test_supply_auto_purchase_destruction.gd
T

275 lines
19 KiB
GDScript

extends SceneTree
## Artifact-only acceptance input, not a new standard test or production value.
## The warehouse is normally paid/built, full durability10000, and auto6 is paid.
## A disclosed existing Defense command fixture attacks it through real automatic
## 5-minute rounds. No pre-damage, direct status/suspension, mock outcome or clock
## assignment is used. Threat10000/interval5 are existing test inputs; 201 rounds
## and1010 minutes merely allow real max50 damage/round to exhaust full10000.
## Mana0/recovery uses the public resource seam and actual Runtime projection;
## this is not a claim of natural outage/thief/bankruptcy threat generation.
const SAVE_PATH := "user://p05-enabled-auto-destruction.ttwb"
const RAID := &"p05.auto.destroy.raid"
const SOURCE_PATHS: Array[String] = [
"autoload/simulation_runtime.gd", "scripts/supply/supply_management_service.gd",
"scripts/supply/supply_auto_purchase_plans.gd", "scripts/ui/supply_management_panel.gd",
"scripts/defense/defense_service.gd", "scripts/facility/facility_instance.gd",
"scripts/facility/facility_service.gd", "scripts/mana/mana_power_projection.gd",
"scripts/core/save_service.gd", "scripts/core/simulation_snapshot.gd",
]
var runtime: Variant
var clock: Variant
var state: Variant
var warehouse: StringName = &""
var output := ""
var mode := ""
var save_path := SAVE_PATH
var checks := 0
var failures := 0
var purchase_notifications := 0
var summary: Dictionary = {}
func _initialize() -> void:
call_deferred("_run")
func expect(ok: bool, label: String) -> void:
checks += 1
if ok: print(" ✓ ", label)
else:
failures += 1
push_error("P05_AUTO_DESTRUCTION: " + label)
func _run() -> void:
var args := OS.get_cmdline_user_args()
if args.size() == 1 and args[0] == "--write": mode = "write"
elif args.size() == 1 and args[0] == "--read": mode = "read"
output = OS.get_environment("THE_TREE_AUTO_DESTRUCTION_OUTPUT")
var user_dir := OS.get_environment("THE_TREE_AUTO_PURCHASE_EXPECTED_USER_DIR")
var res_dir := OS.get_environment("THE_TREE_AUTO_PURCHASE_EXPECTED_RES_DIR")
var profile := OS.get_environment("THE_TREE_AUTO_PURCHASE_EXPECTED_PROFILE")
var any_external_guard := false
for key: String in ["THE_TREE_AUTO_DESTRUCTION_OUTPUT", "THE_TREE_AUTO_PURCHASE_EXPECTED_USER_DIR", "THE_TREE_AUTO_PURCHASE_EXPECTED_RES_DIR", "THE_TREE_AUTO_PURCHASE_EXPECTED_PROFILE"]:
any_external_guard = any_external_guard or OS.has_environment(key)
var guards := false
if any_external_guard:
guards = not mode.is_empty() and output.is_absolute_path() and DirAccess.dir_exists_absolute(output) and not FileAccess.file_exists(output.path_join(mode + "-summary.json")) and user_dir.is_absolute_path() and OS.get_user_data_dir() == user_dir and res_dir.is_absolute_path() and ProjectSettings.globalize_path("res://").simplify_path().trim_suffix("/") == res_dir.simplify_path().trim_suffix("/") and not profile.is_empty() and bool(ProjectSettings.get_setting("application/config/use_custom_user_dir", false)) and ProjectSettings.get_setting("application/config/custom_user_dir_name", "") == profile
elif args.is_empty():
mode = "standard"
var nonce := "%d_%d" % [OS.get_process_id(), Time.get_ticks_usec()]
var user_test_root := "user://tests/p05_auto_destruction_" + nonce
save_path = user_test_root.path_join("formal.ttwb")
output = ProjectSettings.globalize_path(user_test_root.path_join("proofs"))
guards = not DirAccess.dir_exists_absolute(ProjectSettings.globalize_path(user_test_root)) and DirAccess.make_dir_recursive_absolute(output) == OK
user_dir = OS.get_user_data_dir()
res_dir = ProjectSettings.globalize_path("res://")
profile = String(ProjectSettings.get_setting("application/config/custom_user_dir_name", ""))
if not guards:
push_error("P05_AUTO_DESTRUCTION exact mode/profile/resource/fresh output guards failed")
quit(91)
return
runtime = root.get_node("SimulationRuntime")
clock = root.get_node("GameClock")
state = root.get_node("GameState")
clock.set_process(false)
_reset_fresh_runtime()
summary = {"mode": mode, "actual_user_data_dir": OS.get_user_data_dir(), "actual_resource_dir": ProjectSettings.globalize_path("res://"), "actual_profile": profile, "configured_attack_fixture": true, "natural_threat_generation_acceptance": false, "production_balance_change": false, "whole_P05_acceptance": false, "source_sha256": {}}
for path: String in SOURCE_PATHS:
summary["source_sha256"][path] = FileAccess.get_sha256("res://" + path)
expect(runtime.create_snapshot()[&"systems"][&"runtime"][&"runtime_schema_version"] == 14, "exact current14 snapshot required")
if mode in ["write", "standard"]: _write_phase()
if mode == "standard" and failures == 0:
_reset_fresh_runtime()
_read_phase()
elif mode == "read": _read_phase()
summary["checks"] = checks
summary["failures"] = failures
summary["save_path"] = ProjectSettings.globalize_path(save_path)
summary["two_process_restarted_in_this_mode"] = mode == "read"
var file := FileAccess.open(output.path_join(mode + "-summary.json"), FileAccess.WRITE)
if file == null:
push_error("P05_AUTO_DESTRUCTION cannot open fresh summary")
quit(92)
return
file.store_string(JSON.stringify(summary, "\t", true, true) + "\n")
file.close()
print("P05_AUTO_DESTRUCTION phase=%s checks=%d failures=%d" % [mode, checks, failures])
print("Supply auto purchase destruction: %d checks, %d failures" % [checks, failures])
quit(1 if failures > 0 else 0)
func _reset_fresh_runtime() -> void:
clock.reset(1, 6, 0)
state.reset()
expect(runtime.initialize(clock, state, 420_506)[&"valid"], "canonical14 Runtime initializes")
clock.set_paused(true)
runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
purchase_notifications = 0
func _command(kind: StringName, payload: Dictionary) -> CommandResult:
return runtime.submit_command(kind, payload, &"player", warehouse, 75)
func _stock() -> int:
var facility: FacilityInstance = runtime.facility_service.get_instance(warehouse)
return runtime.inventory_service.total_quantity(facility.local_inventory_id, SupplyCatalog.RAW_FOOD) if facility != null else -1
func _plan() -> Dictionary:
return runtime.auto_purchase_plans.get_plan(warehouse, SupplyCatalog.RAW_FOOD)
func _auto_payment_count() -> int:
var count := 0
for entry: LedgerEntry in runtime.economy_service.get_entries():
if String(entry.memo_key).begins_with(SupplyAutoPurchasePlans.RECEIPT_PREFIX): count += 1
return count
func _observe_payment(_id: StringName, entries: Array) -> void:
for entry: LedgerEntry in entries:
if String(entry.memo_key).begins_with(SupplyAutoPurchasePlans.RECEIPT_PREFIX): purchase_notifications += 1
func _typed(label: String, value: Dictionary) -> void:
var path := output.path_join(label + ".var")
expect(not FileAccess.file_exists(path), "fresh typed proof: " + label)
if FileAccess.file_exists(path): return
var file := FileAccess.open(path, FileAccess.WRITE)
expect(file != null, "typed proof opens: " + label)
if file == null: return
file.store_buffer(var_to_bytes(value))
file.close()
func _write_phase() -> void:
var absolute_save := ProjectSettings.globalize_path(save_path)
expect(not DirAccess.dir_exists_absolute(absolute_save) and not FileAccess.file_exists(absolute_save), "fresh formal save never overwrites an existing package")
if DirAccess.dir_exists_absolute(absolute_save) or FileAccess.file_exists(absolute_save): return
expect(_command(&"build_facility", {&"definition_id": &"warehouse", &"origin": Vector3i(-8, 0, 1)}).is_success(), "normal paid warehouse construction begins")
clock.advance_minutes(180)
for facility: FacilityInstance in runtime.facility_service.get_instances():
if facility.definition_id == &"warehouse": warehouse = facility.instance_id
expect(not warehouse.is_empty(), "normal warehouse completes without synthetic Facility instance")
if warehouse.is_empty(): return
var original: FacilityInstance = runtime.facility_service.get_instance(warehouse)
expect(original.durability == FacilityInstance.DEFAULT_DURABILITY and original.durability == 10000 and original.suspension_reason.is_empty(), "paid warehouse starts at full10000 integrity without pre-damage")
runtime.economy_service.transaction_posted.connect(_observe_payment)
var cash: int = state.get_resource(&"gold")
expect(_command(&"set_supply_auto_purchase", {&"warehouse_id": warehouse, &"item_id": SupplyCatalog.RAW_FOOD, &"enabled": true, &"target_stock": 6}).is_success(), "player enables auto6 before actual combat")
clock.advance_minutes(1)
expect(_stock() == 6 and state.get_resource(&"gold") == cash - 18 and _auto_payment_count() == 1 and purchase_notifications == 1, "operating warehouse actually buys6 once for18G before attack")
var pre_combat: Dictionary = runtime.create_snapshot()
var owned_before: Dictionary = _plan()
_typed("before-configured-attack", pre_combat)
var definition := {&"source_id": &"p05.disclosed.configured_attack", &"entry_points": [&"gate.north"], &"objective_ids": [warehouse], &"round_minutes": 5, &"max_duration_minutes": 1010, &"retreat_policy": &"HOLD", &"defender_retreat_threshold": 0, &"terrain_bonus": 0, &"command_bonus": 0, &"surprise_penalty": 0, &"multiple_entry_penalty": 0, &"waves": [{&"wave_id": &"p05.auto.destroy.wave", &"threat_strength": 10000, &"max_rounds": 201, &"retreat_threshold": 0, &"entry_points": [&"gate.north"]}]}
expect(_command(&"create_defense_raid", {&"raid_id": RAID, &"definition": definition}).is_success(), "disclosed configured attack created through actual Runtime command")
expect(_command(&"register_defense_asset", {&"raid_id": RAID, &"asset_id": warehouse, &"kind": &"OBJECTIVE", &"max_integrity": FacilityInstance.DEFAULT_DURABILITY, &"defense": 0, &"options": {&"target_order": 1}}).is_success(), "real full-durability warehouse registered as actual objective")
expect(_command(&"start_defense_raid", {&"raid_id": RAID}).is_success(), "actual raid starts without injected destruction or mock outcome")
var rng_before: Dictionary = runtime.get_rng_stream(&"defense").snapshot()
clock.advance_minutes(4)
expect(runtime.defense_service.get_raid(RAID)[&"current_round"] == 0 and runtime.get_rng_stream(&"defense").snapshot() == rng_before, "deadline-minus1 does not attack or consume RNG")
clock.advance_minutes(1)
for _index: int in range(200):
var ongoing: Dictionary = runtime.defense_service.get_raid(RAID)
if ongoing[&"state"] in DefenseService.TERMINAL_STATES: break
clock.advance_minutes(5)
var raid: Dictionary = runtime.defense_service.get_raid(RAID)
var destroyed: FacilityInstance = runtime.facility_service.get_instance(warehouse)
var damage := 0
var target_matches := true
for round_value: Variant in (raid[&"resolved_rounds"] as Dictionary).values():
var round_row: Dictionary = round_value
var hit: Dictionary = round_row[&"defense_damage"]
damage += int(hit.get(&"damage", 0))
target_matches = target_matches and hit.get(&"target_type", &"") == &"ASSET" and hit.get(&"target_id", &"") == warehouse
expect(raid[&"state"] == DefenseService.RaidState.BREACHED and raid[&"current_round"] == 200 and damage == 10000 and target_matches, "200 real automatic combat rounds destroy full10000 warehouse via actual outcome boundary")
expect(destroyed.durability == 0 and destroyed.state == FacilityInstance.State.BROKEN and destroyed.suspension_reason == &"DESTROYED" and destroyed.operation_state == FacilityInstance.OperationState.SUSPENDED, "Defense terminal projection owns actual persistent DESTROYED suspension")
expect(var_to_bytes(_plan()) == var_to_bytes(owned_before) and _stock() == 6 and _auto_payment_count() == 1 and purchase_notifications == 1, "combat preserves enabled intent and its paid stock/receipt without repeat procurement")
expect(runtime.economy_service.validate_ledger()[&"ok"] and not runtime.get_operations_report()[&"tick_blocked"], "combat settles without ledger or unrelated Operations block")
# A normal post-destruction input makes a real positive deficit; no inventory
# is consumed/replaced and no destroyed/operational status is directly set.
expect(_command(&"set_supply_auto_purchase", {&"warehouse_id": warehouse, &"item_id": SupplyCatalog.RAW_FOOD, &"enabled": true, &"target_stock": 7}).is_success(), "ordinary player target7 is retained even though receiver is destroyed")
_check_blocked("post-combat", 6)
var checkpoint: Dictionary = runtime.create_snapshot()
_typed("formal-checkpoint14", checkpoint)
var saved: Dictionary = runtime.save_to_path(save_path, {&"purpose": &"p05.actual_defense_destroyed_auto"})
expect(saved.get(&"ok", false), "actual Runtime writes fresh formal current14 package")
var inspected: Dictionary = SaveService.inspect_snapshot(save_path)
expect(inspected.get(&"ok", false) and var_to_bytes(inspected.get(&"snapshot", {})) == var_to_bytes(checkpoint), "read-only formal package contains exact whole typed checkpoint14")
summary["warehouse_id"] = String(warehouse)
summary["actual_rounds"] = raid[&"current_round"]
summary["actual_damage"] = damage
summary["actual_resolved_rounds"] = (raid[&"resolved_rounds"] as Dictionary).size()
summary["before_attack_plan"] = owned_before
summary["saved_plan"] = _plan()
summary["auto_payment_count"] = _auto_payment_count()
summary["automatic_purchase_notifications"] = purchase_notifications
func _check_blocked(label: String, expected_stock: int) -> void:
var facility: FacilityInstance = runtime.facility_service.get_instance(warehouse)
var report: Dictionary = runtime.supply_management.auto_purchase_report(runtime, warehouse, SupplyCatalog.RAW_FOOD)
expect(facility != null and facility.durability == 0 and facility.suspension_reason == &"DESTROYED" and not facility.allows_logistics(), label + ": actual destroyed receiver rejects Logistics")
expect(report[&"enabled"] and report[&"target_stock"] == 7 and report[&"physical_quantity"] == expected_stock and report[&"incoming_quantity"] == 0 and report[&"deficit"] == 1 and report[&"reason"] == &"FACILITY_CLOSED", label + ": positive real shortage exposes destroyed facility block")
var before: Dictionary = runtime.create_snapshot()
expect(runtime.supply_management.advance_auto_purchases(runtime, clock.absolute_minute).is_empty() and var_to_bytes(before) == var_to_bytes(runtime.create_snapshot()), label + ": blocked procurement preserves every typed aggregate/ID/receipt/event/RNG byte")
_typed(label + "-blocked-before", before)
_typed(label + "-blocked-after", runtime.create_snapshot())
func _read_phase() -> void:
var expected_file := FileAccess.open(output.path_join("formal-checkpoint14.var"), FileAccess.READ)
var resumed_context := "new process" if mode == "read" else "fresh Runtime reinitialization"
expect(expected_file != null, resumed_context + ": retains actual whole typed checkpoint")
if expected_file == null: return
var expected_value: Variant = bytes_to_var(expected_file.get_buffer(expected_file.get_length()))
expected_file.close()
expect(expected_value is Dictionary, "prior typed checkpoint is a Dictionary")
if not expected_value is Dictionary: return
var expected: Dictionary = expected_value
var inspected: Dictionary = SaveService.inspect_snapshot(save_path)
expect(inspected.get(&"ok", false) and var_to_bytes(inspected.get(&"snapshot", {})) == var_to_bytes(expected), resumed_context + ": same exact current14 formal package")
var loaded: Dictionary = runtime.load_from_path(save_path)
expect(loaded.get(&"ok", false), resumed_context + ": ordinary Runtime load commits strict14 destroyed checkpoint")
if not loaded.get(&"ok", false): return
for facility: FacilityInstance in runtime.facility_service.get_instances():
if facility.definition_id == &"warehouse": warehouse = facility.instance_id
expect(not warehouse.is_empty(), "loaded original warehouse identity remains present")
if warehouse.is_empty(): return
var normal_loaded: Dictionary = runtime.create_snapshot()
_typed("normal-runtime-load14", normal_loaded)
# SAVE_LOADED publishes a legitimate event. Strict restore of that same
# package separately proves whole-state byte equality, without hiding it.
expect(runtime.restore_snapshot(expected) and var_to_bytes(runtime.create_snapshot()) == var_to_bytes(expected), "strict14 restore reproduces every saved typed byte; normal load event kept separately")
var plan_before: Dictionary = _plan()
var inventory_before: Dictionary = runtime.inventory_service.create_snapshot()
var economy_before: Dictionary = runtime.economy_service.create_snapshot()
runtime.economy_service.transaction_posted.connect(_observe_payment)
_check_blocked("loaded-restored", 6)
var minute_before_power: int = clock.absolute_minute
var positive_mana: int = state.get_resource(&"mana")
expect(positive_mana > 0, "pre-outage reserve is actually positive")
state.set_resource(&"mana", 0) # Explicit reserve boundary, actual power projection.
clock.advance_minutes(1)
var outage_facility: FacilityInstance = runtime.facility_service.get_instance(warehouse)
expect(not outage_facility.power_available and outage_facility.durability == 0 and outage_facility.suspension_reason == &"DESTROYED", "actual Mana gate outage changes power only; destroyed status persists")
var outage: Dictionary = runtime.create_snapshot()
_typed("actual-power-outage14", outage)
expect(runtime.restore_snapshot(outage) and var_to_bytes(runtime.create_snapshot()) == var_to_bytes(outage), "strict14 outage snapshot retains destroyed facility and owned enabled plan")
state.set_resource(&"mana", positive_mana)
clock.advance_minutes(1)
var recovered_power: FacilityInstance = runtime.facility_service.get_instance(warehouse)
expect(recovered_power.power_available and recovered_power.durability == 0 and recovered_power.suspension_reason == &"DESTROYED" and recovered_power.operation_state == FacilityInstance.OperationState.SUSPENDED, "actual power recovery cannot clear persistent destruction or resume operations")
_check_blocked("power-restored", 6)
expect(var_to_bytes(_plan()) == var_to_bytes(plan_before) and _stock() == 6 and _auto_payment_count() == 1 and purchase_notifications == 0, "restored power produces no stock/payment/notification and preserves explicit intent history")
expect(clock.absolute_minute == minute_before_power + 2, "two ordinary elapsed projection ticks are separate from no-op procurement")
# Inventory/Economy current_minute legitimately advances with the two clock
# ticks. Compare complete participant state after normalizing only this field.
var inventory_after: Dictionary = runtime.inventory_service.create_snapshot()
var economy_after: Dictionary = runtime.economy_service.create_snapshot()
inventory_after[&"current_minute"] = inventory_before[&"current_minute"]
economy_after[&"current_minute"] = economy_before[&"current_minute"]
expect(var_to_bytes(inventory_after) == var_to_bytes(inventory_before) and var_to_bytes(economy_after) == var_to_bytes(economy_before), "two public Mana projection ticks preserve all stock IDs/reservations/history and payment bytes except declared clocks")
var final: Dictionary = runtime.create_snapshot()
expect(runtime.restore_snapshot(final) and var_to_bytes(runtime.create_snapshot()) == var_to_bytes(final), "after power recovery whole14 snapshot strictly restores without implicit repair")
_typed("final14", final)
summary["warehouse_id"] = String(warehouse)
summary["same_package_two_processes"] = mode == "read"
summary["owned_plan_preserved"] = var_to_bytes(_plan()) == var_to_bytes(plan_before)
summary["actual_power_available_after_recovery"] = recovered_power.power_available
summary["persistent_suspension"] = String(recovered_power.suspension_reason)
summary["auto_payment_count"] = _auto_payment_count()
summary["automatic_purchase_notifications_after_load"] = purchase_notifications