feat: 添加测试框架及功能测试用例
test: 实现BDD测试框架及功能测试 docs: 添加测试配置文件及文档 refactor: 重构命令处理逻辑以支持测试
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
"""
|
||||
Shared Given/Then step definitions used across all feature files.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pytest_bdd import given, then, parsers
|
||||
|
||||
|
||||
# ── Given: user identity ─────────────────────────────────────────────────────
|
||||
|
||||
@given(parsers.parse('user "{user_id}" is sending commands'))
|
||||
def set_user(user_id, pytestconfig):
|
||||
from orchestrator.tools import set_current_user
|
||||
set_current_user(user_id)
|
||||
pytestconfig._test_user_id = user_id
|
||||
|
||||
|
||||
@given(parsers.parse('the current chat_id is "{chat_id}"'))
|
||||
def set_chat(chat_id):
|
||||
from orchestrator.tools import set_current_chat
|
||||
set_current_chat(chat_id)
|
||||
|
||||
|
||||
# ── Given: session setup ─────────────────────────────────────────────────────
|
||||
|
||||
@given(parsers.parse('user has session "{conv_id}" in "{cwd}"'))
|
||||
def add_session(conv_id, cwd, pytestconfig, tmp_path):
|
||||
from agent.manager import manager, Session
|
||||
user_id = getattr(pytestconfig, "_test_user_id", "user_abc123")
|
||||
session = Session(conv_id=conv_id, cwd=str(tmp_path / conv_id), owner_id=user_id, cc_timeout=50.0)
|
||||
(tmp_path / conv_id).mkdir(exist_ok=True)
|
||||
manager._sessions[conv_id] = session
|
||||
|
||||
|
||||
@given(parsers.parse('session "{conv_id}" in "{cwd}" belongs to user "{owner}"'))
|
||||
def add_foreign_session(conv_id, cwd, owner, tmp_path):
|
||||
from agent.manager import manager, Session
|
||||
session = Session(conv_id=conv_id, cwd=str(tmp_path / conv_id), owner_id=owner, cc_timeout=50.0)
|
||||
(tmp_path / conv_id).mkdir(exist_ok=True)
|
||||
manager._sessions[conv_id] = session
|
||||
|
||||
|
||||
@given(parsers.parse('active session is "{conv_id}"'))
|
||||
def set_active_session(conv_id, pytestconfig):
|
||||
from orchestrator.agent import agent
|
||||
user_id = getattr(pytestconfig, "_test_user_id", "user_abc123")
|
||||
agent._active_conv[user_id] = conv_id
|
||||
|
||||
|
||||
@given(parsers.parse('active session is "{conv_id}" which does not exist'))
|
||||
def set_ghost_active_session(conv_id, pytestconfig):
|
||||
from orchestrator.agent import agent
|
||||
user_id = getattr(pytestconfig, "_test_user_id", "user_abc123")
|
||||
agent._active_conv[user_id] = conv_id
|
||||
# intentionally NOT added to manager._sessions
|
||||
|
||||
|
||||
@given(parsers.parse('no active session for user "{user_id}"'))
|
||||
def ensure_no_active_session(user_id):
|
||||
from orchestrator.agent import agent
|
||||
agent._active_conv[user_id] = None
|
||||
|
||||
|
||||
# ── Given: mode toggles ──────────────────────────────────────────────────────
|
||||
|
||||
@given(parsers.parse('direct mode is enabled for user "{user_id}"'))
|
||||
def enable_direct_mode(user_id):
|
||||
from orchestrator.agent import agent
|
||||
agent._passthrough[user_id] = True
|
||||
|
||||
|
||||
# ── Given: mocks ─────────────────────────────────────────────────────────────
|
||||
|
||||
@given(parsers.parse('run_claude returns "{output}"'))
|
||||
def set_run_claude_return(output, mock_run_claude):
|
||||
mock_run_claude.return_value = output
|
||||
|
||||
|
||||
# ── Given: config ────────────────────────────────────────────────────────────
|
||||
|
||||
@given("ROUTER_MODE is disabled")
|
||||
def disable_router_mode(monkeypatch):
|
||||
import config
|
||||
monkeypatch.setattr(config, "ROUTER_MODE", False)
|
||||
|
||||
|
||||
# ── Then: reply assertions ───────────────────────────────────────────────────
|
||||
|
||||
@then(parsers.parse('reply contains "{text}"'))
|
||||
def reply_contains(text, pytestconfig):
|
||||
reply = getattr(pytestconfig, "_reply", None)
|
||||
assert text in (reply or ""), \
|
||||
f"Expected {text!r} in reply, got: {reply!r}"
|
||||
|
||||
|
||||
@then(parsers.parse('reply does not contain "{text}"'))
|
||||
def reply_not_contains(text, pytestconfig):
|
||||
reply = getattr(pytestconfig, "_reply", None)
|
||||
assert text not in (reply or ""), \
|
||||
f"Expected {text!r} NOT in reply, got: {reply!r}"
|
||||
|
||||
|
||||
@then("reply is not empty")
|
||||
def reply_not_empty(pytestconfig):
|
||||
reply = getattr(pytestconfig, "_reply", None)
|
||||
assert reply and reply.strip(), \
|
||||
f"Expected non-empty reply, got: {reply!r}"
|
||||
|
||||
|
||||
@then("text reply is empty")
|
||||
def reply_is_empty(pytestconfig):
|
||||
reply = getattr(pytestconfig, "_reply", None)
|
||||
assert reply == "", \
|
||||
f"Expected empty reply, got: {reply!r}"
|
||||
|
||||
|
||||
@then("command is not handled")
|
||||
def command_not_handled(pytestconfig):
|
||||
reply = getattr(pytestconfig, "_reply", None)
|
||||
assert reply is None
|
||||
|
||||
|
||||
# ── Then: session state ──────────────────────────────────────────────────────
|
||||
|
||||
@then(parsers.parse('session manager has {count:d} session for user "{user_id}"'))
|
||||
@then(parsers.parse('session manager has {count:d} sessions for user "{user_id}"'))
|
||||
def check_session_count(count, user_id):
|
||||
from agent.manager import manager
|
||||
sessions = manager.list_sessions(user_id=user_id)
|
||||
assert len(sessions) == count, \
|
||||
f"Expected {count} sessions, got {len(sessions)}: {sessions}"
|
||||
|
||||
|
||||
@then(parsers.parse('active session for user "{user_id}" is "{conv_id}"'))
|
||||
def check_active_session(user_id, conv_id):
|
||||
from orchestrator.agent import agent
|
||||
assert agent._active_conv.get(user_id) == conv_id
|
||||
|
||||
|
||||
@then(parsers.parse('active session for user "{user_id}" is None'))
|
||||
def check_no_active_session(user_id):
|
||||
from orchestrator.agent import agent
|
||||
assert agent._active_conv.get(user_id) is None
|
||||
|
||||
|
||||
# ── Then: mode state ─────────────────────────────────────────────────────────
|
||||
|
||||
@then(parsers.parse('passthrough mode is enabled for user "{user_id}"'))
|
||||
def check_passthrough_on(user_id):
|
||||
from orchestrator.agent import agent
|
||||
assert agent._passthrough.get(user_id) is True
|
||||
|
||||
|
||||
@then(parsers.parse('passthrough mode is disabled for user "{user_id}"'))
|
||||
def check_passthrough_off(user_id):
|
||||
from orchestrator.agent import agent
|
||||
assert agent._passthrough.get(user_id) is False
|
||||
|
||||
|
||||
# ── Then: Feishu output ──────────────────────────────────────────────────────
|
||||
|
||||
@then(parsers.parse('a sessions card is sent to chat "{chat_id}"'))
|
||||
def check_card_sent(chat_id, feishu_calls):
|
||||
cards = feishu_calls["cards"]
|
||||
assert any(c["receive_id"] == chat_id for c in cards), \
|
||||
f"No card sent to {chat_id!r}, captured: {cards}"
|
||||
|
||||
|
||||
# ── Then: scheduler ──────────────────────────────────────────────────────────
|
||||
|
||||
@then(parsers.parse('scheduler has {count:d} pending job'))
|
||||
@then(parsers.parse('scheduler has {count:d} pending jobs'))
|
||||
def check_scheduler_jobs(count):
|
||||
from agent.scheduler import scheduler
|
||||
assert len(scheduler._jobs) == count, \
|
||||
f"Expected {count} jobs, got {len(scheduler._jobs)}"
|
||||
|
||||
|
||||
# ── Then: run_claude ─────────────────────────────────────────────────────────
|
||||
|
||||
@then("run_claude was called")
|
||||
def check_run_claude_called(mock_run_claude):
|
||||
assert mock_run_claude.call_count >= 1, "Expected run_claude to be called"
|
||||
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
Step definitions for agent routing and passthrough features.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pytest_bdd import scenarios, given, when, then, parsers
|
||||
|
||||
from tests.step_defs.common_steps import * # noqa: F401,F403 — import shared steps
|
||||
|
||||
scenarios(
|
||||
"../features/agent/routing.feature",
|
||||
"../features/agent/passthrough.feature",
|
||||
)
|
||||
|
||||
|
||||
# ── Given: agent-specific setup ──────────────────────────────────────────────
|
||||
|
||||
@given(parsers.parse('user "{user_id}" is in smart mode'))
|
||||
def set_smart_mode(user_id, pytestconfig):
|
||||
from orchestrator.agent import agent
|
||||
from orchestrator.tools import set_current_user
|
||||
set_current_user(user_id)
|
||||
agent._passthrough[user_id] = False
|
||||
pytestconfig._test_user_id = user_id
|
||||
|
||||
|
||||
@given(parsers.parse('user has active session "{conv_id}" in "{cwd}"'))
|
||||
def add_and_activate_session(conv_id, cwd, pytestconfig, tmp_path):
|
||||
from agent.manager import manager, Session
|
||||
from orchestrator.agent import agent
|
||||
user_id = getattr(pytestconfig, "_test_user_id", "user_abc123")
|
||||
session = Session(conv_id=conv_id, cwd=str(tmp_path / conv_id), owner_id=user_id, cc_timeout=50.0)
|
||||
(tmp_path / conv_id).mkdir(exist_ok=True)
|
||||
manager._sessions[conv_id] = session
|
||||
agent._active_conv[user_id] = conv_id
|
||||
|
||||
|
||||
@given(parsers.parse('vcr cassette "{cassette_name}"'))
|
||||
def set_vcr_cassette(cassette_name, pytestconfig):
|
||||
pytestconfig._vcr_cassette = cassette_name
|
||||
|
||||
|
||||
# ── When: send message through agent ─────────────────────────────────────────
|
||||
|
||||
@when(parsers.parse('user sends agent message "{text}"'))
|
||||
def send_agent_message(text, pytestconfig, mock_run_claude, feishu_calls):
|
||||
import asyncio
|
||||
from orchestrator.agent import agent
|
||||
from tests.conftest import make_vcr_cassette
|
||||
user_id = getattr(pytestconfig, "_test_user_id", "user_abc123")
|
||||
cassette_name = getattr(pytestconfig, "_vcr_cassette", None)
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
if cassette_name:
|
||||
with make_vcr_cassette(cassette_name):
|
||||
reply = loop.run_until_complete(agent.run(user_id, text))
|
||||
else:
|
||||
reply = loop.run_until_complete(agent.run(user_id, text))
|
||||
|
||||
pytestconfig._reply = reply
|
||||
|
||||
|
||||
# ── Then: agent-specific assertions ─────────────────────────────────────────
|
||||
|
||||
@then(parsers.parse('agent created a session for user "{user_id}"'))
|
||||
def check_session_created(user_id):
|
||||
from orchestrator.agent import agent
|
||||
assert agent._active_conv.get(user_id) is not None, \
|
||||
f"Expected active session to be set for {user_id}"
|
||||
|
||||
|
||||
@then(parsers.parse('no session is created for user "{user_id}"'))
|
||||
def check_no_session(user_id):
|
||||
from orchestrator.agent import agent
|
||||
assert agent._active_conv.get(user_id) is None, \
|
||||
f"Expected no active session for {user_id}, got {agent._active_conv.get(user_id)}"
|
||||
@@ -0,0 +1,78 @@
|
||||
"""
|
||||
Step definitions for all slash command features.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
|
||||
from pytest_bdd import scenarios, given, when, then, parsers
|
||||
|
||||
from tests.step_defs.common_steps import * # noqa: F401,F403 — import shared steps
|
||||
|
||||
scenarios(
|
||||
"../features/commands/new.feature",
|
||||
"../features/commands/status.feature",
|
||||
"../features/commands/switch.feature",
|
||||
"../features/commands/close.feature",
|
||||
"../features/commands/direct_smart.feature",
|
||||
"../features/commands/shell.feature",
|
||||
"../features/commands/remind.feature",
|
||||
"../features/commands/tasks.feature",
|
||||
"../features/commands/nodes.feature",
|
||||
"../features/commands/help.feature",
|
||||
)
|
||||
|
||||
|
||||
# ── When: send slash command ─────────────────────────────────────────────────
|
||||
|
||||
@when(parsers.parse('user sends "{text}"'))
|
||||
def send_command(text, pytestconfig, feishu_calls, mock_run_claude):
|
||||
import asyncio
|
||||
from bot.commands import handle_command
|
||||
user_id = getattr(pytestconfig, "_test_user_id", "user_abc123")
|
||||
reply = asyncio.get_event_loop().run_until_complete(handle_command(user_id, text))
|
||||
pytestconfig._reply = reply
|
||||
|
||||
|
||||
# ── Given: task runner state ─────────────────────────────────────────────────
|
||||
|
||||
@given(parsers.parse('there is a running task "{task_id}" described as "{desc}"'))
|
||||
def add_running_task(task_id, desc):
|
||||
from agent.task_runner import task_runner, BackgroundTask, TaskStatus
|
||||
task = BackgroundTask(
|
||||
task_id=task_id,
|
||||
description=desc,
|
||||
started_at=time.time(),
|
||||
status=TaskStatus.RUNNING,
|
||||
)
|
||||
task_runner._tasks[task_id] = task
|
||||
|
||||
|
||||
@given(parsers.parse('there is a completed task "{task_id}" described as "{desc}"'))
|
||||
def add_completed_task(task_id, desc):
|
||||
from agent.task_runner import task_runner, BackgroundTask, TaskStatus
|
||||
now = time.time()
|
||||
task = BackgroundTask(
|
||||
task_id=task_id,
|
||||
description=desc,
|
||||
started_at=now - 5,
|
||||
status=TaskStatus.COMPLETED,
|
||||
completed_at=now,
|
||||
result="success",
|
||||
)
|
||||
task_runner._tasks[task_id] = task
|
||||
|
||||
|
||||
@given(parsers.parse('there is a failed task "{task_id}" described as "{desc}"'))
|
||||
def add_failed_task(task_id, desc):
|
||||
from agent.task_runner import task_runner, BackgroundTask, TaskStatus
|
||||
now = time.time()
|
||||
task = BackgroundTask(
|
||||
task_id=task_id,
|
||||
description=desc,
|
||||
started_at=now - 3,
|
||||
status=TaskStatus.FAILED,
|
||||
completed_at=now,
|
||||
error="subprocess failed",
|
||||
)
|
||||
task_runner._tasks[task_id] = task
|
||||
Reference in New Issue
Block a user