mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 03:30:48 +08:00
同时收敛启动与运维脚本默认行为(含 wiki worker)、更新 Admin 可观测性与相关测试,降低首轮时延并提高运行稳定性。 Made-with: Cursor
741 lines
28 KiB
Python
741 lines
28 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from oclaw.interfaces.gateway.server_methods.agent import agent_handlers
|
|
from oclaw.interfaces.gateway.server_methods.agents import agents_handlers
|
|
from oclaw.interfaces.gateway.server_methods.chat import chat_handlers
|
|
from oclaw.interfaces.gateway.server_methods.send import send_handlers
|
|
from oclaw.interfaces.gateway.server_methods.image import image_handlers
|
|
from oclaw.interfaces.gateway.server_methods.skills import skills_handlers
|
|
from oclaw.interfaces.gateway.server_methods.system import system_handlers
|
|
from oclaw.interfaces.gateway.server_methods.cron import cron_handlers
|
|
from oclaw.interfaces.gateway.server_methods.devices import device_handlers
|
|
from oclaw.interfaces.gateway.server_methods.models import models_handlers
|
|
from oclaw.interfaces.gateway.server_methods.models_auth_status import (
|
|
invalidate_model_auth_status_cache,
|
|
models_auth_status_handlers,
|
|
)
|
|
from oclaw.interfaces.gateway.server_methods.push import push_handlers
|
|
from oclaw.interfaces.gateway.server_methods.channels import channels_handlers
|
|
from oclaw.interfaces.gateway.server_methods.update import update_handlers
|
|
from oclaw.interfaces.gateway.server_methods.voicewake import voicewake_handlers
|
|
from oclaw.interfaces.gateway.server_methods.wizard import wizard_handlers
|
|
from oclaw.interfaces.gateway.server_methods.tts import tts_handlers
|
|
from oclaw.interfaces.gateway.server_methods.web import web_handlers
|
|
from oclaw.interfaces.gateway.server_methods.tools_catalog import tools_catalog_handlers
|
|
from oclaw.interfaces.gateway.server_methods.tools_effective import tools_effective_handlers
|
|
from oclaw.interfaces.gateway.server_methods.talk import talk_handlers
|
|
from oclaw.interfaces.gateway.server_methods.usage import usage_handlers
|
|
from oclaw.interfaces.gateway.server_methods.exec_approvals import exec_approvals_handlers
|
|
from oclaw.interfaces.gateway.server_methods.nodes_pending import node_pending_handlers
|
|
from oclaw.interfaces.gateway.server_methods.nodes import node_handlers
|
|
from oclaw.interfaces.gateway.server_methods.sessions import sessions_handlers
|
|
from oclaw.interfaces.gateway.server_methods.doctor import doctor_handlers
|
|
|
|
|
|
def _call(handler, *, params: dict[str, Any], context: dict[str, Any] | None = None, client: dict[str, Any] | None = None):
|
|
out: dict[str, Any] = {}
|
|
|
|
def respond(ok, payload, error, meta): # noqa: ANN001
|
|
out["ok"] = ok
|
|
out["payload"] = payload
|
|
out["error"] = error
|
|
out["meta"] = meta
|
|
|
|
handler(
|
|
{
|
|
"params": params,
|
|
"context": context if context is not None else {},
|
|
"client": client if client is not None else {},
|
|
"respond": respond,
|
|
}
|
|
)
|
|
return out
|
|
|
|
|
|
def test_sessions_subscribe_and_unsubscribe_updates_subscriber_set() -> None:
|
|
context: dict[str, Any] = {}
|
|
client = {"conn_id": "c-1"}
|
|
res1 = _call(sessions_handlers["sessions.subscribe"], params={}, context=context, client=client)
|
|
assert res1["ok"] is True
|
|
assert "c-1" in context["session_event_subscribers"]
|
|
|
|
res2 = _call(sessions_handlers["sessions.unsubscribe"], params={}, context=context, client=client)
|
|
assert res2["ok"] is True
|
|
assert "c-1" not in context["session_event_subscribers"]
|
|
|
|
|
|
def test_agents_list_and_files_set_get_shapes() -> None:
|
|
listed = _call(agents_handlers["agents.list"], params={})
|
|
assert listed["ok"] is True
|
|
assert isinstance((listed["payload"] or {}).get("agents"), list)
|
|
|
|
set_out = _call(
|
|
agents_handlers["agents.files.set"],
|
|
params={"agentId": "main", "name": "SOUL.md", "content": "hello"},
|
|
)
|
|
assert set_out["ok"] is True
|
|
file_payload = (set_out["payload"] or {}).get("file") or {}
|
|
assert file_payload.get("name") == "SOUL.md"
|
|
|
|
get_out = _call(agents_handlers["agents.files.get"], params={"agentId": "main", "name": "SOUL.md"})
|
|
assert get_out["ok"] is True
|
|
|
|
|
|
def test_sessions_messages_subscribe_uses_key_alias() -> None:
|
|
context: dict[str, Any] = {}
|
|
client = {"conn_id": "c-2"}
|
|
res = _call(
|
|
sessions_handlers["sessions.messages.subscribe"],
|
|
params={"key": "sess-1"},
|
|
context=context,
|
|
client=client,
|
|
)
|
|
assert res["ok"] is True
|
|
subscribers = context["session_message_subscribers"]["sess-1"]
|
|
assert "c-2" in subscribers
|
|
|
|
|
|
def test_chat_send_requires_session_key_or_key() -> None:
|
|
bad = _call(chat_handlers["chat.send"], params={"message": "hi"})
|
|
assert bad["ok"] is False
|
|
assert bad["error"]["code"] == "INVALID_REQUEST"
|
|
|
|
good = _call(chat_handlers["chat.send"], params={"key": "sess-x", "message": "hi", "idempotencyKey": "idem-x"})
|
|
assert good["ok"] is True
|
|
assert good["payload"]["status"] == "started"
|
|
assert good["payload"]["runId"] == "idem-x"
|
|
|
|
|
|
def test_chat_send_telegram_transport_fields_are_normalized() -> None:
|
|
out = _call(
|
|
chat_handlers["chat.send"],
|
|
params={
|
|
"key": "sess-telegram",
|
|
"message": "hi",
|
|
"idempotencyKey": "idem-chat-tg",
|
|
"channel": "telegram",
|
|
"to": "telegram:group:-100123456789:topic:9",
|
|
"replyToId": "11",
|
|
},
|
|
)
|
|
assert out["ok"] is True
|
|
payload = out["payload"] or {}
|
|
assert payload.get("channel") == "telegram"
|
|
assert payload.get("to") == "telegram:group:-100123456789:topic:9"
|
|
assert payload.get("threadId") == 9
|
|
assert payload.get("replyToMessageId") == 11
|
|
target = payload.get("target") or {}
|
|
assert target.get("chatId") == "-100123456789"
|
|
assert target.get("chatType") == "group"
|
|
|
|
|
|
def test_chat_send_forwards_normalized_telegram_fields_to_enqueue() -> None:
|
|
captured: dict[str, Any] = {}
|
|
|
|
def _enqueue(session_key: str, message: str, run_id: str, forwarded_params: dict[str, Any]) -> bool:
|
|
captured["session_key"] = session_key
|
|
captured["message"] = message
|
|
captured["run_id"] = run_id
|
|
captured["params"] = dict(forwarded_params)
|
|
return True
|
|
|
|
out = _call(
|
|
chat_handlers["chat.send"],
|
|
params={
|
|
"key": "sess-enqueue",
|
|
"message": "hello",
|
|
"idempotencyKey": "idem-enqueue-tg",
|
|
"channel": "telegram",
|
|
"to": "telegram:group:-100999:topic:5",
|
|
"replyToId": "14",
|
|
},
|
|
context={"enqueue_chat_send": _enqueue},
|
|
)
|
|
assert out["ok"] is True
|
|
forwarded = captured.get("params") or {}
|
|
assert forwarded.get("to") == "telegram:group:-100999:topic:5"
|
|
assert forwarded.get("threadId") == 5
|
|
assert forwarded.get("replyToMessageId") == 14
|
|
target = forwarded.get("target") or {}
|
|
assert target.get("chatId") == "-100999"
|
|
assert target.get("chatType") == "group"
|
|
|
|
|
|
def test_agent_dedupe_then_wait_reads_cached_status() -> None:
|
|
context: dict[str, Any] = {"dedupe": {}}
|
|
res1 = _call(
|
|
agent_handlers["agent"],
|
|
params={"message": "hello", "idempotencyKey": "run-1"},
|
|
context=context,
|
|
)
|
|
assert res1["ok"] is True
|
|
|
|
res2 = _call(
|
|
agent_handlers["agent"],
|
|
params={"message": "hello", "idempotencyKey": "run-1"},
|
|
context=context,
|
|
)
|
|
assert res2["ok"] is True
|
|
assert (res2["meta"] or {}).get("cached") is True
|
|
|
|
wait = _call(agent_handlers["agent.wait"], params={"runId": "run-1"}, context=context)
|
|
assert wait["ok"] is True
|
|
assert wait["payload"]["runId"] == "run-1"
|
|
|
|
|
|
def test_send_dedupe_returns_cached_meta() -> None:
|
|
context: dict[str, Any] = {"dedupe": {}}
|
|
first = _call(
|
|
send_handlers["send"],
|
|
params={"to": "u1", "message": "hi", "idempotencyKey": "idem-s1", "channel": "telegram"},
|
|
context=context,
|
|
)
|
|
assert first["ok"] is True
|
|
assert (first["payload"] or {}).get("runId") == "idem-s1"
|
|
|
|
second = _call(
|
|
send_handlers["send"],
|
|
params={"to": "u1", "message": "hi", "idempotencyKey": "idem-s1", "channel": "telegram"},
|
|
context=context,
|
|
)
|
|
assert second["ok"] is True
|
|
assert (second["meta"] or {}).get("cached") is True
|
|
|
|
|
|
def test_send_telegram_normalizes_target_and_thread_params() -> None:
|
|
context: dict[str, Any] = {"dedupe": {}}
|
|
out = _call(
|
|
send_handlers["send"],
|
|
params={
|
|
"to": "telegram:group:-100987654321:topic:42",
|
|
"message": "hello",
|
|
"idempotencyKey": "idem-tg-1",
|
|
"channel": "telegram",
|
|
"replyToId": "77",
|
|
},
|
|
context=context,
|
|
)
|
|
assert out["ok"] is True
|
|
payload = out["payload"] or {}
|
|
assert payload.get("to") == "telegram:group:-100987654321:topic:42"
|
|
assert payload.get("threadId") == 42
|
|
assert payload.get("replyToMessageId") == 77
|
|
target = payload.get("target") or {}
|
|
assert target.get("chatId") == "-100987654321"
|
|
assert target.get("chatType") == "group"
|
|
assert target.get("messageThreadId") == 42
|
|
|
|
|
|
def test_sessions_send_telegram_target_is_normalized() -> None:
|
|
out = _call(
|
|
sessions_handlers["sessions.send"],
|
|
params={
|
|
"sessionKey": "sess-1",
|
|
"message": "hello",
|
|
"channel": "telegram",
|
|
"to": "telegram:group:-100987654321:topic:7",
|
|
"replyToId": "3",
|
|
},
|
|
)
|
|
assert out["ok"] is True
|
|
payload = out["payload"] or {}
|
|
assert payload.get("channel") == "telegram"
|
|
assert payload.get("to") == "telegram:group:-100987654321:topic:7"
|
|
assert payload.get("threadId") == 7
|
|
assert payload.get("replyToMessageId") == 3
|
|
target = payload.get("target") or {}
|
|
assert target.get("chatId") == "-100987654321"
|
|
assert target.get("chatType") == "group"
|
|
|
|
|
|
class _MemStore:
|
|
def __init__(self) -> None:
|
|
self._settings: dict[str, str] = {}
|
|
|
|
def get_setting(self, key: str) -> str | None: # noqa: ANN001
|
|
return self._settings.get(str(key))
|
|
|
|
def set_setting(self, key: str, value: str) -> None: # noqa: ANN001
|
|
self._settings[str(key)] = str(value)
|
|
|
|
|
|
def test_skills_status_requires_store_then_lists_skills() -> None:
|
|
no_store = _call(skills_handlers["skills.status"], params={}, context={})
|
|
assert no_store["ok"] is False
|
|
assert no_store["error"]["code"] == "UNAVAILABLE"
|
|
|
|
store = _MemStore()
|
|
ok = _call(skills_handlers["skills.status"], params={}, context={"store": store})
|
|
assert ok["ok"] is True
|
|
payload = ok["payload"] or {}
|
|
assert isinstance(payload.get("skills"), list)
|
|
# Skill names come from SKILL.md frontmatter (not directory names).
|
|
names = {str(x.get("name")) for x in payload["skills"] if isinstance(x, dict)}
|
|
assert "weather" in names
|
|
|
|
|
|
def test_system_set_heartbeats_requires_boolean() -> None:
|
|
bad = _call(system_handlers["set-heartbeats"], params={"enabled": "yes"})
|
|
assert bad["ok"] is False
|
|
assert bad["error"]["code"] == "INVALID_REQUEST"
|
|
|
|
ok = _call(system_handlers["set-heartbeats"], params={"enabled": True})
|
|
assert ok["ok"] is True
|
|
assert (ok["payload"] or {}).get("enabled") is True
|
|
|
|
|
|
def test_system_event_requires_text() -> None:
|
|
bad = _call(system_handlers["system-event"], params={})
|
|
assert bad["ok"] is False
|
|
assert bad["error"]["code"] == "INVALID_REQUEST"
|
|
|
|
|
|
def test_cron_update_requires_id() -> None:
|
|
bad = _call(cron_handlers["cron.update"], params={"patch": {"name": "x"}})
|
|
assert bad["ok"] is False
|
|
assert bad["error"]["code"] == "INVALID_REQUEST"
|
|
|
|
|
|
def test_cron_add_and_run_fallback() -> None:
|
|
add = _call(cron_handlers["cron.add"], params={"name": "job-a", "schedule": "*/5 * * * *"})
|
|
assert add["ok"] is True
|
|
assert (add["payload"] or {}).get("id") is not None
|
|
|
|
run = _call(cron_handlers["cron.run"], params={"id": "job-a", "mode": "force"})
|
|
assert run["ok"] is True
|
|
assert (run["payload"] or {}).get("ran") is True
|
|
|
|
|
|
def test_image_generate_falls_back_to_registered_openai_provider() -> None:
|
|
# Get providers via plugin load, then inject into gateway context.
|
|
from oclaw.interfaces.gateway.server_plugins import load_gateway_plugins
|
|
|
|
plugin_out = load_gateway_plugins(
|
|
cfg={"plugins": {"enabled": ["openai"]}},
|
|
workspace_dir=".",
|
|
log={},
|
|
core_gateway_handlers={},
|
|
base_methods=["ping"],
|
|
)
|
|
providers = plugin_out.plugin_registry.get("image_generation_providers") or []
|
|
res = _call(
|
|
image_handlers["image.generate"],
|
|
params={"prompt": "a cat", "provider": "openai", "size": "1024x1024"},
|
|
context={"image_generation_providers": providers},
|
|
)
|
|
assert res["ok"] is True
|
|
payload = res["payload"] or {}
|
|
assert payload.get("ok") is True
|
|
assert payload.get("provider") == "openai"
|
|
assert payload.get("model") == "gpt-image-1"
|
|
assert payload.get("prompt") == "a cat"
|
|
|
|
|
|
def test_image_generate_rejects_unknown_provider_with_not_found() -> None:
|
|
res = _call(
|
|
image_handlers["image.generate"],
|
|
params={"prompt": "a cat", "provider": "missing-provider"},
|
|
context={"image_generation_providers": []},
|
|
)
|
|
assert res["ok"] is False
|
|
assert (res["error"] or {}).get("code") == "NOT_FOUND"
|
|
|
|
|
|
def test_image_generate_rejects_invalid_optional_params() -> None:
|
|
bad_provider = _call(
|
|
image_handlers["image.generate"],
|
|
params={"prompt": "x", "provider": " "},
|
|
)
|
|
assert bad_provider["ok"] is False
|
|
assert (bad_provider["error"] or {}).get("code") == "INVALID_REQUEST"
|
|
|
|
bad_size = _call(
|
|
image_handlers["image.generate"],
|
|
params={"prompt": "x", "size": 1024},
|
|
)
|
|
assert bad_size["ok"] is False
|
|
assert (bad_size["error"] or {}).get("code") == "INVALID_REQUEST"
|
|
|
|
|
|
def test_image_generate_prefers_configured_default_provider() -> None:
|
|
providers = [
|
|
{"id": "openai", "generate": lambda **kw: {"model": "gpt-image-1", "prompt": kw.get("prompt")}},
|
|
{"id": "alt", "generate": lambda **kw: {"model": "alt-image", "prompt": kw.get("prompt")}},
|
|
]
|
|
res = _call(
|
|
image_handlers["image.generate"],
|
|
params={"prompt": "city skyline"},
|
|
context={
|
|
"image_generation_providers": providers,
|
|
"config": {"image": {"defaultProvider": "alt"}},
|
|
},
|
|
)
|
|
assert res["ok"] is True
|
|
payload = res["payload"] or {}
|
|
assert payload.get("provider") == "alt"
|
|
assert payload.get("model") == "alt-image"
|
|
|
|
|
|
def test_image_generate_uses_priority_list_when_no_explicit_or_default() -> None:
|
|
providers = [
|
|
{"id": "openai", "generate": lambda **kw: {"model": "gpt-image-1", "prompt": kw.get("prompt")}},
|
|
{"id": "foo", "generate": lambda **kw: {"model": "foo-image", "prompt": kw.get("prompt")}},
|
|
]
|
|
res = _call(
|
|
image_handlers["image.generate"],
|
|
params={"prompt": "forest"},
|
|
context={
|
|
"image_generation_providers": providers,
|
|
"config": {"image": {"providerPriority": ["bar", "foo", "openai"]}},
|
|
},
|
|
)
|
|
assert res["ok"] is True
|
|
payload = res["payload"] or {}
|
|
assert payload.get("provider") == "foo"
|
|
assert payload.get("model") == "foo-image"
|
|
|
|
|
|
def test_image_generate_filters_non_image_capability_providers() -> None:
|
|
providers = [
|
|
{
|
|
"id": "text-only",
|
|
"capabilities": {"image_generation": False},
|
|
"generate": lambda **kw: {"model": "should-not-use", "prompt": kw.get("prompt")},
|
|
},
|
|
{
|
|
"id": "image-ok",
|
|
"capabilities": {"image_generation": True},
|
|
"generate": lambda **kw: {"model": "ok-image", "prompt": kw.get("prompt")},
|
|
},
|
|
]
|
|
res = _call(
|
|
image_handlers["image.generate"],
|
|
params={"prompt": "mountain"},
|
|
context={"image_generation_providers": providers},
|
|
)
|
|
assert res["ok"] is True
|
|
payload = res["payload"] or {}
|
|
assert payload.get("provider") == "image-ok"
|
|
assert payload.get("model") == "ok-image"
|
|
|
|
|
|
def test_channels_status_exposes_image_generation_providers_from_context() -> None:
|
|
providers = [{"id": "openai", "label": "OpenAI Images"}, {"id": "x"}]
|
|
res = _call(
|
|
channels_handlers["channels.status"],
|
|
params={"probe": False},
|
|
context={"image_generation_providers": providers},
|
|
)
|
|
assert res["ok"] is True
|
|
runtime = (res["payload"] or {}).get("runtime") or {}
|
|
assert isinstance(runtime.get("image_generation_providers"), list)
|
|
assert runtime["image_generation_providers"][0]["id"] == "openai"
|
|
|
|
|
|
def test_doctor_memory_status_shape_and_hook() -> None:
|
|
out = _call(doctor_handlers["doctor.memory.status"], params={})
|
|
assert out["ok"] is True
|
|
payload = out["payload"] or {}
|
|
assert payload.get("agentId") == "main"
|
|
assert isinstance(payload.get("dreaming"), dict)
|
|
|
|
ctx = {"doctor_memory_status": lambda: {"agentId": "custom", "embedding": {"ok": False}}}
|
|
hooked = _call(doctor_handlers["doctor.memory.status"], params={}, context=ctx)
|
|
assert hooked["ok"] is True
|
|
assert (hooked["payload"] or {}).get("agentId") == "custom"
|
|
|
|
|
|
def test_device_pair_remove_denied_for_cross_device_non_admin() -> None:
|
|
res = _call(
|
|
device_handlers["device.pair.remove"],
|
|
params={"deviceId": "dev-b"},
|
|
client={"connect": {"device": {"id": "dev-a"}, "scopes": ["operator.write"]}},
|
|
)
|
|
assert res["ok"] is False
|
|
assert res["error"]["code"] == "INVALID_REQUEST"
|
|
|
|
|
|
def test_device_token_rotate_requires_fields() -> None:
|
|
bad = _call(device_handlers["device.token.rotate"], params={"deviceId": "dev-a"})
|
|
assert bad["ok"] is False
|
|
assert bad["error"]["code"] == "INVALID_REQUEST"
|
|
|
|
ok = _call(device_handlers["device.token.rotate"], params={"deviceId": "dev-a", "role": "worker"})
|
|
assert ok["ok"] is True
|
|
assert (ok["payload"] or {}).get("deviceId") == "dev-a"
|
|
|
|
|
|
def test_models_list_fallback_and_filtered() -> None:
|
|
fallback = _call(models_handlers["models.list"], params={}, context={})
|
|
assert fallback["ok"] is True
|
|
assert (fallback["payload"] or {}).get("models") == []
|
|
|
|
context = {
|
|
"loadGatewayModelCatalog": lambda: [{"provider": "openai", "model": "gpt-4.1"}, {"provider": "x", "model": "y"}],
|
|
"filterAllowedModels": lambda rows: [r for r in rows if r.get("provider") == "openai"],
|
|
}
|
|
filtered = _call(models_handlers["models.list"], params={}, context=context)
|
|
assert filtered["ok"] is True
|
|
models = (filtered["payload"] or {}).get("models") or []
|
|
assert len(models) == 1
|
|
assert models[0]["provider"] == "openai"
|
|
|
|
|
|
def test_models_auth_status_cache_and_refresh() -> None:
|
|
invalidate_model_auth_status_cache()
|
|
calls = {"n": 0}
|
|
|
|
def _loader(): # noqa: ANN001
|
|
calls["n"] += 1
|
|
return [
|
|
{
|
|
"provider": "openai",
|
|
"displayName": "OpenAI",
|
|
"status": "ok",
|
|
"profiles": [{"profileId": "p1", "type": "oauth", "status": "ok"}],
|
|
}
|
|
]
|
|
|
|
ctx = {"load_models_auth_status": _loader}
|
|
first = _call(models_auth_status_handlers["models.authStatus"], params={}, context=ctx)
|
|
second = _call(models_auth_status_handlers["models.authStatus"], params={}, context=ctx)
|
|
refreshed = _call(models_auth_status_handlers["models.authStatus"], params={"refresh": True}, context=ctx)
|
|
|
|
assert first["ok"] is True
|
|
assert second["ok"] is True
|
|
assert refreshed["ok"] is True
|
|
assert calls["n"] == 2
|
|
assert (second["meta"] or {}).get("cached") is True
|
|
|
|
|
|
def test_push_test_requires_node_id() -> None:
|
|
bad = _call(push_handlers["push.test"], params={})
|
|
assert bad["ok"] is False
|
|
assert bad["error"]["code"] == "INVALID_REQUEST"
|
|
|
|
ok = _call(push_handlers["push.test"], params={"nodeId": "node-1"})
|
|
assert ok["ok"] is True
|
|
assert (ok["payload"] or {}).get("nodeId") == "node-1"
|
|
|
|
|
|
def test_update_run_returns_result_shape() -> None:
|
|
out = _call(update_handlers["update.run"], params={"timeoutMs": 5000, "sessionKey": "main"})
|
|
assert out["ok"] is True
|
|
payload = out["payload"] or {}
|
|
assert "result" in payload
|
|
assert "sentinel" in payload
|
|
|
|
|
|
def test_voicewake_set_requires_triggers_array() -> None:
|
|
bad = _call(voicewake_handlers["voicewake.set"], params={"triggers": "hey"})
|
|
assert bad["ok"] is False
|
|
assert bad["error"]["code"] == "INVALID_REQUEST"
|
|
ok = _call(voicewake_handlers["voicewake.set"], params={"triggers": ["Hey Oclaw", " ", "hey oclaw"]})
|
|
assert ok["ok"] is True
|
|
assert (ok["payload"] or {}).get("triggers") == ["Hey Oclaw"]
|
|
|
|
|
|
def test_wizard_start_next_status_cancel_flow() -> None:
|
|
context: dict[str, Any] = {}
|
|
started = _call(wizard_handlers["wizard.start"], params={"mode": "quick"}, context=context)
|
|
assert started["ok"] is True
|
|
session_id = str((started["payload"] or {}).get("sessionId") or "")
|
|
assert session_id != ""
|
|
status = _call(wizard_handlers["wizard.status"], params={"sessionId": session_id}, context=context)
|
|
assert status["ok"] is True
|
|
assert (status["payload"] or {}).get("status") == "running"
|
|
nxt = _call(
|
|
wizard_handlers["wizard.next"],
|
|
params={"sessionId": session_id, "answer": {"stepId": "step-1", "value": "x"}},
|
|
context=context,
|
|
)
|
|
assert nxt["ok"] is True
|
|
cancelled = _call(wizard_handlers["wizard.cancel"], params={"sessionId": session_id}, context=context)
|
|
assert cancelled["ok"] is True
|
|
assert (cancelled["payload"] or {}).get("status") == "cancelled"
|
|
|
|
|
|
def test_tts_convert_requires_text() -> None:
|
|
bad = _call(tts_handlers["tts.convert"], params={})
|
|
assert bad["ok"] is False
|
|
assert bad["error"]["code"] == "INVALID_REQUEST"
|
|
ok = _call(tts_handlers["tts.convert"], params={"text": "hello"})
|
|
assert ok["ok"] is True
|
|
assert (ok["payload"] or {}).get("audioPath")
|
|
|
|
|
|
def test_web_login_provider_required() -> None:
|
|
bad = _call(web_handlers["web.login.start"], params={})
|
|
assert bad["ok"] is False
|
|
assert bad["error"]["code"] == "INVALID_REQUEST"
|
|
|
|
context = {
|
|
"resolve_web_login_provider": lambda: {
|
|
"id": "wechat",
|
|
"loginWithQrStart": lambda _p: {"connected": False, "qrDataUrl": "data:image/png;base64,abc"},
|
|
"loginWithQrWait": lambda _p: {"connected": True},
|
|
}
|
|
}
|
|
started = _call(web_handlers["web.login.start"], params={}, context=context)
|
|
assert started["ok"] is True
|
|
waited = _call(web_handlers["web.login.wait"], params={}, context=context)
|
|
assert waited["ok"] is True
|
|
|
|
|
|
def test_tools_catalog_and_effective_basic_shapes() -> None:
|
|
cat = _call(tools_catalog_handlers["tools.catalog"], params={"includePlugins": False})
|
|
assert cat["ok"] is True
|
|
assert (cat["payload"] or {}).get("agentId")
|
|
assert isinstance((cat["payload"] or {}).get("groups"), list)
|
|
|
|
eff_bad = _call(tools_effective_handlers["tools.effective"], params={})
|
|
assert eff_bad["ok"] is False
|
|
assert eff_bad["error"]["code"] == "INVALID_REQUEST"
|
|
|
|
eff = _call(tools_effective_handlers["tools.effective"], params={"sessionKey": "main"})
|
|
assert eff["ok"] is True
|
|
assert (eff["payload"] or {}).get("sessionKey") == "main"
|
|
|
|
|
|
def test_talk_speak_requires_text_and_mode_shape() -> None:
|
|
bad = _call(talk_handlers["talk.speak"], params={})
|
|
assert bad["ok"] is False
|
|
assert bad["error"]["code"] == "INVALID_REQUEST"
|
|
|
|
ok = _call(talk_handlers["talk.speak"], params={"text": "hi"})
|
|
assert ok["ok"] is True
|
|
assert (ok["payload"] or {}).get("audioBase64")
|
|
|
|
mode = _call(talk_handlers["talk.mode"], params={"enabled": True})
|
|
assert mode["ok"] is True
|
|
assert (mode["payload"] or {}).get("enabled") is True
|
|
|
|
|
|
def test_usage_handlers_basic_shapes_and_cache() -> None:
|
|
status = _call(usage_handlers["usage.status"], params={})
|
|
assert status["ok"] is True
|
|
assert "providers" in (status["payload"] or {})
|
|
|
|
calls = {"n": 0}
|
|
|
|
def _cost_loader(req: dict[str, Any]) -> dict[str, Any]:
|
|
calls["n"] += 1
|
|
return {"startMs": req["startMs"], "endMs": req["endMs"], "totals": {"totalCost": 1}}
|
|
|
|
ctx = {"load_cost_usage_summary": _cost_loader}
|
|
c1 = _call(usage_handlers["usage.cost"], params={"days": 1}, context=ctx)
|
|
c2 = _call(usage_handlers["usage.cost"], params={"days": 1}, context=ctx)
|
|
assert c1["ok"] is True
|
|
assert c2["ok"] is True
|
|
assert calls["n"] == 1
|
|
assert (c2["meta"] or {}).get("cached") is True
|
|
|
|
sess_bad = _call(usage_handlers["sessions.usage"], params={"key": 123})
|
|
assert sess_bad["ok"] is False
|
|
assert sess_bad["error"]["code"] == "INVALID_REQUEST"
|
|
sess_ok = _call(usage_handlers["sessions.usage"], params={"key": "sess-1", "limit": 5})
|
|
assert sess_ok["ok"] is True
|
|
assert isinstance((sess_ok["payload"] or {}).get("sessions"), list)
|
|
|
|
|
|
def test_exec_approvals_base_hash_and_node_shapes() -> None:
|
|
context: dict[str, Any] = {}
|
|
got = _call(exec_approvals_handlers["exec.approvals.get"], params={}, context=context)
|
|
assert got["ok"] is True
|
|
payload = got["payload"] or {}
|
|
assert "path" in payload and "file" in payload
|
|
|
|
bad_set = _call(exec_approvals_handlers["exec.approvals.set"], params={}, context=context)
|
|
assert bad_set["ok"] is False
|
|
assert bad_set["error"]["code"] == "INVALID_REQUEST"
|
|
|
|
set1 = _call(
|
|
exec_approvals_handlers["exec.approvals.set"],
|
|
params={"file": {"allow": ["echo"]}},
|
|
context=context,
|
|
)
|
|
assert set1["ok"] is True
|
|
h = str(((set1["payload"] or {}).get("hash") or "")).strip()
|
|
assert h != ""
|
|
|
|
stale = _call(
|
|
exec_approvals_handlers["exec.approvals.set"],
|
|
params={"file": {"allow": ["ls"]}, "baseHash": "wrong-hash"},
|
|
context=context,
|
|
)
|
|
assert stale["ok"] is False
|
|
assert stale["error"]["code"] == "INVALID_REQUEST"
|
|
|
|
node_bad = _call(exec_approvals_handlers["exec.approvals.node.get"], params={"nodeId": ""}, context=context)
|
|
assert node_bad["ok"] is False
|
|
assert node_bad["error"]["code"] == "INVALID_REQUEST"
|
|
|
|
node_ok = _call(exec_approvals_handlers["exec.approvals.node.get"], params={"nodeId": "n1"}, context=context)
|
|
assert node_ok["ok"] is True
|
|
|
|
|
|
def test_node_pending_enqueue_then_drain() -> None:
|
|
context: dict[str, Any] = {}
|
|
enqueue = _call(
|
|
node_pending_handlers["node.pending.enqueue"],
|
|
params={"nodeId": "n1", "type": "sync", "wake": True},
|
|
context=context,
|
|
)
|
|
assert enqueue["ok"] is True
|
|
assert (enqueue["payload"] or {}).get("wakeTriggered") is True
|
|
|
|
# drain requires connected client identity
|
|
no_client = _call(node_pending_handlers["node.pending.drain"], params={}, context=context)
|
|
assert no_client["ok"] is False
|
|
assert no_client["error"]["code"] == "INVALID_REQUEST"
|
|
|
|
drained = _call(
|
|
node_pending_handlers["node.pending.drain"],
|
|
params={"maxItems": 10},
|
|
context=context,
|
|
client={"connect": {"device": {"id": "n1"}}},
|
|
)
|
|
assert drained["ok"] is True
|
|
payload = drained["payload"] or {}
|
|
assert payload.get("nodeId") == "n1"
|
|
assert int(payload.get("count") or 0) >= 1
|
|
|
|
|
|
def test_node_pair_and_invoke_basics() -> None:
|
|
context: dict[str, Any] = {}
|
|
req = _call(
|
|
node_handlers["node.pair.request"],
|
|
params={"nodeId": "node-1", "displayName": "iPhone"},
|
|
context=context,
|
|
)
|
|
assert req["ok"] is True
|
|
request_id = str(((req["payload"] or {}).get("request") or {}).get("requestId") or "")
|
|
assert request_id != ""
|
|
|
|
approved = _call(node_handlers["node.pair.approve"], params={"requestId": request_id}, context=context)
|
|
assert approved["ok"] is True
|
|
|
|
listed = _call(node_handlers["node.list"], params={}, context=context)
|
|
assert listed["ok"] is True
|
|
assert isinstance((listed["payload"] or {}).get("nodes"), list)
|
|
|
|
bad_invoke = _call(
|
|
node_handlers["node.invoke"],
|
|
params={"nodeId": "node-1", "command": "system.execApprovals.get"},
|
|
context=context,
|
|
)
|
|
assert bad_invoke["ok"] is False
|
|
assert bad_invoke["error"]["code"] == "INVALID_REQUEST"
|
|
|
|
ok_invoke = _call(
|
|
node_handlers["node.invoke"],
|
|
params={"nodeId": "node-1", "command": "screen.capture"},
|
|
context=context,
|
|
)
|
|
assert ok_invoke["ok"] is True
|
|
|
|
|
|
|
|
|