fix(gateway): make Telegram DM topic config writes atomic
This commit is contained in:
parent
4f24db4258
commit
4c50b4689e
@ -11,6 +11,7 @@ import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
import html as _html
|
||||
import re
|
||||
from typing import Dict, List, Optional, Any
|
||||
@ -534,8 +535,23 @@ class TelegramAdapter(BasePlatformAdapter):
|
||||
break
|
||||
|
||||
if changed:
|
||||
with open(config_path, "w") as f:
|
||||
_yaml.dump(config, f, default_flow_style=False, sort_keys=False)
|
||||
fd, tmp_path = tempfile.mkstemp(
|
||||
dir=str(config_path.parent),
|
||||
suffix=".tmp",
|
||||
prefix=".config_",
|
||||
)
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
||||
_yaml.dump(config, f, default_flow_style=False, sort_keys=False)
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
os.replace(tmp_path, config_path)
|
||||
except BaseException:
|
||||
try:
|
||||
os.unlink(tmp_path)
|
||||
except OSError:
|
||||
pass
|
||||
raise
|
||||
logger.info(
|
||||
"[%s] Persisted thread_id=%s for topic '%s' in config.yaml",
|
||||
self.name, thread_id, topic_name,
|
||||
|
||||
@ -283,6 +283,48 @@ def test_persist_dm_topic_thread_id_skips_if_already_set(tmp_path):
|
||||
# ── _get_dm_topic_info ──
|
||||
|
||||
|
||||
def test_persist_dm_topic_thread_id_preserves_config_on_write_failure(tmp_path):
|
||||
"""Failed writes should leave the original config.yaml intact."""
|
||||
import yaml
|
||||
|
||||
config_data = {
|
||||
"platforms": {
|
||||
"telegram": {
|
||||
"extra": {
|
||||
"dm_topics": [
|
||||
{
|
||||
"chat_id": 111,
|
||||
"topics": [
|
||||
{"name": "General", "icon_color": 123},
|
||||
],
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
config_file = tmp_path / ".hermes" / "config.yaml"
|
||||
config_file.parent.mkdir(parents=True)
|
||||
original_text = yaml.dump(config_data)
|
||||
config_file.write_text(original_text, encoding="utf-8")
|
||||
|
||||
adapter = _make_adapter()
|
||||
|
||||
def fail_dump(*args, **kwargs):
|
||||
raise RuntimeError("boom")
|
||||
|
||||
with patch.object(Path, "home", return_value=tmp_path), \
|
||||
patch.dict(os.environ, {"HERMES_HOME": str(tmp_path / ".hermes")}), \
|
||||
patch("yaml.dump", side_effect=fail_dump):
|
||||
adapter._persist_dm_topic_thread_id(111, "General", 999)
|
||||
|
||||
assert config_file.read_text(encoding="utf-8") == original_text
|
||||
result = yaml.safe_load(config_file.read_text(encoding="utf-8"))
|
||||
topics = result["platforms"]["telegram"]["extra"]["dm_topics"][0]["topics"]
|
||||
assert "thread_id" not in topics[0]
|
||||
|
||||
|
||||
def test_get_dm_topic_info_finds_cached_topic():
|
||||
"""Should return topic config when thread_id is in cache."""
|
||||
adapter = _make_adapter([
|
||||
|
||||
Loading…
Reference in New Issue
Block a user