""" 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" # One monolithic app.js used to hold every component; the group-page refactor # split it into one file per "application" (chat-app.js, files-app.js, # video-player.js, group-settings.js) plus the group shell (group-page.js). # A future Videos/Music/Photos app lands in its own file the same way — add it # here so this test keeps seeing it, since `_all_components` below only walks # the files named in this list. STATIC_FILES = [ "app.js", "group-page.js", "chat-app.js", "files-app.js", "video-player.js", "video-app.js", "music-app.js", "music-player.js", "photos-app.js", "group-settings.js", # The per-app settings architecture (docs/refactor-groups.md §3). Reached # through the apps.js registry rather than imported by name, so a file # left out of this list is one nothing checks — the failure is silent. "settings-ui.js", "folder-tree.js", "chat-app-settings.js", "video-app-settings.js", "music-app-settings.js", "photos-app-settings.js", "helloworld-app.js", "helloworld-app-settings.js", "auth-page.js", "explore-page.js", "create-group-page.js", # The Search page was missing from this list while being the densest # `useMemo` chain in the tree — a dozen derived lists, each depending on # the one above it, which is precisely the shape this checks. "search-page.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(src: str): """Each top-level component in one file, with the offset it starts at.""" for m in re.finditer(r"^function ([A-Z]\w*)\(", src, re.M): start = m.start() nxt = src.find("\nfunction ", start + 1) yield m.group(1), src[start:nxt if nxt > 0 else len(src)] def _all_components(): """Every top-level component across every static file that can hold one.""" for name in STATIC_FILES: path = STATIC / name if not path.exists(): continue for cname, body in _components(path.read_text()): yield f"{name}:{cname}", body def test_no_hook_depends_on_something_declared_below_it(): """Every static file that can hold a component, not just app.js.""" problems = [] for name, body in _all_components(): # 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(): """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 _all_components() if n == "video-player.js: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")