feat(matrix): land QA follow-ups and refresh docs

- harden Matrix onboarding/chat lifecycle after manual QA
- refresh README and Matrix docs to match current behavior
- add local ignores for runtime artifacts and include current planning/report docs

Closes #7
Closes #9
Closes #14
This commit is contained in:
Mikhail Putilovskij 2026-04-05 19:08:58 +03:00
parent 7fce4c9b3e
commit 6ced154124
35 changed files with 8380 additions and 67 deletions

View file

@ -3,9 +3,10 @@ from __future__ import annotations
from types import SimpleNamespace
from unittest.mock import AsyncMock
from nio.api import RoomVisibility
from nio.responses import RoomCreateError
from adapter.matrix.handlers.chat import make_handle_archive, make_handle_new_chat
from adapter.matrix.handlers.chat import make_handle_archive, make_handle_new_chat, make_handle_rename
from adapter.matrix.store import set_user_meta
from core.auth import AuthManager
from core.chat import ChatManager
@ -44,7 +45,14 @@ async def test_mat04_new_chat_calls_room_put_state_with_space_id():
)
result = await handler(event, auth_mgr, platform, chat_mgr, settings_mgr)
client.room_create.assert_awaited_once_with(
name="Test",
visibility=RoomVisibility.private,
is_direct=False,
invite=["@alice:example.org"],
)
client.room_put_state.assert_awaited_once()
client.room_invite.assert_not_awaited()
kwargs = client.room_put_state.call_args.kwargs
assert kwargs.get("room_id") == "!space:ex"
assert kwargs.get("event_type") == "m.space.child"
@ -79,7 +87,8 @@ async def test_mat05_new_chat_without_space_id_returns_error():
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)
client = SimpleNamespace(room_leave=AsyncMock())
handler = make_handle_archive(client, store)
event = IncomingCommand(
user_id="@alice:example.org",
platform="matrix",
@ -98,6 +107,61 @@ async def test_mat10_archive_calls_chat_mgr_archive():
assert len(result) == 1
assert "архивирован" in result[0].text
client.room_leave.assert_awaited_once_with("!room:ex")
chats = await chat_mgr.list_active("@alice:example.org")
assert chats == []
async def test_mat11_rename_updates_matrix_room_name_via_state_event():
platform, store, chat_mgr, auth_mgr, settings_mgr = await _setup()
await chat_mgr.get_or_create(
user_id="@alice:example.org",
chat_id="C1",
platform="matrix",
surface_ref="!room:ex",
name="Old",
)
client = SimpleNamespace(room_put_state=AsyncMock())
handler = make_handle_rename(client, store)
event = IncomingCommand(
user_id="@alice:example.org",
platform="matrix",
chat_id="C1",
command="rename",
args=["New", "Name"],
)
result = await handler(event, auth_mgr, platform, chat_mgr, settings_mgr)
client.room_put_state.assert_awaited_once_with(
room_id="!room:ex",
event_type="m.room.name",
content={"name": "New Name"},
state_key="",
)
assert len(result) == 1
assert "Переименован" in result[0].text
async def test_mat11b_rename_from_unregistered_room_returns_error_message():
platform, store, chat_mgr, auth_mgr, settings_mgr = await _setup()
client = SimpleNamespace(room_put_state=AsyncMock())
handler = make_handle_rename(client, store)
event = IncomingCommand(
user_id="@alice:example.org",
platform="matrix",
chat_id="unregistered:!old:example.org",
command="rename",
args=["New"],
)
result = await handler(event, auth_mgr, platform, chat_mgr, settings_mgr)
client.room_put_state.assert_not_awaited()
assert len(result) == 1
assert "не найден" in result[0].text.lower() or "примите приглашение" in result[0].text.lower()
async def test_mat12_room_create_error_returns_user_message():

View file

@ -3,7 +3,10 @@ from __future__ import annotations
from types import SimpleNamespace
from unittest.mock import AsyncMock
from adapter.matrix.bot import MatrixBot, build_runtime
from nio.api import RoomVisibility
from nio.responses import SyncResponse
from adapter.matrix.bot import MatrixBot, build_runtime, prepare_live_sync
from adapter.matrix.handlers.auth import handle_invite
from adapter.matrix.store import get_room_meta, get_user_meta, set_user_meta
from core.protocol import IncomingCallback, IncomingCommand, OutgoingMessage
@ -72,7 +75,12 @@ async def test_new_chat_creates_real_matrix_room_when_client_available():
)
result = await runtime.dispatcher.dispatch(new)
client.room_create.assert_awaited_once_with(name="Research", visibility="private", is_direct=False)
client.room_create.assert_awaited_once_with(
name="Research",
visibility=RoomVisibility.private,
is_direct=False,
invite=["u1"],
)
client.room_put_state.assert_awaited_once()
put_call = client.room_put_state.call_args
assert put_call.kwargs.get("room_id") == "!space:example" or put_call.args[0] == "!space:example"
@ -97,13 +105,27 @@ async def test_invite_event_creates_space_and_chat_room():
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,
runtime.chat_mgr,
)
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
)
assert first_call.kwargs.get("visibility") is RoomVisibility.private
assert first_call.kwargs.get("invite") == ["@alice:example.org"]
second_call = client.room_create.call_args_list[1]
assert second_call.kwargs.get("visibility") is RoomVisibility.private
assert second_call.kwargs.get("invite") == ["@alice:example.org"]
client.room_invite.assert_not_awaited()
client.room_put_state.assert_awaited_once()
put_state_call = client.room_put_state.call_args
@ -137,8 +159,24 @@ async def test_invite_event_is_idempotent_per_user():
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)
await handle_invite(
client,
room,
event,
runtime.platform,
runtime.store,
runtime.auth_mgr,
runtime.chat_mgr,
)
await handle_invite(
client,
room,
event,
runtime.platform,
runtime.store,
runtime.auth_mgr,
runtime.chat_mgr,
)
assert client.room_create.await_count == 2
@ -179,3 +217,40 @@ async def test_mat11_settings_returns_dashboard():
assert "Изменить" not in text
assert "!connectors" not in text
assert "!whoami" not in text
async def test_mat12_help_returns_command_reference():
runtime = build_runtime(platform=MockPlatformClient())
result = await runtime.dispatcher.dispatch(
IncomingCommand(user_id="u1", platform="matrix", chat_id="C1", command="help")
)
assert len(result) == 1
text = result[0].text
assert "!new" in text
assert "!rename" in text
assert "!archive" in text
assert "!settings" in text
assert "!yes" in text
async def test_prepare_live_sync_returns_next_batch_from_bootstrap_sync():
client = SimpleNamespace(
sync=AsyncMock(
return_value=SyncResponse(
next_batch="s123",
rooms={},
device_key_count={},
device_list=SimpleNamespace(changed=[], left=[]),
to_device_events=[],
presence_events=[],
account_data_events=[],
)
)
)
since = await prepare_live_sync(client)
client.sync.assert_awaited_once_with(timeout=0, full_state=True)
assert since == "s123"

View file

@ -3,6 +3,8 @@ from __future__ import annotations
from types import SimpleNamespace
from unittest.mock import AsyncMock
from nio.api import RoomVisibility
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, set_user_meta
@ -28,11 +30,25 @@ async def test_mat01_invite_creates_space_and_chat1():
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,
runtime.chat_mgr,
)
first_call = client.room_create.call_args_list[0]
assert first_call.kwargs.get("space") is True
assert first_call.kwargs.get("visibility") is RoomVisibility.private
assert first_call.kwargs.get("invite") == ["@alice:example.org"]
second_call = client.room_create.call_args_list[1]
assert second_call.kwargs.get("visibility") is RoomVisibility.private
assert second_call.kwargs.get("invite") == ["@alice:example.org"]
assert client.room_create.await_count == 2
client.room_invite.assert_not_awaited()
client.room_put_state.assert_awaited_once()
kwargs = client.room_put_state.call_args.kwargs
@ -50,6 +66,10 @@ async def test_mat01_invite_creates_space_and_chat1():
assert room_meta["space_id"] == "!space:example.org"
assert user_meta["next_chat_index"] == 5
chats = await runtime.chat_mgr.list_active("@alice:example.org")
assert [chat.chat_id for chat in chats] == ["C4"]
assert [chat.surface_ref for chat in chats] == ["!chat1:example.org"]
async def test_mat02_invite_idempotent():
runtime = build_runtime(platform=MockPlatformClient())
@ -57,8 +77,24 @@ async def test_mat02_invite_idempotent():
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)
await handle_invite(
client,
room,
event,
runtime.platform,
runtime.store,
runtime.auth_mgr,
runtime.chat_mgr,
)
await handle_invite(
client,
room,
event,
runtime.platform,
runtime.store,
runtime.auth_mgr,
runtime.chat_mgr,
)
assert client.room_create.await_count == 2
@ -70,7 +106,15 @@ async def test_mat03_no_hardcoded_c1():
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,
runtime.chat_mgr,
)
room_meta = await get_room_meta(runtime.store, "!chat1:example.org")
assert room_meta is not None