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