"""Unit: the shared-header control bindings are EXPLICIT init, once per document (2026-09-08 production double-binding fix). The bug: header.js's control bindings (sign-out, the mobile hamburger, the SINGLE New chat button) used to run at MODULE IMPORT. Under native ESM that is one instance per document — fine. But the Containerfile stage-1 build inlines header.js into every bundle that imports it (the shell's app.js, token-gate.js and the router's lazy views each carry a copy), and top-level code runs once per copy: the shell page registered the #nav-toggle click handler twice (the app.js + token-gate.js bundles), and two toggle handlers cancel each other — one tap = open + close = a menu dead in the deployed image only. The dev tree (raw ESM, single instance) never showed it, and neither did any test that runs against the dev tree; once a lazy view load added a THIRD copy the odd count made the menu work again, which is why the failure looked state-dependent (chat cold boot dead, /sources.html alive). The contract pinned here: * header.js owns the three bindings in ONE exported function — ``bindSharedHeaderControls()`` — whose idempotency marker lives on ```` (NOT in module state: every bundle copy has its own function instance, so only the document can say "the first caller won"); * NO top-level ``addEventListener`` remains in header.js — the one surviving listener outside the init is the steering per-note delete inside ``renderSteeringPanel`` (a per-node callback factory, not a module binding); * the four page scripts that ship header controls (app.js / login.js / shared.js / document.js) each call ``bindSharedHeaderControls()`` exactly once as a module-top statement; doc-edit.js ships no header controls and calls nothing. """ from __future__ import annotations import re from pathlib import Path FRONTEND = Path(__file__).resolve().parents[2] / "frontend" ASSETS = FRONTEND / "assets" HEADER_JS = ASSETS / "header.js" #: The call as a module-top statement (anchored — comment mentions of #: the function can never match). _CALL = re.compile(r"^\s*bindSharedHeaderControls\(\);\s*$", re.M) _IMPORT = re.compile( r"import\s*\{[^}]*bindSharedHeaderControls[^}]*\}\s*from\s*\"./header\.js\"", re.S, ) def _text(path: Path) -> str: assert path.is_file(), f"missing frontend file: {path.name}" return path.read_text(encoding="utf-8") def _js() -> str: return _text(HEADER_JS) def _strip_comments(code: str) -> str: """Block + line comments stripped (comments may legally carry the keywords the pins below count). header.js carries no string literal containing ``//`` or braces, so the strip is exact.""" code = re.sub(r"/\*.*?\*/", "", code, flags=re.S) return re.sub(r"//[^\n]*", "", code) def _function_body(code: str, signature: str) -> str: """The full text from ``signature`` to its brace-matched closing brace (neither body carries an unbalanced brace inside a string, so the count is exact).""" start = code.index(signature) i = code.index("{", start) depth = 0 for j in range(i, len(code)): if code[j] == "{": depth += 1 elif code[j] == "}": depth -= 1 if depth == 0: return code[start : j + 1] raise AssertionError(f"unbalanced braces after {signature!r}") # ---------- the init function: one entry point, document-level guard ---------- def test_header_exports_the_explicit_binding_init() -> None: js = _js() assert "export function bindSharedHeaderControls" in js, ( "header.js must export bindSharedHeaderControls — the single " "entry point for the shared-header control bindings" ) def test_the_idempotency_marker_lives_on_the_document() -> None: """The guard key is read once + written once, on document.body — NOT in module state (every bundle copy has its own function instance; only the document can say the first caller won).""" js = _strip_comments(_js()) assert js.count("borHeaderBound") == 2, ( f"exactly one read (the guard) + one write (the marker) of the " f"body dataset key (found {js.count('borHeaderBound')})" ) assert "document.body" in js, "the marker lives on the document body" assert 'body.dataset.borHeaderBound = "1"' in js, ( "the marker must be WRITTEN by the first caller" ) # ---------- all three bindings live inside the init ---------- def test_all_three_control_bindings_live_inside_the_init() -> None: js = _strip_comments(_js()) body = _function_body(js, "export function bindSharedHeaderControls") for needle in ( # sign-out (bar copy for desktop + mobile dropdown copy) 'querySelectorAll(".sign-out-btn")', 'fetch("/api/logout", { method: "POST" })', # the mobile hamburger (phase 46) 'querySelector("#nav-toggle")', 'querySelector("#app-nav")', "function setNavMenu(open)", 'navToggle.addEventListener("click"', # the SINGLE New chat binding (phase 34 task 02) 'querySelector("#new-chat-btn")', 'new CustomEvent("bor:new-chat")', ): assert needle in body, ( f"{needle!r} must live INSIDE bindSharedHeaderControls — " "a binding outside the init runs at module import in every " "bundle copy that inlines header.js" ) # ---------- the anti-regression pin: no top-level listener remains ---------- def test_no_top_level_add_event_listener_remains() -> None: """The pin for the double-binding class itself: NOTHING outside bindSharedHeaderControls may register a listener at module scope — the Containerfile inlines header.js into every bundle that imports it, and top-level binding code runs once per bundle copy (two #nav-toggle toggle handlers cancel each other = the dead production menu). The ONE surviving addEventListener outside the init is the steering per-note delete INSIDE renderSteeringPanel — a per-node callback factory (one listener per note button created in the DOM), not a module binding.""" js = _strip_comments(_js()) init_body = _function_body(js, "export function bindSharedHeaderControls") cut = js.index(init_body) rest = js[:cut] + js[cut + len(init_body) :] assert rest.count("addEventListener") == 1, ( "exactly one addEventListener may survive outside the init — " f"the steering per-note delete (found {rest.count('addEventListener')})" ) panel = _function_body(rest, "function renderSteeringPanel") assert "addEventListener" in panel, ( "the surviving listener must be the steering per-note delete " "(renderSteeringPanel)" ) # ---------- the page scripts: one explicit call each, at module top ---------- def test_each_header_carrying_page_script_calls_the_init_exactly_once() -> None: for name in ("app.js", "login.js", "shared.js", "document.js"): js = _text(ASSETS / name) assert _IMPORT.search(js), ( f"{name}: must import bindSharedHeaderControls from " '"./header.js" (relative — the bundler contract)' ) calls = _CALL.findall(js) assert len(calls) == 1, ( f"{name}: must CALL bindSharedHeaderControls() exactly once " f"as a module-top statement (found {len(calls)})" ) # doc-edit.js ships NO header controls (no nav / auth pair / steering # panel — its own header comment says the page carries none), so it # calls nothing: there is nothing to bind. doc_edit = _text(ASSETS / "doc-edit.js") assert "bindSharedHeaderControls" not in doc_edit, ( "doc-edit.js ships no header controls — no binding init call" )