mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 06:51:16 +08:00
feat(update): snapshot pairing data before git pull (#16383)
Quick state snapshot now includes pairing JSONs (generic + legacy + Feishu comment pairing), and `hermes update` takes a pre-update snapshot labeled `pre-update` before pulling. Pairing data lives outside state.db in platform-specific JSONs under ~/.hermes/pairing/, ~/.hermes/platforms/pairing/, and ~/.hermes/feishu_comment_pairing.json. The update command already couldn't touch $HERMES_HOME, but #15733 reports lost pairing after an update — this gives users something to restore from via `/snapshot list` / `/snapshot restore <id>` if anything clobbers the approved-user lists. - Extend _QUICK_STATE_FILES with pairing paths (files + dirs) - Snapshot walks directories recursively and records each file in the manifest individually so restore logic is unchanged - _cmd_update_impl calls create_quick_snapshot(label='pre-update') after 'Found N new commits' and before 'Pulling updates' - Snapshot failures are logged at debug and never block the update Refs #15733.
This commit is contained in:
@@ -1141,3 +1141,80 @@ class TestQuickSnapshot:
|
||||
deleted = prune_quick_snapshots(keep=3, hermes_home=hermes_home)
|
||||
assert deleted == 7
|
||||
assert len(list_quick_snapshots(hermes_home=hermes_home)) == 3
|
||||
|
||||
def test_snapshot_includes_pairing_directories(self, hermes_home):
|
||||
"""Pairing JSONs live outside state.db — snapshot must capture them
|
||||
recursively (generic + per-platform) so approved-user lists survive
|
||||
disasters like #15733."""
|
||||
from hermes_cli.backup import create_quick_snapshot
|
||||
|
||||
# Generic pairing store (new location)
|
||||
(hermes_home / "platforms" / "pairing").mkdir(parents=True)
|
||||
(hermes_home / "platforms" / "pairing" / "telegram-approved.json").write_text(
|
||||
'{"12345": {"user_name": "alice"}}'
|
||||
)
|
||||
(hermes_home / "platforms" / "pairing" / "discord-approved.json").write_text(
|
||||
'{"67890": {"user_name": "bob"}}'
|
||||
)
|
||||
# Legacy pairing store (old location)
|
||||
(hermes_home / "pairing").mkdir()
|
||||
(hermes_home / "pairing" / "matrix-approved.json").write_text(
|
||||
'{"@charlie:server": {"user_name": "charlie"}}'
|
||||
)
|
||||
# Feishu's separate JSON
|
||||
(hermes_home / "feishu_comment_pairing.json").write_text(
|
||||
'{"doc_abc": {"allow_from": ["user_xyz"]}}'
|
||||
)
|
||||
|
||||
snap_id = create_quick_snapshot(hermes_home=hermes_home)
|
||||
assert snap_id is not None
|
||||
|
||||
snap_dir = hermes_home / "state-snapshots" / snap_id
|
||||
assert (snap_dir / "platforms" / "pairing" / "telegram-approved.json").exists()
|
||||
assert (snap_dir / "platforms" / "pairing" / "discord-approved.json").exists()
|
||||
assert (snap_dir / "pairing" / "matrix-approved.json").exists()
|
||||
assert (snap_dir / "feishu_comment_pairing.json").exists()
|
||||
|
||||
with open(snap_dir / "manifest.json") as f:
|
||||
meta = json.load(f)
|
||||
files = meta["files"]
|
||||
assert "platforms/pairing/telegram-approved.json" in files
|
||||
assert "platforms/pairing/discord-approved.json" in files
|
||||
assert "pairing/matrix-approved.json" in files
|
||||
assert "feishu_comment_pairing.json" in files
|
||||
|
||||
def test_restore_recovers_pairing_data(self, hermes_home):
|
||||
"""After restore, deleted pairing files reappear with original content."""
|
||||
from hermes_cli.backup import create_quick_snapshot, restore_quick_snapshot
|
||||
|
||||
pairing_dir = hermes_home / "platforms" / "pairing"
|
||||
pairing_dir.mkdir(parents=True)
|
||||
approved = pairing_dir / "telegram-approved.json"
|
||||
approved.write_text('{"12345": {"user_name": "alice"}}')
|
||||
feishu = hermes_home / "feishu_comment_pairing.json"
|
||||
feishu.write_text('{"doc_abc": {"allow_from": ["user_xyz"]}}')
|
||||
|
||||
snap_id = create_quick_snapshot(hermes_home=hermes_home)
|
||||
assert snap_id is not None
|
||||
|
||||
# Simulate the disaster — user loses both pairing files.
|
||||
approved.unlink()
|
||||
feishu.unlink()
|
||||
assert not approved.exists()
|
||||
assert not feishu.exists()
|
||||
|
||||
assert restore_quick_snapshot(snap_id, hermes_home=hermes_home) is True
|
||||
assert approved.exists()
|
||||
assert '"alice"' in approved.read_text()
|
||||
assert feishu.exists()
|
||||
assert '"user_xyz"' in feishu.read_text()
|
||||
|
||||
def test_empty_pairing_dir_does_not_fail(self, hermes_home):
|
||||
"""An empty pairing directory should be silently skipped."""
|
||||
from hermes_cli.backup import create_quick_snapshot
|
||||
|
||||
(hermes_home / "platforms" / "pairing").mkdir(parents=True)
|
||||
# Directory exists but contains no files.
|
||||
snap_id = create_quick_snapshot(hermes_home=hermes_home)
|
||||
# Other state still present → snapshot succeeds.
|
||||
assert snap_id is not None
|
||||
|
||||
Reference in New Issue
Block a user