diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-17 02:16:28 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-17 02:16:28 +0200 |
| commit | 42047dac4041e72e09499e3adf145f1c0f83b284 (patch) | |
| tree | b816f20fd8b3b190faa69473b7a2892f3bfc9706 /packages/meshbay-hub/tests/test_hook_ordering.py | |
| parent | f5c4c058aa7e91fbdbbdd8cf34042535a05df433 (diff) | |
| download | meshbay-42047dac4041e72e09499e3adf145f1c0f83b284.tar.gz | |
test(hub): a hook that depends on one declared below it never runs
`const a = useCallback(fn, [b])` evaluates `[b]` where it is written, so a `b`
further down the component is still in its temporal dead zone. ReferenceError on
every render, before anything the component does can run — and the symptom is
the component simply not appearing. Clicking a video did nothing at all: no
picture, no error on screen, nothing in the node's log because nothing was ever
requested. It reached production.
Nothing caught it. `node --check` passes, the code is well-formed. Worse, the
MSE harness extracts the player functions into an order of its own and therefore
*reordered* them before running — quietly repairing the one class of defect it
was best placed to catch. It sorts by position in the file now, and
test_hook_ordering.py checks the property directly across the whole SPA. Both
the rule and the harness are checked against the layout that actually shipped.
test_video_seek.py covers the rest of seeking, and window_leak.mjs forces the
race that made the third seek hang: the whole in-flight window arriving while
`reinitAt` is still awaiting. Before, the player is left believing eight
segments are in flight and grants nothing; after, the window comes back. A run
that happens to work proves nothing about a race, which is the point of forcing
the worst case rather than trusting a longer session.
Diffstat (limited to 'packages/meshbay-hub/tests/test_hook_ordering.py')
| -rw-r--r-- | packages/meshbay-hub/tests/test_hook_ordering.py | 112 |
1 files changed, 112 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/test_hook_ordering.py b/packages/meshbay-hub/tests/test_hook_ordering.py new file mode 100644 index 0000000..cd9a11e --- /dev/null +++ b/packages/meshbay-hub/tests/test_hook_ordering.py @@ -0,0 +1,112 @@ +""" +A hook cannot depend on one declared below it. + +`const a = useCallback(fn, [b])` evaluates `[b]` where it is written. If `b` is +another `const` further down the component, it is still in its temporal dead +zone and the array throws `ReferenceError: Cannot access 'b' before +initialization` — during render, every render, before anything the component +does can run. + +The symptom is the component simply not appearing. Clicking a video did +nothing at all: no picture, no error on screen, nothing in the node's log +because nothing was ever requested. It reached production. + +Nothing else catches it. `node --check` validates syntax and this is +well-formed. The MSE harness runs the same functions but extracts them into a +list of its own choosing, so it *reorders* them and cannot see an ordering +fault — it is now ordered by position in the file for that reason, and this +test covers the case directly. +""" + +import re +from pathlib import Path + +import pytest + +STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static" +APP = STATIC / "app.js" + +pytestmark = pytest.mark.skipif(not APP.exists(), reason="SPA sources unavailable") + +# `const NAME = useCallback(` / `useMemo(` — the declarations that both define a +# binding and take a dependency array. +DECL = re.compile(r"^ const (\w+) = (?:useCallback|useMemo)\(", re.M) +# The closing `}, [a, b]);` of such a declaration. +DEPS = re.compile(r"^ \}, \[([^\]]*)\]\);", re.M) + + +@pytest.fixture(scope="module") +def app(): + return APP.read_text() + + +def _components(app: str): + """Each top-level component, with the offset it starts at.""" + for m in re.finditer(r"^function ([A-Z]\w*)\(", app, re.M): + start = m.start() + nxt = app.find("\nfunction ", start + 1) + yield m.group(1), app[start:nxt if nxt > 0 else len(app)] + + +def test_no_hook_depends_on_something_declared_below_it(app): + """The whole file, not just the player that was broken by it.""" + problems = [] + for name, body in _components(app): + # Where each hook binding becomes usable. + declared_at = {m.group(1): m.start() for m in DECL.finditer(body)} + for deps in DEPS.finditer(body): + for dep in (d.strip() for d in deps.group(1).split(",")): + if not dep or dep not in declared_at: + continue + if declared_at[dep] > deps.start(): + problems.append( + f"{name}: a hook at offset {deps.start()} lists `{dep}` " + f"as a dependency, but `{dep}` is declared below it") + assert not problems, ( + "a dependency array is evaluated where it is written, so this throws " + "on every render and the component never appears:\n " + + "\n ".join(problems)) + + +def test_the_check_would_notice(app): + """A test that cannot fail proves nothing — so make it fail on purpose. + + Swaps two declarations in the real file and confirms the rule fires. If + this stops working the rule above has quietly become decoration. + """ + body = next(b for n, b in _components(app) if n == "VideoPlayer") + decls = list(DECL.finditer(body)) + assert len(decls) >= 2, "VideoPlayer has too few hooks to test the check" + + # Build a body where the first hook depends on the last one, declared after. + first, last = decls[0].group(1), decls[-1].group(1) + broken = body.replace(decls[0].group(0), + decls[0].group(0), 1) + # Inject a dependency on `last` into the first declaration's dep array. + end = broken.index("\n }, [", decls[0].start()) + close = broken.index("]", end) + broken = broken[:close] + (", " if broken[end + 7:close].strip() else "") + last + broken[close:] + + declared_at = {m.group(1): m.start() for m in DECL.finditer(broken)} + caught = False + for deps in DEPS.finditer(broken): + for dep in (d.strip() for d in deps.group(1).split(",")): + if dep in declared_at and declared_at[dep] > deps.start(): + caught = True + assert caught, ( + f"made `{first}` depend on `{last}` which is declared after it, and the " + "rule did not fire — it is not checking what it claims to") + + +def test_the_mse_harness_reads_functions_in_source_order(): + """Otherwise it hides exactly this fault. + + The harness exists to run the shipped code rather than a paraphrase of it. + Extracting into an order of its own quietly repairs an ordering bug before + running it, which is the one class of defect it would otherwise be well + placed to catch. + """ + harness = (Path(__file__).parent / "harness" / "mse_harness.mjs").read_text() + assert "sort" in harness and "indexOf" in harness, ( + "the harness still extracts the player functions in a hardcoded order, " + "so it cannot see one declared before its own dependency") |