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.
543 lines
31 KiB
GDScript
543 lines
31 KiB
GDScript
extends SceneTree
|
|
|
|
const AudioTeardown := preload("res://tests/helpers/ui_audio_teardown.gd")
|
|
|
|
## Real Main controls, paid builds and the Runtime command queue. Dedicated disk
|
|
## saves verify that a pending edit cannot be charged/applied twice after Load.
|
|
var _checks := 0
|
|
var _failures := 0
|
|
var _main: Control
|
|
var _panel: BusinessManagementPanel
|
|
var _runtime: Variant
|
|
var _clock: Variant
|
|
var _rooms: Array[StringName] = []
|
|
var _reception: StringName = &""
|
|
var _warehouse: StringName = &""
|
|
var _save_path := "user://tests/business_ui_%d/queued_policy" % OS.get_process_id()
|
|
|
|
|
|
func _test_layer_palette() -> void:
|
|
_main.call("_set_layer", 0)
|
|
var before: Color = _panel.facility_status_label.get_theme_color("font_color")
|
|
var selected: bool = _panel.business_open_button.button_pressed
|
|
_main.call("_set_layer", 1)
|
|
_panel.refresh()
|
|
_expect(_panel.facility_status_label.get_theme_color("font_color") == Color("d8eeea"), "BACK panel text survives refresh")
|
|
_expect(_panel.business_open_button.get_theme_color("font_hover_color") == Color("d8eeea") and _panel.business_open_button.get_theme_color("font_disabled_color") == Color("8da4a4"), "BACK toggle remains readable for hover and disabled states")
|
|
_panel.call("_set_result", "条件未達", false)
|
|
_expect(_panel.result_label.get_theme_color("font_color") == Color("ffb498"), "BACK error retains warning colour")
|
|
_main.call("_set_layer", 0)
|
|
_expect(_panel.facility_status_label.get_theme_color("font_color") == before and _panel.result_label.get_theme_color("font_color") == Color("a7402e"), "FRONT restores panel and warning colours")
|
|
_expect(_panel.business_open_button.button_pressed == selected, "palette does not toggle policy")
|
|
_panel.call("_set_result", "", true)
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("_run")
|
|
|
|
|
|
func _run() -> void:
|
|
_clock = root.get_node("GameClock")
|
|
_runtime = root.get_node("SimulationRuntime")
|
|
var state: Variant = root.get_node("GameState")
|
|
_clock.reset(1, 6, 0)
|
|
state.reset()
|
|
_runtime.initialize(_clock, state, 824260)
|
|
_clock.set_paused(true)
|
|
root.size = Vector2i(1280, 720)
|
|
_main = load("res://scenes/main/main.tscn").instantiate() as Control
|
|
_main.set("manual_save_path", "user://tests/business_ui_%d/manual" % OS.get_process_id())
|
|
root.add_child(_main)
|
|
_runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
|
|
await process_frame
|
|
await process_frame
|
|
_panel = _main.find_child("BusinessManagementPanel", true, false) as BusinessManagementPanel
|
|
_expect(_panel != null, "normal Main includes business management")
|
|
if _panel == null:
|
|
await _finish()
|
|
return
|
|
(_main.get("navigation_buttons")[&"business"] as Button).pressed.emit()
|
|
_expect(_panel.visible and _panel.apply_button.disabled and _panel.facility_option.disabled,
|
|
"business nav opens a safe empty state before completed construction")
|
|
_expect("完成" in _panel.facility_status_label.text, "empty state explains completed facility prerequisite")
|
|
_test_layer_palette()
|
|
for request: Array in [
|
|
[&"reception", Vector3i(-8, 0, 0)],
|
|
[&"standard_room", Vector3i(-8, 2, 0)],
|
|
[&"standard_room", Vector3i(-6, 2, 0)],
|
|
[&"warehouse", Vector3i(-8, 0, 1)],
|
|
]:
|
|
var before: int = _runtime.construction_service.get_jobs().size()
|
|
_main.call("_select_build_definition", request[0])
|
|
_main.call("_on_map_cell_clicked", request[1])
|
|
_expect(_runtime.construction_service.get_jobs().size() == before + 1, "normal UI pays for construction of %s" % request[0])
|
|
if request[0] == &"reception":
|
|
_clock.advance_minutes(120)
|
|
_clock.advance_minutes(180)
|
|
for facility: FacilityInstance in _runtime.facility_service.get_instances():
|
|
if facility.definition_id == &"standard_room":
|
|
_rooms.append(facility.instance_id)
|
|
elif facility.definition_id == &"reception":
|
|
_reception = facility.instance_id
|
|
elif facility.definition_id == &"warehouse":
|
|
_warehouse = facility.instance_id
|
|
_expect(_rooms.size() == 2 and not _reception.is_empty() and not _warehouse.is_empty(), "paid builds complete two rooms, free reception and warehouse")
|
|
if _rooms.size() != 2 or _reception.is_empty() or _warehouse.is_empty():
|
|
await _finish()
|
|
return
|
|
_main.call("_activate_management_mode", &"business")
|
|
_panel.refresh()
|
|
_test_related_facility_disk_save()
|
|
_test_prices_and_drafts()
|
|
_test_open_close()
|
|
_test_saved_pending_edit()
|
|
_test_customer_counts()
|
|
await _test_navigation_and_layout()
|
|
await _test_principal_repayment_controls()
|
|
await _finish()
|
|
|
|
|
|
func _test_related_facility_disk_save() -> void:
|
|
_clock.set_paused(true)
|
|
var linked: CommandResult = _runtime.submit_command(&"set_facility_conditions", {&"facility_id": _rooms[0], &"conditions": {&"related_facility_id": _rooms[1]}})
|
|
_expect(linked.is_success(), "built room accepts related facility through Runtime")
|
|
var snapshot: Dictionary = _runtime.create_snapshot()
|
|
var saved: Dictionary = _runtime.save_to_path(_save_path)
|
|
_expect(bool(saved.get(&"ok", false)), "related facility saves to real disk slot")
|
|
_runtime.submit_command(&"set_facility_conditions", {&"facility_id": _rooms[0], &"conditions": {&"related_facility_id": &""}})
|
|
var loaded: Dictionary = _runtime.load_from_path(_save_path)
|
|
_expect(bool(loaded.get(&"ok", false)), "related facility loads through Runtime crosslink validation")
|
|
_expect(_runtime.facility_service.get_instance(_rooms[0]).related_facility_id == _rooms[1], "restored room retains the selected dependency")
|
|
_expect(_runtime.facility_service.create_snapshot() == snapshot[&"systems"][&"runtime"][&"facilities"], "facility state is exact across disk roundtrip")
|
|
_runtime.submit_command(&"set_facility_conditions", {&"facility_id": _rooms[0], &"conditions": {&"related_facility_id": &""}})
|
|
|
|
|
|
func _test_prices_and_drafts() -> void:
|
|
_choose(_panel.facility_option, _rooms[0])
|
|
_expect(_panel.price_option.item_count == 5 and not _panel.price_option.disabled, "five price modes are available for paid guest rooms")
|
|
var row := _row(_rooms[0])
|
|
_expect(int(row.get(&"base_price", 0)) == 120 and "120G" in _panel.quote_label.text,
|
|
"initial room shows actual 120 G base and current quotation")
|
|
var digest: String = _runtime.latest_digest
|
|
var cash: int = _runtime.economy_service.get_balance(&"PLAYER_CASH")
|
|
for pair: Array in [[&"charitable", 72], [&"affordable", 96], [&"standard", 120], [&"expensive", 180], [&"exorbitant", 300]]:
|
|
_choose(_panel.price_option, pair[0])
|
|
_expect("設定後 %dG" % pair[1] in _panel.quote_label.text, "%s mode previews authoritative rounded price" % pair[0])
|
|
_expect(_runtime.latest_digest == digest and _runtime.economy_service.get_balance(&"PLAYER_CASH") == cash,
|
|
"changing drafts and displaying quotes never mutates Runtime or spends cash")
|
|
_expect("満足度" in _panel.effect_label.text and "支払えない" in _panel.effect_label.text,
|
|
"highest price explains satisfaction and affordability instead of an invented demand forecast")
|
|
_choose(_panel.price_option, &"affordable")
|
|
_main.call("_activate_management_mode", &"product")
|
|
_clock.advance_minutes(1)
|
|
_main.call("_activate_management_mode", &"business")
|
|
_expect(_selected_mode() == &"affordable" and not _panel.apply_button.disabled,
|
|
"unsaved price draft survives time updates and other management panels")
|
|
_choose(_panel.facility_option, _rooms[1])
|
|
_choose(_panel.price_option, &"expensive")
|
|
_choose(_panel.facility_option, _rooms[0])
|
|
_expect(_selected_mode() == &"affordable", "separate facilities keep separate price drafts")
|
|
_clock.set_paused(false)
|
|
_panel.apply_button.pressed.emit()
|
|
_expect(_panel.apply_button.disabled and _panel.price_option.disabled and _panel.business_open_button.disabled,
|
|
"queued policy disables edits and duplicate submissions for that facility")
|
|
_choose(_panel.facility_option, _rooms[1])
|
|
_clock.advance_minutes(1)
|
|
_clock.set_paused(true)
|
|
_expect(_selected_mode() == &"expensive" and _panel.selected_facility_id == _rooms[1],
|
|
"completion on one facility preserves the selected other facility draft")
|
|
_expect(StringName(_row(_rooms[0]).get(&"price_mode", &"")) == &"affordable" and int(_row(_rooms[0]).get(&"price", 0)) == 96,
|
|
"accepted price reaches facility policy and Runtime report")
|
|
_panel.apply_button.pressed.emit()
|
|
_expect(int(_row(_rooms[1]).get(&"price", 0)) == 180 and "反映" in _panel.result_label.text,
|
|
"paused submission applies another facility's own draft with Japanese confirmation")
|
|
_expect(_panel.apply_button.disabled, "applied unchanged draft cannot be submitted again")
|
|
|
|
|
|
func _test_open_close() -> void:
|
|
_panel.business_open_button.button_pressed = false
|
|
_expect(not _panel.apply_button.disabled, "closing admission is an explicit unsaved draft")
|
|
_panel.apply_button.pressed.emit()
|
|
_expect(not bool(_row(_rooms[1]).get(&"business_open", true)) and "停止" in _panel.facility_status_label.text,
|
|
"close reaches actual facility operator state and status text")
|
|
_expect("受付済みの料金は変わりません" in _panel.explanation_label.text
|
|
and "開始済みのサービス" in _panel.explanation_label.text
|
|
and "休業中も給与と施設維持費は発生" in _panel.explanation_label.text,
|
|
"UI explains fixed accepted prices, service completion, and continued wages plus upkeep during closure")
|
|
_panel.business_open_button.button_pressed = true
|
|
_panel.apply_button.pressed.emit()
|
|
_expect(bool(_row(_rooms[1]).get(&"business_open", false)), "admission can be reopened through the same controls")
|
|
_choose(_panel.facility_option, _reception)
|
|
_expect(_panel.price_option.disabled and "利用料金はありません" in _panel.quote_label.text,
|
|
"free reception has no editable price")
|
|
_panel.business_open_button.button_pressed = false
|
|
_panel.apply_button.pressed.emit()
|
|
_expect(not bool(_row(_reception).get(&"business_open", true)), "free facility still supports admission control")
|
|
_panel.business_open_button.button_pressed = true
|
|
_panel.apply_button.pressed.emit()
|
|
_expect(bool(_row(_reception).get(&"business_open", false)), "free reception can reopen without sending a price patch")
|
|
|
|
|
|
_choose(_panel.facility_option, _warehouse)
|
|
_expect(_panel.price_option.disabled and _panel.business_open_button.text == "営業する"
|
|
and "調理と新しい補給" in _panel.explanation_label.text and "荷物は保持" in _panel.explanation_label.text,
|
|
"backroom warehouse explains supply pause and retained cargo instead of guest admission")
|
|
_panel.business_open_button.button_pressed = false
|
|
_panel.apply_button.pressed.emit()
|
|
_expect(not bool(_row(_warehouse).get(&"business_open", true)), "warehouse closing updates the real operator policy")
|
|
_panel.business_open_button.button_pressed = true
|
|
_panel.apply_button.pressed.emit()
|
|
_expect(bool(_row(_warehouse).get(&"business_open", false)), "warehouse can reopen through the same business panel")
|
|
|
|
|
|
func _test_saved_pending_edit() -> void:
|
|
_choose(_panel.facility_option, _rooms[0])
|
|
_choose(_panel.price_option, &"charitable")
|
|
_clock.set_paused(false)
|
|
_panel.apply_button.pressed.emit()
|
|
var pending_before: int = _runtime.command_queue.create_snapshot().get("pending", []).size()
|
|
_panel.submit_policy()
|
|
_expect(pending_before == 1 and _runtime.command_queue.create_snapshot().get("pending", []).size() == 1,
|
|
"a second press cannot enqueue duplicate policy edits")
|
|
var saved: Dictionary = _runtime.save_to_path(_save_path)
|
|
_expect(bool(saved.get(&"ok", false)), "pending price policy saves to an isolated disk slot")
|
|
_clock.advance_minutes(1)
|
|
var loaded: Dictionary = _runtime.load_from_path(_save_path)
|
|
_expect(bool(loaded.get(&"ok", false)), "pending policy restores from disk")
|
|
_panel.refresh()
|
|
_expect(_panel.apply_button.disabled and _selected_mode() == &"charitable" and "設定後 72G" in _panel.quote_label.text,
|
|
"restored pending edit remains locked and displays its accepted draft, not stale live policy")
|
|
_clock.advance_minutes(1)
|
|
_clock.set_paused(true)
|
|
_panel.refresh()
|
|
_expect(int(_row(_rooms[0]).get(&"price", 0)) == 72 and not _panel.price_option.disabled,
|
|
"restored policy executes once and unlocks editing")
|
|
_choose(_panel.price_option, &"standard")
|
|
_panel.apply_button.pressed.emit()
|
|
_expect(int(_row(_rooms[0]).get(&"price", 0)) == 120,
|
|
"restored command key sequence permits a genuinely new policy edit")
|
|
var report := _panel.get_report()
|
|
(report[&"facilities"] as Array).clear()
|
|
_panel.refresh()
|
|
_expect(not (_panel.get_report().get(&"facilities", []) as Array).is_empty(), "read model returned to callers is defensive")
|
|
|
|
|
|
func _test_navigation_and_layout() -> void:
|
|
for entry: Array in [[&"ledger", "operating_ledger_panel"], [&"product", "supply_management_panel"], [&"staff", "staff_management_panel"]]:
|
|
_main.call("_activate_management_mode", entry[0])
|
|
_expect(not _panel.visible and (_main.get(entry[1]) as Control).visible, "%s exclusively replaces business controls" % entry[0])
|
|
_main.call("_activate_management_mode", &"business")
|
|
_expect(_panel.visible and not (_main.get(entry[1]) as Control).visible, "business exclusively replaces %s controls" % entry[0])
|
|
_main.call("_unhandled_key_input", _key(KEY_H))
|
|
_expect(not _panel.visible and (_main.get("staff_candidate_panel") as Control).visible, "H replaces business with hiring candidates")
|
|
_main.call("_activate_management_mode", &"business")
|
|
_choose(_panel.facility_option, _rooms[1])
|
|
_main.call("_set_layer", 0)
|
|
await process_frame
|
|
await process_frame
|
|
var side := _main.find_child("SidePanel", true, false) as Control
|
|
var guide := _main.find_child("InnOpeningGuide", true, false) as Control
|
|
_expect(not side.get_global_rect().intersects(guide.get_global_rect()), "1280x720 business inspector stays clear of opening guidance")
|
|
for control: Control in [_panel, _panel.facility_option, _panel.price_option, _panel.business_open_button, _panel.apply_button]:
|
|
_expect(control.get_global_rect().position.x >= side.get_global_rect().position.x
|
|
and control.get_global_rect().end.x <= side.get_global_rect().end.x, "%s fits inside narrow sidebar" % control.name)
|
|
if OS.get_environment("THE_TREE_BUSINESS_UI_CAPTURE") == "1":
|
|
var requested_capture := OS.get_environment("THE_TREE_BUSINESS_CAPTURE_DIR")
|
|
var capture_dir := ProjectSettings.globalize_path(requested_capture if not requested_capture.is_empty() else "res://artifacts/business-management")
|
|
DirAccess.make_dir_recursive_absolute(capture_dir)
|
|
await RenderingServer.frame_post_draw
|
|
root.get_texture().get_image().save_png(capture_dir.path_join("business-panel.png"))
|
|
_choose(_panel.facility_option, _warehouse)
|
|
_main.call("_set_layer", 1)
|
|
await process_frame
|
|
await RenderingServer.frame_post_draw
|
|
root.get_texture().get_image().save_png(capture_dir.path_join("warehouse-policy.png"))
|
|
if OS.get_environment("THE_TREE_PANEL_SKIN_CAPTURE") == "1":
|
|
_main.call("_set_layer", 1)
|
|
await process_frame;await RenderingServer.frame_post_draw
|
|
_expect(root.get_texture().get_image().save_png(ProjectSettings.globalize_path("res://artifacts/facility-dimensions-2026-09-29/business-skin-back.png")) == OK, "capture BACK management skin")
|
|
_main.call("_unhandled_key_input", _key(KEY_ESCAPE))
|
|
_expect(not _panel.visible and not bool(_main.get("_management_panel_open")), "Escape closes business inspector and restores full map")
|
|
|
|
|
|
func _row(id: StringName) -> Dictionary:
|
|
for row: Dictionary in _runtime.get_business_management_report().get(&"facilities", []):
|
|
if StringName(row.get(&"facility_id", &"")) == id:
|
|
return row
|
|
return {}
|
|
|
|
|
|
func _selected_mode() -> StringName:
|
|
return StringName(_panel.price_option.get_item_metadata(_panel.price_option.selected))
|
|
|
|
|
|
func _choose(picker: OptionButton, id: StringName) -> void:
|
|
for index: int in picker.item_count:
|
|
if StringName(picker.get_item_metadata(index)) == id:
|
|
picker.select(index)
|
|
picker.item_selected.emit(index)
|
|
return
|
|
_expect(false, "requested business dropdown value exists: %s" % id)
|
|
|
|
|
|
func _key(code: Key) -> InputEventKey:
|
|
var event := InputEventKey.new()
|
|
event.keycode = code
|
|
event.pressed = true
|
|
return event
|
|
|
|
|
|
func _expect(condition: bool, message: String) -> void:
|
|
_checks += 1
|
|
if not condition:
|
|
_failures += 1
|
|
push_error(message)
|
|
|
|
|
|
func _finish() -> void:
|
|
SaveService.delete_save(_save_path)
|
|
var teardown: Dictionary = await AudioTeardown.free_owner_and_wait(self, _main)
|
|
_expect(bool(teardown.get(&"passed", false)) and not bool(teardown.get(&"deadline_reached", true)), "owned audio references release before bounded teardown deadline with whole Runtime unchanged")
|
|
print("Business management UI: %d checks, %d failures" % [_checks, _failures])
|
|
quit(1 if _failures > 0 else 0)
|
|
|
|
|
|
func _test_customer_counts() -> void:
|
|
# Controlled multi-person read-model fixture; no simulated time advances.
|
|
var saved: Dictionary = _runtime.facility_service.create_snapshot()
|
|
var room: FacilityInstance = _runtime.facility_service.get_instance(_rooms[1])
|
|
room.active_services.clear()
|
|
room.customer_capacity = 3
|
|
var group := ServiceInstance.new().configure(&"group", &"group_request", room.instance_id, _clock.absolute_minute, 60, [&"group_a", &"group_b"], [])
|
|
room.active_services[&"group"] = group
|
|
var request := ServiceRequest.new().configure(&"waiting_group", room.instance_id, [&"waiting_a", &"waiting_b", &"waiting_c"], &"default", _clock.absolute_minute)
|
|
_expect(_runtime.facility_service.enqueue_request(request).is_success(), "queue a three-person request for the occupancy read model")
|
|
var row := _row(room.instance_id)
|
|
_expect(row[&"active_count"] == 1 and row[&"active_customer_count"] == 2, "one grouped service reports two users separately from service count")
|
|
_expect(row[&"queued_count"] == 1 and row[&"queued_customer_count"] == 3, "one queued request reports three waiting people")
|
|
_panel.refresh()
|
|
_choose(_panel.facility_option, room.instance_id)
|
|
_expect("待ち 3人 / 利用中 2人" in _panel.facility_status_label.text and "上限 3人" in _panel.facility_status_label.text, "business screen shows people and explicit capacity")
|
|
room.customer_capacity = -1
|
|
_panel.refresh()
|
|
_expect("上限 未設定" in _panel.facility_status_label.text, "unset customer limit is never replaced by generic capacity")
|
|
room.active_services.clear()
|
|
room.customer_capacity = 0
|
|
_panel.refresh()
|
|
_expect("利用中 0人" in _panel.facility_status_label.text and "上限 0人" in _panel.facility_status_label.text, "zero limit remains distinct from an unspecified limit")
|
|
_expect(_runtime.facility_service.restore_snapshot(saved), "discard controlled occupancy fixture without changing later gameplay tests")
|
|
_panel.refresh()
|
|
|
|
|
|
func _fixture_principal(id: StringName, amount: int) -> void:
|
|
var account := StringName("LOAN_LIABILITY:" + String(id))
|
|
_expect(_runtime.economy_service.register_account(account, true), "explicit existing-debt fixture account")
|
|
_expect(_runtime.economy_service.transfer(account, &"PLAYER_CASH", amount, _clock.absolute_minute, &"LOAN_PRINCIPAL_BORROWED", id, StringName("fixture.borrow." + String(id))).is_success(), "explicit borrowed-principal fixture; not production loan issuance")
|
|
|
|
|
|
func _loan_remaining(id: StringName) -> int:
|
|
return -_runtime.economy_service.get_balance(StringName("LOAN_LIABILITY:" + String(id)))
|
|
|
|
|
|
func _test_principal_repayment_controls() -> void:
|
|
# Real Main repayment controls and real ledger; issuance is an explicit fixture.
|
|
# Restore the original business test afterwards; no production bank terms added.
|
|
var baseline: Dictionary = _runtime.create_snapshot()
|
|
var ledger: OperatingLedgerPanel = _main.get("operating_ledger_panel")
|
|
_clock.set_paused(true)
|
|
_main.call("_activate_management_mode", &"ledger")
|
|
ledger.refresh()
|
|
_expect(not ledger.get_node("LoanRepaymentControls").visible and "借入はありません" in ledger.get_node("LoanRepaymentHint").text, "default ledger shows no fabricated loan or borrow contract")
|
|
var before_projection: Dictionary = _runtime.create_snapshot()
|
|
var empty_report: Dictionary = _runtime.get_loan_principal_report()
|
|
_expect(_runtime.create_snapshot() == before_projection and empty_report[&"loans"].is_empty(), "existing-debt read model does not mutate Runtime")
|
|
_fixture_principal(&"ui.control.a", 1000)
|
|
_fixture_principal(&"ui.control.b", 600)
|
|
ledger.refresh()
|
|
_expect(ledger.loan_option.item_count == 2 and ledger.get_node("LoanRepaymentControls").visible, "two attested liabilities populate real Main controls")
|
|
var report: Dictionary = _runtime.get_loan_principal_report()
|
|
(report[&"loans"] as Array).clear()
|
|
_expect(_runtime.get_loan_principal_report()[&"loans"].size() == 2, "returned debt list is defensive")
|
|
_choose(ledger.loan_option, &"ui.control.a")
|
|
var cash: int = _runtime.economy_service.get_balance(&"PLAYER_CASH")
|
|
await _enter_principal_amount(ledger.loan_amount, "300")
|
|
_expect(not ledger.repay_button.disabled, "real text input enables an affordable principal request")
|
|
await _click_principal_button(ledger.repay_button)
|
|
_principal_proof("ui-pointer-result", {&"pid": OS.get_process_id(), &"loan_remaining": _loan_remaining(&"ui.control.a"), &"input": &"Viewport mouse left press/release"})
|
|
_expect(_loan_remaining(&"ui.control.a") == 700 and _loan_remaining(&"ui.control.b") == 600, "specified payment changes only selected liability")
|
|
_expect(_runtime.economy_service.get_balance(&"PLAYER_CASH") == cash - 300 and "300Gを返済" in ledger.get_node("LoanRepaymentResult").text, "real Main submitter posts exact principal and shows completed result")
|
|
for invalid: String in ["0", "-1", "2.5", "701", "abc"]:
|
|
_set_principal_amount(ledger, invalid)
|
|
_expect(ledger.repay_button.disabled, "invalid or excessive amount disables payment: " + invalid)
|
|
var before: Dictionary = _runtime.create_snapshot()
|
|
ledger.submit_repayment(false)
|
|
_expect(_runtime.create_snapshot() == before, "invalid UI request is whole Runtime noop: " + invalid)
|
|
_choose(ledger.loan_option, &"ui.control.b")
|
|
ledger.repay_all_button.pressed.emit()
|
|
_expect(_loan_remaining(&"ui.control.b") == 0 and ledger.repay_button.disabled and ledger.repay_all_button.disabled, "full payment settles selected loan and locks paid controls")
|
|
var paid_state: Dictionary = _runtime.create_snapshot()
|
|
ledger.submit_repayment(true)
|
|
_expect(_runtime.create_snapshot() == paid_state, "repeat paid-loan press is whole Runtime noop")
|
|
_choose(ledger.loan_option, &"ui.control.a")
|
|
_set_principal_amount(ledger, "200")
|
|
_clock.set_paused(false)
|
|
ledger.repay_button.pressed.emit()
|
|
_expect(_runtime.command_queue.pending_count() == 1 and ledger.repay_button.disabled and ledger.repay_all_button.disabled, "running-clock repayment queues once and locks both buttons")
|
|
var queued: Dictionary = _runtime.create_snapshot()
|
|
_principal_proof("ui-queued", queued)
|
|
ledger.submit_repayment(false)
|
|
ledger.submit_repayment(true)
|
|
_expect(_runtime.create_snapshot() == queued, "two repeated pending presses do not add commands or change any state")
|
|
var detached: Array[SimulationCommand] = _runtime.command_queue.get_pending_commands()
|
|
detached[0].payload[&"amount"] = 999
|
|
_expect(int(_runtime.command_queue.get_pending_commands()[0].payload[&"amount"]) == 200, "pending projection is a deep defensive command copy")
|
|
var slot := _save_path + "_loan"
|
|
_expect(bool(_runtime.save_to_path(slot).get(&"ok", false)), "UI pending repayment uses formal SaveService")
|
|
var exact_slot := slot + "_exact"
|
|
_expect(bool(SaveService.save_snapshot(exact_slot, _runtime.create_snapshot()).get(&"ok", false)), "post-save exact state stored by regular SaveService")
|
|
_clock.advance_minutes(1)
|
|
_clock.set_paused(true)
|
|
_expect(_loan_remaining(&"ui.control.a") == 500, "queued UI payment executes once")
|
|
var completed: Dictionary = _runtime.create_snapshot()
|
|
_principal_proof("ui-first-continued", completed)
|
|
var exact: Dictionary = SaveService.load_snapshot(exact_slot)
|
|
_expect(bool(exact.get(&"ok", false)), "regular SaveService reads exact UI pending state")
|
|
_expect(_runtime.restore_snapshot(exact[&"snapshot"]), "exact pending UI snapshot restores all Runtime participants")
|
|
ledger.refresh()
|
|
_principal_proof("ui-exact-restored-pending", _runtime.create_snapshot())
|
|
_expect(ledger.repay_button.disabled and ledger.repay_all_button.disabled, "exact restored pending payment remains locked")
|
|
_clock.advance_minutes(1)
|
|
_clock.set_paused(true)
|
|
_principal_proof("ui-exact-continued", _runtime.create_snapshot())
|
|
_expect(var_to_bytes(_runtime.create_snapshot()) == var_to_bytes(completed), "regular SaveService pending UI continuation is whole typed byte exact")
|
|
if not OS.get_environment("THE_TREE_COMMAND_RESTART_EVIDENCE").is_absolute_path(): SaveService.delete_save(exact_slot)
|
|
_expect(bool(_runtime.load_from_path(slot).get(&"ok", false)), "formal UI pending repayment restores")
|
|
ledger.refresh()
|
|
_principal_proof("ui-restored-pending", _runtime.create_snapshot())
|
|
_expect(ledger.repay_button.disabled and ledger.repay_all_button.disabled and "受理済み" in ledger.get_node("LoanRepaymentHint").text, "restored pending debt remains locked without a local draft")
|
|
_clock.advance_minutes(1)
|
|
_clock.set_paused(true)
|
|
_principal_proof("ui-second-continued", _runtime.create_snapshot())
|
|
_expect(_same_principal_facade_state(completed, _runtime.create_snapshot()) and _loan_remaining(&"ui.control.a") == 500, "facade continuation matches whole state except one strictly attested save-versus-load I/O receipt")
|
|
ledger.refresh()
|
|
_set_principal_amount(ledger, "100")
|
|
ledger.repay_button.pressed.emit()
|
|
_expect(_loan_remaining(&"ui.control.a") == 400, "new UI payment after Load uses persisted next-command identity")
|
|
var reopened := OperatingLedgerPanel.new()
|
|
_main.add_child(reopened)
|
|
reopened.setup(_runtime, _clock)
|
|
_choose(reopened.loan_option, &"ui.control.a")
|
|
_set_principal_amount(reopened, "100")
|
|
reopened.repay_button.pressed.emit()
|
|
_expect(_loan_remaining(&"ui.control.a") == 300, "recreated panel permits new payment without reusing earlier UI key")
|
|
reopened.free()
|
|
ledger.refresh()
|
|
_set_principal_amount(ledger, "200")
|
|
_clock.set_paused(false)
|
|
ledger.repay_button.pressed.emit()
|
|
var available: int = _runtime.economy_service.get_balance(&"PLAYER_CASH")
|
|
_expect(_runtime.submit_command(&"spend_resources", {&"cost": {&"gold": available - 100}}, &"player", &"", 75).is_success(), "another ordinary expense is accepted ahead of the queued payment")
|
|
_principal_proof("ui-cash-race-before", _runtime.create_snapshot())
|
|
_clock.advance_minutes(1)
|
|
_clock.set_paused(true)
|
|
ledger.refresh()
|
|
_principal_proof("ui-cash-race-after", _runtime.create_snapshot())
|
|
_expect(_loan_remaining(&"ui.control.a") == 300 and _runtime.economy_service.get_balance(&"PLAYER_CASH") == 100, "insufficient execution-time funds leave principal and remaining cash intact")
|
|
_expect("実行できません" in ledger.get_node("LoanRepaymentResult").text and ledger.repay_button.disabled and ledger.repay_all_button.disabled, "failed queued payment shows failure and disables unaffordable amounts")
|
|
var blocked: Dictionary = _runtime.create_snapshot()
|
|
ledger.submit_repayment(true)
|
|
_expect(var_to_bytes(_runtime.create_snapshot()) == var_to_bytes(blocked), "unaffordable full payment is whole typed state noop")
|
|
_set_principal_amount(ledger, "50")
|
|
ledger.repay_button.pressed.emit()
|
|
_expect(_loan_remaining(&"ui.control.a") == 250 and _runtime.economy_service.get_balance(&"PLAYER_CASH") == 50, "new affordable request retries successfully after the queued failure")
|
|
var evidence_dir := OS.get_environment("THE_TREE_COMMAND_RESTART_EVIDENCE")
|
|
if evidence_dir.is_absolute_path():
|
|
var file := FileAccess.open(evidence_dir.path_join("ui-formal-inputs.json"), FileAccess.WRITE)
|
|
assert(file != null)
|
|
file.store_string(JSON.stringify({"pid": OS.get_process_id(), "facade_slot": ProjectSettings.globalize_path(slot), "exact_slot": ProjectSettings.globalize_path(exact_slot)}))
|
|
file.flush()
|
|
else:
|
|
SaveService.delete_save(slot)
|
|
_expect(_runtime.restore_snapshot(baseline), "discard explicit bank fixture and restore original business Runtime")
|
|
ledger.refresh()
|
|
_expect(not ledger.get_node("LoanRepaymentControls").visible, "loaded loan-free state clears old debt controls")
|
|
await process_frame
|
|
|
|
|
|
func _principal_proof(label: String, value: Dictionary) -> void:
|
|
var directory := OS.get_environment("THE_TREE_COMMAND_RESTART_EVIDENCE")
|
|
if not directory.is_absolute_path(): return
|
|
assert(DirAccess.make_dir_recursive_absolute(directory) == OK)
|
|
var file := FileAccess.open(directory.path_join(label + ".var"), FileAccess.WRITE)
|
|
assert(file != null)
|
|
file.store_buffer(var_to_bytes(value))
|
|
file.flush()
|
|
|
|
|
|
func _same_principal_facade_state(before: Dictionary, after: Dictionary) -> bool:
|
|
# Keep both real snapshots intact; construct the exact expected I/O receipt.
|
|
# Direct SaveService above separately requires entire var_to_bytes equality.
|
|
var expected: Dictionary = before.duplicate(true)
|
|
var left: Array = expected[&"systems"][&"runtime"][&"domain_events"]["recent_events"]
|
|
var right: Array = after[&"systems"][&"runtime"][&"domain_events"]["recent_events"]
|
|
if left.size() != right.size(): return false
|
|
var receipts := 0
|
|
for index: int in left.size():
|
|
if left[index] == right[index]: continue
|
|
var old_event: Dictionary = left[index]
|
|
var new_event: Dictionary = right[index]
|
|
if StringName(old_event["event_type"]) != &"SAVE_CHECKPOINT_SUCCEEDED" or StringName(new_event["event_type"]) != &"SAVE_LOADED": return false
|
|
var old_payload: Dictionary = old_event["payload"]
|
|
var new_payload: Dictionary = new_event["payload"]
|
|
if StringName(old_payload[&"operation"]) != &"save" or StringName(new_payload[&"operation"]) != &"load" or StringName(old_payload[&"reason"]) != &"manual" or StringName(new_payload[&"reason"]) != &"": return false
|
|
for key: StringName in [&"duration_us", &"runtime_duration_us"]:
|
|
if typeof(new_payload.get(key)) != TYPE_INT or int(new_payload[key]) < 0: return false
|
|
old_payload[key] = new_payload[key]
|
|
old_payload[&"operation"] = &"load"
|
|
old_payload[&"reason"] = &""
|
|
old_event["event_type"] = new_event["event_type"]
|
|
if old_event != new_event: return false
|
|
receipts += 1
|
|
return receipts == 1 and expected == after
|
|
|
|
|
|
func _set_principal_amount(ledger: OperatingLedgerPanel, text: String) -> void:
|
|
# Programmatic fixture assignments do not emit LineEdit.text_changed.
|
|
ledger.loan_amount.text = text
|
|
ledger.loan_amount.text_changed.emit(text)
|
|
|
|
|
|
func _enter_principal_amount(edit: LineEdit, text: String) -> void:
|
|
await _click_principal_button(edit)
|
|
for character: String in text:
|
|
for pressed: bool in [true, false]:
|
|
var event := InputEventKey.new()
|
|
event.unicode = character.unicode_at(0)
|
|
event.keycode = character.unicode_at(0)
|
|
event.pressed = pressed
|
|
root.push_input(event, true)
|
|
await process_frame
|
|
_expect(edit.text == text, "viewport key input enters the requested repayment amount")
|
|
|
|
|
|
func _click_principal_button(control: Control) -> void:
|
|
var ancestor: Node = control.get_parent()
|
|
while ancestor != null:
|
|
if ancestor is ScrollContainer: ancestor.ensure_control_visible(control)
|
|
ancestor = ancestor.get_parent()
|
|
await process_frame
|
|
var point := control.get_global_rect().get_center()
|
|
var motion := InputEventMouseMotion.new()
|
|
motion.position = point
|
|
motion.global_position = point
|
|
root.push_input(motion, true)
|
|
for pressed: bool in [true, false]:
|
|
var event := InputEventMouseButton.new()
|
|
event.button_index = MOUSE_BUTTON_LEFT
|
|
event.pressed = pressed
|
|
event.position = point
|
|
event.global_position = point
|
|
event.button_mask = MOUSE_BUTTON_MASK_LEFT if pressed else 0
|
|
root.push_input(event, true)
|
|
await process_frame
|