494 lines
17 KiB
Python
494 lines
17 KiB
Python
|
|
"""Adversarial contracts for chat-only continuity and truthful rendering."""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import importlib.util
|
||
|
|
import json
|
||
|
|
import runpy
|
||
|
|
import sqlite3
|
||
|
|
import sys
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
import yaml
|
||
|
|
|
||
|
|
|
||
|
|
ROOT = Path(__file__).parents[2]
|
||
|
|
HERMES = ROOT / "services" / "hermes"
|
||
|
|
|
||
|
|
|
||
|
|
def _load(name: str, path: Path):
|
||
|
|
spec = importlib.util.spec_from_file_location(name, path)
|
||
|
|
assert spec and spec.loader
|
||
|
|
module = importlib.util.module_from_spec(spec)
|
||
|
|
spec.loader.exec_module(module)
|
||
|
|
return module
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def continuity():
|
||
|
|
return _load(
|
||
|
|
"telegram_continuity",
|
||
|
|
HERMES / "scripts" / "migrate_telegram_api_sessions.py",
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def test_context_is_deterministic_bounded_deduplicated_and_truthful(continuity):
|
||
|
|
turns = []
|
||
|
|
for index in range(30):
|
||
|
|
user = {"role": "user", "content": f"question {index}", "_db_persisted": True}
|
||
|
|
assistant = {
|
||
|
|
"role": "assistant",
|
||
|
|
"content": f"answer {index}",
|
||
|
|
"_db_persisted": True,
|
||
|
|
}
|
||
|
|
turns.extend((user, assistant))
|
||
|
|
if index == 4:
|
||
|
|
# Simulate one complete transcript replay.
|
||
|
|
turns.extend((user.copy(), assistant.copy()))
|
||
|
|
turns.extend(
|
||
|
|
[
|
||
|
|
{
|
||
|
|
"role": "assistant",
|
||
|
|
"content": "",
|
||
|
|
"tool_calls": [
|
||
|
|
{
|
||
|
|
"id": "call-1",
|
||
|
|
"type": "function",
|
||
|
|
"function": {
|
||
|
|
"name": "browser",
|
||
|
|
"arguments": "x" * 40_000,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"role": "tool",
|
||
|
|
"tool_call_id": "call-1",
|
||
|
|
"tool_name": "browser",
|
||
|
|
"content": "tool output " * 4_000,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"role": "user",
|
||
|
|
"content": [
|
||
|
|
{"type": "input_text", "text": "edit this"},
|
||
|
|
{
|
||
|
|
"type": "input_image",
|
||
|
|
"image_url": "data:image/png;base64," + "a" * 80_000,
|
||
|
|
},
|
||
|
|
],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"role": "assistant",
|
||
|
|
"content": "Created it MEDIA:/opt/data/cache/images/private.png",
|
||
|
|
},
|
||
|
|
]
|
||
|
|
)
|
||
|
|
|
||
|
|
compacted = continuity.compact_telegram_history(turns)
|
||
|
|
|
||
|
|
assert compacted == continuity.compact_telegram_history(turns)
|
||
|
|
assert compacted[0]["role"] == "system"
|
||
|
|
assert compacted[0]["_db_persisted"] is True
|
||
|
|
assert compacted[0]["content"].startswith(continuity.SUMMARY_PREFIX)
|
||
|
|
assert sum(item["role"] == "user" for item in compacted[1:]) <= 12
|
||
|
|
assert len(compacted[1:]) <= continuity.MAX_HISTORY_ITEMS
|
||
|
|
assert len(json.dumps(compacted).encode()) <= (
|
||
|
|
continuity.MAX_HISTORY_BYTES + continuity.MAX_SUMMARY_CHARS
|
||
|
|
)
|
||
|
|
encoded = json.dumps(compacted)
|
||
|
|
assert "data:image" not in encoded
|
||
|
|
assert "MEDIA:" not in encoded
|
||
|
|
assert "/opt/data/" not in encoded
|
||
|
|
assert continuity.ATTACHMENT_NOTE in encoded
|
||
|
|
assert "question 29" in encoded
|
||
|
|
assert (
|
||
|
|
next(item for item in compacted if item.get("content") == "question 29")[
|
||
|
|
"_db_persisted"
|
||
|
|
]
|
||
|
|
is True
|
||
|
|
)
|
||
|
|
assert "x" * 3_000 not in encoded
|
||
|
|
fingerprints = [continuity._fingerprint(item) for item in compacted]
|
||
|
|
assert all(
|
||
|
|
left != right
|
||
|
|
for left, right in zip(fingerprints, fingerprints[1:], strict=False)
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def test_context_rejects_malformed_history_and_bounds_single_large_item(continuity):
|
||
|
|
assert continuity.compact_telegram_history(None) == []
|
||
|
|
assert continuity.compact_telegram_history({"role": "user"}) == []
|
||
|
|
malformed = [
|
||
|
|
None,
|
||
|
|
"text",
|
||
|
|
{"role": "unknown", "content": "skip"},
|
||
|
|
{"role": "tool", "content": ""},
|
||
|
|
{"role": "user", "content": {"nested": "value"}},
|
||
|
|
{"role": "assistant", "content": "z" * 2_000_000},
|
||
|
|
]
|
||
|
|
compacted = continuity.compact_telegram_history(malformed)
|
||
|
|
assert len(compacted) == 2
|
||
|
|
assert compacted[0]["content"] == '{"nested": "value"}'
|
||
|
|
assert len(compacted[1]["content"]) == continuity.MAX_ITEM_CHARS
|
||
|
|
|
||
|
|
|
||
|
|
def test_context_defensive_normalization_and_replay_edges(continuity, monkeypatch):
|
||
|
|
assert continuity._text(["skip", {"type": "text", "text": "keep"}]) == "keep"
|
||
|
|
assert continuity._text({"not-json-serializable"}) == "{'not-json-serializable'}"
|
||
|
|
normalized = continuity._normalized_item(
|
||
|
|
{
|
||
|
|
"role": "assistant",
|
||
|
|
"tool_calls": [
|
||
|
|
None,
|
||
|
|
{"id": "call", "function": {"name": "browser", "arguments": {1}}},
|
||
|
|
],
|
||
|
|
}
|
||
|
|
)
|
||
|
|
assert normalized and normalized["tool_calls"][0]["id"] == "call"
|
||
|
|
assert continuity._fingerprint({"role": "user", "content": {1}}).startswith(
|
||
|
|
"{'role':"
|
||
|
|
)
|
||
|
|
|
||
|
|
user = {"role": "user", "content": "same"}
|
||
|
|
assistant = {"role": "assistant", "content": "reply"}
|
||
|
|
assert continuity._deduplicate(
|
||
|
|
[user, assistant, user.copy(), assistant.copy()]
|
||
|
|
) == [
|
||
|
|
user,
|
||
|
|
assistant,
|
||
|
|
]
|
||
|
|
assert continuity._deduplicate(
|
||
|
|
[
|
||
|
|
{"role": "tool", "content": "first", "id": "stable"},
|
||
|
|
{"role": "tool", "content": "replay", "id": "stable"},
|
||
|
|
]
|
||
|
|
) == [{"role": "tool", "content": "first", "id": "stable"}]
|
||
|
|
|
||
|
|
summary = continuity._summary(
|
||
|
|
[
|
||
|
|
{
|
||
|
|
"role": "system",
|
||
|
|
"content": continuity.SUMMARY_PREFIX + "\n- existing fact",
|
||
|
|
},
|
||
|
|
{"role": "tool", "content": "not durable prose"},
|
||
|
|
]
|
||
|
|
)
|
||
|
|
assert summary and "existing fact" in summary["content"]
|
||
|
|
monkeypatch.setattr(
|
||
|
|
continuity, "MAX_SUMMARY_CHARS", len(continuity.SUMMARY_PREFIX) + 1
|
||
|
|
)
|
||
|
|
assert (
|
||
|
|
continuity._summary([{"role": "user", "content": "too long"}])["content"]
|
||
|
|
== continuity.SUMMARY_PREFIX
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def test_context_final_serialized_size_guard(continuity, monkeypatch):
|
||
|
|
monkeypatch.setattr(continuity, "MAX_HISTORY_ITEMS", 2)
|
||
|
|
monkeypatch.setattr(continuity, "MAX_HISTORY_BYTES", 1_000)
|
||
|
|
monkeypatch.setattr(
|
||
|
|
continuity,
|
||
|
|
"_summary",
|
||
|
|
lambda _older: {
|
||
|
|
"role": "system",
|
||
|
|
"content": "s" * 20_000,
|
||
|
|
"_db_persisted": True,
|
||
|
|
},
|
||
|
|
)
|
||
|
|
compacted = continuity.compact_telegram_history(
|
||
|
|
[
|
||
|
|
{"role": "user", "content": "older"},
|
||
|
|
{"role": "assistant", "content": "recent one"},
|
||
|
|
{"role": "assistant", "content": "recent two"},
|
||
|
|
]
|
||
|
|
)
|
||
|
|
assert [item["content"] for item in compacted[1:]] == ["recent two"]
|
||
|
|
|
||
|
|
|
||
|
|
def _create_session_databases(tmp_path: Path):
|
||
|
|
state = tmp_path / "state.db"
|
||
|
|
responses = tmp_path / "response_store.db"
|
||
|
|
with sqlite3.connect(state) as connection:
|
||
|
|
connection.execute(
|
||
|
|
"""CREATE TABLE sessions (
|
||
|
|
id TEXT PRIMARY KEY, source TEXT, session_key TEXT, chat_type TEXT,
|
||
|
|
display_name TEXT, origin_json TEXT, title TEXT
|
||
|
|
)"""
|
||
|
|
)
|
||
|
|
connection.executemany(
|
||
|
|
"INSERT INTO sessions VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||
|
|
(
|
||
|
|
(
|
||
|
|
"legacy",
|
||
|
|
"api_server",
|
||
|
|
"wrong",
|
||
|
|
"group",
|
||
|
|
"Unassigned",
|
||
|
|
"{}",
|
||
|
|
"Unassigned",
|
||
|
|
),
|
||
|
|
("named", "api_server", None, None, None, None, "Keep this title"),
|
||
|
|
("other", "cli", None, None, "Unassigned", None, None),
|
||
|
|
),
|
||
|
|
)
|
||
|
|
connection.execute(
|
||
|
|
"CREATE TABLE messages (id INTEGER PRIMARY KEY, session_id TEXT, role TEXT, content TEXT, timestamp REAL)"
|
||
|
|
)
|
||
|
|
connection.executemany(
|
||
|
|
"INSERT INTO messages VALUES (?, 'legacy', 'user', 'same', 1)", ((1,), (2,))
|
||
|
|
)
|
||
|
|
with sqlite3.connect(responses) as connection:
|
||
|
|
connection.execute(
|
||
|
|
"CREATE TABLE conversations (name TEXT PRIMARY KEY, response_id TEXT NOT NULL)"
|
||
|
|
)
|
||
|
|
connection.execute(
|
||
|
|
"CREATE TABLE responses (response_id TEXT PRIMARY KEY, data TEXT NOT NULL, accessed_at REAL NOT NULL)"
|
||
|
|
)
|
||
|
|
connection.executemany(
|
||
|
|
"INSERT INTO responses VALUES (?, ?, 0)",
|
||
|
|
(
|
||
|
|
("general", json.dumps({"session_id": "legacy"})),
|
||
|
|
("topic", json.dumps({"session_id": "named"})),
|
||
|
|
("bad-json", "{"),
|
||
|
|
("bad-id", json.dumps({"session_id": "../unsafe"})),
|
||
|
|
(
|
||
|
|
"oversized",
|
||
|
|
json.dumps({"session_id": "other", "padding": "x" * 500}),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
)
|
||
|
|
connection.executemany(
|
||
|
|
"INSERT INTO conversations VALUES (?, ?)",
|
||
|
|
(
|
||
|
|
("telegram", "general"),
|
||
|
|
("telegram-topic-plans-g1", "topic"),
|
||
|
|
("telegram-topic-malformed", "bad-json"),
|
||
|
|
("telegram-topic-unsafe", "bad-id"),
|
||
|
|
("telegram-topic-oversized", "oversized"),
|
||
|
|
),
|
||
|
|
)
|
||
|
|
return state, responses
|
||
|
|
|
||
|
|
|
||
|
|
def test_existing_session_migration_repairs_labels_and_is_idempotent(
|
||
|
|
tmp_path: Path, continuity, monkeypatch
|
||
|
|
):
|
||
|
|
state, responses = _create_session_databases(tmp_path)
|
||
|
|
monkeypatch.setattr(continuity, "MAX_RESPONSE_BYTES", 300)
|
||
|
|
|
||
|
|
assert continuity.migrate(state, responses) == 2
|
||
|
|
assert continuity.migrate(state, responses) == 0
|
||
|
|
|
||
|
|
with sqlite3.connect(state) as connection:
|
||
|
|
rows = {
|
||
|
|
row[0]: row[1:]
|
||
|
|
for row in connection.execute(
|
||
|
|
"SELECT id, session_key, chat_type, display_name, origin_json, title "
|
||
|
|
"FROM sessions ORDER BY id"
|
||
|
|
)
|
||
|
|
}
|
||
|
|
assert rows["legacy"][:3] == ("telegram", "private", "Telegram")
|
||
|
|
assert json.loads(rows["legacy"][3]) == {
|
||
|
|
"platform": "telegram",
|
||
|
|
"session_key": "telegram",
|
||
|
|
}
|
||
|
|
assert rows["legacy"][4] == "Telegram · General"
|
||
|
|
assert rows["named"][:3] == (
|
||
|
|
"telegram-topic-plans-g1",
|
||
|
|
"private",
|
||
|
|
"Telegram",
|
||
|
|
)
|
||
|
|
assert rows["named"][4] == "Keep this title"
|
||
|
|
assert rows["other"] == (None, None, "Unassigned", None, None)
|
||
|
|
with sqlite3.connect(state) as connection:
|
||
|
|
assert connection.execute("SELECT COUNT(*) FROM messages").fetchone() == (1,)
|
||
|
|
|
||
|
|
|
||
|
|
def test_migration_fails_closed_on_missing_or_malformed_databases(
|
||
|
|
tmp_path: Path, continuity
|
||
|
|
):
|
||
|
|
assert continuity.telegram_sessions(tmp_path / "missing.db") == {}
|
||
|
|
assert (
|
||
|
|
continuity.migrate(tmp_path / "missing-state.db", tmp_path / "missing.db") == 0
|
||
|
|
)
|
||
|
|
broken = tmp_path / "broken.db"
|
||
|
|
broken.write_text("not sqlite", encoding="utf-8")
|
||
|
|
assert continuity.telegram_sessions(broken) == {}
|
||
|
|
|
||
|
|
state = tmp_path / "state.db"
|
||
|
|
responses = tmp_path / "responses.db"
|
||
|
|
with sqlite3.connect(state) as connection:
|
||
|
|
connection.execute("CREATE TABLE sessions (id TEXT PRIMARY KEY)")
|
||
|
|
with sqlite3.connect(responses) as connection:
|
||
|
|
connection.execute("CREATE TABLE conversations (name TEXT, response_id TEXT)")
|
||
|
|
connection.execute("CREATE TABLE responses (response_id TEXT, data TEXT)")
|
||
|
|
connection.execute(
|
||
|
|
"INSERT INTO responses VALUES ('valid', '{\"session_id\": \"known\"}')"
|
||
|
|
)
|
||
|
|
connection.execute("INSERT INTO conversations VALUES ('telegram', 'valid')")
|
||
|
|
assert continuity.migrate(state, responses) == 0
|
||
|
|
|
||
|
|
|
||
|
|
def test_migration_skips_invalid_conversation_and_rolls_back_database_errors(
|
||
|
|
tmp_path: Path, continuity
|
||
|
|
):
|
||
|
|
state = tmp_path / "state.db"
|
||
|
|
responses = tmp_path / "responses.db"
|
||
|
|
with sqlite3.connect(state) as connection:
|
||
|
|
connection.execute(
|
||
|
|
"""CREATE TABLE sessions (
|
||
|
|
id TEXT PRIMARY KEY, source TEXT, session_key TEXT, chat_type TEXT,
|
||
|
|
display_name TEXT, origin_json TEXT, title TEXT
|
||
|
|
)"""
|
||
|
|
)
|
||
|
|
connection.execute(
|
||
|
|
"INSERT INTO sessions VALUES ('known', 'api_server', NULL, NULL, NULL, NULL, NULL)"
|
||
|
|
)
|
||
|
|
connection.execute(
|
||
|
|
"""CREATE TRIGGER reject_updates BEFORE UPDATE ON sessions
|
||
|
|
BEGIN SELECT RAISE(FAIL, 'read only for test'); END"""
|
||
|
|
)
|
||
|
|
with sqlite3.connect(responses) as connection:
|
||
|
|
connection.execute(
|
||
|
|
"CREATE TABLE conversations (name TEXT PRIMARY KEY, response_id TEXT NOT NULL)"
|
||
|
|
)
|
||
|
|
connection.execute(
|
||
|
|
"CREATE TABLE responses (response_id TEXT PRIMARY KEY, data TEXT NOT NULL)"
|
||
|
|
)
|
||
|
|
connection.executemany(
|
||
|
|
"INSERT INTO responses VALUES (?, ?)",
|
||
|
|
(
|
||
|
|
("valid", json.dumps({"session_id": "known"})),
|
||
|
|
("invalid", json.dumps({"session_id": "ignored"})),
|
||
|
|
),
|
||
|
|
)
|
||
|
|
connection.executemany(
|
||
|
|
"INSERT INTO conversations VALUES (?, ?)",
|
||
|
|
(("telegram", "valid"), ("telegram-topic-BAD", "invalid")),
|
||
|
|
)
|
||
|
|
|
||
|
|
assert continuity.telegram_sessions(responses) == {"known": "telegram"}
|
||
|
|
assert continuity.migrate(state, responses) == 0
|
||
|
|
with sqlite3.connect(state) as connection:
|
||
|
|
assert connection.execute(
|
||
|
|
"SELECT session_key FROM sessions WHERE id = 'known'"
|
||
|
|
).fetchone() == (None,)
|
||
|
|
|
||
|
|
|
||
|
|
def test_api_patch_compacts_input_batch_and_disconnect_snapshots(tmp_path: Path):
|
||
|
|
patcher = _load(
|
||
|
|
"chat_api_patch",
|
||
|
|
HERMES / "scripts" / "patch_api_server_sessions.py",
|
||
|
|
)
|
||
|
|
source = tmp_path / "api_server.py"
|
||
|
|
destination = tmp_path / "patched.py"
|
||
|
|
source.write_text(
|
||
|
|
patcher.BEFORE
|
||
|
|
+ patcher.RUNS_BEFORE
|
||
|
|
+ patcher.RUN_CLOSE_BEFORE
|
||
|
|
+ patcher.TRUNCATION_BEFORE
|
||
|
|
+ patcher.RESPONSES_SESSION_BEFORE
|
||
|
|
+ patcher.EVENT_CALLBACK_SIGNATURE_BEFORE
|
||
|
|
+ patcher.EVENT_CALLBACK_BODY_BEFORE
|
||
|
|
+ patcher.EVENT_CALLBACK_END_BEFORE
|
||
|
|
+ patcher.EVENT_CALLBACK_CALL_BEFORE
|
||
|
|
+ patcher.RUN_SWEEP_BEFORE
|
||
|
|
+ patcher.SSE_STORE_BEFORE
|
||
|
|
+ patcher.HISTORY_STORE_BEFORE,
|
||
|
|
encoding="utf-8",
|
||
|
|
)
|
||
|
|
|
||
|
|
patcher.patch(source, destination)
|
||
|
|
patched = destination.read_text(encoding="utf-8")
|
||
|
|
|
||
|
|
assert (
|
||
|
|
"conversation_history = compact_telegram_history(conversation_history)"
|
||
|
|
in patched
|
||
|
|
)
|
||
|
|
assert "conversation_history_snapshot = compact_telegram_history(" in patched
|
||
|
|
assert "full_history = compact_telegram_history(full_history)" in patched
|
||
|
|
assert patched.count("from gateway.platforms.telegram_continuity import") == 2
|
||
|
|
|
||
|
|
|
||
|
|
def _patch_source(patcher) -> str:
|
||
|
|
return (
|
||
|
|
patcher.BEFORE
|
||
|
|
+ patcher.RUNS_BEFORE
|
||
|
|
+ patcher.RUN_CLOSE_BEFORE
|
||
|
|
+ patcher.RESPONSES_SESSION_BEFORE
|
||
|
|
+ patcher.EVENT_CALLBACK_SIGNATURE_BEFORE
|
||
|
|
+ patcher.EVENT_CALLBACK_BODY_BEFORE
|
||
|
|
+ patcher.EVENT_CALLBACK_END_BEFORE
|
||
|
|
+ patcher.EVENT_CALLBACK_CALL_BEFORE
|
||
|
|
+ patcher.RUN_SWEEP_BEFORE
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.parametrize(
|
||
|
|
"marker_name",
|
||
|
|
[
|
||
|
|
"BEFORE",
|
||
|
|
"RUNS_BEFORE",
|
||
|
|
"RUN_CLOSE_BEFORE",
|
||
|
|
"RESPONSES_SESSION_BEFORE",
|
||
|
|
"EVENT_CALLBACK_SIGNATURE_BEFORE",
|
||
|
|
],
|
||
|
|
)
|
||
|
|
def test_api_patch_fails_closed_on_upstream_drift(tmp_path: Path, marker_name: str):
|
||
|
|
patcher = _load(
|
||
|
|
"chat_api_patch_drift",
|
||
|
|
HERMES / "scripts" / "patch_api_server_sessions.py",
|
||
|
|
)
|
||
|
|
source = tmp_path / "api_server.py"
|
||
|
|
marker = getattr(patcher, marker_name)
|
||
|
|
source.write_text(_patch_source(patcher).replace(marker, "", 1), encoding="utf-8")
|
||
|
|
with pytest.raises(RuntimeError, match="patch context changed"):
|
||
|
|
patcher.patch(source, tmp_path / "patched.py")
|
||
|
|
|
||
|
|
|
||
|
|
def test_script_entrypoints(tmp_path: Path, monkeypatch):
|
||
|
|
continuity_path = HERMES / "scripts" / "migrate_telegram_api_sessions.py"
|
||
|
|
monkeypatch.setattr(
|
||
|
|
sys,
|
||
|
|
"argv",
|
||
|
|
[
|
||
|
|
str(continuity_path),
|
||
|
|
str(tmp_path / "state.db"),
|
||
|
|
str(tmp_path / "responses.db"),
|
||
|
|
],
|
||
|
|
)
|
||
|
|
with pytest.raises(SystemExit, match="0"):
|
||
|
|
runpy.run_path(str(continuity_path), run_name="__main__")
|
||
|
|
|
||
|
|
patch_path = HERMES / "scripts" / "patch_api_server_sessions.py"
|
||
|
|
patcher = _load("chat_api_patch_cli", patch_path)
|
||
|
|
source = tmp_path / "api_server.py"
|
||
|
|
destination = tmp_path / "patched.py"
|
||
|
|
source.write_text(_patch_source(patcher), encoding="utf-8")
|
||
|
|
monkeypatch.setattr(sys, "argv", [str(patch_path), str(source), str(destination)])
|
||
|
|
with pytest.raises(SystemExit, match="0"):
|
||
|
|
runpy.run_path(str(patch_path), run_name="__main__")
|
||
|
|
assert destination.exists()
|
||
|
|
|
||
|
|
|
||
|
|
def test_chat_mounts_continuity_only_into_isolated_tenants():
|
||
|
|
statefulset = yaml.safe_load((HERMES / "chat-statefulset.yaml").read_text())
|
||
|
|
pod = statefulset["spec"]["template"]["spec"]
|
||
|
|
hermes = next(item for item in pod["containers"] if item["name"] == "hermes")
|
||
|
|
assert {
|
||
|
|
"name": "coordinator",
|
||
|
|
"mountPath": "/opt/hermes/gateway/platforms/telegram_continuity.py",
|
||
|
|
"subPath": "migrate_telegram_api_sessions.py",
|
||
|
|
"readOnly": True,
|
||
|
|
} in hermes["volumeMounts"]
|
||
|
|
patch_init = next(
|
||
|
|
item
|
||
|
|
for item in pod["initContainers"]
|
||
|
|
if item["name"] == "patch-api-server-sessions"
|
||
|
|
)
|
||
|
|
command = patch_init["args"][0]
|
||
|
|
assert command.count("grep -Fq") == 3
|
||
|
|
assert "compact_telegram_history" in command
|
||
|
|
assert "agent-deployment" not in (HERMES / "chat-statefulset.yaml").read_text()
|