Files
aetherbound-guild/runtime/tests/dev2/initial_company_draft_test.gd
T

202 lines
9.5 KiB
GDScript

extends SceneTree
const DraftService = preload("res://entry/initial_company_draft_service.gd")
const DRAFT_PATH := "user://dev2_initial_company_draft_test.json"
const SEED := "0123456789abcdeffedcba9876543210"
const OTHER_SEED := "11111111111111112222222222222222"
var failures: Array[String] = []
func _initialize() -> void:
call_deferred("run_test")
func run_test() -> void:
_cleanup()
verify_seed_and_validation_boundaries()
verify_create_restart_selection_and_no_company()
verify_invalid_records_and_backup_recovery()
verify_interrupted_candidate_recovery()
_cleanup()
if failures.is_empty():
print("ABG_DEV2_DRAFT_OK created=true offers=4 restart=true no_reroll=true selected=true invalid=true backup=true no_company=true")
quit(0)
else:
for failure in failures:
push_error(failure)
quit(1)
func verify_seed_and_validation_boundaries() -> void:
var service: RefCounted = _service()
var invalid: Dictionary = service.begin_or_load("not-a-seed")
expect(not invalid.ok and invalid.code == "DRAFT_SEED_INVALID", "invalid seed is rejected without creating data")
expect(not FileAccess.file_exists(DRAFT_PATH), "invalid seed leaves the draft path absent")
var created: Dictionary = service.begin_or_load(SEED)
expect(created.ok and created.code == "created", "a valid seed creates one draft [%s/%s]" % [String(created.get("code", "missing_code")), String(created.get("reason", "no_reason"))])
if not created.ok:
_cleanup()
return
expect(service.validate_draft(created.draft).ok, "created draft passes its public validator")
expect(created.draft.offers.size() == 4, "created draft contains exactly four offers")
var offer_ids: Array[String] = []
var recruit_ids: Array[String] = []
for index in range(4):
var offer: Dictionary = created.draft.offers[index]
expect(int(offer.offer_index) == index, "offer %d keeps its fixed position" % index)
expect(offer.attributes.values().reduce(func(total, amount): return total + int(amount), 0) == 35, "offer %d uses the equal numeric attribute budget" % index)
expect(not String(offer.trait_benefit).is_empty() and not String(offer.trait_cost).is_empty(), "offer %d has one Trait benefit and cost" % index)
expect(String(offer.starting_item_state) == "equipped", "offer %d has a truthful starting-item state" % index)
offer_ids.append(String(offer.offer_id))
recruit_ids.append(String(offer.recruit_id))
expect(_unique(offer_ids) and _unique(recruit_ids), "offer and Recruit IDs are unique")
_cleanup()
func verify_create_restart_selection_and_no_company() -> void:
var first: RefCounted = _service()
var created: Dictionary = first.begin_or_load(SEED)
if not created.ok:
expect(false, "restart fixture creates the initial draft [%s/%s]" % [String(created.get("code", "missing_code")), String(created.get("reason", "no_reason"))])
return
var original_hash := String(created.draft.offer_set_hash)
var original_ids := _offer_ids(created.draft)
var one: Dictionary = first.set_selection([original_ids[2]])
expect(one.ok and one.draft.selected_offer_ids == [original_ids[2]], "one selection persists without committing")
var two: Dictionary = first.set_selection([original_ids[3], original_ids[0]])
expect(two.ok and two.draft.selected_offer_ids == [original_ids[0], original_ids[3]], "two selections normalize by fixed offer position")
var selection_revision := int(two.draft.selection_revision)
var unchanged: Dictionary = first.set_selection([original_ids[3], original_ids[0]])
expect(unchanged.ok and unchanged.code == "selection_unchanged" and int(unchanged.draft.selection_revision) == selection_revision, "duplicate selection input is idempotent")
var too_many: Dictionary = first.set_selection([original_ids[0], original_ids[1], original_ids[2]])
expect(not too_many.ok and too_many.code == "DRAFT_SELECTION_LIMIT", "three selected IDs are rejected")
var duplicate: Dictionary = first.set_selection([original_ids[0], original_ids[0]])
expect(not duplicate.ok and duplicate.code == "DRAFT_SELECTION_DUPLICATE_ID", "duplicate selected IDs are rejected")
var unknown: Dictionary = first.set_selection(["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"])
expect(not unknown.ok and unknown.code == "DRAFT_SELECTION_UNKNOWN_ID", "unknown selected ID is rejected")
var restarted: RefCounted = _service()
var loaded: Dictionary = restarted.begin_or_load(OTHER_SEED)
expect(loaded.ok and loaded.code == "loaded", "restart loads the existing draft instead of creating another")
expect(String(loaded.draft.seed) == SEED, "restart ignores a new seed while a draft exists")
expect(String(loaded.draft.offer_set_hash) == original_hash and _offer_ids(loaded.draft) == original_ids, "restart preserves offer identity and order without reroll")
expect(loaded.draft.selected_offer_ids == [original_ids[0], original_ids[3]], "restart preserves the latest selected IDs")
for forbidden in ["company", "expedition_company", "current_run", "active_contract"]:
expect(not loaded.draft.has(forbidden), "pre-commit draft contains no %s" % forbidden)
func verify_invalid_records_and_backup_recovery() -> void:
var service: RefCounted = _service()
var loaded: Dictionary = service.load_draft()
if not loaded.ok:
expect(false, "invalid-record fixture loads the existing draft [%s]" % String(loaded.get("code", "missing_code")))
return
var missing_offer: Dictionary = loaded.draft.duplicate(true)
missing_offer.offers.pop_back()
expect(service.validate_draft(missing_offer).code == "DRAFT_OFFER_COUNT_INVALID", "missing offer is invalid")
var duplicate_offer: Dictionary = loaded.draft.duplicate(true)
duplicate_offer.offers[1].offer_id = duplicate_offer.offers[0].offer_id
duplicate_offer.offers[1].erase("canonical_output_hash")
duplicate_offer.offers[1].canonical_output_hash = _hash(duplicate_offer.offers[1])
duplicate_offer.offer_set_hash = _hash(duplicate_offer.offers)
duplicate_offer.erase("draft_hash")
duplicate_offer.draft_hash = _hash(duplicate_offer)
expect(service.validate_draft(duplicate_offer).code == "DRAFT_OFFER_ID_DUPLICATE", "duplicate offer ID is invalid even with refreshed hashes")
_write(DRAFT_PATH, "{broken primary")
var recovered: Dictionary = service.load_draft()
expect(recovered.ok and recovered.code == "recovered_backup", "valid backup repairs a corrupt primary")
expect(recovered.draft.selected_offer_ids == loaded.draft.selected_offer_ids, "backup recovery preserves current selection")
expect(service.load_draft().ok, "repaired primary remains readable on the next restart")
_write(DRAFT_PATH, "{broken primary again")
_write("%s.bak" % DRAFT_PATH, "{broken backup")
var corrupt: Dictionary = service.begin_or_load(OTHER_SEED)
expect(not corrupt.ok and corrupt.code == "DRAFT_CORRUPT", "two corrupt replicas return an explicit error instead of rerolling")
expect(FileAccess.file_exists(DRAFT_PATH) and FileAccess.file_exists("%s.bak" % DRAFT_PATH), "corrupt replicas remain available for diagnostics")
func verify_interrupted_candidate_recovery() -> void:
_cleanup()
var service: RefCounted = _service()
var created: Dictionary = service.begin_or_load(SEED)
if not created.ok:
expect(false, "interruption fixture creates a valid draft")
return
var expected_hash := String(created.draft.offer_set_hash)
var expected_ids := _offer_ids(created.draft)
_cleanup()
_write("%s.candidate" % DRAFT_PATH, JSON.stringify(_canonical(created.draft)))
var recovered: Dictionary = service.begin_or_load(OTHER_SEED)
expect(recovered.ok and recovered.code == "recovered_candidate", "a verified interrupted candidate promotes on restart")
expect(String(recovered.draft.seed) == SEED and String(recovered.draft.offer_set_hash) == expected_hash, "candidate recovery preserves the reserved seed and offer set")
expect(_offer_ids(recovered.draft) == expected_ids, "candidate recovery preserves all four fixed positions")
expect(FileAccess.file_exists(DRAFT_PATH) and not FileAccess.file_exists("%s.candidate" % DRAFT_PATH), "candidate recovery promotes one primary without leaving a pending file")
func _service() -> RefCounted:
var service: RefCounted = DraftService.new()
service.path = DRAFT_PATH
service.company_path = "%s.company" % DRAFT_PATH
return service
func _offer_ids(draft: Dictionary) -> Array[String]:
var result: Array[String] = []
for offer in draft.offers:
result.append(String(offer.offer_id))
return result
func _unique(values: Array[String]) -> bool:
var seen := {}
for value in values:
if seen.has(value):
return false
seen[value] = true
return true
func _write(file_path: String, text: String) -> void:
var file := FileAccess.open(file_path, FileAccess.WRITE)
file.store_string(text)
file.close()
func _hash(value) -> String:
return JSON.stringify(_canonical(value)).sha256_text()
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 _cleanup() -> void:
for suffix in ["", ".bak", ".candidate", ".bak.candidate", ".corrupt"]:
var file_path := "%s%s" % [DRAFT_PATH, suffix]
if FileAccess.file_exists(file_path):
DirAccess.remove_absolute(file_path)
var company_file := "%s.company%s" % [DRAFT_PATH, suffix]
if FileAccess.file_exists(company_file):
DirAccess.remove_absolute(company_file)
func expect(condition: bool, message: String) -> void:
if not condition:
failures.append("DEV-2 initial draft: %s" % message)