mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-11 08:23:45 +00:00
cf73074e27
A second adversarial review of the previous fix commit found two of its nine fixes silently defeated each other: the seq-range guard (fix for a MEDIUM epoch-mismatch finding) capped the exact variable the ring-overflow resync (fix for the original CRITICAL finding) depends on, making the resync unreachable in production and recreating permanent input death at a lower failure threshold, reachable via ordinary server tick loss alone. - CRITICAL: rebind the seq-range guard to InputJitterBuffer's own highest_ingested_seq (now public) instead of the consumer-side last_applied_seq, so it tracks the client's send epoch rather than a value that can lag arbitrarily far behind during a stall. - HIGH: InputLeadController's release logic still ANDed the old `lead > LEAD_MIN` gate onto the new depth-driven condition, so a backlog the controller never caused still couldn't drain. Split into two independent decisions: the seq-duplicate action follows real depth alone; lead's own bookkeeping separately never drops below its floor. - MEDIUM: widen the CI driver's movement/stalled sampling margin (run_seconds - 2.0, was - 0.5) and assert the peer is still in multiplayer.get_peers() at sample time, since the old margin let the check pass on residual starvation grace after a bot had already disconnected. - LOW: measure horizontal-only displacement in the human smoke test's movement check — the old 3D-distance bar was beatable by pure gravity settling with fully dead input. - LOW: fix a real "clean stderr" violation (match_net.gd broadcasting a departure notice to a peer whose ENet channels are already torn down, including a second peer disconnecting in the same poll batch) by deferring the notification to the next idle frame. - Wire the server's per-slot stalled bit into the client debug overlay for real — a prior commit message claimed this already reached the overlay when only the CI gate actually read it. Re-verified end-to-end against the real production RPC path (not just unit tests in isolation, which is how the composition bug got past the first round): a 2-bot CI match with a 1.5s host SIGSTOP freeze injected mid-run, well past the 0.6s threshold the review reproduced the bug at, now recovers cleanly on repeated runs with zero stderr noise.
148 lines
7.4 KiB
GDScript
148 lines
7.4 KiB
GDScript
extends "res://tests/test_case.gd"
|
|
|
|
const InputLeadController = preload("res://scripts/input_lead_controller.gd")
|
|
|
|
|
|
func test_starts_at_minimum() -> void:
|
|
var c := InputLeadController.new()
|
|
assert_eq(c.lead, InputLeadController.LEAD_MIN, "initial lead")
|
|
|
|
|
|
func test_unknown_depth_is_a_normal_tick() -> void:
|
|
var c := InputLeadController.new()
|
|
assert_eq(c.update(-1), 1, "no snapshot info yet -> ordinary +1 seq increment")
|
|
assert_eq(c.lead, InputLeadController.LEAD_MIN, "lead unchanged with no info")
|
|
|
|
|
|
func test_healthy_depth_is_a_normal_tick_and_no_immediate_release() -> void:
|
|
var c := InputLeadController.new()
|
|
for i in 10:
|
|
assert_eq(c.update(1), 1, "healthy depth -> ordinary +1 tick %d" % i)
|
|
assert_eq(c.lead, InputLeadController.LEAD_MIN, "release needs 2s clean, not 10 ticks")
|
|
|
|
|
|
# §3.3: "on any starve, increase by up to 3 immediately" — but debounced by
|
|
# MIN_CHANGE_INTERVAL_TICKS so it isn't literally same-tick.
|
|
func test_starve_triggers_fast_attack_after_debounce_floor() -> void:
|
|
var c := InputLeadController.new()
|
|
var deltas: Array[int] = []
|
|
for i in InputLeadController.MIN_CHANGE_INTERVAL_TICKS:
|
|
deltas.append(c.update(0))
|
|
# Every tick before the debounce floor is an ordinary +1 (no jump yet).
|
|
for i in InputLeadController.MIN_CHANGE_INTERVAL_TICKS - 1:
|
|
assert_eq(deltas[i], 1, "no lead change before the debounce floor, tick %d" % i)
|
|
assert_eq(deltas[InputLeadController.MIN_CHANGE_INTERVAL_TICKS - 1], 4, "attack fires on the debounce-floor tick: +1 ordinary + 3 skip")
|
|
assert_eq(c.lead, InputLeadController.LEAD_MIN + 3, "lead jumped by 3")
|
|
|
|
|
|
func test_repeated_starvation_climbs_toward_max_and_clamps() -> void:
|
|
var c := InputLeadController.new()
|
|
# Enough sustained starvation to trigger several attack steps.
|
|
for i in InputLeadController.MIN_CHANGE_INTERVAL_TICKS * 6:
|
|
c.update(0)
|
|
assert_eq(c.lead, InputLeadController.LEAD_MAX, "clamps at LEAD_MAX under sustained starvation, never exceeds it")
|
|
|
|
|
|
func test_release_requires_both_clean_surplus_and_its_own_interval() -> void:
|
|
var c := InputLeadController.new()
|
|
# Force lead above minimum first via one attack step.
|
|
for i in InputLeadController.MIN_CHANGE_INTERVAL_TICKS:
|
|
c.update(0)
|
|
var lead_after_attack := c.lead
|
|
assert_true(lead_after_attack > InputLeadController.LEAD_MIN, "lead raised above minimum before testing release")
|
|
|
|
# Fewer than CLEAN_SURPLUS_TICKS of surplus depth (above TARGET_DEPTH):
|
|
# must not release yet.
|
|
for i in InputLeadController.CLEAN_SURPLUS_TICKS - 1:
|
|
c.update(InputLeadController.TARGET_DEPTH + 1)
|
|
assert_eq(c.lead, lead_after_attack, "no release before 2s of clean surplus has elapsed")
|
|
|
|
# One more surplus tick crosses the clean-surplus threshold AND the
|
|
# release interval (both are already satisfied by now since the
|
|
# debounce timer has been running the whole time) -> releases by 1.
|
|
var delta := c.update(InputLeadController.TARGET_DEPTH + 1)
|
|
assert_eq(delta, 0, "release tick duplicates rather than incrementing seq")
|
|
assert_eq(c.lead, lead_after_attack - 1, "lead released by exactly 1")
|
|
|
|
|
|
func test_release_stops_at_minimum() -> void:
|
|
var c := InputLeadController.new()
|
|
# Sustained surplus depth with lead already at LEAD_MIN: `lead` itself
|
|
# must never drop below the floor, but release must still fire
|
|
# (duplicate a seq) once its own timing conditions are met, since a
|
|
# real reported surplus at floor lead is exactly the "backlog this
|
|
# controller never caused" case — capping `lead` is cosmetic, it must
|
|
# not also block the seq-duplicate action that drains real depth.
|
|
var released := false
|
|
for i in InputLeadController.CLEAN_SURPLUS_TICKS * 3:
|
|
if c.update(InputLeadController.TARGET_DEPTH + 1) == 0:
|
|
released = true
|
|
assert_eq(c.lead, InputLeadController.LEAD_MIN, "lead never drops below the floor, tick %d" % i)
|
|
assert_true(released, "release still fires (duplicates a seq) even though lead itself is pinned at minimum")
|
|
|
|
|
|
func test_starve_resets_clean_surplus_counter() -> void:
|
|
var c := InputLeadController.new()
|
|
for i in InputLeadController.MIN_CHANGE_INTERVAL_TICKS:
|
|
c.update(0) # raise lead above minimum via one attack step
|
|
var lead_after_attack := c.lead
|
|
|
|
# Some, but not all, of a clean surplus window — and well under the
|
|
# 30-tick attack debounce floor too, so the interrupting starve below
|
|
# can't accidentally retrigger a second attack step of its own.
|
|
var partial_clean_ticks := 10
|
|
for i in partial_clean_ticks:
|
|
c.update(InputLeadController.TARGET_DEPTH + 1)
|
|
c.update(0) # a lone starve tick, resetting _clean_surplus_ticks
|
|
assert_eq(c.lead, lead_after_attack, "the lone starve tick was too soon after the last change to trigger another attack")
|
|
|
|
# A full clean window from this fresh starting point is required before
|
|
# release fires — one tick short must not be enough.
|
|
for i in InputLeadController.CLEAN_SURPLUS_TICKS - 1:
|
|
c.update(InputLeadController.TARGET_DEPTH + 1)
|
|
assert_eq(c.lead, lead_after_attack, "the starve interruption forced a fresh 2s clean window, so no release yet")
|
|
c.update(InputLeadController.TARGET_DEPTH + 1)
|
|
assert_eq(c.lead, lead_after_attack - 1, "release finally fires once a full fresh clean window has elapsed since the interruption")
|
|
|
|
|
|
# A first attempt at fixing this gated the whole release branch on `lead >
|
|
# LEAD_MIN` — this controller's own memory of past attacks — so a backlog
|
|
# it did NOT itself create (a server hitch, persistent client/server clock
|
|
# drift, a burst re-delivery) was never drained: lead stayed at 1 forever
|
|
# even while the server kept reporting a deep, real backlog, and — because
|
|
# that gate blocked the seq-duplicate action too, not just lead's own
|
|
# bookkeeping — the actual buffered depth was never drained either. A
|
|
# second adversarial review caught that the depth check added alongside
|
|
# it didn't remove the old gate, just sat next to it. This reproduces the
|
|
# scenario directly: lead never attacks (depth is never reported as a
|
|
# starve, <= 0), yet release must still fire from sustained real surplus
|
|
# alone, even while lead itself stays pinned at its floor throughout.
|
|
func test_release_drains_a_backlog_it_never_caused_itself() -> void:
|
|
var c := InputLeadController.new()
|
|
assert_eq(c.lead, InputLeadController.LEAD_MIN, "starts at minimum, never attacked")
|
|
|
|
# A large, externally-caused surplus (e.g. right after the server's own
|
|
# ring-overflow resync) reported for well over 2s — lead never moves
|
|
# via attack since depth is never <= 0.
|
|
var released_at_floor := false
|
|
for i in InputLeadController.CLEAN_SURPLUS_TICKS + InputLeadController.RELEASE_INTERVAL_TICKS:
|
|
if c.update(10) == 0:
|
|
released_at_floor = true
|
|
assert_eq(c.lead, InputLeadController.LEAD_MIN, "lead's own bookkeeping never drops below its floor")
|
|
assert_true(released_at_floor, "release still fires (duplicates a seq, actually draining real depth) even while lead is pinned at the floor")
|
|
|
|
# Raise it above the floor via one real attack, then confirm sustained
|
|
# external surplus (not self-caused) still drains it back down.
|
|
for i in InputLeadController.MIN_CHANGE_INTERVAL_TICKS:
|
|
c.update(0)
|
|
var lead_after_attack := c.lead
|
|
assert_true(lead_after_attack > InputLeadController.LEAD_MIN, "attack raised lead")
|
|
|
|
var released := false
|
|
for i in InputLeadController.CLEAN_SURPLUS_TICKS + InputLeadController.RELEASE_INTERVAL_TICKS:
|
|
if c.update(10) == 0:
|
|
released = true
|
|
break
|
|
assert_true(released, "sustained externally-caused surplus (depth=10) must eventually trigger a release")
|
|
assert_true(c.lead < lead_after_attack, "lead actually decreased in response to real depth, not just internal bookkeeping")
|