oclaw/tests/test_oclaw_extensions_webhooks_py.py
oliver 4a23b715a2 重构仓库目录为统一的 runtime 分层并清理历史 openclaw 残留。
本次迁移将网关/通道/工具/技能/脚本与协议资源集中到新结构,统一路径常量与脚本转发机制,减少顶层噪音并保证运行与测试行为一致。

Made-with: Cursor
2026-04-25 01:24:23 +08:00

211 lines
7.5 KiB
Python

from __future__ import annotations
import importlib.util
import sys
import types
from pathlib import Path
def _load_module(module_name: str, path: Path):
spec = importlib.util.spec_from_file_location(module_name, str(path))
if spec is None or spec.loader is None:
raise RuntimeError(f"cannot load module: {module_name} from {path}")
mod = importlib.util.module_from_spec(spec)
sys.modules[module_name] = mod
spec.loader.exec_module(mod)
return mod
def _mount_webhooks_package():
root = Path(__file__).resolve().parents[1] / "runtime" / "extensions" / "webhooks"
pkg_root = types.ModuleType("ocx")
pkg_root.__path__ = [str(root.parent)]
sys.modules["ocx"] = pkg_root
pkg = types.ModuleType("ocx.webhooks")
pkg.__path__ = [str(root)]
sys.modules["ocx.webhooks"] = pkg
runtime_api = _load_module("ocx.webhooks.runtime_api", root / "runtime_api.py")
config = _load_module("ocx.webhooks.config", root / "config.py")
http = _load_module("ocx.webhooks.http", root / "http.py")
return runtime_api, config, http
def test_webhooks_config_resolve_and_conflict() -> None:
_, config, _ = _mount_webhooks_package()
rows = config.resolve_webhooks_plugin_config(
plugin_config={
"routes": {
"r1": {
"sessionKey": "s1",
"secret": "abc",
}
}
}
)
assert len(rows) == 1
assert rows[0].path == "/plugins/webhooks/r1"
assert rows[0].controller_id == "webhooks/r1"
try:
config.resolve_webhooks_plugin_config(
plugin_config={
"routes": {
"a": {"path": "/x", "sessionKey": "s1", "secret": "k1"},
"b": {"path": "/x", "sessionKey": "s2", "secret": "k2"},
}
}
)
except ValueError as exc:
assert "conflicts" in str(exc)
else:
raise AssertionError("expected conflict error")
class _TaskFlow:
def create_managed(self, **kwargs):
return {"flowId": "f1", "goal": kwargs.get("goal"), "revision": 0, "status": "queued", "createdAt": 1, "updatedAt": 1}
def get(self, flow_id: str):
return {"flowId": flow_id, "revision": 1, "status": "running", "createdAt": 1, "updatedAt": 2}
def list(self):
return [self.get("f1")]
def request_cancel(self, **kwargs):
if kwargs.get("flow_id") == "missing":
return {"applied": False, "code": "not_found"}
return {"applied": True, "flow": self.get(kwargs.get("flow_id", "f1"))}
def resume(self, **kwargs):
flow_id = kwargs.get("flow_id")
if flow_id == "conflict":
return {"applied": False, "code": "revision_conflict", "current": self.get("f1")}
return {"applied": True, "flow": self.get(flow_id or "f1")}
def finish(self, **kwargs):
return {"applied": True, "flow": self.get(kwargs.get("flow_id", "f1"))}
def fail(self, **kwargs):
return {"applied": True, "flow": self.get(kwargs.get("flow_id", "f1"))}
def set_waiting(self, **kwargs):
return {"applied": True, "flow": self.get(kwargs.get("flow_id", "f1"))}
def cancel(self, **kwargs):
flow_id = kwargs.get("flow_id")
if flow_id == "pending":
return {"found": True, "cancelled": False, "reason": "One or more child tasks are still active."}
return {"found": True, "cancelled": True, "flow": self.get(flow_id or "f1"), "tasks": []}
def run_task(self, **kwargs):
flow_id = kwargs.get("flow_id")
if flow_id == "missing":
return {"created": False, "found": False, "reason": "not found"}
if flow_id == "terminal":
return {"created": False, "found": True, "reason": "Flow is already completed."}
return {
"created": True,
"flow": self.get(flow_id or "f1"),
"task": {
"taskId": "t1",
"runtime": kwargs.get("runtime", "subagent"),
"scopeKind": "flow",
"task": kwargs.get("task", ""),
"status": "queued",
"deliveryStatus": "queued",
"notifyPolicy": "done_only",
"createdAt": 1,
},
}
def test_webhooks_handler_auth_and_actions() -> None:
_, _, http = _mount_webhooks_package()
target = http.TaskFlowWebhookTarget(
route_id="r1",
path="/hooks/a",
secret_input="sekret",
secret_config_path="plugins.entries.webhooks.routes.r1.secret",
default_controller_id="webhooks/r1",
task_flow=_TaskFlow(),
)
handler = http.create_taskflow_webhook_request_handler(cfg={}, targets_by_path={"/hooks/a": [target]})
unauthorized = handler({"path": "/hooks/a", "headers": {}, "json": {"action": "list_flows"}})
assert unauthorized["ok"] is False
assert unauthorized["code"] == "unauthorized"
created = handler(
{
"path": "/hooks/a",
"headers": {"authorization": "Bearer sekret"},
"json": {"action": "create_flow", "goal": "ship it"},
}
)
assert created["ok"] is True
assert created["statusCode"] == 200
assert created["result"]["flow"]["flowId"] == "f1"
missing = handler(
{
"path": "/hooks/a",
"headers": {"x-oclaw-webhook-secret": "sekret"},
"json": {"action": "request_cancel", "flowId": "missing", "expectedRevision": 1},
}
)
assert missing["ok"] is False
assert missing["statusCode"] == 404
assert missing["code"] == "not_found"
def test_webhooks_handler_action_status_mappings() -> None:
_, _, http = _mount_webhooks_package()
target = http.TaskFlowWebhookTarget(
route_id="r2",
path="/hooks/b",
secret_input="sekret2",
secret_config_path="plugins.entries.webhooks.routes.r2.secret",
default_controller_id="webhooks/r2",
task_flow=_TaskFlow(),
)
handler = http.create_taskflow_webhook_request_handler(cfg={}, targets_by_path={"/hooks/b": [target]})
def _call(payload: dict):
return handler(
{
"path": "/hooks/b",
"headers": {"authorization": "Bearer sekret2"},
"json": payload,
}
)
resume_conflict = _call({"action": "resume_flow", "flowId": "conflict", "expectedRevision": 1})
assert resume_conflict["ok"] is False
assert resume_conflict["statusCode"] == 409
assert resume_conflict["code"] == "revision_conflict"
cancel_pending = _call({"action": "cancel_flow", "flowId": "pending"})
assert cancel_pending["ok"] is True
assert cancel_pending["statusCode"] == 202
assert cancel_pending["code"] == "cancel_pending"
run_task_missing = _call(
{"action": "run_task", "flowId": "missing", "runtime": "subagent", "task": "ping"}
)
assert run_task_missing["ok"] is False
assert run_task_missing["statusCode"] == 404
assert run_task_missing["code"] == "not_found"
run_task_terminal = _call(
{"action": "run_task", "flowId": "terminal", "runtime": "subagent", "task": "ping"}
)
assert run_task_terminal["ok"] is False
assert run_task_terminal["statusCode"] == 409
assert run_task_terminal["code"] == "terminal"
run_task_ok = _call({"action": "run_task", "flowId": "f1", "runtime": "subagent", "task": "ping"})
assert run_task_ok["ok"] is True
assert run_task_ok["statusCode"] == 200
assert run_task_ok["result"]["created"] is True