assistant/tests/conftest.py

193 lines
7.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""共享夹具。
分两层:
- 纯逻辑测试:不连数据库、不调模型、不开浏览器,随时可跑。
- `@pytest.mark.db` 测试:需要本机 PostgreSQL(`npm run db:up`),不可达时整组跳过。
它们把 `settings.school_id` 换成一次性的测试学校,靠多校隔离避免污染 demo_school,跑完删干净。
"""
from __future__ import annotations
import socket
import sys
import uuid
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parent.parent
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from backend.config import settings # noqa: E402
def _pg_reachable() -> bool:
s = socket.socket()
s.settimeout(1.0)
try:
s.connect((settings.pg_host, settings.pg_port))
return True
except OSError:
return False
finally:
s.close()
PG_UP = _pg_reachable()
@pytest.fixture(scope="session", autouse=True)
def bootstrap_database_tests(request):
"""全新开发环境也能跑数据库用例,历史中心服务测试使用一次性库。"""
if not PG_UP or not any(item.get_closest_marker("db") for item in request.session.items):
yield
return
from psycopg import sql
from backend.db import database as local
from server.db import database as hub
local.init_db(retries=1, delay=0)
original = hub.settings.pg_database
temporary = f"pytest_hub_{uuid.uuid4().hex[:12]}"
hub.settings.pg_database = temporary
try:
hub.init_db(retries=1, delay=0)
yield
finally:
hub.settings.pg_database = original
conn = hub._connect(hub.settings.pg_maintenance_database)
try:
conn.autocommit = True
conn.execute(sql.SQL("DROP DATABASE IF EXISTS {} WITH (FORCE)").format(sql.Identifier(temporary)))
finally:
conn.close()
def pytest_collection_modifyitems(config, items):
if PG_UP:
return
skip = pytest.mark.skip(reason=f"PostgreSQL {settings.pg_host}:{settings.pg_port} 不可达,跳过数据库测试(npm run db:up)")
for item in items:
if "db" in item.keywords:
item.add_marker(skip)
@pytest.fixture
def test_school(monkeypatch):
"""把本次测试隔离到一所一次性的学校,退出时删掉它的全部数据。"""
school_id = f"pytest_{uuid.uuid4().hex[:10]}"
monkeypatch.setattr(settings, "school_id", school_id)
yield school_id
cleanup_school(school_id)
@pytest.fixture(autouse=True)
def isolate_ranked_recall_in_unit_tests(request, monkeypatch):
# Unit tests use in-memory task stores. They must not read the user's
# live knowledge workspace; DB integration tests exercise the real store.
if request.node.get_closest_marker("db") is None:
from backend.prospecting import orchestrator
monkeypatch.setattr(orchestrator, "ranked_experience", lambda task: [])
monkeypatch.setattr(orchestrator, "persist_knowledge", lambda task, artifacts: artifacts)
from backend.prospecting import routes
monkeypatch.setattr(routes, "save_business_feedback", lambda *args: None)
# 带 school_id 的表(settings 是全局键值表、run_logs 挂在 task 上,都不带)
SCHOOL_SCOPED_TABLES = ("learned_entries", "tasks", "system_accounts", "knowledge", "skills")
def cleanup_school(school_id: str) -> None:
from backend.db import database as db
with db.get_conn() as conn:
for table in ("prospecting_validation_events", "prospecting_business_feedback", "prospecting_methods"):
db._execute(conn, f"DELETE FROM {table} WHERE workspace_id = %s", (school_id,))
db._execute(conn, "DELETE FROM run_logs WHERE task_id IN (SELECT id FROM tasks WHERE school_id = %s)", (school_id,))
for table in SCHOOL_SCOPED_TABLES:
db._execute(conn, f"DELETE FROM {table} WHERE school_id = %s", (school_id,))
@pytest.fixture
def knowledge() -> dict:
"""一份精简的知识包:两个业务、一个系统,形状与 knowledge/xuegong_demo.yaml 一致。"""
return {
"id": "test_pack",
"name": "测试业务包",
"systems": [
{
"id": "xgxt",
"name": "学工系统",
"url": "https://example.edu/xuegong/",
"allowed_domains": ["example.edu", "*.example.edu"],
"login": {"fallback_mode": "quick_login"},
},
{
"id": "lib",
"name": "图书馆",
"url": "https://lib.example.edu/",
"allowed_domains": ["lib.example.edu"],
"login": {"fallback_mode": "form"},
},
],
"businesses": [
{
"id": "counselor_worklog",
"name": "辅导员工作日志填报(查课)",
"role": "辅导员",
"system": "xgxt",
"intent": "辅导员填写工作日志、每月查课日志、听课记录",
"required_inputs": [
{"name": "class_name", "label": "班级", "type": "text", "required": True},
{"name": "observe_date", "label": "听课日期", "type": "date", "required": True},
{"name": "course_name", "label": "课程名称", "type": "text", "required": True},
{"name": "location", "label": "听课地点", "type": "text", "required": False},
],
"workflow": [
{"step_id": "explore", "enabled": False, "goal": "只读探索"},
{
"step_id": "fill_and_submit",
"enabled": True,
"goal": "填写 {class_name} 的查课日志",
"human_confirm_before": "submit",
"on_success_status": "submitted",
"success_artifacts_any": ["submitted_record", "result_message", "cancelled"],
"prerequisites": [
{
"id": "return_verification",
"title": "返校核验",
"resolution": "other_role",
"advice": "联系辅导员先完成返校核验",
},
{
"id": "profile_complete",
"title": "完善个人信息",
"resolution": "self",
"business_id": "profile_fill",
},
],
},
],
"tracking": {
"interval_minutes": 60,
"max_checks": 24,
"step": {"goal": "回查审批状态"},
},
},
{
"id": "generic_explore",
"name": "通用探索",
"system": "xgxt",
"intent": "看看有什么、随便找找",
"required_inputs": [],
"workflow": [{"step_id": "explore", "enabled": True, "goal": "探索"}],
},
],
}
@pytest.fixture
def business(knowledge) -> dict:
return knowledge["businesses"][0]