218 lines
9.9 KiB
GDScript
218 lines
9.9 KiB
GDScript
extends SceneTree
|
|
|
|
const EntryShell = preload("res://entry/entry_shell.gd")
|
|
const SettingsStore = preload("res://entry/settings_store.gd")
|
|
const DraftService = preload("res://entry/initial_company_draft_service.gd")
|
|
|
|
const DRAFT_PATH := "user://dev2_company_commit_draft.json"
|
|
const COMPANY_PATH := "user://dev2_company_commit_company.json"
|
|
const SETTINGS_PATH := "user://dev2_company_commit_settings.json"
|
|
const SEED := "13579bdf2468ace00eca8642fdb97531"
|
|
|
|
var failures: Array[String] = []
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("run_test")
|
|
|
|
|
|
func run_test() -> void:
|
|
_cleanup()
|
|
await verify_review_commit_handoff_and_restart()
|
|
verify_idempotency_lingering_draft_and_candidate_recovery()
|
|
await verify_corrupt_company_blocks_reroll()
|
|
_cleanup()
|
|
if failures.is_empty():
|
|
print("ABG_DEV2_COMMIT_OK review=true revise=true selected=2 unchosen=0 atomic=true idempotent=true candidate_recovery=true lingering_draft=true corrupt_blocks_reroll=true guild=true reasons=true settings_return=true continue=true restart=true")
|
|
quit(0)
|
|
else:
|
|
for failure in failures:
|
|
push_error(failure)
|
|
quit(1)
|
|
|
|
|
|
func verify_review_commit_handoff_and_restart() -> void:
|
|
var service: RefCounted = _service()
|
|
var created: Dictionary = service.begin_or_load(SEED)
|
|
var selected_offer_ids := [String(created.draft.offers[1].offer_id), String(created.draft.offers[3].offer_id)]
|
|
var selected_recruit_ids := [String(created.draft.offers[1].recruit_id), String(created.draft.offers[3].recruit_id)]
|
|
var unchosen_recruit_ids := [String(created.draft.offers[0].recruit_id), String(created.draft.offers[2].recruit_id)]
|
|
var saved: Dictionary = service.set_selection(selected_offer_ids)
|
|
expect(saved.ok and saved.draft.selected_offer_ids == selected_offer_ids, "fixture persists the intended pair")
|
|
|
|
var shell = await _shell()
|
|
shell.show_initial_company()
|
|
await _frames(3)
|
|
var page = shell.initial_company_page
|
|
page.request_review()
|
|
await _frames(2)
|
|
expect(page.mode == "review" and page.find_child("ReviewSelected", true, false).get_child_count() == 2, "Review names exactly two selected offers")
|
|
expect(page.find_child("ReviewUnchosen", true, false).get_child_count() >= 4, "Review names both unchosen provisional offers")
|
|
page.revise_selection()
|
|
await _frames(2)
|
|
expect(page.mode == "choice" and page.draft.selected_offer_ids == selected_offer_ids, "Revise preserves the same draft and pair")
|
|
page.request_review()
|
|
page.create_company()
|
|
await _frames(4)
|
|
expect(shell.current_screen == "guild_handoff" and shell.find_child("GuildMasterDesignCanvas", true, false) != null, "Create Company reaches the bounded Guild handoff")
|
|
var loaded: Dictionary = service.load_company()
|
|
expect(loaded.ok and service.validate_company(loaded.company).ok, "committed Company reloads and validates")
|
|
var actual_recruit_ids: Array[String] = []
|
|
for recruit in loaded.company.owned_recruits:
|
|
actual_recruit_ids.append(String(recruit.recruit_id))
|
|
expect(actual_recruit_ids == selected_recruit_ids, "Company owns exactly the selected Recruit IDs in offer order")
|
|
var company_json := JSON.stringify(loaded.company)
|
|
for recruit_id in unchosen_recruit_ids:
|
|
expect(not company_json.contains(recruit_id), "unchosen Recruit %s is not stored" % recruit_id)
|
|
expect(not FileAccess.file_exists(DRAFT_PATH) and not FileAccess.file_exists("%s.bak" % DRAFT_PATH), "successful commit removes draft replicas")
|
|
expect(not shell.find_child("GuildContractBoard", true, false).disabled and shell.find_child("GuildMarketUnavailable", true, false).disabled, "current Guild enables Contract Board while Market waits for Contract creation")
|
|
expect(not shell.find_child("GuildContractReason", true, false).text.is_empty() and not shell.find_child("GuildMarketReason", true, false).text.is_empty(), "Guild destinations have truthful availability reasons")
|
|
shell.show_settings_from_guild()
|
|
await _frames(2)
|
|
shell.request_settings_back()
|
|
await _frames(2)
|
|
expect(shell.current_screen == "guild_handoff", "Settings returns to the Guild caller")
|
|
shell.back_to_title()
|
|
await _frames(2)
|
|
expect(shell.find_child("TitleNewGame", true, false).disabled and not shell.find_child("TitleContinue", true, false).disabled, "Title disables New Game and enables Continue after Company creation")
|
|
shell.open_continue_boundary()
|
|
await _frames(2)
|
|
expect(shell.current_screen == "guild_handoff", "Continue returns to the committed Guild")
|
|
shell.queue_free()
|
|
await _frames(3)
|
|
|
|
var settings_store = SettingsStore.new()
|
|
settings_store.path = SETTINGS_PATH
|
|
var settings := settings_store.defaults()
|
|
settings.setup_completed = true
|
|
settings.locale = "en"
|
|
settings_store.save_settings(settings)
|
|
var restarted = EntryShell.new()
|
|
restarted.auto_start = false
|
|
restarted.settings_store.path = SETTINGS_PATH
|
|
restarted.initial_company_service.path = DRAFT_PATH
|
|
restarted.initial_company_service.company_path = COMPANY_PATH
|
|
root.add_child(restarted)
|
|
await _frames(2)
|
|
restarted._open_after_boot()
|
|
await _frames(3)
|
|
expect(restarted.current_screen == "guild_handoff" and restarted.find_child("GuildRoster", true, false).get_child_count() == 2, "restart resumes the committed Guild with two members")
|
|
restarted.queue_free()
|
|
await _frames(3)
|
|
|
|
|
|
func verify_idempotency_lingering_draft_and_candidate_recovery() -> void:
|
|
var service: RefCounted = _service()
|
|
var company_loaded: Dictionary = service.load_company()
|
|
var company: Dictionary = company_loaded.company.duplicate(true)
|
|
var original_receipt := String(company.creation_receipt.receipt_hash)
|
|
var duplicate: Dictionary = service.commit_company()
|
|
expect(duplicate.ok and duplicate.code == "already_committed" and String(duplicate.company.creation_receipt.receipt_hash) == original_receipt, "duplicate commit returns the original receipt")
|
|
|
|
var draft_service: RefCounted = _service()
|
|
_remove_company_files()
|
|
var recreated: Dictionary = draft_service.begin_or_load(SEED)
|
|
var ids := [String(recreated.draft.offers[1].offer_id), String(recreated.draft.offers[3].offer_id)]
|
|
draft_service.set_selection(ids)
|
|
var lingering_draft: Dictionary = draft_service.load_draft().draft.duplicate(true)
|
|
var committed: Dictionary = draft_service.commit_company()
|
|
expect(committed.ok, "lingering-draft fixture commits Company")
|
|
_write(DRAFT_PATH, JSON.stringify(_canonical(lingering_draft)))
|
|
var replay: Dictionary = draft_service.commit_company()
|
|
expect(replay.ok and replay.code == "already_committed" and not FileAccess.file_exists(DRAFT_PATH), "matching lingering draft is cleaned without a second Company")
|
|
|
|
var persisted: Dictionary = draft_service.load_company().company.duplicate(true)
|
|
_remove_company_files()
|
|
_write("%s.candidate" % COMPANY_PATH, JSON.stringify(_canonical(persisted)))
|
|
var recovered: Dictionary = draft_service.load_company()
|
|
expect(recovered.ok and recovered.code == "recovered_candidate", "interrupted Company candidate promotes on restart")
|
|
expect(String(recovered.company.creation_receipt.receipt_hash) == String(persisted.creation_receipt.receipt_hash), "candidate recovery preserves the exact receipt")
|
|
|
|
|
|
func verify_corrupt_company_blocks_reroll() -> void:
|
|
_remove_company_files()
|
|
_write(COMPANY_PATH, "{broken")
|
|
var service: RefCounted = _service()
|
|
var blocked: Dictionary = service.begin_or_load(SEED)
|
|
expect(not blocked.ok and blocked.code == "COMPANY_CORRUPT", "corrupt Company blocks begin_or_load instead of rerolling")
|
|
expect(not FileAccess.file_exists(DRAFT_PATH), "corrupt Company does not create a replacement draft")
|
|
var shell = await _shell()
|
|
shell.show_title()
|
|
await _frames(2)
|
|
expect(shell.find_child("TitleNewGame", true, false).disabled and shell.find_child("TitleContinue", true, false).disabled, "Title blocks New Game and Continue while Company data is corrupt")
|
|
expect("attention" in shell.find_child("TitleContinueReason", true, false).text and shell.find_child("TitleSettings", true, false).has_focus(), "Title shows the recovery reason and focuses an available action")
|
|
shell.open_new_game_boundary()
|
|
await _frames(2)
|
|
expect(shell.current_screen == "title" and "COMPANY_CORRUPT" in shell.find_child("TitleNotice", true, false).text, "New Game boundary reports the corrupt Company without creating offers")
|
|
shell.queue_free()
|
|
await _frames(3)
|
|
|
|
|
|
func _shell():
|
|
var shell = EntryShell.new()
|
|
shell.auto_start = false
|
|
shell.initial_company_service.path = DRAFT_PATH
|
|
shell.initial_company_service.company_path = COMPANY_PATH
|
|
root.add_child(shell)
|
|
await _frames(2)
|
|
shell.settings = SettingsStore.new().defaults()
|
|
shell.settings.setup_completed = true
|
|
shell.settings.locale = "en"
|
|
return shell
|
|
|
|
|
|
func _service() -> RefCounted:
|
|
var service: RefCounted = DraftService.new()
|
|
service.path = DRAFT_PATH
|
|
service.company_path = COMPANY_PATH
|
|
return service
|
|
|
|
|
|
func _write(file_path: String, text: String) -> void:
|
|
var file := FileAccess.open(file_path, FileAccess.WRITE)
|
|
file.store_string(text)
|
|
file.close()
|
|
|
|
|
|
func _remove_company_files() -> void:
|
|
for suffix in ["", ".bak", ".candidate", ".bak.candidate", ".corrupt"]:
|
|
var file_path := "%s%s" % [COMPANY_PATH, suffix]
|
|
if FileAccess.file_exists(file_path):
|
|
DirAccess.remove_absolute(file_path)
|
|
|
|
|
|
func _cleanup() -> void:
|
|
for base in [DRAFT_PATH, COMPANY_PATH, SETTINGS_PATH]:
|
|
for suffix in ["", ".bak", ".candidate", ".bak.candidate", ".corrupt"]:
|
|
var file_path := "%s%s" % [base, suffix]
|
|
if FileAccess.file_exists(file_path):
|
|
DirAccess.remove_absolute(file_path)
|
|
|
|
|
|
func _canonical(value):
|
|
if value is float and is_equal_approx(value, round(value)):
|
|
return int(round(value))
|
|
if value is Dictionary:
|
|
var result := {}
|
|
var keys: Array = value.keys()
|
|
keys.sort_custom(func(left, right): return String(left) < String(right))
|
|
for key in keys:
|
|
result[key] = _canonical(value[key])
|
|
return result
|
|
if value is Array:
|
|
var result: Array = []
|
|
for item in value:
|
|
result.append(_canonical(item))
|
|
return result
|
|
return value
|
|
|
|
|
|
func _frames(count: int) -> void:
|
|
for frame in range(count):
|
|
await process_frame
|
|
|
|
|
|
func expect(condition: bool, message: String) -> void:
|
|
if not condition:
|
|
failures.append("DEV-2 Company commit: %s" % message)
|