From 44657ff6282ebb5ded0b8fa9fa0aff7e95bbf5ff Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Wed, 16 Sep 2026 12:31:12 -0700 Subject: [PATCH] fix(achievements): apply the sticky-unlock floor to in-flight snapshots too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _compute_from_scan(is_partial=True) evaluated against an empty unlock set, and _run_background_scan publishes those partials to _SNAPSHOT_CACHE every progress_every sessions. So for the duration of every background rescan an already-earned badge rendered as locked and unlocked_count collapsed then climbed back — the exact 39<->40 flicker from #112273 that the finished-scan floor alone did not remove. Read state.json for partials as well (the floor applies); partials still never record new unlocks or save_state, since an unlock time from half a scan could be invalidated by a later session. Also: - tests/plugins fake SessionDB accepts include_compacted, which the scanner now passes (this was the red 'Python tests' check). - engine tests patch get_hermes_home so _data_file's legacy migration cannot copy the developer's real state.json into the temp dir. Part of #112273 --- plugins/hermes-achievements/dashboard/plugin_api.py | 6 ++++-- .../tests/test_achievement_engine.py | 10 +++++++--- tests/plugins/test_achievements_plugin.py | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/plugins/hermes-achievements/dashboard/plugin_api.py b/plugins/hermes-achievements/dashboard/plugin_api.py index a4891c0a36..92e0d7decd 100644 --- a/plugins/hermes-achievements/dashboard/plugin_api.py +++ b/plugins/hermes-achievements/dashboard/plugin_api.py @@ -711,9 +711,11 @@ def _compute_from_scan(scan: Dict[str, Any], *, is_partial: bool = False) -> Dic """Evaluate every achievement definition against a scan result. Used by ``compute_all`` for finished scans AND by the background progress callback for in-flight snapshots; ``is_partial=True`` skips persisting ``state.json`` unlocks — an "unlock time" from - half a scan could be invalidated by a later session.""" + half a scan could be invalidated by a later session. Persisted unlocks are still read + for partials: the background scan publishes them to the cache, so an earned badge would + otherwise render as locked for the whole rescan (#112273).""" aggregate = scan.get("aggregate", {}) - state = load_state() if not is_partial else {"unlocks": {}} + state = load_state() unlocks = state.setdefault("unlocks", {}) now = int(time.time()) evaluated = [] diff --git a/plugins/hermes-achievements/tests/test_achievement_engine.py b/plugins/hermes-achievements/tests/test_achievement_engine.py index 4458059592..fa20eda0c1 100644 --- a/plugins/hermes-achievements/tests/test_achievement_engine.py +++ b/plugins/hermes-achievements/tests/test_achievement_engine.py @@ -67,15 +67,19 @@ class AchievementEngineTests(unittest.TestCase): "threshold_metric": "total_terminal_calls", "tiers": [{"name": "Copper", "threshold": 40}], } - with TemporaryDirectory() as data_dir, patch.object(plugin_api, "ACHIEVEMENTS", [definition]), patch.object(plugin_api, "_data_dir", return_value=Path(data_dir)): + with TemporaryDirectory() as data_dir, patch.object(plugin_api, "ACHIEVEMENTS", [definition]), patch.object(plugin_api, "_data_dir", return_value=Path(data_dir)), patch.object(plugin_api, "get_hermes_home", return_value=Path(data_dir)): unlocked = plugin_api._compute_from_scan({"aggregate": {"total_terminal_calls": 40}, "sessions": []}) rescanned = plugin_api._compute_from_scan({"aggregate": {"total_terminal_calls": 39}, "sessions": []}) partial = plugin_api._compute_from_scan({"aggregate": {"total_terminal_calls": 39}, "sessions": []}, is_partial=True) + persisted = plugin_api.load_state()["unlocks"] self.assertTrue(unlocked["achievements"][0]["unlocked"]) self.assertTrue(rescanned["achievements"][0]["unlocked"]) self.assertEqual(rescanned["achievements"][0]["state"], "unlocked") - self.assertFalse(partial["achievements"][0]["unlocked"]) + # In-flight snapshots are published to the cache during rescans: the floor applies there too. + self.assertTrue(partial["achievements"][0]["unlocked"]) + self.assertEqual(partial["unlocked_count"], 1) + self.assertEqual(list(persisted), ["durable_unlock"]) def test_secret_achievement_stays_hidden_without_progress(self): definition = { @@ -197,7 +201,7 @@ class CompactionScanTests(unittest.TestCase): import hermes_state from hermes_state import SessionDB - with TemporaryDirectory() as tmp, patch.object(plugin_api, "_data_dir", return_value=Path(tmp) / "data"): + with TemporaryDirectory() as tmp, patch.object(plugin_api, "_data_dir", return_value=Path(tmp) / "data"), patch.object(plugin_api, "get_hermes_home", return_value=Path(tmp)): db = SessionDB(Path(tmp) / "state.db") try: db.create_session("s1", "cli", model="m") diff --git a/tests/plugins/test_achievements_plugin.py b/tests/plugins/test_achievements_plugin.py index 0e5fac9e3e..df2a3101ba 100644 --- a/tests/plugins/test_achievements_plugin.py +++ b/tests/plugins/test_achievements_plugin.py @@ -100,7 +100,7 @@ class _FakeSessionDB: for i in range(effective) ] - def get_messages(self, session_id: str) -> List[Dict[str, Any]]: + def get_messages(self, session_id: str, include_compacted: bool = False) -> List[Dict[str, Any]]: self.messages_calls += 1 return [ {"role": "user", "content": f"ask {session_id}"},