feat(deploy): finalize MVP deployment and file transfer approach
This commit is contained in:
parent
6369721876
commit
0f79494fbe
43 changed files with 3078 additions and 645 deletions
|
|
@ -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",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -185,6 +185,24 @@ async def test_real_platform_client_send_message_uses_direct_agent_api_per_chat(
|
|||
assert await prototype_state.get_last_tokens_used_for_context("chat-7") == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_real_platform_client_preserves_path_base_url_without_trailing_slash():
|
||||
agent_api = FakeAgentApiFactory()
|
||||
client = RealPlatformClient(
|
||||
agent_id="agent-17",
|
||||
agent_base_url="http://lambda.coredump.ru:7000/agent_17",
|
||||
agent_api_cls=agent_api,
|
||||
prototype_state=PrototypeStateStore(),
|
||||
platform="matrix",
|
||||
)
|
||||
|
||||
await client.send_message("@alice:example.org", "41", "hello")
|
||||
|
||||
assert agent_api.created_calls == [
|
||||
("agent-17", "http://lambda.coredump.ru:7000/agent_17/", "41")
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_real_platform_client_forwards_attachments_to_chat_api():
|
||||
agent_api = FakeAgentApiFactory(chat_api_cls=AttachmentTrackingChatAgentApi)
|
||||
|
|
@ -213,15 +231,15 @@ async def test_real_platform_client_forwards_attachments_to_chat_api():
|
|||
|
||||
def test_attachment_paths_normalize_workspace_roots_to_relative_paths():
|
||||
attachments = [
|
||||
Attachment(workspace_path="/workspace/output/report.pdf"),
|
||||
Attachment(workspace_path="/agents/7/output/report.csv"),
|
||||
Attachment(workspace_path="surfaces/matrix/alice/room/inbox/note.txt"),
|
||||
Attachment(workspace_path="/workspace/report.pdf"),
|
||||
Attachment(workspace_path="/agents/7/report.csv"),
|
||||
Attachment(workspace_path="note.txt"),
|
||||
]
|
||||
|
||||
assert RealPlatformClient._attachment_paths(attachments) == [
|
||||
"output/report.pdf",
|
||||
"output/report.csv",
|
||||
"surfaces/matrix/alice/room/inbox/note.txt",
|
||||
"report.pdf",
|
||||
"report.csv",
|
||||
"note.txt",
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -257,9 +275,12 @@ async def test_real_platform_client_preserves_send_file_events_in_sync_result(mo
|
|||
@pytest.mark.parametrize(
|
||||
("location", "expected_workspace_path"),
|
||||
[
|
||||
("/workspace/output/report.pdf", "output/report.pdf"),
|
||||
("/agents/7/output/report.pdf", "output/report.pdf"),
|
||||
("surfaces/matrix/alice/room/inbox/report.pdf", "surfaces/matrix/alice/room/inbox/report.pdf"),
|
||||
("/workspace/report.pdf", "report.pdf"),
|
||||
("/agents/7/report.pdf", "report.pdf"),
|
||||
(
|
||||
"surfaces/matrix/alice/room/inbox/report.pdf",
|
||||
"surfaces/matrix/alice/room/inbox/report.pdf",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_attachment_from_send_file_event_normalizes_shared_volume_paths(
|
||||
|
|
|
|||
22
tests/test_check_matrix_agents.py
Normal file
22
tests/test_check_matrix_agents.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
from tools.check_matrix_agents import build_agent_ws_url
|
||||
|
||||
|
||||
def test_build_agent_ws_url_preserves_path_prefix_without_trailing_slash():
|
||||
assert (
|
||||
build_agent_ws_url("http://lambda.coredump.ru:7000/agent_17", "41")
|
||||
== "http://lambda.coredump.ru:7000/agent_17/v1/agent_ws/41/"
|
||||
)
|
||||
|
||||
|
||||
def test_build_agent_ws_url_preserves_path_prefix_with_trailing_slash():
|
||||
assert (
|
||||
build_agent_ws_url("http://lambda.coredump.ru:7000/agent_17/", "41")
|
||||
== "http://lambda.coredump.ru:7000/agent_17/v1/agent_ws/41/"
|
||||
)
|
||||
|
||||
|
||||
def test_build_agent_ws_url_accepts_existing_agent_ws_url():
|
||||
assert (
|
||||
build_agent_ws_url("http://lambda.coredump.ru:7000/agent_17/v1/agent_ws/0/", "41")
|
||||
== "http://lambda.coredump.ru:7000/agent_17/v1/agent_ws/41/"
|
||||
)
|
||||
|
|
@ -39,6 +39,21 @@ def test_dockerfile_production_build_does_not_require_local_external_tree():
|
|||
assert "uv pip install --system --ignore-requires-python" not in dockerfile
|
||||
|
||||
|
||||
def test_dockerfile_installs_agent_api_after_final_uv_sync():
|
||||
dockerfile = (ROOT / "Dockerfile").read_text(encoding="utf-8")
|
||||
development = dockerfile.split("FROM base AS development", maxsplit=1)[1].split(
|
||||
"FROM base AS production", maxsplit=1
|
||||
)[0]
|
||||
production = dockerfile.split("FROM base AS production", maxsplit=1)[1]
|
||||
|
||||
assert development.index("RUN uv sync --no-dev --frozen") < development.index(
|
||||
"pip install --no-cache-dir --ignore-requires-python -e /agent_api/"
|
||||
)
|
||||
assert production.index("RUN uv sync --no-dev --frozen") < production.index(
|
||||
"git+https://git.lambda.coredump.ru/platform/agent_api.git"
|
||||
)
|
||||
|
||||
|
||||
def test_dockerignore_excludes_local_only_and_runtime_artifacts():
|
||||
dockerignore = (ROOT / ".dockerignore").read_text(encoding="utf-8")
|
||||
|
||||
|
|
@ -60,3 +75,28 @@ def test_agent_registry_example_documents_multi_agent_volume_contract():
|
|||
for index, agent in enumerate(agents):
|
||||
assert agent["base_url"].endswith(f"/agent_{index}/")
|
||||
assert agent["workspace_path"] == f"/agents/{index}"
|
||||
|
||||
|
||||
def test_smoke_compose_models_deploy_like_proxy_and_surface_checker():
|
||||
smoke = _compose("docker-compose.smoke.yml")
|
||||
|
||||
assert set(smoke["services"]) >= {"surface-smoke", "agent-proxy", "agent-0", "agent-1"}
|
||||
assert "tools.check_matrix_agents" in smoke["services"]["surface-smoke"]["command"]
|
||||
assert smoke["services"]["agent-proxy"]["ports"] == ["${SMOKE_PROXY_PORT:-7000}:7000"]
|
||||
|
||||
|
||||
def test_smoke_timeout_override_routes_one_agent_to_no_status_stub():
|
||||
smoke_timeout = _compose("docker-compose.smoke.timeout.yml")
|
||||
|
||||
assert set(smoke_timeout["services"]) >= {"agent-proxy", "agent-no-status"}
|
||||
|
||||
|
||||
def test_smoke_registry_targets_local_proxy_routes():
|
||||
registry = yaml.safe_load(
|
||||
(ROOT / "config" / "matrix-agents.smoke.yaml").read_text(encoding="utf-8")
|
||||
)
|
||||
|
||||
assert [agent["base_url"] for agent in registry["agents"]] == [
|
||||
"http://agent-proxy:7000/agent_0/",
|
||||
"http://agent-proxy:7000/agent_1/",
|
||||
]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue