docs(deploy): finalize multi-agent surface image handoff
This commit is contained in:
parent
51241d79e0
commit
5b537880ae
11 changed files with 361 additions and 27 deletions
|
|
@ -15,8 +15,10 @@ from nio import (
|
|||
from nio.api import RoomVisibility
|
||||
from nio.responses import SyncResponse
|
||||
|
||||
from adapter.matrix.agent_registry import AgentDefinition, AgentRegistry
|
||||
from adapter.matrix.bot import MatrixBot, build_runtime, prepare_live_sync
|
||||
from adapter.matrix.handlers.auth import handle_invite
|
||||
from adapter.matrix.routed_platform import RoutedPlatformClient
|
||||
from adapter.matrix.store import (
|
||||
add_staged_attachment,
|
||||
get_platform_chat_id,
|
||||
|
|
@ -36,7 +38,6 @@ from core.protocol import (
|
|||
)
|
||||
from sdk.interface import PlatformError
|
||||
from sdk.mock import MockPlatformClient
|
||||
from adapter.matrix.routed_platform import RoutedPlatformClient
|
||||
|
||||
|
||||
async def test_matrix_dispatcher_registers_custom_handlers():
|
||||
|
|
@ -107,7 +108,10 @@ async def test_new_chat_creates_real_matrix_room_when_client_available():
|
|||
assert client.room_create.await_count >= 1
|
||||
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"
|
||||
assert (
|
||||
put_call.kwargs.get("room_id") == "!space:example"
|
||||
or put_call.args[0] == "!space:example"
|
||||
)
|
||||
chats = await runtime.chat_mgr.list_active("u1")
|
||||
assert [c.chat_id for c in chats] == ["C7"]
|
||||
assert [c.surface_ref for c in chats] == ["!r2:example"]
|
||||
|
|
@ -333,6 +337,119 @@ async def test_bot_downloads_matrix_file_to_workspace_before_staging(tmp_path, m
|
|||
bot._send_all.assert_not_awaited()
|
||||
|
||||
|
||||
async def test_bot_downloads_matrix_file_to_configured_agent_workspace(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("SURFACES_WORKSPACE_DIR", str(tmp_path / "agents"))
|
||||
runtime = build_runtime(platform=MockPlatformClient())
|
||||
runtime.registry = AgentRegistry(
|
||||
[
|
||||
AgentDefinition(
|
||||
agent_id="agent-17",
|
||||
label="Agent 17",
|
||||
base_url="http://lambda.coredump.ru:7000/agent_17/",
|
||||
workspace_path=str(tmp_path / "agents" / "17"),
|
||||
)
|
||||
]
|
||||
)
|
||||
await set_room_meta(
|
||||
runtime.store,
|
||||
"!chat17:example.org",
|
||||
{
|
||||
"chat_id": "C17",
|
||||
"matrix_user_id": "@alice:example.org",
|
||||
"platform_chat_id": "17",
|
||||
"agent_id": "agent-17",
|
||||
},
|
||||
)
|
||||
client = SimpleNamespace(
|
||||
user_id="@bot:example.org",
|
||||
download=AsyncMock(return_value=SimpleNamespace(body=b"%PDF-1.7")),
|
||||
)
|
||||
bot = MatrixBot(client, runtime)
|
||||
runtime.dispatcher.dispatch = AsyncMock(return_value=[])
|
||||
room = SimpleNamespace(room_id="!chat17:example.org")
|
||||
event = SimpleNamespace(
|
||||
sender="@alice:example.org",
|
||||
body="report.pdf",
|
||||
msgtype="m.file",
|
||||
replyto_event_id=None,
|
||||
url="mxc://server/id",
|
||||
mimetype="application/pdf",
|
||||
)
|
||||
|
||||
await bot.on_room_message(room, event)
|
||||
|
||||
staged = await get_staged_attachments(
|
||||
runtime.store, "!chat17:example.org", "@alice:example.org"
|
||||
)
|
||||
assert staged[0]["workspace_path"].startswith("incoming/")
|
||||
assert (
|
||||
tmp_path / "agents" / "17" / staged[0]["workspace_path"]
|
||||
).read_bytes() == b"%PDF-1.7"
|
||||
|
||||
|
||||
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.parent.mkdir(parents=True)
|
||||
output_file.write_text("ready", encoding="utf-8")
|
||||
runtime = build_runtime(platform=MockPlatformClient())
|
||||
runtime.registry = AgentRegistry(
|
||||
[
|
||||
AgentDefinition(
|
||||
agent_id="agent-17",
|
||||
label="Agent 17",
|
||||
base_url="http://lambda.coredump.ru:7000/agent_17/",
|
||||
workspace_path=str(tmp_path / "agents" / "17"),
|
||||
)
|
||||
]
|
||||
)
|
||||
await set_room_meta(
|
||||
runtime.store,
|
||||
"!chat17:example.org",
|
||||
{
|
||||
"chat_id": "C17",
|
||||
"matrix_user_id": "@alice:example.org",
|
||||
"platform_chat_id": "17",
|
||||
"agent_id": "agent-17",
|
||||
},
|
||||
)
|
||||
client = SimpleNamespace(
|
||||
user_id="@bot:example.org",
|
||||
upload=AsyncMock(return_value=(SimpleNamespace(content_uri="mxc://server/result"), {})),
|
||||
room_send=AsyncMock(),
|
||||
)
|
||||
bot = MatrixBot(client, runtime)
|
||||
runtime.dispatcher.dispatch = AsyncMock(
|
||||
return_value=[
|
||||
OutgoingMessage(
|
||||
chat_id="C17",
|
||||
text="Файл готов",
|
||||
attachments=[
|
||||
Attachment(
|
||||
type="document",
|
||||
filename="result.txt",
|
||||
mime_type="text/plain",
|
||||
workspace_path="output/result.txt",
|
||||
)
|
||||
],
|
||||
)
|
||||
]
|
||||
)
|
||||
room = SimpleNamespace(room_id="!chat17:example.org")
|
||||
event = SimpleNamespace(
|
||||
sender="@alice:example.org",
|
||||
body="сделай отчёт",
|
||||
msgtype="m.text",
|
||||
replyto_event_id=None,
|
||||
)
|
||||
|
||||
await bot.on_room_message(room, event)
|
||||
|
||||
uploaded_handle = client.upload.await_args.args[0]
|
||||
assert uploaded_handle.name == str(output_file)
|
||||
assert client.room_send.await_args_list[1].args[2]["url"] == "mxc://server/result"
|
||||
|
||||
|
||||
async def test_file_only_event_is_staged_and_does_not_dispatch():
|
||||
runtime = build_runtime(platform=MockPlatformClient())
|
||||
client = SimpleNamespace(user_id="@bot:example.org", room_send=AsyncMock())
|
||||
|
|
|
|||
|
|
@ -3,7 +3,11 @@ from __future__ import annotations
|
|||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
from adapter.matrix.files import build_workspace_attachment_path, download_matrix_attachment
|
||||
from adapter.matrix.files import (
|
||||
build_agent_incoming_path,
|
||||
build_workspace_attachment_path,
|
||||
download_matrix_attachment,
|
||||
)
|
||||
from core.protocol import Attachment
|
||||
|
||||
|
||||
|
|
@ -65,3 +69,37 @@ def test_build_workspace_attachment_path_keeps_room_safe_agents_relative_contrac
|
|||
)
|
||||
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(
|
||||
workspace_root=tmp_path / "agents" / "17",
|
||||
filename="quarterly status.pdf",
|
||||
timestamp="20260428-110000",
|
||||
)
|
||||
|
||||
assert rel_path == "incoming/20260428-110000-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):
|
||||
async def download(url: str):
|
||||
assert url == "mxc://server/id"
|
||||
return SimpleNamespace(body=b"%PDF-1.7")
|
||||
|
||||
saved = await download_matrix_attachment(
|
||||
client=SimpleNamespace(download=download),
|
||||
workspace_root=tmp_path / "agents" / "17",
|
||||
matrix_user_id="@alice:example.org",
|
||||
room_id="!room:example.org",
|
||||
attachment=Attachment(
|
||||
type="document",
|
||||
url="mxc://server/id",
|
||||
filename="report.pdf",
|
||||
mime_type="application/pdf",
|
||||
),
|
||||
timestamp="20260428-110000",
|
||||
)
|
||||
|
||||
assert saved.workspace_path == "incoming/20260428-110000-report.pdf"
|
||||
assert (tmp_path / "agents" / "17" / saved.workspace_path).read_bytes() == b"%PDF-1.7"
|
||||
|
|
|
|||
62
tests/test_deploy_handoff.py
Normal file
62
tests/test_deploy_handoff.py
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def _compose(path: str) -> dict:
|
||||
return yaml.safe_load((ROOT / path).read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def test_prod_compose_uses_registry_image_not_local_build():
|
||||
prod = _compose("docker-compose.prod.yml")
|
||||
service = prod["services"]["matrix-bot"]
|
||||
|
||||
assert "image" in service
|
||||
assert "build" not in service
|
||||
assert service["image"].startswith("${SURFACES_BOT_IMAGE:?")
|
||||
|
||||
|
||||
def test_fullstack_compose_keeps_local_dev_build_with_agent_api_context():
|
||||
fullstack = _compose("docker-compose.fullstack.yml")
|
||||
service = fullstack["services"]["matrix-bot"]
|
||||
|
||||
assert service["build"]["target"] == "development"
|
||||
assert service["build"]["additional_contexts"]["agent_api"] == "./external/platform-agent_api"
|
||||
assert service["extends"]["file"] == "docker-compose.prod.yml"
|
||||
|
||||
|
||||
def test_dockerfile_production_build_does_not_require_local_external_tree():
|
||||
dockerfile = (ROOT / "Dockerfile").read_text(encoding="utf-8")
|
||||
|
||||
assert "/app/external/platform-agent_api" not in dockerfile
|
||||
assert "external/platform-agent_api" not in dockerfile
|
||||
assert "git+https://git.lambda.coredump.ru/platform/agent_api.git" in dockerfile
|
||||
assert "python -m pip install --no-cache-dir --ignore-requires-python" in dockerfile
|
||||
assert "uv pip install --system --ignore-requires-python" not in dockerfile
|
||||
|
||||
|
||||
def test_dockerignore_excludes_local_only_and_runtime_artifacts():
|
||||
dockerignore = (ROOT / ".dockerignore").read_text(encoding="utf-8")
|
||||
|
||||
assert "external/" in dockerignore
|
||||
assert ".planning/" in dockerignore
|
||||
assert "config/matrix-agents.yaml" in dockerignore
|
||||
assert ".env" in dockerignore
|
||||
|
||||
|
||||
def test_agent_registry_example_documents_multi_agent_volume_contract():
|
||||
registry = yaml.safe_load(
|
||||
(ROOT / "config" / "matrix-agents.example.yaml").read_text(encoding="utf-8")
|
||||
)
|
||||
agents = registry["agents"]
|
||||
|
||||
assert len(agents) >= 3
|
||||
assert len({agent["id"] for agent in agents}) == len(agents)
|
||||
assert len({agent["workspace_path"] for agent in agents}) == len(agents)
|
||||
for index, agent in enumerate(agents):
|
||||
assert agent["base_url"].endswith(f"/agent_{index}/")
|
||||
assert agent["workspace_path"] == f"/agents/{index}"
|
||||
Loading…
Add table
Add a link
Reference in a new issue