mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-22 16:25:58 +00:00
test(cli): cover the web UI build flock contention paths
PR #63455 shipped the flock without tests. Cover the three contention paths: contended+dist serves stale without a second build, uncontended builds under the lock (creating the lock file), and contended+no-dist blocks then skips the rebuild because the staleness re-check runs under the lock and sees the winner's stamp. Also pin the lock filename into .gitignore via a regression assertion.
This commit is contained in:
parent
cca2276115
commit
9adb5a368f
1 changed files with 77 additions and 0 deletions
|
|
@ -486,3 +486,80 @@ class TestBuildWebUIRetryAndStaleFallback:
|
|||
assert "Web UI build failed" in out
|
||||
assert "vite ENOMEM" in out
|
||||
assert "Run manually" in out
|
||||
|
||||
|
||||
class TestBuildWebUIFlock:
|
||||
"""Cross-process build serialization (salvaged from PR #63455).
|
||||
|
||||
One process builds under an exclusive flock on <root>/.web_ui_build.lock;
|
||||
contenders either serve the existing (possibly stale) dist or, when no
|
||||
dist exists yet, block until the builder finishes. The staleness walk
|
||||
itself runs inside _do_build_web_ui, i.e. under the lock, so a process
|
||||
that queued behind a successful build skips the rebuild.
|
||||
"""
|
||||
|
||||
def test_contended_lock_with_dist_serves_stale_without_building(self, tmp_path):
|
||||
import fcntl
|
||||
from hermes_cli.main import _build_web_ui as build
|
||||
|
||||
web_dir, dist_dir = _make_web_dir(tmp_path)
|
||||
_touch(dist_dir / "index.html")
|
||||
|
||||
lock_path = tmp_path / ".web_ui_build.lock"
|
||||
holder = open(lock_path, "a")
|
||||
try:
|
||||
fcntl.flock(holder.fileno(), fcntl.LOCK_EX)
|
||||
with patch("hermes_cli.main._do_build_web_ui") as mock_do:
|
||||
result = build(web_dir)
|
||||
finally:
|
||||
holder.close()
|
||||
|
||||
assert result is True
|
||||
mock_do.assert_not_called() # served existing dist, no second build
|
||||
|
||||
def test_uncontended_lock_builds_and_creates_lock_file(self, tmp_path):
|
||||
from hermes_cli.main import _build_web_ui as build
|
||||
|
||||
web_dir, _ = _make_web_dir(tmp_path)
|
||||
with patch("hermes_cli.main._do_build_web_ui", return_value=True) as mock_do:
|
||||
result = build(web_dir)
|
||||
|
||||
assert result is True
|
||||
mock_do.assert_called_once()
|
||||
assert (tmp_path / ".web_ui_build.lock").exists()
|
||||
|
||||
def test_contended_lock_without_dist_waits_then_skips_fresh_build(self, tmp_path):
|
||||
"""First-ever build race: the waiter blocks, and once it acquires the
|
||||
lock the callee's own staleness check (running under the lock) sees
|
||||
the winner's output and skips a duplicate build."""
|
||||
import fcntl
|
||||
import threading
|
||||
from hermes_cli.main import _build_web_ui as build
|
||||
|
||||
web_dir, dist_dir = _make_web_dir(tmp_path)
|
||||
# No dist yet — contender must take the blocking-wait path.
|
||||
lock_path = tmp_path / ".web_ui_build.lock"
|
||||
holder = open(lock_path, "a")
|
||||
fcntl.flock(holder.fileno(), fcntl.LOCK_EX)
|
||||
|
||||
def release_after_building():
|
||||
# Simulate the winning process finishing its build.
|
||||
_touch(dist_dir / ".vite" / "manifest.json")
|
||||
_write_web_ui_build_stamp(tmp_path, web_dir)
|
||||
holder.close() # releases the flock
|
||||
|
||||
t = threading.Timer(0.2, release_after_building)
|
||||
t.start()
|
||||
try:
|
||||
with patch("hermes_cli.main.shutil.which", return_value="/usr/bin/npm"), \
|
||||
patch("hermes_cli.main.subprocess.run") as mock_run:
|
||||
result = build(web_dir)
|
||||
finally:
|
||||
t.join()
|
||||
|
||||
assert result is True
|
||||
mock_run.assert_not_called() # fresh after the wait -> no rebuild
|
||||
|
||||
def test_lock_file_is_gitignored(self):
|
||||
gitignore = Path(__file__).resolve().parents[2] / ".gitignore"
|
||||
assert ".web_ui_build.lock" in gitignore.read_text(encoding="utf-8")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue