"""Pytest configuration and shared fixtures for QE acceptance tests. Usage:: pytest tests/acceptance/ -v --run-acceptance [--acceptance-runs=3] Environment variables: DASHSCOPE_API_KEY — LLM API key (required for Layers B/C) TEST_IR_PATH — path to IR JSON to validate (default: ir_final.json sample) TEST_PARSED_PATH — path to _parsed.json or _updated.json for coverage analysis """ from __future__ import annotations import json import os import sys import tempfile from pathlib import Path from typing import Any import pytest # ── Path setup ────────────────────────────────────────────────────────────── _PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent sys.path.insert(0, str(_PROJECT_ROOT)) def _skill_path(skill_name: str) -> str: return str(_PROJECT_ROOT / "skills" / skill_name / "scripts") # ── pytest configuration ──────────────────────────────────────────────────── def pytest_addoption(parser): parser.addoption( "--run-acceptance", action="store_true", default=False, help="Run QE acceptance tests (requires DASHSCOPE_API_KEY)", ) parser.addoption( "--acceptance-runs", type=int, default=1, help="Number of IR generation runs for Layer B stability testing (default: 1 = skip)", ) parser.addoption( "--ir-path", type=str, default=None, help="Path to IR JSON file to validate", ) parser.addoption( "--parsed-path", type=str, default=None, help="Path to _parsed.json or _updated.json for coverage analysis", ) def pytest_configure(config): config.addinivalue_line( "markers", "acceptance: QE acceptance test (requires --run-acceptance flag and DASHSCOPE_API_KEY)", ) def pytest_collection_modifyitems(config, items): acceptance_dir = str(_PROJECT_ROOT / "tests" / "acceptance") acceptance_items = [i for i in items if str(i.fspath).startswith(acceptance_dir)] non_acceptance_items = [i for i in items if not str(i.fspath).startswith(acceptance_dir)] if not config.getoption("--run-acceptance"): skip_msg = pytest.mark.skip(reason="Need --run-acceptance flag to run") for item in acceptance_items: item.add_marker(skip_msg) # Don't skip non-acceptance tests return if not os.environ.get("DASHSCOPE_API_KEY"): skip_msg = pytest.mark.skip(reason="DASHSCOPE_API_KEY not set") for item in acceptance_items: item.add_marker(skip_msg) # ── Shared fixtures ───────────────────────────────────────────────────────── @pytest.fixture(scope="session") def project_root() -> Path: return _PROJECT_ROOT @pytest.fixture(scope="session") def ir_path(request) -> str: """Path to the IR JSON file under test.""" path = ( request.config.getoption("--ir-path") or os.environ.get("TEST_IR_PATH") or str( Path.home() / ".openclaw/workspace/skills/doc_parser_skill/output/ir_final.json" ) ) if not os.path.exists(path): pytest.skip(f"IR file not found: {path}") return path @pytest.fixture(scope="session") def ir_data(ir_path: str) -> dict: """Load the IR JSON data.""" with open(ir_path, "r", encoding="utf-8") as f: return json.load(f) @pytest.fixture(scope="session") def parsed_path(request) -> str | None: """Path to the corresponding _parsed.json or _updated.json.""" path = ( request.config.getoption("--parsed-path") or os.environ.get("TEST_PARSED_PATH") or str( _PROJECT_ROOT / "skills/ir_generation_skill/车机娱乐系统禁止功能文档_精简_updated.json" ) ) if os.path.exists(path): return path return None @pytest.fixture(scope="session") def parsed_data(parsed_path: str | None) -> dict | None: """Load the parsed document JSON for coverage analysis.""" if parsed_path is None: return None with open(parsed_path, "r", encoding="utf-8") as f: return json.load(f) @pytest.fixture(scope="session") def llm_client(): """Create an LLMClient instance for acceptance tests. Uses the DashScope-compatible LLMClient from the project. """ sys.path.insert(0, _skill_path("doc_parser_skill")) from LLM import LLMClient return LLMClient() @pytest.fixture(scope="session") def acceptance_runs(request) -> int: return request.config.getoption("--acceptance-runs", default=1) # ── Pipeline runner ───────────────────────────────────────────────────────── @pytest.fixture(scope="session") def run_ir_pipeline(): """Return a callable that runs the IR generation pipeline on a parsed JSON. Usage:: ir_data, ir_path = run_ir_pipeline(parsed_json_path, output_dir) """ sys.path.insert(0, _skill_path("ir_generation_skill")) from ir_generator import generate_ir def _run(parsed_path: str, output_dir: str | None = None) -> tuple[dict, str]: """Run IR generation and return (ir_data, ir_path).""" out = output_dir or tempfile.mkdtemp(prefix="qe_acceptance_") result = generate_ir(parsed_path, out, dry_run=False) ir_list = result.get("ir", []) ir_path = result.get("path", "") # ir_generator produces a list; wrap to match rich format expectations # for schema validation we accept both formats return ir_list, ir_path return _run