tests: dash-chat hermetic suite (7/7) — лента БД портала, секрет команд, anchor-дедуп, коллапс повторов

This commit is contained in:
Simbiot Harness 2026-10-02 22:12:58 +00:00
parent 5c02bc7cdd
commit b9d7fd9d23

160
tests/test_dash_chat.py Normal file
View 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)