mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-10 20:50:52 +08:00
Update channel dispatch UX and harden sidecar startup behavior.
This aligns Weixin/WhatsApp with admin-managed model routing by adding channel/account-level specialist controls, improves OSS startup resilience by skipping missing sidecars gracefully, and refreshes runbook/readme guidance for the new default behavior and Weixin OpenAI-key silent handling. Made-with: Cursor
This commit is contained in:
parent
b1a0f94cd6
commit
5ab69e88d3
29 changed files with 1226 additions and 100 deletions
51
tests/test_channel_dispatch_defaults.py
Normal file
51
tests/test_channel_dispatch_defaults.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from oclaw.runtime.application.gateway.inbound_service import (
|
||||
_resolve_channel_dispatch,
|
||||
)
|
||||
|
||||
|
||||
class _DummyStore:
|
||||
def __init__(self, data: dict[str, str] | None = None) -> None:
|
||||
self._data = dict(data or {})
|
||||
|
||||
def get_setting(self, key: str) -> str:
|
||||
return str(self._data.get(key) or "")
|
||||
|
||||
|
||||
def test_channel_dispatch_defaults_to_expert_and_generalist() -> None:
|
||||
store = _DummyStore()
|
||||
interaction_mode, specialist = _resolve_channel_dispatch(store, channel="weixin", account=None)
|
||||
assert interaction_mode == "expert"
|
||||
assert specialist == "generalist"
|
||||
|
||||
|
||||
def test_channel_dispatch_uses_global_settings() -> None:
|
||||
store = _DummyStore(
|
||||
{
|
||||
"channel.dispatch.interaction_mode.whatsapp": "comprehensive",
|
||||
"channel.dispatch.specialist.whatsapp": "ops",
|
||||
}
|
||||
)
|
||||
interaction_mode, specialist = _resolve_channel_dispatch(store, channel="whatsapp", account=None)
|
||||
assert interaction_mode == "comprehensive"
|
||||
assert specialist == "ops"
|
||||
|
||||
|
||||
def test_channel_dispatch_account_config_overrides_global() -> None:
|
||||
store = _DummyStore(
|
||||
{
|
||||
"channel.dispatch.interaction_mode.weixin": "comprehensive",
|
||||
"channel.dispatch.specialist.weixin": "ops",
|
||||
}
|
||||
)
|
||||
account = {
|
||||
"config": {
|
||||
"interaction_mode": "expert",
|
||||
"specialist": "generalist",
|
||||
}
|
||||
}
|
||||
interaction_mode, specialist = _resolve_channel_dispatch(store, channel="weixin", account=account)
|
||||
assert interaction_mode == "expert"
|
||||
assert specialist == "generalist"
|
||||
|
||||
|
|
@ -121,6 +121,46 @@ class WeixinIlinkApiTests(unittest.TestCase):
|
|||
finally:
|
||||
weixin_ilink_api._process_inbound_payload_usecase = old_usecase # type: ignore[assignment]
|
||||
|
||||
def test_whatsapp_native_reply_returns_sync_replies(self) -> None:
|
||||
old_usecase = weixin_ilink_api._process_inbound_payload_usecase
|
||||
|
||||
def _fake_usecase(payload: dict[str, object]) -> dict[str, object]:
|
||||
text = str(payload.get("text") or "")
|
||||
self.assertEqual(str(payload.get("channel") or ""), "whatsapp")
|
||||
self.assertEqual(str(payload.get("user_id") or ""), "8613800138000@s.whatsapp.net")
|
||||
return {
|
||||
"ok": True,
|
||||
"replies": [
|
||||
{
|
||||
"chat_id": str(payload.get("chat_id") or ""),
|
||||
"text": f"wa:{text}",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
try:
|
||||
weixin_ilink_api._process_inbound_payload_usecase = _fake_usecase # type: ignore[assignment]
|
||||
r = self.client.post(
|
||||
"/whatsapp/native/reply",
|
||||
headers=self.headers,
|
||||
json={
|
||||
"account_id": "wa-bot-1",
|
||||
"ctx": {
|
||||
"From": "8613800138000@s.whatsapp.net",
|
||||
"To": "8613800138000@s.whatsapp.net",
|
||||
"Body": "hello whatsapp",
|
||||
},
|
||||
},
|
||||
)
|
||||
self.assertEqual(r.status_code, 200, r.text)
|
||||
data = r.json() or {}
|
||||
self.assertTrue(data.get("ok"), data)
|
||||
replies = data.get("replies") if isinstance(data.get("replies"), list) else []
|
||||
self.assertEqual(len(replies), 1, data)
|
||||
self.assertEqual(str((replies[0] or {}).get("text") or ""), "wa:hello whatsapp")
|
||||
finally:
|
||||
weixin_ilink_api._process_inbound_payload_usecase = old_usecase # type: ignore[assignment]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
40
tests/test_whatsapp_inbound_payload_shape.py
Normal file
40
tests/test_whatsapp_inbound_payload_shape.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from oclaw.interfaces.http.fastapi_app import create_app
|
||||
from oclaw.interfaces.http import fastapi_app
|
||||
|
||||
|
||||
def test_inbound_whatsapp_accepts_basic_payload_and_returns_replies() -> None:
|
||||
client = TestClient(create_app())
|
||||
old_usecase = fastapi_app.process_inbound_payload_usecase
|
||||
|
||||
def _fake_usecase(payload: dict[str, object]) -> dict[str, object]:
|
||||
assert str(payload.get("channel") or "") == "whatsapp"
|
||||
assert str(payload.get("account_id") or "") == "wa-default"
|
||||
assert str(payload.get("user_id") or "") == "111@s.whatsapp.net"
|
||||
assert str(payload.get("chat_id") or "") == "111@s.whatsapp.net"
|
||||
return {"ok": True, "replies": [{"chat_id": payload.get("chat_id"), "text": "ok"}]}
|
||||
|
||||
try:
|
||||
fastapi_app.process_inbound_payload_usecase = _fake_usecase # type: ignore[assignment]
|
||||
r = client.post(
|
||||
"/inbound/whatsapp",
|
||||
json={
|
||||
"account_id": "wa-default",
|
||||
"user_id": "111@s.whatsapp.net",
|
||||
"chat_id": "111@s.whatsapp.net",
|
||||
"text": "hello",
|
||||
"metadata": {"source": "test"},
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200, r.text
|
||||
data = r.json() or {}
|
||||
assert data.get("ok") is True
|
||||
replies = data.get("replies") if isinstance(data.get("replies"), list) else []
|
||||
assert replies and isinstance(replies[0], dict)
|
||||
assert str(replies[0].get("text") or "") == "ok"
|
||||
finally:
|
||||
fastapi_app.process_inbound_payload_usecase = old_usecase # type: ignore[assignment]
|
||||
|
||||
39
tests/test_whatsapp_ops_scripts.py
Normal file
39
tests/test_whatsapp_ops_scripts.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def _read(rel: str) -> str:
|
||||
return (REPO_ROOT / rel).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_whatsapp_ops_scripts_exist_and_use_openclaw() -> None:
|
||||
for rel in (
|
||||
"runtime/operations/scripts/whatsapp_install.ps1",
|
||||
"runtime/operations/scripts/whatsapp_login.ps1",
|
||||
"runtime/operations/scripts/whatsapp_start.ps1",
|
||||
"runtime/operations/scripts/whatsapp_status.ps1",
|
||||
"runtime/operations/scripts/whatsapp_stop.ps1",
|
||||
):
|
||||
text = _read(rel)
|
||||
assert "openclaw" not in text.lower()
|
||||
start_text = _read("runtime/operations/scripts/whatsapp_start.ps1")
|
||||
assert "baileys_runner.ts" in start_text
|
||||
assert "AIA_GATEWAY_BASE_URL" in start_text
|
||||
|
||||
|
||||
def test_runbook_mentions_whatsapp_scripts() -> None:
|
||||
text = _read("docs/RUNBOOK.md")
|
||||
assert "whatsapp_install.ps1" in text
|
||||
assert "whatsapp_login.ps1" in text
|
||||
assert "whatsapp_start.ps1" in text
|
||||
assert "/inbound/whatsapp" in text
|
||||
|
||||
|
||||
def test_start_all_gracefully_skips_missing_channel_sidecars() -> None:
|
||||
text = _read("runtime/operations/scripts/start_all.ps1")
|
||||
assert 'Warn "weixin sidecar skipped:' in text
|
||||
assert 'Warn "whatsapp sidecar skipped:' in text
|
||||
Loading…
Add table
Add a link
Reference in a new issue