Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,322 @@
|
||||
"""Tests for the Raft channel adapter."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from aiohttp import web
|
||||
from aiohttp.test_utils import TestClient, TestServer
|
||||
|
||||
from gateway.config import Platform, PlatformConfig
|
||||
from plugins.platforms.raft.adapter import (
|
||||
ACTIVITY_DRAIN_SCHEMA,
|
||||
ACTIVITY_EVENT_SCHEMA,
|
||||
ActivityQueue,
|
||||
BRIDGE_TOKEN_HEADER,
|
||||
DEFAULT_PATH,
|
||||
RaftAdapter,
|
||||
_ACTIVE_ADAPTERS,
|
||||
_ACTIVE_ADAPTERS_LOCK,
|
||||
_RAFT_CONTEXT_LOCK,
|
||||
_RAFT_PROMPT_TURN_IDS,
|
||||
_RAFT_SESSION_IDS,
|
||||
_RAFT_TURN_IDS,
|
||||
_has_content_field,
|
||||
_env_enablement,
|
||||
_is_connected,
|
||||
_on_session_start,
|
||||
_on_pre_llm_call,
|
||||
_on_pre_tool_call,
|
||||
_on_post_llm_call,
|
||||
_on_post_tool_call,
|
||||
_on_session_end,
|
||||
_on_session_finalize,
|
||||
check_raft_requirements,
|
||||
interactive_setup,
|
||||
register,
|
||||
)
|
||||
from gateway.session import build_session_key
|
||||
|
||||
RAFT_CHANNEL_SCHEMA = "raft-channel-wake.v1"
|
||||
FUTURE_RAFT_CHANNEL_SCHEMA = "raft-channel-wake.v2"
|
||||
|
||||
|
||||
def _make_config(**extra):
|
||||
data = {
|
||||
"bridge_token": "bridge-secret",
|
||||
"runtime_session": "default",
|
||||
"port": 0,
|
||||
}
|
||||
data.update(extra)
|
||||
return PlatformConfig(enabled=True, extra=data)
|
||||
|
||||
|
||||
def _make_adapter(**extra):
|
||||
return RaftAdapter(_make_config(**extra))
|
||||
|
||||
|
||||
def _create_app(adapter: RaftAdapter) -> web.Application:
|
||||
# Mirror connect(): client_max_size enforces the cap on chunked bodies.
|
||||
app = web.Application(client_max_size=adapter._max_body_bytes)
|
||||
app.router.add_get("/health", adapter._handle_health)
|
||||
app.router.add_post(adapter._path, adapter._handle_wake)
|
||||
app.router.add_post("/activity", adapter._handle_activity)
|
||||
app.router.add_get("/activity/drain", adapter._handle_activity_drain)
|
||||
return app
|
||||
|
||||
|
||||
def _activity_event(event_id: str, **overrides):
|
||||
event = {
|
||||
"schema": ACTIVITY_EVENT_SCHEMA,
|
||||
"eventId": event_id,
|
||||
"sessionId": "session-1",
|
||||
"hookEventName": "PreToolUse",
|
||||
"status": "ok",
|
||||
"occurredAt": "2026-06-16T06:00:00Z",
|
||||
"toolName": "execute_code",
|
||||
}
|
||||
event.update(overrides)
|
||||
return event
|
||||
|
||||
|
||||
class TestRaftWakePayload:
|
||||
def test_detects_content_fields(self):
|
||||
assert _has_content_field({"text": "hello"}) is True
|
||||
assert _has_content_field({"nested": {"messages": []}}) is True
|
||||
assert _has_content_field({"eventId": "evt-1", "messageId": "msg-1"}) is False
|
||||
|
||||
|
||||
class TestRaftWakeHttp:
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_is_noop_success(self):
|
||||
adapter = _make_adapter()
|
||||
|
||||
result = await adapter.send("default", "hello")
|
||||
|
||||
assert result.success is True
|
||||
assert result.message_id is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rejects_content_bearing_payload(self):
|
||||
adapter = _make_adapter()
|
||||
adapter.set_message_handler(AsyncMock())
|
||||
adapter.handle_message = AsyncMock()
|
||||
|
||||
app = _create_app(adapter)
|
||||
async with TestClient(TestServer(app)) as client:
|
||||
resp = await client.post(
|
||||
DEFAULT_PATH,
|
||||
json={"eventId": "wake-1", "text": "do work"},
|
||||
headers={BRIDGE_TOKEN_HEADER: "bridge-secret"},
|
||||
)
|
||||
assert resp.status == 400
|
||||
body = await resp.json()
|
||||
|
||||
assert body == {"ok": False, "error": "content_not_allowed"}
|
||||
adapter.handle_message.assert_not_called()
|
||||
|
||||
|
||||
class TestRaftActivityHttp:
|
||||
@pytest.mark.asyncio
|
||||
async def test_activity_endpoint_auth_validation_and_drain(self):
|
||||
adapter = _make_adapter()
|
||||
adapter._activity_queue = ActivityQueue(cap=2)
|
||||
|
||||
app = _create_app(adapter)
|
||||
async with TestClient(TestServer(app)) as client:
|
||||
unauthorized = await client.post("/activity", json=_activity_event("evt-1"))
|
||||
assert unauthorized.status == 401
|
||||
|
||||
unknown = await client.post(
|
||||
"/activity",
|
||||
json={**_activity_event("evt-1"), "transcript_path": "/tmp/session.jsonl"},
|
||||
headers={BRIDGE_TOKEN_HEADER: "bridge-secret"},
|
||||
)
|
||||
assert unknown.status == 400
|
||||
|
||||
for event_id in ["evt-1", "evt-2", "evt-3"]:
|
||||
resp = await client.post(
|
||||
"/activity",
|
||||
json=_activity_event(event_id),
|
||||
headers={BRIDGE_TOKEN_HEADER: "bridge-secret"},
|
||||
)
|
||||
assert resp.status == 202
|
||||
|
||||
drain = await client.get(
|
||||
"/activity/drain?max=10",
|
||||
headers={BRIDGE_TOKEN_HEADER: "bridge-secret"},
|
||||
)
|
||||
assert drain.status == 200
|
||||
body = await drain.json()
|
||||
|
||||
assert body["schema"] == ACTIVITY_DRAIN_SCHEMA
|
||||
assert body["dropped"] == 1
|
||||
assert [event["eventId"] for event in body["events"]] == ["evt-2", "evt-3"]
|
||||
|
||||
|
||||
class TestBodySize:
|
||||
"""The wake/activity endpoints enforced max_body_bytes only via the
|
||||
Content-Length header; a Transfer-Encoding: chunked request
|
||||
(content_length=None) bypassed the cap entirely and read the full body,
|
||||
bounded only by aiohttp's implicit 1 MiB default. Mirrors
|
||||
gateway/platforms/webhook.py's TestBodySize."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wake_chunked_oversized_payload_rejected(self):
|
||||
adapter = _make_adapter(max_body_bytes=100)
|
||||
adapter.set_message_handler(AsyncMock())
|
||||
adapter.handle_message = AsyncMock()
|
||||
|
||||
async def _chunked_body():
|
||||
payload = json.dumps({"eventId": "x" * 500}).encode("utf-8")
|
||||
for i in range(0, len(payload), 64):
|
||||
yield payload[i : i + 64]
|
||||
await asyncio.sleep(0)
|
||||
|
||||
app = _create_app(adapter)
|
||||
async with TestClient(TestServer(app)) as client:
|
||||
resp = await client.post(
|
||||
DEFAULT_PATH,
|
||||
data=_chunked_body(),
|
||||
headers={
|
||||
BRIDGE_TOKEN_HEADER: "bridge-secret",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
)
|
||||
assert resp.status == 413
|
||||
body = await resp.json()
|
||||
|
||||
assert body == {"ok": False, "error": "payload_too_large"}
|
||||
adapter.handle_message.assert_not_awaited()
|
||||
|
||||
|
||||
class TestRaftConfig:
|
||||
def test_env_enablement_auto_enables_with_raft_profile(self, monkeypatch):
|
||||
monkeypatch.setenv("RAFT_PROFILE", "my-agent")
|
||||
|
||||
extra = _env_enablement()
|
||||
|
||||
assert extra is not None
|
||||
assert extra["enabled"] is True
|
||||
|
||||
|
||||
def test_interactive_setup_keeps_existing_profile_when_not_reconfigured(
|
||||
self, monkeypatch, tmp_path, capsys
|
||||
):
|
||||
env_path = tmp_path / ".env"
|
||||
env_path.write_text("RAFT_PROFILE=existing\n", encoding="utf-8")
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
monkeypatch.setenv("RAFT_PROFILE", "existing")
|
||||
monkeypatch.setattr("builtins.input", lambda _prompt: "n")
|
||||
|
||||
interactive_setup()
|
||||
|
||||
assert env_path.read_text(encoding="utf-8") == "RAFT_PROFILE=existing\n"
|
||||
assert os.environ["RAFT_PROFILE"] == "existing"
|
||||
assert "Keeping RAFT_PROFILE=existing" in capsys.readouterr().out
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Multiplex secondary-profile scope (RAFT_PROFILE resolution)
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# _spawn_bridge, _env_enablement, and register()'s platform_hint all
|
||||
# previously read RAFT_PROFILE via raw os.environ.get unconditionally. Under
|
||||
# a multiplexed secondary profile, os.environ holds the DEFAULT profile's
|
||||
# YAML-to-env bridge output — a secondary profile with its own RAFT_PROFILE
|
||||
# (set only in its own .env, resolved via the installed secret scope) would
|
||||
# silently connect the bridge subprocess / CLI hint to the default profile's
|
||||
# external Raft workspace/agent identity instead of its own. Mirrors the
|
||||
# Buzz/SimpleX fix for #98738.
|
||||
|
||||
@pytest.fixture
|
||||
def multiplex_scope():
|
||||
"""Install multiplex + a secondary-profile secret scope; restore after."""
|
||||
tokens = []
|
||||
|
||||
def install(scope=None):
|
||||
from agent.secret_scope import set_multiplex_active, set_secret_scope
|
||||
|
||||
set_multiplex_active(True)
|
||||
tokens.append(set_secret_scope(scope or {}))
|
||||
return tokens[-1]
|
||||
|
||||
yield install
|
||||
|
||||
from agent.secret_scope import reset_secret_scope, set_multiplex_active
|
||||
|
||||
for token in reversed(tokens):
|
||||
reset_secret_scope(token)
|
||||
set_multiplex_active(False)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def default_profile_env(monkeypatch):
|
||||
"""The default profile's YAML-to-env bridge output in os.environ."""
|
||||
monkeypatch.setenv("RAFT_PROFILE", "default-profile-slug")
|
||||
|
||||
|
||||
class _FakeCtx:
|
||||
"""Minimal ``ctx`` capturing ``register_platform``'s kwargs."""
|
||||
|
||||
def __init__(self):
|
||||
self.platform_kwargs = None
|
||||
|
||||
def register_platform(self, **kwargs):
|
||||
self.platform_kwargs = kwargs
|
||||
|
||||
def register_hook(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class TestMultiplexProfileScope:
|
||||
|
||||
def test_secondary_profile_uses_its_own_slug_and_never_borrows_default(
|
||||
self, multiplex_scope, default_profile_env, monkeypatch
|
||||
):
|
||||
"""Bridge spawn, env-enablement and the register() hint all resolve
|
||||
the secondary profile's own RAFT_PROFILE; with none of its own the
|
||||
profile fails closed (bridge not spawned, not auto-enabled)."""
|
||||
import plugins.platforms.raft.adapter as raft_mod
|
||||
|
||||
monkeypatch.setattr(raft_mod.shutil, "which", lambda name: "/usr/bin/raft")
|
||||
spawned = []
|
||||
monkeypatch.setattr(
|
||||
raft_mod.subprocess, "Popen",
|
||||
lambda cmd, **kwargs: spawned.append(cmd) or SimpleNamespace(pid=1),
|
||||
)
|
||||
|
||||
multiplex_scope({"RAFT_PROFILE": "secondary-profile-slug"})
|
||||
_make_adapter()._spawn_bridge(9999)
|
||||
assert spawned[-1][:3] == ["/usr/bin/raft", "--profile", "secondary-profile-slug"]
|
||||
assert _env_enablement() == {"enabled": True}
|
||||
ctx = _FakeCtx()
|
||||
register(ctx)
|
||||
assert "--profile secondary-profile-slug" in ctx.platform_kwargs["platform_hint"]
|
||||
assert "default-profile-slug" not in ctx.platform_kwargs["platform_hint"]
|
||||
|
||||
spawned.clear()
|
||||
multiplex_scope({})
|
||||
_make_adapter()._spawn_bridge(9999)
|
||||
assert spawned == []
|
||||
assert _env_enablement() is None
|
||||
|
||||
def test_default_profile_unscoped_keeps_env_precedence(
|
||||
self, monkeypatch, default_profile_env
|
||||
):
|
||||
"""Multiplex ON but no scope (the DEFAULT profile constructs
|
||||
unscoped): env is its own bridge output and still wins."""
|
||||
from agent.secret_scope import set_multiplex_active
|
||||
|
||||
set_multiplex_active(True)
|
||||
try:
|
||||
assert _env_enablement() == {"enabled": True}
|
||||
ctx = _FakeCtx()
|
||||
register(ctx)
|
||||
assert "--profile default-profile-slug" in ctx.platform_kwargs["platform_hint"]
|
||||
finally:
|
||||
set_multiplex_active(False)
|
||||
Reference in New Issue
Block a user