mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
4007fab855
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.
149 lines
7.3 KiB
Python
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}"
|
|
)
|