""" Making sure the browser runs the build we deployed. A fix can be written, tested, deployed, and served, and still not be what runs. `Cache-Control: no-cache` requires a browser to revalidate — but it only binds one that asks, and a browser that cached the SPA *before* that header existed applies heuristic freshness instead: a fraction of the file's age, which for a file dated weeks ago is days. It does not ask, so it never learns. That happened here. A phone ran a player without the read-ahead bound and filled the browser's buffer ceiling at 106 MB — the exact symptom the bound was written to remove — for an hour after the bounded player went live. Two sessions were spent looking at the node. So the URL now carries a fingerprint of what is being served, and the whole module graph lives under it: `/a//app.js` importing `./i18n.js` resolves to `/a//i18n.js`. A URL that changes with the content cannot serve yesterday's build, and cannot serve half of each. """ import re import pytest from fastapi.testclient import TestClient from meshbay_hub.api.webapp import ASSET_V, STATIC_DIR, _asset_version from meshbay_hub.app import create_app @pytest.fixture(scope="module") def client(): return TestClient(create_app()) def _shell_refs(html: str) -> list[str]: return re.findall(r'(?:src|href)="([^"]+)"', html) def test_every_asset_the_shell_loads_is_versioned(client): refs = _shell_refs(client.get("/").text) assert refs, "the shell references nothing at all" stale = [u for u in refs if not u.startswith(f"/a/{ASSET_V}/")] assert not stale, ( f"served from an unversioned URL, so a stale cache can win: {stale}") def test_the_module_graph_resolves_under_the_same_prefix(client): """The point of a path prefix rather than a query string. `import './i18n.js'` from a versioned app.js resolves against the versioned directory. A query string on app.js alone would not: its imports would fall back to the bare paths, and the graph could be assembled from two builds — which does not render a stale page, it fails to link at all. """ for path in ("app.js", "i18n.js", "locales/fr.js", "transport.js", "vendor/argon2.min.js"): r = client.get(f"/a/{ASSET_V}/{path}") assert r.status_code == 200, f"{path} is not served under the version" def test_versioned_assets_may_be_cached_hard(client): """Which is the trade the fingerprint buys.""" cc = client.get(f"/a/{ASSET_V}/app.js").headers.get("cache-control", "") assert "immutable" in cc and "max-age=31536000" in cc, ( f"versioned assets are served as {cc!r} — the whole point of a URL that " "changes with the content is that it need never be revalidated") def test_the_unversioned_path_still_revalidates(client): """It still answers — old bookmarks, and sw.js has to live there.""" r = client.get("/app.js") assert r.status_code == 200 assert r.headers.get("cache-control") == "no-cache" def test_the_service_worker_is_not_versioned(client): """Its scope is its own path. Served from /a//sw.js it would only control /a//, and the pages whose downloads it exists to intercept are not under there. """ assert client.get("/sw.js").status_code == 200 assert "/sw.js" not in _shell_refs(client.get("/").text) def test_the_fingerprint_follows_the_content(tmp_path, monkeypatch): """Otherwise it is decoration. Pinning this to the file contents rather than to the package version is deliberate: a redeploy without a version bump is the common case during a debugging session, and that is exactly when a stale player costs the most. """ before = _asset_version() target = STATIC_DIR / "app.js" original = target.read_bytes() try: target.write_bytes(original + b"\n// touched\n") assert _asset_version() != before, ( "the fingerprint did not move when app.js did, so a redeploy " "serves the new file at the old URL") finally: target.write_bytes(original) assert _asset_version() == before, "the fingerprint is not reproducible" @pytest.mark.parametrize("relative", ["locales/en.js", "vendor/htm-preact.js", "style.css"]) def test_a_change_anywhere_under_the_prefix_moves_the_fingerprint(relative): """ Everything under the static directory is served at `/a//` and cached as immutable, so everything has to feed the hash — subdirectories included. The fingerprint used to cover a hand-kept list of top-level modules, and the check that guarded the list globbed `*.js` at the top level only. The catalogues and `vendor/` were on neither: a change confined to the English catalogue shipped at the URL a phone already held, and it went on showing a heading that had been rewritten and deployed. Pull-to-refresh fetches the shell, which is `no-store` and was current; it does not refetch an immutable file whose URL has not moved. """ before = _asset_version() target = STATIC_DIR / relative original = target.read_bytes() try: target.write_bytes(original + b"\n/* touched */\n") assert _asset_version() != before, ( f"the fingerprint did not move when {relative} did, so a browser " "that cached it keeps the old copy for a year") finally: target.write_bytes(original) assert _asset_version() == before def test_a_new_file_moves_the_fingerprint(): """A rename or an addition changes what is served even when no existing file's bytes do.""" before = _asset_version() extra = STATIC_DIR / "locales" / "zz-test-only.js" try: extra.write_text("export default {};\n") assert _asset_version() != before finally: extra.unlink() assert _asset_version() == before