From b9d7fd9d2304ef5385b726c2e82705b206d31e21 Mon Sep 17 00:00:00 2001 From: Simbiot Harness Date: Fri, 2 Oct 2026 22:12:58 +0000 Subject: [PATCH] =?UTF-8?q?tests:=20dash-chat=20hermetic=20suite=20(7/7)?= =?UTF-8?q?=20=E2=80=94=20=D0=BB=D0=B5=D0=BD=D1=82=D0=B0=20=D0=91=D0=94=20?= =?UTF-8?q?=D0=BF=D0=BE=D1=80=D1=82=D0=B0=D0=BB=D0=B0,=20=D1=81=D0=B5?= =?UTF-8?q?=D0=BA=D1=80=D0=B5=D1=82=20=D0=BA=D0=BE=D0=BC=D0=B0=D0=BD=D0=B4?= =?UTF-8?q?,=20anchor-=D0=B4=D0=B5=D0=B4=D1=83=D0=BF,=20=D0=BA=D0=BE=D0=BB?= =?UTF-8?q?=D0=BB=D0=B0=D0=BF=D1=81=20=D0=BF=D0=BE=D0=B2=D1=82=D0=BE=D1=80?= =?UTF-8?q?=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_dash_chat.py | 160 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 tests/test_dash_chat.py diff --git a/tests/test_dash_chat.py b/tests/test_dash_chat.py new file mode 100644 index 0000000..e889834 --- /dev/null +++ b/tests/test_dash_chat.py @@ -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--»), подряд одинаковые 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) \ No newline at end of file