simbiot-harness/tests/test_dash_chat.py

160 lines
No EOL
7.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- 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)