test(01-05): cover matrix confirm flow round trip

- assert room_id is preserved on !yes and !no callbacks
- exercise send_outgoing to confirm and cancel with user+room scope
This commit is contained in:
Mikhail Putilovskij 2026-04-03 12:27:42 +03:00
parent 35695e043f
commit 716dec5dfd
3 changed files with 160 additions and 16 deletions

View file

@ -19,7 +19,8 @@ async def test_mat09_yes_reads_pending_confirm():
await set_pending_confirm( await set_pending_confirm(
store, store,
"C1", "@alice:example.org",
"!confirm:example.org",
{ {
"action_id": "delete_file", "action_id": "delete_file",
"description": "Удалить файл config.yaml", "description": "Удалить файл config.yaml",
@ -31,16 +32,16 @@ async def test_mat09_yes_reads_pending_confirm():
event = IncomingCallback( event = IncomingCallback(
user_id="@alice:example.org", user_id="@alice:example.org",
platform="matrix", platform="matrix",
chat_id="C1", chat_id="C7",
action="confirm", action="confirm",
payload={"source": "command", "command": "yes"}, payload={"source": "command", "command": "yes", "room_id": "!confirm:example.org"},
) )
result = await handler(event, auth_mgr, platform, chat_mgr, settings_mgr) result = await handler(event, auth_mgr, platform, chat_mgr, settings_mgr)
assert len(result) == 1 assert len(result) == 1
assert isinstance(result[0], OutgoingMessage) assert isinstance(result[0], OutgoingMessage)
assert "Удалить файл config.yaml" in result[0].text assert "Удалить файл config.yaml" in result[0].text
assert await get_pending_confirm(store, "C1") is None assert await get_pending_confirm(store, "@alice:example.org", "!confirm:example.org") is None
async def test_no_clears_pending_confirm(): async def test_no_clears_pending_confirm():
@ -52,7 +53,8 @@ async def test_no_clears_pending_confirm():
await set_pending_confirm( await set_pending_confirm(
store, store,
"C1", "@alice:example.org",
"!confirm:example.org",
{ {
"action_id": "delete_file", "action_id": "delete_file",
"description": "Удалить файл", "description": "Удалить файл",
@ -64,15 +66,15 @@ async def test_no_clears_pending_confirm():
event = IncomingCallback( event = IncomingCallback(
user_id="@alice:example.org", user_id="@alice:example.org",
platform="matrix", platform="matrix",
chat_id="C1", chat_id="C7",
action="cancel", action="cancel",
payload={"source": "command", "command": "no"}, payload={"source": "command", "command": "no", "room_id": "!confirm:example.org"},
) )
result = await handler(event, auth_mgr, platform, chat_mgr, settings_mgr) result = await handler(event, auth_mgr, platform, chat_mgr, settings_mgr)
assert len(result) == 1 assert len(result) == 1
assert "отменено" in result[0].text.lower() assert "отменено" in result[0].text.lower()
assert await get_pending_confirm(store, "C1") is None assert await get_pending_confirm(store, "@alice:example.org", "!confirm:example.org") is None
async def test_yes_without_pending_returns_no_pending(): async def test_yes_without_pending_returns_no_pending():
@ -94,3 +96,35 @@ async def test_yes_without_pending_returns_no_pending():
assert len(result) == 1 assert len(result) == 1
assert "Нет ожидающих" in result[0].text assert "Нет ожидающих" in result[0].text
async def test_yes_falls_back_to_legacy_chat_key_without_room_payload():
store = InMemoryStore()
platform = MockPlatformClient()
chat_mgr = ChatManager(platform, store)
auth_mgr = AuthManager(platform, store)
settings_mgr = SettingsManager(platform, store)
await set_pending_confirm(
store,
"legacy-chat",
{
"action_id": "delete_file",
"description": "Legacy confirm",
"payload": {},
},
)
handler = make_handle_confirm(store)
event = IncomingCallback(
user_id="@alice:example.org",
platform="matrix",
chat_id="legacy-chat",
action="confirm",
payload={"source": "command", "command": "yes"},
)
result = await handler(event, auth_mgr, platform, chat_mgr, settings_mgr)
assert len(result) == 1
assert "Legacy confirm" in result[0].text
assert await get_pending_confirm(store, "legacy-chat") is None

View file

@ -68,15 +68,19 @@ async def test_skills_alias_to_settings_command():
async def test_yes_to_callback(): async def test_yes_to_callback():
result = from_room_event(text_event("!yes"), room_id="!r:m.org", chat_id="C1") result = from_room_event(text_event("!yes"), room_id="!room:example.org", chat_id="C7")
assert isinstance(result, IncomingCallback) assert isinstance(result, IncomingCallback)
assert result.action == "confirm" assert result.action == "confirm"
assert result.chat_id == "C7"
assert result.payload["room_id"] == "!room:example.org"
async def test_no_to_callback(): async def test_no_to_callback():
result = from_room_event(text_event("!no"), room_id="!r:m.org", chat_id="C1") result = from_room_event(text_event("!no"), room_id="!room:example.org", chat_id="C7")
assert isinstance(result, IncomingCallback) assert isinstance(result, IncomingCallback)
assert result.action == "cancel" assert result.action == "cancel"
assert result.chat_id == "C7"
assert result.payload["room_id"] == "!room:example.org"
async def test_file_attachment(): async def test_file_attachment():

View file

@ -4,21 +4,28 @@ from types import SimpleNamespace
from unittest.mock import AsyncMock from unittest.mock import AsyncMock
from adapter.matrix.bot import send_outgoing from adapter.matrix.bot import send_outgoing
from adapter.matrix.store import get_pending_confirm from adapter.matrix.converter import from_room_event
from adapter.matrix.handlers.confirm import make_handle_cancel, make_handle_confirm
from adapter.matrix.store import get_pending_confirm, set_room_meta
from core.auth import AuthManager
from core.chat import ChatManager
from core.protocol import OutgoingUI, UIButton from core.protocol import OutgoingUI, UIButton
from core.settings import SettingsManager
from core.store import InMemoryStore from core.store import InMemoryStore
from sdk.mock import MockPlatformClient
async def test_mat06_outgoing_ui_renders_text_with_yes_no(): async def test_mat06_outgoing_ui_renders_text_with_yes_no():
client = SimpleNamespace(room_send=AsyncMock()) client = SimpleNamespace(room_send=AsyncMock())
store = InMemoryStore() store = InMemoryStore()
await set_room_meta(store, "!confirm:example.org", {"matrix_user_id": "@alice:example.org"})
event = OutgoingUI( event = OutgoingUI(
chat_id="C1", chat_id="C7",
text="Удалить файл?", text="Удалить файл?",
buttons=[UIButton(label="Подтвердить", action="confirm")], buttons=[UIButton(label="Подтвердить", action="confirm")],
) )
await send_outgoing(client, "!room:ex", event, store=store) await send_outgoing(client, "!confirm:example.org", event, store=store)
client.room_send.assert_awaited_once() client.room_send.assert_awaited_once()
body = client.room_send.call_args.args[2]["body"] body = client.room_send.call_args.args[2]["body"]
@ -31,22 +38,121 @@ async def test_mat06_outgoing_ui_renders_text_with_yes_no():
async def test_mat07_outgoing_ui_no_reaction_sent(): async def test_mat07_outgoing_ui_no_reaction_sent():
client = SimpleNamespace(room_send=AsyncMock()) client = SimpleNamespace(room_send=AsyncMock())
store = InMemoryStore() store = InMemoryStore()
await set_room_meta(store, "!confirm:example.org", {"matrix_user_id": "@alice:example.org"})
event = OutgoingUI( event = OutgoingUI(
chat_id="C1", chat_id="C7",
text="Confirm action?", text="Confirm action?",
buttons=[UIButton(label="OK", action="confirm", payload={"id": 1})], buttons=[UIButton(label="OK", action="confirm", payload={"id": 1})],
) )
await send_outgoing(client, "!room:ex", event, store=store) await send_outgoing(client, "!confirm:example.org", event, store=store)
assert client.room_send.await_count == 1 assert client.room_send.await_count == 1
assert client.room_send.call_args.args[1] == "m.room.message" assert client.room_send.call_args.args[1] == "m.room.message"
for call in client.room_send.call_args_list: for call in client.room_send.call_args_list:
assert call.args[1] != "m.reaction" assert call.args[1] != "m.reaction"
pending = await get_pending_confirm(store, "!room:ex") pending = await get_pending_confirm(store, "@alice:example.org", "!confirm:example.org")
assert pending == { assert pending == {
"action_id": "confirm", "action_id": "confirm",
"description": "Confirm action?", "description": "Confirm action?",
"payload": {"id": 1}, "payload": {"id": 1},
} }
async def test_outgoing_ui_yes_round_trip_uses_user_and_room_scope():
client = SimpleNamespace(room_send=AsyncMock())
store = InMemoryStore()
platform = MockPlatformClient()
chat_mgr = ChatManager(platform, store)
auth_mgr = AuthManager(platform, store)
settings_mgr = SettingsManager(platform, store)
await set_room_meta(store, "!confirm:example.org", {"matrix_user_id": "@alice:example.org"})
await set_room_meta(store, "!other:example.org", {"matrix_user_id": "@bob:example.org"})
await send_outgoing(
client,
"!confirm:example.org",
OutgoingUI(
chat_id="C7",
text="Archive room",
buttons=[UIButton(label="Confirm", action="archive", payload={"id": 7})],
),
store=store,
)
await send_outgoing(
client,
"!other:example.org",
OutgoingUI(
chat_id="C8",
text="Keep other room",
buttons=[UIButton(label="Confirm", action="archive", payload={"id": 8})],
),
store=store,
)
callback = from_room_event(
SimpleNamespace(
sender="@alice:example.org",
body="!yes",
event_id="$yes",
msgtype="m.text",
replyto_event_id=None,
),
room_id="!confirm:example.org",
chat_id="C7",
)
result = await make_handle_confirm(store)(callback, auth_mgr, platform, chat_mgr, settings_mgr)
assert "Archive room" in result[0].text
assert await get_pending_confirm(store, "@alice:example.org", "!confirm:example.org") is None
assert await get_pending_confirm(store, "@bob:example.org", "!other:example.org") is not None
async def test_outgoing_ui_no_round_trip_uses_user_and_room_scope():
client = SimpleNamespace(room_send=AsyncMock())
store = InMemoryStore()
platform = MockPlatformClient()
chat_mgr = ChatManager(platform, store)
auth_mgr = AuthManager(platform, store)
settings_mgr = SettingsManager(platform, store)
await set_room_meta(store, "!confirm:example.org", {"matrix_user_id": "@alice:example.org"})
await set_room_meta(store, "!other:example.org", {"matrix_user_id": "@bob:example.org"})
await send_outgoing(
client,
"!confirm:example.org",
OutgoingUI(
chat_id="C7",
text="Delete room",
buttons=[UIButton(label="Confirm", action="delete", payload={"id": 7})],
),
store=store,
)
await send_outgoing(
client,
"!other:example.org",
OutgoingUI(
chat_id="C8",
text="Keep other room",
buttons=[UIButton(label="Confirm", action="archive", payload={"id": 8})],
),
store=store,
)
callback = from_room_event(
SimpleNamespace(
sender="@alice:example.org",
body="!no",
event_id="$no",
msgtype="m.text",
replyto_event_id=None,
),
room_id="!confirm:example.org",
chat_id="C7",
)
result = await make_handle_cancel(store)(callback, auth_mgr, platform, chat_mgr, settings_mgr)
assert "отменено" in result[0].text.lower()
assert await get_pending_confirm(store, "@alice:example.org", "!confirm:example.org") is None
assert await get_pending_confirm(store, "@bob:example.org", "!other:example.org") is not None