Files
Aetheel/test_all.py
tanmay11k 82c2640481 feat: openclaw-style secrets (env.vars + \) and per-task model routing
- Replace python-dotenv with config.json env.vars block + \ substitution
- Add models section for per-task model routing (heartbeat, subagent, default)
- Heartbeat/subagent tasks can use different models/providers than main chat
- Remove python-dotenv from dependencies
- Update all docs to reflect new config approach
- Reorganize docs into project/ and research/ subdirectories
2026-02-20 23:49:05 -05:00

405 lines
13 KiB
Python

#!/usr/bin/env python3
"""
Aetheel — Full Feature Test Script (cross-platform)
====================================================
Runs all tests and smoke checks for every Aetheel feature.
Works on Windows, Linux, and macOS.
Usage:
uv run python test_all.py
# or
python test_all.py
"""
import importlib
import json
import os
import shutil
import subprocess
import sys
import tempfile
PASS = 0
FAIL = 0
SKIP = 0
def _pass(msg):
global PASS
PASS += 1
print(f"{msg}")
def _fail(msg):
global FAIL
FAIL += 1
print(f"{msg}")
def _skip(msg):
global SKIP
SKIP += 1
print(f" ⚠️ {msg}")
def section(title):
print(f"\n━━━ {title} ━━━")
def run_cmd(args, cwd=None, timeout=120):
"""Run a command and return (returncode, stdout)."""
try:
result = subprocess.run(
args, capture_output=True, text=True,
cwd=cwd, timeout=timeout,
)
return result.returncode, result.stdout + result.stderr
except FileNotFoundError:
return -1, f"Command not found: {args[0]}"
except subprocess.TimeoutExpired:
return -1, "Timeout"
# =========================================================================
section("1. Environment Check")
# =========================================================================
# Python version
py_ver = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
if sys.version_info >= (3, 12):
_pass(f"Python {py_ver} (>= 3.12 required)")
else:
_fail(f"Python {py_ver} — need >= 3.12")
# uv
if shutil.which("uv"):
rc, out = run_cmd(["uv", "--version"])
_pass(f"uv found: {out.strip()}")
else:
_skip("uv not found — using pip directly")
# Working directory
if os.path.isfile("pyproject.toml"):
_pass("Running from Aetheel directory")
else:
_fail("pyproject.toml not found — run from the Aetheel/ directory")
sys.exit(1)
# =========================================================================
section("2. Dependency Check")
# =========================================================================
required = {
"pytest": "pytest",
"pytest_asyncio": "pytest-asyncio",
"hypothesis": "hypothesis",
"click": "click",
"aiohttp": "aiohttp",
"apscheduler": "apscheduler",
}
for mod_name, pkg_name in required.items():
try:
importlib.import_module(mod_name)
_pass(f"{pkg_name} installed")
except ImportError:
_fail(f"{pkg_name} NOT installed — run: uv sync --extra test")
# =========================================================================
section("3. Pytest — Unit Tests")
# =========================================================================
print(" Running full test suite...")
rc, out = run_cmd(
[sys.executable, "-m", "pytest", "tests/", "-v", "--tb=short",
"--ignore=tests/test_scheduler.py"],
timeout=120,
)
# Print last 20 lines
lines = out.strip().splitlines()
for line in lines[-20:]:
print(f" {line}")
if rc == 0:
_pass("Core test suite passed")
else:
_fail("Core test suite had failures")
# Scheduler tests
print("\n Attempting scheduler tests...")
rc2, out2 = run_cmd(
[sys.executable, "-m", "pytest", "tests/test_scheduler.py", "-v", "--tb=short"],
timeout=30,
)
if rc2 == 0:
_pass("Scheduler tests passed")
else:
_skip("Scheduler tests skipped (apscheduler import issue)")
# =========================================================================
section("4. Config System")
# =========================================================================
try:
from config import load_config, AetheelConfig, MCPConfig, MCPServerConfig, write_mcp_config
from config import HeartbeatConfig, WebChatConfig, HooksConfig, WebhookConfig
cfg = load_config()
assert isinstance(cfg, AetheelConfig)
assert cfg.claude.no_tools is False
assert len(cfg.claude.allowed_tools) > 0
assert hasattr(cfg, "heartbeat")
assert hasattr(cfg, "webchat")
assert hasattr(cfg, "mcp")
assert hasattr(cfg, "hooks")
assert hasattr(cfg, "webhooks")
_pass("Config loads with all sections (heartbeat, webchat, mcp, hooks, webhooks)")
except Exception as e:
_fail(f"Config system: {e}")
# MCP config writer
try:
with tempfile.TemporaryDirectory() as tmpdir:
mcp = MCPConfig(servers={
"test": MCPServerConfig(command="echo", args=["hi"], env={"K": "V"})
})
write_mcp_config(mcp, tmpdir, use_claude=True)
with open(os.path.join(tmpdir, ".mcp.json")) as f:
data = json.load(f)
assert "mcpServers" in data and "test" in data["mcpServers"]
write_mcp_config(mcp, tmpdir, use_claude=False)
with open(os.path.join(tmpdir, "opencode.json")) as f:
data = json.load(f)
assert "mcp" in data
_pass("MCP config writer (Claude + OpenCode formats)")
except Exception as e:
_fail(f"MCP config writer: {e}")
# =========================================================================
section("5. System Prompt")
# =========================================================================
try:
from agent.opencode_runtime import build_aetheel_system_prompt
prompt = build_aetheel_system_prompt()
for s in ["Your Tools", "Self-Modification", "Subagents & Teams"]:
assert s in prompt, f"Missing: {s}"
_pass("System prompt contains all new sections")
except Exception as e:
_fail(f"System prompt: {e}")
# =========================================================================
section("6. CLI")
# =========================================================================
rc, out = run_cmd([sys.executable, "cli.py", "--help"])
if rc == 0:
_pass("CLI --help works")
else:
_fail("CLI --help failed")
for cmd in ["start", "chat", "status", "doctor"]:
rc, _ = run_cmd([sys.executable, "cli.py", cmd, "--help"])
if rc == 0:
_pass(f"CLI command '{cmd}' exists")
else:
_fail(f"CLI command '{cmd}' missing")
for grp in ["cron", "config", "memory"]:
rc, _ = run_cmd([sys.executable, "cli.py", grp, "--help"])
if rc == 0:
_pass(f"CLI group '{grp}' exists")
else:
_fail(f"CLI group '{grp}' missing")
# =========================================================================
section("7. Heartbeat Parser")
# =========================================================================
try:
from heartbeat.heartbeat import HeartbeatRunner
tests = [
("Every 30 minutes", "*/30 * * * *"),
("Every hour", "0 * * * *"),
("Every 2 hours", "0 */2 * * *"),
("Every morning (9:00 AM)", "0 9 * * *"),
("Every evening (6:00 PM)", "0 18 * * *"),
]
for header, expected in tests:
result = HeartbeatRunner._parse_schedule_header(header)
assert result == expected, f"{header}: got {result}"
_pass(f"Schedule parser: all {len(tests)} patterns correct")
except Exception as e:
_fail(f"Heartbeat parser: {e}")
# =========================================================================
section("8. Hook System")
# =========================================================================
try:
from hooks.hooks import HookManager, HookEvent
mgr = HookManager()
received = []
mgr.register("gateway:startup", lambda e: received.append(e.event_key))
mgr.trigger(HookEvent(type="gateway", action="startup"))
assert received == ["gateway:startup"]
_pass("Programmatic hook register + trigger")
mgr2 = HookManager()
mgr2.register("test:event", lambda e: e.messages.append("hello"))
event = HookEvent(type="test", action="event")
msgs = mgr2.trigger(event)
assert "hello" in msgs
_pass("Hook message passing")
# File-based hook discovery
with tempfile.TemporaryDirectory() as tmpdir:
hook_dir = os.path.join(tmpdir, "hooks", "test-hook")
os.makedirs(hook_dir)
with open(os.path.join(hook_dir, "HOOK.md"), "w") as f:
f.write("---\nname: test-hook\nevents: [gateway:startup]\n---\n# Test\n")
with open(os.path.join(hook_dir, "handler.py"), "w") as f:
f.write("def handle(event):\n event.messages.append('file-hook-fired')\n")
mgr3 = HookManager(workspace_dir=tmpdir)
hooks = mgr3.discover()
assert len(hooks) == 1
event3 = HookEvent(type="gateway", action="startup")
msgs3 = mgr3.trigger(event3)
assert "file-hook-fired" in msgs3
_pass("File-based hook discovery + execution")
except Exception as e:
_fail(f"Hook system: {e}")
# =========================================================================
section("9. Webhook Receiver")
# =========================================================================
try:
from webhooks.receiver import WebhookReceiver, WebhookConfig as WHConfig
config = WHConfig(enabled=True, port=0, host="127.0.0.1", token="test")
receiver = WebhookReceiver(
ai_handler_fn=lambda msg: "ok",
send_fn=lambda *a: None,
config=config,
)
# Check routes exist
route_info = [str(r) for r in receiver._app.router.routes()]
has_wake = any("wake" in r for r in route_info)
has_agent = any("agent" in r for r in route_info)
has_health = any("health" in r for r in route_info)
assert has_wake and has_agent and has_health
_pass("Webhook routes registered (wake, agent, health)")
# Auth check
assert receiver._check_auth(type("R", (), {"headers": {"Authorization": "Bearer test"}, "query": {}})())
assert not receiver._check_auth(type("R", (), {"headers": {}, "query": {}})())
_pass("Webhook bearer token auth works")
except Exception as e:
_fail(f"Webhook receiver: {e}")
# =========================================================================
section("10. SubagentBus")
# =========================================================================
try:
from agent.subagent import SubagentBus, SubagentManager
bus = SubagentBus()
received = []
bus.subscribe("ch1", lambda msg, sender: received.append((msg, sender)))
bus.publish("ch1", "hello", "agent-1")
assert received == [("hello", "agent-1")]
_pass("SubagentBus pub/sub")
mgr = SubagentManager(runtime_factory=lambda: None, send_fn=lambda *a: None)
assert isinstance(mgr.bus, SubagentBus)
_pass("SubagentManager.bus property")
except Exception as e:
_fail(f"SubagentBus: {e}")
# =========================================================================
section("11. WebChat Adapter")
# =========================================================================
try:
from adapters.webchat_adapter import WebChatAdapter
from adapters.base import BaseAdapter
adapter = WebChatAdapter(host="127.0.0.1", port=9999)
assert isinstance(adapter, BaseAdapter)
assert adapter.source_name == "webchat"
_pass("WebChatAdapter extends BaseAdapter, source_name='webchat'")
assert os.path.isfile(os.path.join("static", "chat.html"))
_pass("static/chat.html exists")
except Exception as e:
_fail(f"WebChat adapter: {e}")
# =========================================================================
section("12. Claude Runtime Config")
# =========================================================================
try:
from agent.claude_runtime import ClaudeCodeConfig
cfg = ClaudeCodeConfig()
assert cfg.no_tools is False
assert len(cfg.allowed_tools) >= 15
assert "Bash" in cfg.allowed_tools
assert "TeamCreate" in cfg.allowed_tools
assert "SendMessage" in cfg.allowed_tools
_pass(f"ClaudeCodeConfig: no_tools=False, {len(cfg.allowed_tools)} tools, Team tools included")
except Exception as e:
_fail(f"Claude runtime config: {e}")
# =========================================================================
section("13. Module Imports")
# =========================================================================
modules = [
"config", "adapters.base", "adapters.webchat_adapter",
"agent.opencode_runtime", "agent.claude_runtime", "agent.subagent",
"heartbeat.heartbeat", "hooks.hooks", "webhooks.receiver",
"skills.skills", "cli",
]
for mod in modules:
try:
importlib.import_module(mod)
_pass(f"import {mod}")
except Exception as e:
_fail(f"import {mod}: {e}")
# =========================================================================
section("RESULTS")
# =========================================================================
total = PASS + FAIL + SKIP
print(f"\nTotal: {total} checks")
print(f" Passed: {PASS}")
print(f" Failed: {FAIL}")
print(f" Skipped: {SKIP}")
print()
if FAIL == 0:
print("All checks passed! 🎉")
sys.exit(0)
else:
print(f"{FAIL} check(s) failed.")
sys.exit(1)