fix(discord): retry slash sync after failed fingerprint

This commit is contained in:
Gille 2026-07-29 12:06:42 -06:00 committed by Teknium
parent b84389c625
commit 6d292fd5eb
2 changed files with 85 additions and 9 deletions

View file

@ -1773,18 +1773,24 @@ class DiscordAdapter(BasePlatformAdapter):
if retry_after_until > now:
remaining = max(1, int(retry_after_until - now))
return f"Discord asked us to wait before syncing slash commands; retry in {remaining}s"
if entry.get("fingerprint") == fingerprint and entry.get("last_success_at"):
last_success_at = float(entry.get("last_success_at") or 0)
last_attempt_at = float(entry.get("last_attempt_at") or 0)
if (
entry.get("fingerprint") == fingerprint
and last_success_at
and last_success_at >= last_attempt_at
):
return "same slash-command fingerprint already synced"
return None
def _record_command_sync_attempt(self, app_id: Any, fingerprint: str) -> None:
state = self._read_command_sync_state()
state[self._command_sync_state_key(app_id)] = {
**(
state.get(self._command_sync_state_key(app_id))
if isinstance(state.get(self._command_sync_state_key(app_id)), dict)
else {}
),
key = self._command_sync_state_key(app_id)
entry = state.get(key) if isinstance(state.get(key), dict) else {}
entry.pop("last_success_at", None)
entry.pop("summary", None)
state[key] = {
**entry,
"fingerprint": fingerprint,
"last_attempt_at": time.time(),
}

View file

@ -754,6 +754,72 @@ async def test_post_connect_initialization_skips_same_fingerprint_after_success(
fake_http.upsert_global_command.assert_awaited_once()
@pytest.mark.asyncio
async def test_post_connect_initialization_retries_fingerprint_after_timeout(tmp_path, monkeypatch):
adapter = DiscordAdapter(PlatformConfig(enabled=True, token="test-token"))
monkeypatch.setattr("hermes_constants.get_hermes_home", lambda: tmp_path)
class _DesiredCommand:
def to_dict(self, tree):
return {
"name": "skill",
"description": "Run a skill",
"type": 1,
"options": [],
}
adapter._client = SimpleNamespace(
tree=SimpleNamespace(get_commands=lambda: [_DesiredCommand()]),
application_id=999,
user=SimpleNamespace(id=999),
)
desired_fingerprint = adapter._desired_command_sync_fingerprint()
state_path = (
tmp_path
/ discord_platform._DISCORD_COMMAND_SYNC_STATE_SUBDIR
/ discord_platform._DISCORD_COMMAND_SYNC_STATE_FILENAME
)
state_path.parent.mkdir(parents=True)
state_path.write_text(
json.dumps(
{
"999": {
"fingerprint": desired_fingerprint,
"last_attempt_at": 102.0,
"last_success_at": 101.0,
"summary": {"total": 1},
}
}
),
encoding="utf-8",
)
summary = {
"total": 1,
"unchanged": 0,
"updated": 0,
"recreated": 0,
"created": 1,
"deleted": 0,
}
sync = AsyncMock(side_effect=[asyncio.TimeoutError(), summary])
monkeypatch.setattr(adapter, "_safe_sync_slash_commands", sync)
await adapter._run_post_connect_initialization()
timed_out_entry = json.loads(state_path.read_text(encoding="utf-8"))["999"]
assert timed_out_entry["fingerprint"] == desired_fingerprint
assert "last_success_at" not in timed_out_entry
assert "summary" not in timed_out_entry
await adapter._run_post_connect_initialization()
assert sync.await_count == 2
recovered_entry = json.loads(state_path.read_text(encoding="utf-8"))["999"]
assert recovered_entry["last_success_at"] >= recovered_entry["last_attempt_at"]
assert recovered_entry["summary"] == summary
@pytest.mark.asyncio
async def test_post_connect_initialization_respects_discord_retry_after(tmp_path, monkeypatch):
adapter = DiscordAdapter(PlatformConfig(enabled=True, token="test-token"))
@ -788,7 +854,7 @@ async def test_post_connect_initialization_respects_discord_retry_after(tmp_path
/ discord_platform._DISCORD_COMMAND_SYNC_STATE_SUBDIR
/ discord_platform._DISCORD_COMMAND_SYNC_STATE_FILENAME
)
state = json.loads(state_path.read_text())
state = json.loads(state_path.read_text(encoding="utf-8"))
entry = state["999"]
assert entry["retry_after"] == 123.0
assert entry["retry_after_until"] > entry["last_attempt_at"]
@ -828,7 +894,11 @@ async def test_post_connect_initialization_reraises_non_rate_limit_exceptions(tm
/ discord_platform._DISCORD_COMMAND_SYNC_STATE_SUBDIR
/ discord_platform._DISCORD_COMMAND_SYNC_STATE_FILENAME
)
state = json.loads(state_path.read_text()) if state_path.exists() else {}
state = (
json.loads(state_path.read_text(encoding="utf-8"))
if state_path.exists()
else {}
)
entry = state.get("4242", {})
# Attempt was recorded before the sync call, but no rate-limit cooldown
# should have been persisted from the unrelated exception.