feat(system):优化后端
1.新增后端测试 2.增加了后端的加密 3.增加了i18n(国际化)
This commit is contained in:
@@ -0,0 +1,290 @@
|
||||
"""Workflow graph 引擎本身(节点跳转 / 失败处理 / 派发 / HITL)的单元测试。
|
||||
|
||||
不经过 ConsciousnessNode 入口,也不依赖 ray runtime —— 直接调用
|
||||
``run_workflow_graph(workflow_data, trace_id, deps=...)``,注入一套全部用
|
||||
``AsyncMock``/lambda 实现的 ``WorkflowDeps``。验证 graph 驱动确实把状态机跑通。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Tuple
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
from kilostar.core.work.workflow.workflow_engine import (
|
||||
WorkflowDeps,
|
||||
WorkflowGraphState,
|
||||
run_workflow_graph,
|
||||
)
|
||||
from kilostar.core.work.workflow.model import WorkflowStatus
|
||||
|
||||
|
||||
def _make_deps(
|
||||
*,
|
||||
skill_outputs: List[Tuple[str, bool]] | None = None,
|
||||
consciousness_outputs: List[Tuple[str, bool]] | None = None,
|
||||
received_replies: List[str] | None = None,
|
||||
) -> tuple[WorkflowDeps, Dict[str, List[Any]]]:
|
||||
"""构造一对 ``(deps, sink)``:sink 收集所有 IO 调用,方便断言。
|
||||
|
||||
``skill_outputs`` / ``consciousness_outputs`` 是按顺序消费的 ``(text, success)``
|
||||
队列,run_skill / run_consciousness 每被调一次取一个。``received_replies``
|
||||
用于 HumanApproval 节点的 get_received。
|
||||
"""
|
||||
sink: Dict[str, List[Any]] = {
|
||||
"upsert": [],
|
||||
"status": [],
|
||||
"pending": [],
|
||||
"received_calls": [],
|
||||
"skill_calls": [],
|
||||
"consciousness_calls": [],
|
||||
}
|
||||
|
||||
skill_queue = list(skill_outputs or [])
|
||||
consc_queue = list(consciousness_outputs or [])
|
||||
reply_queue = list(received_replies or [])
|
||||
|
||||
upsert = AsyncMock(side_effect=lambda *a, **kw: sink["upsert"].append((a, kw)))
|
||||
status = AsyncMock(side_effect=lambda tid, st: sink["status"].append((tid, st)))
|
||||
pending = AsyncMock(side_effect=lambda tid, msg: sink["pending"].append((tid, msg)))
|
||||
|
||||
async def _get_received(tid: str) -> str:
|
||||
sink["received_calls"].append(tid)
|
||||
return reply_queue.pop(0) if reply_queue else ""
|
||||
|
||||
async def _run_skill(step, state):
|
||||
sink["skill_calls"].append((step, state.current_step_index))
|
||||
if not skill_queue:
|
||||
return "(no fixture)", True
|
||||
return skill_queue.pop(0)
|
||||
|
||||
async def _run_consciousness(step, state):
|
||||
sink["consciousness_calls"].append((step, state.current_step_index))
|
||||
if not consc_queue:
|
||||
return "(no fixture)", True
|
||||
return consc_queue.pop(0)
|
||||
|
||||
return (
|
||||
WorkflowDeps(
|
||||
upsert_workflow_context=upsert,
|
||||
update_workflow_status=status,
|
||||
put_pending=pending,
|
||||
get_received=_get_received,
|
||||
run_skill=_run_skill,
|
||||
run_consciousness=_run_consciousness,
|
||||
),
|
||||
sink,
|
||||
)
|
||||
|
||||
|
||||
# ─── 基本路径 ─────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_skill_individual_path_completes_linear_steps():
|
||||
"""两步顺序工作流 (skill_individual):成功 → 成功,应推进到 COMPLETED。"""
|
||||
deps, sink = _make_deps(skill_outputs=[("ok1", True), ("ok2", True)])
|
||||
workflow_data = {
|
||||
"work_link": [
|
||||
{"step": 1, "name": "s1", "action": "do1", "outputs": "o1",
|
||||
"node": "skill_individual", "agent_id": "a1"},
|
||||
{"step": 2, "name": "s2", "action": "do2", "outputs": "o2",
|
||||
"node": "skill_individual", "agent_id": "a2"},
|
||||
]
|
||||
}
|
||||
final = await run_workflow_graph(workflow_data, "trace-ok", deps=deps)
|
||||
assert final == WorkflowStatus.COMPLETED.value
|
||||
# run_skill 被调了两次,consciousness 没被调
|
||||
assert len(sink["skill_calls"]) == 2
|
||||
assert len(sink["consciousness_calls"]) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_consciousness_node_path_dispatches_to_consciousness():
|
||||
"""node=consciousness_node 的 step 应被派给 run_consciousness。"""
|
||||
deps, sink = _make_deps(
|
||||
consciousness_outputs=[("ok-from-consciousness", True)]
|
||||
)
|
||||
workflow_data = {
|
||||
"work_link": [
|
||||
{"step": 1, "name": "s1", "action": "summarize",
|
||||
"node": "consciousness_node"},
|
||||
]
|
||||
}
|
||||
final = await run_workflow_graph(workflow_data, "trace-c", deps=deps)
|
||||
assert final == WorkflowStatus.COMPLETED.value
|
||||
assert len(sink["consciousness_calls"]) == 1
|
||||
assert len(sink["skill_calls"]) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mixed_path_dispatches_correctly():
|
||||
"""混合 step:第一步 skill_individual,第二步 consciousness_node。"""
|
||||
deps, sink = _make_deps(
|
||||
skill_outputs=[("o1", True)],
|
||||
consciousness_outputs=[("o2", True)],
|
||||
)
|
||||
workflow_data = {
|
||||
"work_link": [
|
||||
{"step": 1, "name": "s1", "action": "do",
|
||||
"node": "skill_individual", "agent_id": "a1"},
|
||||
{"step": 2, "name": "s2", "action": "review",
|
||||
"node": "consciousness_node"},
|
||||
]
|
||||
}
|
||||
final = await run_workflow_graph(workflow_data, "trace-mix", deps=deps)
|
||||
assert final == WorkflowStatus.COMPLETED.value
|
||||
assert len(sink["skill_calls"]) == 1
|
||||
assert len(sink["consciousness_calls"]) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unknown_node_type_falls_to_failed():
|
||||
"""未识别的 node 类型应直接收尾 FAILED,不静默跑成功。"""
|
||||
deps, sink = _make_deps()
|
||||
workflow_data = {
|
||||
"work_link": [
|
||||
{"step": 1, "name": "s1", "action": "do",
|
||||
"node": "phantom_node"},
|
||||
]
|
||||
}
|
||||
final = await run_workflow_graph(workflow_data, "trace-unknown", deps=deps)
|
||||
assert final == WorkflowStatus.FAILED.value
|
||||
assert len(sink["skill_calls"]) == 0
|
||||
assert len(sink["consciousness_calls"]) == 0
|
||||
|
||||
|
||||
# ─── logic_gate 跳转语义 ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_if_pass_exit_short_circuits():
|
||||
"""``logic_gate.if_pass=exit`` 应在该步成功后立即收尾。"""
|
||||
deps, sink = _make_deps(skill_outputs=[("ok", True)])
|
||||
workflow_data = {
|
||||
"work_link": [
|
||||
{"step": 1, "name": "s1", "action": "do", "outputs": "o",
|
||||
"node": "skill_individual", "agent_id": "a1",
|
||||
"logic_gate": {"if_pass": "exit", "if_fail": "jump_to_step_1"}},
|
||||
{"step": 2, "name": "s2", "action": "skipped",
|
||||
"node": "skill_individual", "agent_id": "a2"},
|
||||
]
|
||||
}
|
||||
final = await run_workflow_graph(workflow_data, "trace-exit", deps=deps)
|
||||
assert final == WorkflowStatus.COMPLETED.value
|
||||
# 第二步不应被派发
|
||||
assert len(sink["skill_calls"]) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_failure_without_jump_marks_failed():
|
||||
"""run_skill 报失败且 if_fail 不指向跳转 → FAILED。"""
|
||||
deps, sink = _make_deps(skill_outputs=[("boom", False)])
|
||||
workflow_data = {
|
||||
"work_link": [
|
||||
{"step": 1, "name": "s1", "action": "do",
|
||||
"node": "skill_individual", "agent_id": "a1"},
|
||||
]
|
||||
}
|
||||
final = await run_workflow_graph(workflow_data, "trace-fail", deps=deps)
|
||||
assert final == WorkflowStatus.FAILED.value
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_failure_jumps_back_then_succeeds():
|
||||
"""步 2 失败但 if_fail=jump_to_step_1,回到步 1 重跑后继续到步 2 成功。"""
|
||||
deps, sink = _make_deps(
|
||||
skill_outputs=[
|
||||
("o1-first", True), # step 1 第一次成功
|
||||
("boom", False), # step 2 第一次失败 → 跳回 step 1
|
||||
("o1-retry", True), # step 1 重跑
|
||||
("o2-final", True), # step 2 第二次成功
|
||||
]
|
||||
)
|
||||
workflow_data = {
|
||||
"work_link": [
|
||||
{"step": 1, "name": "s1", "action": "do",
|
||||
"node": "skill_individual", "agent_id": "a1"},
|
||||
{"step": 2, "name": "s2", "action": "do",
|
||||
"node": "skill_individual", "agent_id": "a2",
|
||||
"logic_gate": {"if_fail": "jump_to_step_1", "if_pass": "continue"}},
|
||||
]
|
||||
}
|
||||
final = await run_workflow_graph(workflow_data, "trace-jump", deps=deps)
|
||||
assert final == WorkflowStatus.COMPLETED.value
|
||||
# skill 应被调 4 次
|
||||
assert len(sink["skill_calls"]) == 4
|
||||
|
||||
|
||||
# ─── HITL ────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_human_approval_approve_continues():
|
||||
"""require_approval=True 时进入 HumanApproval;用户回 approve 后继续执行。"""
|
||||
deps, sink = _make_deps(
|
||||
skill_outputs=[("ok", True)],
|
||||
received_replies=["approve"],
|
||||
)
|
||||
workflow_data = {
|
||||
"work_link": [
|
||||
{"step": 1, "name": "danger", "action": "rm -rf /",
|
||||
"node": "skill_individual", "agent_id": "a1",
|
||||
"require_approval": True},
|
||||
]
|
||||
}
|
||||
final = await run_workflow_graph(workflow_data, "trace-hitl-ok", deps=deps)
|
||||
assert final == WorkflowStatus.COMPLETED.value
|
||||
# get_received 被调用过一次
|
||||
assert sink["received_calls"] == ["trace-hitl-ok"]
|
||||
# 应该有一条"需要人工审批"的 SSE
|
||||
msgs = [m for _, m in sink["pending"]]
|
||||
assert any("需要人工审批" in m for m in msgs)
|
||||
# skill 才被实际派发
|
||||
assert len(sink["skill_calls"]) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_human_approval_reject_aborts():
|
||||
"""用户回 reject → 不执行 step,整个工作流落到 FAILED。"""
|
||||
deps, sink = _make_deps(
|
||||
skill_outputs=[("ok", True)],
|
||||
received_replies=["reject"],
|
||||
)
|
||||
workflow_data = {
|
||||
"work_link": [
|
||||
{"step": 1, "name": "danger", "action": "do",
|
||||
"node": "skill_individual", "agent_id": "a1",
|
||||
"require_approval": True},
|
||||
]
|
||||
}
|
||||
final = await run_workflow_graph(workflow_data, "trace-hitl-no", deps=deps)
|
||||
assert final == WorkflowStatus.FAILED.value
|
||||
# skill 不应被派发
|
||||
assert len(sink["skill_calls"]) == 0
|
||||
|
||||
|
||||
# ─── 边界 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_empty_work_link_completes_immediately():
|
||||
"""空 work_link:Initialize → Dispatch 直接判定越界 → Finalize(COMPLETED)。"""
|
||||
deps, sink = _make_deps()
|
||||
final = await run_workflow_graph({"work_link": []}, "trace-empty", deps=deps)
|
||||
assert final == WorkflowStatus.COMPLETED.value
|
||||
msgs = [m for _, m in sink["pending"]]
|
||||
assert not any("执行步骤" in m for m in msgs)
|
||||
assert any("工作流执行完成" in m for m in msgs)
|
||||
|
||||
|
||||
def test_workflow_graph_state_defaults():
|
||||
"""State 默认值确保各字段类型契约稳定。"""
|
||||
state = WorkflowGraphState(trace_id="t")
|
||||
assert state.blackboard == {}
|
||||
assert state.work_link == []
|
||||
assert state.current_step_index == 0
|
||||
assert state.final_status == WorkflowStatus.RUNNING.value
|
||||
assert state.logs == []
|
||||
assert state.original_command == ""
|
||||
Reference in New Issue
Block a user