Files
TheTree_mock/tests/s4/test_placement_risk_preview_service.gd

474 lines
23 KiB
GDScript

extends SceneTree
const PreviewServiceType = preload("res://scripts/relationship/placement_risk_preview_service.gd")
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S4: Placement Risk Preview Service")
_test_half_open_multiday_contact_and_replacement()
_test_deterministic_order_across_facilities()
_test_high_contact_branch_and_route_confirmation()
_test_content_filter_changes_presentation_only()
_test_determinism_purity_and_statelessness()
_test_avoidance_suggestions_reduce_contact_and_risk()
_test_strict_fail_closed_schema()
_test_collection_limits_and_derived_values()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _test_half_open_multiday_contact_and_replacement() -> void:
var service := PreviewServiceType.new()
var adjacent := _base_request(false)
adjacent[&"period"] = {&"start_at": 0, &"end_at": 120}
adjacent[&"actors"][0][&"existing_shifts"] = [
_shift("hero_edge", "shared", 0, 60),
]
adjacent[&"actors"][1][&"existing_shifts"] = [
_shift("rival_edge", "shared", 60, 120),
]
var adjacent_result: Dictionary = service.preview(adjacent)
_expect(adjacent_result[&"ok"], "adjacent half-open fixture previews")
_expect(adjacent_result[&"totals"][&"contact_minutes"] == 0, "adjacent half-open Shifts add no phantom contact")
_expect(adjacent_result[&"totals"][&"shield_attack_candidates"] == 0, "adjacent half-open Shifts add no attack candidate")
var one_minute := adjacent.duplicate(true)
one_minute[&"actors"][1][&"existing_shifts"][0][&"start_at"] = 59
var one_minute_result: Dictionary = service.preview(one_minute)
_expect(one_minute_result[&"ok"], "one-minute overlap fixture previews")
_expect(one_minute_result[&"totals"][&"contact_minutes"] == 1, "half-open overlap counts the exact minute")
var edge_interval: Dictionary = one_minute_result[&"relationships"][0][&"contacts_by_facility"][0][&"intervals"][0]
_expect(edge_interval[&"start_at"] == 59 and edge_interval[&"end_at"] == 60, "half-open interval bounds remain exact")
var high_result: Dictionary = service.preview(_base_request(true))
_expect(high_result[&"ok"], "multi-day proposal fixture previews")
var relationship: Dictionary = high_result[&"relationships"][0]
_expect(relationship[&"total_contact_minutes"] == 240, "same-ID proposals replace existing rival Shifts instead of appending them")
_expect(relationship[&"contacts_by_facility"].size() == 1, "shared contacts group by facility")
var intervals: Array = relationship[&"contacts_by_facility"][0][&"intervals"]
_expect(intervals.size() == 2, "separate days retain separate overlap intervals")
_expect(
intervals[0][&"start_at"] == 90
and intervals[0][&"end_at"] == 240
and intervals[0][&"minutes"] == 150,
"adjacent heroine Shifts merge without duplicate minutes"
)
_expect(intervals[0][&"heroine_shift_ids"] == ["hero_day_a", "hero_day_b"], "merged interval preserves deterministic source Shift IDs")
_expect(
intervals[1][&"start_at"] == 1_500
and intervals[1][&"end_at"] == 1_590
and intervals[1][&"minutes"] == 90,
"absolute-minute contact works across the day boundary"
)
func _test_high_contact_branch_and_route_confirmation() -> void:
var service := PreviewServiceType.new()
var low: Dictionary = service.preview(_base_request(false))
var high: Dictionary = service.preview(_base_request(true))
_expect(low[&"ok"] and high[&"ok"], "low and high contact branches both preview")
_expect(low[&"totals"][&"contact_minutes"] == 0, "separate existing facilities are a zero-contact branch")
_expect(low[&"totals"][&"shield_attack_candidates"] == 0, "low-contact branch creates no Shield attack candidates")
_expect(low[&"totals"][&"triggerable_events"] == 0, "low-contact branch creates no Event candidates")
_expect(high[&"totals"][&"shield_attack_candidates"] == 5, "only high contact creates the deterministic Shield attack sequence")
_expect(high[&"totals"][&"triggerable_events"] == 1, "only high contact unlocks the threshold Event")
var relationship: Dictionary = high[&"relationships"][0]
var triggers: Array[int] = []
for attack: Dictionary in relationship[&"shield_attack_candidates"]:
triggers.append(int(attack[&"trigger_at"]))
_expect(triggers == [119, 179, 239, 1_529, 1_589], "Shield candidates use stable 30-minute then 60-minute trigger points")
_expect(relationship[&"projected_values"][&"shield_current"] == 0, "candidate mechanics clamp projected Shield at zero")
_expect(relationship[&"projected_values"][&"rival_bond"] == 49, "post-break candidates project bounded Rival Bond gains")
var candidate: Dictionary = relationship[&"triggerable_events"][0]
_expect(candidate[&"event_id"] == "danger_event", "eligible Event is selected by mechanical thresholds")
_expect(_reason_codes(candidate[&"reasons"]) == [
"CONTACT_THRESHOLD_MET", "SHIELD_THRESHOLD_MET", "RIVAL_BOND_RANGE_MET", "SHARED_FACILITY",
], "Event candidate reports every deterministic eligibility reason")
_expect(candidate[&"mechanics"][&"route_lock_requested"] == "EXCLUSIVE_LOCKED", "candidate retains requested exclusive Route Lock")
_expect(candidate[&"mechanics"][&"route_lock_preview"] == "EXCLUSIVE_PENDING", "preview downgrades unconfirmed exclusive lock to pending")
_expect(not candidate[&"mechanics"][&"route_lock_confirmed"], "Event candidate never claims an unconfirmed Route Lock")
var warning: Dictionary = relationship[&"route_lock_warning"]
_expect(warning[&"confirmation_required"], "exclusive candidate emits a confirmation-required warning")
_expect(not warning[&"preview_confirms_lock"], "read-only preview never confirms Route Lock")
_expect(warning[&"projected_route_lock"] == "EXCLUSIVE_PENDING", "mechanical summary keeps Route Lock pending")
_expect(relationship[&"risk_level"] == "CRITICAL", "confirmation-required high-contact branch is critical risk")
var soft_request := _base_request(true)
soft_request[&"events"][1][&"route_lock"] = "SOFT"
soft_request[&"events"][1][&"route_id"] = ""
var soft_result: Dictionary = service.preview(soft_request)
_expect(soft_result[&"relationships"][0][&"route_lock_warning"][&"projected_route_lock"] == "SOFT", "non-exclusive candidate Route Lock is still projected mechanically")
_expect(not soft_result[&"relationships"][0][&"route_lock_warning"][&"confirmation_required"], "SOFT Route Lock does not invent an exclusive confirmation")
var vulnerable_low := _base_request(false)
vulnerable_low[&"relationships"][0][&"shield_current"] = 0
vulnerable_low[&"relationships"][0][&"rival_bond"] = 90
_expect(service.preview(vulnerable_low)[&"relationships"][0][&"risk_level"] == "NONE", "zero-contact placement is not mislabeled from vulnerable baseline values alone")
func _test_deterministic_order_across_facilities() -> void:
var service := PreviewServiceType.new()
var request := _base_request(false)
request[&"period"] = {&"start_at": 0, &"end_at": 200}
request[&"actors"][0][&"existing_shifts"] = [
_shift("hero_zeta", "zeta", 0, 60),
_shift("hero_alpha", "alpha", 100, 160),
]
request[&"actors"][1][&"existing_shifts"] = [
_shift("rival_zeta", "zeta", 0, 60),
_shift("rival_alpha", "alpha", 100, 160),
]
var result: Dictionary = service.preview(request)
_expect(result[&"ok"], "multi-facility ordering fixture previews")
var relationship: Dictionary = result[&"relationships"][0]
_expect(
[
relationship[&"contacts_by_facility"][0][&"facility_id"],
relationship[&"contacts_by_facility"][1][&"facility_id"],
] == ["alpha", "zeta"],
"facility summaries use stable lexical order"
)
var triggers: Array[int] = []
for attack: Dictionary in relationship[&"shield_attack_candidates"]:
triggers.append(int(attack[&"trigger_at"]))
_expect(triggers == [29, 129], "cross-facility Shield candidates use chronological mechanical order")
func _test_content_filter_changes_presentation_only() -> void:
var service := PreviewServiceType.new()
var outputs: Dictionary = {}
for mode: String in ["NONE", "FADE", "REPLACE", "DISABLE"]:
var request := _base_request(true)
request[&"content_filter"][&"mode"] = mode
outputs[mode] = service.preview(request)
_expect(outputs[mode][&"ok"], "%s Content Filter preview succeeds" % mode)
var baseline: Dictionary = outputs["NONE"][&"relationships"][0]
var expected_modes := {
"NONE": "FULL",
"FADE": "FADE",
"REPLACE": "REPLACE",
"DISABLE": "DISABLE",
}
for mode: String in ["NONE", "FADE", "REPLACE", "DISABLE"]:
var current: Dictionary = outputs[mode][&"relationships"][0]
_expect(current[&"mechanical_summary"] == baseline[&"mechanical_summary"], "%s preserves risk, cost, cooldown, lock, and projected values" % mode)
_expect(current[&"contacts_by_facility"] == baseline[&"contacts_by_facility"], "%s preserves mechanical contact intervals" % mode)
_expect(current[&"shield_attack_candidates"] == baseline[&"shield_attack_candidates"], "%s preserves Shield attack mechanics" % mode)
_expect(current[&"triggerable_events"][0][&"mechanics"] == baseline[&"triggerable_events"][0][&"mechanics"], "%s preserves Event mechanics" % mode)
_expect(current[&"triggerable_events"][0][&"reasons"] == baseline[&"triggerable_events"][0][&"reasons"], "%s preserves Event reasons" % mode)
_expect(current[&"triggerable_events"][0][&"presentation"][&"mode"] == expected_modes[mode], "%s changes only the Event presentation mode" % mode)
_expect(outputs["REPLACE"][&"relationships"][0][&"triggerable_events"][0][&"presentation"][&"display_event_id"] == "safe_notice", "REPLACE displays the validated safe replacement")
_expect(not outputs["DISABLE"][&"relationships"][0][&"triggerable_events"][0][&"presentation"][&"visible"], "DISABLE hides presentation without removing mechanics")
func _test_determinism_purity_and_statelessness() -> void:
var service := PreviewServiceType.new()
var request := _base_request(true)
var before := request.duplicate(true)
var first: Dictionary = service.preview(request)
var second: Dictionary = service.preview(request)
_expect(first == second, "identical request produces exact deterministic output")
_expect(request == before, "preview does not mutate any nested input data")
var shuffled := request.duplicate(true)
shuffled[&"actors"].reverse()
shuffled[&"events"].reverse()
for actor: Dictionary in shuffled[&"actors"]:
actor[&"existing_shifts"].reverse()
actor[&"proposed_shifts"].reverse()
_expect(service.preview(shuffled) == first, "input array order does not change canonical Preview order")
var low: Dictionary = service.preview(_base_request(false))
var third: Dictionary = service.preview(request)
_expect(low[&"totals"][&"contact_minutes"] == 0 and third == first, "interleaved preview calls retain no service state")
_expect(not service.has_method("create_snapshot") and not service.has_method("restore_snapshot"), "stateless preview exposes no snapshot lifecycle")
_expect(not service.has_method("configure_callbacks"), "pure preview exposes no callback or re-entry surface")
func _test_avoidance_suggestions_reduce_contact_and_risk() -> void:
var service := PreviewServiceType.new()
var request := _base_request(true)
var original: Dictionary = service.preview(request)
var relationship: Dictionary = original[&"relationships"][0]
var suggestions: Array = relationship[&"avoidance_suggestions"]
_expect(suggestions.size() == 2, "preview returns both facility and time separation suggestions")
_expect(suggestions[0][&"kind"] == "CHANGE_FACILITY" and suggestions[1][&"kind"] == "SEPARATE_TIME", "avoidance suggestions have deterministic kind order")
for suggestion: Dictionary in suggestions:
_expect(suggestion[&"reduction_minutes"] > 0, "%s advertises only a positive deterministic reduction" % suggestion[&"kind"])
var applied := request.duplicate(true)
_replace_actor_proposal(applied, String(suggestion[&"actor_id"]), suggestion[&"proposed_shift"])
var reduced: Dictionary = service.preview(applied)
_expect(reduced[&"ok"], "%s proposal is directly previewable" % suggestion[&"kind"])
_expect(reduced[&"totals"][&"contact_minutes"] == suggestion[&"contact_minutes_after"], "%s predicted contact matches reapplied result" % suggestion[&"kind"])
_expect(reduced[&"totals"][&"contact_minutes"] < original[&"totals"][&"contact_minutes"], "%s lowers shared-facility contact" % suggestion[&"kind"])
_expect(reduced[&"totals"][&"shield_attack_candidates"] < original[&"totals"][&"shield_attack_candidates"], "%s lowers Shield attack candidates" % suggestion[&"kind"])
_expect(_risk_rank(reduced[&"relationships"][0][&"risk_level"]) < _risk_rank(relationship[&"risk_level"]), "%s lowers the resulting risk level" % suggestion[&"kind"])
_expect(request == _base_request(true), "applying suggestions to copies leaves source proposal unchanged")
func _test_strict_fail_closed_schema() -> void:
var service := PreviewServiceType.new()
_expect_rejected(service.preview(null), "INVALID_TYPE", "non-Dictionary request is rejected")
var unknown_top := _base_request(false)
unknown_top[&"derived_risk"] = "NONE"
_expect_rejected(service.preview(unknown_top), "INVALID_SCHEMA", "unknown top-level field is rejected")
var missing_top := _base_request(false)
missing_top.erase(&"events")
_expect_rejected(service.preview(missing_top), "INVALID_SCHEMA", "missing top-level field is rejected")
var fake_version := _base_request(false)
fake_version[&"schema_version"] = "1"
_expect_rejected(service.preview(fake_version), "UNSUPPORTED_SCHEMA", "numeric-string schema version is rejected")
var fake_time := _base_request(false)
fake_time[&"period"][&"start_at"] = 0.0
_expect_rejected(service.preview(fake_time), "INVALID_TYPE", "float period bound is rejected")
var long_period := _base_request(false)
long_period[&"period"][&"end_at"] = 14 * 1_440 + 1
_expect_rejected(service.preview(long_period), "OUT_OF_RANGE", "period beyond the 14-day limit is rejected")
var actor_unknown := _base_request(false)
actor_unknown[&"actors"][0][&"derived_availability"] = true
_expect_rejected(service.preview(actor_unknown), "INVALID_SCHEMA", "unknown actor field is rejected")
var role_fake := _base_request(false)
role_fake[&"actors"][0][&"role"] = "heroine"
_expect_rejected(service.preview(role_fake), "INVALID_ENUM", "case-forged role enum is rejected")
var shift_unknown := _base_request(false)
shift_unknown[&"actors"][0][&"existing_shifts"][0][&"duration"] = 120
_expect_rejected(service.preview(shift_unknown), "INVALID_SCHEMA", "derived Shift duration is rejected")
var shift_string := _base_request(false)
shift_string[&"actors"][0][&"existing_shifts"][0][&"start_at"] = "60"
_expect_rejected(service.preview(shift_string), "INVALID_TYPE", "numeric-string Shift bound is rejected")
var double_booked := _base_request(true)
double_booked[&"actors"][1][&"proposed_shifts"][1][&"start_at"] = 200
double_booked[&"actors"][1][&"proposed_shifts"][1][&"end_at"] = 300
_expect_rejected(service.preview(double_booked), "ACTOR_DOUBLE_BOOKED", "overlapping effective Shift placement is rejected")
var filter_unknown := _base_request(false)
filter_unknown[&"content_filter"][&"visible"] = false
_expect_rejected(service.preview(filter_unknown), "INVALID_SCHEMA", "unknown Content Filter field is rejected")
var bad_filter_enum := _base_request(false)
bad_filter_enum[&"content_filter"][&"mode"] = "HIDE"
_expect_rejected(service.preview(bad_filter_enum), "INVALID_ENUM", "unknown Content Filter mode is rejected")
var event_unknown := _base_request(false)
event_unknown[&"events"][0][&"eligible"] = true
_expect_rejected(service.preview(event_unknown), "INVALID_SCHEMA", "derived Event eligibility is rejected")
var event_missing := _base_request(false)
event_missing[&"events"][0].erase(&"cooldown_minutes")
_expect_rejected(service.preview(event_missing), "INVALID_SCHEMA", "missing Event field is rejected")
var event_float := _base_request(false)
event_float[&"events"][0][&"cost_gold"] = 75.0
_expect_rejected(service.preview(event_float), "INVALID_TYPE", "float Event cost is rejected")
var event_route := _base_request(false)
event_route[&"events"][0][&"route_lock"] = "LOCKED"
_expect_rejected(service.preview(event_route), "INVALID_ENUM", "forged Route Lock enum is rejected")
var event_relationship := _base_request(false)
event_relationship[&"events"][0][&"relationship_id"] = "missing"
_expect_rejected(service.preview(event_relationship), "MISSING_RELATIONSHIP", "Event cannot reference an unknown relationship")
var duplicate_pair := _base_request(false)
var repeated_relationship: Dictionary = duplicate_pair[&"relationships"][0].duplicate(true)
repeated_relationship[&"relationship_id"] = "duplicate_pair"
duplicate_pair[&"relationships"].append(repeated_relationship)
_expect_rejected(service.preview(duplicate_pair), "DUPLICATE_PAIR", "duplicate heroine-rival pair cannot double-count Preview totals")
var missing_replacement := _base_request(true)
missing_replacement[&"content_filter"][&"mode"] = "REPLACE"
missing_replacement[&"events"][1][&"replacement_event_id"] = "missing"
_expect_rejected(service.preview(missing_replacement), "REPLACEMENT_MISSING", "REPLACE fails closed when safe replacement is absent")
var unsafe_replacement := _base_request(true)
unsafe_replacement[&"content_filter"][&"mode"] = "REPLACE"
unsafe_replacement[&"events"][0][&"content_rating"] = "RESTRICTED"
_expect_rejected(service.preview(unsafe_replacement), "REPLACEMENT_UNSAFE", "REPLACE rejects a filtered replacement chain")
func _test_collection_limits_and_derived_values() -> void:
var service := PreviewServiceType.new()
var too_many_actors := _base_request(false)
while too_many_actors[&"actors"].size() <= 64:
too_many_actors[&"actors"].append(too_many_actors[&"actors"][0].duplicate(true))
_expect_rejected(service.preview(too_many_actors), "COLLECTION_LIMIT", "actor collection upper bound is enforced before parsing")
var too_many_shifts := _base_request(false)
too_many_shifts[&"actors"][0][&"existing_shifts"] = []
for index: int in range(65):
too_many_shifts[&"actors"][0][&"existing_shifts"].append(_shift("h_%02d" % index, "solo", index * 10, index * 10 + 5))
_expect_rejected(service.preview(too_many_shifts), "COLLECTION_LIMIT", "per-list Shift upper bound is enforced")
var too_many_effective := _base_request(false)
too_many_effective[&"actors"][0][&"existing_shifts"] = []
for index: int in range(64):
too_many_effective[&"actors"][0][&"existing_shifts"].append(_shift("h_%02d" % index, "solo", index * 10, index * 10 + 5))
too_many_effective[&"actors"][0][&"proposed_shifts"] = [_shift("h_new", "solo", 700, 705)]
_expect_rejected(service.preview(too_many_effective), "COLLECTION_LIMIT", "combined effective Shift upper bound prevents overlap-pair truncation")
var too_many_events := _base_request(false)
too_many_events[&"events"] = []
for _index: int in range(257):
too_many_events[&"events"].append(_safe_event())
_expect_rejected(service.preview(too_many_events), "COLLECTION_LIMIT", "Event definition upper bound is enforced before parsing")
var forged_shield := _base_request(false)
forged_shield[&"relationships"][0][&"shield_current"] = 90
forged_shield[&"relationships"][0][&"shield_max"] = 80
_expect_rejected(service.preview(forged_shield), "INVALID_DERIVED_VALUE", "forged current Shield above Shield Max is rejected")
var duplicate_tag := _base_request(false)
duplicate_tag[&"events"][1][&"content_tags"] = ["intimacy", "intimacy"]
_expect_rejected(service.preview(duplicate_tag), "DUPLICATE_VALUE", "duplicate content tags are rejected")
func _base_request(high_contact: bool) -> Dictionary:
var rival_proposals: Array[Dictionary] = []
if high_contact:
rival_proposals = [
_shift("rival_day", "shared", 90, 240),
_shift("rival_next_day", "shared", 1_500, 1_590),
]
return {
&"schema_version": 1,
&"period": {&"start_at": 0, &"end_at": 2_880},
&"actors": [
{
&"actor_id": "heroine",
&"role": "HEROINE",
&"existing_shifts": [
_shift("hero_day_a", "shared", 60, 180),
_shift("hero_day_b", "shared", 180, 300),
_shift("hero_next_day", "shared", 1_500, 1_620),
],
&"proposed_shifts": [],
&"mechanical_effects": {
&"base_shield_attack": 0,
&"night_shield_attack_bonus": 0,
},
},
{
&"actor_id": "rival",
&"role": "RIVAL",
&"existing_shifts": [
_shift("rival_day", "archive", 90, 240),
_shift("rival_next_day", "archive", 1_500, 1_590),
],
&"proposed_shifts": rival_proposals,
&"mechanical_effects": {
&"base_shield_attack": 8,
&"night_shield_attack_bonus": 4,
},
},
],
&"relationships": [
{
&"relationship_id": "main_pair",
&"heroine_id": "heroine",
&"rival_id": "rival",
&"affection": 55,
&"rival_bond": 40,
&"shield_current": 20,
&"shield_max": 100,
&"route_lock": "NONE",
&"route_id": "",
&"target_resistance": 0,
&"monitoring_bonus": 0,
},
],
&"events": [_safe_event(), _danger_event()],
&"content_filter": {
&"mode": "NONE",
&"maximum_rating": "GENERAL",
&"disabled_tags": [],
},
}
func _safe_event() -> Dictionary:
return {
&"event_id": "safe_notice",
&"relationship_id": "main_pair",
&"priority": 1,
&"min_contact_minutes": 1_000,
&"max_shield_current": 100,
&"min_rival_bond": 0,
&"max_rival_bond": 100,
&"required_facility_id": "",
&"shield_damage": 0,
&"rival_bond_delta": 0,
&"cost_gold": 0,
&"cooldown_minutes": 0,
&"route_lock": "NONE",
&"route_id": "",
&"content_rating": "GENERAL",
&"content_tags": [],
&"filter_behavior": "NONE",
&"replacement_event_id": "",
&"display_name": "A quiet scheduling notice",
}
func _danger_event() -> Dictionary:
return {
&"event_id": "danger_event",
&"relationship_id": "main_pair",
&"priority": 100,
&"min_contact_minutes": 120,
&"max_shield_current": 0,
&"min_rival_bond": 0,
&"max_rival_bond": 100,
&"required_facility_id": "shared",
&"shield_damage": 0,
&"rival_bond_delta": 5,
&"cost_gold": 75,
&"cooldown_minutes": 720,
&"route_lock": "EXCLUSIVE_LOCKED",
&"route_id": "rival_route",
&"content_rating": "RESTRICTED",
&"content_tags": ["intimacy"],
&"filter_behavior": "NONE",
&"replacement_event_id": "safe_notice",
&"display_name": "Restricted encounter",
}
func _shift(shift_id: String, facility_id: String, start_at: int, end_at: int) -> Dictionary:
return {
&"shift_id": shift_id,
&"facility_id": facility_id,
&"start_at": start_at,
&"end_at": end_at,
}
func _replace_actor_proposal(request: Dictionary, actor_id: String, proposal: Dictionary) -> void:
for actor: Dictionary in request[&"actors"]:
if actor[&"actor_id"] != actor_id:
continue
var replaced := false
for index: int in range(actor[&"proposed_shifts"].size()):
if actor[&"proposed_shifts"][index][&"shift_id"] == proposal[&"shift_id"]:
actor[&"proposed_shifts"][index] = proposal.duplicate(true)
replaced = true
break
if not replaced:
actor[&"proposed_shifts"].append(proposal.duplicate(true))
return
func _reason_codes(reasons: Array) -> Array[String]:
var result: Array[String] = []
for reason: Dictionary in reasons:
result.append(String(reason[&"code"]))
return result
func _risk_rank(level: String) -> int:
return ["NONE", "LOW", "MEDIUM", "HIGH", "CRITICAL"].find(level)
func _expect_rejected(result: Dictionary, code: String, message: String) -> void:
_expect(not bool(result.get(&"ok", true)) and result.get(&"error_code", "") == code, message)
func _expect(condition: bool, message: String) -> void:
_checks += 1
if condition:
return
_failures += 1
push_error("FAIL: %s" % message)