202 lines
9.5 KiB
GDScript
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)
|