Add supply auto-purchase and preserve save recovery state

This commit is contained in:
Takeshi
2026-10-02 07:35:03 +09:00
parent 285528b7a3
commit 39f6068461
44 changed files with 2625 additions and 61 deletions
@@ -51,6 +51,7 @@ func run() -> void:
expect(not runtime.restore_snapshot(bad) and runtime.create_snapshot()==intact, "missing live facility cleanliness rejects")
var legacy: Dictionary=intact.duplicate(true)
legacy[&"systems"][&"runtime"][&"runtime_schema_version"]=11
preload("res://tests/fixtures/pre_auto_purchase_runtime.gd").strip_empty(legacy[&"systems"][&"runtime"])
legacy[&"systems"][&"runtime"].erase(&"facility_cleanliness")
expect(runtime.restore_snapshot(legacy) and runtime.facility_cleanliness.get_facility(id)["current"]==100, "v11 initializes cleanliness without guessing past dirt")
expect(runtime.create_snapshot()[&"systems"][&"runtime"][&"weather"]==intact[&"systems"][&"runtime"][&"weather"], "v11 weather survives migration unchanged")
@@ -16,6 +16,7 @@ func run() -> void:
var original: Dictionary = runtime.create_snapshot()
var legacy := original.duplicate(true)
legacy[&"systems"][&"runtime"][&"runtime_schema_version"] = 10
preload("res://tests/fixtures/pre_auto_purchase_runtime.gd").strip_empty(legacy[&"systems"][&"runtime"])
legacy[&"systems"][&"runtime"].erase(&"weather")
legacy[&"systems"][&"runtime"].erase(&"facility_cleanliness")
expect(runtime.restore_snapshot(legacy) and runtime.weather.get_kind() == &"clear" and runtime.weather.get_minute() == 360, "v10 migrates to neutral weather at saved minute")
+8
View File
@@ -0,0 +1,8 @@
extends RefCounted
## A genuine pre-v14 fixture must have no player automatic purchase intent.
static func strip_empty(data: Dictionary) -> void:
assert(data.has(&"auto_purchase_plans"))
var plans: Dictionary = data[&"auto_purchase_plans"]
assert(plans[&"version"] == 1 and (plans[&"plans"] as Array).is_empty())
data.erase(&"auto_purchase_plans")
+1
View File
@@ -0,0 +1 @@
uid://dqjyetek8aurm
+1
View File
@@ -2,6 +2,7 @@ extends RefCounted
## Build a genuine pre-v10 fixture only when no upkeep ledger history exists.
static func strip_empty_upkeep(data: Dictionary) -> void:
preload("res://tests/fixtures/pre_auto_purchase_runtime.gd").strip_empty(data)
for entry: Dictionary in data[&"economy"][&"entries"]:
assert(String(entry.get(&"category", "")) not in ["MAINTENANCE_ACCRUAL", "MAINTENANCE_PAYMENT"])
data.erase(&"weather")
@@ -404,7 +404,7 @@ func _test_v3_migration_and_v4_tamper_guards() -> void:
_expect(int(_runtime.progression_service.metrics[&"guest_lifetime"]) == 2, "v3 migration derives lifetime Guests from active plus persistent records")
_expect(_runtime.get_mvp_guest_definition_ids().size() == 32, "v3 migration registers the current 32 catalog definitions")
var migrated: Dictionary = _runtime.create_snapshot()
_expect(int(migrated[&"systems"][&"runtime"][&"runtime_schema_version"]) == 13, "v3 migration emits canonical Runtime schema v13")
_expect(int(migrated[&"systems"][&"runtime"][&"runtime_schema_version"]) == 14, "v3 migration emits canonical Runtime schema v14")
var digest := SimulationDigest.compute(migrated)
_expect(_runtime.restore_snapshot(migrated), "canonical v9 snapshot restores")
_expect(SimulationDigest.compute(_runtime.create_snapshot()) == digest, "canonical v9 restore is exact")
@@ -1,7 +1,7 @@
extends SceneTree
const FOURTEEN_DAYS_MINUTES := 14 * SimulationKernel.MINUTES_PER_DAY
const RUNTIME_SNAPSHOT_VERSION := 13
const RUNTIME_SNAPSHOT_VERSION := 14
var _checks := 0
var _failures := 0
@@ -39,7 +39,7 @@ func _new_fixture(seed: int, enable_operations := true) -> Dictionary:
int(initial_snapshot[&"systems"][&"runtime"].get(
&"runtime_schema_version", -1
)) == RUNTIME_SNAPSHOT_VERSION,
"fixture emits Runtime snapshot schema v10"
"fixture emits Runtime snapshot schema v14"
)
if enable_operations:
var enabled: Dictionary = runtime.enable_production_operations()
@@ -58,7 +58,7 @@ func _test_disabled_compatibility_and_enable() -> void:
_expect(not disabled[&"enabled"], "Operations remain disabled for existing Runtime tests")
var disabled_snapshot: Dictionary = _runtime.create_snapshot()
var disabled_runtime: Dictionary = disabled_snapshot[&"systems"][&"runtime"]
_expect(int(disabled_runtime[&"runtime_schema_version"]) == 13, "Runtime snapshot advances to schema v13")
_expect(int(disabled_runtime[&"runtime_schema_version"]) == 14, "Runtime snapshot advances to schema v14")
_expect(
disabled_runtime[&"operations"] == {
&"wrapper_version": 1,
+4 -2
View File
@@ -25,9 +25,11 @@ func _run() -> void:
func _test_value_types() -> void:
# Escaped NUL was parsed as U+FFFD; keep identical values without import diagnostics.
# Runtime NUL-attempt coverage remains in _test_text_chunk_boundaries.
var values: Array = [
null, false, true, -9223372036854775807, 9223372036854775807,
0, 0.0, -0.0, -0.125, 1.25e100, "", "厨房\u0000🌳", &"", &"運搬",
0, 0.0, -0.0, -0.125, 1.25e100, "", "厨房" + String.chr(0xfffd) + "🌳", &"", &"運搬",
Vector2(-1.5, 2.25), Vector2i(-3, 7), Rect2(-1.5, 2.25, 3.5, 4.75),
Rect2i(-3, 7, 2, 8), Vector3(-1.5, 2.25, 3.5), Vector3i(-3, 7, 2),
Vector4(-1.5, 2.25, 3.5, 4.75), Vector4i(-3, 7, 2, 8), Color(0.25, 0.5, 0.75, 1),
@@ -53,7 +55,7 @@ func _test_dictionary_layouts() -> void:
# keep their type tags apart even when the textual schemas are identical.
for index in 60:
var row := {}
var keys: Array = ["z", "aa", "日本語", "a", "\u0000", "", "001"]
var keys: Array = ["z", "aa", "日本語", "a", String.chr(0xfffd), "", "001"]
if index % 2 == 1:
keys.reverse()
for key: String in keys:
@@ -0,0 +1,48 @@
extends SceneTree
## A corrupt transit ID must reject the entire save projection without losing
## real cabin occupants or making the infrastructure collision check fail open.
var checks := 0
var failures := 0
func _initialize() -> void:
call_deferred("run")
func expect(ok: bool, label: String) -> void:
checks += 1
if not ok:
failures += 1
push_error(label)
func run() -> void:
var path := PathService.new()
var cells: Array[Vector3i] = [Vector3i.ZERO, Vector3i(0, 1, 0)]
expect(path.rebuild_graph(cells), "build a normal graph")
expect(path.configure_elevator(&"lift", cells[0], cells[1], {&"cabin_capacity": 2}), "configure a real cabin")
for actor: StringName in [&"guest:a", &"guest:b"]:
expect(path.project_actor_position(actor, cells[0], 10), "position cabin passenger")
expect(path.enqueue_elevator_request(&"lift", {
&"request_id": StringName("ride." + String(actor)), &"unit_id": actor,
&"floor_from": 0, &"floor_to": 1, &"enqueued_at": 10,
})[&"ok"], "queue real passenger")
expect(path.dequeue_elevator_boarding(&"lift", 0, 11, 2).size() == 2, "board both passengers")
var valid: Dictionary = path.create_snapshot()
expect(valid[&"transit_actor_ids"].size() == 2, "normal save keeps both cabin occupants")
for bad_id: Variant in [123, true, 1.25, Vector3i(3, 2, 1), null]:
path._transit_actor_ids[bad_id] = true
var before := var_to_bytes(path._transit_actor_ids)
var saved: Dictionary = path.create_snapshot()
expect(saved.is_empty(), "unsupported transit ID rejects the whole projection")
expect(var_to_bytes(path._transit_actor_ids) == before, "refusal preserves all transit IDs")
expect(FacilityInfrastructureProjector._has_managed_namespace_collision(SpatialSubslotService.new(), path, {}), "consumer refuses rejected snapshot")
saved[&"caller"] = true
expect(path.create_snapshot().is_empty(), "repeat refusal returns a new empty projection")
path._transit_actor_ids.erase(bad_id)
expect(var_to_bytes(path.create_snapshot()) == var_to_bytes(valid), "correction recovers both original occupants")
var restored := PathService.new()
expect(restored.restore_snapshot(valid) and restored.rebuild_graph(cells), "save restores real passenger references")
expect(var_to_bytes(restored.create_snapshot()) == var_to_bytes(valid), "restored full save matches original")
print("Path snapshot transit guard: %d checks, %d failures" % [checks, failures])
quit(1 if failures else 0)
@@ -0,0 +1 @@
uid://cqhxb665hdby6
@@ -1,6 +1,6 @@
extends SceneTree
const RUNTIME_SNAPSHOT_VERSION := 13
const RUNTIME_SNAPSHOT_VERSION := 14
const INFRASTRUCTURE_SNAPSHOT_KEYS: Array[StringName] = [
&"excavation",
&"spatial_subslots",
@@ -186,7 +186,7 @@ func _test_runtime_surface(fixture: Dictionary) -> bool:
var snapshot: Dictionary = runtime.create_snapshot()
var runtime_data: Dictionary = snapshot[&"systems"][&"runtime"]
var current_version := int(runtime_data.get(&"runtime_schema_version", -1)) == RUNTIME_SNAPSHOT_VERSION
_expect(current_version, "Runtime emits schema v13 with infrastructure and weather state")
_expect(current_version, "Runtime emits schema v14 with infrastructure and weather state")
ready = ready and current_version
for key: StringName in INFRASTRUCTURE_SNAPSHOT_KEYS:
var valid_dictionary := runtime_data.get(key, null) is Dictionary
@@ -923,7 +923,7 @@ func _test_v5_proxy_migration_determinism() -> void:
_expect(first_runtime.restore_snapshot(legacy), "v5 proxy snapshot migrates through Runtime restore")
var first_migrated: Dictionary = first_runtime.create_snapshot()
_expect(
int(first_migrated[&"systems"][&"runtime"][&"runtime_schema_version"]) == 13,
int(first_migrated[&"systems"][&"runtime"][&"runtime_schema_version"]) == 14,
"v5 migration is immediately re-emitted as v9"
)
_expect(
+2 -2
View File
@@ -318,7 +318,7 @@ func _test_current_snapshot_crosslinks_and_legacy_v2() -> void:
_clock.advance_one_minute()
var snapshot: Dictionary = _runtime.create_snapshot()
var runtime_data: Dictionary = snapshot[&"systems"][&"runtime"]
_expect(int(runtime_data[&"runtime_schema_version"]) == 13, "Runtime snapshot advertises schema v13")
_expect(int(runtime_data[&"runtime_schema_version"]) == 14, "Runtime snapshot advertises schema v14")
_expect(not runtime_data[&"movement_credits"].is_empty(), "mid-move snapshot persists fixed-point movement credits")
var digest := SimulationDigest.compute(snapshot)
_expect(_runtime.restore_snapshot(snapshot), "valid mid-move v9 snapshot restores")
@@ -376,7 +376,7 @@ func _test_current_snapshot_crosslinks_and_legacy_v2() -> void:
_expect(_runtime.restore_snapshot(legacy_v2), "legacy Runtime v2 deterministically rebuilds Path actors and credits")
var migrated: Dictionary = _runtime.create_snapshot()
_expect(
int(migrated[&"systems"][&"runtime"][&"runtime_schema_version"]) == 13
int(migrated[&"systems"][&"runtime"][&"runtime_schema_version"]) == 14
and migrated[&"systems"][&"runtime"][&"path"][&"actor_cells"].size() == 2
and migrated[&"systems"][&"runtime"][&"movement_credits"].size() == 2
and bool(_runtime.path_service.get_cell_traffic(origin)[&"temporary_block"]),
+3 -2
View File
@@ -129,12 +129,13 @@ func _test_save_branch_provenance_corruption_and_legacy() -> void:
_expect(_submit(&"create_defense_raid", {&"raid_id": &"raid.legacy.manual", &"definition": _threat_definition(&"objective.event.legacy")}).is_success(), "real manual raid exists before legacy provenance migration")
var legacy: Dictionary = _runtime.create_snapshot()
legacy[&"systems"][&"runtime"][&"runtime_schema_version"] = 12
preload("res://tests/fixtures/pre_auto_purchase_runtime.gd").strip_empty(legacy[&"systems"][&"runtime"])
(legacy[&"systems"][&"runtime"][&"defense"][&"raids"] as Array)[0].erase(&"event_source")
var clean: Dictionary = legacy[&"systems"][&"runtime"][&"facility_cleanliness"].duplicate(true)
_expect(_runtime.restore_snapshot(legacy) and _runtime.facility_cleanliness.create_snapshot() == clean, "version-12 save keeps its required cleaning state when migrating to event schema 13")
_expect(not clean["facilities"].is_empty() and _runtime.defense_service.get_raid(&"raid.legacy.manual")[&"event_source"].is_empty(), "legacy migration preserves a nonempty cleanliness participant and old manual raid")
var migrated: Dictionary = _runtime.create_snapshot()
_expect(migrated[&"systems"][&"runtime"][&"runtime_schema_version"] == 13, "legacy12 re-emits current13 without losing required12 data")
_expect(migrated[&"systems"][&"runtime"][&"runtime_schema_version"] == 14, "legacy12 re-emits current14 without losing required12 data")
var missing_clean: Dictionary = legacy.duplicate(true)
missing_clean[&"systems"][&"runtime"].erase(&"facility_cleanliness")
_expect(not _runtime.restore_snapshot(missing_clean) and _runtime.create_snapshot() == migrated, "legacy12 missing mandatory cleanliness is rejected atomically")
@@ -143,7 +144,7 @@ func _test_save_branch_provenance_corruption_and_legacy() -> void:
_expect(not _runtime.restore_snapshot(malformed_clean) and _runtime.create_snapshot() == migrated, "legacy12 malformed mandatory cleanliness is rejected atomically")
var future: Dictionary = _runtime.create_snapshot()
var current: Dictionary = future.duplicate(true)
future[&"systems"][&"runtime"][&"runtime_schema_version"] = 14
future[&"systems"][&"runtime"][&"runtime_schema_version"] = 15
_expect(not _runtime.restore_snapshot(future) and _runtime.create_snapshot() == current, "unknown future runtime schema remains rejected")
@@ -0,0 +1,121 @@
extends SceneTree
# Genuine Runtime command fixture. No direct raid, RNG, ID or event mutation.
# JSON/old untyped-wave compatibility is a separate Defense input contract.
var checks := 0
var failures := 0
var runtime: Variant
var clock: Variant
var state: Variant
func _initialize() -> void:
call_deferred("run")
func expect(ok: bool, label: String) -> void:
checks += 1
if not ok:
failures += 1
push_error("Runtime snapshot serialization: " + label)
func run() -> void:
runtime = load("res://autoload/simulation_runtime.gd").new()
clock = load("res://autoload/game_clock.gd").new()
state = load("res://autoload/game_state.gd").new()
clock.reset(1, 6, 0)
state.reset()
var initialized: Dictionary = runtime.initialize(clock, state, 4260214)
clock.set_paused(true)
expect(initialized.get(&"valid", false), "current Runtime initializes")
if not initialized.get(&"valid", false):
finish()
return
var created: CommandResult = runtime.submit_command(&"create_defense_raid", {
&"raid_id": &"serialization.raid",
&"definition": {
&"source_id": &"serialization.disclosed_fixture",
&"entry_points": [&"gate.north"],
&"objective_ids": [&"serialization.objective"],
&"round_minutes": 5,
&"max_duration_minutes": 30,
&"retreat_policy": &"HOLD",
&"defender_retreat_threshold": 0,
&"terrain_bonus": 0,
&"command_bonus": 0,
&"surprise_penalty": 0,
&"multiple_entry_penalty": 0,
&"waves": [{&"wave_id": &"serialization.wave", &"threat_strength": 10, &"max_rounds": 6, &"retreat_threshold": 0}],
},
})
expect(created.is_success(), "ordinary Runtime command creates genuine raid and wave")
if not created.is_success():
finish()
return
var expected: Dictionary = runtime.create_snapshot()
var data := runtime_data(expected)
expect(data[&"runtime_schema_version"] == runtime.RUNTIME_SNAPSHOT_VERSION, "snapshot is the actual current schema")
var waves: Array = saved_waves(expected)
expect(waves.is_typed() and waves.get_typed_builtin() == TYPE_DICTIONARY, "genuine created waves are Array[Dictionary]")
expect(waves.size() == 1 and waves[0][&"wave_id"] == &"serialization.wave", "ordinary source wave identity and order are explicit")
var rng_keys: Array[String] = []
var all_rng_keys_are_strings := true
for key: Variant in (data[&"rng_streams"] as Dictionary).keys():
all_rng_keys_are_strings = all_rng_keys_are_strings and typeof(key) == TYPE_STRING
rng_keys.append(String(key))
var lexical_keys := rng_keys.duplicate()
lexical_keys.sort()
expect(all_rng_keys_are_strings and rng_keys == lexical_keys, "RNG snapshot String keys are lexically ordered")
var expected_bytes := var_to_bytes(expected)
for iteration: int in 3:
expect(runtime.restore_snapshot(expected), "same genuine checkpoint restore%d succeeds" % iteration)
var after: Dictionary = runtime.create_snapshot()
var after_data := runtime_data(after)
var after_waves: Array = saved_waves(after)
expect(after_waves.is_typed() and after_waves.get_typed_builtin() == TYPE_DICTIONARY, "restore%d retains wave container declaration" % iteration)
expect(var_to_bytes(after_waves) == var_to_bytes(waves), "restore%d retains all wave values and element order" % iteration)
expect(var_to_bytes(after_data[&"rng_streams"]) == var_to_bytes(data[&"rng_streams"]), "restore%d retains every RNG value and lexical key order" % iteration)
expect(var_to_bytes(after_data[&"id_generator"]) == var_to_bytes(data[&"id_generator"]) and var_to_bytes(after_data[&"domain_events"]) == var_to_bytes(data[&"domain_events"]), "restore%d retains all IDs and domain event sequence" % iteration)
expect(var_to_bytes(after) == expected_bytes, "restore%d is exact whole typed binary state" % iteration)
# Older Defense restore paths exported identical wave values with an untyped
# container. Such input stays loadable and re-exports the normal canonical type.
var legacy := expected.duplicate(true)
var untyped_waves: Array = []
for wave: Dictionary in waves:
untyped_waves.append(wave.duplicate(true))
runtime_data(legacy)[&"defense"][&"raids"][0][&"waves"] = untyped_waves
expect(not untyped_waves.is_typed() and var_to_bytes(legacy) != expected_bytes, "old untyped-wave input is an actual typed-byte difference")
expect(runtime.restore_snapshot(legacy), "old untyped-wave Runtime snapshot remains accepted")
var canonical: Dictionary = runtime.create_snapshot()
var canonical_waves: Array = saved_waves(canonical)
expect(canonical_waves.is_typed() and canonical_waves.get_typed_builtin() == TYPE_DICTIONARY, "old input exports canonical Dictionary wave type")
expect(var_to_bytes(canonical_waves) == var_to_bytes(waves), "old input preserves wave values and element order")
expect(var_to_bytes(canonical) == expected_bytes, "old input changes only missing wave declaration and reproduces genuine whole canonical state")
var defense_snapshot: Dictionary = runtime.defense_service.create_snapshot()
var json_input: Variant = JSON.parse_string(JSON.stringify(defense_snapshot))
var json_restored := DefenseService.new()
expect(json_input is Dictionary and json_restored.restore_snapshot(json_input), "existing JSON Defense transport remains accepted")
var json_output: Dictionary = json_restored.create_snapshot()
var json_waves: Array = json_output[&"raids"][0][&"waves"]
expect(json_waves.is_typed() and json_waves.get_typed_builtin() == TYPE_DICTIONARY, "JSON waves export canonical Dictionary container type")
expect(JSON.stringify(json_output) == JSON.stringify(defense_snapshot), "JSON value/order transport contract is retained separately from typed binary transport")
var malformed: Dictionary = defense_snapshot.duplicate(true)
malformed[&"raids"][0][&"waves"] = [42]
var json_before := var_to_bytes(json_output)
expect(not json_restored.restore_snapshot(malformed) and var_to_bytes(json_restored.create_snapshot()) == json_before, "non-Dictionary wave input rejects and preserves every live typed byte")
var string_waves: Array[String] = ["malformed wave"]
malformed = defense_snapshot.duplicate(true)
malformed[&"raids"][0][&"waves"] = string_waves
expect(not json_restored.restore_snapshot(malformed) and var_to_bytes(json_restored.create_snapshot()) == json_before, "typed String waves reject without container mutation or type error")
finish()
func runtime_data(snapshot: Dictionary) -> Dictionary:
return snapshot[&"systems"][&"runtime"]
func saved_waves(snapshot: Dictionary) -> Array:
return runtime_data(snapshot)[&"defense"][&"raids"][0][&"waves"]
func finish() -> void:
if runtime != null: runtime.free()
if clock != null: clock.free()
if state != null: state.free()
print("Runtime snapshot serialization: %d checks, %d failures" % [checks, failures])
quit(failures)
@@ -0,0 +1 @@
uid://dl6brsqwn5lr8
+222
View File
@@ -0,0 +1,222 @@
extends SceneTree
var _checks := 0
var _failures := 0
var _prefix := ""
var _runtime: Variant
var _source: Dictionary
func _initialize() -> void: call_deferred("_run")
func _run() -> void:
_prefix = "user://tests/save_preflight_%d" % OS.get_process_id()
_runtime = root.get_node("SimulationRuntime")
var clock: Variant = root.get_node("GameClock")
var state: Variant = root.get_node("GameState")
clock.reset(1,6,0); state.reset(); _runtime.initialize(clock,state,42602); clock.set_paused(true)
_source = _runtime.create_snapshot()
_test_read_only_preview_and_recovery()
_test_manifest_types_and_versions()
_test_resource_limits_and_corruption()
_test_runtime_schema_rejection()
_test_envelope_rejection_and_legacy_defaults()
_measure_fixture_sizes()
print("Save preflight: %d checks, %d failures" % [_checks, _failures])
quit(_failures)
func _test_read_only_preview_and_recovery() -> void:
var path := _prefix + "/read_only"
_expect(SaveService.save_snapshot(path,_source)[&"ok"],"write valid disposable Runtime save")
var before := _slot_fingerprint(path)
var inspected := SaveService.inspect_snapshot(path)
_expect(inspected[&"ok"] and inspected[&"read_only"] and inspected[&"snapshot"] == _source,"inspection returns exact formal snapshot")
_expect(_slot_fingerprint(path) == before,"valid inspection leaves every package byte unchanged")
_write_marker(path); before = _slot_fingerprint(path)
inspected = SaveService.inspect_snapshot(path)
_expect(inspected[&"ok"] and _slot_fingerprint(path) == before,"inspection preserves stale marker and formal bytes")
var live: Dictionary = _runtime.create_snapshot()
var preview: Dictionary = _runtime.preview_facility_dimensions_at_path(path)
_expect(preview[&"ok"] and _runtime.create_snapshot() == live,"dimension preview preserves live Runtime")
_expect(_slot_fingerprint(path) == before,"dimension preview does not reconcile transaction artifacts")
_expect(SaveService.load_snapshot(path)[&"ok"] and not FileAccess.file_exists(ProjectSettings.globalize_path(path) + SaveService.SAVE_TRANSACTION_SUFFIX),"ordinary load retains interrupted-save reconciliation")
var absolute := ProjectSettings.globalize_path(path)
_expect(SaveService.save_snapshot(path+".tmp",_source)[&"ok"],"write verified interrupted candidate")
_expect(DirAccess.rename_absolute(absolute,absolute+".bak") == OK,"hold formal source at crash boundary")
_write_marker(path); before = _slot_fingerprint(path)
inspected = SaveService.inspect_snapshot(path)
_expect(not inspected[&"ok"] and inspected[&"code"] == &"SAVE_NOT_FOUND" and inspected[&"read_only"],"inspection refuses to promote missing formal")
preview = _runtime.preview_facility_dimensions_at_path(path)
_expect(not preview[&"ok"] and _runtime.create_snapshot() == live,"missing-formal preview refuses without live mutation")
_expect(_slot_fingerprint(path) == before,"rejected preview preserves candidate, backup and marker SHA")
var recovered := SaveService.load_snapshot(path)
_expect(recovered[&"ok"] and recovered[&"interrupted_save_reconciled"] and recovered[&"snapshot"] == _source,"ordinary load still promotes verified candidate")
SaveService.delete_save(path)
func _test_manifest_types_and_versions() -> void:
var path := _prefix+"/manifest"
_expect(SaveService.save_snapshot(path,_source)[&"ok"],"write manifest fixture")
var manifest_path := ProjectSettings.globalize_path(path).path_join(SaveService.MANIFEST_FILE)
var original := FileAccess.get_file_as_string(manifest_path)
for key: String in ["format_version","uncompressed_size","checksum_world"]:
var values: Array = [true,"1",1.5,null]
if key == "format_version": values.append_array([-1,4294967296.0])
if key == "uncompressed_size": values.append_array([0,-1])
if key == "checksum_world": values = [true,123,null,"bad_hash"]
for value: Variant in values:
var manifest: Dictionary = JSON.parse_string(original)
manifest[key] = value
_write_text(manifest_path,JSON.stringify(manifest))
_assert_read_rejection(path,&"INVALID_MANIFEST","malformed %s %s" % [key,str(value)])
var missing: Dictionary = JSON.parse_string(original)
missing.erase("format_version"); _write_text(manifest_path,JSON.stringify(missing))
_assert_read_rejection(path,&"INVALID_MANIFEST","missing format version")
missing = JSON.parse_string(original)
missing.erase("uncompressed_size"); _write_text(manifest_path,JSON.stringify(missing))
_expect(SaveService.inspect_snapshot(path)[&"ok"],"legacy missing duplicate size retains bounded binary compatibility")
_write_text(manifest_path,original)
for version: int in [0,2]:
var manifest: Dictionary = JSON.parse_string(original)
manifest["format_version"] = version; _write_text(manifest_path,JSON.stringify(manifest))
var world := FileAccess.open(ProjectSettings.globalize_path(path).path_join(SaveService.WORLD_FILE),FileAccess.READ_WRITE)
world.seek(4); world.store_32(version); world = null
_assert_read_rejection(path,&"UNSUPPORTED_FORMAT","old or unknown package version %d" % version)
var before := _slot_fingerprint(path)
var migration := SaveService.migrate_save(path,SaveMigrationRegistry.new())
_expect(not migration[&"ok"] and _slot_fingerprint(path) == before,"unregistered old migration or future downgrade preserves source")
SaveService.delete_save(path)
func _test_resource_limits_and_corruption() -> void:
var path := _prefix+"/limits"
_expect(SaveService.save_snapshot(path,_source)[&"ok"],"write resource boundary fixture")
var absolute := ProjectSettings.globalize_path(path)
var world_path := absolute.path_join(SaveService.WORLD_FILE)
var manifest_path := absolute.path_join(SaveService.MANIFEST_FILE)
var original_world := FileAccess.get_file_as_bytes(world_path)
var original_manifest := FileAccess.get_file_as_string(manifest_path)
for size: int in [0,-1,SaveService.MAX_WORLD_BYTES,SaveService.MAX_WORLD_BYTES+1]:
_write_bytes(world_path,original_world)
var world := FileAccess.open(world_path,FileAccess.READ_WRITE)
world.seek(8); world.store_64(size); world = null
# The inclusive limit reaches the independent manifest mismatch before
# decompression; over-limit values reject before any expansion allocation.
var code: StringName = &"SIZE_MISMATCH" if size == SaveService.MAX_WORLD_BYTES else &"SAVE_SIZE_LIMIT_EXCEEDED"
_assert_read_rejection(path,code,"advertised expansion boundary %d" % size)
_write_bytes(world_path,original_world)
var world := FileAccess.open(world_path,FileAccess.READ_WRITE)
world.seek(SaveService.MAX_WORLD_BYTES+16); world.store_8(0); world = null
_assert_read_rejection(path,&"SAVE_SIZE_LIMIT_EXCEEDED","oversized compressed file rejected before bulk read")
_write_bytes(world_path,original_world)
_write_text(manifest_path," ".repeat(SaveService.MAX_MANIFEST_BYTES+1))
_assert_read_rejection(path,&"SAVE_SIZE_LIMIT_EXCEEDED","oversized manifest rejected before text read")
_write_text(manifest_path,"{invalid JSON")
_assert_read_rejection(path,&"INVALID_MANIFEST","malformed JSON")
_write_text(manifest_path,original_manifest)
var corrupt := original_world.duplicate()
corrupt[corrupt.size()-1] = corrupt[corrupt.size()-1] ^ 0xff
_write_bytes(world_path,corrupt); _assert_read_rejection(path,&"CHECKSUM_MISMATCH","corrupt world checksum")
_write_bytes(world_path,original_world.slice(0,15)); _assert_read_rejection(path,&"INVALID_WORLD_HEADER","truncated binary header")
_write_bytes(world_path,original_world)
var before := _slot_fingerprint(path)
var too_large := SaveService.save_snapshot(path,_source,{&"custom_note":"x".repeat(SaveService.MAX_MANIFEST_BYTES)})
_expect(not too_large[&"ok"] and too_large[&"code"] == &"SAVE_SIZE_LIMIT_EXCEEDED","writer refuses manifest beyond reader resource limit")
_expect(_slot_fingerprint(path) == before,"oversized save preserves formal and backup SHA")
SaveService.delete_save(path)
func _test_runtime_schema_rejection() -> void:
for mutation: String in ["unknown","missing","string","extra"]:
var candidate := _source.duplicate(true)
var data: Dictionary = candidate[&"systems"][&"runtime"]
match mutation:
"unknown": data[&"runtime_schema_version"] = 999
"missing": data.erase(&"facilities")
"string": data[&"runtime_schema_version"] = "13"
"extra": data[&"unknown_feature"] = {}
var original := candidate.duplicate(true)
var live: Dictionary = _runtime.create_snapshot()
_expect(not _runtime.restore_snapshot(candidate),"Runtime rejects %s current-schema payload" % mutation)
_expect(candidate == original and _runtime.create_snapshot() == live,"Runtime rejection preserves source and live state for %s" % mutation)
func _measure_fixture_sizes() -> void:
var raw := var_to_bytes(_source)
var world := WorldService.new()
var harness := preload("res://tests/performance/perf_mvp_001_harness.gd").new()
var cells: Array[Vector3i] = harness._populate_world(world,Vector2i(200,50))
var world_raw := var_to_bytes(world.create_snapshot())
_expect(cells.size() == 20000,"resource sizing uses existing PERF 20000-cell world fixture")
print("SAVE_RESOURCE_SIZE_REPORT=%s" % JSON.stringify({
"starter_runtime_raw_bytes":raw.size(),"starter_runtime_compressed_bytes":raw.compress(FileAccess.COMPRESSION_ZSTD).size(),
"world_cells":cells.size(),"world_only_raw_bytes":world_raw.size(),"world_only_compressed_bytes":world_raw.compress(FileAccess.COMPRESSION_ZSTD).size(),
"world_limit_bytes":SaveService.MAX_WORLD_BYTES,"manifest_limit_bytes":SaveService.MAX_MANIFEST_BYTES,"full_runtime_20000_cell_acceptance":false,
}))
func _test_envelope_rejection_and_legacy_defaults() -> void:
var clock: Variant = root.get_node("GameClock")
var state: Variant = root.get_node("GameState")
var source := SimulationSnapshot.capture(clock,state)
for row: Array in [
[&"schema_version","1"],[&"schema_version",true],[&"schema_version",1.5],
[&"absolute_minute","360"],[&"absolute_minute",-1],[&"absolute_minute",360.5],
[&"speed",true],[&"speed",1.5],[&"speed",0],[&"paused","false"],
[&"snapshot_version",-1],[&"snapshot_version","0"],[&"snapshot_version",1.5],
]:
var candidate := source.duplicate(true)
if row[0] == &"schema_version": candidate[row[0]] = row[1]
else: candidate[&"clock"][row[0]] = row[1]
var original := candidate.duplicate(true)
var live: Dictionary = _runtime.create_snapshot()
_expect(not SimulationSnapshot.validate(candidate)[&"ok"],"envelope rejects %s = %s" % [row[0],str(row[1])])
_expect(not SimulationSnapshot.restore(candidate,clock,state) and not _runtime.restore_snapshot(candidate),"invalid envelope refuses both restore entrypoints")
_expect(candidate == original and _runtime.create_snapshot() == live,"envelope rejection preserves source and complete live Runtime")
var json_numeric := source.duplicate(true)
json_numeric[&"schema_version"] = 1.0
json_numeric[&"clock"][&"absolute_minute"] = 360.0
json_numeric[&"clock"][&"speed"] = 1.0
json_numeric[&"clock"][&"snapshot_version"] = 0.0
_expect(SimulationSnapshot.validate(json_numeric)[&"ok"],"exact integral JSON numbers remain valid generic envelopes")
var legacy := source.duplicate(true)
legacy[&"clock"].erase(&"paused"); legacy[&"clock"].erase(&"snapshot_version")
_expect(SimulationSnapshot.validate(legacy)[&"ok"],"absent legacy pause/version retain compatible defaults")
legacy[&"clock"][&"speed"] = 4
_expect(SimulationSnapshot.restore(legacy,clock,state) and clock.speed == 3,"legacy x4 still resumes at x3 through validated envelope")
_expect(_runtime.restore_snapshot(_source),"restore original live state after legacy compatibility probe")
func _assert_read_rejection(path: String,code: StringName,label: String) -> void:
var before := _slot_fingerprint(path)
var inspected := SaveService.inspect_snapshot(path)
_expect(not inspected[&"ok"] and inspected[&"code"] == code,label+": correct rejection")
_expect(_slot_fingerprint(path) == before,label+": all disk SHA unchanged")
func _slot_fingerprint(path: String) -> Dictionary:
var absolute := ProjectSettings.globalize_path(path)
var parent := DirAccess.open(absolute.get_base_dir())
var result := {}
if parent == null: return result
for file: String in parent.get_files():
if file.begins_with(absolute.get_file()): result[file] = FileAccess.get_sha256(absolute.get_base_dir().path_join(file))
for directory: String in parent.get_directories():
if directory.begins_with(absolute.get_file()): result[directory+"/"] = _tree_fingerprint(absolute.get_base_dir().path_join(directory))
return result
func _tree_fingerprint(path: String) -> Dictionary:
var dir := DirAccess.open(path)
var result := {}
for file: String in dir.get_files(): result[file] = FileAccess.get_sha256(path.path_join(file))
for child: String in dir.get_directories(): result[child+"/"] = _tree_fingerprint(path.path_join(child))
return result
func _write_marker(path: String) -> void:
_write_text(ProjectSettings.globalize_path(path)+SaveService.SAVE_TRANSACTION_SUFFIX,SaveService.SAVE_TRANSACTION_MARKER)
func _write_text(path: String,value: String) -> void:
var file := FileAccess.open(path,FileAccess.WRITE)
file.store_string(value)
func _write_bytes(path: String,value: PackedByteArray) -> void:
var file := FileAccess.open(path,FileAccess.WRITE)
file.store_buffer(value)
func _expect(ok: bool,label: String) -> void:
_checks += 1
if not ok:
_failures += 1
push_error(label)
+1
View File
@@ -0,0 +1 @@
uid://bn6w8j53gr8ql
+145
View File
@@ -0,0 +1,145 @@
extends SceneTree
# Candidate acceptance/reproduction driver. Disposable user:// fixtures only.
# On the unpatched base, EXPECT failures expose the two preservation defects.
var _checks := 0
var _failures := 0
var _source: Dictionary
var _prefix := ""
func _initialize() -> void: call_deferred("_run")
func _run() -> void:
_prefix = "user://tests/recovery_readonly_%d" % OS.get_process_id()
var clock: Variant = root.get_node("GameClock")
var state: Variant = root.get_node("GameState")
clock.reset(1,6,0); clock.set_paused(true); state.reset()
_source = SimulationSnapshot.capture(clock,state)
_test_explicit_read_only()
_test_identified_unknown_preservation()
_test_current_corruption_recovery()
_test_registered_legacy_migration()
print("Recovery read-only candidate: %d checks, %d failures" % [_checks,_failures])
quit(_failures)
func _test_explicit_read_only() -> void:
for formal_present: bool in [true,false]:
var path := _prefix + "/explicit_%s" % str(formal_present)
_setup(path)
if not formal_present: _remove_tree(ProjectSettings.globalize_path(path))
var before := _fingerprint(path)
var result := SaveService.load_recovery_candidate(path,&"backup")
_expect(result[&"ok"] and result[&"read_only"] and result[&"snapshot"] == _source,"explicit backup is readable with/without formal")
_expect(_fingerprint(path)==before,"explicit backup selection never promotes, removes or migrates artifacts")
var normal := SaveService.load_snapshot(path)
_expect(normal[&"ok"],"ordinary load still reconciles stale marker or missing formal")
_expect(not FileAccess.file_exists(ProjectSettings.globalize_path(path)+SaveService.SAVE_TRANSACTION_SUFFIX),"ordinary load owns marker cleanup")
_cleanup(path)
func _test_identified_unknown_preservation() -> void:
var operations := ["load_snapshot","load_with_recovery","inspect_recovery_candidates","migrate_save","load_recovery_candidate","save_snapshot","replace_snapshot_with_backup","inspect_snapshot"]
for variant: String in ["matching_future","corrupt_future","header_only_future","manifest_only_future","unsupported_legacy"]:
for operation: String in operations:
var path := _prefix + "/%s_%s" % [variant,operation]
_setup(path)
var version := 0 if variant=="unsupported_legacy" else 2
_set_version(path,version,variant!="header_only_future",variant!="manifest_only_future")
if variant=="header_only_future": _write_text(ProjectSettings.globalize_path(path).path_join(SaveService.MANIFEST_FILE),"{bad json")
if variant=="corrupt_future":
var world_path := ProjectSettings.globalize_path(path).path_join(SaveService.WORLD_FILE)
var bytes := FileAccess.get_file_as_bytes(world_path)
bytes[bytes.size()-1] = bytes[bytes.size()-1] ^ 0xff
_write_bytes(world_path,bytes)
var before := _fingerprint(path)
var result: Dictionary
match operation:
"load_snapshot": result=SaveService.load_snapshot(path)
"load_with_recovery": result=SaveService.load_with_recovery(path)
"inspect_recovery_candidates": result=SaveService.inspect_recovery_candidates(path)
"migrate_save": result=SaveService.migrate_save(path,SaveMigrationRegistry.new())
"load_recovery_candidate": result=SaveService.load_recovery_candidate(path,&"backup")
"save_snapshot": result=SaveService.save_snapshot(path,_source)
"replace_snapshot_with_backup":
var candidate := _source.duplicate(true)
candidate[&"clock"][&"absolute_minute"] += 1
result=SaveService.replace_snapshot_with_backup(path,_source,candidate)
"inspect_snapshot": result=SaveService.inspect_snapshot(path)
_expect(not result[&"ok"],"%s rejects %s formal instead of silently loading old backup" % [operation,variant])
_expect(_fingerprint(path)==before,"%s preserves formal and every related artifact for %s" % [operation,variant])
_cleanup(path)
func _test_current_corruption_recovery() -> void:
var path := _prefix + "/current_corruption"
_setup(path)
var world_path := ProjectSettings.globalize_path(path).path_join(SaveService.WORLD_FILE)
var bytes := FileAccess.get_file_as_bytes(world_path);bytes[bytes.size()-1] = bytes[bytes.size()-1] ^ 0xff
_write_bytes(world_path,bytes)
var result := SaveService.load_snapshot(path)
_expect(result[&"ok"] and result[&"interrupted_save_reconciled"],"damaged recognized current formal retains authorized transaction recovery")
_expect(result[&"snapshot"] == _source,"recognized current transaction recovers exact verified snapshot")
_cleanup(path)
func _test_registered_legacy_migration() -> void:
var path := _prefix + "/registered_legacy"
_setup(path);_set_version(path,0,true,true)
var before_world := FileAccess.get_file_as_bytes(ProjectSettings.globalize_path(path).path_join(SaveService.WORLD_FILE))
var before_artifacts := _fingerprint(path)
var registry := SaveMigrationRegistry.new()
_expect(registry.register_step(0,&"registered_zero_to_one",Callable(self,"_migrate_legacy"))[&"ok"],"register explicit known legacy migration")
var result := SaveService.migrate_save(path,registry)
_expect(result[&"ok"] and result[&"backup_created"],"explicit known legacy chain remains supported")
_expect(FileAccess.get_file_as_bytes(ProjectSettings.globalize_path(path+SaveService.MIGRATION_BACKUP_SUFFIX).path_join(SaveService.WORLD_FILE))==before_world,"legacy migration preserves exact original binary backup")
_expect(SaveService.inspect_snapshot(path)[&"ok"],"migrated formal is current-loadable")
var after_artifacts := _fingerprint(path)
var owned_name := ProjectSettings.globalize_path(path).get_file()
for suffix: String in [".tmp/",".bak/",SaveService.SAVE_TRANSACTION_SUFFIX]:
_expect(after_artifacts.get(owned_name+suffix)==before_artifacts.get(owned_name+suffix),"registered legacy migration does not pre-clean stale normal artifact "+suffix)
_cleanup(path)
path = _prefix + "/registered_legacy_failure"
_setup(path);_set_version(path,0,true,true)
before_artifacts = _fingerprint(path)
registry = SaveMigrationRegistry.new()
registry.register_step(0,&"explicit_reject",Callable(self,"_reject_legacy"))
result = SaveService.migrate_save(path,registry)
_expect(not result[&"ok"] and result[&"code"]==&"FIXTURE_REJECTED","registered legacy callback failure is returned")
_expect(_fingerprint(path)==before_artifacts,"failed legacy migration preserves source and stale normal artifacts")
_cleanup(path)
func _migrate_legacy(payload: Dictionary,_from: int,_to: int) -> Dictionary:return payload
func _reject_legacy(_payload: Dictionary,_from: int,_to: int) -> Dictionary:return {&"ok":false,&"code":&"FIXTURE_REJECTED"}
func _setup(path: String) -> void:
_expect(SaveService.save_snapshot(path,_source)[&"ok"],"write disposable formal")
_expect(SaveService.save_snapshot(path+".bak",_source)[&"ok"],"write disposable verified backup")
_expect(SaveService.save_snapshot(path+".tmp",_source)[&"ok"],"write disposable verified candidate")
_write_text(ProjectSettings.globalize_path(path)+SaveService.SAVE_TRANSACTION_SUFFIX,SaveService.SAVE_TRANSACTION_MARKER)
func _set_version(path: String,version: int,manifest: bool,header: bool) -> void:
var absolute := ProjectSettings.globalize_path(path)
if manifest:
var value: Dictionary=JSON.parse_string(FileAccess.get_file_as_string(absolute.path_join(SaveService.MANIFEST_FILE)))
value["format_version"]=version;_write_text(absolute.path_join(SaveService.MANIFEST_FILE),JSON.stringify(value))
if header:
var file:=FileAccess.open(absolute.path_join(SaveService.WORLD_FILE),FileAccess.READ_WRITE)
file.seek(4);file.store_32(version)
func _fingerprint(path: String) -> Dictionary:
var absolute:=ProjectSettings.globalize_path(path);var parent:=DirAccess.open(absolute.get_base_dir());var result:={}
for file: String in parent.get_files():
if file.begins_with(absolute.get_file()):result[file]=FileAccess.get_sha256(absolute.get_base_dir().path_join(file))
for directory: String in parent.get_directories():
if directory.begins_with(absolute.get_file()):result[directory+"/"]=_tree(absolute.get_base_dir().path_join(directory))
return result
func _tree(path: String) -> Dictionary:
var dir:=DirAccess.open(path);var result:={}
for file: String in dir.get_files():result[file]=FileAccess.get_sha256(path.path_join(file))
for child: String in dir.get_directories():result[child+"/"]=_tree(path.path_join(child))
return result
func _cleanup(path: String) -> void:
var absolute:=ProjectSettings.globalize_path(path);var parent:=DirAccess.open(absolute.get_base_dir())
for file: String in parent.get_files():
if file.begins_with(absolute.get_file()):DirAccess.remove_absolute(absolute.get_base_dir().path_join(file))
for directory: String in parent.get_directories():
if directory.begins_with(absolute.get_file()):_remove_tree(absolute.get_base_dir().path_join(directory))
func _remove_tree(path: String) -> void:
var dir:=DirAccess.open(path)
if dir==null:return
for file: String in dir.get_files():DirAccess.remove_absolute(path.path_join(file))
for child: String in dir.get_directories():_remove_tree(path.path_join(child))
DirAccess.remove_absolute(path)
func _write_text(path: String,value: String) -> void:
var file:=FileAccess.open(path,FileAccess.WRITE);file.store_string(value)
func _write_bytes(path: String,value: PackedByteArray) -> void:
var file:=FileAccess.open(path,FileAccess.WRITE);file.store_buffer(value)
func _expect(ok: bool,label: String) -> void:
_checks+=1
if not ok:_failures+=1;push_error(label)
@@ -0,0 +1 @@
uid://bn6i1dsht6xtp
@@ -0,0 +1,262 @@
extends SceneTree
## A real inn from the default 5,000 G / 50 materials. No inserted guests,
## inventory, employees, completed construction, or free resource commands.
var _checks := 0
var _failures := 0
var _runtime: Variant
var _clock: Variant
var _state: Variant
var _ids: Dictionary = {}
var _saw_loaded := false
var _saw_movement := false
var _saw_batch := false
var _save_checked := false
var _last_porter_cell := Vector3i.ZERO
var _save_path := "user://tests/normal_auto_supply_%d/cargo" % OS.get_process_id()
var _main: Control
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
_runtime = root.get_node("SimulationRuntime")
_clock = root.get_node("GameClock")
_state = root.get_node("GameState")
_clock.reset(1, 6, 0)
_state.reset()
_runtime.initialize(_clock, _state, 424242)
_clock.set_paused(true)
root.size = Vector2i(1440, 810)
_main = load("res://scenes/main/main.tscn").instantiate() as Control
_main.set("manual_save_path", "user://tests/supply_auto_normal_%d/manual" % OS.get_process_id())
root.add_child(_main)
await process_frame
_runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
_expect(_state.get_resource(&"gold") == 5000 and _state.get_resource(&"materials") == 50, "ordinary starting resources")
for row: Array in [[&"reception", Vector3i(-8, 0, 0)], [&"cafeteria", Vector3i(2, 0, 0)], [&"warehouse", Vector3i(-8, 0, 1)], [&"kitchen", Vector3i(2, 0, 1)], [&"staff_door", Vector3i(0, 0, 1)]]:
if not _command(&"build_facility", {&"definition_id": row[0], &"origin": row[1]}):
_finish()
return
await _advance(180)
_command(&"build_facility", {&"definition_id": &"standard_room", &"origin": Vector3i(-8, 2, 0)})
_command(&"build_facility", {&"definition_id": &"guest_stairs", &"origin": Vector3i(-4, 1, 0)})
await _advance(60)
_command(&"build_facility", {&"definition_id": &"guest_stairs", &"origin": Vector3i(-4, 2, 0)})
await _advance(10)
for x: int in [-5, -6]:
_command(&"build_facility", {&"definition_id": &"guest_corridor", &"origin": Vector3i(x, 2, 0)})
await _advance(10)
for facility: FacilityInstance in _runtime.facility_service.get_instances():
_ids[facility.definition_id] = facility.instance_id
for assignment: Array in [[&"staff_akane", &"reception"], [&"staff_gina", &"warehouse"], [&"staff_chika", &"kitchen"], [&"staff_iris", &"cafeteria"], [&"staff_juno", &"cafeteria"]]:
if not _command(&"hire_staff_candidate", {&"candidate_id": assignment[0]}) or not _command(&"set_staff_work_assignment", {&"staff_id": assignment[0], &"facility_id": _ids[assignment[1]], &"template_id": &"DAY"}):
_finish()
return
_expect(_state.get_resource(&"materials") == 4, "all buildings and the staff door fit within the starting material budget")
var warehouse: FacilityInstance = _runtime.facility_service.get_instance(_ids[&"warehouse"])
var cash_before: int = _state.get_resource(&"gold")
_command(&"purchase_supplies", {&"warehouse_id": warehouse.instance_id, &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 40})
_expect(_state.get_resource(&"gold") == cash_before - 120 and _runtime.inventory_service.total_quantity(warehouse.local_inventory_id, SupplyCatalog.RAW_FOOD) == 40, "40 raw food costs exactly 120 G and arrives only in the warehouse")
_main.call("_activate_management_mode", &"product")
var auto_panel := _main.find_child("SupplyManagementPanel", true, false) as SupplyManagementPanel
_expect(auto_panel != null, "normal product UI exposes automatic paid ordering")
if auto_panel == null: _finish(); return
auto_panel.auto_purchase_enabled.button_pressed = true
auto_panel.auto_purchase_target.value = 20 # Explicit player-input fixture; not a default balance value.
auto_panel.auto_purchase_button.pressed.emit()
_expect(_runtime.auto_purchase_plans.get_plan(warehouse.instance_id, SupplyCatalog.RAW_FOOD)[&"enabled"], "normal product controls arm player-selected target20")
_command(&"set_kitchen_production", {&"facility_id": _ids[&"kitchen"], &"enabled": true, &"target_stock": 8})
await _advance(120)
_expect(_saw_movement and _saw_loaded, "an employed porter walks the real route and carries purchased food")
_expect(_saw_batch, "the kitchen starts autonomous production without a guest request")
_expect(_save_checked, "a mid-delivery disk save restores ownership and replays exactly")
_expect(_completed_deliveries(SupplyCatalog.RAW_FOOD) > 0 and _completed_deliveries(SupplyCatalog.BASIC_MEAL) > 0, "both warehouse-to-kitchen and kitchen-to-cafeteria deliveries complete")
_expect(_sales(&"cafeteria") > 0, "natural guests pay for meals made from purchased food")
await _capture()
if OS.get_environment("THE_TREE_SUPPLY_PREVIEW") == "1":
_start_preview()
return
if OS.get_environment("THE_TREE_SUPPLY_CAPTURE_ONLY") == "1":
_finish()
return
for day: int in range(1, 4):
await _advance(360 + 1440 * day - _clock.absolute_minute)
var summaries: Array = _runtime.operations_coordinator.get_day_summaries()
_expect(summaries.size() == day and bool(summaries[-1].get(&"balanced", false)), "business day %d closes with a reconciled ledger" % day)
_expect(_state.get_resource(&"gold") >= 0, "day %d retains cash after real payroll and purchases" % day)
_expect(int(summaries[-1].get(&"arrears_total", -1)) == 0, "day %d pays all real wages without arrears" % day)
_check_auto_financial_close(summaries[-1])
_expect(_auto_order_count() >= 2, "at least two real shortages cause paid automatic orders without another manual restock")
_expect(_sales(&"standard_room") > 0, "the same inn earns lodging revenue")
_expect(_sales(&"reception") == 0, "three days do not depend on artificial reception revenue")
_expect(_runtime.restore_snapshot(_runtime.create_snapshot()), "three-day supply history remains restorable")
var evidence := {"checks": _checks, "failures": _failures, "meal_sales": _sales(&"cafeteria"), "lodging_sales": _sales(&"standard_room"), "raw_deliveries": _completed_deliveries(SupplyCatalog.RAW_FOOD), "meal_deliveries": _completed_deliveries(SupplyCatalog.BASIC_MEAL), "cash": _state.get_resource(&"gold"), "closes": _runtime.operations_coordinator.get_day_summaries(), "supply": _runtime.get_supply_management_report(), "upkeep": _runtime.upkeep_settlement.get_report(), "maintenance_expense": _runtime.economy_service.get_balance(FacilityUpkeepSettlement.EXPENSE)}
var evidence_dir := "res://artifacts/supply-auto-chain" if OS.get_environment("THE_TREE_BUSINESS_ACCEPTANCE") == "1" else "res://artifacts/supply-auto-chain"
if not OS.get_environment("THE_TREE_SUPPLY_EVIDENCE_DIR").is_empty():
evidence_dir = OS.get_environment("THE_TREE_SUPPLY_EVIDENCE_DIR")
var evidence_path := ProjectSettings.globalize_path(evidence_dir.path_join("normal-play-report.json"))
DirAccess.make_dir_recursive_absolute(evidence_path.get_base_dir())
var file := FileAccess.open(evidence_path, FileAccess.WRITE)
if file != null:
file.store_string(JSON.stringify(evidence, "\t"))
print("NORMAL_AUTO_PURCHASE_REPORT ", JSON.stringify({"cash": evidence["cash"], "meal_sales": evidence["meal_sales"], "raw_deliveries": evidence["raw_deliveries"], "meal_deliveries": evidence["meal_deliveries"]}))
_finish()
func _auto_order_count() -> int:
var count := 0
for row: Dictionary in _runtime.economy_service.create_snapshot()[&"idempotency"]:
if String(row[&"key"]).begins_with("supply.auto."): count += 1
return count
func _check_auto_financial_close(summary: Dictionary) -> void:
var day := int(summary[&"day"])
var start: int = _runtime.operations_coordinator.get_financial_period_start(day)
var end := int(summary[&"closed_at"])
var purchase_expense := 0
var signing_expense := 0
var auto_expense := 0
var cash_in := 0
var cash_out := 0
for entry: LedgerEntry in _runtime.economy_service.get_entries():
if entry.category != &"INITIALIZATION":
if entry.debit_account == &"PLAYER_CASH": cash_in += entry.amount
if entry.credit_account == &"PLAYER_CASH": cash_out += entry.amount
if entry.absolute_minute < start or entry.absolute_minute >= end: continue
if entry.category == &"STAFF_SIGNING_COST": signing_expense += entry.amount
if entry.category == &"OPERATING_EXPENSE":
purchase_expense += entry.amount
if entry.source_entity_id == _ids[&"warehouse"] and _runtime.economy_service.get_transaction_id_for_idempotency_key(
SupplyAutoPurchasePlans.order_key(_ids[&"warehouse"], SupplyCatalog.RAW_FOOD, entry.absolute_minute)) == entry.transaction_id:
auto_expense += entry.amount
var receipt: Dictionary = SupplyAutoPurchasePlans.receipt_from_ledger(_runtime.economy_service, _ids[&"warehouse"], SupplyCatalog.RAW_FOOD, entry.absolute_minute, entry.transaction_id)
_expect(not receipt.is_empty() and receipt[&"quantity"] * 3 == entry.amount and receipt[&"unit_price"] == 3 and receipt[&"total_cost"] == entry.amount, "actual automatic receipt retains current normal3G quote and unchanged cost/source/category")
_expect(summary[&"category_totals"][&"cost_of_goods"] == purchase_expense + signing_expense, "day%d actual purchase+contract cost exactly reconciles daily cost_of_goods" % day)
_expect(int(summary[&"category_totals"][&"cost_of_goods"]) >= auto_expense, "automatic payment is included in operating expense once")
_expect(_runtime.economy_service.get_balance(&"PLAYER_CASH") == 5000 + cash_in - cash_out, "real initial cash+all ledger flows exactly equals player cash")
var warehouse_row: Dictionary = summary[&"facility_profit_loss"].get(_ids[&"warehouse"], {})
_expect(not warehouse_row.is_empty() and int(warehouse_row.get(&"expenses", 0)) >= auto_expense, "actual warehouse P&L retains automatic expense attribution")
func _advance(minutes: int) -> void:
for index: int in maxi(minutes, 0):
var before: int = _clock.absolute_minute
_clock.advance_minutes(1)
if _clock.absolute_minute != before + 1:
_expect(false, "clock must progress: %s" % _runtime.get_operations_report())
return
var porter: StaffRuntime = _runtime.staff_service.get_staff(&"staff_gina")
if porter != null:
_saw_movement = _saw_movement or porter.current_cell != _last_porter_cell
_last_porter_cell = porter.current_cell
if not porter.carried_inventory_id.is_empty():
_saw_loaded = true
if not _save_checked:
_check_loaded_save()
if _ids.has(&"kitchen"):
_saw_batch = _saw_batch or not _runtime.kitchen_production.get_report(_ids[&"kitchen"]).get(&"active_batch", {}).is_empty()
if index % 120 == 119:
print("SUPPLY_PROGRESS ", _clock.formatted_time(), " meals=", _sales(&"cafeteria"))
await process_frame
func _check_loaded_save() -> void:
_save_checked = true
var saved: Dictionary = _runtime.save_to_path(_save_path)
_expect(bool(saved.get(&"ok", false)), "mid-delivery save writes to an isolated slot")
var before: Dictionary = _runtime.create_snapshot()
var loaded: Dictionary = _runtime.load_from_path(_save_path)
_expect(bool(loaded.get(&"ok", false)), "mid-delivery disk restore: %s" % loaded)
var normalized: Dictionary = _runtime.create_snapshot()
_clock.advance_minutes(5)
var expected: Dictionary = _runtime.create_snapshot()
_expect(_runtime.restore_snapshot(normalized), "mid-delivery checkpoint rewinds")
_clock.advance_minutes(5)
_expect(_runtime.create_snapshot() == expected, "mid-delivery replay preserves exact inventory, tasks, positions, and ledger")
_expect(_runtime.restore_snapshot(before), "resume the observed minute after replay verification")
func _completed_deliveries(item: StringName) -> int:
var count := 0
for request: LogisticsRequest in _runtime.logistics_requests.values():
if String(request.request_id).begins_with("supply.") and request.state == LogisticsRequest.State.COMPLETED and request.item_requirements.has(item):
count += 1
return count
func _sales(definition_id: StringName) -> int:
var total := 0
for entry: LedgerEntry in _runtime.economy_service.get_entries():
if entry.category == &"SERVICE_SALE" and entry.source_entity_id == _ids.get(definition_id, &"missing"):
total += entry.amount
return total
func _command(type: StringName, payload: Dictionary) -> bool:
var result: CommandResult = _runtime.submit_command(type, payload)
_expect(result.is_success(), "%s %s: %s" % [type, payload, result.message])
return result.is_success()
func _capture() -> void:
if OS.get_environment("THE_TREE_SUPPLY_CAPTURE") != "1":
return
await process_frame
_main.call("_activate_management_mode", &"product")
var panel := _main.find_child("SupplyManagementPanel", true, false) as SupplyManagementPanel
for tab: StringName in [&"purchase", &"kitchen", &"delivery"]:
panel.select_tab(tab)
await process_frame
await RenderingServer.frame_post_draw
var path := ProjectSettings.globalize_path("res://artifacts/supply-auto-chain/%s.png" % tab)
DirAccess.make_dir_recursive_absolute(path.get_base_dir())
root.get_texture().get_image().save_png(path)
func _expect(condition: bool, message: String) -> void:
_checks += 1
if not condition:
_failures += 1
push_error("FAIL: " + message)
func _finish() -> void:
SaveService.delete_save(_save_path)
print("Normal auto purchase chain: %d checks, %d failures" % [_checks, _failures])
quit(1 if _failures > 0 else 0)
func _start_preview() -> void:
if _failures > 0:
_finish()
return
SaveService.delete_save(_save_path)
root.title = "The Tree · 仕入れと厨房の試遊(保存なし)"
var load_menu := _main.find_child("SaveLoadMenu", true, false) as MenuButton
if load_menu != null:
load_menu.disabled = true
load_menu.tooltip_text = "この試遊は保存・読込を行いません。"
for node: Node in _main.find_children("*", "Button", true, false):
var button := node as Button
for connection: Dictionary in button.pressed.get_connections():
var callback: Callable = connection[&"callable"]
if callback.get_method() in [&"_save_manual", &"_load_manual"]:
button.disabled = true
button.tooltip_text = "この試遊は保存しません。"
if OS.get_environment("THE_TREE_BUSINESS_PREVIEW") == "1":
root.title = "The Tree · 料金と営業の試遊(保存なし)"
_main.call("_activate_management_mode", &"business")
var business := _main.find_child("BusinessManagementPanel", true, false)
business.call("select_facility", _ids[&"cafeteria"])
_main.call("_set_status", "料金と営業状態を変更できます。Spaceで再開すると、客の行動に反映されます(保存なし)。", true, true)
else:
_main.call("_activate_management_mode", &"product")
var panel := _main.find_child("SupplyManagementPanel", true, false) as SupplyManagementPanel
panel.select_tab(&"kitchen")
_main.call("_set_status", "通常予算から育てた宿の試遊です。Spaceで再開し、商品タブで仕入れ・調理・配送を確認できます(保存なし)。", true, true)
_clock.set_paused(true)
print("SUPPLY_PREVIEW_READY checks=%d failures=0" % _checks)
@@ -0,0 +1 @@
uid://b35pphufmkii8
+365
View File
@@ -0,0 +1,365 @@
extends SceneTree
## Real command/ledger boundaries; failure injections are disclosed separately.
class RejectingPaymentEconomy:
extends EconomyService
func transfer(_from: StringName, _to: StringName, _amount: int,
_minute: int = -1, _category: StringName = DEFAULT_CATEGORY,
_source: StringName = &"", _key: StringName = &"", _memo: StringName = &"",
_related: StringName = &"", _notify := true) -> CommandResult:
return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, 0, "Payment refusal fixture")
class RejectingNotificationEconomy:
extends EconomyService
func commit_transaction_notification(_transaction: StringName) -> bool:
return false
class ProbedInventory:
extends InventoryService
var transaction_boundary: Dictionary = {}
func add_stack(owner: StringName, item: StringName, quantity: int,
quality: int = DEFAULT_QUALITY, expiry: int = -1, created: int = -1,
weight: int = DEFAULT_UNIT_WEIGHT, stack_id: StringName = &"") -> Dictionary:
transaction_boundary = create_snapshot()
return super.add_stack(owner, item, quantity, quality, expiry, created, weight, stack_id)
class PartialFailInventory:
extends ProbedInventory
func add_stack(owner: StringName, item: StringName, quantity: int,
quality: int = DEFAULT_QUALITY, expiry: int = -1, created: int = -1,
weight: int = DEFAULT_UNIT_WEIGHT, stack_id: StringName = &"") -> Dictionary:
super.add_stack(owner, item, quantity, quality, expiry, created, weight, stack_id)
return {&"ok": false, &"code": &"INJECTED_AFTER_STOCK"}
var _runtime: Variant
var _clock: Variant
var _state: Variant
var _warehouse: StringName
var _built: Dictionary
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
_runtime = root.get_node("SimulationRuntime")
_clock = root.get_node("GameClock")
_state = root.get_node("GameState")
_clock.set_process(false)
_clock.reset(1, 6, 0)
_state.reset()
_expect(_runtime.initialize(_clock, _state, 420_501)[&"valid"], "canonical Runtime initializes")
_clock.set_paused(true)
_runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
_expect(_runtime.submit_command(&"build_facility", {&"definition_id": &"warehouse", &"origin": Vector3i(-8, 0, 1)}).is_success(), "ordinary paid warehouse build")
_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(), "ordinary construction yields a warehouse")
if _warehouse.is_empty(): _finish(); return
_built = _runtime.create_snapshot()
_test_receipt_codec()
_test_default_manual_and_paid_order()
_test_invalid_input_and_corrupt_save()
_test_atomic_failures_and_same_minute_retry()
_test_hold_disable_and_removal()
_test_actual_budget_power_reservation_and_terminal()
_test_multiple_warehouse_order()
_test_legacy13_and_composed_rollback()
_finish()
func _plan(enabled: bool, target: int, key: StringName = &"") -> CommandResult:
return _runtime.submit_command(&"set_supply_auto_purchase", {&"warehouse_id": _warehouse,
&"item_id": SupplyCatalog.RAW_FOOD, &"enabled": enabled, &"target_stock": target}, &"player", _warehouse, 75, -1, key)
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)
func _test_receipt_codec() -> void:
var memo := SupplyAutoPurchasePlans.receipt_memo(SupplyCatalog.RAW_FOOD, 6, 3, 18)
var decoded := SupplyAutoPurchasePlans.decode_receipt_memo(memo)
_expect(decoded == {&"item_id": String(SupplyCatalog.RAW_FOOD), &"quantity": 6, &"unit_price": 3, &"total_cost": 18}, "canonical versioned actual quote codec is lossless")
decoded[&"quantity"] = 3
_expect(SupplyAutoPurchasePlans.decode_receipt_memo(memo)[&"quantity"] == 6, "decoded receipt is independently owned")
var punctuation: StringName = &"item:part.日本語"
_expect(SupplyAutoPurchasePlans.decode_receipt_memo(SupplyAutoPurchasePlans.receipt_memo(punctuation, 6, 3, 18))[&"item_id"] == String(punctuation), "length encoding handles punctuation and Unicode without ambiguity")
var prefix: String = SupplyAutoPurchasePlans.RECEIPT_PREFIX + "19:ingredient/raw_food:"
for malformed: String in ["", "supply_purchase", prefix + "3:6:18:extra", prefix + "6:03:18", prefix + "6:+3:18", prefix + "6:3.0:18", prefix + "6:3e0:18", prefix + "6:-3:18", prefix + "6:0:0", prefix + "6:3:019", prefix + "6:3:19", prefix + "10001:3:30003", prefix + "1:9223372036854775808:9223372036854775808", prefix + "2:9223372036854775807:9223372036854775807", "supply_purchase.auto.v2:19:ingredient/raw_food:6:3:18", "supply_purchase.auto.v1:019:ingredient/raw_food:6:3:18", "supply_purchase.auto.v1:18:ingredient/raw_food:6:3:18", "supply_purchase.auto.v1:9223372036854775808:ingredient/raw_food:6:3:18", "supply_purchase.auto.v1:1: :6:3:18"]:
_expect(SupplyAutoPurchasePlans.decode_receipt_memo(StringName(malformed)).is_empty(), "malformed/noncanonical/overflow quote memo is rejected: " + malformed)
var max_memo: StringName = &"supply_purchase.auto.v1:1:x:1:9223372036854775807:9223372036854775807"
_expect(SupplyAutoPurchasePlans.decode_receipt_memo(max_memo).get(&"unit_price", 0) == 9223372036854775807, "signed64 canonical maximum parses without floating conversion or overflow")
func _test_default_manual_and_paid_order() -> void:
_expect(_runtime.auto_purchase_plans.get_plans().is_empty(), "new game has no inferred automatic intent")
var before: Dictionary = _runtime.create_snapshot()
_clock.advance_minutes(1)
_expect(_stock() == 0 and _runtime.economy_service.create_snapshot() == before[&"systems"][&"runtime"][&"economy"], "default disabled tick spends and stocks nothing")
var manual: CommandResult = _runtime.submit_command(&"purchase_supplies", {&"warehouse_id": _warehouse, &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 4})
_expect(manual.is_success() and int(manual.details.get(&"total_cost", -1)) == 12 and _stock() == 4, "manual contract still charges4 food exactly12G")
var transaction_id := StringName(manual.details.get(&"transaction_id", &""))
var entries: Array = _runtime.economy_service.get_transaction_entries(transaction_id)
_expect(entries.size() == 1 and entries[0].category == &"OPERATING_EXPENSE" and entries[0].memo_key == &"supply_purchase", "manual category/memo remain unchanged")
_expect(_runtime.economy_service.get_transaction_id_for_idempotency_key(StringName("supply.purchase.%d" % manual.command_id)) == transaction_id, "manual ledger key still uses accepted commandID")
var configured: CommandResult = _plan(true, 10, &"auto.test.intent")
_expect(configured.is_success() and _stock() == 4, "player intent does not immediately stock or pay")
var armed: Dictionary = _runtime.create_snapshot()
var cash: int = _runtime.economy_service.get_balance(&"PLAYER_CASH")
var notifications: Array[Dictionary] = []
_runtime.economy_service.transaction_posted.connect(func(id: StringName, ledger_rows: Array) -> void:
var rows: Array[Dictionary] = []
for entry: LedgerEntry in ledger_rows: rows.append(entry.to_dictionary())
notifications.append({&"transaction_id": id, &"entries": rows}))
_clock.advance_minutes(1)
var paid: Dictionary = _runtime.create_snapshot()
var row: Dictionary = _runtime.auto_purchase_plans.get_plan(_warehouse, SupplyCatalog.RAW_FOOD)
_expect(_stock() == 10 and _runtime.economy_service.get_balance(&"PLAYER_CASH") == cash - 18, "ordinary tick buys the exact deficit6 at existing3G")
_expect(_state.get_resource(&"gold") == cash - 18 and row[&"last_result"] == &"PURCHASED" and row[&"last_attempt_minute"] == _clock.absolute_minute, "cash mirror and saved receipt record the real purchase")
var receipt := SupplyAutoPurchasePlans.receipt_from_ledger(_runtime.economy_service, _warehouse, SupplyCatalog.RAW_FOOD, _clock.absolute_minute, StringName(row[&"last_transaction_id"]))
_expect(receipt == {&"item_id": String(SupplyCatalog.RAW_FOOD), &"quantity": 6, &"unit_price": 3, &"total_cost": 18}, "independent actual ledger memo anchors item/quantity/historic price/total")
_expect(notifications.size() == 1 and notifications[0][&"transaction_id"] == StringName(row[&"last_transaction_id"]) and notifications[0][&"entries"].size() == 1 and notifications[0][&"entries"][0][&"memo_key"] == String(SupplyAutoPurchasePlans.receipt_memo(SupplyCatalog.RAW_FOOD, 6, 3, 18)), "normal successful automatic purchase delivers one actual quoted ledger notification")
_expect((_runtime.economy_service.get("_pending_transaction_notifications") as Dictionary).is_empty(), "normal automatic notification is committed without pending residue")
_expect(paid[&"systems"][&"runtime"][&"next_command_id"] == armed[&"systems"][&"runtime"][&"next_command_id"] and var_to_bytes(paid[&"systems"][&"runtime"][&"command_queue"]) == var_to_bytes(armed[&"systems"][&"runtime"][&"command_queue"]), "automatic order allocates no fake player command or queue history")
_expect(var_to_bytes(paid[&"systems"][&"runtime"][&"rng_streams"]) == var_to_bytes(armed[&"systems"][&"runtime"][&"rng_streams"]), "automatic purchase makes no RNG draw")
_expect(_runtime.supply_management.advance_auto_purchases(_runtime, _clock.absolute_minute).is_empty() and var_to_bytes(_runtime.create_snapshot()) == var_to_bytes(paid), "same-minute reentry is exact no-op")
_expect(notifications.size() == 1, "same-minute automatic reentry does not duplicate actual notification")
_expect(_runtime.restore_snapshot(armed), "whole pre-order snapshot restores")
_clock.advance_minutes(1)
_expect(var_to_bytes(_runtime.create_snapshot()) == var_to_bytes(paid), "saved pre-order branch replays all typed state/order/events/IDs exactly")
_expect(_runtime.restore_snapshot(paid) and var_to_bytes(_runtime.create_snapshot()) == var_to_bytes(paid), "paid order/current14 receipt roundtrip is exact")
var duplicate_before: Dictionary = _runtime.create_snapshot()
var duplicate: CommandResult = _plan(true, 10, &"auto.test.intent")
_expect(not duplicate.is_success() and duplicate.code == CommandResult.Code.DUPLICATE_IDEMPOTENCY_KEY and not duplicate.is_replay and _stock() == 10, "already accepted intent key is rejected by the canonical command queue")
_expect(var_to_bytes(_runtime.create_snapshot()) == var_to_bytes(duplicate_before), "duplicate intent refusal changes no plan, inventory, ledger, IDs, queue or Runtime state")
var owned: Dictionary = _runtime.auto_purchase_plans.get_plan(_warehouse, SupplyCatalog.RAW_FOOD)
owned[&"enabled"] = false
_expect(_runtime.auto_purchase_plans.get_plan(_warehouse, SupplyCatalog.RAW_FOOD)[&"enabled"], "returned plan is owned")
func _test_invalid_input_and_corrupt_save() -> void:
_expect(_runtime.restore_snapshot(_built), "reset to paid real warehouse fixture")
var before: Dictionary = _runtime.create_snapshot()
for value: Variant in [true, -1, 101, 1.5, "4", NAN, INF]:
var result: CommandResult = _runtime.supply_management.set_auto_purchase(_runtime,
SimulationCommand.new(999, _clock.absolute_minute, _clock.absolute_minute, 75,
&"set_supply_auto_purchase", &"player", _warehouse,
{&"warehouse_id": _warehouse, &"item_id": SupplyCatalog.RAW_FOOD, &"enabled": true, &"target_stock": value}))
_expect(not result.is_success() and var_to_bytes(_runtime.create_snapshot()) == var_to_bytes(before), "invalidtarget rejects atomically: %s" % value)
for patch: Dictionary in [
{&"warehouse_id": &"missing", &"item_id": SupplyCatalog.RAW_FOOD, &"enabled": true, &"target_stock": 1},
{&"warehouse_id": _warehouse, &"item_id": SupplyCatalog.BASIC_MEAL, &"enabled": true, &"target_stock": 1},
{&"warehouse_id": _warehouse, &"item_id": SupplyCatalog.RAW_FOOD, &"enabled": "true", &"target_stock": 1},
{&"warehouse_id": _warehouse, &"item_id": SupplyCatalog.RAW_FOOD, &"enabled": true, &"target_stock": 0},
{&"warehouse_id": _warehouse, &"item_id": SupplyCatalog.RAW_FOOD, &"enabled": true, &"target_stock": 1, &"unit_price": 0}]:
var result: CommandResult = _runtime.supply_management.set_auto_purchase(_runtime, SimulationCommand.new(999,0,0,75,&"set_supply_auto_purchase",&"player",_warehouse,patch))
_expect(not result.is_success() and var_to_bytes(_runtime.create_snapshot()) == var_to_bytes(before), "forged/wrongintent changes no state")
_expect(_plan(true, 6).is_success(), "configure a canonical saved plan")
var canonical: Dictionary = _runtime.create_snapshot()
for mutation: String in ["missing", "version", "duplicate", "unknown", "target", "bool", "clock", "receipt", "price", "extra"]:
var bad: Dictionary = canonical.duplicate(true)
var data: Dictionary = bad[&"systems"][&"runtime"]
var plans: Dictionary = data[&"auto_purchase_plans"]
var row: Dictionary = plans[&"plans"][0]
match mutation:
"missing": data.erase(&"auto_purchase_plans")
"version": plans[&"version"] = 2
"duplicate": (plans[&"plans"] as Array).append(row.duplicate(true))
"unknown": row[&"warehouse_id"] = "missing"
"target": row[&"target_stock"] = 101
"bool": row[&"enabled"] = 1
"clock": row[&"last_attempt_minute"] = _clock.absolute_minute + 1
"receipt": row[&"last_transaction_id"] = "txn.foreign"
"price": row[&"last_unit_price"] = 0.0
"extra": row[&"unit_price"] = 0
_expect(not _runtime.restore_snapshot(bad) and var_to_bytes(_runtime.create_snapshot()) == var_to_bytes(canonical), "corrupt14 plan rejected without mutation: " + mutation)
func _test_atomic_failures_and_same_minute_retry() -> void:
for kind: String in ["payment", "notification", "partial_stock"]:
_expect(_runtime.restore_snapshot(_built) and _plan(true, 6).is_success(), "configure independent failure fixture: " + kind)
var economy_before: Dictionary = _runtime.economy_service.create_snapshot()
var inventory_before: Dictionary = _runtime.inventory_service.create_snapshot()
var probe: ProbedInventory = PartialFailInventory.new() if kind == "partial_stock" else ProbedInventory.new()
_expect(probe.restore_snapshot(inventory_before), "Inventory probe starts from full actual snapshot: " + kind)
_runtime.inventory_service = probe
if kind != "partial_stock":
var rejecting: EconomyService = RejectingPaymentEconomy.new() if kind == "payment" else RejectingNotificationEconomy.new()
_expect(rejecting.restore_snapshot(economy_before), "injected Economy starts from actual snapshot")
_runtime.economy_service = rejecting
_runtime.call("_connect_economy_signals")
var notifications: Array[StringName] = []
_runtime.economy_service.transaction_posted.connect(func(id: StringName, _entries: Array) -> void: notifications.append(id))
var pending_before: Dictionary = (_runtime.economy_service.get("_pending_transaction_notifications") as Dictionary).duplicate(true)
_clock.advance_minutes(1)
_expect(_stock() == 0 and var_to_bytes(_runtime.economy_service.create_snapshot()) == var_to_bytes(economy_before), "failed order keeps actual cash and all ledger bytes unchanged: " + kind)
_expect(not probe.transaction_boundary.is_empty() and var_to_bytes(_runtime.inventory_service.create_snapshot()) == var_to_bytes(probe.transaction_boundary), "transaction-boundary rollback restores full Inventory IDs/reservations/history/order, allowing the preceding ordinary clock advancement: " + kind)
_expect(probe.transaction_boundary.get(&"current_minute", -1) == _clock.absolute_minute and _clock.absolute_minute > int(inventory_before[&"current_minute"]), "Inventory probe observes the actual next-minute transaction boundary: " + kind)
_expect(notifications.is_empty() and var_to_bytes(_runtime.economy_service.get("_pending_transaction_notifications")) == var_to_bytes(pending_before), "failed automatic transaction emits no notification and leaves the observed pending set exact: " + kind)
var failed: Dictionary = _runtime.create_snapshot()
var row: Dictionary = _runtime.auto_purchase_plans.get_plan(_warehouse, SupplyCatalog.RAW_FOOD)
_expect(row[&"last_attempt_minute"] == _clock.absolute_minute and row[&"last_result"] != &"PURCHASED", "actual failure stays recorded in same minute: " + kind)
_expect(_runtime.restore_snapshot(failed), "recorded failure restores into regular services: " + kind)
_expect(_runtime.supply_management.advance_auto_purchases(_runtime, _clock.absolute_minute).is_empty() and var_to_bytes(_runtime.create_snapshot()) == var_to_bytes(failed), "same-minute regular retry cannot hide the actual failure: " + kind)
_clock.advance_minutes(1)
_expect(_stock() == 6 and _runtime.economy_service.get_balance(&"PLAYER_CASH") == int(_built[&"game_state"][&"resources"][&"gold"]) - 18, "next ordinary tick retries exactly once: " + kind)
func _test_hold_disable_and_removal() -> void:
_expect(_runtime.restore_snapshot(_built), "hold fixture restores")
_expect(_plan(true, 6).is_success(), "enable before normal closure")
_expect(_runtime.submit_command(&"set_facility_policy", {&"facility_id": _warehouse, &"patch": {&"business_open": false}}).is_success(), "normal closure precedes auto tick")
var cash: int = _state.get_resource(&"gold")
_clock.advance_minutes(1)
_expect(_stock() == 0 and _state.get_resource(&"gold") == cash, "closed warehouse does not stock/pay")
_expect(_plan(false, 0).is_success(), "setting disable works despite closed receiver")
_expect(_runtime.submit_command(&"set_facility_policy", {&"facility_id": _warehouse, &"patch": {&"business_open": true}}).is_success(), "normal opening restores logistical eligibility")
_clock.advance_minutes(1)
_expect(_stock() == 0, "disabled plan stays off after reopening")
_expect(_plan(true, 6).is_success(), "enable again")
_clock.advance_minutes(1)
var paid: Dictionary = _runtime.create_snapshot()
var removal: CommandResult = _runtime.submit_command(&"demolish_facility", {&"instance_id": _warehouse})
_expect(not removal.is_success() and _runtime.auto_purchase_plans.create_snapshot() == paid[&"systems"][&"runtime"][&"auto_purchase_plans"], "stock-bearing failed removal preserves plan")
_expect(_runtime.restore_snapshot(_built) and _plan(true, 6).is_success(), "future closure fixture resets")
cash = _state.get_resource(&"gold")
_expect(_runtime.submit_command(&"set_facility_policy", {&"facility_id": _warehouse, &"patch": {&"business_open": false}, &"effective_at": _clock.absolute_minute + 1}).is_success(), "normal future closure is scheduled for next tick")
_clock.advance_minutes(1)
_expect(_stock() == 0 and _state.get_resource(&"gold") == cash and not _runtime.facility_service.get_instance(_warehouse).operator_open, "due closure blocks auto purchase before FacilityCompletion without early policy mutation")
_expect(_runtime.restore_snapshot(_built) and _plan(true, 6).is_success(), "empty warehouse with explicit intent exists")
_expect(_runtime.submit_command(&"demolish_facility", {&"instance_id": _warehouse}).is_success() and _runtime.auto_purchase_plans.get_plans().is_empty(), "successful empty warehouse removal prunes its intent")
_expect(_runtime.restore_snapshot(_runtime.create_snapshot()), "removed warehouse history/ledger remains restorable")
func _test_actual_budget_power_reservation_and_terminal() -> void:
_expect(_runtime.restore_snapshot(_built) and _plan(true, 6).is_success(), "configure a paid budget boundary")
var cash: int = _runtime.economy_service.get_balance(&"PLAYER_CASH")
_expect(_runtime.economy_service.transfer(&"PLAYER_CASH", &"CONSTRUCTION_EXPENSE", cash - 17, _clock.absolute_minute, &"CONSTRUCTION", &"", &"auto.acceptance.budget").is_success(), "disclosed ledger cash fixture leaves17G")
_clock.advance_minutes(1)
var row: Dictionary = _runtime.supply_management.auto_purchase_report(_runtime, _warehouse, SupplyCatalog.RAW_FOOD)
_expect(_stock() == 0 and _state.get_resource(&"gold") == 17 and row[&"reason"] == &"INSUFFICIENT_FUNDS", "full deficit18G never silently becomes partial free or affordable17G order")
_expect(row[&"last_attempt_minute"] == -1, "ineligible quote does not fabricate an executed order")
_expect(_runtime.economy_service.transfer(&"CONSTRUCTION_EXPENSE", &"PLAYER_CASH", 1, _clock.absolute_minute, &"CONSTRUCTION", &"", &"auto.acceptance.budget.refund").is_success(), "same actual ledger can return1G")
_clock.advance_minutes(1)
_expect(_stock() == 6 and _state.get_resource(&"gold") == 0, "eligible full order charges exactly available18G")
var paid: Dictionary = _runtime.create_snapshot()
var manual_key: StringName = &"auto.acceptance.manual.replacement"
var manual: CommandResult = _runtime.submit_command(&"purchase_supplies", {&"warehouse_id": _warehouse, &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 1}, &"player", _warehouse, 75, -1, manual_key)
_expect(not manual.is_success(), "manual insufficient cash remains rejected")
_expect(_runtime.restore_snapshot(_built) and _plan(true, 6).is_success(), "reserved-stock fixture resets")
_expect(_runtime.submit_command(&"purchase_supplies", {&"warehouse_id": _warehouse, &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 6}).is_success(), "reserved stock starts from an actual paid order")
var facility: FacilityInstance = _runtime.facility_service.get_instance(_warehouse)
var reservation: Dictionary = _runtime.inventory_service.reserve(facility.local_inventory_id, SupplyCatalog.RAW_FOOD, 6, &"disclosed.reserve.fixture", &"TEST", _clock.absolute_minute)
_expect(reservation.get(&"ok", false), "disclosed Inventory reservation retains physical stock")
cash = _state.get_resource(&"gold")
_clock.advance_minutes(1)
_expect(_stock() == 6 and _runtime.inventory_service.available_quantity(facility.local_inventory_id, SupplyCatalog.RAW_FOOD) == 0 and _state.get_resource(&"gold") == cash, "reserved physical units are not mistaken for missing stock or double-purchased")
_expect(_runtime.restore_snapshot(_built) and _plan(true, 6).is_success(), "power boundary resets")
_state.set_resource(&"mana", 0) # Disclosed resource boundary, not natural Mana generation.
cash = _state.get_resource(&"gold")
_clock.advance_minutes(1)
_expect(_stock() == 0 and _state.get_resource(&"gold") == cash, "Mana0 stops automatic paid order")
_expect(_runtime.restore_snapshot(_built) and _plan(true, 6).is_success(), "paused real-time boundary resets")
var paused: Dictionary = _runtime.create_snapshot()
_expect(_clock.process_real_delta(60.0) == 0 and var_to_bytes(_runtime.create_snapshot()) == var_to_bytes(paused), "paused real clock causes no automatic purchase or ID/timer change")
var collision_key: StringName = SupplyAutoPurchasePlans.order_key(_warehouse, SupplyCatalog.RAW_FOOD, _clock.absolute_minute)
_expect(_runtime.economy_service.transfer(&"PLAYER_CASH", &"CONSTRUCTION_EXPENSE", 1, _clock.absolute_minute, &"CONSTRUCTION", _warehouse, collision_key).is_success(), "disclosed foreign ledger-key fixture uses a valid ordinary transfer")
cash = _state.get_resource(&"gold")
var collisions: Array = _runtime.supply_management.advance_auto_purchases(_runtime, _clock.absolute_minute)
_expect(collisions.size() == 1 and _stock() == 0 and _state.get_resource(&"gold") == cash and _runtime.auto_purchase_plans.get_plan(_warehouse, SupplyCatalog.RAW_FOOD)[&"last_result"] == &"REPLAY_MISMATCH", "foreign ledger namespace cannot be treated as a free stocked purchase")
var collision: Dictionary = _runtime.create_snapshot()
_expect(_runtime.restore_snapshot(collision) and var_to_bytes(_runtime.create_snapshot()) == var_to_bytes(collision), "foreign-key failure receipt remains restorable without making it purchase income")
_expect(_runtime.restore_snapshot(paid), "restore an actual paid receipt for corruption tests")
var corrupt: Dictionary = paid.duplicate(true)
corrupt[&"systems"][&"runtime"][&"auto_purchase_plans"][&"plans"][0][&"last_transaction_id"] = String(_runtime.economy_service.get_entries()[0].transaction_id)
_expect(not _runtime.restore_snapshot(corrupt) and var_to_bytes(_runtime.create_snapshot()) == var_to_bytes(paid), "an existing unrelated actual transaction cannot replace the automatic receipt")
for field: StringName in [&"last_quantity", &"last_unit_price", &"last_total_cost"]:
corrupt = paid.duplicate(true)
corrupt[&"systems"][&"runtime"][&"auto_purchase_plans"][&"plans"][0][field] += 1
_expect(not _runtime.restore_snapshot(corrupt) and var_to_bytes(_runtime.create_snapshot()) == var_to_bytes(paid), "saved auto quantity/price/total quote cannot disagree with payment: %s" % field)
for pair: Vector2i in [Vector2i(3, 6), Vector2i(2, 9), Vector2i(1, 18), Vector2i(9, 2)]:
corrupt = paid.duplicate(true)
var changed_row: Dictionary = corrupt[&"systems"][&"runtime"][&"auto_purchase_plans"][&"plans"][0]
changed_row[&"last_quantity"] = pair.x
changed_row[&"last_unit_price"] = pair.y
_expect(not _runtime.restore_snapshot(corrupt) and var_to_bytes(_runtime.create_snapshot()) == var_to_bytes(paid), "coordinated factorization retaining actual18G cannot rewrite paid6x3G quote: %s" % pair)
var receipt_id: String = paid[&"systems"][&"runtime"][&"auto_purchase_plans"][&"plans"][0][&"last_transaction_id"]
for malformed: StringName in [&"supply_purchase", SupplyAutoPurchasePlans.receipt_memo(SupplyCatalog.BASIC_MEAL, 6, 3, 18), SupplyAutoPurchasePlans.receipt_memo(SupplyCatalog.RAW_FOOD, 3, 6, 18), &"supply_purchase.auto.v1:19:ingredient/raw_food:6:3:18:extra"]:
corrupt = paid.duplicate(true)
for entry: Dictionary in corrupt[&"systems"][&"runtime"][&"economy"][&"entries"]:
if entry[&"transaction_id"] == receipt_id: entry[&"memo_key"] = String(malformed)
_expect(not _runtime.restore_snapshot(corrupt) and var_to_bytes(_runtime.create_snapshot()) == var_to_bytes(paid), "ledger quote missing/wrong item/re-factored/noncanonical is rejected against independent plan atomically: %s" % malformed)
# Operations can only be enabled at fresh Day1 06:00 (360), before building.
# Keep the normal paid construction and intent after that canonical start.
_clock.reset(1, 6, 0)
_state.reset()
_expect(_runtime.initialize(_clock, _state, 420_501)[&"valid"], "fresh canonical bankruptcy fixture initializes")
_clock.set_paused(true)
_runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
var started: Dictionary = _runtime.enable_production_operations()
_expect(started[&"ok"] and _clock.absolute_minute == 360, "actual bankruptcy boundary enables ordinary daily operation at fresh Day1 06:00")
_expect(_runtime.submit_command(&"build_facility", {&"definition_id": &"warehouse", &"origin": Vector3i(-8, 0, 1)}).is_success(), "terminal fixture uses ordinary paid warehouse construction")
_clock.advance_minutes(180)
_warehouse = &""
for endpoint: FacilityInstance in _runtime.facility_service.get_instances():
if endpoint.definition_id == &"warehouse": _warehouse = endpoint.instance_id
_expect(not _warehouse.is_empty() and _plan(true, 6).is_success(), "terminal fixture configures explicit player intent after ordinary warehouse completion")
_clock.advance_minutes(4320 - _clock.absolute_minute)
_expect(_runtime.operations_coordinator.get_day_summaries().size() == 3 and _runtime.operations_coordinator.get_terminal_state() == &"BANKRUPT", "warehouse-only actual costs reach existing three-deficit terminal rule")
var terminal: Dictionary = _runtime.create_snapshot()
_expect(_runtime.supply_management.advance_auto_purchases(_runtime, _clock.absolute_minute + 1).is_empty() and var_to_bytes(_runtime.create_snapshot()) == var_to_bytes(terminal), "BANKRUPT blocks every further internal purchase without fake commands")
_expect(_runtime.restore_snapshot(terminal), "terminal intent/ledger/current14 save still restores")
func _test_multiple_warehouse_order() -> void:
_expect(_runtime.restore_snapshot(_built), "multiple actual warehouses fixture starts with one paid unit")
_expect(_runtime.submit_command(&"build_facility", {&"definition_id": &"warehouse", &"origin": Vector3i(2,0,1)}).is_success(), "ordinary second warehouse construction is paid")
_clock.advance_minutes(180)
var ids: Array[StringName] = []
for facility: FacilityInstance in _runtime.facility_service.get_instances():
if facility.definition_id == &"warehouse": ids.append(facility.instance_id)
ids.sort_custom(func(a: StringName, b: StringName) -> bool: return String(a) < String(b))
_expect(ids.size() == 2, "two actual warehouse endpoints exist")
if ids.size() != 2: return
for index: int in [1, 0]:
_expect(_runtime.submit_command(&"set_supply_auto_purchase", {&"warehouse_id": ids[index], &"item_id": SupplyCatalog.RAW_FOOD, &"enabled": true, &"target_stock": 1}).is_success(), "reverse player setting order does not define payment priority")
var cash: int = _state.get_resource(&"gold")
_expect(_runtime.economy_service.transfer(&"PLAYER_CASH", &"CONSTRUCTION_EXPENSE", cash - 3, _clock.absolute_minute, &"CONSTRUCTION", &"", &"auto.acceptance.multi_budget").is_success(), "disclosed shared cash fixture funds exactly one existing3G unit")
var before: Dictionary = _runtime.create_snapshot()
_clock.advance_minutes(1)
var after: Dictionary = _runtime.create_snapshot()
_expect(_runtime.inventory_service.total_quantity(_runtime.facility_service.get_instance(ids[0]).local_inventory_id, SupplyCatalog.RAW_FOOD) == 1 and _runtime.inventory_service.total_quantity(_runtime.facility_service.get_instance(ids[1]).local_inventory_id, SupplyCatalog.RAW_FOOD) == 0, "explicit lexicographic warehouse/item ordering deterministically spends shared cash once")
_expect(_runtime.restore_snapshot(before), "multiple-plan pre-order snapshot restores")
_clock.advance_minutes(1)
_expect(var_to_bytes(_runtime.create_snapshot()) == var_to_bytes(after), "restored multiple-plan allocation reproduces every typed field and event in order")
func _test_legacy13_and_composed_rollback() -> void:
_expect(_runtime.restore_snapshot(_built), "legacy fixture resets")
var legacy: Dictionary = _built.duplicate(true)
legacy[&"systems"][&"runtime"][&"runtime_schema_version"] = 13
legacy[&"systems"][&"runtime"].erase(&"auto_purchase_plans")
_expect(_runtime.restore_snapshot(legacy) and _runtime.auto_purchase_plans.get_plans().is_empty(), "valid13 snapshot migrates to disabled14 without purchase inference")
var migrated: Dictionary = _runtime.create_snapshot()
var disguised: Dictionary = legacy.duplicate(true)
disguised[&"systems"][&"runtime"][&"auto_purchase_plans"] = {&"version": 1, &"plans": []}
_expect(not _runtime.restore_snapshot(disguised) and var_to_bytes(_runtime.create_snapshot()) == var_to_bytes(migrated), "oldversion cannot covertly retain new aggregate")
_clock.set_paused(false)
_expect(_plan(true, 6).is_success(), "actual current14 pending automatic intent queues")
_clock.set_paused(true)
var pending: Dictionary = _runtime.create_snapshot()
var disguised_pending: Dictionary = pending.duplicate(true)
disguised_pending[&"systems"][&"runtime"][&"runtime_schema_version"] = 13
disguised_pending[&"systems"][&"runtime"].erase(&"auto_purchase_plans")
_expect(not _runtime.restore_snapshot(disguised_pending) and var_to_bytes(_runtime.create_snapshot()) == var_to_bytes(pending), "old13 cannot reinterpret a covert queued automatic order as paid feature intent")
_expect(_runtime.restore_snapshot(_built), "reset after pending downgrade refusal")
_expect(_plan(true, 6).is_success(), "intent exists before composed rollback token")
var token: Dictionary = _runtime.call("_capture_operations_external_state")
_expect(not token.is_empty() and token.has(&"auto_purchase_plans"), "Operations rollback includes owned plan state")
_expect(_runtime.auto_purchase_plans.set_plan(_warehouse, SupplyCatalog.RAW_FOOD, false, 0, _runtime.facility_service)[&"ok"], "disclosed boundary mutation fixture changes only owned intent")
_expect(_runtime.call("_restore_operations_external_state", token) and _runtime.call("_operations_external_state_matches", token), "composed rollback restores exact plan and other participants")
_runtime.call("_discard_operations_domain_events")
func _expect(condition: bool, message: String) -> void:
_checks += 1
if not condition:
_failures += 1
push_error("FAIL: " + message)
func _finish() -> void:
print("SUPPLY_AUTO_PURCHASE checks=%d failures=%d" % [_checks, _failures])
quit(_failures)
@@ -0,0 +1 @@
uid://camsaleipkl22
@@ -0,0 +1,264 @@
extends SceneTree
## Supplemental P05 QA only. Actual paid construction/purchases and porter paths
## are used. Capacity pressure, cargo lot expiry, callback failures and a ledger
## refund are explicit boundary fixtures, not normal-business success evidence.
var runtime: Variant
var clock: Variant
var state: Variant
var warehouse: StringName
var second: StringName
var kitchen: StringName
var checks := 0
var failures := 0
var output := ""
var observations: Array[Dictionary] = []
const PORTER := &"staff_gina"
const RETURN_REQUEST := &"supply.boundary_return"
func _initialize() -> void:
call_deferred("_run")
func expect(ok: bool, label: String) -> void:
checks += 1
if ok:
print(" ✓ ", label)
else:
failures += 1
push_error(label)
func _run() -> void:
output = OS.get_environment("THE_TREE_AUTO_BOUNDARY_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 external_evidence: bool = not output.is_empty()
var valid := false
if external_evidence:
valid = output.is_absolute_path() and DirAccess.dir_exists_absolute(output) and not FileAccess.file_exists(output.path_join("boundaries.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
else:
# Standard regression needs no external output/guards. Use only a new
# process-specific tests child, never a player slot or res:// source file.
var relative := "user://tests/supply_auto_boundaries_%d_%d" % [OS.get_process_id(), Time.get_ticks_usec()]
output = ProjectSettings.globalize_path(relative).simplify_path()
var allowed_parent := OS.get_user_data_dir().path_join("tests").simplify_path()
valid = user_dir.is_empty() and res_dir.is_empty() and profile.is_empty() and output.get_base_dir() == allowed_parent and not DirAccess.dir_exists_absolute(output)
if valid: valid = DirAccess.make_dir_recursive_absolute(output) == OK
profile = ProjectSettings.get_setting("application/config/custom_user_dir_name", "")
if not valid:
push_error("P05_AUTO_BOUNDARIES requires exact fresh actual profile/resource/output guards")
quit(91)
return
runtime = root.get_node("SimulationRuntime")
clock = root.get_node("GameClock")
state = root.get_node("GameState")
clock.set_process(false)
_storage_and_incoming()
_local_power_and_suspension()
_operations_deferred()
_operations_quarantine()
_unpaid_expenses()
_returned_cargo()
var file := FileAccess.open(output.path_join("boundaries.json"), FileAccess.WRITE)
expect(file != null, "fresh boundary summary opens")
if file != null:
file.store_string(JSON.stringify({"checks": checks, "failures": failures, "actual_user_data_dir": OS.get_user_data_dir(), "actual_resource_dir": ProjectSettings.globalize_path("res://"), "actual_profile": profile, "output": output, "external_evidence": external_evidence, "observations": observations, "normal_business_acceptance": false, "p05_full_acceptance": false}, "\t", true, true) + "\n")
file.close()
print("Supply auto purchase boundaries: %d checks, %d failures" % [checks, failures])
print("P05_AUTO_BOUNDARY_OUTPUT ", output)
quit(1 if failures > 0 else 0)
func fresh(rows: Array = [[&"warehouse", Vector3i(-8, 0, 1)]], operations := false) -> bool:
warehouse = &""
second = &""
kitchen = &""
clock.reset(1, 6, 0)
state.reset()
expect(runtime.initialize(clock, state, 420_505)[&"valid"], "canonical14 Runtime resets")
clock.set_paused(true)
runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
if operations:
expect(runtime.enable_production_operations()[&"ok"], "actual Operations enable at original Day1 06:00 before construction")
for row: Array in rows:
expect(runtime.submit_command(&"build_facility", {&"definition_id": row[0], &"origin": row[1]}).is_success(), "ordinary paid build: %s" % row[0])
clock.advance_minutes(180)
for facility: FacilityInstance in runtime.facility_service.get_instances():
if facility.definition_id == &"warehouse":
if warehouse.is_empty(): warehouse = facility.instance_id
else: second = facility.instance_id
if facility.definition_id == &"kitchen": kitchen = facility.instance_id
expect(not warehouse.is_empty(), "real warehouse completed")
return not warehouse.is_empty()
func inv(id: StringName) -> StringName:
return runtime.facility_service.get_instance(id).local_inventory_id
func stock(id: StringName = &"") -> int:
if id.is_empty(): id = warehouse
return runtime.inventory_service.total_quantity(inv(id), SupplyCatalog.RAW_FOOD)
func plan(target: int, minute: int = -1) -> CommandResult:
return runtime.submit_command(&"set_supply_auto_purchase", {&"warehouse_id": warehouse, &"item_id": SupplyCatalog.RAW_FOOD, &"enabled": true, &"target_stock": target}, &"player", warehouse, 75, minute)
func record(label: String, before: Dictionary, report: Dictionary) -> void:
var after: Dictionary = runtime.create_snapshot()
for row: Array in [["before", before], ["after", after]]:
var path: String = output.path_join(label + "-" + String(row[0]) + ".var")
expect(not FileAccess.file_exists(path), "fresh actual typed boundary snapshot: " + label + "/" + String(row[0]))
var file := FileAccess.open(path, FileAccess.WRITE)
expect(file != null, "typed boundary evidence opens")
if file != null:
file.store_buffer(var_to_bytes(row[1]))
file.close()
observations.append({"boundary": label, "report": report, "before_digest": SimulationDigest.compute(before), "after_digest": SimulationDigest.compute(after), "whole_bytes_equal": var_to_bytes(before) == var_to_bytes(after), "checks_so_far": checks, "failures_so_far": failures})
func _storage_and_incoming() -> void:
if not fresh([[&"warehouse", Vector3i(-8, 0, 1)], [&"warehouse", Vector3i(2, 0, 1)]]): return
expect(not second.is_empty(), "actual second warehouse is inbound source")
if second.is_empty(): return
# Disclosed capacity-pressure seam: actual supported other item, not invented
# commodity/capacity or ordinary free-stock success evidence.
expect(runtime.inventory_service.add_stack(inv(warehouse), SupplyCatalog.BASIC_MEAL, 95, 100, clock.absolute_minute + 720, clock.absolute_minute, 1)[&"ok"], "disclosed95 other-item pressure occupies canonical100 capacity")
expect(runtime.submit_command(&"purchase_supplies", {&"warehouse_id": second, &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 6}).is_success(), "inbound6 are actually paid at the source")
expect(runtime.submit_command(&"create_logistics_request", {&"request_id": &"supply.boundary_inbound", &"source_inventory_id": inv(second), &"destination_inventory_id": inv(warehouse), &"item_requirements": {SupplyCatalog.RAW_FOOD: 6}}).is_success(), "actual reserved inbound6 request targets the receiver")
expect(plan(10).is_success(), "player arms10 actual enabled auto target")
var before: Dictionary = runtime.create_snapshot()
var report: Dictionary = runtime.supply_management.auto_purchase_report(runtime, warehouse, SupplyCatalog.RAW_FOOD)
expect(report[&"enabled"] and report[&"incoming_quantity"] == 6 and report[&"deficit"] == 4 and report[&"reason"] == &"STORAGE_FULL", "other95 plus actual inbound6 blocks remaining auto4 with STORAGE_FULL")
expect(runtime.supply_management.advance_auto_purchases(runtime, clock.absolute_minute).is_empty() and var_to_bytes(before) == var_to_bytes(runtime.create_snapshot()), "STORAGE_FULL enabled auto changes no whole typed state or payment")
record("storage-full-with-inbound", before, report)
var reserved: Dictionary = runtime.inventory_service.reserve(inv(warehouse), SupplyCatalog.BASIC_MEAL, 95, &"boundary.pressure.release", &"TEST", clock.absolute_minute)
expect(reserved.get(&"ok", false), "disclosed pressure release uses actual reservation API")
var ids: Array[StringName] = []
for id: Variant in reserved.get(&"reservation_ids", []): ids.append(StringName(id))
expect(runtime.inventory_service.consume_reservations(ids, &"boundary.pressure.consume", clock.absolute_minute).get(&"ok", false), "pressure releases only its disclosed95 cooked meals")
before = runtime.create_snapshot()
var cash: int = state.get_resource(&"gold")
clock.advance_minutes(1)
expect(stock() == 4 and state.get_resource(&"gold") == cash - 12, "released capacity buys actual deficit4 once, retaining inbound6 deduction")
expect(runtime.restore_snapshot(runtime.create_snapshot()), "enabled auto plus real reserved inbound14 saves strictly")
record("inbound-deducted-purchase", before, runtime.supply_management.auto_purchase_report(runtime, warehouse, SupplyCatalog.RAW_FOOD))
func _local_power_and_suspension() -> void:
for kind: String in ["local-power", "suspension"]:
if not fresh(): return
expect(plan(6).is_success(), "arm enabled auto before " + kind)
var conditions: Dictionary = {&"power_available": false} if kind == "local-power" else {&"suspension_reason": &"RAIDED"}
expect(runtime.facility_service.set_operational_status(warehouse, conditions, clock.absolute_minute).is_success(), "disclosed public operational boundary: " + kind)
var facility: FacilityInstance = runtime.facility_service.get_instance(warehouse)
expect(state.get_resource(&"mana") > 0 and not facility.allows_logistics(), "global Mana remains available while warehouse local condition blocks " + kind)
var before: Dictionary = runtime.create_snapshot()
var report: Dictionary = runtime.supply_management.auto_purchase_report(runtime, warehouse, SupplyCatalog.RAW_FOOD)
expect(report[&"enabled"] and report[&"state"] == &"BLOCKED" and report[&"reason"] == &"FACILITY_CLOSED", "owned auto report exposes local blocked receiver: " + kind)
expect(runtime.supply_management.advance_auto_purchases(runtime, clock.absolute_minute).is_empty() and var_to_bytes(before) == var_to_bytes(runtime.create_snapshot()), "local stopped enabled auto spends/stocks/notifies nothing: " + kind)
record(kind, before, report)
func _operations_deferred() -> void:
if not fresh([[&"warehouse", Vector3i(-8, 0, 1)]], true): return
var due: int = clock.absolute_minute + 30
expect(plan(6, due).is_success(), "actual auto intent is queued for failing Operations boundary")
expect(runtime.set_operations_test_boundary_hook(&"ARRIVAL_PLAN", func(_payload: Dictionary, phase: StringName, _token: Variant) -> Dictionary: return {&"ok": false, &"reason": &"DISCLOSED_BOUNDARY_PREPARE_FAILURE", &"phase": phase}), "disclosed actual prepare fault seam registered")
clock.advance_minutes(30)
var operations: Dictionary = runtime.get_operations_report()
expect(operations[&"tick_blocked"] and operations[&"runtime_failure_latched"] and operations[&"deferred_runtime_minute"] == due and runtime.auto_purchase_plans.get_plan(warehouse, SupplyCatalog.RAW_FOOD)[&"enabled"], "actual deferred suffix is blocked with actual enabled auto intent")
var before: Dictionary = runtime.create_snapshot()
expect(stock() == 0 and runtime.supply_management.auto_purchase_report(runtime, warehouse, SupplyCatalog.RAW_FOOD)[&"reason"] == &"OPERATIONS_BLOCKED", "deferred boundary never purchases before its skipped suffix")
expect(runtime.supply_management.advance_auto_purchases(runtime, due).is_empty() and var_to_bytes(before) == var_to_bytes(runtime.create_snapshot()), "deferred enabled auto is whole typed no-op")
record("operations-deferred", before, operations)
expect(runtime.restore_snapshot(before), "actual deferred enabled-auto14 snapshot strictly restores")
var cash: int = state.get_resource(&"gold")
var acknowledged: Dictionary = runtime.acknowledge_operations_pause()
expect(acknowledged.get(&"ok", false) and acknowledged.get(&"suffix_replayed", false) and stock() == 6 and state.get_resource(&"gold") == cash - 18, "explicit actual retry runs deferred suffix and paid auto6 exactly once")
var completed: Dictionary = runtime.create_snapshot()
expect(runtime.supply_management.advance_auto_purchases(runtime, due).is_empty() and var_to_bytes(completed) == var_to_bytes(runtime.create_snapshot()), "retried actual suffix cannot purchase twice in the same minute")
func _quarantine_arrival(payload: Dictionary, phase: StringName, token: Variant) -> Dictionary:
if phase == OperationsCoordinator.PHASE_APPLY:
return {&"ok": false, &"reason": &"DISCLOSED_APPLY_FAILURE"}
if phase == OperationsCoordinator.PHASE_ROLLBACK:
return {&"ok": false, &"reason": &"DISCLOSED_ROLLBACK_UNAVAILABLE"}
return runtime.call("_operations_arrival_boundary", payload, phase, token)
func _operations_quarantine() -> void:
if not fresh([[&"warehouse", Vector3i(-8, 0, 1)]], true): return
var due: int = clock.absolute_minute + 30
expect(plan(6, due).is_success(), "actual auto intent is queued for quarantined boundary")
# Public callback contract, no manual tick_blocked/quarantine flag mutation.
runtime.operations_coordinator.configure_callbacks(Callable(runtime, "_operations_daily_boundary"), Callable(self, "_quarantine_arrival"), Callable(runtime, "_operations_morning_boundary"), Callable(runtime, "_operations_midnight_upkeep_boundary"))
clock.advance_minutes(30)
var operations: Dictionary = runtime.get_operations_report()
expect(runtime.operations_coordinator.is_external_quarantined() and operations[&"tick_blocked"] and runtime.auto_purchase_plans.get_plan(warehouse, SupplyCatalog.RAW_FOOD)[&"enabled"], "actual external rollback failure quarantines owning Coordinator with enabled auto")
var before: Dictionary = runtime.create_snapshot()
expect(stock() == 0 and runtime.supply_management.auto_purchase_report(runtime, warehouse, SupplyCatalog.RAW_FOOD)[&"reason"] == &"OPERATIONS_BLOCKED", "actual quarantine exposes blocked enabled auto")
expect(runtime.supply_management.advance_auto_purchases(runtime, due).is_empty() and var_to_bytes(before) == var_to_bytes(runtime.create_snapshot()), "quarantined enabled auto is whole typed no-op")
expect(not runtime.acknowledge_operations_pause().get(&"ok", true), "normal acknowledgement cannot clear external uncertainty")
record("operations-quarantine", before, {"operations": operations, "quarantine": runtime.operations_coordinator.get_quarantine_report()})
func _unpaid_expenses() -> void:
if not fresh([[&"warehouse", Vector3i(-8, 0, 1)]], true): return
clock.advance_minutes(1439 - clock.absolute_minute)
var cash: int = state.get_resource(&"gold")
expect(runtime.economy_service.transfer(&"PLAYER_CASH", &"CONSTRUCTION_EXPENSE", cash, clock.absolute_minute, &"CONSTRUCTION", warehouse, &"boundary.disclosed.budget").is_success(), "disclosed real ledger budget fixture spends cash before midnight")
clock.advance_minutes(1)
var debt: int = runtime.upkeep_settlement.get_arrears_total()
expect(debt > 0 and runtime.operations_coordinator.get_arrears_total() == debt and state.get_resource(&"gold") == 0, "actual midnight accrual creates unpaid upkeep, not crafted debt")
expect(runtime.economy_service.transfer(&"CONSTRUCTION_EXPENSE", &"PLAYER_CASH", 18, clock.absolute_minute, &"CONSTRUCTION_REFUND", warehouse, &"boundary.disclosed.refund").is_success(), "disclosed actual ledger refund funds a6-food order")
expect(plan(6).is_success(), "player arms auto while actual debt remains unpaid")
var before: Dictionary = runtime.create_snapshot()
var settlement: Dictionary = runtime.upkeep_settlement.create_snapshot()
clock.advance_minutes(1)
expect(stock() == 6 and state.get_resource(&"gold") == 0 and runtime.upkeep_settlement.get_arrears_total() == debt, "actual18G paid auto preserves unpaid expense instead of erasing or netting debt")
expect(var_to_bytes(settlement) == var_to_bytes(runtime.upkeep_settlement.create_snapshot()), "procurement leaves all unpaid settlement receipts/history/IDs exact")
expect(runtime.restore_snapshot(runtime.create_snapshot()), "actual paid auto plus unpaid expense14 strictly restores")
record("unpaid-expense-procurement", before, {"actual_arrears": debt, "settlement": settlement, "ordinary_sales_fixture": false})
func _returned_cargo() -> void:
if not fresh([[&"warehouse", Vector3i(-8, 0, 1)], [&"kitchen", Vector3i(2, 0, 1)], [&"staff_door", Vector3i(0, 0, 1)]]): return
expect(not kitchen.is_empty(), "real kitchen destination completes")
if kitchen.is_empty(): return
expect(runtime.submit_command(&"hire_staff_candidate", {&"candidate_id": PORTER}).is_success(), "ordinary paid hire provides real porter")
expect(runtime.submit_command(&"set_staff_work_assignment", {&"staff_id": PORTER, &"facility_id": warehouse, &"template_id": &"DAY"}).is_success(), "real recurring shift assigns warehouse porter")
for minute: int in range(40):
if runtime.staff_service.get_staff(PORTER).state == StaffRuntime.State.AVAILABLE: break
clock.advance_minutes(1)
expect(runtime.staff_service.get_staff(PORTER).state == StaffRuntime.State.AVAILABLE, "porter physically checks in")
expect(runtime.submit_command(&"purchase_supplies", {&"warehouse_id": warehouse, &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 8}).is_success(), "all cargo8 originate in ordinary paid purchase")
expect(runtime.submit_command(&"create_logistics_request", {&"request_id": RETURN_REQUEST, &"source_inventory_id": inv(warehouse), &"destination_inventory_id": inv(kitchen), &"item_requirements": {SupplyCatalog.RAW_FOOD: 8}}).is_success(), "actual reserved request starts porter delivery")
for minute: int in range(40):
if runtime.logistics_requests[RETURN_REQUEST].state == LogisticsRequest.State.DELIVERING: break
clock.advance_minutes(1)
var request: LogisticsRequest = runtime.logistics_requests[RETURN_REQUEST]
var cargo: StringName = StringName("cargo:%s" % PORTER)
expect(request.state == LogisticsRequest.State.DELIVERING and stock() == 0 and runtime.inventory_service.total_quantity(cargo, SupplyCatalog.RAW_FOOD) == 8, "real employee picks up8 paid stock into owned cargo")
if request.state != LogisticsRequest.State.DELIVERING: return
clock.advance_minutes(2)
# Disclosed expiry-lot seam preserves the exact8 paid units. No extra or free
# stock is inserted; only half the existing lot expires at the next minute.
var inventory: Dictionary = runtime.inventory_service.create_snapshot()
for owner: Dictionary in inventory[&"inventories"]:
if owner[&"owner_inventory_id"] == String(cargo):
var lot: Dictionary = owner[&"stacks"][0]
lot[&"quantity"] = 4
var expiring: Dictionary = lot.duplicate(true)
expiring[&"stack_id"] = "boundary-expiring-paid-lot"
expiring[&"expires_at"] = clock.absolute_minute + 1
(owner[&"stacks"] as Array).append(expiring)
(inventory[&"known_stack_ids"] as Array).append("boundary-expiring-paid-lot")
expect(runtime.inventory_service.restore_snapshot(inventory), "disclosed expiry split preserves8 actual paid units")
clock.advance_minutes(1)
request = runtime.logistics_requests[RETURN_REQUEST]
expect(runtime.supply_logistics.is_returning(RETURN_REQUEST) and runtime.inventory_service.total_quantity(cargo, SupplyCatalog.RAW_FOOD) == 4, "actual bridge retargets unspoiled paid4 as return cargo")
expect(plan(4).is_success(), "player enables auto4 while actual4 cargo are returning")
var before: Dictionary = runtime.create_snapshot()
var report: Dictionary = runtime.supply_management.auto_purchase_report(runtime, warehouse, SupplyCatalog.RAW_FOOD)
expect(report[&"enabled"] and report[&"physical_quantity"] == 0 and report[&"incoming_quantity"] == 4 and report[&"deficit"] == 0 and report[&"state"] == &"TARGET_REACHED", "actual return cargo4 satisfies target despite warehouse physical0")
expect(runtime.supply_management.advance_auto_purchases(runtime, clock.absolute_minute).is_empty() and var_to_bytes(before) == var_to_bytes(runtime.create_snapshot()), "actual incoming return suppresses duplicate whole-state purchase")
record("return-cargo-pending", before, report)
var cash: int = state.get_resource(&"gold")
for minute: int in range(40):
if request.state == LogisticsRequest.State.CANCELLED: break
clock.advance_minutes(1)
expect(request.state == LogisticsRequest.State.CANCELLED and request.state_reason == "CARGO_EXPIRED_RETURNED" and stock() == 4 and state.get_resource(&"gold") == cash, "actual porter return unloads paid4 with no duplicate auto charge")
expect(runtime.inventory_service.total_quantity(cargo, SupplyCatalog.RAW_FOOD) == 0 and runtime.logistics_dispatcher.get_reserved_load(PORTER) == 0 and SupplyLogisticsBridge.validate_crosslinks(runtime.logistics_requests, runtime.staff_service, runtime.inventory_service, runtime.facility_service), "actual return clears cargo/task/reservations/carrier ownership")
expect(runtime.restore_snapshot(runtime.create_snapshot()), "actual completed return plus enabled intent strictly restores14")
record("return-cargo-completed", before, runtime.supply_management.auto_purchase_report(runtime, warehouse, SupplyCatalog.RAW_FOOD))
@@ -0,0 +1 @@
uid://pyqycsjyyeuo
@@ -0,0 +1,274 @@
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
@@ -0,0 +1 @@
uid://cqccwfjhooq1t
+2 -2
View File
@@ -19,7 +19,7 @@ func _run() -> void:
source[&"clock"].advance_minutes(3)
var canonical: Dictionary = source_runtime.create_snapshot()
var canonical_data: Dictionary = canonical[&"systems"][&"runtime"]
_expect(canonical_data[&"runtime_schema_version"] == 13, "current Runtime emits schema v13")
_expect(canonical_data[&"runtime_schema_version"] == 14, "current Runtime emits schema v14")
_expect(canonical_data[&"kitchen_production"][&"current_minute"] == source[&"clock"].absolute_minute, "production clock follows Runtime clock without existing kitchens")
var legacy := canonical.duplicate(true)
var legacy_data: Dictionary = legacy[&"systems"][&"runtime"]
@@ -35,7 +35,7 @@ func _run() -> void:
_expect(legacy_data.has(&"facility_infrastructure") and legacy_data.has(&"route_projection"), "v6 fixture retains infrastructure introduced in v6")
_expect(first_runtime.restore_snapshot(legacy), "genuine v6 state restores into v9")
var migrated: Dictionary = first_runtime.create_snapshot()
_expect(migrated[&"systems"][&"runtime"][&"runtime_schema_version"] == 13, "v6 migration re-emits current schema")
_expect(migrated[&"systems"][&"runtime"][&"runtime_schema_version"] == 14, "v6 migration re-emits current schema")
var kitchen: Dictionary = migrated[&"systems"][&"runtime"][&"kitchen_production"]
_expect(kitchen[&"plans"].is_empty() and kitchen[&"active_batches"].is_empty(), "legacy migration does not infer kitchen production plans or batches")
_expect(kitchen[&"current_minute"] == source[&"clock"].absolute_minute, "legacy production clock binds to the restored Runtime minute")
+1 -1
View File
@@ -93,7 +93,7 @@ func _verify_mid_ride() -> bool:
_expect(bool(_runtime.save_to_path(save_path).get(&"ok", false)), "boarded save writes isolated disk slot")
_expect(bool(_runtime.load_from_path(save_path).get(&"ok", false)), "boarded disk slot reloads")
var snapshot: Dictionary = _runtime.create_snapshot()
_expect(snapshot[&"systems"][&"runtime"][&"runtime_schema_version"] == 13, "runtime schema13 records elevator state and explicit vertical routing")
_expect(snapshot[&"systems"][&"runtime"][&"runtime_schema_version"] == 14, "runtime schema14 records elevator state and explicit vertical routing")
_expect(_runtime.restore_snapshot(snapshot), "mid-ride Runtime snapshot restores actor/cabin/Path ownership")
var shaft: Dictionary = _runtime.elevators.get_report()[&"shafts"][0]
var busy := _cmd(&"demolish_facility", {&"instance_id": shaft[&"module_ids"][1]})
@@ -68,7 +68,7 @@ func _run() -> void:
for guest: GuestRuntime in _runtime.guest_service.get_guests():
_expect(guest.current_cell == Vector3i.ZERO and _runtime.path_service.get_actor_projected_cell(StringName("guest:" + String(guest.guest_id))) == Vector3i.ZERO, "migration preserves actual passenger's logical boarding anchor")
var canonical: Dictionary = _runtime.create_snapshot()
_expect(canonical[&"systems"][&"runtime"][&"runtime_schema_version"] == 13 and canonical[&"systems"][&"runtime"][&"path"][&"schema_version"] == 5, "resave emits current Runtime13 and Path5 only")
_expect(canonical[&"systems"][&"runtime"][&"runtime_schema_version"] == 14 and canonical[&"systems"][&"runtime"][&"path"][&"schema_version"] == 5, "resave emits current Runtime14 and Path5 only")
_expect(_runtime.restore_snapshot(canonical) and _runtime.create_snapshot() == canonical, "canonical migrated save reloads exactly without applying density or stairs twice")
forged = canonical.duplicate(true)
forged[&"systems"][&"runtime"][&"path"][&"transit_actor_ids"].pop_back()
+1 -1
View File
@@ -15,7 +15,7 @@ func _run() -> void:
var clock: Variant = fixture[&"clock"]
clock.advance_minutes(3)
var current: Dictionary = runtime.create_snapshot()
_expect(current[&"systems"][&"runtime"][&"runtime_schema_version"] == 13, "Runtime emits schema13 with elevator and explicit vertical-route aggregates")
_expect(current[&"systems"][&"runtime"][&"runtime_schema_version"] == 14, "Runtime emits schema14 with elevator and explicit vertical-route aggregates")
var legacy := current.duplicate(true)
var data: Dictionary = legacy[&"systems"][&"runtime"]
data[&"runtime_schema_version"] = 7
+156
View File
@@ -0,0 +1,156 @@
extends SceneTree
## Normal build/product controls exercise drafts, a real paid tick, queued Save,
## normal load, duplicate prevention and removal-free disabled restart state.
var checks := 0
var failures := 0
var main: Control
var panel: SupplyManagementPanel
var runtime: Variant
var clock: Variant
var state: Variant
var warehouse: StringName = &""
var slot := ""
func _initialize() -> void:
call_deferred("run")
func expect(ok: bool, message: String) -> void:
checks += 1
if not ok:
failures += 1
push_error("AUTO_PURCHASE_UI: " + message)
func setup_scene(save_slot: String) -> void:
runtime = root.get_node("SimulationRuntime")
clock = root.get_node("GameClock")
state = root.get_node("GameState")
clock.set_process(false)
clock.reset(1, 6, 0)
state.reset()
expect(runtime.initialize(clock, state, 420_502)[&"valid"], "current canonical Runtime initializes")
clock.set_paused(true)
root.size = Vector2i(1280, 720)
main = load("res://scenes/main/main.tscn").instantiate() as Control
main.manual_save_path = save_slot
root.add_child(main)
runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
await process_frame
await process_frame
panel = main.find_child("SupplyManagementPanel", true, false) as SupplyManagementPanel
expect(panel != null, "ordinary scene owns product controls")
if panel != null: main.call("_activate_management_mode", &"product")
func build_warehouse() -> void:
var count: int = runtime.construction_service.get_jobs().size()
main.call("_select_build_definition", &"warehouse")
main.call("_on_map_cell_clicked", Vector3i(-8, 0, 1))
expect(runtime.construction_service.get_jobs().size() == count + 1, "normal build palette/map submits paid construction")
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(), "completed ordinary warehouse becomes real purchase endpoint")
main.call("_activate_management_mode", &"product")
panel.refresh()
expect(panel.selected_warehouse_id == warehouse and panel.selected_item_id == SupplyCatalog.RAW_FOOD, "ordinary product selects actual warehouse/catalog item")
expect(not panel.auto_purchase_enabled.button_pressed and int(panel.auto_purchase_target.value) == 0, "new warehouse is explicitly disabled with no funded default")
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 0
func save_button() -> void:
for button: Node in main.find_children("*", "Button", true, false):
if button.text == "保存":
expect(not button.disabled, "normal Save button enabled")
button.pressed.emit()
expect(bool(runtime.latest_save_operation_result.get(&"ok", false)), "ordinary Save reports success")
return
expect(false, "normal Save button exists")
func load_menu() -> void:
var menu := main.find_child("SaveLoadMenu", true, false) as MenuButton
expect(menu != null and not menu.disabled, "normal load menu enabled")
if menu != null and not menu.disabled: menu.get_popup().id_pressed.emit(0)
func verify_order(target: int = 6) -> void:
warehouse = panel.selected_warehouse_id
var row: Dictionary = runtime.auto_purchase_plans.get_plan(warehouse, SupplyCatalog.RAW_FOOD)
expect(row[&"enabled"] and row[&"target_stock"] == target and row[&"last_result"] == &"PURCHASED", "normal tick retains enabled player goal and paid receipt")
expect(stock() == target and runtime.command_queue.pending_count() == 0, "actual warehouse has desired paid stock and no synthetic queued order")
expect(runtime.economy_service.get_transaction_id_for_idempotency_key(SupplyAutoPurchasePlans.order_key(warehouse, SupplyCatalog.RAW_FOOD, row[&"last_attempt_minute"])) == StringName(row[&"last_transaction_id"]), "normal restored receipt resolves real ledger key")
expect(state.get_resource(&"gold") == runtime.economy_service.get_balance(&"PLAYER_CASH"), "ordinary UI money mirror is ledger-owned")
expect(panel.auto_purchase_status.text.contains("ON") and not panel.auto_purchase_button.disabled, "ordinary UI reconstructs enabled completed controls")
func queue_intent(target: int = 6) -> void:
var before: Dictionary = runtime.create_snapshot()
panel.auto_purchase_enabled.button_pressed = true
panel.auto_purchase_target.value = target
expect(var_to_bytes(runtime.create_snapshot()) == var_to_bytes(before), "editing toggle/target changes draft only")
expect(panel.auto_purchase_status.text.contains("未保存"), "UI distinguishes draft from active intent")
clock.set_paused(false)
panel.auto_purchase_button.pressed.emit()
clock.set_paused(true)
expect(runtime.command_queue.pending_count() == 1 and stock() == 0 and panel.auto_purchase_button.disabled, "normal Apply queues one intent without stock/payment")
panel.auto_purchase_button.pressed.emit()
expect(runtime.command_queue.pending_count() == 1, "disabled/repeated normal Apply cannot double queue")
func run() -> void:
var explicit_slot := OS.get_environment("THE_TREE_AUTO_PURCHASE_UI_SLOT")
slot = explicit_slot if not explicit_slot.is_empty() else "user://tests/supply_auto_ui_%d_%d/manual" % [OS.get_process_id(), Time.get_ticks_usec()]
if not valid_slot(slot) or DirAccess.dir_exists_absolute(ProjectSettings.globalize_path(slot)):
push_error("AUTO_PURCHASE_UI needs a fresh user://tests slot; player fallback forbidden")
quit(90); return
await setup_scene(slot)
if panel == null: quit(failures); return
expect(panel.auto_purchase_button.disabled, "no warehouse is a safe empty setting state")
build_warehouse()
if warehouse.is_empty(): quit(failures); return
queue_intent()
save_button()
expect(SaveService.load_snapshot(slot)[&"ok"], "pending actual command is written to isolated disk")
load_menu()
panel.refresh()
expect(runtime.command_queue.pending_count() == 1 and not runtime.auto_purchase_plans.get_plan(warehouse, SupplyCatalog.RAW_FOOD)[&"enabled"] and panel.auto_purchase_button.disabled, "normal reload reconstructs one pending setting with no premature enabling")
var cash: int = state.get_resource(&"gold")
var next_id: int = runtime.create_snapshot()[&"systems"][&"runtime"][&"next_command_id"]
clock.advance_minutes(1)
panel.refresh()
verify_order()
expect(state.get_resource(&"gold") == cash - 18 and runtime.create_snapshot()[&"systems"][&"runtime"][&"next_command_id"] == next_id, "loaded intent charges18G once without allocating auto commandID")
var paid: Dictionary = runtime.create_snapshot()
panel.refresh()
panel.select_tab(&"delivery")
panel.select_tab(&"purchase")
expect(var_to_bytes(runtime.create_snapshot()) == var_to_bytes(paid), "product navigation/refresh mutate no gameplay")
panel.auto_purchase_enabled.button_pressed = false
panel.auto_purchase_button.pressed.emit()
expect(not runtime.auto_purchase_plans.get_plan(warehouse, SupplyCatalog.RAW_FOOD)[&"enabled"], "normal controls disable auto ordering")
save_button()
load_menu()
panel.refresh()
expect(not panel.auto_purchase_enabled.button_pressed and stock() == 6, "normal load preserves disabled intent and paid stock")
await capture("normal-auto-purchase")
SaveService.delete_save(slot)
main.queue_free()
await process_frame
print("SUPPLY_AUTO_PURCHASE_UI checks=%d failures=%d" % [checks, failures])
quit(failures)
func capture(label: String) -> void:
var folder := OS.get_environment("THE_TREE_AUTO_PURCHASE_UI_CAPTURE_DIR")
if folder.is_empty(): return
expect(folder.is_absolute_path() and DirAccess.dir_exists_absolute(folder) and DisplayServer.get_name() != "headless", "explicit UI capture uses an existing QA output and real renderer")
if not folder.is_absolute_path() or DisplayServer.get_name() == "headless": return
var path := folder.path_join(label + ".png")
expect(not FileAccess.file_exists(path), "UI capture is fresh")
if FileAccess.file_exists(path): return
await process_frame
await RenderingServer.frame_post_draw
expect(root.get_texture().get_image().save_png(path) == OK, "actual product image saved for ROOT visual review")
func valid_slot(value: String) -> bool:
if not value.begins_with("user://tests/") or value.contains("\\"): return false
for segment: String in value.trim_prefix("user://tests/").split("/", true):
if segment in ["", ".", ".."]: return false
return value.ends_with("/manual")
@@ -0,0 +1 @@
uid://ddpkpqypwj4f0