Files
turnstone/tests/test_sse_overflow_js.py
T
Patrick Buckley 4007fab855 fix(#900): close two vacuity holes the round-2 scenarios left open
Round-3 review, unprimed. Two of its majors were the new scenarios
asserting things they did not prove — the false-detector class this
campaign keeps returning to.

E8 never checked that the held /history was still OUTSTANDING when the
redial completed. The disconnect/send/wait_turn/redial sequence is
unbounded (wait_turn alone allows 45s), so on a slow box the payload
resolves while evtSource is still null, the PRESENCE term declines it, and
the run stamps dupes1-healed1 without ever evaluating the generation term.
It now fails loudly with the counter values instead. E7 gained the same
positive proof its siblings already carried: sse_opens == 0 only means
"nothing connected in 8s", which is not the same as "the held load
settled and its .finally chose not to reconnect".

E5's stated control was simply wrong, in three places. A hide nulls
evtSource, and connectSSE early-returns while hidden, so the scenario
cannot produce the non-null-but-not-OPEN source that readyState === OPEN
exists for — it exercises the presence term only. The earlier control
removed both terms at once, which is what disguised it. The readyState
half is covered by reasoning plus coord parity, and its correctness twin
IS covered through the render-time gate by E6/E8; that scope is now
written down rather than overclaimed. Coord's G5 has the same shape.

The retry floor becomes a shared export beside its jitter: four sites must
move together (both clients' arms, both non-occurrence windows) and it was
the only one of them with no single source of truth. Interactive's use of
the expression had no pin at all — reverting it to a bare 2000 would have
broken cross-client parity with the suite green. Coord's re-anchor still
raised ValueError rather than failing on a named assertion, and its first
replacement used a fixed window that truncated mid-expression.
2026-07-24 18:15:03 -07:00

149 lines
7.3 KiB
Python

"""Static + runtime guards for the shared SSE overflow-recovery helper.
``turnstone/shared_static/sse_overflow.js`` is the client half of the SSE
overflow recovery — the storm-guard threshold, the cooldown-ladder constants,
and the two pure helpers (``overflowWindowTripped`` / ``degradedCooldownStep``)
— extracted so BOTH the interactive pane (``shared_static/interactive.js``) and
the coordinator pane (``console/static/coordinator/coordinator.js``) share one
source of truth for the trip math instead of drifting copies. The panes keep
their own transport/DOM glue; only the pure core lives here.
Like the rest of the WebUI the module has no JS test framework, so these are
Python-side string-presence assertions plus two ``node`` runtime probes that
execute the extracted pure functions — the storm-guard math is the part the
design review marked UNCONFIRMED, so it gets run, not just string-pinned.
"""
from __future__ import annotations
import os
import re
import subprocess
import tempfile
from pathlib import Path
import pytest
_ROOT = Path(__file__).resolve().parent.parent
_SSE_OVERFLOW = _ROOT / "turnstone/shared_static/sse_overflow.js"
def test_module_exports_constants_and_pure_helpers() -> None:
"""The single source of truth exports the eight tuning constants and the two
pure helpers. Both panes import these by name (pinned in their own suites),
so a rename here is a breaking change that must surface loudly."""
body = _SSE_OVERFLOW.read_text(encoding="utf-8")
for const, value in (
("OVERFLOW_TRIP_COUNT", "3"),
("OVERFLOW_TRIP_WINDOW_MS", "60000"),
("DEGRADED_COOLDOWN_BASE_MS", "15000"),
("DEGRADED_COOLDOWN_MAX_MS", "120000"),
("DEGRADED_COOLDOWN_RESET_MS", "300000"),
("TRUNCATED_RESYNC_JITTER_MS", "10000"),
# ADDITIVE over the retry's 2000 floor, and deliberately NOT the
# truncated-resync spread: this one works against #884's /history
# single-flight, so it stays small enough to sit inside a typical
# flight. Both e2e non-occurrence detectors size their windows on
# the floor plus this value.
("STALE_RETRY_JITTER_MS", "500"),
("STALE_RETRY_BASE_MS", "2000"),
):
assert f"export const {const} = {value};" in body, f"missing export const {const}"
assert "export function overflowWindowTripped(" in body
assert "export function degradedCooldownStep(" in body
def test_overflow_window_tripped_runtime() -> None:
"""Runtime probe for the limiter's rolling-window helper — the storm-guard
math is the part of Fix A the design review marked UNCONFIRMED, so it gets
executed, not just string-pinned: prunes stale entries in place, trips at
exactly K-in-window, and does not trip for closes spread wider than the
window."""
body = _SSE_OVERFLOW.read_text(encoding="utf-8")
m = re.search(
r"^export function overflowWindowTripped\(times, nowMs, count, windowMs\) \{.*?^\}",
body,
re.S | re.M,
)
assert m is not None, "overflowWindowTripped not found (keep it a module-level export)"
harness = (
m.group(0)
+ "\n"
+ "// trips at exactly count-in-window\n"
+ "let t = [1000, 2000, 3000];\n"
+ "if (!overflowWindowTripped(t, 3000, 3, 60000)) throw new Error('K-in-window must trip');\n"
+ "// stale entries prune in place and prevent the trip\n"
+ "t = [1000, 2000, 70000];\n"
+ "if (overflowWindowTripped(t, 70000, 3, 60000)) throw new Error('stale entries must not trip');\n"
+ "if (JSON.stringify(t) !== '[70000]') throw new Error('prune in place failed: ' + JSON.stringify(t));\n"
+ "// boundary: an entry exactly windowMs old is still counted\n"
+ "t = [10000, 70000];\n"
+ "if (!overflowWindowTripped(t, 70000, 2, 60000)) throw new Error('boundary entry must count');\n"
+ "// below threshold never trips\n"
+ "t = [];\n"
+ "if (overflowWindowTripped(t, 1, 1, 60000) !== false) throw new Error('empty must not trip');\n"
)
with tempfile.NamedTemporaryFile(mode="w", suffix=".mjs", delete=False) as f:
f.write(harness)
tmp = f.name
try:
proc = subprocess.run(["node", tmp], capture_output=True, text=True, timeout=15)
except FileNotFoundError:
pytest.skip("node binary not available on PATH")
finally:
os.unlink(tmp)
assert proc.returncode == 0, (
f"overflowWindowTripped runtime probe failed. stdout={proc.stdout!r} stderr={proc.stderr!r}"
)
def test_degraded_cooldown_ladder_escalates_and_resets_runtime() -> None:
"""Review finding [0] regression: the degraded-catchup cooldown ladder must
actually ESCALATE across consecutive trips (15→30→60→120s, capped) and reset
to base only after a genuine quiet gap. The original bug cleared the
overflow-window array in the trip handler, so the empty-window check reset
the cooldown to base on every storm's first overflow and the doubling never
took effect. The fix keys the ladder off a last-trip timestamp via the pure
degradedCooldownStep helper, exercised here directly."""
body = _SSE_OVERFLOW.read_text(encoding="utf-8")
m = re.search(
r"^export function degradedCooldownStep\(.*?\) \{.*?^\}",
body,
re.S | re.M,
)
assert m is not None, "degradedCooldownStep not found (keep it a module-level export)"
harness = (
m.group(0)
+ "\n"
+ "const BASE=15000, MAX=120000, RESET=300000;\n"
+ "function assert(c,msg){ if(!c) throw new Error(msg); }\n"
+ "// First trip: gap since lastTrip(0) exceeds RESET -> base, next doubles.\n"
+ "let s = degradedCooldownStep(BASE, 0, 1000000, BASE, MAX, RESET);\n"
+ "assert(s.cooldown===15000, 'first trip cooldown '+s.cooldown);\n"
+ "assert(s.nextCooldownMs===30000, 'first next '+s.nextCooldownMs);\n"
+ "// Second trip recurs within RESET -> escalates (uses the doubled prev).\n"
+ "s = degradedCooldownStep(30000, 1000000, 1030000, BASE, MAX, RESET);\n"
+ "assert(s.cooldown===30000, 'second trip must ESCALATE not reset, got '+s.cooldown);\n"
+ "assert(s.nextCooldownMs===60000, 'second next '+s.nextCooldownMs);\n"
+ "// Third + fourth keep escalating and cap at MAX.\n"
+ "s = degradedCooldownStep(60000, 1030000, 1060000, BASE, MAX, RESET);\n"
+ "assert(s.cooldown===60000 && s.nextCooldownMs===120000, 'third '+JSON.stringify(s));\n"
+ "s = degradedCooldownStep(120000, 1060000, 1090000, BASE, MAX, RESET);\n"
+ "assert(s.cooldown===120000 && s.nextCooldownMs===120000, 'fourth must cap at MAX '+JSON.stringify(s));\n"
+ "// A quiet gap longer than RESET resets the ladder to base.\n"
+ "s = degradedCooldownStep(120000, 1090000, 1090000+RESET+1, BASE, MAX, RESET);\n"
+ "assert(s.cooldown===15000, 'quiet gap must reset to base, got '+s.cooldown);\n"
)
with tempfile.NamedTemporaryFile(mode="w", suffix=".mjs", delete=False) as f:
f.write(harness)
tmp = f.name
try:
proc = subprocess.run(["node", tmp], capture_output=True, text=True, timeout=15)
except FileNotFoundError:
pytest.skip("node binary not available on PATH")
finally:
os.unlink(tmp)
assert proc.returncode == 0, (
f"degradedCooldownStep escalation probe failed. stderr={proc.stderr!r}"
)