Files
TheTree_mock/tests/ui/test_approved_free_unique_arrears_ui.gd
Takeshi b84bade045 Save The Tree implementation and acceptance work in progress
Preserve production code, regression fixtures, audio assets and provenance, test infrastructure, and the original-completion plan and progress ledger. Final game and Windows acceptance remain pending; excluded local artifacts and native experiments are not part of this snapshot.
2026-10-11 15:58:00 +09:00

173 lines
9.9 KiB
GDScript

extends SceneTree
const FixtureCleanup := preload("res://tests/helpers/ui_audio_fixture_cleanup.gd")
## Candidate only, not compiled/executed. Normal construction, staff, save and
## load controls are exercised; the sole money fixture is an explicitly labelled
## ledger spend at 23:59 using existing valid accounts/category. No tuning edits.
## Invoke -- --read in a second process with the SAME unique test save slot.
var checks := 0
var failures := 0
var main: Control
var runtime: Variant
var clock: Variant
var slot := ""
func _initialize() -> void:
call_deferred("run")
func expect(ok: bool, label: String) -> void:
checks += 1
if not ok:
failures += 1
push_error("FREE_UNIQUE_UI: " + label)
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()
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 unique_hire_result() -> CommandResult:
for saved: Dictionary in runtime.command_queue.create_snapshot()["processed"]:
var details: Dictionary = saved.get("details",{})
if StringName(details.get(&"staff_id",&"")) == &"heroine_anri":
return CommandResult.from_dictionary(saved)
return null
func verify_joined() -> void:
var anri: StaffRuntime = runtime.staff_service.get_staff(&"heroine_anri")
expect(anri != null and anri.employment_kind == StaffRuntime.EmploymentKind.UNIQUE and anri.role_id == &"innkeeper","normal read has canonical unique innkeeper")
if anri == null:
return
expect(runtime.economy_service.get_balance(runtime.PLAYER_CASH_ACCOUNT) == 0 and runtime.upkeep_settlement.get_arrears_total() > 0,"joining preserves zero funds and actual unpaid upkeep")
expect(anri.daily_wage == 0 and runtime.staff_service.get_daily_payroll_total() == 0,"unique upkeep remains zero")
main.call("_set_staff_candidate_panel_open",true)
var button: Button = main.staff_candidate_buttons.get(&"heroine_anri")
expect(button != null and button.disabled and button.text == "加入済み","loaded normal UI cannot join unique twice")
main.staff_management_toggle_button.pressed.emit()
var panel := main.find_child("StaffManagementPanel",true,false) as StaffManagementPanel
expect(panel != null,"normal hired staff panel present")
if panel != null:
panel.select_staff(&"heroine_anri")
expect(panel.staff_detail_label.text.contains("アンリ") and panel.staff_detail_label.text.contains("日給 G0"),"normal management shows named character and no upkeep")
var completed := unique_hire_result()
expect(completed != null and completed.is_success() and completed.details.get(&"employment_kind") == &"UNIQUE","disk-persisted completed UI command has actual UNIQUE mode")
var events := 0
for event: SimulationDomainEvent in runtime.domain_events.recent_events():
if event.event_type == &"STAFF_CANDIDATE_HIRED" and &"heroine_anri" in event.entity_ids:
events += 1
expect(event.payload.get(&"employment_kind") == &"UNIQUE","disk-persisted joining event has corrected mode")
expect(events == 1,"UI load contains one successful joining event")
var before: Dictionary = runtime.create_snapshot()
expect(not runtime.staff_service.retire_staff(&"heroine_anri").is_success() and not runtime.staff_service.remove_staff(&"heroine_anri",&"SOLD").is_success() and runtime.create_snapshot() == before,"loaded unique remains unsellable and non-retirable")
var corrupt := before.duplicate(true)
for row: Dictionary in corrupt[&"systems"][&"runtime"][&"staff"][&"staff"]:
if row[&"staff_id"] == "heroine_anri":
row[&"employment_kind"] = "FREE_HIRE"
expect(not runtime.restore_snapshot(corrupt) and runtime.create_snapshot() == before,"save corruption cannot downgrade unique protection")
func run() -> void:
var explicit_slot: bool = OS.has_environment("THE_TREE_UNIQUE_FREE_UI_SLOT")
slot = OS.get_environment("THE_TREE_UNIQUE_FREE_UI_SLOT")
if not explicit_slot:
slot = "user://tests/approved_free_unique_%d_%d/manual" % [OS.get_process_id(), Time.get_ticks_usec()]
if DirAccess.dir_exists_absolute(ProjectSettings.globalize_path(slot)):
push_error("Generated test slot already exists; refuse overwrite.")
quit(90)
return
var relative_slot := slot.trim_prefix("user://tests/")
var valid_slot: bool = slot.begins_with("user://tests/") and not relative_slot.is_empty() and not relative_slot.ends_with("/") and not relative_slot.contains("\\")
for segment: String in relative_slot.split("/", true):
valid_slot = valid_slot and segment not in ["", ".", ".."]
if not valid_slot:
push_error("Explicit test slot is invalid; no production fallback.")
quit(90)
return
runtime = root.get_node("SimulationRuntime")
clock = root.get_node("GameClock")
var state: Variant = root.get_node("GameState")
clock.reset(1,6,0)
state.reset()
expect(runtime.initialize(clock,state,424242)[&"valid"],"canonical real Runtime initializes")
clock.set_paused(true)
root.size = Vector2i(1440,900)
main = load("res://scenes/main/main.tscn").instantiate()
main.manual_save_path = slot
root.add_child(main)
runtime.configure_save_policy({&"enabled":false,&"daily_checkpoint_enabled":false})
await process_frame
await process_frame
if "--read" in OS.get_cmdline_user_args():
load_menu()
expect(clock.absolute_minute == 1441,"second-process normal load restores exact joined minute")
verify_joined()
else:
# Actual build palette and map handler; no direct completed-room insertion.
main.call("_select_build_definition",&"reception")
main.call("_on_map_cell_clicked",Vector3i(-6,0,0))
expect(runtime.construction_service.get_jobs().size() == 1,"normal UI submits reception construction")
clock.advance_minutes(runtime.definition_registry.get_facility(&"reception").build_minutes)
clock.advance_minutes(1439-clock.absolute_minute)
var cash: int = runtime.economy_service.get_balance(runtime.PLAYER_CASH_ACCOUNT)
# Explicit boundary fixture only; not a natural-spending/balance assertion.
expect(runtime.economy_service.transfer(runtime.PLAYER_CASH_ACCOUNT,&"CONSTRUCTION_EXPENSE",cash,clock.absolute_minute,&"CONSTRUCTION",&"",&"acceptance.free_unique.ui_cash_fixture").is_success(),"controlled ledger spend before real upkeep close")
clock.advance_one_minute()
expect(clock.absolute_minute == 1440 and not runtime._operations_should_block_tick(),"ordinary first unpaid close completes without terminal/pause/failure")
expect(runtime.upkeep_settlement.get_arrears_total() > 0,"real ownership upkeep supplies test debt")
var economy_before: Dictionary = runtime.economy_service.create_snapshot()
var arrears_before: int = runtime.upkeep_settlement.get_arrears_total()
main.staff_candidate_toggle_button.pressed.emit()
var paid: Button = main.staff_candidate_buttons.get(&"staff_tessa")
expect(paid != null and not paid.disabled,"ordinary paid candidate still present")
if paid != null:
paid.pressed.emit()
expect(not runtime.staff_service.has_staff(&"staff_tessa") and runtime.economy_service.create_snapshot() == economy_before,"normal paid hire still denied without ledger changes")
var button: Button = main.staff_candidate_buttons.get(&"heroine_anri")
expect(button != null and button.is_visible_in_tree() and not button.disabled and button.text == "無料で加入","normal staff pane offers already-approved free Anri despite unrelated debt")
if button == null:
var early_cleanup: Dictionary = await FixtureCleanup.free_owner_and_wait(self, main)
if not bool(early_cleanup.get(&"passed", false)):
push_error("Approved free unique UI ordinary owner cleanup failed: %s" % str(early_cleanup))
quit(96)
return
quit(failures)
return
var before: Dictionary = runtime.create_snapshot()
clock.set_paused(false)
button.pressed.emit()
clock.set_paused(true)
expect(not runtime.staff_service.has_staff(&"heroine_anri") and button.disabled and runtime.command_queue.pending_count() == 1,"running UI queues exactly one free joining request")
button.pressed.emit()
expect(runtime.command_queue.pending_count() == 1,"repeated UI press cannot enqueue twice")
save_button()
expect(SaveService.load_snapshot(slot)[&"ok"],"actual UI stores pending free request")
expect(runtime.restore_snapshot(before),"restore control before normal pending load")
load_menu()
expect(runtime.command_queue.pending_count() == 1 and not runtime.staff_service.has_staff(&"heroine_anri"),"normal disk load preserves pending request without premature joining")
clock.advance_one_minute()
expect(runtime.economy_service.create_snapshot() == economy_before and runtime.upkeep_settlement.get_arrears_total() == arrears_before,"executing restored free request does not spend or erase debt")
verify_joined()
save_button() # intentionally replace this explicit disposable test slot
expect(SaveService.load_snapshot(slot)[&"ok"],"joined state saved for independent process read")
var capture := OS.get_environment("THE_TREE_UNIQUE_FREE_UI_CAPTURE")
if not capture.is_empty() and DisplayServer.get_name() != "headless":
expect(capture.is_absolute_path() and not FileAccess.file_exists(capture),"new absolute rendered evidence path required")
if capture.is_absolute_path() and not FileAccess.file_exists(capture):
await process_frame
await RenderingServer.frame_post_draw
expect(root.get_texture().get_image().save_png(capture) == OK,"rendered named character and unpaid HUD captured")
var cleanup: Dictionary = await FixtureCleanup.free_owner_and_wait(self, main)
if not bool(cleanup.get(&"passed", false)):
push_error("Approved free unique UI ordinary owner cleanup failed: %s" % str(cleanup))
quit(96)
return
print("APPROVED_FREE_UNIQUE_UI phase=%s checks=%d failures=%d" % ["read" if "--read" in OS.get_cmdline_user_args() else "write",checks,failures])
quit(failures)