test(01-04): add matrix space regression coverage

- add MAT-01..MAT-07 and MAT-09..MAT-12 regression tests for matrix adapter
- extend store and dispatcher coverage for pending confirmations and settings dashboard
- verify matrix adapter suite and full pytest suite stay green
This commit is contained in:
Mikhail Putilovskij 2026-04-02 23:03:17 +03:00
parent 6f1bdb4077
commit 97a3dc35ea
6 changed files with 382 additions and 0 deletions

View file

@ -0,0 +1,125 @@
from __future__ import annotations
from types import SimpleNamespace
from unittest.mock import AsyncMock
from nio.responses import RoomCreateError
from adapter.matrix.handlers.chat import make_handle_archive, make_handle_new_chat
from adapter.matrix.store import set_user_meta
from core.auth import AuthManager
from core.chat import ChatManager
from core.protocol import IncomingCommand, OutgoingMessage
from core.settings import SettingsManager
from core.store import InMemoryStore
from sdk.mock import MockPlatformClient
async def _setup():
platform = MockPlatformClient()
store = InMemoryStore()
chat_mgr = ChatManager(platform, store)
auth_mgr = AuthManager(platform, store)
settings_mgr = SettingsManager(platform, store)
await auth_mgr.confirm("@alice:example.org")
return platform, store, chat_mgr, auth_mgr, settings_mgr
async def test_mat04_new_chat_calls_room_put_state_with_space_id():
platform, store, chat_mgr, auth_mgr, settings_mgr = await _setup()
await set_user_meta(store, "@alice:example.org", {"space_id": "!space:ex", "next_chat_index": 2})
client = SimpleNamespace(
room_create=AsyncMock(return_value=SimpleNamespace(room_id="!newroom:ex")),
room_put_state=AsyncMock(),
room_invite=AsyncMock(),
)
handler = make_handle_new_chat(client, store)
event = IncomingCommand(
user_id="@alice:example.org",
platform="matrix",
chat_id="C1",
command="new",
args=["Test"],
)
result = await handler(event, auth_mgr, platform, chat_mgr, settings_mgr)
client.room_put_state.assert_awaited_once()
kwargs = client.room_put_state.call_args.kwargs
assert kwargs.get("room_id") == "!space:ex"
assert kwargs.get("event_type") == "m.space.child"
assert kwargs.get("state_key") == "!newroom:ex"
assert any(isinstance(item, OutgoingMessage) and "Test" in item.text for item in result)
async def test_mat05_new_chat_without_space_id_returns_error():
platform, store, chat_mgr, auth_mgr, settings_mgr = await _setup()
await set_user_meta(store, "@alice:example.org", {"next_chat_index": 1})
client = SimpleNamespace(
room_create=AsyncMock(),
room_put_state=AsyncMock(),
room_invite=AsyncMock(),
)
handler = make_handle_new_chat(client, store)
event = IncomingCommand(
user_id="@alice:example.org",
platform="matrix",
chat_id="C1",
command="new",
)
result = await handler(event, auth_mgr, platform, chat_mgr, settings_mgr)
assert len(result) == 1
assert isinstance(result[0], OutgoingMessage)
assert "Space" in result[0].text or "ошибка" in result[0].text.lower()
client.room_create.assert_not_awaited()
async def test_mat10_archive_calls_chat_mgr_archive():
platform, store, chat_mgr, auth_mgr, settings_mgr = await _setup()
handler = make_handle_archive(None, store)
event = IncomingCommand(
user_id="@alice:example.org",
platform="matrix",
chat_id="C1",
command="archive",
)
await chat_mgr.get_or_create(
user_id="@alice:example.org",
chat_id="C1",
platform="matrix",
surface_ref="!room:ex",
name="Test",
)
result = await handler(event, auth_mgr, platform, chat_mgr, settings_mgr)
assert len(result) == 1
assert "архивирован" in result[0].text
async def test_mat12_room_create_error_returns_user_message():
platform, store, chat_mgr, auth_mgr, settings_mgr = await _setup()
await set_user_meta(store, "@alice:example.org", {"space_id": "!space:ex", "next_chat_index": 2})
client = SimpleNamespace(
room_create=AsyncMock(return_value=RoomCreateError(message="rate limited", status_code="429")),
room_put_state=AsyncMock(),
room_invite=AsyncMock(),
)
handler = make_handle_new_chat(client, store)
event = IncomingCommand(
user_id="@alice:example.org",
platform="matrix",
chat_id="C1",
command="new",
args=["Fail"],
)
result = await handler(event, auth_mgr, platform, chat_mgr, settings_mgr)
assert len(result) == 1
assert isinstance(result[0], OutgoingMessage)
assert "Не удалось" in result[0].text or "не удалось" in result[0].text
client.room_put_state.assert_not_awaited()

View file

@ -0,0 +1,96 @@
from __future__ import annotations
from adapter.matrix.handlers.confirm import make_handle_cancel, make_handle_confirm
from adapter.matrix.store import get_pending_confirm, set_pending_confirm
from core.auth import AuthManager
from core.chat import ChatManager
from core.protocol import IncomingCallback, OutgoingMessage
from core.settings import SettingsManager
from core.store import InMemoryStore
from sdk.mock import MockPlatformClient
async def test_mat09_yes_reads_pending_confirm():
store = InMemoryStore()
platform = MockPlatformClient()
chat_mgr = ChatManager(platform, store)
auth_mgr = AuthManager(platform, store)
settings_mgr = SettingsManager(platform, store)
await set_pending_confirm(
store,
"C1",
{
"action_id": "delete_file",
"description": "Удалить файл config.yaml",
"payload": {},
},
)
handler = make_handle_confirm(store)
event = IncomingCallback(
user_id="@alice:example.org",
platform="matrix",
chat_id="C1",
action="confirm",
payload={"source": "command", "command": "yes"},
)
result = await handler(event, auth_mgr, platform, chat_mgr, settings_mgr)
assert len(result) == 1
assert isinstance(result[0], OutgoingMessage)
assert "Удалить файл config.yaml" in result[0].text
assert await get_pending_confirm(store, "C1") is None
async def test_no_clears_pending_confirm():
store = InMemoryStore()
platform = MockPlatformClient()
chat_mgr = ChatManager(platform, store)
auth_mgr = AuthManager(platform, store)
settings_mgr = SettingsManager(platform, store)
await set_pending_confirm(
store,
"C1",
{
"action_id": "delete_file",
"description": "Удалить файл",
"payload": {},
},
)
handler = make_handle_cancel(store)
event = IncomingCallback(
user_id="@alice:example.org",
platform="matrix",
chat_id="C1",
action="cancel",
payload={"source": "command", "command": "no"},
)
result = await handler(event, auth_mgr, platform, chat_mgr, settings_mgr)
assert len(result) == 1
assert "отменено" in result[0].text.lower()
assert await get_pending_confirm(store, "C1") is None
async def test_yes_without_pending_returns_no_pending():
store = InMemoryStore()
platform = MockPlatformClient()
chat_mgr = ChatManager(platform, store)
auth_mgr = AuthManager(platform, store)
settings_mgr = SettingsManager(platform, store)
handler = make_handle_confirm(store)
event = IncomingCallback(
user_id="@alice:example.org",
platform="matrix",
chat_id="C1",
action="confirm",
payload={},
)
result = await handler(event, auth_mgr, platform, chat_mgr, settings_mgr)
assert len(result) == 1
assert "Нет ожидающих" in result[0].text

View file

@ -151,3 +151,20 @@ async def test_bot_ignores_its_own_messages():
runtime.dispatcher.dispatch.assert_not_awaited()
bot._send_all.assert_not_awaited()
async def test_mat11_settings_returns_dashboard():
runtime = build_runtime(platform=MockPlatformClient())
start = IncomingCommand(user_id="u1", platform="matrix", chat_id="C1", command="start")
await runtime.dispatcher.dispatch(start)
settings_cmd = IncomingCommand(user_id="u1", platform="matrix", chat_id="C1", command="settings")
result = await runtime.dispatcher.dispatch(settings_cmd)
assert len(result) >= 1
text = result[0].text
assert "Скиллы" in text or "скиллы" in text.lower()
assert "Изменить" in text or "!skills" in text
assert "!connectors" not in text
assert "!whoami" not in text

View file

@ -0,0 +1,78 @@
from __future__ import annotations
from types import SimpleNamespace
from unittest.mock import AsyncMock
from adapter.matrix.bot import build_runtime
from adapter.matrix.handlers.auth import handle_invite
from adapter.matrix.store import get_room_meta, get_user_meta
from sdk.mock import MockPlatformClient
def _make_client():
space_resp = SimpleNamespace(room_id="!space:example.org")
chat_resp = SimpleNamespace(room_id="!chat1:example.org")
return SimpleNamespace(
join=AsyncMock(),
room_create=AsyncMock(side_effect=[space_resp, chat_resp]),
room_put_state=AsyncMock(),
room_invite=AsyncMock(),
room_send=AsyncMock(),
)
async def test_mat01_invite_creates_space_and_chat1():
runtime = build_runtime(platform=MockPlatformClient())
client = _make_client()
room = SimpleNamespace(room_id="!dm:example.org", display_name="Alice")
event = SimpleNamespace(sender="@alice:example.org", membership="invite")
await handle_invite(client, room, event, runtime.platform, runtime.store, runtime.auth_mgr)
first_call = client.room_create.call_args_list[0]
assert first_call.kwargs.get("space") is True
assert client.room_create.await_count == 2
client.room_put_state.assert_awaited_once()
kwargs = client.room_put_state.call_args.kwargs
assert kwargs.get("event_type") == "m.space.child"
assert kwargs.get("state_key") == "!chat1:example.org"
assert kwargs.get("room_id") == "!space:example.org"
user_meta = await get_user_meta(runtime.store, "@alice:example.org")
assert user_meta is not None
assert user_meta["space_id"] == "!space:example.org"
room_meta = await get_room_meta(runtime.store, "!chat1:example.org")
assert room_meta is not None
assert room_meta["chat_id"] == "C1"
assert room_meta["space_id"] == "!space:example.org"
async def test_mat02_invite_idempotent():
runtime = build_runtime(platform=MockPlatformClient())
client = _make_client()
room = SimpleNamespace(room_id="!dm:example.org", display_name="Alice")
event = SimpleNamespace(sender="@alice:example.org", membership="invite")
await handle_invite(client, room, event, runtime.platform, runtime.store, runtime.auth_mgr)
await handle_invite(client, room, event, runtime.platform, runtime.store, runtime.auth_mgr)
assert client.room_create.await_count == 2
async def test_mat03_no_hardcoded_c1():
runtime = build_runtime(platform=MockPlatformClient())
client = _make_client()
room = SimpleNamespace(room_id="!dm:example.org", display_name="Alice")
event = SimpleNamespace(sender="@alice:example.org", membership="invite")
await handle_invite(client, room, event, runtime.platform, runtime.store, runtime.auth_mgr)
room_meta = await get_room_meta(runtime.store, "!chat1:example.org")
assert room_meta is not None
assert room_meta["chat_id"] == "C1"
user_meta = await get_user_meta(runtime.store, "@alice:example.org")
assert user_meta is not None
assert user_meta["next_chat_index"] == 2

View file

@ -0,0 +1,52 @@
from __future__ import annotations
from types import SimpleNamespace
from unittest.mock import AsyncMock
from adapter.matrix.bot import send_outgoing
from adapter.matrix.store import get_pending_confirm
from core.protocol import OutgoingUI, UIButton
from core.store import InMemoryStore
async def test_mat06_outgoing_ui_renders_text_with_yes_no():
client = SimpleNamespace(room_send=AsyncMock())
store = InMemoryStore()
event = OutgoingUI(
chat_id="C1",
text="Удалить файл?",
buttons=[UIButton(label="Подтвердить", action="confirm")],
)
await send_outgoing(client, "!room:ex", event, store=store)
client.room_send.assert_awaited_once()
body = client.room_send.call_args.args[2]["body"]
assert "Удалить файл?" in body
assert "!yes" in body
assert "!no" in body
assert "Подтвердить" in body
async def test_mat07_outgoing_ui_no_reaction_sent():
client = SimpleNamespace(room_send=AsyncMock())
store = InMemoryStore()
event = OutgoingUI(
chat_id="C1",
text="Confirm action?",
buttons=[UIButton(label="OK", action="confirm", payload={"id": 1})],
)
await send_outgoing(client, "!room:ex", event, store=store)
assert client.room_send.await_count == 1
assert client.room_send.call_args.args[1] == "m.room.message"
for call in client.room_send.call_args_list:
assert call.args[1] != "m.reaction"
pending = await get_pending_confirm(store, "!room:ex")
assert pending == {
"action_id": "confirm",
"description": "Confirm action?",
"payload": {"id": 1},
}

View file

@ -3,11 +3,14 @@ from __future__ import annotations
import pytest
from adapter.matrix.store import (
clear_pending_confirm,
get_pending_confirm,
get_room_meta,
get_room_state,
get_skills_message_id,
get_user_meta,
next_chat_id,
set_pending_confirm,
set_room_meta,
set_room_state,
set_skills_message_id,
@ -70,3 +73,14 @@ async def test_next_chat_id_increments(store: InMemoryStore):
async def test_skills_message_roundtrip(store: InMemoryStore):
await set_skills_message_id(store, "!room", "$event")
assert await get_skills_message_id(store, "!room") == "$event"
async def test_pending_confirm_roundtrip(store: InMemoryStore):
assert await get_pending_confirm(store, "!room:m.org") is None
meta = {"action_id": "test", "description": "Do thing"}
await set_pending_confirm(store, "!room:m.org", meta)
assert await get_pending_confirm(store, "!room:m.org") == meta
await clear_pending_confirm(store, "!room:m.org")
assert await get_pending_confirm(store, "!room:m.org") is None