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:
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