feat: config-driven architecture, install wizard, live runtime switching, usage tracking, auto-failover
Major changes: - Config-driven adapters: all channels (Slack, Discord, Telegram, WebChat, Webhooks) controlled via config.json with enabled flags and token auto-detection, no CLI flags required - Runtime engine field: runtime.engine selects opencode/claude from config - Interactive install script: 8-phase setup wizard with AI runtime detection/installation, token setup, identity file personalization (personality presets), aetheel CLI command, background service (launchd/systemd) - Live runtime switching: /engine, /model, /provider commands hot-swap the AI runtime from chat without restart, changes persisted to config.json - Usage tracking: per-request cost extraction from Claude Code JSON output, cumulative stats via /usage command - Auto-failover: rate limit detection on both runtimes, automatic switch to other engine on quota errors with user notification - Chat commands work without / prefix (Slack intercepts / in channels), commands: engine, model, provider, config, usage, reload, cron, subagents, status, help - /config set for editing config.json from chat with dotted key notation - Security audit saved to docs/security-audit.md - Full command reference in docs/commands.md - Future changes doc with NanoClaw agent teams analysis - Logo added to README and WebChat UI - README fully rewritten with all features documented
This commit is contained in:
172
tests/test_hooks.py
Normal file
172
tests/test_hooks.py
Normal file
@@ -0,0 +1,172 @@
|
||||
"""
|
||||
Tests for the Hook system (HookManager, HookEvent).
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from hooks.hooks import HookEvent, HookEntry, HookManager
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def workspace(tmp_path):
|
||||
return str(tmp_path)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def manager(workspace):
|
||||
return HookManager(workspace_dir=workspace)
|
||||
|
||||
|
||||
def _create_hook(workspace, name, events, enabled=True, handler_code=None):
|
||||
"""Helper to create a hook directory with HOOK.md and optional handler.py."""
|
||||
hook_dir = os.path.join(workspace, "hooks", name)
|
||||
os.makedirs(hook_dir, exist_ok=True)
|
||||
|
||||
enabled_str = "true" if enabled else "false"
|
||||
events_str = "[" + ", ".join(events) + "]"
|
||||
hook_md = f"""---
|
||||
name: {name}
|
||||
description: Test hook {name}
|
||||
events: {events_str}
|
||||
enabled: {enabled_str}
|
||||
---
|
||||
# {name}
|
||||
Test hook documentation.
|
||||
"""
|
||||
with open(os.path.join(hook_dir, "HOOK.md"), "w") as f:
|
||||
f.write(hook_md)
|
||||
|
||||
if handler_code:
|
||||
with open(os.path.join(hook_dir, "handler.py"), "w") as f:
|
||||
f.write(handler_code)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: HookEvent
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestHookEvent:
|
||||
def test_event_key(self):
|
||||
event = HookEvent(type="gateway", action="startup")
|
||||
assert event.event_key == "gateway:startup"
|
||||
|
||||
def test_messages_default_empty(self):
|
||||
event = HookEvent(type="command", action="new")
|
||||
assert event.messages == []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: HookManager discovery
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestHookManagerDiscovery:
|
||||
def test_discover_empty_workspace(self, manager):
|
||||
hooks = manager.discover()
|
||||
assert hooks == []
|
||||
|
||||
def test_discover_finds_hooks(self, workspace, manager):
|
||||
_create_hook(workspace, "my-hook", ["gateway:startup"])
|
||||
hooks = manager.discover()
|
||||
assert len(hooks) == 1
|
||||
assert hooks[0].name == "my-hook"
|
||||
assert hooks[0].events == ["gateway:startup"]
|
||||
|
||||
def test_discover_skips_disabled(self, workspace, manager):
|
||||
_create_hook(workspace, "disabled-hook", ["gateway:startup"], enabled=False)
|
||||
hooks = manager.discover()
|
||||
assert len(hooks) == 0
|
||||
|
||||
def test_discover_multiple_hooks(self, workspace, manager):
|
||||
_create_hook(workspace, "hook-a", ["gateway:startup"])
|
||||
_create_hook(workspace, "hook-b", ["command:new"])
|
||||
hooks = manager.discover()
|
||||
assert len(hooks) == 2
|
||||
|
||||
def test_discover_multiple_events(self, workspace, manager):
|
||||
_create_hook(workspace, "multi", ["gateway:startup", "command:new"])
|
||||
hooks = manager.discover()
|
||||
assert hooks[0].events == ["gateway:startup", "command:new"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: HookManager trigger
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestHookManagerTrigger:
|
||||
def test_trigger_calls_handler(self, workspace, manager):
|
||||
handler_code = """
|
||||
results = []
|
||||
def handle(event):
|
||||
results.append(event.event_key)
|
||||
"""
|
||||
_create_hook(workspace, "test-hook", ["gateway:startup"], handler_code=handler_code)
|
||||
manager.discover()
|
||||
event = HookEvent(type="gateway", action="startup")
|
||||
manager.trigger(event)
|
||||
# Handler was called without error — that's the test
|
||||
|
||||
def test_trigger_no_matching_hooks(self, manager):
|
||||
event = HookEvent(type="gateway", action="startup")
|
||||
messages = manager.trigger(event)
|
||||
assert messages == []
|
||||
|
||||
def test_trigger_handler_error_doesnt_crash(self, workspace, manager):
|
||||
handler_code = """
|
||||
def handle(event):
|
||||
raise RuntimeError("boom")
|
||||
"""
|
||||
_create_hook(workspace, "bad-hook", ["gateway:startup"], handler_code=handler_code)
|
||||
manager.discover()
|
||||
event = HookEvent(type="gateway", action="startup")
|
||||
# Should not raise
|
||||
manager.trigger(event)
|
||||
|
||||
def test_trigger_messages(self, workspace, manager):
|
||||
handler_code = """
|
||||
def handle(event):
|
||||
event.messages.append("hello from hook")
|
||||
"""
|
||||
_create_hook(workspace, "msg-hook", ["gateway:startup"], handler_code=handler_code)
|
||||
manager.discover()
|
||||
event = HookEvent(type="gateway", action="startup")
|
||||
messages = manager.trigger(event)
|
||||
assert "hello from hook" in messages
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: Programmatic hooks
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestProgrammaticHooks:
|
||||
def test_register_and_trigger(self, manager):
|
||||
received = []
|
||||
manager.register("gateway:startup", lambda e: received.append(e.event_key))
|
||||
event = HookEvent(type="gateway", action="startup")
|
||||
manager.trigger(event)
|
||||
assert received == ["gateway:startup"]
|
||||
|
||||
def test_unregister(self, manager):
|
||||
received = []
|
||||
handler = lambda e: received.append(e.event_key)
|
||||
manager.register("gateway:startup", handler)
|
||||
manager.unregister("gateway:startup", handler)
|
||||
manager.trigger(HookEvent(type="gateway", action="startup"))
|
||||
assert received == []
|
||||
|
||||
def test_type_level_handler(self, manager):
|
||||
received = []
|
||||
manager.register("gateway", lambda e: received.append(e.action))
|
||||
manager.trigger(HookEvent(type="gateway", action="startup"))
|
||||
manager.trigger(HookEvent(type="gateway", action="shutdown"))
|
||||
assert received == ["startup", "shutdown"]
|
||||
134
tests/test_mcp_config.py
Normal file
134
tests/test_mcp_config.py
Normal file
@@ -0,0 +1,134 @@
|
||||
"""
|
||||
Tests for the MCP config writer utility (write_mcp_config).
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from config import MCPConfig, MCPServerConfig, write_mcp_config
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def workspace(tmp_path):
|
||||
"""Return a temporary workspace directory path."""
|
||||
return str(tmp_path)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_mcp_config():
|
||||
"""Return an MCPConfig with one server entry."""
|
||||
return MCPConfig(
|
||||
servers={
|
||||
"my-tool": MCPServerConfig(
|
||||
command="npx",
|
||||
args=["-y", "@my/tool"],
|
||||
env={"API_KEY": "test123"},
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: write_mcp_config
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestWriteMcpConfig:
|
||||
def test_skips_when_no_servers(self, workspace):
|
||||
"""Empty servers dict should not create any file."""
|
||||
write_mcp_config(MCPConfig(), workspace, use_claude=True)
|
||||
assert not os.path.exists(os.path.join(workspace, ".mcp.json"))
|
||||
assert not os.path.exists(os.path.join(workspace, "opencode.json"))
|
||||
|
||||
def test_writes_mcp_json_for_claude(self, workspace, sample_mcp_config):
|
||||
"""Claude runtime should produce .mcp.json with 'mcpServers' key."""
|
||||
write_mcp_config(sample_mcp_config, workspace, use_claude=True)
|
||||
path = os.path.join(workspace, ".mcp.json")
|
||||
assert os.path.isfile(path)
|
||||
|
||||
with open(path, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
assert "mcpServers" in data
|
||||
assert "my-tool" in data["mcpServers"]
|
||||
srv = data["mcpServers"]["my-tool"]
|
||||
assert srv["command"] == "npx"
|
||||
assert srv["args"] == ["-y", "@my/tool"]
|
||||
assert srv["env"] == {"API_KEY": "test123"}
|
||||
|
||||
def test_writes_opencode_json_for_opencode(self, workspace, sample_mcp_config):
|
||||
"""OpenCode runtime should produce opencode.json with 'mcp' key."""
|
||||
write_mcp_config(sample_mcp_config, workspace, use_claude=False)
|
||||
path = os.path.join(workspace, "opencode.json")
|
||||
assert os.path.isfile(path)
|
||||
|
||||
with open(path, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
assert "mcp" in data
|
||||
assert "my-tool" in data["mcp"]
|
||||
|
||||
def test_multiple_servers(self, workspace):
|
||||
"""Multiple server entries should all appear in the output."""
|
||||
cfg = MCPConfig(
|
||||
servers={
|
||||
"server-a": MCPServerConfig(command="cmd-a", args=["--flag"]),
|
||||
"server-b": MCPServerConfig(command="cmd-b", env={"X": "1"}),
|
||||
}
|
||||
)
|
||||
write_mcp_config(cfg, workspace, use_claude=True)
|
||||
|
||||
with open(os.path.join(workspace, ".mcp.json"), encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
assert len(data["mcpServers"]) == 2
|
||||
assert "server-a" in data["mcpServers"]
|
||||
assert "server-b" in data["mcpServers"]
|
||||
|
||||
def test_creates_parent_directories(self, tmp_path):
|
||||
"""Should create intermediate directories if workspace doesn't exist."""
|
||||
nested = str(tmp_path / "deep" / "nested" / "workspace")
|
||||
cfg = MCPConfig(
|
||||
servers={"s": MCPServerConfig(command="echo")}
|
||||
)
|
||||
write_mcp_config(cfg, nested, use_claude=True)
|
||||
assert os.path.isfile(os.path.join(nested, ".mcp.json"))
|
||||
|
||||
def test_produces_valid_json(self, workspace, sample_mcp_config):
|
||||
"""Output file must be valid, parseable JSON."""
|
||||
write_mcp_config(sample_mcp_config, workspace, use_claude=True)
|
||||
path = os.path.join(workspace, ".mcp.json")
|
||||
with open(path, encoding="utf-8") as f:
|
||||
data = json.load(f) # Would raise on invalid JSON
|
||||
assert isinstance(data, dict)
|
||||
|
||||
def test_overwrites_existing_file(self, workspace):
|
||||
"""Writing twice should overwrite the previous config."""
|
||||
cfg1 = MCPConfig(servers={"old": MCPServerConfig(command="old-cmd")})
|
||||
cfg2 = MCPConfig(servers={"new": MCPServerConfig(command="new-cmd")})
|
||||
|
||||
write_mcp_config(cfg1, workspace, use_claude=True)
|
||||
write_mcp_config(cfg2, workspace, use_claude=True)
|
||||
|
||||
with open(os.path.join(workspace, ".mcp.json"), encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
assert "new" in data["mcpServers"]
|
||||
assert "old" not in data["mcpServers"]
|
||||
|
||||
def test_tilde_expansion(self, monkeypatch, tmp_path):
|
||||
"""Workspace path with ~ should be expanded."""
|
||||
monkeypatch.setenv("HOME", str(tmp_path))
|
||||
# Also handle Windows
|
||||
monkeypatch.setattr(os.path, "expanduser", lambda p: p.replace("~", str(tmp_path)))
|
||||
|
||||
cfg = MCPConfig(servers={"s": MCPServerConfig(command="echo")})
|
||||
write_mcp_config(cfg, "~/my-workspace", use_claude=False)
|
||||
assert os.path.isfile(os.path.join(str(tmp_path), "my-workspace", "opencode.json"))
|
||||
122
tests/test_subagent_bus.py
Normal file
122
tests/test_subagent_bus.py
Normal file
@@ -0,0 +1,122 @@
|
||||
"""
|
||||
Tests for SubagentBus pub/sub message bus.
|
||||
"""
|
||||
|
||||
import threading
|
||||
|
||||
import pytest
|
||||
|
||||
from agent.subagent import SubagentBus, SubagentManager
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bus():
|
||||
return SubagentBus()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: SubagentBus
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestSubagentBus:
|
||||
def test_subscribe_and_publish(self, bus):
|
||||
received = []
|
||||
bus.subscribe("ch1", lambda msg, sender: received.append((msg, sender)))
|
||||
bus.publish("ch1", "hello", "agent-1")
|
||||
assert received == [("hello", "agent-1")]
|
||||
|
||||
def test_multiple_subscribers(self, bus):
|
||||
received = []
|
||||
bus.subscribe("ch1", lambda msg, sender: received.append(("a", msg)))
|
||||
bus.subscribe("ch1", lambda msg, sender: received.append(("b", msg)))
|
||||
bus.publish("ch1", "ping", "sender")
|
||||
assert len(received) == 2
|
||||
assert ("a", "ping") in received
|
||||
assert ("b", "ping") in received
|
||||
|
||||
def test_publish_to_empty_channel(self, bus):
|
||||
# Should not raise
|
||||
bus.publish("nonexistent", "msg", "sender")
|
||||
|
||||
def test_unsubscribe_all(self, bus):
|
||||
received = []
|
||||
bus.subscribe("ch1", lambda msg, sender: received.append(msg))
|
||||
bus.unsubscribe_all("ch1")
|
||||
bus.publish("ch1", "hello", "sender")
|
||||
assert received == []
|
||||
|
||||
def test_unsubscribe_all_nonexistent_channel(self, bus):
|
||||
# Should not raise
|
||||
bus.unsubscribe_all("nonexistent")
|
||||
|
||||
def test_channels_are_isolated(self, bus):
|
||||
received_a = []
|
||||
received_b = []
|
||||
bus.subscribe("a", lambda msg, sender: received_a.append(msg))
|
||||
bus.subscribe("b", lambda msg, sender: received_b.append(msg))
|
||||
bus.publish("a", "only-a", "sender")
|
||||
assert received_a == ["only-a"]
|
||||
assert received_b == []
|
||||
|
||||
def test_callback_error_does_not_stop_others(self, bus):
|
||||
received = []
|
||||
|
||||
def bad_cb(msg, sender):
|
||||
raise RuntimeError("boom")
|
||||
|
||||
bus.subscribe("ch1", bad_cb)
|
||||
bus.subscribe("ch1", lambda msg, sender: received.append(msg))
|
||||
bus.publish("ch1", "hello", "sender")
|
||||
# Second callback should still fire
|
||||
assert received == ["hello"]
|
||||
|
||||
def test_thread_safety(self, bus):
|
||||
"""Concurrent subscribes and publishes should not crash."""
|
||||
results = []
|
||||
barrier = threading.Barrier(4)
|
||||
|
||||
def subscriber():
|
||||
barrier.wait()
|
||||
bus.subscribe("stress", lambda msg, sender: results.append(msg))
|
||||
|
||||
def publisher():
|
||||
barrier.wait()
|
||||
bus.publish("stress", "msg", "sender")
|
||||
|
||||
threads = [
|
||||
threading.Thread(target=subscriber),
|
||||
threading.Thread(target=subscriber),
|
||||
threading.Thread(target=publisher),
|
||||
threading.Thread(target=publisher),
|
||||
]
|
||||
for t in threads:
|
||||
t.start()
|
||||
for t in threads:
|
||||
t.join(timeout=5)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: SubagentManager.bus property
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestSubagentManagerBus:
|
||||
def test_manager_has_bus(self):
|
||||
mgr = SubagentManager(
|
||||
runtime_factory=lambda: None,
|
||||
send_fn=lambda *a: None,
|
||||
)
|
||||
assert isinstance(mgr.bus, SubagentBus)
|
||||
|
||||
def test_bus_is_same_instance(self):
|
||||
mgr = SubagentManager(
|
||||
runtime_factory=lambda: None,
|
||||
send_fn=lambda *a: None,
|
||||
)
|
||||
assert mgr.bus is mgr.bus
|
||||
171
tests/test_webhooks.py
Normal file
171
tests/test_webhooks.py
Normal file
@@ -0,0 +1,171 @@
|
||||
"""
|
||||
Tests for the Webhook receiver.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from aiohttp.test_utils import TestClient, TestServer
|
||||
|
||||
from webhooks.receiver import WebhookReceiver, WebhookConfig
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _make_handler(response_text="AI response"):
|
||||
def handler(msg):
|
||||
return response_text
|
||||
return handler
|
||||
|
||||
|
||||
def _make_send_fn():
|
||||
calls = []
|
||||
def send(channel_id, text, thread_id, channel_type):
|
||||
calls.append({"channel_id": channel_id, "text": text, "channel_type": channel_type})
|
||||
send.calls = calls
|
||||
return send
|
||||
|
||||
|
||||
def _make_receiver(token="", handler=None, send_fn=None):
|
||||
config = WebhookConfig(enabled=True, port=0, host="127.0.0.1", token=token)
|
||||
return WebhookReceiver(
|
||||
ai_handler_fn=handler or _make_handler(),
|
||||
send_fn=send_fn or _make_send_fn(),
|
||||
config=config,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: Health endpoint
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_endpoint():
|
||||
receiver = _make_receiver()
|
||||
async with TestClient(TestServer(receiver._app)) as client:
|
||||
resp = await client.get("/hooks/health")
|
||||
assert resp.status == 200
|
||||
data = await resp.json()
|
||||
assert data["status"] == "ok"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: Wake endpoint
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wake_no_auth():
|
||||
receiver = _make_receiver()
|
||||
async with TestClient(TestServer(receiver._app)) as client:
|
||||
resp = await client.post("/hooks/wake", json={"text": "hello"})
|
||||
assert resp.status == 200
|
||||
data = await resp.json()
|
||||
assert data["response"] == "AI response"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wake_auth_required():
|
||||
receiver = _make_receiver(token="secret")
|
||||
async with TestClient(TestServer(receiver._app)) as client:
|
||||
# No auth → 401
|
||||
resp = await client.post("/hooks/wake", json={"text": "hello"})
|
||||
assert resp.status == 401
|
||||
|
||||
# With auth → 200
|
||||
resp = await client.post(
|
||||
"/hooks/wake",
|
||||
json={"text": "hello"},
|
||||
headers={"Authorization": "Bearer secret"},
|
||||
)
|
||||
assert resp.status == 200
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wake_empty_text():
|
||||
receiver = _make_receiver()
|
||||
async with TestClient(TestServer(receiver._app)) as client:
|
||||
resp = await client.post("/hooks/wake", json={"text": ""})
|
||||
assert resp.status == 400
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wake_invalid_json():
|
||||
receiver = _make_receiver()
|
||||
async with TestClient(TestServer(receiver._app)) as client:
|
||||
resp = await client.post(
|
||||
"/hooks/wake",
|
||||
data="not json",
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
assert resp.status == 400
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wake_delivers_to_channel():
|
||||
send_fn = _make_send_fn()
|
||||
receiver = _make_receiver(handler=_make_handler("response"), send_fn=send_fn)
|
||||
async with TestClient(TestServer(receiver._app)) as client:
|
||||
resp = await client.post("/hooks/wake", json={
|
||||
"text": "hello",
|
||||
"channel": "slack",
|
||||
"channel_id": "C123",
|
||||
})
|
||||
assert resp.status == 200
|
||||
assert len(send_fn.calls) == 1
|
||||
assert send_fn.calls[0]["channel_id"] == "C123"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: Agent endpoint
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_agent_endpoint():
|
||||
send_fn = _make_send_fn()
|
||||
receiver = _make_receiver(send_fn=send_fn)
|
||||
async with TestClient(TestServer(receiver._app)) as client:
|
||||
resp = await client.post("/hooks/agent", json={
|
||||
"message": "research something",
|
||||
"channel": "slack",
|
||||
"channel_id": "C456",
|
||||
})
|
||||
assert resp.status == 200
|
||||
assert len(send_fn.calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_agent_empty_message():
|
||||
receiver = _make_receiver()
|
||||
async with TestClient(TestServer(receiver._app)) as client:
|
||||
resp = await client.post("/hooks/agent", json={"message": ""})
|
||||
assert resp.status == 400
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_agent_auth_bearer():
|
||||
receiver = _make_receiver(token="secret")
|
||||
async with TestClient(TestServer(receiver._app)) as client:
|
||||
resp = await client.post(
|
||||
"/hooks/agent",
|
||||
json={"message": "test"},
|
||||
headers={"Authorization": "Bearer secret"},
|
||||
)
|
||||
assert resp.status == 200
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_agent_auth_query_param():
|
||||
receiver = _make_receiver(token="secret")
|
||||
async with TestClient(TestServer(receiver._app)) as client:
|
||||
resp = await client.post(
|
||||
"/hooks/agent?token=secret",
|
||||
json={"message": "test"},
|
||||
)
|
||||
assert resp.status == 200
|
||||
Reference in New Issue
Block a user