fix(sw-cache): version style.css link so old SW cannot return stale CSS (#1507)
Container restart / in-place upgrade left the previous service worker still controlling open tabs. Its fetch handler intercepted 'static/style.css', matched the unversioned URL exactly against its old shell cache, and returned the OLD CSS — while the JS files (which already carry ?v=__WEBUI_VERSION__) hit the cache as misses and loaded fresh from network. New JS + old CSS broke the layout until a force refresh bypassed the SW. Fix is a 1-line attribute change plus aligning the SW pre-cache list: * static/index.html: add ?v=__WEBUI_VERSION__ to the style.css link, matching the pattern already in use for every JS file in the page. * static/sw.js: add the same ?v=__CACHE_VERSION__ suffix to every versioned entry in SHELL_ASSETS so that pre-cache URLs match what the page actually requests. Unversioned entries (root, manifest, favicons) stay unversioned. Tests: * New regression test_index_versions_stylesheet (lock the href) and test_sw_shell_assets_match_versioned_asset_urls in test_pwa_manifest_sw.py. * test_workspace_panel_preload_marker_restored_in_head in test_sprint37.py loosened to match the css link prefix (preserves the ordering invariant). Verified live on port 8789: served HTML carries 'static/style.css?v=v0.50.275-dirty' and SW SHELL_ASSETS receive the matching VQ at request time. Closes #1507.
This commit is contained in:
1 parent
52226bcdd7
commit
4fea813adc
4 files changed
+84
-15
No files matched your search
+1
-1
@@ -18,7 +18,7 @@
|
||||
<script>(function(){var themes={light:1,dark:1,system:1},skins={default:1,ares:1,mono:1,slate:1,poseidon:1,sisyphus:1,charizard:1,sienna:1},legacy={slate:['dark','slate'],solarized:['dark','poseidon'],monokai:['dark','sisyphus'],nord:['dark','slate'],oled:['dark','default']},t=(localStorage.getItem('hermes-theme')||'dark').toLowerCase(),s=(localStorage.getItem('hermes-skin')||'').toLowerCase(),m=legacy[t],theme=m?m[0]:(themes[t]?t:'dark'),skin=skins[s]?s:(m?m[1]:'default');localStorage.setItem('hermes-theme',theme);localStorage.setItem('hermes-skin',skin);if(theme==='system')theme=window.matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light';if(theme==='dark')document.documentElement.classList.add('dark');if(skin!=='default')document.documentElement.dataset.skin=skin;})()</script>
|
||||
<script>(function(){var fs=localStorage.getItem('hermes-font-size');if(fs&&fs!=='default')document.documentElement.dataset.fontSize=fs;})()</script>
|
||||
<script>(function(){try{document.documentElement.dataset.workspacePanel=localStorage.getItem('hermes-webui-workspace-panel')==='open'?'open':'closed';}catch(e){document.documentElement.dataset.workspacePanel='closed';}})()</script>
|
||||
<link rel="stylesheet" href="static/style.css">
|
||||
<link rel="stylesheet" href="static/style.css?v=__WEBUI_VERSION__">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css" integrity="sha384-LJcOxlx9IMbNXDqJ2axpfEQKkAYbFjJfhXexLfiRJhjDU81mzgkiQq8rkV0j6dVh" crossorigin="anonymous">
|
||||
<!-- KaTeX math rendering CSS (loaded eagerly to prevent layout shift) -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.css" integrity="sha384-5TcZemv2l/9On385z///+d7MSYlvIEw9FuZTIdZ14vJLqWphw7e7ZPuOiCHJcFCP" crossorigin="anonymous">
|
||||
|
||||
+22
-13
@@ -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',
|
||||
|
||||
@@ -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 `<link rel=stylesheet>` 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")')
|
||||
|
||||
@@ -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 = '<link rel="stylesheet" href="static/style.css">'
|
||||
# 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 = '<link rel="stylesheet" href="static/style.css'
|
||||
marker_pos = HTML.find(marker)
|
||||
css_pos = HTML.find(css_link)
|
||||
assert marker_pos >= 0, "index.html must preload documentElement.dataset.workspacePanel from localStorage"
|
||||
|
||||
Reference in new issue
Block a user