Install and update artemis-use with task diagnostics and plugin guidance. Preserve cross-device file receipts, review canonical reports, guard stage transitions and reduce small history summaries. Improve exploration lifecycle, request recovery and browser live connections.
100 lines
3.2 KiB
Python
100 lines
3.2 KiB
Python
"""Local protocol fixtures; no devices, model calls or user data."""
|
|
|
|
import asyncio
|
|
from contextlib import asynccontextmanager
|
|
import os
|
|
import socket
|
|
import tempfile
|
|
import threading
|
|
import time
|
|
|
|
from fastapi import FastAPI, HTTPException, Request
|
|
import pytest
|
|
import uvicorn
|
|
|
|
from artemis.service_registry import ServiceRegistration, TOKEN_HEADER
|
|
from mcp_server.shared import LocalMcpBoundary, create_shared_mcp
|
|
|
|
|
|
@pytest.fixture
|
|
def shared_mcp_backend(tmp_path, monkeypatch):
|
|
monkeypatch.setattr(tempfile, "tempdir", str(tmp_path))
|
|
monkeypatch.setenv("TEMP", str(tmp_path))
|
|
monkeypatch.setenv("TMP", str(tmp_path))
|
|
monkeypatch.setenv("TMPDIR", str(tmp_path))
|
|
monkeypatch.setenv("ARTEMIS_APP_DIR", str(tmp_path / "app"))
|
|
monkeypatch.setenv("ARTEMIS_TRACES_DIR", str(tmp_path / "traces"))
|
|
monkeypatch.delenv("ARTEMIS_DAEMON_PORT", raising=False)
|
|
monkeypatch.delenv("ARTEMIS_DAEMON_HOST", raising=False)
|
|
listener = socket.socket()
|
|
listener.bind(("127.0.0.1", 0))
|
|
port = listener.getsockname()[1]
|
|
registration = ServiceRegistration(
|
|
port=port, token="fixture-token", app_dir=tmp_path / "app", traces_dir=tmp_path / "traces"
|
|
)
|
|
shared, executor = create_shared_mcp()
|
|
calls = []
|
|
|
|
async def record(delay: float):
|
|
calls.append(delay)
|
|
await asyncio.sleep(delay)
|
|
return {"pid": os.getpid(), "thread": threading.get_ident(), "count": len(calls)}
|
|
|
|
@shared.tool()
|
|
async def shared_probe(delay: float = 0.0) -> dict[str, int]:
|
|
return await executor.invoke(record, delay)
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app):
|
|
registration.acquire()
|
|
executor.start()
|
|
try:
|
|
async with shared.session_manager.run():
|
|
yield
|
|
finally:
|
|
await executor.stop()
|
|
registration.close()
|
|
|
|
app = FastAPI(lifespan=lifespan)
|
|
app.mount("/api/mcp", LocalMcpBoundary(shared.streamable_http_app(), "fixture-token"))
|
|
|
|
@app.get("/api/system/shared-service")
|
|
async def identity(request: Request):
|
|
if request.headers.get(TOKEN_HEADER) != "fixture-token":
|
|
raise HTTPException(403)
|
|
return {
|
|
key: registration.info[key] for key in ("protocol", "installation", "instance", "pid")
|
|
}
|
|
|
|
server = uvicorn.Server(
|
|
uvicorn.Config(
|
|
app,
|
|
host="127.0.0.1",
|
|
port=port,
|
|
log_level="error",
|
|
timeout_graceful_shutdown=5, # Match the user-started service's SSE shutdown budget.
|
|
)
|
|
)
|
|
thread = threading.Thread(target=server.run, kwargs={"sockets": [listener]}, daemon=True)
|
|
thread.start()
|
|
deadline = time.monotonic() + 8
|
|
while not server.started and thread.is_alive() and time.monotonic() < deadline:
|
|
time.sleep(0.01)
|
|
assert server.started
|
|
try:
|
|
yield {
|
|
"port": port,
|
|
"registration": registration,
|
|
"server": server,
|
|
"calls": calls,
|
|
"env": {
|
|
key: os.environ[key]
|
|
for key in ("TEMP", "TMP", "TMPDIR", "ARTEMIS_APP_DIR", "ARTEMIS_TRACES_DIR")
|
|
},
|
|
}
|
|
finally:
|
|
server.should_exit = True
|
|
thread.join(timeout=8)
|
|
listener.close()
|
|
assert not thread.is_alive()
|