From 000e1dc9b62b3372fdc4701f720ffa89376864c7 Mon Sep 17 00:00:00 2001 From: ai-ag2026 <261867348+ai-ag2026@users.noreply.github.com> Date: Sat, 30 May 2026 11:25:33 +0200 Subject: [PATCH] fix: hash dirty WebUI dev builds --- CHANGELOG.md | 4 +++ api/updates.py | 4 +++ tests/test_version_badge.py | 61 +++++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8ec9465..6989bdaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ ## [Unreleased] +### Fixed + +- Dev-build cache-busting now includes a short hash of the tracked dirty diff, so local asset URLs change on each edit instead of staying at a constant `-dirty` suffix (#3159). + ## [v0.51.171] — 2026-05-30 — Release EQ (stage-batch53 — tool-output card badge + Neon opt-in skin) ### Added diff --git a/api/updates.py b/api/updates.py index 86c1d738..4269e199 100644 --- a/api/updates.py +++ b/api/updates.py @@ -195,6 +195,10 @@ def _dirty_suffix(path: Path, timeout=1) -> str: # the dirty signal; real errors (timeouts, missing git) carry a different # diagnostic and correctly suppress the suffix. if not out or out.startswith('git exited with status '): + diff, diff_ok = _run_git(['diff', '--binary', 'HEAD', '--'], path, timeout=timeout) + if diff_ok and diff: + digest = hashlib.sha1(diff.encode('utf-8', errors='replace')).hexdigest()[:8] + return f"-dirty-{digest}" return "-dirty" return "" diff --git a/tests/test_version_badge.py b/tests/test_version_badge.py index be53d5b5..27112bd5 100644 --- a/tests/test_version_badge.py +++ b/tests/test_version_badge.py @@ -11,6 +11,7 @@ Covers: 7. server.py: server_version is not the old hardcoded string """ import importlib +import subprocess import sys import types from pathlib import Path @@ -117,6 +118,66 @@ class TestDetectWebUIVersion: assert result == 'v0.50.123-dirty' assert calls[1][0][:2] == ['diff-index', '--quiet'] + def test_dirty_check_hashes_tracked_diff_when_available(self, tmp_path): + """Dirty dev-build asset URLs should change on each tracked diff edit.""" + import hashlib + + diff = "diff --git a/static/ui.js b/static/ui.js\n+console.log('dev edit')\n" + expected = hashlib.sha1(diff.encode('utf-8', errors='replace')).hexdigest()[:8] + calls = [] + + def fake_run_git(args, cwd, timeout=10): + calls.append((args, timeout)) + if args[:3] == ['describe', '--tags', '--always']: + return ('v0.50.123', True) + if args[:2] == ['diff-index', '--quiet']: + return ('git exited with status 1', False) + if args[:3] == ['diff', '--binary', 'HEAD']: + return (diff, True) + return ('unexpected', False) + + result = self._fresh_detect(mock_run_git=fake_run_git, tmp_path=tmp_path) + assert result == f'v0.50.123-dirty-{expected}' + assert calls[2][0][:3] == ['diff', '--binary', 'HEAD'] + + def test_dirty_check_falls_back_when_diff_hash_unavailable(self, tmp_path): + """If the diff read fails, keep the old best-effort -dirty suffix.""" + def fake_run_git(args, cwd, timeout=10): + if args[:3] == ['describe', '--tags', '--always']: + return ('v0.50.123', True) + if args[:2] == ['diff-index', '--quiet']: + return ('git exited with status 1', False) + if args[:3] == ['diff', '--binary', 'HEAD']: + return ('git diff timed out after 1s', False) + return ('unexpected', False) + + result = self._fresh_detect(mock_run_git=fake_run_git, tmp_path=tmp_path) + assert result == 'v0.50.123-dirty' + + def test_dirty_suffix_changes_when_tracked_diff_changes(self, tmp_path): + """Real git diff hashing should produce a new suffix for each tracked edit.""" + import api.updates as upd + + def git(*args): + subprocess.run(['git', *args], cwd=tmp_path, check=True, capture_output=True, text=True) + + git('init') + git('config', 'user.name', 'Test User') + git('config', 'user.email', 'test@example.invalid') + tracked = tmp_path / 'tracked.txt' + tracked.write_text('base\n', encoding='utf-8') + git('add', 'tracked.txt') + git('commit', '-m', 'base') + + tracked.write_text('edit one\n', encoding='utf-8') + first = upd._dirty_suffix(tmp_path) + tracked.write_text('edit two\n', encoding='utf-8') + second = upd._dirty_suffix(tmp_path) + + assert first.startswith('-dirty-') + assert second.startswith('-dirty-') + assert first != second + def test_dirty_check_timeout_does_not_hide_base_version(self, tmp_path): """If dirty detection times out, keep the base version instead of unknown.""" def fake_run_git(args, cwd, timeout=10):