Files
PraisonAIUI/tests/test_all_gap_features.py
a433c483c1 fix: document bootstrap entrypoints in examples (fixes #71) (#96)
* feat: standardize bootstrap entrypoint to 'aiui run app.py' across examples

Standardizes platform bootstrap patterns across examples per issue #71:

- Update examples README table to use 'aiui run app.py' consistently
- Update docstrings in examples 10, 12, 19-21 to show 'aiui run' instead of 'python app.py'
- Remove fragmented bootstrap approaches (uvicorn.run, asyncio patterns)
- Enforce single bootstrap SSOT as designed in CLI

Co-authored-by: Mervin Praison <[email protected]>

* feat: consolidate bootstrap entrypoints - auto-detect gateway apps (fixes #71)

- Add bootstrap detection in CLI run command
- Auto-detect gateway pattern (async main + AIUIGateway imports)
- Auto-switch to praisonai backend for gateway apps
- Delegate to user-defined main() when present
- Remove sys.path hacks from example 14
- Update examples README to show unified aiui run entrypoint
- Add BOOTSTRAP.md documentation for migration patterns

Co-authored-by: Mervin Praison <[email protected]>

* fix: resolve critical bootstrap entrypoint issues

- Fix AST parsing bug in gateway auto-detection (cli.py:1419)
- Revert background bots (email-bot, agentmail-bot) to python app.py
- Revert custom UI apps (agent-dashboard) to python app.py
- Update README.md to reflect correct run commands
- Fix import formatting and remove unused imports

These changes ensure that:
1. Gateway auto-detection works correctly by properly checking ast.alias.name
2. Background bots execute their __main__ blocks and don't start unnecessary web servers
3. Custom UI apps register their custom routes in __main__ blocks
4. Documentation matches actual working run commands

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Mervin Praison <[email protected]>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
Co-authored-by: Mervin Praison <[email protected]>
Co-authored-by: Cursor <[email protected]>
2026-06-15 16:36:35 +01:00

314 lines
13 KiB
Python

"""Comprehensive tests for ALL gap features (Gaps 1-25).
Tests each new feature module:
* Protocol compliance (BaseFeatureProtocol)
* Data models
* Manager implementations
* Auto-registration
"""
from __future__ import annotations
import pytest
# ── Helper ───────────────────────────────────────────────────────
def _make_features():
"""Auto-register and return features dict."""
from praisonaiui.features import auto_register_defaults, get_features
auto_register_defaults()
return get_features()
# ═══════════════════════════════════════════════════════════════════
# Gap 1-5: Chat Feature
# ═══════════════════════════════════════════════════════════════════
class TestChatFeature:
def test_registered(self):
features = _make_features()
assert "chat" in features
def test_protocol_compliance(self):
from praisonaiui.features.chat import PraisonAIChat
f = PraisonAIChat()
assert f.name == "chat"
assert len(f.routes()) >= 3
@pytest.mark.asyncio
async def test_send_and_history(self):
from praisonaiui.features.chat import ChatManager
mgr = ChatManager()
result = await mgr.send_message("hi", session_id="s1")
assert "message_id" in result
history = await mgr.get_history("s1")
assert len(history) == 1
assert history[0]["content"] == "hi"
@pytest.mark.asyncio
async def test_abort_no_run(self):
from praisonaiui.features.chat import ChatManager
mgr = ChatManager()
r = await mgr.abort_run("nonexistent")
assert r["status"] == "no_active_run"
def test_chat_message_model(self):
from praisonaiui.features.chat import ChatMessage
msg = ChatMessage(role="user", content="test", session_id="s1")
d = msg.to_dict()
assert d["role"] == "user"
assert "message_id" in d
assert "timestamp" in d
# ═══════════════════════════════════════════════════════════════════
# Gap 6: Attachments Feature
# ═══════════════════════════════════════════════════════════════════
class TestAttachmentsFeature:
def test_registered(self):
features = _make_features()
assert "attachments" in features
def test_protocol_compliance(self):
from praisonaiui.features.attachments import PraisonAIAttachments
f = PraisonAIAttachments()
assert f.name == "attachments"
paths = [r.path for r in f.routes()]
assert "/api/chat/attachments" in paths
def test_upload_and_list(self):
from praisonaiui.features.attachments import AttachmentManager
mgr = AttachmentManager()
meta = mgr.upload(
data=b"hello world",
filename="test.txt",
content_type="text/plain",
session_id="s1",
)
assert meta["id"]
assert meta["size"] == 11
items = mgr.list_for_session("s1")
assert len(items) == 1
# cleanup
mgr.delete(meta["id"])
def test_upload_too_large(self):
from praisonaiui.features.attachments import AttachmentManager
mgr = AttachmentManager(max_size_mb=0)
with pytest.raises(ValueError, match="too large"):
mgr.upload(b"x", "f.txt", "text/plain")
def test_upload_bad_type(self):
from praisonaiui.features.attachments import AttachmentManager
mgr = AttachmentManager()
with pytest.raises(ValueError, match="not allowed"):
mgr.upload(b"x", "f.exe", "application/x-executable")
# ═══════════════════════════════════════════════════════════════════
# Gap 8: Config Hot-Reload Feature
# ═══════════════════════════════════════════════════════════════════
class TestConfigHotReloadFeature:
def test_registered(self):
features = _make_features()
assert "config_hot_reload" in features
def test_protocol_compliance(self):
from praisonaiui.features.config_hot_reload import PraisonAIConfigHotReload
f = PraisonAIConfigHotReload()
assert f.name == "config_hot_reload"
assert len(f.routes()) >= 3
def test_watcher_status(self):
from praisonaiui.features.config_hot_reload import ConfigWatcher
w = ConfigWatcher(poll_interval=1.0)
status = w.get_status()
assert status["watching"] is False
assert status["poll_interval"] == 1.0
# ═══════════════════════════════════════════════════════════════════
# Gap 10: Model Fallback Feature
# ═══════════════════════════════════════════════════════════════════
class TestModelFallbackFeature:
def test_registered(self):
features = _make_features()
assert "model_fallback" in features
def test_protocol_compliance(self):
from praisonaiui.features.model_fallback import PraisonAIModelFallback
f = PraisonAIModelFallback()
assert f.name == "model_fallback"
def test_fallback_chain(self):
from praisonaiui.features.model_fallback import ModelFallbackManager
mgr = ModelFallbackManager()
assert mgr.get_fallback_chain() == []
mgr.set_fallback_chain(["gpt-4o", "gpt-4o-mini"])
assert mgr.get_fallback_chain() == ["gpt-4o", "gpt-4o-mini"]
def test_models_list(self):
from praisonaiui.features.model_fallback import ModelFallbackManager
mgr = ModelFallbackManager()
models = mgr.get_models()
assert len(models) > 0
# ═══════════════════════════════════════════════════════════════════
# Gap 15: Subagents Feature
# ═══════════════════════════════════════════════════════════════════
class TestSubagentsFeature:
def test_registered(self):
features = _make_features()
assert "subagents" in features
def test_protocol_compliance(self):
from praisonaiui.features.subagents import PraisonAISubagents
f = PraisonAISubagents()
assert f.name == "subagents"
def test_spawn_and_tree(self):
from praisonaiui.features.subagents import SubagentManager
mgr = SubagentManager()
mgr.register_spawn("parent", "child", "s1")
tree = mgr.get_tree("s1")
assert tree["total_spawns"] == 1
assert len(tree["roots"]) == 1
assert tree["roots"][0]["name"] == "parent"
assert tree["roots"][0]["children"][0]["name"] == "child"
# ═══════════════════════════════════════════════════════════════════
# Gap 19: Theme Feature
# ═══════════════════════════════════════════════════════════════════
class TestThemeFeature:
def test_registered(self):
features = _make_features()
assert "theme" in features
def test_protocol_compliance(self):
from praisonaiui.features.theme import PraisonAITheme
f = PraisonAITheme()
assert f.name == "theme"
def test_themes_available(self):
from praisonaiui.features.theme import ThemeManager
mgr = ThemeManager()
themes = mgr.list_themes()
assert "dark" in themes
assert "light" in themes
def test_set_theme(self):
from praisonaiui.features.theme import ThemeManager
mgr = ThemeManager()
assert mgr.get_theme() == "dark"
mgr.set_theme("light")
assert mgr.get_theme() == "light"
def test_css_vars(self):
from praisonaiui.features.theme import ThemeManager
mgr = ThemeManager()
v = mgr.get_vars("dark")
assert "--chat-bg" in v
assert "--chat-text" in v
def test_custom_theme(self):
from praisonaiui.features.theme import ThemeManager
mgr = ThemeManager()
mgr.register_theme("ocean", {"--chat-bg": "#001122"})
assert "ocean" in mgr.list_themes()
v = mgr.get_vars("ocean")
assert v["--chat-bg"] == "#001122"
# ═══════════════════════════════════════════════════════════════════
# Gap 22: Browser Automation Feature
# ═══════════════════════════════════════════════════════════════════
class TestBrowserAutomationFeature:
def test_registered(self):
features = _make_features()
assert "browser_automation" in features
def test_protocol_compliance(self):
from praisonaiui.features.browser_automation import PraisonAIBrowserAutomation
f = PraisonAIBrowserAutomation()
assert f.name == "browser_automation"
def test_status(self):
from praisonaiui.features.browser_automation import BrowserAutomationManager
mgr = BrowserAutomationManager()
status = mgr.get_status()
assert "available" in status
assert "backends" in status
# ═══════════════════════════════════════════════════════════════════
# Gap 25: Protocol Version Feature
# ═══════════════════════════════════════════════════════════════════
class TestProtocolVersionFeature:
def test_registered(self):
features = _make_features()
assert "protocol" in features
def test_protocol_compliance(self):
from praisonaiui.features.protocol_version import PraisonAIProtocol
f = PraisonAIProtocol()
assert f.name == "protocol"
def test_version_info(self):
from praisonaiui.features.protocol_version import ProtocolInfo
info = ProtocolInfo()
d = info.to_dict()
assert "version" in d
assert "event_types" in d
assert "features" in d
def test_compatibility(self):
from praisonaiui.features.protocol_version import ProtocolInfo
info = ProtocolInfo()
assert info.is_compatible("1.0.0")
assert info.is_compatible("1.5.0")
assert not info.is_compatible("2.0.0")
assert not info.is_compatible("invalid")
def test_event_types(self):
from praisonaiui.features.protocol_version import EVENT_TYPES
assert "chat" in EVENT_TYPES
assert "run_content" in EVENT_TYPES
assert "tool_call_started" in EVENT_TYPES
# ═══════════════════════════════════════════════════════════════════
# Meta: All Features Registered
# ═══════════════════════════════════════════════════════════════════
class TestAllFeaturesRegistered:
def test_total_feature_count(self):
features = _make_features()
# Original 16 + 7 new = 23+
assert len(features) >= 23
def test_all_new_features_present(self):
features = _make_features()
new_features = [
"chat", "theme", "config_hot_reload", "protocol",
"attachments", "model_fallback", "browser_automation",
"subagents",
]
for name in new_features:
assert name in features, f"Feature '{name}' not registered"
@pytest.mark.asyncio
async def test_all_features_healthy(self):
features = _make_features()
for name, f in features.items():
h = await f.health()
assert h["status"] in ("ok", "degraded"), f"Feature '{name}' unhealthy: {h}"