diff --git a/static/index.html b/static/index.html index 08ebb423..b972611f 100644 --- a/static/index.html +++ b/static/index.html @@ -18,7 +18,7 @@ - + diff --git a/static/sw.js b/static/sw.js index 58fa2c46..9e43db66 100644 --- a/static/sw.js +++ b/static/sw.js @@ -9,21 +9,30 @@ // Bumps automatically whenever the git commit changes — no manual edits needed. const CACHE_NAME = 'hermes-shell-__CACHE_VERSION__'; -// Static assets that form the app shell +// Static assets that form the app shell. +// +// Versioned assets (CSS + JS) include `?v=__CACHE_VERSION__` to match the +// query string the page sends — see index.html. Without the version query +// here, every cache lookup against `?v=...` URLs would miss and fall through +// to network, defeating the pre-cache. +// +// Unversioned assets (`./`, manifest.json, favicons) are referenced from +// index.html without a cache-bust query, so they stay unversioned here too. +const VQ = '?v=__CACHE_VERSION__'; const SHELL_ASSETS = [ './', - './static/style.css', - './static/boot.js', - './static/ui.js', - './static/messages.js', - './static/sessions.js', - './static/panels.js', - './static/commands.js', - './static/icons.js', - './static/i18n.js', - './static/workspace.js', - './static/terminal.js', - './static/onboarding.js', + './static/style.css' + VQ, + './static/boot.js' + VQ, + './static/ui.js' + VQ, + './static/messages.js' + VQ, + './static/sessions.js' + VQ, + './static/panels.js' + VQ, + './static/commands.js' + VQ, + './static/icons.js' + VQ, + './static/i18n.js' + VQ, + './static/workspace.js' + VQ, + './static/terminal.js' + VQ, + './static/onboarding.js' + VQ, './static/favicon.svg', './static/favicon-32.png', './manifest.json', diff --git a/tests/test_pwa_manifest_sw.py b/tests/test_pwa_manifest_sw.py index f2d0e5db..40220dec 100644 --- a/tests/test_pwa_manifest_sw.py +++ b/tests/test_pwa_manifest_sw.py @@ -160,6 +160,63 @@ class TestIndexHtmlIntegration: assert "sw.js?v=__WEBUI_VERSION__" in src assert "static/ui.js?v=__WEBUI_VERSION__" in src + def test_index_versions_stylesheet(self): + """Regression for #1507: the `` for style.css MUST + carry the same `?v=__WEBUI_VERSION__` cache-bust query as the JS files. + + Without the version, a stale service worker controlling a tab across a + version upgrade would intercept `static/style.css`, find an exact URL + match in its old shell cache, and return OLD CSS — while the new JS + URLs (which DO carry `?v=`) miss the cache and load fresh. The mismatch + breaks the layout until a force-refresh bypasses the SW. + """ + src = INDEX.read_text(encoding="utf-8") + assert "static/style.css?v=__WEBUI_VERSION__" in src, ( + "static/style.css must carry ?v=__WEBUI_VERSION__ so an old service " + "worker controlling the tab across a version upgrade does not return " + "stale CSS — see #1507" + ) + # And the unversioned form must NOT appear (defensive — catches accidental + # reverts that leave both lines). + assert 'href="static/style.css"' not in src, ( + "unversioned static/style.css link found — must include " + "?v=__WEBUI_VERSION__ for cache busting" + ) + + def test_sw_shell_assets_match_versioned_asset_urls(self): + """The service worker's SHELL_ASSETS pre-cache list must use the same + `?v=__CACHE_VERSION__` suffix on JS+CSS that index.html sends, so that + the pre-cached entries actually serve when the page requests them. + + Without this, every `cache.match()` for a versioned asset URL (e.g. + `static/style.css?v=vN`) would miss against the unversioned pre-cached + entry (`static/style.css`), defeating the pre-cache. + """ + src = SW.read_text(encoding="utf-8") + # Versioned shell assets must include the cache version query. + for asset in ( + "style.css", + "boot.js", + "ui.js", + "messages.js", + "sessions.js", + "panels.js", + "commands.js", + "icons.js", + "i18n.js", + "workspace.js", + "terminal.js", + "onboarding.js", + ): + # Either inline `?v=__CACHE_VERSION__` or via the VQ constant + # produces a URL string the cache lookup can match. + has_inline = f"{asset}?v=__CACHE_VERSION__" in src + has_concat = f"{asset}' + VQ" in src or f"{asset}\" + VQ" in src + assert has_inline or has_concat, ( + f"sw.js SHELL_ASSETS entry for {asset} must carry " + "?v=__CACHE_VERSION__ to match the URL the page requests" + ) + def test_index_route_url_encodes_asset_version(self): src = ROUTES.read_text(encoding="utf-8") idx = src.find('parsed.path in ("/", "/index.html")') diff --git a/tests/test_sprint37.py b/tests/test_sprint37.py index cf8c3de8..b16a253d 100644 --- a/tests/test_sprint37.py +++ b/tests/test_sprint37.py @@ -94,7 +94,10 @@ def test_workspace_panel_restore_before_sync(): def test_workspace_panel_preload_marker_restored_in_head(): """index.html must preload the workspace panel state before the main stylesheet paints.""" marker = "document.documentElement.dataset.workspacePanel" - css_link = '' + # The stylesheet href carries `?v=__WEBUI_VERSION__` for SW cache busting (#1507). + # We just need the link's start position; the version query doesn't change the + # ordering invariant this test enforces. + css_link = '