feat(deploy): finalize MVP deployment and file transfer approach

This commit is contained in:
Mikhail Putilovskij 2026-05-02 23:45:52 +03:00
parent 6369721876
commit 0f79494fbe
43 changed files with 3078 additions and 645 deletions

View file

@ -211,7 +211,7 @@ async def test_invite_event_is_idempotent_per_user():
assert client.join.await_count == 2
assert client.room_create.await_count == 2
client.room_send.assert_awaited_once()
assert client.room_send.await_count == 2
async def test_bot_ignores_its_own_messages():
@ -348,7 +348,8 @@ async def test_bot_downloads_matrix_file_to_configured_agent_workspace(tmp_path,
base_url="http://lambda.coredump.ru:7000/agent_17/",
workspace_path=str(tmp_path / "agents" / "17"),
)
]
],
user_agents={"@alice:example.org": "agent-17"},
)
await set_room_meta(
runtime.store,
@ -381,7 +382,7 @@ async def test_bot_downloads_matrix_file_to_configured_agent_workspace(tmp_path,
staged = await get_staged_attachments(
runtime.store, "!chat17:example.org", "@alice:example.org"
)
assert staged[0]["workspace_path"].startswith("incoming/")
assert staged[0]["workspace_path"] == "report.pdf"
assert (
tmp_path / "agents" / "17" / staged[0]["workspace_path"]
).read_bytes() == b"%PDF-1.7"
@ -389,7 +390,7 @@ async def test_bot_downloads_matrix_file_to_configured_agent_workspace(tmp_path,
async def test_bot_uploads_agent_output_from_configured_agent_workspace(tmp_path, monkeypatch):
monkeypatch.setenv("SURFACES_WORKSPACE_DIR", str(tmp_path / "agents"))
output_file = tmp_path / "agents" / "17" / "output" / "result.txt"
output_file = tmp_path / "agents" / "17" / "result.txt"
output_file.parent.mkdir(parents=True)
output_file.write_text("ready", encoding="utf-8")
runtime = build_runtime(platform=MockPlatformClient())
@ -401,7 +402,8 @@ async def test_bot_uploads_agent_output_from_configured_agent_workspace(tmp_path
base_url="http://lambda.coredump.ru:7000/agent_17/",
workspace_path=str(tmp_path / "agents" / "17"),
)
]
],
user_agents={"@alice:example.org": "agent-17"},
)
await set_room_meta(
runtime.store,
@ -429,7 +431,7 @@ async def test_bot_uploads_agent_output_from_configured_agent_workspace(tmp_path
type="document",
filename="result.txt",
mime_type="text/plain",
workspace_path="output/result.txt",
workspace_path="result.txt",
)
],
)

View file

@ -4,29 +4,12 @@ from pathlib import Path
from types import SimpleNamespace
from adapter.matrix.files import (
build_agent_incoming_path,
build_workspace_attachment_path,
build_agent_workspace_path,
download_matrix_attachment,
)
from core.protocol import Attachment
def test_build_workspace_attachment_path_scopes_by_surface_user_and_room(tmp_path: Path):
rel_path, abs_path = build_workspace_attachment_path(
workspace_root=tmp_path,
matrix_user_id="@alice:example.org",
room_id="!room:example.org",
filename="report.pdf",
timestamp="20260420-153000",
)
assert (
rel_path
== "surfaces/matrix/alice_example.org/room_example.org/inbox/20260420-153000-report.pdf"
)
assert abs_path == tmp_path / rel_path
async def test_download_matrix_attachment_persists_file_and_returns_workspace_path(tmp_path: Path):
async def download(url: str):
assert url == "mxc://server/id"
@ -49,40 +32,46 @@ async def test_download_matrix_attachment_persists_file_and_returns_workspace_pa
timestamp="20260420-153000",
)
assert saved.workspace_path is not None
assert saved.workspace_path.endswith("20260420-153000-report.pdf")
assert (tmp_path / saved.workspace_path).read_bytes() == b"%PDF-1.7"
assert saved.workspace_path == "report.pdf"
assert (tmp_path / "report.pdf").read_bytes() == b"%PDF-1.7"
def test_build_workspace_attachment_path_keeps_room_safe_agents_relative_contract(tmp_path: Path):
rel_path, abs_path = build_workspace_attachment_path(
workspace_root=tmp_path / "agents" / "7",
matrix_user_id="@alice+bob:example.org",
room_id="!room/ops:example.org",
filename="quarterly status (final).pdf",
timestamp="20260420-153000",
)
assert rel_path == (
"surfaces/matrix/alice_bob_example.org/room_ops_example.org/inbox/"
"20260420-153000-quarterly_status_final_.pdf"
)
assert not Path(rel_path).is_absolute()
assert abs_path == tmp_path / "agents" / "7" / rel_path
def test_build_agent_incoming_path_uses_agent_workspace_volume(tmp_path: Path):
rel_path, abs_path = build_agent_incoming_path(
def test_build_agent_workspace_path_uses_agent_workspace_volume(tmp_path: Path):
rel_path, abs_path = build_agent_workspace_path(
workspace_root=tmp_path / "agents" / "17",
filename="quarterly status.pdf",
timestamp="20260428-110000",
)
assert rel_path == "incoming/20260428-110000-quarterly_status.pdf"
assert rel_path == "quarterly status.pdf"
assert abs_path == tmp_path / "agents" / "17" / rel_path
async def test_download_matrix_attachment_uses_agent_workspace_incoming_dir(tmp_path: Path):
def test_build_agent_workspace_path_uses_windows_style_copy_index(tmp_path: Path):
workspace_root = tmp_path / "agents" / "17"
workspace_root.mkdir(parents=True)
(workspace_root / "report.pdf").write_bytes(b"old")
(workspace_root / "report (1).pdf").write_bytes(b"older")
rel_path, abs_path = build_agent_workspace_path(
workspace_root=workspace_root,
filename="report.pdf",
)
assert rel_path == "report (2).pdf"
assert abs_path == workspace_root / "report (2).pdf"
def test_build_agent_workspace_path_sanitizes_to_basename(tmp_path: Path):
rel_path, abs_path = build_agent_workspace_path(
workspace_root=tmp_path / "agents" / "17",
filename="../../quarterly: status?.pdf",
)
assert rel_path == "quarterly_ status_.pdf"
assert abs_path == tmp_path / "agents" / "17" / "quarterly_ status_.pdf"
async def test_download_matrix_attachment_uses_agent_workspace_root(tmp_path: Path):
async def download(url: str):
assert url == "mxc://server/id"
return SimpleNamespace(body=b"%PDF-1.7")
@ -101,5 +90,5 @@ async def test_download_matrix_attachment_uses_agent_workspace_incoming_dir(tmp_
timestamp="20260428-110000",
)
assert saved.workspace_path == "incoming/20260428-110000-report.pdf"
assert saved.workspace_path == "report.pdf"
assert (tmp_path / "agents" / "17" / saved.workspace_path).read_bytes() == b"%PDF-1.7"

View file

@ -7,7 +7,7 @@ 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
from adapter.matrix.store import get_room_meta, get_user_meta, set_room_meta, set_user_meta
from sdk.mock import MockPlatformClient
@ -100,6 +100,53 @@ async def test_mat02_invite_idempotent():
assert client.room_create.await_count == 2
async def test_existing_user_invite_reinvites_space_and_active_chats():
runtime = build_runtime(platform=MockPlatformClient())
await set_user_meta(
runtime.store,
"@alice:example.org",
{"space_id": "!space:example.org", "next_chat_index": 2},
)
await set_room_meta(
runtime.store,
"!chat1:example.org",
{
"room_type": "chat",
"chat_id": "C1",
"display_name": "Чат 1",
"matrix_user_id": "@alice:example.org",
"space_id": "!space:example.org",
"platform_chat_id": "1",
"agent_id": "agent-1",
},
)
await runtime.chat_mgr.get_or_create(
user_id="@alice:example.org",
chat_id="C1",
platform="matrix",
surface_ref="!chat1:example.org",
name="Чат 1",
)
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,
runtime.chat_mgr,
)
client.room_create.assert_not_awaited()
client.room_invite.assert_any_await("!space:example.org", "@alice:example.org")
client.room_invite.assert_any_await("!chat1:example.org", "@alice:example.org")
client.room_send.assert_awaited()
async def test_mat03_no_hardcoded_c1():
runtime = build_runtime(platform=MockPlatformClient())
await set_user_meta(runtime.store, "@alice:example.org", {"next_chat_index": 7})

View file

@ -4,6 +4,7 @@ import importlib
from types import SimpleNamespace
from unittest.mock import AsyncMock
from adapter.matrix.agent_registry import AgentDefinition, AgentRegistry
from adapter.matrix.bot import MatrixBot, build_runtime
from adapter.matrix.reconciliation import reconcile_startup_state
from adapter.matrix.store import get_room_meta, get_user_meta, set_room_meta, set_user_meta
@ -124,6 +125,55 @@ async def test_reconcile_startup_state_is_idempotent_with_existing_local_state()
assert chats[0].chat_id == "C3"
async def test_reconcile_updates_default_agent_assignment_after_user_is_configured():
runtime = build_runtime(platform=MockPlatformClient())
runtime.registry = AgentRegistry(
[
AgentDefinition("agent-default", "Default"),
AgentDefinition("agent-alice", "Alice"),
],
user_agents={"@alice:example.org": "agent-alice"},
)
client = SimpleNamespace(
user_id="@bot:example.org",
rooms={
"!space:example.org": _room(
"!space:example.org",
"Lambda - Alice",
["@bot:example.org", "@alice:example.org"],
),
"!chat3:example.org": _room(
"!chat3:example.org",
"Чат 3",
["@bot:example.org", "@alice:example.org"],
parents=("!space:example.org",),
),
},
)
await set_room_meta(
runtime.store,
"!chat3:example.org",
{
"room_type": "chat",
"chat_id": "C3",
"display_name": "Чат 3",
"matrix_user_id": "@alice:example.org",
"space_id": "!space:example.org",
"platform_chat_id": "42",
"agent_id": "agent-default",
"agent_assignment": "default",
},
)
await reconcile_startup_state(client, runtime)
room_meta = await get_room_meta(runtime.store, "!chat3:example.org")
assert room_meta is not None
assert room_meta["agent_id"] == "agent-alice"
assert room_meta["agent_assignment"] == "configured"
assert room_meta["platform_chat_id"] == "42"
async def test_reconciliation_prevents_lazy_bootstrap_for_existing_room():
runtime = build_runtime(platform=MockPlatformClient())
client = SimpleNamespace(