mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 00:40:45 +08:00
本次迁移将网关/通道/工具/技能/脚本与协议资源集中到新结构,统一路径常量与脚本转发机制,减少顶层噪音并保证运行与测试行为一致。 Made-with: Cursor
211 lines
7.5 KiB
Python
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
|
|
|