"""src/wov_app/registry.py 的模块级测试(数据 → 测试过程 → 验证结果)。 被测模块:`src/wov_app/registry.py`(节点注册表:节点调用的唯一入口), 可独立调用。注册表是进程内全局状态,用例通过保存/恢复快照隔离,不依赖 全局 conftest(原 tests/conftest.py 的 autouse 夹具已按规则取消)。 """ from __future__ import annotations from pathlib import Path import pytest from wov_app import registry from wov_sdk.models import InvokeRequest, InvokeResponse, NodeManifest # 仓库根目录(用于定位真实 manifests/)。 WORKSPACE = Path(__file__).resolve().parents[3] @pytest.fixture(autouse=True) def _restore_registry(): """用例前清空注册表、用例后恢复快照:保证用例看到的是干净基线, 不受其他模块(如 main 生命周期 register_all)的注册结果影响。""" snapshot = dict(registry._registry) registry._registry.clear() yield registry._registry.clear() registry._registry.update(snapshot) def _manifest(node_id: str = "demo-node") -> NodeManifest: """构造一个最小合法清单(真实字段结构)。""" return NodeManifest( id=node_id, name="演示节点", version="0.1.0", capability="demo", command=["python", "-m", "demo"], ) def _request(tmp_path: Path) -> InvokeRequest: """构造真实调用请求。""" return InvokeRequest( run_id="run-test", node_instance_id="n1", params={}, inputs={"text": "hi"}, output_dir=str(tmp_path), ) # --------------------------------------------------------------------------- # 注册与查询 # --------------------------------------------------------------------------- def test_register_and_get_node() -> None: """注册后可查询到清单,未注册返回 None。""" # 数据:一个清单 + 一个真实处理器。 registry.register(_manifest("echo-x"), lambda request: InvokeResponse(status="completed")) # 测试过程 found = registry.get_node("echo-x") missing = registry.get_node("not-registered") # 验证结果 assert found is not None and found.id == "echo-x" assert missing is None def test_register_overwrites_same_id() -> None: """同一 ID 重复注册按后者覆盖(启动时 register_all 幂等)。""" # 数据:同一 ID 注册两次,名称不同。 first = _manifest("node-x") second = NodeManifest( id="node-x", name="覆盖后", version="0.2.0", capability="demo", command=["python", "-m", "demo"], ) registry.register(first, lambda request: InvokeResponse(status="completed")) registry.register(second, lambda request: InvokeResponse(status="completed")) # 测试过程 found = registry.get_node("node-x") # 验证结果 assert found.name == "覆盖后" assert found.version == "0.2.0" def test_register_rejects_invalid_manifest() -> None: """非法清单(空 ID)注册时校验失败并抛错。""" # 数据:id 为空的清单。 invalid = NodeManifest(id="", name="x", version="1", capability="c", command=["python"]) # 测试过程与验证结果 with pytest.raises(ValueError): registry.register(invalid, lambda request: InvokeResponse(status="completed")) def test_register_all_registers_builtin_nodes() -> None: """register_all 从真实 manifests/ 注册全部内置节点(含关键节点)。""" # 数据:真实仓库清单目录。 assert (WORKSPACE / "manifests").is_dir() # 测试过程 registry.register_all() ids = {manifest.id for manifest in registry.list_nodes()} # 验证结果:字幕流水线所需的节点全部在册(ID 取自 manifests/*.json)。 for expected in ( "echo", "ffmpeg-extract", "faster-whisper", "llm-translate", "vlm-ocr", "frame-extract", "subtitle-ocr", "llm-filter", "subtitle-correction", "srt-to-dual-eye-ass", ): assert expected in ids, f"内置节点未注册:{expected}" def test_register_all_is_idempotent() -> None: """重复调用 register_all 不产生重复条目(按 ID 覆盖)。""" # 数据:无。 # 测试过程 registry.register_all() first = len(registry.list_nodes()) registry.register_all() second = len(registry.list_nodes()) # 验证结果 assert first == second # --------------------------------------------------------------------------- # invoke:唯一调用入口 # --------------------------------------------------------------------------- def test_invoke_routes_to_registered_handler(tmp_path: Path) -> None: """invoke 把请求路由到注册的处理器并原样返回其响应。""" # 数据:记录收到的请求的处理器。 received: list[InvokeRequest] = [] def handler(request: InvokeRequest) -> InvokeResponse: received.append(request) return InvokeResponse(status="completed", outputs={"text": "ok"}) registry.register(_manifest("node-x"), handler) # 测试过程 response = registry.invoke("node-x", _request(tmp_path)) # 验证结果:响应来自处理器,且请求原样传递。 assert response.status == "completed" assert response.outputs == {"text": "ok"} assert received[0].inputs == {"text": "hi"} def test_invoke_raises_for_unregistered_node(tmp_path: Path) -> None: """未注册节点调用抛 ValueError(不静默返回空结果)。""" # 数据:未注册的 node_id。 # 测试过程与验证结果 with pytest.raises(ValueError, match="not registered"): registry.invoke("nope", _request(tmp_path)) def test_invoke_returns_failed_response_unchanged(tmp_path: Path) -> None: """处理器返回 failed 时原样透传(注册表不改变节点语义)。""" # 数据:总是失败的处理器。 registry.register( _manifest("node-x"), lambda request: InvokeResponse(status="failed", error="boom"), ) # 测试过程 response = registry.invoke("node-x", _request(tmp_path)) # 验证结果 assert response.status == "failed" assert response.error == "boom" def test_list_nodes_returns_sorted_by_id() -> None: """list_nodes 按节点 ID 排序返回(前端展示稳定)。""" # 数据:乱序注册的节点。 for node_id in ("z-node", "a-node", "m-node"): registry.register(_manifest(node_id), lambda request: InvokeResponse(status="completed")) # 测试过程 ids = [m.id for m in registry.list_nodes()] # 验证结果 assert ids == ["a-node", "m-node", "z-node"]