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:
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
|
||||
Reference in New Issue
Block a user