tests: dash-chat hermetic suite (7/7) — лента БД портала, секрет команд, anchor-дедуп, коллапс повторов
This commit is contained in:
parent
5c02bc7cdd
commit
b9d7fd9d23
1 changed files with 160 additions and 0 deletions
160
tests/test_dash_chat.py
Normal file
160
tests/test_dash_chat.py
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""t-261001100740-99d55e: чат Simbiot в дашборде (hermetic, без сети/портала).
|
||||
|
||||
Запуск: ./venv/bin/python tests/test_dash_chat.py
|
||||
|
||||
Проверяет: build_chat собирает ленту из БД портала (agent_portal_chat) +
|
||||
реплаев стора, команды канала (/set-tg-bot, cmd-settg-*) скрыты (секрет),
|
||||
пара «вопрос + ответ» по anchor-дедупу (реплай стора vs push-строка БД,
|
||||
«reply-<ms>-<id>»), подряд одинаковые agent-пуши схлопываются (repeat ≥ 2),
|
||||
chat_send пишет sender='user' с consumed=0 и валидным msg_id (dash-*),
|
||||
пустой текст отбрасывается.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
# изоляция: подменяем пути ДО импорта dashboard
|
||||
_TMP = tempfile.mkdtemp(prefix="simbiot-chat-test-")
|
||||
_COOP_DB = os.path.join(_TMP, "coop.db")
|
||||
|
||||
|
||||
def _setup_coop_db():
|
||||
db = sqlite3.connect(_COOP_DB)
|
||||
db.execute("""CREATE TABLE agent_portal_chat (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
agent_id INTEGER NOT NULL,
|
||||
msg_id TEXT UNIQUE,
|
||||
sender TEXT NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
ts INTEGER NOT NULL,
|
||||
consumed INTEGER DEFAULT 0)""")
|
||||
db.execute("CREATE TABLE agents (id INTEGER PRIMARY KEY, agent_name TEXT, "
|
||||
"harness_type TEXT)")
|
||||
db.execute("INSERT INTO agents (id, agent_name, harness_type) VALUES (80, 'Simbiot', 'simbiot')")
|
||||
db.commit()
|
||||
db.close()
|
||||
|
||||
|
||||
def _row(msg_id, sender, text, ts, consumed=0):
|
||||
return (msg_id, sender, text, ts, consumed)
|
||||
|
||||
|
||||
class DashChatTest(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
_setup_coop_db()
|
||||
import harness.dashboard as dash
|
||||
dash._COOP_DB = _COOP_DB
|
||||
dash._COOP_AGENT_CACHE.update({"id": 80, "name": "Simbiot", "ts": time.time() + 600})
|
||||
cls.dash = dash
|
||||
|
||||
def _empty_store(self):
|
||||
return {"inbox": {}}
|
||||
|
||||
# ── лента: БД портала ──
|
||||
def test_feed_reads_portal_db(self):
|
||||
d = self._seed([_row("p1", "user", "привет", 1_000),
|
||||
_row("r1", "agent", "ответ", 2_000)])
|
||||
# hermetic: реальный стор мастер-харнеса не участвует
|
||||
with mock.patch.object(self.dash.es, "run_projections",
|
||||
return_value={"inbox": {}}):
|
||||
out = d.build_chat(limit=10)
|
||||
self.assertTrue(out["ok"])
|
||||
self.assertEqual(out["agent"], "Simbiot")
|
||||
self.assertEqual([(m["from"], m["text"]) for m in out["messages"]],
|
||||
[("user", "привет"), ("agent", "ответ")])
|
||||
self.assertFalse(out["messages"][0]["pending"]) # consumed=0, но старше окна
|
||||
|
||||
# ── секрет: команды канала не попадают в ленту ──
|
||||
def test_command_secret_hidden(self):
|
||||
self._seed([_row("cmd-settg-80-1-abc", "user", "/set-tg-bot 7245:SECRET", 3_000, consumed=1),
|
||||
_row("settg-1790932676601", "agent", "✅ Telegram подключён", 4_000),
|
||||
_row("p1", "user", "обычное сообщение", 5_000)])
|
||||
out = self.dash.build_chat(limit=10)
|
||||
texts = " || ".join(m["text"] for m in out["messages"])
|
||||
self.assertNotIn("SECRET", texts)
|
||||
self.assertIn("обычное сообщение", texts)
|
||||
self.assertIn("Telegram подключён", texts) # агентский отчёт виден
|
||||
|
||||
# ── anchor-дедуп: реплай стора не дублирует push-строку БД ──
|
||||
def test_reply_anchor_dedup(self):
|
||||
self._seed([_row("p1", "user", "вопрос к агенту", 6_000, consumed=0),
|
||||
_row("reply-1790937000000-p1", "agent", "ответ агента", 6_500)])
|
||||
store = {"inbox": {"m-p1": {
|
||||
"update_id": "p1", "chat_id": "portal:Simbiot", "text": "вопрос к агенту",
|
||||
"replied": True, "reply_text": "ответ агента", "ts": "2026-10-02T21:50:00+00:00"}}}
|
||||
with mock.patch.object(self.dash.es, "run_projections", return_value=store):
|
||||
out = self.dash.build_chat(limit=10)
|
||||
msgs = out["messages"]
|
||||
pairs = [(m["from"], m["text"]) for m in msgs]
|
||||
# ровно одна пара вопрос+ответ: push-строка БД и реплай стора не задваивают
|
||||
self.assertEqual(pairs.count(("agent", "ответ агента")), 1, "задвоили ответ (push vs стор)")
|
||||
self.assertEqual(pairs.count(("user", "вопрос к агенту")), 1)
|
||||
|
||||
# ── реплай стора, пуш которого не доехал, — пара видна ──
|
||||
def test_unpushed_store_reply_shown(self):
|
||||
self._seed([_row("p2", "user", "вопрос два", 7_000, consumed=0)])
|
||||
store = {"inbox": {"m-p2": {
|
||||
"update_id": "p2", "chat_id": "portal:Simbiot", "text": "вопрос два",
|
||||
"replied": True, "reply_text": "ответ два (не ушёл push)", "ts": "2026-10-02T21:55:00+00:00"}}}
|
||||
with mock.patch.object(self.dash.es, "run_projections", return_value=store):
|
||||
out = self.dash.build_chat(limit=10)
|
||||
got = {(m["from"], m["text"]) for m in out["messages"]}
|
||||
self.assertIn(("agent", "ответ два (не ушёл push)"), got)
|
||||
self.assertIn(("user", "вопрос два"), got)
|
||||
|
||||
# ── коллапс подряд одинаковых agent-пушей ──
|
||||
def test_repeat_collapse(self):
|
||||
self._seed([_row("s1", "agent", "одно и то же", 8_000),
|
||||
_row("s2", "agent", "одно и то же", 9_000),
|
||||
_row("s3", "agent", "одно и то же", 10_000),
|
||||
_row("s4", "agent", "другое", 11_000)])
|
||||
out = self.dash.build_chat(limit=10)
|
||||
by_text = {}
|
||||
for m in out["messages"]:
|
||||
by_text.setdefault(m["text"], []).append(m)
|
||||
self.assertEqual(len(by_text["одно и то же"]), 1)
|
||||
self.assertEqual(by_text["одно и то же"][0]["repeat"], 3)
|
||||
self.assertEqual(len(by_text["другое"]), 1)
|
||||
|
||||
# ── chat_send: user-строка consumed=0 ──
|
||||
def test_chat_send_inserts_user_row(self):
|
||||
out = self.dash.chat_send("тест из теста")
|
||||
self.assertTrue(out["ok"])
|
||||
self.assertTrue(out["msg_id"].startswith("dash-80-"))
|
||||
db = sqlite3.connect(_COOP_DB)
|
||||
db.row_factory = sqlite3.Row
|
||||
row = db.execute("SELECT sender, text, consumed FROM agent_portal_chat "
|
||||
"WHERE msg_id=?", (out["msg_id"],)).fetchone()
|
||||
db.close()
|
||||
self.assertEqual(row["sender"], "user")
|
||||
self.assertEqual(row["consumed"], 0)
|
||||
self.assertEqual(row["text"], "тест из теста")
|
||||
|
||||
def test_chat_send_empty_rejected(self):
|
||||
self.assertFalse(self.dash.chat_send(" ")["ok"])
|
||||
|
||||
# ── helpers ──
|
||||
def _seed(self, rows):
|
||||
d = self.dash
|
||||
db = sqlite3.connect(_COOP_DB)
|
||||
db.execute("DELETE FROM agent_portal_chat")
|
||||
for mid, sender, text, ts, *rest in rows:
|
||||
consumed = rest[0] if rest else 0
|
||||
db.execute("INSERT INTO agent_portal_chat (msg_id, agent_id, sender, text, ts, consumed)"
|
||||
" VALUES (?,?,?,?,?,?)", (mid, 80, sender, text, ts, consumed))
|
||||
db.commit()
|
||||
db.close()
|
||||
return d
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
Loading…
Reference in a new issue