Drop the three cases that pin trivia the invariant tests already cover (idempotence is exercised by the routing-store test; the empty-name / no-store guard and the default-profile refusal are one-line argument checks). Salvage bar: invariants only.
124 lines
4.4 KiB
Python
124 lines
4.4 KiB
Python
"""Profile identity purge for `hermes profile delete`: the delete-only path, and what an unserve
|
|
must leave alone.
|
|
|
|
`_unserve_profile()` runs for every name that leaves the served set — a rename's old name leaves it
|
|
exactly like a deleted one (its directory is gone either way) and the rename's rekey still needs that
|
|
identity — so the purge lives behind the delete-only ``purge-profile-identity`` control verb and
|
|
never in the unserve path (#111926, delete side).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import time
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
|
|
def _make_store(tmp_path):
|
|
from gateway.config import GatewayConfig
|
|
from gateway.session import SessionStore
|
|
sessions_dir = tmp_path / "sessions"
|
|
sessions_dir.mkdir(exist_ok=True)
|
|
store = SessionStore(
|
|
sessions_dir,
|
|
GatewayConfig(sessions_dir=sessions_dir, write_sessions_json=False,
|
|
multiplex_profiles=True),
|
|
)
|
|
store._ensure_loaded()
|
|
return store
|
|
|
|
|
|
def _entry(session_key, chat_id, profile):
|
|
from gateway.session import SessionEntry, SessionSource, Platform
|
|
from gateway.session_lifecycle import _now
|
|
now = _now()
|
|
return SessionEntry(
|
|
session_key=session_key, session_id=f"sid-{chat_id}",
|
|
platform=Platform.FEISHU, chat_type="dm", created_at=now, updated_at=now,
|
|
origin=SessionSource(platform=Platform.FEISHU, chat_id=chat_id, profile=profile),
|
|
)
|
|
|
|
|
|
def _state_db():
|
|
from hermes_constants import get_hermes_home
|
|
from hermes_state import SessionDB
|
|
return SessionDB(Path(get_hermes_home()) / "state.db")
|
|
|
|
|
|
def _runner_stub(store):
|
|
return SimpleNamespace(
|
|
session_store=store,
|
|
_profile_failed_platforms={},
|
|
_profile_adapters={},
|
|
pairing_stores={},
|
|
_busy_text_modes_by_profile={},
|
|
_busy_input_modes_by_profile={},
|
|
_served_profile_homes={},
|
|
_served_profile_signatures={},
|
|
_agent_cache={},
|
|
_evict_cached_agent=lambda key: None,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unserve_profile_keeps_identity_for_a_rename_to_rekey(tmp_path):
|
|
"""Unserving is not deleting: the purge must not run here.
|
|
|
|
A rename's old name leaves the served set exactly like a deleted one, and
|
|
``migrate-profile-identity`` still has to find that identity to rekey it. A purge inside
|
|
``_unserve_profile()`` would delete it first and break the rename it runs beside.
|
|
"""
|
|
from gateway.run_profile_reconcile import GatewayProfileReconcileMixin
|
|
store = _make_store(tmp_path)
|
|
with store._lock:
|
|
store._entries["agent:oldname:feishu:dm:chatA"] = _entry(
|
|
"agent:oldname:feishu:dm:chatA", "chatA", "oldname")
|
|
db = _state_db()
|
|
db.register_backend_heartbeat(
|
|
backend_id="be1", pid=1, started_at=time.time(), profile="oldname", host="h")
|
|
db.close()
|
|
|
|
home = tmp_path / "home"
|
|
home.mkdir()
|
|
await GatewayProfileReconcileMixin._unserve_profile(_runner_stub(store), "oldname", home)
|
|
|
|
assert "agent:oldname:feishu:dm:chatA" in store._entries
|
|
db = _state_db()
|
|
try:
|
|
assert db._read_one(
|
|
"SELECT COUNT(*) AS n FROM gateway_heartbeats WHERE profile = ?",
|
|
("oldname",))["n"] == 1
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def test_purge_verb_drops_routing_identity_and_reports_ok(tmp_path):
|
|
"""The delete-only verb settles identity in the process that owns the routing index."""
|
|
from gateway.run_profile_reconcile import purge_profile_identity_verb
|
|
store = _make_store(tmp_path)
|
|
with store._lock:
|
|
store._entries["agent:gone:feishu:dm:chatA"] = _entry(
|
|
"agent:gone:feishu:dm:chatA", "chatA", "gone")
|
|
store._entries["agent:keepme:feishu:dm:chatB"] = _entry(
|
|
"agent:keepme:feishu:dm:chatB", "chatB", "keepme")
|
|
db = _state_db()
|
|
db.register_backend_heartbeat(
|
|
backend_id="be1", pid=1, started_at=time.time(), profile="gone", host="h")
|
|
db.close()
|
|
|
|
answer = purge_profile_identity_verb(_runner_stub(store))({"name": "gone"})
|
|
|
|
assert answer["ok"] is True
|
|
assert answer["dropped"] == 1
|
|
assert "agent:gone:feishu:dm:chatA" not in store._entries
|
|
assert "agent:keepme:feishu:dm:chatB" in store._entries
|
|
db = _state_db()
|
|
try:
|
|
assert db._read_one(
|
|
"SELECT COUNT(*) AS n FROM gateway_heartbeats WHERE profile = ?", ("gone",))["n"] == 0
|
|
finally:
|
|
db.close()
|