docs: 为全部代码补充中文注释并加入 AGENTS 注释规范
This commit is contained in:
@@ -19,3 +19,10 @@
|
||||
|
||||
- Python 环境统一使用 uv 管理,不直接使用 pip 修改依赖。
|
||||
- 测试必须达到 100% 行覆盖率,且只能通过调用真实代码路径覆盖。
|
||||
|
||||
## 代码注释规范
|
||||
|
||||
- 本仓库所有源码(Python、TOML 等支持注释的文件)必须配有详细中文注释,说明模块职责、协议模型字段语义、类与函数的作用以及关键逻辑,确保后续维护人员可以快速理解代码工作原理。
|
||||
- 新增或修改代码时,必须同步补充或更新对应注释;不得删除已有注释。
|
||||
- 测试代码同样必须配有中文注释,说明每条测试验证的行为。
|
||||
- JSON 数据文件不支持注释,字段语义以本仓库 `models.py` 中的模型注释为准;修改 JSON 字段时须同步更新文档。
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
# WOV SDK 项目配置:使用 uv 管理环境与依赖。
|
||||
[project]
|
||||
name = "wov-sdk"
|
||||
version = "0.1.0"
|
||||
description = "WOV node protocol and SDK"
|
||||
requires-python = ">=3.11"
|
||||
|
||||
# 开发依赖放在 dev 组,由 uv sync 默认安装。
|
||||
[dependency-groups]
|
||||
dev = ["pytest", "pytest-cov"]
|
||||
|
||||
# pytest 配置:只扫描 tests 目录,并强制 100% 行覆盖率。
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
addopts = "--cov=wov_sdk --cov-report=term-missing --cov-fail-under=100"
|
||||
|
||||
# 作为可发布包处理,便于节点仓库通过路径依赖引用。
|
||||
[tool.uv]
|
||||
package = true
|
||||
|
||||
# 使用 src 布局,源码位于 src/wov_sdk。
|
||||
[tool.setuptools]
|
||||
package-dir = {"" = "src"}
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
"""WOV SDK 公共导出入口。
|
||||
|
||||
节点仓库和 wov-api 统一从 wov_sdk 导入协议模型,而无需关心具体模块路径。
|
||||
"""
|
||||
|
||||
from wov_sdk.models import (
|
||||
HealthResponse,
|
||||
InvokeRequest,
|
||||
@@ -9,6 +14,7 @@ from wov_sdk.models import (
|
||||
WorkflowNode,
|
||||
)
|
||||
|
||||
# 对外稳定的公共 API 清单;新增模型时必须同步追加到这里。
|
||||
__all__ = [
|
||||
"HealthResponse",
|
||||
"InvokeRequest",
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
"""WOV 节点协议核心数据模型。
|
||||
|
||||
本模块定义节点 Manifest、调用请求/响应、健康检查、进度事件以及工作流 DAG 的
|
||||
通用数据结构。所有节点仓库和 wov-api 都依赖这些类,字段语义必须长期保持稳定,
|
||||
新增能力时只能向后兼容地扩展字段。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
@@ -6,32 +13,54 @@ from typing import Any
|
||||
|
||||
|
||||
def _require_non_empty(value: str, name: str) -> None:
|
||||
"""校验必填字符串字段,空字符串或纯空白字符串都会被拒绝。"""
|
||||
if not value or not value.strip():
|
||||
raise ValueError(f"{name} must not be empty")
|
||||
|
||||
|
||||
@dataclass
|
||||
class NodeManifest:
|
||||
"""节点注册清单:描述节点能力、启动命令、生命周期与资源参数。
|
||||
|
||||
该清单由节点仓库根目录的 node.manifest.json 提供,也通过管理 API 注册到
|
||||
wov-api 的 SQLite 节点注册表中。NodeManager 依据它启动和回收节点进程。
|
||||
"""
|
||||
|
||||
# 节点稳定唯一 ID,例如 faster-whisper;注册后不可随意更改。
|
||||
id: str
|
||||
# 展示名称,仅用于管理后台等界面。
|
||||
name: str
|
||||
# 节点版本号,与节点仓库 Git tag 保持一致。
|
||||
version: str
|
||||
# 能力标识,工作流通过 node_type 引用能力,而不是直接绑定具体仓库。
|
||||
capability: str
|
||||
# 启动命令;首项为 python 时 NodeManager 会替换为节点仓库 .venv 解释器。
|
||||
command: list[str]
|
||||
# 节点代码所在子仓库目录,相对 WOV 工作区根目录。
|
||||
repo_dir: str = "."
|
||||
# 启动节点进程时额外注入的环境变量。
|
||||
env: dict[str, str] = field(default_factory=dict)
|
||||
# 输入字段 schema,当前主要用于文档展示,后续可用于运行时校验。
|
||||
input_schema: dict[str, Any] = field(default_factory=dict)
|
||||
# 输出字段 schema,用于描述节点产物的名称与类型。
|
||||
output_schema: dict[str, Any] = field(default_factory=dict)
|
||||
# 单个实例允许并发承载的调用数;1 表示严格串行复用。
|
||||
max_concurrency: int = 1
|
||||
# 空闲回收 TTL(秒);0 表示调用结束即可被回收。
|
||||
idle_ttl_seconds: int = 300
|
||||
# 节点进程启动后等待就绪输出的超时(秒)。
|
||||
health_timeout_seconds: int = 10
|
||||
# 常驻标记;开启后即使空闲超过 TTL 也不会被回收。
|
||||
keep_warm: bool = False
|
||||
|
||||
def validate(self) -> None:
|
||||
"""校验 manifest 必填字段与数值边界,非法配置抛出 ValueError。"""
|
||||
_require_non_empty(self.id, "id")
|
||||
_require_non_empty(self.name, "name")
|
||||
_require_non_empty(self.version, "version")
|
||||
_require_non_empty(self.capability, "capability")
|
||||
_require_non_empty(self.repo_dir, "repo_dir")
|
||||
# 命令不能为空,否则节点进程无法启动。
|
||||
if not self.command:
|
||||
raise ValueError("command must not be empty")
|
||||
if self.max_concurrency < 1:
|
||||
@@ -42,6 +71,7 @@ class NodeManifest:
|
||||
raise ValueError("health_timeout_seconds must be >= 1")
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""转换为可 JSON 序列化的普通字典。"""
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
@@ -60,16 +90,19 @@ class NodeManifest:
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "NodeManifest":
|
||||
"""从注册 API 或 JSON 文件解析出的字典恢复 manifest。"""
|
||||
return cls(
|
||||
id=str(data["id"]),
|
||||
name=str(data["name"]),
|
||||
version=str(data["version"]),
|
||||
capability=str(data["capability"]),
|
||||
# command 可能缺省,解析时提供空列表兜底。
|
||||
command=[str(item) for item in data.get("command", [])],
|
||||
repo_dir=str(data.get("repo_dir", ".")),
|
||||
env={str(k): str(v) for k, v in data.get("env", {}).items()},
|
||||
input_schema=dict(data.get("input_schema", {})),
|
||||
output_schema=dict(data.get("output_schema", {})),
|
||||
# 数值字段缺省时使用与 dataclass 一致的默认值。
|
||||
max_concurrency=int(data.get("max_concurrency", 1)),
|
||||
idle_ttl_seconds=int(data.get("idle_ttl_seconds", 300)),
|
||||
health_timeout_seconds=int(data.get("health_timeout_seconds", 10)),
|
||||
@@ -78,6 +111,7 @@ class NodeManifest:
|
||||
|
||||
@classmethod
|
||||
def load(cls, path: str) -> "NodeManifest":
|
||||
"""从磁盘上的 node.manifest.json 加载并校验 manifest。"""
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
manifest = cls.from_dict(json.load(f))
|
||||
manifest.validate()
|
||||
@@ -86,13 +120,21 @@ class NodeManifest:
|
||||
|
||||
@dataclass
|
||||
class InvokeRequest:
|
||||
"""节点调用请求:由调度器或管理后台发送给节点 HTTP 服务。"""
|
||||
|
||||
# 工作流运行 ID,用于追踪一次完整执行。
|
||||
run_id: str
|
||||
# 实际承载本次调用的节点实例 ID,由 NodeManager 回填。
|
||||
node_instance_id: str
|
||||
# 输入产物映射,key 为输入名,value 为产物 URI 或直接文本。
|
||||
inputs: dict[str, Any] = field(default_factory=dict)
|
||||
# 节点运行参数,例如采样率、语言、模型路径等。
|
||||
params: dict[str, Any] = field(default_factory=dict)
|
||||
# 节点产物输出目录。
|
||||
output_dir: str = "."
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""转换为可 JSON 序列化的普通字典。"""
|
||||
return {
|
||||
"run_id": self.run_id,
|
||||
"node_instance_id": self.node_instance_id,
|
||||
@@ -103,6 +145,7 @@ class InvokeRequest:
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "InvokeRequest":
|
||||
"""从 HTTP 请求 JSON 解析调用请求。"""
|
||||
return cls(
|
||||
run_id=str(data["run_id"]),
|
||||
node_instance_id=str(data["node_instance_id"]),
|
||||
@@ -114,11 +157,17 @@ class InvokeRequest:
|
||||
|
||||
@dataclass
|
||||
class InvokeResponse:
|
||||
"""节点调用响应:completed 表示成功,failed 表示执行失败。"""
|
||||
|
||||
# 执行状态,固定为 completed / failed。
|
||||
status: str
|
||||
# 输出产物映射,key 为输出名,value 为产物 URI。
|
||||
outputs: dict[str, Any] = field(default_factory=dict)
|
||||
# 失败原因,仅在 failed 时有意义。
|
||||
error: str | None = None
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""转换为可 JSON 序列化的普通字典。"""
|
||||
return {
|
||||
"status": self.status,
|
||||
"outputs": self.outputs,
|
||||
@@ -127,7 +176,9 @@ class InvokeResponse:
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "InvokeResponse":
|
||||
"""从 HTTP 响应 JSON 解析调用结果。"""
|
||||
return cls(
|
||||
# 缺省按失败处理,避免未知状态被误判为成功。
|
||||
status=str(data.get("status", "failed")),
|
||||
outputs=dict(data.get("outputs", {})),
|
||||
error=data.get("error"),
|
||||
@@ -136,11 +187,14 @@ class InvokeResponse:
|
||||
|
||||
@dataclass
|
||||
class HealthResponse:
|
||||
"""节点健康检查响应:节点进程就绪后返回 ok 与自身标识。"""
|
||||
|
||||
status: str
|
||||
node_id: str
|
||||
version: str
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""转换为可 JSON 序列化的普通字典。"""
|
||||
return {
|
||||
"status": self.status,
|
||||
"node_id": self.node_id,
|
||||
@@ -150,12 +204,15 @@ class HealthResponse:
|
||||
|
||||
@dataclass
|
||||
class ProgressEvent:
|
||||
"""进度事件:预留用于节点向调度器上报执行进度。"""
|
||||
|
||||
run_id: str
|
||||
node_id: str
|
||||
progress: float
|
||||
message: str | None = None
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""转换为可 JSON 序列化的普通字典。"""
|
||||
return {
|
||||
"run_id": self.run_id,
|
||||
"node_id": self.node_id,
|
||||
@@ -166,12 +223,19 @@ class ProgressEvent:
|
||||
|
||||
@dataclass
|
||||
class WorkflowNode:
|
||||
"""工作流中的一个节点:声明节点类型、参数和输入引用。"""
|
||||
|
||||
# 节点在 DAG 内的唯一 ID,例如 extract、asr。
|
||||
id: str
|
||||
# 引用的节点能力,例如 ffmpeg-extract、faster-whisper。
|
||||
node_type: str
|
||||
# 传递给节点 invoke 的 params。
|
||||
params: dict[str, Any] = field(default_factory=dict)
|
||||
# 输入引用,value 形如 "前序节点ID.输出名" 或 "input.入口字段"。
|
||||
inputs: dict[str, str] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""转换为可 JSON 序列化的普通字典。"""
|
||||
return {
|
||||
"id": self.id,
|
||||
"node_type": self.node_type,
|
||||
@@ -181,6 +245,7 @@ class WorkflowNode:
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "WorkflowNode":
|
||||
"""从工作流定义 JSON 解析节点。"""
|
||||
return cls(
|
||||
id=str(data["id"]),
|
||||
node_type=str(data["node_type"]),
|
||||
@@ -191,30 +256,40 @@ class WorkflowNode:
|
||||
|
||||
@dataclass
|
||||
class WorkflowEdge:
|
||||
"""工作流有向边:from_node 的输出流向 to_node 的输入。"""
|
||||
|
||||
from_node: str
|
||||
to_node: str
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""转换为 JSON 时使用 from/to 短字段名。"""
|
||||
return {"from": self.from_node, "to": self.to_node}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "WorkflowEdge":
|
||||
"""从工作流定义 JSON 解析边。"""
|
||||
return cls(from_node=str(data["from"]), to_node=str(data["to"]))
|
||||
|
||||
|
||||
@dataclass
|
||||
class WorkflowDefinition:
|
||||
"""工作流 DAG 定义:包含节点列表、依赖边和输入输出映射。"""
|
||||
|
||||
name: str
|
||||
version: int
|
||||
nodes: list[WorkflowNode] = field(default_factory=list)
|
||||
edges: list[WorkflowEdge] = field(default_factory=list)
|
||||
# 用户上传入口与入口字段名的映射,例如 {"video_uri": "file"}。
|
||||
entry_inputs: dict[str, Any] = field(default_factory=dict)
|
||||
# 最终对外暴露的产物别名映射,例如 {"ass": "ass.ass_uri"}。
|
||||
final_outputs: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def validate(self) -> None:
|
||||
"""校验 DAG 基本约束:名称、版本、节点 ID 唯一、边引用有效。"""
|
||||
_require_non_empty(self.name, "name")
|
||||
if self.version < 1:
|
||||
raise ValueError("version must be >= 1")
|
||||
# 节点 ID 集合用于检查重复和边引用。
|
||||
node_ids = {node.id for node in self.nodes}
|
||||
if len(node_ids) != len(self.nodes):
|
||||
raise ValueError("workflow node ids must be unique")
|
||||
@@ -223,6 +298,7 @@ class WorkflowDefinition:
|
||||
raise ValueError(f"edge references unknown node: {edge}")
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""转换为可 JSON 序列化的普通字典。"""
|
||||
return {
|
||||
"name": self.name,
|
||||
"version": self.version,
|
||||
@@ -234,6 +310,7 @@ class WorkflowDefinition:
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "WorkflowDefinition":
|
||||
"""从工作流定义 JSON 解析 DAG。"""
|
||||
return cls(
|
||||
name=str(data["name"]),
|
||||
version=int(data.get("version", 1)),
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
"""标准节点 HTTP 服务端。
|
||||
|
||||
每个节点仓库通过 run_node 启动自己的 HTTP 服务,对外提供 GET /health 与
|
||||
POST /invoke 两个协议端点。wov-api 的 NodeManager 通过读取启动时打印的
|
||||
WOV_NODE_READY 行获得端口,再据此进行健康检查和调用。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
@@ -6,12 +13,15 @@ from typing import Callable
|
||||
|
||||
from wov_sdk.models import HealthResponse, InvokeRequest, InvokeResponse, NodeManifest
|
||||
|
||||
# 节点业务处理函数签名:接收 InvokeRequest,返回 InvokeResponse。
|
||||
InvokeHandler = Callable[[InvokeRequest], InvokeResponse]
|
||||
|
||||
|
||||
def _send_json(handler: BaseHTTPRequestHandler, status: int, payload: dict) -> None:
|
||||
"""以 UTF-8 JSON 形式向客户端写出 HTTP 响应。"""
|
||||
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||
handler.send_response(status)
|
||||
# 显式声明 JSON 字符集,保证中文内容在旧客户端下也能正确解析。
|
||||
handler.send_header("Content-Type", "application/json; charset=utf-8")
|
||||
handler.send_header("Content-Length", str(len(body)))
|
||||
handler.end_headers()
|
||||
@@ -19,7 +29,11 @@ def _send_json(handler: BaseHTTPRequestHandler, status: int, payload: dict) -> N
|
||||
|
||||
|
||||
class NodeHTTPServer(ThreadingHTTPServer):
|
||||
"""线程化节点 HTTP 服务器,保存 manifest 和业务处理函数。"""
|
||||
|
||||
# 每个请求使用独立线程,避免单个慢请求阻塞其他调用。
|
||||
daemon_threads = True
|
||||
# 允许地址复用,方便节点快速重启。
|
||||
allow_reuse_address = True
|
||||
|
||||
def __init__(
|
||||
@@ -28,18 +42,23 @@ class NodeHTTPServer(ThreadingHTTPServer):
|
||||
manifest: NodeManifest,
|
||||
handler: InvokeHandler,
|
||||
) -> None:
|
||||
"""绑定节点清单与调用处理器,并使用内部请求处理器提供服务。"""
|
||||
self.manifest = manifest
|
||||
self.invoke_handler = handler
|
||||
super().__init__(server_address, _NodeRequestHandler)
|
||||
|
||||
|
||||
class _NodeRequestHandler(BaseHTTPRequestHandler):
|
||||
"""节点协议请求处理器:只实现 /health 与 /invoke 两个端点。"""
|
||||
|
||||
server: NodeHTTPServer
|
||||
|
||||
def log_message(self, format: str, *args) -> None: # noqa: A002
|
||||
"""静默访问日志,避免测试和日志系统产生噪声。"""
|
||||
return
|
||||
|
||||
def do_GET(self) -> None:
|
||||
"""处理健康检查请求,返回节点 ID 与版本。"""
|
||||
if self.path != "/health":
|
||||
_send_json(self, 404, {"status": "error", "message": "not found"})
|
||||
return
|
||||
@@ -51,16 +70,19 @@ class _NodeRequestHandler(BaseHTTPRequestHandler):
|
||||
_send_json(self, 200, response.to_dict())
|
||||
|
||||
def do_POST(self) -> None:
|
||||
"""处理调用请求,业务异常统一转换为 failed 响应。"""
|
||||
if self.path != "/invoke":
|
||||
_send_json(self, 404, {"status": "error", "message": "not found"})
|
||||
return
|
||||
try:
|
||||
# 按 Content-Length 精确读取请求体,避免长连接下的读悬挂。
|
||||
length = int(self.headers.get("Content-Length", "0"))
|
||||
body = json.loads(self.rfile.read(length).decode("utf-8"))
|
||||
request = InvokeRequest.from_dict(body)
|
||||
response = self.server.invoke_handler(request)
|
||||
_send_json(self, 200, response.to_dict())
|
||||
except Exception as exc: # noqa: BLE001
|
||||
# 任何处理异常都不能让节点进程崩溃,统一返回失败结果。
|
||||
_send_json(
|
||||
self,
|
||||
500,
|
||||
@@ -74,6 +96,7 @@ def create_node_server(
|
||||
host: str = "127.0.0.1",
|
||||
port: int = 0,
|
||||
) -> NodeHTTPServer:
|
||||
"""创建节点服务器;manifest 校验通过后才允许启动。"""
|
||||
manifest.validate()
|
||||
return NodeHTTPServer((host, port), manifest, invoke_handler)
|
||||
|
||||
@@ -84,7 +107,9 @@ def run_node(
|
||||
host: str = "127.0.0.1",
|
||||
port: int = 0,
|
||||
) -> None:
|
||||
"""启动节点服务并阻塞运行,直到进程被外部终止。"""
|
||||
server = create_node_server(manifest, invoke_handler, host=host, port=port)
|
||||
# 节点管理器从标准输出解析该行,因此必须 flush。
|
||||
print(f"WOV_NODE_READY port={server.server_address[1]}", flush=True)
|
||||
try:
|
||||
server.serve_forever()
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
"""wov_sdk.models 的单元测试。
|
||||
|
||||
测试覆盖所有数据模型的 JSON 往返序列化、字段校验和 manifest 文件加载,
|
||||
确保协议模型的稳定性。
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
@@ -15,6 +21,7 @@ from wov_sdk.models import (
|
||||
|
||||
|
||||
def valid_manifest() -> NodeManifest:
|
||||
"""构造一个覆盖全部字段的合法 NodeManifest,供测试复用。"""
|
||||
return NodeManifest(
|
||||
id="echo",
|
||||
name="Echo",
|
||||
@@ -33,6 +40,7 @@ def valid_manifest() -> NodeManifest:
|
||||
|
||||
|
||||
def test_manifest_round_trip() -> None:
|
||||
"""验证 manifest 经过 to_dict/from_dict 后保持原值。"""
|
||||
manifest = valid_manifest()
|
||||
restored = NodeManifest.from_dict(manifest.to_dict())
|
||||
assert restored == manifest
|
||||
@@ -53,6 +61,7 @@ def test_manifest_round_trip() -> None:
|
||||
],
|
||||
)
|
||||
def test_manifest_validation(field: str, value: object) -> None:
|
||||
"""验证必填字段为空或数值越界时抛出 ValueError。"""
|
||||
manifest = valid_manifest()
|
||||
setattr(manifest, field, value)
|
||||
with pytest.raises(ValueError):
|
||||
@@ -60,6 +69,7 @@ def test_manifest_validation(field: str, value: object) -> None:
|
||||
|
||||
|
||||
def test_manifest_load(tmp_path) -> None:
|
||||
"""验证 NodeManifest.load 能从 JSON 文件读取并校验。"""
|
||||
path = tmp_path / "node.manifest.json"
|
||||
path.write_text(json.dumps(valid_manifest().to_dict()), encoding="utf-8")
|
||||
loaded = NodeManifest.load(str(path))
|
||||
@@ -67,6 +77,7 @@ def test_manifest_load(tmp_path) -> None:
|
||||
|
||||
|
||||
def test_invoke_request_round_trip() -> None:
|
||||
"""验证 InvokeRequest 的 JSON 往返序列化。"""
|
||||
request = InvokeRequest(
|
||||
run_id="run_1",
|
||||
node_instance_id="ni_1",
|
||||
@@ -79,12 +90,14 @@ def test_invoke_request_round_trip() -> None:
|
||||
|
||||
|
||||
def test_invoke_response_round_trip() -> None:
|
||||
"""验证 InvokeResponse 的 JSON 往返序列化。"""
|
||||
response = InvokeResponse(status="completed", outputs={"text": "hello"})
|
||||
restored = InvokeResponse.from_dict(response.to_dict())
|
||||
assert restored == response
|
||||
|
||||
|
||||
def test_health_and_progress_serialization() -> None:
|
||||
"""验证健康检查和进度事件模型的字典输出。"""
|
||||
health = HealthResponse(status="ok", node_id="echo", version="1.0.0")
|
||||
assert health.to_dict() == {
|
||||
"status": "ok",
|
||||
@@ -102,6 +115,7 @@ def test_health_and_progress_serialization() -> None:
|
||||
|
||||
|
||||
def test_workflow_node_and_edge_round_trip() -> None:
|
||||
"""验证工作流节点与边的 JSON 往返序列化。"""
|
||||
node = WorkflowNode(
|
||||
id="asr",
|
||||
node_type="faster-whisper",
|
||||
@@ -117,6 +131,7 @@ def test_workflow_node_and_edge_round_trip() -> None:
|
||||
|
||||
|
||||
def test_workflow_definition_round_trip_and_validation() -> None:
|
||||
"""验证完整 DAG 定义可往返序列化并通过校验。"""
|
||||
definition = WorkflowDefinition(
|
||||
name="demo",
|
||||
version=1,
|
||||
@@ -134,6 +149,7 @@ def test_workflow_definition_round_trip_and_validation() -> None:
|
||||
|
||||
|
||||
def test_workflow_definition_invalid() -> None:
|
||||
"""验证非法 DAG(空名、版本为 0、重复节点、未知边)被拒绝。"""
|
||||
with pytest.raises(ValueError):
|
||||
WorkflowDefinition(name="", version=1).validate()
|
||||
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
"""wov_sdk.server 的协议测试。
|
||||
|
||||
通过真实 HTTP 请求验证 /health 与 /invoke 端点的正常、404 和 500 分支,
|
||||
并验证 run_node 的启动输出格式。
|
||||
"""
|
||||
|
||||
import json
|
||||
import threading
|
||||
import urllib.error
|
||||
@@ -10,6 +16,7 @@ from wov_sdk.server import NodeHTTPServer, create_node_server, run_node
|
||||
|
||||
|
||||
def valid_manifest() -> NodeManifest:
|
||||
"""构造一个合法的最小 Echo 节点 manifest。"""
|
||||
return NodeManifest(
|
||||
id="echo",
|
||||
name="Echo",
|
||||
@@ -20,6 +27,7 @@ def valid_manifest() -> NodeManifest:
|
||||
|
||||
|
||||
def test_create_node_server_validates_manifest() -> None:
|
||||
"""验证空 manifest 在创建服务器前就会被校验拒绝。"""
|
||||
with pytest.raises(ValueError):
|
||||
create_node_server(
|
||||
NodeManifest(
|
||||
@@ -34,6 +42,7 @@ def test_create_node_server_validates_manifest() -> None:
|
||||
|
||||
|
||||
def _post(server: NodeHTTPServer, path: str, body: dict | None = None) -> tuple[int, dict]:
|
||||
"""向节点服务器发送 POST 请求并返回状态码与 JSON 响应。"""
|
||||
data = json.dumps(body or {}).encode("utf-8")
|
||||
request = urllib.request.Request(
|
||||
f"http://127.0.0.1:{server.server_address[1]}{path}",
|
||||
@@ -49,6 +58,7 @@ def _post(server: NodeHTTPServer, path: str, body: dict | None = None) -> tuple[
|
||||
|
||||
|
||||
def _get(server: NodeHTTPServer, path: str) -> tuple[int, dict]:
|
||||
"""向节点服务器发送 GET 请求并返回状态码与 JSON 响应。"""
|
||||
try:
|
||||
with urllib.request.urlopen(
|
||||
f"http://127.0.0.1:{server.server_address[1]}{path}", timeout=5
|
||||
@@ -59,6 +69,7 @@ def _get(server: NodeHTTPServer, path: str) -> tuple[int, dict]:
|
||||
|
||||
|
||||
def test_http_protocol() -> None:
|
||||
"""验证健康检查、正常调用、404 与非法请求体等协议分支。"""
|
||||
def handler(request: InvokeRequest) -> InvokeResponse:
|
||||
return InvokeResponse(
|
||||
status="completed",
|
||||
@@ -66,6 +77,7 @@ def test_http_protocol() -> None:
|
||||
)
|
||||
|
||||
server = create_node_server(valid_manifest(), handler)
|
||||
# 在独立线程中运行服务器,主线程执行 HTTP 客户端断言。
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
try:
|
||||
@@ -103,6 +115,7 @@ def test_http_protocol() -> None:
|
||||
|
||||
|
||||
def test_http_protocol_handler_error() -> None:
|
||||
"""验证业务处理函数抛异常时返回 500 与 failed 状态。"""
|
||||
def failing_handler(request: InvokeRequest) -> InvokeResponse:
|
||||
raise RuntimeError("boom")
|
||||
|
||||
@@ -130,6 +143,7 @@ def test_http_protocol_handler_error() -> None:
|
||||
|
||||
|
||||
def test_run_node(monkeypatch, capsys) -> None:
|
||||
"""验证 run_node 打印 WOV_NODE_READY 端口行并进入服务循环。"""
|
||||
called = []
|
||||
|
||||
def fake_serve_forever(server: NodeHTTPServer) -> None:
|
||||
|
||||
Reference in New Issue
Block a user