363 lines
19 KiB
GDScript
363 lines
19 KiB
GDScript
extends SceneTree
|
|
|
|
const HeatStateType = preload("res://scripts/authority/heat_state.gd")
|
|
const IncidentType = preload("res://scripts/authority/incident.gd")
|
|
const EvidenceType = preload("res://scripts/authority/evidence.gd")
|
|
const InspectionType = preload("res://scripts/authority/inspection.gd")
|
|
const AuthorityServiceType = preload("res://scripts/authority/authority_service.gd")
|
|
|
|
var _checks := 0
|
|
var _failures := 0
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("_run")
|
|
|
|
|
|
func _run() -> void:
|
|
print("S5: Authority, Incident, Evidence and Inspection")
|
|
_test_heat_incident_dedup_and_decay_floor()
|
|
_test_evidence_visibility_and_incident_scoring()
|
|
_test_inspection_selection_and_patrol_order()
|
|
_test_path_unreachable_records_departure_reason()
|
|
_test_resolution_idempotency()
|
|
_test_resolution_failure_is_atomic()
|
|
_test_snapshot_round_trip_and_atomicity()
|
|
if _failures == 0:
|
|
print("PASS: %d checks" % _checks)
|
|
else:
|
|
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
|
|
quit(_failures)
|
|
|
|
|
|
func _test_heat_incident_dedup_and_decay_floor() -> void:
|
|
var service := AuthorityServiceType.new()
|
|
var first = service.create_incident(
|
|
&"incident_shared",
|
|
&"illegal_service",
|
|
10,
|
|
&"noise_detector",
|
|
{&"severity": 80, &"heat_delta": 30, &"facility_id": &"hidden_room"}
|
|
)
|
|
_expect(first.is_success(), "first Incident report is accepted")
|
|
_expect(service.get_authority_state().heat == 30, "Incident applies its Heat delta once")
|
|
var duplicate = service.report_incident(
|
|
IncidentType.new().configure(
|
|
&"incident_shared",
|
|
&"conflicting_duplicate",
|
|
11,
|
|
{&"severity": 100, &"heat_delta": 100}
|
|
),
|
|
&"witness_detector"
|
|
)
|
|
_expect(duplicate.is_success() and duplicate.is_replay, "duplicate incident_id is an idempotent replay")
|
|
_expect(duplicate.details[&"heat_applied"] == 0 and service.get_authority_state().heat == 30, "second detector cannot add Heat again")
|
|
var canonical := service.get_incident(&"incident_shared")
|
|
_expect(canonical.detector_ids == [&"noise_detector", &"witness_detector"], "all unique detector IDs remain auditable")
|
|
_expect(canonical.incident_type == &"illegal_service" and canonical.severity == 80, "first Incident payload remains canonical")
|
|
|
|
service.advance_time(1_440, 15, 20)
|
|
_expect(service.get_authority_state().heat == 20, "daily decay stops at the active-case floor")
|
|
service.advance_time(2_880, 15, 20)
|
|
_expect(service.get_authority_state().heat == 20, "active-case floor remains stable on later days")
|
|
service.dismiss_incident(&"incident_shared")
|
|
service.advance_time(4_320, 15, 20)
|
|
_expect(service.get_authority_state().heat == 5, "Heat decays below the floor after the case closes")
|
|
var low_state := HeatStateType.new().configure({&"heat": 10, &"active_case_ids": [&"case_a"]})
|
|
low_state.apply_daily_decay(1, 5, 20)
|
|
_expect(low_state.heat == 10, "daily decay never raises pre-existing low Heat up to its floor")
|
|
_expect(service.validate_invariants()[&"ok"], "Incident dedup and active-case state satisfy invariants")
|
|
|
|
|
|
func _test_evidence_visibility_and_incident_scoring() -> void:
|
|
var los_calls := {&"count": 0, &"visible": false}
|
|
var service := AuthorityServiceType.new().configure_callbacks(
|
|
func(_from: Vector3i, _to: Vector3i, _context: Dictionary) -> bool:
|
|
los_calls[&"count"] = int(los_calls[&"count"]) + 1
|
|
return bool(los_calls[&"visible"])
|
|
)
|
|
service.create_incident(&"incident_evidence", &"hidden_facility", 0, &"detector_a", {&"heat_delta": 5})
|
|
service.create_evidence(&"evidence_wall", &"visual_trace", 25, {
|
|
&"incident_id": &"incident_evidence",
|
|
&"facility_id": &"illegal_room",
|
|
&"location_cell": Vector3i(5, 5, -1),
|
|
&"created_at": 0,
|
|
&"expires_at": 1_000,
|
|
})
|
|
var blocked = service.attempt_discover_evidence(
|
|
&"evidence_wall", &"inspector_a", Vector3i(4, 5, -1), 10
|
|
)
|
|
_expect(not blocked.is_success() and blocked.details[&"reason"] == &"LINE_OF_SIGHT_BLOCKED", "SYS-AUT-001: a wall-blocked callback prevents discovery")
|
|
_expect(service.get_authority_state().evidence_points == 0, "blocked visual evidence contributes no points")
|
|
var other_layer = service.attempt_discover_evidence(
|
|
&"evidence_wall", &"inspector_a", Vector3i(5, 5, 0), 11
|
|
)
|
|
_expect(not other_layer.is_success() and other_layer.details[&"reason"] == &"DIFFERENT_LAYER", "different layers never fall through to line-of-sight")
|
|
_expect(los_calls[&"count"] == 1, "cross-layer evidence does not call the visibility boundary")
|
|
los_calls[&"visible"] = true
|
|
var visible = service.attempt_discover_evidence(
|
|
&"evidence_wall", &"inspector_a", Vector3i(4, 5, -1), 12,
|
|
{&"illegal_confirmed": true}
|
|
)
|
|
_expect(visible.is_success() and visible.details[&"discovery_method"] == &"LINE_OF_SIGHT", "same-layer evidence is found only after the injected boundary confirms sight")
|
|
_expect(service.get_authority_state().evidence_points == 25, "discovered evidence contributes its severity")
|
|
_expect(service.get_authority_state().known_illegal_facility_ids == [&"illegal_room"], "confirmed visual evidence records the known illegal facility")
|
|
|
|
service.create_evidence(&"evidence_same_incident", &"second_photo", 25, {
|
|
&"incident_id": &"incident_evidence",
|
|
&"location_cell": Vector3i(8, 8, -1),
|
|
&"created_at": 0,
|
|
})
|
|
service.attempt_discover_evidence(
|
|
&"evidence_same_incident", &"inspector_a", Vector3i(8, 8, -1), 13
|
|
)
|
|
_expect(service.get_authority_state().evidence_points == 25, "SYS-AUT-002: the same Incident is not added to evidence points twice")
|
|
service.create_evidence(&"evidence_stronger", &"ledger", 40, {
|
|
&"incident_id": &"incident_evidence",
|
|
&"location_cell": Vector3i(99, 99, 5),
|
|
&"created_at": 0,
|
|
})
|
|
service.attempt_discover_evidence(
|
|
&"evidence_stronger", &"inspector_a", Vector3i.ZERO, 14,
|
|
{&"trusted_source": true}
|
|
)
|
|
_expect(service.get_authority_state().evidence_points == 40, "corroboration upgrades an Incident to its strongest evidence rather than stacking")
|
|
var replay = service.attempt_discover_evidence(
|
|
&"evidence_stronger", &"inspector_a", Vector3i.ZERO, 15,
|
|
{&"trusted_source": true}
|
|
)
|
|
_expect(replay.is_success() and replay.is_replay and service.get_authority_state().evidence_points == 40, "repeated evidence discovery is idempotent")
|
|
_expect(service.validate_invariants()[&"ok"], "visibility discoveries and reverse references satisfy invariants")
|
|
|
|
|
|
func _test_inspection_selection_and_patrol_order() -> void:
|
|
var service := AuthorityServiceType.new().configure_callbacks(
|
|
Callable(),
|
|
func(_from: Vector3i, _to: Vector3i, _context: Dictionary) -> Dictionary:
|
|
return {&"reachable": true, &"travel_minutes": 2}
|
|
)
|
|
service.apply_heat_delta(30, &"fixture", 0)
|
|
service.set_suspicion(20)
|
|
var selected = service.evaluate_inspection(360, {
|
|
&"base_chance": 5,
|
|
&"heat_weight": 1,
|
|
&"suspicion_weight": 1,
|
|
&"overdue_case_bonus": 0,
|
|
&"reputation_protection": 0,
|
|
&"roll": 54,
|
|
&"inspection_id": &"inspection_order",
|
|
&"inspector_id": &"inspector_order",
|
|
&"entrance_cell": Vector3i.ZERO,
|
|
})
|
|
_expect(selected.is_success() and selected.details[&"chance"] == 55 and selected.details[&"selected"], "06:00 inspection chance uses deterministic integer terms")
|
|
var replay = service.evaluate_inspection(360, {&"roll": 99})
|
|
_expect(replay.is_success() and replay.is_replay and replay.details[&"inspection_id"] == &"inspection_order", "same-day inspection selection replays the stored result")
|
|
_expect(service.get_authority_state().next_inspection_window == 1_800, "selection updates the next inspection Window")
|
|
service.start_inspection(&"inspection_order", 360)
|
|
service.mark_inspector_arrived(&"inspection_order", 361)
|
|
var candidates := [
|
|
{&"waypoint_id": &"z_tie", &"cell": Vector3i(3, 0, 0), &"suspect_score": 10, &"evidence_potential": 10, &"travel_minutes": 5, &"inspection_minutes": 5},
|
|
{&"waypoint_id": &"top", &"cell": Vector3i(1, 0, 0), &"suspect_score": 10, &"evidence_potential": 20, &"travel_minutes": 5, &"inspection_minutes": 5},
|
|
{&"waypoint_id": &"a_tie", &"cell": Vector3i(2, 0, 0), &"suspect_score": 10, &"evidence_potential": 10, &"travel_minutes": 5, &"inspection_minutes": 5},
|
|
]
|
|
var plan = service.plan_patrol(&"inspection_order", candidates, 362, 30)
|
|
_expect(plan.is_success(), "patrol plan is accepted after CHECK_IN")
|
|
_expect(plan.details[&"waypoint_ids"] == [&"top", &"a_tie", &"z_tie"], "patrol sorts Utility descending then waypoint ID lexically")
|
|
_expect(service.get_inspection(&"inspection_order").discovered_evidence_ids.is_empty(), "planning patrol does not perform an instantaneous map scan")
|
|
var moved = service.advance_patrol(&"inspection_order", 363)
|
|
_expect(moved.is_success() and moved.details[&"waypoint_id"] == &"top", "patrol advances exactly one waypoint per call")
|
|
|
|
var second := AuthorityServiceType.new()
|
|
second.schedule_inspection(360, {&"inspection_id": &"stable", &"inspector_id": &"stable_i"})
|
|
second.start_inspection(&"stable", 360)
|
|
second.mark_inspector_arrived(&"stable", 361)
|
|
var reversed := candidates.duplicate(true)
|
|
reversed.reverse()
|
|
var second_plan = second.plan_patrol(&"stable", reversed, 362, 30)
|
|
_expect(second_plan.details[&"waypoint_ids"] == plan.details[&"waypoint_ids"], "patrol route is independent of candidate input order")
|
|
|
|
|
|
func _test_path_unreachable_records_departure_reason() -> void:
|
|
var service := AuthorityServiceType.new().configure_callbacks(
|
|
Callable(),
|
|
func(_from: Vector3i, _to: Vector3i, _context: Dictionary) -> Dictionary:
|
|
return {&"reachable": false, &"reason": &"SEALED_BY_WALL"}
|
|
)
|
|
service.schedule_inspection(10, {
|
|
&"inspection_id": &"inspection_blocked",
|
|
&"inspector_id": &"inspector_blocked",
|
|
&"entrance_cell": Vector3i.ZERO,
|
|
})
|
|
service.start_inspection(&"inspection_blocked", 10)
|
|
service.mark_inspector_arrived(&"inspection_blocked", 11)
|
|
service.plan_patrol(&"inspection_blocked", [{
|
|
&"waypoint_id": &"sealed_room",
|
|
&"cell": Vector3i(9, 9, -1),
|
|
&"suspect_score": 90,
|
|
&"evidence_potential": 90,
|
|
&"travel_minutes": 10,
|
|
}], 12)
|
|
var blocked = service.advance_patrol(&"inspection_blocked", 13)
|
|
var inspection := service.get_inspection(&"inspection_blocked")
|
|
_expect(blocked.is_success() and not blocked.details[&"reachable"], "unreachable patrol target is a handled inspection outcome")
|
|
_expect(inspection.state == InspectionType.State.DEPARTING, "SYS-AUT-004: unreachable inspector enters DEPARTING instead of teleporting")
|
|
_expect(inspection.exit_reason == &"PATH_UNREACHABLE" and inspection.result_reason == &"SEALED_BY_WALL", "path failure records both stable exit and detailed result reasons")
|
|
_expect(inspection.inaccessible_waypoint_ids == [&"sealed_room"] and inspection.visited_waypoint_ids.is_empty(), "unreachable waypoint is recorded without being marked visited")
|
|
var departed = service.depart_inspection(&"inspection_blocked", 14)
|
|
_expect(departed.is_success() and inspection.state == InspectionType.State.CLOSED, "inspector safely closes after departure")
|
|
_expect(departed.details[&"exit_reason"] == &"PATH_UNREACHABLE", "closed inspection preserves the path-unreachable exit reason")
|
|
_expect(service.validate_invariants()[&"ok"], "path-unreachable inspection remains snapshot-safe")
|
|
|
|
|
|
func _test_resolution_idempotency() -> void:
|
|
var applications := {&"validate": 0, &"apply": 0, &"fine_total": 0, &"sanctions": []}
|
|
var service := AuthorityServiceType.new().configure_callbacks(
|
|
func(_from: Vector3i, _to: Vector3i, _context: Dictionary) -> bool: return true,
|
|
func(_from: Vector3i, _to: Vector3i, _context: Dictionary) -> bool: return true,
|
|
func(payload: Dictionary, phase: StringName, _token: Variant) -> Dictionary:
|
|
applications[phase] = int(applications.get(phase, 0)) + 1
|
|
if phase == &"apply":
|
|
applications[&"fine_total"] = int(applications[&"fine_total"]) + int(payload[&"fine_amount"])
|
|
(applications[&"sanctions"] as Array).append(payload[&"sanction"])
|
|
return {&"ok": true, &"token": &"validated"}
|
|
)
|
|
service.create_incident(&"incident_fine", &"illegal_facility", 0, &"detector", {&"severity": 80, &"heat_delta": 10})
|
|
service.create_evidence(&"evidence_fine", &"physical", 60, {
|
|
&"incident_id": &"incident_fine",
|
|
&"facility_id": &"facility_illegal",
|
|
&"location_cell": Vector3i(2, 0, 0),
|
|
&"created_at": 0,
|
|
})
|
|
_prepare_reviewing_inspection(service, &"inspection_fine", &"inspector_fine", &"evidence_fine")
|
|
var first = service.resolve_inspection(&"inspection_fine", &"resolution_once", 5, {&"fine_per_point": 10})
|
|
_expect(first.is_success() and first.details[&"result"] == &"TEMPORARY_CLOSURE", "evidence thresholds deterministically select the sanction tier")
|
|
_expect(first.details[&"fine_amount"] == 600 and first.details[&"sanction"] == &"TEMPORARY_CLOSURE", "resolution projection contains fine and sanction for external domains")
|
|
_expect(applications[&"validate"] == 1 and applications[&"apply"] == 1, "resolution boundary validates then applies exactly once")
|
|
_expect(service.get_authority_state().fines_due == 600 and applications[&"fine_total"] == 600, "successful resolution records one fine liability and one external application")
|
|
_expect(service.get_incident(&"incident_fine").state == IncidentType.State.RESOLVED, "resolution closes linked active Incident cases")
|
|
var duplicate = service.resolve_inspection(&"inspection_fine", &"resolution_once", 6, {&"fine_per_point": 999})
|
|
_expect(duplicate.is_success() and duplicate.is_replay, "SYS-AUT-003: duplicate resolution_id returns its stored result")
|
|
_expect(applications[&"apply"] == 1 and service.get_authority_state().fines_due == 600, "duplicate resolution cannot apply fine or sanction twice")
|
|
_expect(duplicate.details[&"fine_amount"] == 600, "replay ignores conflicting later resolution terms")
|
|
_expect(service.validate_invariants()[&"ok"], "resolved inspection, resolution and Incident references satisfy invariants")
|
|
|
|
|
|
func _test_resolution_failure_is_atomic() -> void:
|
|
var external := {&"fine": 0, &"rollback": 0}
|
|
var service := AuthorityServiceType.new().configure_callbacks(
|
|
func(_from: Vector3i, _to: Vector3i, _context: Dictionary) -> bool: return true,
|
|
func(_from: Vector3i, _to: Vector3i, _context: Dictionary) -> bool: return true,
|
|
func(payload: Dictionary, phase: StringName, token: Variant) -> Dictionary:
|
|
if phase == &"validate":
|
|
return {&"ok": true, &"token": int(external[&"fine"])}
|
|
if phase == &"apply":
|
|
external[&"fine"] = int(external[&"fine"]) + int(payload[&"fine_amount"])
|
|
return {&"ok": false, &"token": token, &"reason": &"LEDGER_REJECTED"}
|
|
if phase == &"rollback":
|
|
external[&"fine"] = int(token)
|
|
external[&"rollback"] = int(external[&"rollback"]) + 1
|
|
return {&"ok": true}
|
|
return {&"ok": false}
|
|
)
|
|
service.create_incident(&"incident_fail", &"illegal_facility", 0, &"detector", {&"heat_delta": 5})
|
|
service.create_evidence(&"evidence_fail", &"physical", 30, {
|
|
&"incident_id": &"incident_fail",
|
|
&"location_cell": Vector3i(2, 0, 0),
|
|
&"created_at": 0,
|
|
})
|
|
_prepare_reviewing_inspection(service, &"inspection_fail", &"inspector_fail", &"evidence_fail")
|
|
var before: Dictionary = service.create_snapshot()
|
|
var failed = service.resolve_inspection(&"inspection_fail", &"resolution_fail", 5)
|
|
_expect(not failed.is_success() and failed.details[&"rollback_ok"], "failed external resolution invokes its rollback boundary")
|
|
_expect(external[&"fine"] == 0 and external[&"rollback"] == 1, "failed fine application leaves no external value behind")
|
|
_expect(service.create_snapshot() == before, "failed resolution leaves the full Authority snapshot unchanged")
|
|
_expect(service.get_resolution(&"resolution_fail").is_empty(), "failed resolution_id is not marked processed")
|
|
|
|
|
|
func _test_snapshot_round_trip_and_atomicity() -> void:
|
|
var application_count := {&"apply": 0}
|
|
var service := AuthorityServiceType.new().configure_callbacks(
|
|
func(_from: Vector3i, _to: Vector3i, _context: Dictionary) -> bool: return true,
|
|
func(_from: Vector3i, _to: Vector3i, _context: Dictionary) -> bool: return true,
|
|
func(_payload: Dictionary, phase: StringName, _token: Variant) -> Dictionary:
|
|
if phase == &"apply":
|
|
application_count[&"apply"] = int(application_count[&"apply"]) + 1
|
|
return {&"ok": true}
|
|
)
|
|
service.create_incident(&"incident_save", &"contraband", 2, &"detector_b", {&"heat_delta": 12})
|
|
service.report_incident(
|
|
IncidentType.new().configure(&"incident_save", &"contraband", 2, {&"heat_delta": 12}),
|
|
&"detector_a"
|
|
)
|
|
service.create_evidence(&"evidence_save", &"inventory_record", 30, {
|
|
&"incident_id": &"incident_save",
|
|
&"location_cell": Vector3i(1, 1, 0),
|
|
&"created_at": 2,
|
|
})
|
|
_prepare_reviewing_inspection(service, &"inspection_save", &"inspector_save", &"evidence_save")
|
|
service.resolve_inspection(&"inspection_save", &"resolution_save", 5)
|
|
service.depart_inspection(&"inspection_save", 6)
|
|
service.advance_time(1_440)
|
|
var snapshot: Dictionary = service.create_snapshot()
|
|
var restored := AuthorityServiceType.new().configure_callbacks(
|
|
Callable(), Callable(),
|
|
func(_payload: Dictionary, phase: StringName, _token: Variant) -> Dictionary:
|
|
if phase == &"apply":
|
|
application_count[&"apply"] = int(application_count[&"apply"]) + 1
|
|
return {&"ok": true}
|
|
)
|
|
_expect(restored.restore_snapshot(snapshot), "valid Authority snapshot restores")
|
|
_expect(restored.create_snapshot() == snapshot, "Authority snapshot round trips exactly")
|
|
_expect(restored.validate_invariants()[&"ok"], "restored Authority aggregate satisfies invariants")
|
|
var replay = restored.resolve_inspection(&"inspection_save", &"resolution_save", 1_441)
|
|
_expect(replay.is_success() and replay.is_replay, "resolution idempotency survives save and load")
|
|
_expect(application_count[&"apply"] == 1, "loaded resolution replay never calls external application again")
|
|
var before: Dictionary = restored.create_snapshot()
|
|
var corrupt := snapshot.duplicate(true)
|
|
corrupt[&"authority_state"][&"evidence_points"] = 999
|
|
_expect(not restored.restore_snapshot(corrupt), "cross-domain cache mismatch rejects a corrupt Authority snapshot")
|
|
_expect(restored.create_snapshot() == before, "failed Authority restore leaves every live field unchanged")
|
|
var wrong_version := snapshot.duplicate(true)
|
|
wrong_version[&"version"] = 999
|
|
_expect(not restored.restore_snapshot(wrong_version), "unsupported Authority snapshot version is rejected")
|
|
_expect(restored.create_snapshot() == before, "version rejection is also atomic")
|
|
var out_of_range := snapshot.duplicate(true)
|
|
out_of_range[&"authority_state"][&"heat"] = 101
|
|
_expect(not restored.restore_snapshot(out_of_range), "out-of-range snapshot values are rejected instead of silently clamped")
|
|
_expect(restored.create_snapshot() == before, "strict value rejection preserves the live aggregate")
|
|
|
|
|
|
func _prepare_reviewing_inspection(
|
|
service: AuthorityService,
|
|
inspection_id: StringName,
|
|
inspector_id: StringName,
|
|
evidence_id: StringName
|
|
) -> void:
|
|
service.schedule_inspection(0, {
|
|
&"inspection_id": inspection_id,
|
|
&"inspector_id": inspector_id,
|
|
&"entrance_cell": Vector3i.ZERO,
|
|
})
|
|
service.start_inspection(inspection_id, 0)
|
|
service.mark_inspector_arrived(inspection_id, 1)
|
|
service.plan_patrol(inspection_id, [{
|
|
&"waypoint_id": StringName("waypoint_%s" % inspection_id),
|
|
&"cell": Vector3i(1, 1, 0) if evidence_id == &"evidence_save" else Vector3i(2, 0, 0),
|
|
&"suspect_score": 100,
|
|
&"evidence_potential": 100,
|
|
&"travel_minutes": 1,
|
|
&"inspection_minutes": 1,
|
|
}], 2)
|
|
service.advance_patrol(inspection_id, 3)
|
|
service.inspect_current_waypoint(inspection_id, [evidence_id], 4)
|
|
service.finish_patrol(inspection_id, 5)
|
|
|
|
|
|
func _expect(condition: bool, label: String) -> void:
|
|
_checks += 1
|
|
if condition:
|
|
print(" ✓ %s" % label)
|
|
return
|
|
_failures += 1
|
|
push_error(" ✗ %s" % label)
|