825 lines
32 KiB
Markdown
825 lines
32 KiB
Markdown
---
|
||
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"
|
||
---
|
||
|
||
<objective>
|
||
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.
|
||
</objective>
|
||
|
||
<execution_context>
|
||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||
</execution_context>
|
||
|
||
<context>
|
||
@.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
|
||
|
||
<interfaces>
|
||
<!-- After Plans 01-03, these are the key function signatures to test against: -->
|
||
|
||
```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
|
||
```
|
||
|
||
<!-- From core/store.py — InMemoryStore for test fixtures: -->
|
||
|
||
```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)
|
||
```
|
||
|
||
<!-- From sdk.mock — MockPlatformClient: -->
|
||
|
||
```python
|
||
class MockPlatformClient:
|
||
# Provides get_or_create_user, get_settings, etc.
|
||
```
|
||
|
||
<!-- From sdk.interface — UserSettings for test data: -->
|
||
|
||
```python
|
||
@dataclass
|
||
class UserSettings:
|
||
skills: dict
|
||
connectors: dict
|
||
soul: dict
|
||
safety: dict
|
||
plan: dict
|
||
```
|
||
</interfaces>
|
||
</context>
|
||
|
||
<tasks>
|
||
|
||
<task type="auto">
|
||
<name>Task 1: Fix 4 broken tests in test_dispatcher.py and test_reactions.py</name>
|
||
<files>tests/adapter/matrix/test_dispatcher.py, tests/adapter/matrix/test_reactions.py</files>
|
||
<read_first>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</read_first>
|
||
<action>
|
||
**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.
|
||
</action>
|
||
<verify>
|
||
<automated>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</automated>
|
||
</verify>
|
||
<acceptance_criteria>
|
||
- `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
|
||
</acceptance_criteria>
|
||
<done>All 4 previously-broken tests fixed and passing (renamed/updated for Space+rooms)</done>
|
||
</task>
|
||
|
||
<task type="auto">
|
||
<name>Task 2: Create new test files and implement MAT-01..MAT-12</name>
|
||
<files>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</files>
|
||
<read_first>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</read_first>
|
||
<action>
|
||
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.
|
||
</action>
|
||
<verify>
|
||
<automated>cd /Users/a/MAI/sem2/lambda/surfaces-bot && pytest tests/adapter/matrix/ -x -q 2>&1 | tail -10</automated>
|
||
</verify>
|
||
<acceptance_criteria>
|
||
- 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
|
||
</acceptance_criteria>
|
||
<done>All 12 new tests (MAT-01..MAT-12) implemented and green; 4 broken tests fixed; total 96+ passing</done>
|
||
</task>
|
||
|
||
</tasks>
|
||
|
||
<verification>
|
||
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
|
||
</verification>
|
||
|
||
<success_criteria>
|
||
- 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
|
||
</success_criteria>
|
||
|
||
<output>
|
||
After completion, create `.planning/phases/01-matrix-qa-polish/01-04-SUMMARY.md`
|
||
</output>
|