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)