--- phase: 01-matrix-qa-polish plan: 04 type: execute wave: 3 depends_on: ["01-01", "01-02", "01-03"] files_modified: - tests/adapter/matrix/test_dispatcher.py - tests/adapter/matrix/test_reactions.py - tests/adapter/matrix/test_store.py - tests/adapter/matrix/test_invite_space.py - tests/adapter/matrix/test_chat_space.py - tests/adapter/matrix/test_send_outgoing.py - tests/adapter/matrix/test_confirm.py autonomous: true requirements: [] must_haves: truths: - "All 4 previously-broken tests are fixed and green" - "12 new tests (MAT-01..MAT-12) are implemented and green" - "pytest tests/ -q shows 96+ tests passing" - "No test uses hardcoded 'C1' assumption from old DM flow" artifacts: - path: "tests/adapter/matrix/test_invite_space.py" provides: "MAT-01, MAT-02, MAT-03 tests" contains: "space=True" - path: "tests/adapter/matrix/test_chat_space.py" provides: "MAT-04, MAT-05, MAT-10, MAT-12 tests" contains: "room_put_state" - path: "tests/adapter/matrix/test_send_outgoing.py" provides: "MAT-06, MAT-07 tests" contains: "!yes" - path: "tests/adapter/matrix/test_confirm.py" provides: "MAT-09 test" contains: "get_pending_confirm" - path: "tests/adapter/matrix/test_dispatcher.py" provides: "Fixed broken tests + MAT-11" - path: "tests/adapter/matrix/test_reactions.py" provides: "Fixed broken tests" - path: "tests/adapter/matrix/test_store.py" provides: "MAT-08 pending_confirm roundtrip test" contains: "pending_confirm" key_links: - from: "tests/adapter/matrix/test_invite_space.py" to: "adapter/matrix/handlers/auth.py" via: "tests handle_invite" pattern: "handle_invite" - from: "tests/adapter/matrix/test_chat_space.py" to: "adapter/matrix/handlers/chat.py" via: "tests make_handle_new_chat" pattern: "make_handle_new_chat" --- Fix all broken tests and implement 12 new test cases (MAT-01..MAT-12) covering the Space+rooms refactor. Purpose: Achieve 96+ green tests as required by Phase 1 deliverables. Currently 97 pass; 4 will break from Plans 01-03 changes. This plan fixes those 4 and adds 12 new, targeting ~109 total. Output: Full green test suite with comprehensive Space+rooms coverage. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/01-matrix-qa-polish/01-CONTEXT.md @.planning/phases/01-matrix-qa-polish/01-RESEARCH.md @.planning/phases/01-matrix-qa-polish/01-VALIDATION.md @tests/adapter/matrix/test_dispatcher.py @tests/adapter/matrix/test_reactions.py @tests/adapter/matrix/test_store.py @adapter/matrix/handlers/auth.py @adapter/matrix/handlers/chat.py @adapter/matrix/handlers/confirm.py @adapter/matrix/handlers/settings.py @adapter/matrix/bot.py @adapter/matrix/store.py @adapter/matrix/reactions.py @adapter/matrix/converter.py @core/protocol.py ```python # adapter/matrix/handlers/auth.py async def handle_invite(client, room, event, platform, store, auth_mgr) -> None # Creates Space (space=True), chat room, room_put_state, room_invite x2, stores user_meta+room_meta # adapter/matrix/store.py async def get_pending_confirm(store: StateStore, room_id: str) -> dict | None async def set_pending_confirm(store: StateStore, room_id: str, meta: dict) -> None async def clear_pending_confirm(store: StateStore, room_id: str) -> None # adapter/matrix/handlers/chat.py def make_handle_new_chat(client, store) -> Callable # closure factory def make_handle_archive(client, store) -> Callable # closure factory def make_handle_rename(client, store) -> Callable # closure factory # adapter/matrix/handlers/confirm.py def make_handle_confirm(store=None) -> Callable # closure factory def make_handle_cancel(store=None) -> Callable # closure factory # adapter/matrix/bot.py async def send_outgoing(client, room_id, event, store=None) -> None # For OutgoingUI: renders text + "!yes/!no", calls set_pending_confirm # adapter/matrix/reactions.py def build_skills_text(settings) -> str # No longer mentions "Реакции 1-9" def build_confirmation_text(description) -> str # Uses "!yes/!no" not emojis ``` ```python class InMemoryStore: async def get(key) -> Any async def set(key, value) -> None async def delete(key) -> None # Check if exists; if not, use set(key, None) ``` ```python class MockPlatformClient: # Provides get_or_create_user, get_settings, etc. ``` ```python @dataclass class UserSettings: skills: dict connectors: dict soul: dict safety: dict plan: dict ``` Task 1: Fix 4 broken tests in test_dispatcher.py and test_reactions.py tests/adapter/matrix/test_dispatcher.py, tests/adapter/matrix/test_reactions.py tests/adapter/matrix/test_dispatcher.py, tests/adapter/matrix/test_reactions.py, adapter/matrix/handlers/auth.py, adapter/matrix/handlers/chat.py, adapter/matrix/bot.py, adapter/matrix/reactions.py, adapter/matrix/store.py **Fix 1: test_dispatcher.py::test_invite_event_creates_dm_room_and_sends_welcome** The old test checks `client.join` and `meta["chat_id"] == "C1"` via room_meta on the DM room. After refactor, handle_invite creates a Space + chat room, so the test needs different mocks and assertions. Replace the entire test function with: ```python async def test_invite_event_creates_space_and_chat_room(): from adapter.matrix.store import get_user_meta, get_room_meta runtime = build_runtime(platform=MockPlatformClient()) # Mock client with room_create, room_put_state, room_invite, room_send, join space_resp = SimpleNamespace(room_id="!space:example.org") chat_resp = SimpleNamespace(room_id="!chat1:example.org") client = SimpleNamespace( join=AsyncMock(), room_create=AsyncMock(side_effect=[space_resp, chat_resp]), room_put_state=AsyncMock(), room_invite=AsyncMock(), room_send=AsyncMock(), ) 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) # Verify Space created with space=True assert client.room_create.await_count == 2 first_call = client.room_create.call_args_list[0] assert first_call.kwargs.get("space") is True or (len(first_call.args) > 0 and first_call.kwargs.get("space") is True) # Verify room_put_state called to add child to Space client.room_put_state.assert_awaited_once() put_state_call = client.room_put_state.call_args assert put_state_call.kwargs.get("event_type") == "m.space.child" or put_state_call.args[1] == "m.space.child" # Verify user_meta has space_id user_meta = await get_user_meta(runtime.store, "@alice:example.org") assert user_meta is not None assert user_meta.get("space_id") == "!space:example.org" # Verify room_meta for chat room 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" # Verify auth confirmed assert await runtime.auth_mgr.is_authenticated("@alice:example.org") is True # Verify welcome message sent client.room_send.assert_awaited_once() ``` Also add import at top if not present: ```python from adapter.matrix.store import get_user_meta, get_room_meta ``` (get_room_meta is already imported) **Fix 2: test_dispatcher.py::test_invite_event_is_idempotent_per_room** This test now needs to check idempotency on user_meta (not room_meta). Replace with: ```python async def test_invite_event_is_idempotent_per_user(): runtime = build_runtime(platform=MockPlatformClient()) space_resp = SimpleNamespace(room_id="!space:example.org") chat_resp = SimpleNamespace(room_id="!chat1:example.org") client = SimpleNamespace( join=AsyncMock(), room_create=AsyncMock(side_effect=[space_resp, chat_resp]), room_put_state=AsyncMock(), room_invite=AsyncMock(), room_send=AsyncMock(), ) 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) # Second call should be a no-op (user already has space_id) await handle_invite(client, room, event, runtime.platform, runtime.store, runtime.auth_mgr) # room_create called only twice (once for Space, once for chat room) — not 4 times assert client.room_create.await_count == 2 ``` **Fix 3: test_dispatcher.py::test_new_chat_creates_real_matrix_room_when_client_available** After refactor, make_handle_new_chat needs space_id in user_meta and calls room_put_state. Update: ```python async def test_new_chat_creates_real_matrix_room_when_client_available(): from adapter.matrix.store import set_user_meta client = SimpleNamespace( room_create=AsyncMock(return_value=SimpleNamespace(room_id="!r2:example")), room_put_state=AsyncMock(), room_invite=AsyncMock(), ) runtime = build_runtime(platform=MockPlatformClient(), client=client) # Pre-populate user_meta with space_id (as if invite flow already ran) await set_user_meta(runtime.store, "u1", {"space_id": "!space:example", "next_chat_index": 1}) start = IncomingCommand(user_id="u1", platform="matrix", chat_id="C1", command="start") await runtime.dispatcher.dispatch(start) new = IncomingCommand( user_id="u1", platform="matrix", chat_id="C1", command="new", args=["Research"], ) result = await runtime.dispatcher.dispatch(new) client.room_create.assert_awaited_once_with(name="Research", visibility="private", is_direct=False) client.room_put_state.assert_awaited_once() # Verify room_put_state adds child to space put_call = client.room_put_state.call_args assert put_call.kwargs.get("room_id") == "!space:example" or put_call.args[0] == "!space:example" assert any(isinstance(r, OutgoingMessage) and "Research" in r.text for r in result) ``` **Fix 4: test_dispatcher.py::test_matrix_dispatcher_registers_custom_handlers** This test checks `"Реакции 1️⃣-9️⃣" in r.text` on line 39. After reactions removal, this string no longer appears. Update: Change line 39 from: ```python assert any(isinstance(r, OutgoingMessage) and "Реакции 1️⃣-9️⃣" in r.text for r in result) ``` to: ```python assert any(isinstance(r, OutgoingMessage) and "!skill on/off" in r.text for r in result) ``` **Fix 5: test_reactions.py::test_build_skills_text** Change assertion from: ```python assert "Реакции 1️⃣-9️⃣" in text ``` to: ```python assert "!skill on/off" in text ``` **Fix 6: test_reactions.py::test_build_confirmation_text** The old test checks for "подтвердить" which may still be in the text. Update to check for new format: ```python def test_build_confirmation_text(): text = build_confirmation_text("Отправить письмо?") assert "Отправить письмо?" in text assert "!yes" in text assert "!no" in text ``` Also make sure the `get_room_meta` import and `get_user_meta` import are present in test_dispatcher.py. Add `from adapter.matrix.store import get_user_meta, set_user_meta` if not already imported. cd /Users/a/MAI/sem2/lambda/surfaces-bot && pytest tests/adapter/matrix/test_dispatcher.py tests/adapter/matrix/test_reactions.py -x -q 2>&1 | tail -5 - `test_dispatcher.py` does NOT contain `test_invite_event_creates_dm_room_and_sends_welcome` (renamed to `test_invite_event_creates_space_and_chat_room`) - `test_dispatcher.py` contains `test_invite_event_creates_space_and_chat_room` - `test_dispatcher.py` contains `space=True` in assertions - `test_dispatcher.py` contains `room_put_state` in assertions - `test_reactions.py` contains `!skill on/off` instead of `Реакции 1` - `test_reactions.py` contains `!yes` in confirmation text test - `pytest tests/adapter/matrix/test_dispatcher.py tests/adapter/matrix/test_reactions.py -x -q` passes All 4 previously-broken tests fixed and passing (renamed/updated for Space+rooms) Task 2: Create new test files and implement MAT-01..MAT-12 tests/adapter/matrix/test_invite_space.py, tests/adapter/matrix/test_chat_space.py, tests/adapter/matrix/test_send_outgoing.py, tests/adapter/matrix/test_confirm.py, tests/adapter/matrix/test_store.py, tests/adapter/matrix/test_dispatcher.py adapter/matrix/handlers/auth.py, adapter/matrix/handlers/chat.py, adapter/matrix/bot.py, adapter/matrix/handlers/confirm.py, adapter/matrix/handlers/settings.py, adapter/matrix/store.py, tests/adapter/matrix/test_store.py, tests/adapter/matrix/test_dispatcher.py Create 4 new test files and extend 2 existing ones. All tests use `pytest-asyncio` (async test functions are auto-detected). **File 1: tests/adapter/matrix/test_invite_space.py (MAT-01, MAT-02, MAT-03)** ```python from __future__ import annotations from types import SimpleNamespace from unittest.mock import AsyncMock from adapter.matrix.handlers.auth import handle_invite from adapter.matrix.store import get_user_meta, get_room_meta from adapter.matrix.bot import build_runtime from sdk.mock import MockPlatformClient def _make_client(): """Helper: create mock client with Space+room creation responses.""" 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(): """MAT-01: handle_invite creates Space + Чат 1, saves space_id in user_meta.""" 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) # Space created with space=True first_call = client.room_create.call_args_list[0] assert first_call.kwargs.get("space") is True # Chat room created assert client.room_create.await_count == 2 # room_put_state links child to Space client.room_put_state.assert_awaited_once() ps_kwargs = client.room_put_state.call_args.kwargs assert ps_kwargs.get("event_type") == "m.space.child" assert ps_kwargs.get("state_key") == "!chat1:example.org" assert ps_kwargs.get("room_id") == "!space:example.org" # user_meta stores space_id 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 stores chat metadata 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(): """MAT-02: Repeated invite does not create second Space.""" 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) # Reset side_effect for potential second call client.room_create.side_effect = None client.room_create.return_value = SimpleNamespace(room_id="!should-not-exist:example.org") await handle_invite(client, room, event, runtime.platform, runtime.store, runtime.auth_mgr) # Still only 2 room_create calls (from first invite) assert client.room_create.await_count == 2 async def test_mat03_no_hardcoded_c1(): """MAT-03: handle_invite uses next_chat_id, not hardcoded 'C1'.""" import ast import inspect source = inspect.getsource(handle_invite) # Check that the literal string '"C1"' or "'C1'" does not appear as a value assignment assert '"C1"' not in source or "chat_id" not in source.split('"C1"')[0].split("\n")[-1] # More robust: verify via actual behavior — chat_id comes from next_chat_id 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") # C1 is correct for first user, but it came from next_chat_id (not hardcode) assert room_meta["chat_id"] == "C1" # Verify next_chat_index was incremented (proves next_chat_id was used) user_meta = await get_user_meta(runtime.store, "@alice:example.org") assert user_meta["next_chat_index"] == 2 # Incremented from 1 to 2 ``` **File 2: tests/adapter/matrix/test_chat_space.py (MAT-04, MAT-05, MAT-10, MAT-12)** ```python 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_new_chat, make_handle_archive from adapter.matrix.store import set_user_meta from core.protocol import IncomingCommand, OutgoingMessage from core.store import InMemoryStore from core.chat import ChatManager from core.auth import AuthManager from core.settings import SettingsManager from sdk.mock import MockPlatformClient async def _setup(): """Helper: create platform, store, managers, authenticate user.""" 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(): """MAT-04: !new calls room_put_state to add room to Space.""" 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() ps_kwargs = client.room_put_state.call_args.kwargs assert ps_kwargs.get("room_id") == "!space:ex" assert ps_kwargs.get("event_type") == "m.space.child" assert ps_kwargs.get("state_key") == "!newroom:ex" assert any(isinstance(r, OutgoingMessage) and "Test" in r.text for r in result) async def test_mat05_new_chat_without_space_id_returns_error(): """MAT-05: !new without space_id in user_meta returns error message.""" platform, store, chat_mgr, auth_mgr, settings_mgr = await _setup() # user_meta exists but no space_id 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) # Should return error, not crash assert len(result) == 1 assert isinstance(result[0], OutgoingMessage) assert "Space" in result[0].text or "ошибка" in result[0].text.lower() or "Ошибка" in result[0].text # room_create should NOT have been called client.room_create.assert_not_awaited() async def test_mat10_archive_calls_chat_mgr_archive(): """MAT-10: !archive archives chat via chat_mgr.archive (Space removal deferred).""" 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" ) # Create a chat first so archive has something to work with 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(): """MAT-12: RoomCreateError is handled gracefully with user-facing 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}) # Simulate RoomCreateError error_resp = RoomCreateError(message="rate limited", status_code="429") client = SimpleNamespace( room_create=AsyncMock(return_value=error_resp), 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 # room_put_state should NOT have been called (room creation failed) client.room_put_state.assert_not_awaited() ``` NOTE: For MAT-12, `RoomCreateError` constructor signature may differ. Check the actual nio source. It might be `RoomCreateError(message="...", status_code="...")` or just `RoomCreateError(message="...")`. If the constructor fails, create a mock: ```python error_resp = SimpleNamespace(status_code="429") # Duck-typing: no room_id attr ``` and rely on `isinstance(resp, RoomCreateError)` check in the handler. If isinstance check is used, the SimpleNamespace won't match — so use the real class or mock it. Actually, the handler uses `isinstance(resp, RoomCreateError)` so we MUST use a real `RoomCreateError` instance or the check won't match. Try both approaches: - First: `RoomCreateError(message="error")` - If that fails: mock the isinstance check by making room_create return an object where `hasattr(resp, 'room_id')` is False Read `nio/responses.py` source to find the exact constructor if `RoomCreateError(message="error")` fails during test execution. **File 3: tests/adapter/matrix/test_send_outgoing.py (MAT-06, MAT-07)** ```python 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(): """MAT-06: OutgoingUI renders as text + '!yes / !no' hint.""" 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() call_args = client.room_send.call_args body = call_args.args[2]["body"] if len(call_args.args) > 2 else call_args.kwargs.get("content", {}).get("body", "") assert "Удалить файл?" in body assert "!yes" in body assert "!no" in body assert "Подтвердить" in body async def test_mat07_outgoing_ui_no_reaction_sent(): """MAT-07: OutgoingUI does NOT send m.reaction event.""" client = SimpleNamespace(room_send=AsyncMock()) store = InMemoryStore() event = OutgoingUI( chat_id="C1", text="Confirm action?", buttons=[UIButton(label="OK", action="confirm")], ) await send_outgoing(client, "!room:ex", event, store=store) # Only one room_send call (the text message), no m.reaction assert client.room_send.await_count == 1 call_args = client.room_send.call_args msg_type = call_args.args[1] if len(call_args.args) > 1 else "" assert msg_type == "m.room.message" # Verify no m.reaction calls for call in client.room_send.call_args_list: assert call.args[1] != "m.reaction" ``` **File 4: tests/adapter/matrix/test_confirm.py (MAT-09)** ```python from __future__ import annotations from adapter.matrix.handlers.confirm import make_handle_confirm, make_handle_cancel from adapter.matrix.store import set_pending_confirm, get_pending_confirm from core.protocol import IncomingCallback, OutgoingMessage from core.store import InMemoryStore from sdk.mock import MockPlatformClient from core.chat import ChatManager from core.auth import AuthManager from core.settings import SettingsManager async def test_mat09_yes_reads_pending_confirm(): """MAT-09: !yes reads pending_confirm and returns action description.""" store = InMemoryStore() platform = MockPlatformClient() chat_mgr = ChatManager(platform, store) auth_mgr = AuthManager(platform, store) settings_mgr = SettingsManager(platform, store) # Set up pending confirmation 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 # pending_confirm should be cleared after confirmation pending = await get_pending_confirm(store, "C1") assert pending is None async def test_no_clears_pending_confirm(): """!no clears pending_confirm and returns cancellation.""" 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() pending = await get_pending_confirm(store, "C1") assert pending is None async def test_yes_without_pending_returns_no_pending(): """!yes with no pending confirmation returns 'no pending' message.""" 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 ``` **File 5: Extend tests/adapter/matrix/test_store.py (MAT-08)** Add at the end of the existing file: ```python async def test_pending_confirm_roundtrip(store: InMemoryStore): """MAT-08: get/set/clear_pending_confirm roundtrip.""" from adapter.matrix.store import get_pending_confirm, set_pending_confirm, clear_pending_confirm # Initially None assert await get_pending_confirm(store, "!room:m.org") is None # Set 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 # Clear await clear_pending_confirm(store, "!room:m.org") assert await get_pending_confirm(store, "!room:m.org") is None ``` **File 6: Extend tests/adapter/matrix/test_dispatcher.py (MAT-11)** Add at the end of test_dispatcher.py: ```python async def test_mat11_settings_returns_dashboard(): """MAT-11: !settings returns a read-only dashboard with status info.""" runtime = build_runtime(platform=MockPlatformClient()) # Authenticate user first 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 # Dashboard should contain section headers assert "Скиллы" in text or "скиллы" in text.lower() assert "Изменить" in text or "!skills" in text # Should NOT be the old command list format assert "!connectors" not in text assert "!whoami" not in text ``` IMPORTANT: Check that `core/store.py` InMemoryStore has a `delete` method. If it does NOT, the `clear_pending_confirm` function will fail. Read `core/store.py` and if `delete` is missing, implement `clear_pending_confirm` using `store.set(key, None)` instead and update the test accordingly. cd /Users/a/MAI/sem2/lambda/surfaces-bot && pytest tests/adapter/matrix/ -x -q 2>&1 | tail -10 - File `tests/adapter/matrix/test_invite_space.py` exists and contains `test_mat01`, `test_mat02`, `test_mat03` - File `tests/adapter/matrix/test_chat_space.py` exists and contains `test_mat04`, `test_mat05`, `test_mat10`, `test_mat12` - File `tests/adapter/matrix/test_send_outgoing.py` exists and contains `test_mat06`, `test_mat07` - File `tests/adapter/matrix/test_confirm.py` exists and contains `test_mat09` - `tests/adapter/matrix/test_store.py` contains `test_pending_confirm_roundtrip` - `tests/adapter/matrix/test_dispatcher.py` contains `test_mat11_settings_returns_dashboard` - `pytest tests/adapter/matrix/ -x -q` passes with 0 failures - `pytest tests/ -q` shows 96+ tests passing All 12 new tests (MAT-01..MAT-12) implemented and green; 4 broken tests fixed; total 96+ passing After both tasks: - `pytest tests/ -q` shows 96+ tests passing, 0 failures - `pytest tests/adapter/matrix/ -q` shows all Matrix tests passing - New test files exist: test_invite_space.py, test_chat_space.py, test_send_outgoing.py, test_confirm.py - 96+ tests passing in full suite - 4 broken tests fixed (renamed/updated for Space model) - 12 new tests implemented covering MAT-01..MAT-12 - No test references hardcoded "C1" from old DM flow - All test files importable and runnable After completion, create `.planning/phases/01-matrix-qa-polish/01-04-SUMMARY.md`