416 lines
24 KiB
GDScript
416 lines
24 KiB
GDScript
extends SceneTree
|
|
|
|
const ProgressionType := preload("res://scripts/progression/progression_service.gd")
|
|
|
|
var _checks := 0
|
|
var _failures := 0
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("_run")
|
|
|
|
|
|
func _run() -> void:
|
|
_test_rank_bounds_and_evidence()
|
|
_test_metrics_are_atomic_and_lifetime_monotonic()
|
|
_test_rank_one_through_seven()
|
|
_test_external_transaction_and_idempotency()
|
|
_test_failed_rollback_quarantines_progression()
|
|
_test_callback_reentry_and_internal_tamper()
|
|
_test_snapshot_round_trip_and_atomic_rejection()
|
|
_test_promotion_evidence_and_legacy_migration()
|
|
_test_input_and_history_bounds()
|
|
if _failures == 0:
|
|
print("PASS: ProgressionService (%d checks)" % _checks)
|
|
else:
|
|
push_error("FAIL: ProgressionService (%d of %d checks failed)" % [_failures, _checks])
|
|
quit(_failures)
|
|
|
|
|
|
func _test_rank_bounds_and_evidence() -> void:
|
|
var service := ProgressionType.new()
|
|
_expect(service.configure(), "progression starts at Rank 1")
|
|
_expect(service.current_rank == 1, "configured rank is authoritative")
|
|
_expect(service.get_rank_bounds(1) == {&"min_x":-12, &"max_x":12, &"min_y":0, &"max_y":6}, "Rank 1 bounds match the system specification")
|
|
_expect(service.get_rank_bounds(3) == {&"min_x":-24, &"max_x":24, &"min_y":-8, &"max_y":16}, "Rank 3 MVP bounds include eight underground levels")
|
|
_expect(service.get_rank_bounds(7) == {&"min_x":-60, &"max_x":60, &"min_y":-40, &"max_y":56}, "Rank 7 bounds match the final expansion")
|
|
_expect(service.get_rank_bounds(8).is_empty(), "unknown rank has no implicit clamped bounds")
|
|
var report := service.evaluate_rank(2)
|
|
_expect(not report[&"eligible"] and report[&"reason"] == &"REQUIREMENTS_NOT_MET", "Rank 2 reports unmet requirements")
|
|
_expect(_requirement_ids(report) == [&"reputation", &"asset_gold"], "Rank 2 evidence order is stable and player-readable")
|
|
_expect(report[&"requirements"][0][&"current"] == 0 and report[&"requirements"][0][&"required"] == 25, "eligibility evidence includes current and required values")
|
|
var skip := service.evaluate_rank(3)
|
|
_expect(not skip[&"eligible"] and skip[&"reason"] == &"TARGET_MUST_BE_NEXT_RANK", "rank skipping is rejected before metric evaluation")
|
|
|
|
|
|
func _test_metrics_are_atomic_and_lifetime_monotonic() -> void:
|
|
var service := ProgressionType.new()
|
|
service.configure()
|
|
_expect(service.update_metrics({&"reputation": 25, &"asset_gold": 10_000, &"faith_lifetime": 100}), "valid metric patch commits atomically")
|
|
var before := service.metrics.duplicate(true)
|
|
_expect(not service.update_metrics({&"reputation": 101, &"asset_gold": 20_000}), "out-of-range percentage rejects the whole patch")
|
|
_expect(service.metrics == before, "failed metric patch preserves every prior metric")
|
|
_expect(not service.update_metrics({&"faith_lifetime": 99}), "lifetime Faith cannot decrease after spending current Faith")
|
|
_expect(not service.update_metrics({&"guest_lifetime": -1}), "lifetime guest count cannot become negative")
|
|
_expect(not service.update_metrics({&"unknown": 1}), "unknown progression metric is rejected")
|
|
_expect(not service.update_metrics({&"asset_gold": "10000"}), "string numbers are not coerced into progression facts")
|
|
_expect(service.metrics == before, "all rejected patches leave progression facts unchanged")
|
|
_expect(service.complete_quest(&"important_rank_5"), "valid stable quest ID records once")
|
|
_expect(service.complete_quest(&"important_rank_5"), "completing the same quest is idempotent")
|
|
_expect(service.completed_quest_ids.size() == 1, "idempotent quest completion creates no duplicate")
|
|
_expect(not service.complete_quest(&"Bad Quest"), "invalid quest ID cannot enter persistent progression")
|
|
|
|
|
|
func _test_rank_one_through_seven() -> void:
|
|
var service := ProgressionType.new()
|
|
service.configure()
|
|
service.update_metrics({&"reputation":25, &"asset_gold":10_000})
|
|
_expect(service.request_rank_up(&"rank_up_2", 2).is_success(), "Rank 2 unlocks at Reputation 25 and assets 10,000G")
|
|
service.update_metrics({&"faith_lifetime":100, &"guest_lifetime":300})
|
|
_expect(service.request_rank_up(&"rank_up_3", 3).is_success(), "Rank 3 unlocks at lifetime Faith 100 and 300 guests")
|
|
service.update_metrics({
|
|
&"reputation":45,
|
|
&"asset_gold":50_000,
|
|
&"active_above_ground_facilities":1,
|
|
&"active_underground_facilities":1,
|
|
})
|
|
_expect(service.request_rank_up(&"rank_up_4", 4).is_success(), "Rank 4 additionally requires active facilities above and below ground")
|
|
service.update_metrics({&"faith_lifetime":500})
|
|
var rank_five_missing := service.evaluate_rank(5)
|
|
_expect(not rank_five_missing[&"eligible"], "Rank 5 does not unlock from numbers alone")
|
|
service.complete_quest(&"important_rank_5")
|
|
_expect(service.request_rank_up(&"rank_up_5", 5).is_success(), "Rank 5 requires its explicit important quest")
|
|
service.update_metrics({&"underground_influence":65})
|
|
var rank_six := service.evaluate_rank(6)
|
|
_expect(rank_six[&"eligible"], "Rank 6 accepts underground influence as the specified Reputation alternative")
|
|
_expect(service.request_rank_up(&"rank_up_6", 6).is_success(), "Rank 6 commits through the alternative branch")
|
|
service.update_metrics({&"faith_lifetime":1500, &"reputation":60})
|
|
var rank_seven_missing := service.evaluate_rank(7)
|
|
_expect(not rank_seven_missing[&"eligible"], "Rank 7 still requires the main chapter flag")
|
|
service.complete_quest(&"main_chapter_complete")
|
|
_expect(service.request_rank_up(&"rank_up_7", 7).is_success(), "Rank 7 requires Faith, both progress axes, dual operation and chapter completion")
|
|
_expect(service.current_rank == 7, "rank progression is monotonic through the final rank")
|
|
_expect(not service.request_rank_up(&"rank_up_8", 8).is_success(), "Rank 8 is rejected")
|
|
|
|
|
|
func _test_external_transaction_and_idempotency() -> void:
|
|
var service := _eligible_rank_two_service()
|
|
var external := {&"rank":1, &"bounds":service.get_rank_bounds(1), &"apply_calls":0, &"rollback_calls":0}
|
|
var capture := func(_current: int, _target: int) -> Dictionary:
|
|
return {&"ok":true, &"snapshot":{&"rank":external[&"rank"], &"bounds":external[&"bounds"].duplicate(true)}}
|
|
var apply := func(target: int, bounds: Dictionary) -> Dictionary:
|
|
external[&"apply_calls"] += 1
|
|
external[&"rank"] = target
|
|
external[&"bounds"] = bounds.duplicate(true)
|
|
return {&"ok":true}
|
|
var rollback := func(snapshot: Dictionary) -> Dictionary:
|
|
external[&"rollback_calls"] += 1
|
|
external[&"rank"] = snapshot[&"rank"]
|
|
external[&"bounds"] = snapshot[&"bounds"].duplicate(true)
|
|
return {&"ok":true}
|
|
var result := service.request_rank_up(&"atomic_rank_2", 2, capture, apply, rollback)
|
|
_expect(result.is_success(), "eligible rank-up commits through capture/apply boundary")
|
|
_expect(service.current_rank == 2 and external[&"rank"] == 2, "internal rank and external world commit together")
|
|
_expect(external[&"bounds"] == service.get_rank_bounds(2), "external world receives exact target bounds")
|
|
var replay := service.request_rank_up(&"atomic_rank_2", 2)
|
|
_expect(replay.is_success() and replay.is_replay, "same request ID and target returns its recorded success")
|
|
_expect(external[&"apply_calls"] == 1, "idempotent replay does not apply external expansion twice")
|
|
var conflict := service.request_rank_up(&"atomic_rank_2", 3)
|
|
_expect(not conflict.is_success() and conflict.code == CommandResult.Code.DUPLICATE_IDEMPOTENCY_KEY, "same request ID with another target conflicts")
|
|
var partial := service.request_rank_up(&"partial_callbacks", 3, capture, Callable(), rollback)
|
|
_expect(not partial.is_success() and partial.code == CommandResult.Code.INVALID_PAYLOAD, "partial callback set is rejected before mutation")
|
|
var strict_callbacks := _eligible_rank_two_service()
|
|
var bad_capture := func(_current: int, _target: int) -> Dictionary:
|
|
return {&"ok":"true", &"snapshot":{}}
|
|
var bad_capture_result := strict_callbacks.request_rank_up(&"bad_capture_type", 2, bad_capture, apply, rollback)
|
|
_expect(not bad_capture_result.is_success() and external[&"apply_calls"] == 1, "truthy string capture status is rejected before external apply")
|
|
var string_apply := func(_target: int, _bounds: Dictionary) -> Dictionary:
|
|
return {&"ok":"true"}
|
|
var strict_rollback := func(_snapshot: Dictionary) -> Dictionary:
|
|
return {&"ok":true}
|
|
var bad_apply_result := strict_callbacks.request_rank_up(&"bad_apply_type", 2, capture, string_apply, strict_rollback)
|
|
_expect(not bad_apply_result.is_success() and strict_callbacks.current_rank == 1, "truthy string apply status is treated as failure and rolled back")
|
|
|
|
|
|
func _test_failed_rollback_quarantines_progression() -> void:
|
|
var rolled_back := _eligible_rank_two_service()
|
|
var external_rank := 1
|
|
var capture := func(_current: int, _target: int) -> Dictionary:
|
|
return {&"ok":true, &"snapshot":external_rank}
|
|
var failed_apply := func(target: int, _bounds: Dictionary) -> Dictionary:
|
|
external_rank = target
|
|
return {&"ok":false}
|
|
var successful_rollback := func(snapshot: int) -> Dictionary:
|
|
external_rank = snapshot
|
|
return {&"ok":true}
|
|
var rolled_back_result := rolled_back.request_rank_up(
|
|
&"failed_but_rolled_back", 2, capture, failed_apply, successful_rollback
|
|
)
|
|
_expect(not rolled_back_result.is_success(), "failed external expansion rejects rank-up")
|
|
_expect(rolled_back.current_rank == 1 and external_rank == 1, "successful rollback restores both sides to Rank 1")
|
|
_expect(not rolled_back.quarantined, "verified rollback keeps progression available for a later request")
|
|
|
|
var quarantined := _eligible_rank_two_service()
|
|
external_rank = 1
|
|
var failed_rollback := func(_snapshot: int) -> Dictionary:
|
|
return {&"ok":false}
|
|
var quarantined_result := quarantined.request_rank_up(
|
|
&"rollback_failure", 2, capture, failed_apply, failed_rollback
|
|
)
|
|
_expect(not quarantined_result.is_success() and quarantined_result.code == CommandResult.Code.TRANSACTION_CLOSED, "rollback failure closes the progression boundary")
|
|
_expect(quarantined.quarantined and quarantined.quarantine_reason == &"RANK_ROLLBACK_FAILED", "rollback failure records a durable quarantine reason")
|
|
_expect(not quarantined.update_metrics({&"reputation":30}), "quarantine blocks later metric mutation")
|
|
_expect(not quarantined.request_rank_up(&"retry_after_quarantine", 2).is_success(), "quarantine blocks retry and prevents double expansion")
|
|
|
|
|
|
func _test_callback_reentry_and_internal_tamper() -> void:
|
|
var service := _eligible_rank_two_service()
|
|
var external_rank := 1
|
|
var observations := {&"metric_update":true, &"nested":null}
|
|
var capture := func(_current: int, _target: int) -> Dictionary:
|
|
return {&"ok":true, &"snapshot":external_rank}
|
|
var reentrant_apply := func(target: int, _bounds: Dictionary) -> Dictionary:
|
|
observations[&"metric_update"] = service.update_metrics({&"reputation":99})
|
|
observations[&"nested"] = service.request_rank_up(&"nested_rank_request", 2)
|
|
external_rank = target
|
|
return {&"ok":true}
|
|
var rollback := func(snapshot: int) -> Dictionary:
|
|
external_rank = snapshot
|
|
return {&"ok":true}
|
|
var result := service.request_rank_up(&"guarded_rank_request", 2, capture, reentrant_apply, rollback)
|
|
_expect(result.is_success(), "blocked reentry cannot prevent an otherwise valid outer promotion")
|
|
_expect(not bool(observations[&"metric_update"]), "rank callback cannot mutate eligibility metrics reentrantly")
|
|
var nested := observations[&"nested"] as CommandResult
|
|
_expect(nested != null and not nested.is_success() and nested.code == CommandResult.Code.TRANSACTION_CLOSED, "nested promotion is rejected while the outer transaction is active")
|
|
_expect(service.metrics[&"reputation"] == 25 and service.current_rank == 2, "blocked callback reentry leaves one exact promotion and original evidence")
|
|
_expect(service.create_snapshot()[&"processed_requests"].size() == 1, "reentry guard creates no hidden second idempotency record")
|
|
|
|
var tampered := _eligible_rank_two_service()
|
|
external_rank = 1
|
|
var tampering_apply := func(target: int, _bounds: Dictionary) -> Dictionary:
|
|
external_rank = target
|
|
tampered.metrics[&"reputation"] = 99
|
|
return {&"ok":true}
|
|
result = tampered.request_rank_up(&"tampered_apply", 2, capture, tampering_apply, rollback)
|
|
_expect(not result.is_success() and result.code == CommandResult.Code.TRANSACTION_APPLY_FAILED, "direct internal mutation by an apply callback invalidates the transaction")
|
|
_expect(tampered.current_rank == 1 and tampered.metrics[&"reputation"] == 25 and external_rank == 1, "tampered apply rolls both internal and external state back exactly")
|
|
_expect(not tampered.quarantined, "verified rollback after callback tamper keeps later promotion available")
|
|
|
|
var rollback_tampered := _eligible_rank_two_service()
|
|
external_rank = 1
|
|
var failed_apply := func(target: int, _bounds: Dictionary) -> Dictionary:
|
|
external_rank = target
|
|
return {&"ok":false}
|
|
var mutating_rollback := func(snapshot: int) -> Dictionary:
|
|
external_rank = snapshot
|
|
rollback_tampered.current_rank = 7
|
|
return {&"ok":true}
|
|
result = rollback_tampered.request_rank_up(&"tampered_rollback", 2, capture, failed_apply, mutating_rollback)
|
|
_expect(not result.is_success() and rollback_tampered.current_rank == 1, "internal state is restored even when a successful rollback callback tampers with it")
|
|
_expect(external_rank == 1 and not rollback_tampered.quarantined, "successful external rollback remains authoritative after internal tamper repair")
|
|
|
|
var valid_capture_tampered := _eligible_rank_two_service()
|
|
var callback_counts := {&"apply":0, &"rollback":0}
|
|
var mutating_capture := func(_current: int, _target: int) -> Dictionary:
|
|
valid_capture_tampered.metrics[&"reputation"] = 99
|
|
return {&"ok":true, &"snapshot":1}
|
|
var counted_apply := func(_target: int, _bounds: Dictionary) -> Dictionary:
|
|
callback_counts[&"apply"] += 1
|
|
return {&"ok":true}
|
|
var counted_rollback := func(_snapshot: int) -> Dictionary:
|
|
callback_counts[&"rollback"] += 1
|
|
return {&"ok":true}
|
|
result = valid_capture_tampered.request_rank_up(&"valid_capture_tamper", 2, mutating_capture, counted_apply, counted_rollback)
|
|
_expect(not result.is_success() and callback_counts[&"apply"] == 0, "valid-looking capture tamper is detected before external apply")
|
|
_expect(callback_counts[&"rollback"] == 1 and valid_capture_tampered.metrics[&"reputation"] == 25, "capture tamper invokes one rollback and restores internal eligibility")
|
|
|
|
var capture_tampered := _eligible_rank_two_service()
|
|
var bad_capture := func(_current: int, _target: int) -> Dictionary:
|
|
capture_tampered.current_rank = 7
|
|
return {&"ok":false}
|
|
result = capture_tampered.request_rank_up(&"tampered_capture", 2, bad_capture, reentrant_apply, rollback)
|
|
_expect(not result.is_success() and result.code == CommandResult.Code.TRANSACTION_CLOSED, "invalid capture that mutates progression enters quarantine")
|
|
_expect(capture_tampered.current_rank == 1 and capture_tampered.quarantine_reason == &"RANK_CALLBACK_TAMPERED", "capture tamper restores the last valid rank and records the uncertainty reason")
|
|
|
|
|
|
func _test_snapshot_round_trip_and_atomic_rejection() -> void:
|
|
var source := _eligible_rank_two_service()
|
|
source.complete_quest(&"important_rank_5")
|
|
source.request_rank_up(&"snapshot_rank_2", 2)
|
|
var snapshot := source.create_snapshot()
|
|
var restored := ProgressionType.new()
|
|
_expect(restored.restore_snapshot(snapshot), "valid progression snapshot restores")
|
|
_expect(restored.create_snapshot() == snapshot, "progression snapshot round trip is exact")
|
|
var json_snapshot: Variant = JSON.parse_string(JSON.stringify(snapshot))
|
|
var json_restored := ProgressionType.new()
|
|
var json_ok := json_snapshot is Dictionary and json_restored.restore_snapshot(json_snapshot)
|
|
_expect(json_ok, "JSON-decoded string-key snapshot restores through the same strict boundary (%s)" % json_restored.last_error)
|
|
_expect(json_restored.create_snapshot() == snapshot, "JSON transport preserves all promotion evidence exactly")
|
|
var before := restored.create_snapshot()
|
|
|
|
var missing_metric := snapshot.duplicate(true)
|
|
missing_metric[&"metrics"].erase("asset_gold")
|
|
_expect(not restored.restore_snapshot(missing_metric), "snapshot missing one progression metric is rejected")
|
|
_expect(restored.create_snapshot() == before, "missing-metric rejection is atomic")
|
|
|
|
var bad_fingerprint := snapshot.duplicate(true)
|
|
bad_fingerprint[&"processed_requests"][0][&"fingerprint"] = "rank:7"
|
|
_expect(not restored.restore_snapshot(bad_fingerprint), "request fingerprint inconsistent with recorded rank is rejected")
|
|
_expect(restored.create_snapshot() == before, "fingerprint rejection preserves live progression")
|
|
|
|
var bad_bounds := snapshot.duplicate(true)
|
|
bad_bounds[&"processed_requests"][0][&"details"][&"bounds"][&"max_x"] = 999
|
|
_expect(not restored.restore_snapshot(bad_bounds), "derived rank bounds cannot be tampered in a snapshot")
|
|
_expect(restored.create_snapshot() == before, "derived-bounds rejection is atomic")
|
|
|
|
var bad_quarantine := snapshot.duplicate(true)
|
|
bad_quarantine[&"quarantined"] = true
|
|
bad_quarantine[&"quarantine_reason"] = ""
|
|
_expect(not restored.restore_snapshot(bad_quarantine), "quarantine boolean requires a non-empty reason")
|
|
_expect(restored.create_snapshot() == before, "quarantine mismatch rejection is atomic")
|
|
|
|
var string_rank := snapshot.duplicate(true)
|
|
string_rank[&"current_rank"] = "2"
|
|
_expect(not restored.restore_snapshot(string_rank), "string rank is not coerced during restore")
|
|
_expect(restored.create_snapshot() == before, "type rejection preserves live progression")
|
|
|
|
var string_version := snapshot.duplicate(true)
|
|
string_version[&"version"] = "1"
|
|
_expect(not restored.restore_snapshot(string_version), "string schema version is not coerced during restore")
|
|
_expect(restored.create_snapshot() == before, "schema type rejection is atomic")
|
|
|
|
var unknown_root := snapshot.duplicate(true)
|
|
unknown_root[&"unexpected"] = true
|
|
_expect(not restored.restore_snapshot(unknown_root), "unknown root snapshot field is rejected")
|
|
_expect(restored.create_snapshot() == before, "unknown-field rejection preserves live progression")
|
|
|
|
var duplicate_rank_history := snapshot.duplicate(true)
|
|
var duplicate_record: Dictionary = duplicate_rank_history[&"processed_requests"][0].duplicate(true)
|
|
duplicate_record[&"request_id"] = "snapshot_rank_2_b"
|
|
duplicate_record[&"details"][&"request_id"] = &"snapshot_rank_2_b"
|
|
duplicate_rank_history[&"processed_requests"].append(duplicate_record)
|
|
_expect(not restored.restore_snapshot(duplicate_rank_history), "two successful request IDs cannot claim the same one-time rank transition")
|
|
_expect(restored.create_snapshot() == before, "duplicate-transition rejection is atomic")
|
|
|
|
|
|
func _test_promotion_evidence_and_legacy_migration() -> void:
|
|
var source := _eligible_rank_two_service()
|
|
source.request_rank_up(&"evidence_rank_2", 2)
|
|
var snapshot := source.create_snapshot()
|
|
var evidence: Dictionary = snapshot[&"processed_requests"][0][&"details"][&"eligibility"]
|
|
_expect(snapshot[&"version"] == 2, "promotion evidence is stored in Progression Snapshot 2")
|
|
_expect(evidence[&"eligible"] and evidence[&"current_rank"] == 1 and evidence[&"target_rank"] == 2, "saved eligibility header binds the exact one-step transition")
|
|
_expect(_requirement_ids(evidence) == [&"reputation", &"asset_gold"], "saved eligibility retains deterministic condition order")
|
|
|
|
var restored := ProgressionType.new()
|
|
_expect(restored.configure(), "evidence rejection target starts from a known valid state")
|
|
var before := restored.create_snapshot()
|
|
var below_threshold := snapshot.duplicate(true)
|
|
below_threshold[&"processed_requests"][0][&"details"][&"eligibility"][&"requirements"][0][&"current"] = 24
|
|
_expect(not restored.restore_snapshot(below_threshold), "promotion history below its threshold is rejected")
|
|
_expect(restored.create_snapshot() == before, "invalid promotion evidence rejection is atomic")
|
|
|
|
var changed_requirement := snapshot.duplicate(true)
|
|
changed_requirement[&"processed_requests"][0][&"details"][&"eligibility"][&"requirements"][0][&"required"] = 24
|
|
_expect(not restored.restore_snapshot(changed_requirement), "saved threshold definitions cannot be weakened")
|
|
_expect(restored.create_snapshot() == before, "threshold tamper preserves the live progression")
|
|
|
|
var missing_history := snapshot.duplicate(true)
|
|
missing_history[&"processed_requests"] = []
|
|
_expect(not restored.restore_snapshot(missing_history), "Rank 2 cannot be restored without its Rank 2 promotion record")
|
|
_expect(restored.create_snapshot() == before, "incomplete-history rejection is atomic")
|
|
|
|
var rank_three := _rank_three_service()
|
|
var lifetime_tamper := rank_three.create_snapshot()
|
|
lifetime_tamper[&"metrics"]["faith_lifetime"] = 99
|
|
_expect(not restored.restore_snapshot(lifetime_tamper), "current lifetime Faith cannot fall below its saved Rank 3 evidence")
|
|
_expect(restored.create_snapshot() == before, "lifetime-history mismatch preserves the live progression")
|
|
|
|
var rank_five := _rank_five_service()
|
|
var quest_tamper := rank_five.create_snapshot()
|
|
quest_tamper[&"completed_quest_ids"].erase("important_rank_5")
|
|
_expect(not restored.restore_snapshot(quest_tamper), "a monotonic quest used for Rank 5 cannot disappear from the snapshot")
|
|
_expect(restored.create_snapshot() == before, "quest-history mismatch is rejected atomically")
|
|
|
|
var legacy_source := ProgressionType.new()
|
|
legacy_source.configure(1, {&"reputation":10}, [&"tutorial_complete"])
|
|
var legacy_rank_one := legacy_source.create_snapshot()
|
|
legacy_rank_one[&"version"] = 1
|
|
_expect(restored.restore_snapshot(legacy_rank_one), "unambiguous legacy Rank 1 snapshot migrates safely")
|
|
_expect(restored.create_snapshot()[&"version"] == 2 and restored.current_rank == 1, "legacy restore is immediately re-emitted as Snapshot 2")
|
|
|
|
var legacy_progressed := snapshot.duplicate(true)
|
|
legacy_progressed[&"version"] = 1
|
|
legacy_progressed[&"processed_requests"][0][&"details"].erase(&"eligibility")
|
|
var migrated_before := restored.create_snapshot()
|
|
_expect(not restored.restore_snapshot(legacy_progressed), "legacy progressed snapshot without condition evidence is rejected as ambiguous")
|
|
_expect(restored.create_snapshot() == migrated_before, "ambiguous legacy rejection preserves the migrated Rank 1 state")
|
|
|
|
|
|
func _test_input_and_history_bounds() -> void:
|
|
var service := ProgressionType.new()
|
|
_expect(not service.configure(2), "configuration cannot bypass the one-rank-at-a-time promotion boundary")
|
|
_expect(service.configure(), "valid Rank 1 configuration remains available after rejection")
|
|
_expect(not service.update_metrics({&"asset_gold":ProgressionType.MAX_SAFE_INTEGER + 1}), "metrics above the JSON-safe integer range are rejected")
|
|
_expect(not service.update_metrics({&"asset_gold":INF}), "non-finite metrics are rejected")
|
|
var oversized_quests: Array = []
|
|
for index in ProgressionType.MAX_COMPLETED_QUESTS + 1:
|
|
oversized_quests.append("quest_%03d" % index)
|
|
_expect(not service.configure(1, {}, oversized_quests), "oversized initial quest input is rejected before per-entry work")
|
|
|
|
service = _eligible_rank_two_service()
|
|
var invalid_id := service.request_rank_up(&"rank request with spaces", 2)
|
|
_expect(not invalid_id.is_success() and invalid_id.code == CommandResult.Code.INVALID_PAYLOAD, "idempotency keys must use stable lowercase IDs")
|
|
_expect(service.current_rank == 1, "invalid idempotency key cannot mutate rank")
|
|
|
|
var impossible := service.create_snapshot()
|
|
impossible[&"current_rank"] = 7
|
|
var before := service.create_snapshot()
|
|
_expect(not service.restore_snapshot(impossible), "fabricated high rank without contiguous promotion history is rejected")
|
|
_expect(service.create_snapshot() == before, "fabricated-rank rejection is atomic")
|
|
|
|
|
|
func _eligible_rank_two_service() -> ProgressionService:
|
|
var service := ProgressionType.new()
|
|
service.configure(1, {&"reputation":25, &"asset_gold":10_000})
|
|
return service
|
|
|
|
|
|
func _rank_three_service() -> ProgressionService:
|
|
var service := _eligible_rank_two_service()
|
|
service.request_rank_up(&"history_rank_2", 2)
|
|
service.update_metrics({&"faith_lifetime":100, &"guest_lifetime":300})
|
|
service.request_rank_up(&"history_rank_3", 3)
|
|
return service
|
|
|
|
|
|
func _rank_five_service() -> ProgressionService:
|
|
var service := _rank_three_service()
|
|
service.update_metrics({
|
|
&"reputation":45,
|
|
&"asset_gold":50_000,
|
|
&"active_above_ground_facilities":1,
|
|
&"active_underground_facilities":1,
|
|
})
|
|
service.request_rank_up(&"history_rank_4", 4)
|
|
service.update_metrics({&"faith_lifetime":500})
|
|
service.complete_quest(&"important_rank_5")
|
|
service.request_rank_up(&"history_rank_5", 5)
|
|
return service
|
|
|
|
|
|
func _requirement_ids(report: Dictionary) -> Array[StringName]:
|
|
var result: Array[StringName] = []
|
|
for requirement: Dictionary in report[&"requirements"]:
|
|
result.append(StringName(requirement[&"id"]))
|
|
return result
|
|
|
|
|
|
func _expect(condition: bool, label: String) -> void:
|
|
_checks += 1
|
|
if condition:
|
|
print(" ✓ %s" % label)
|
|
return
|
|
_failures += 1
|
|
push_error(" ✗ %s" % label)
|