Ctrl K
ComunidadGuías…

tests/test_helpers.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Shared helpers for script unit tests.

Avoid duplicating subprocess.run boilerplate across multiple test files.
"""
from __future__ import annotations

import json
import os
import subprocess
import sys
from pathlib import Path
from typing import Any


def run_script(
    script_path: Path,
    *args: str,
    cwd: Path | None = None,
    extra_env: dict[str, str] | None = None,
) -> subprocess.CompletedProcess[str]:
    """Invoke a script via subprocess, capturing stdout/stderr as text.

    extra_env values are merged into os.environ (not replaced).
    """
    if args and not all(isinstance(a, str) for a in args):
        raise TypeError(f"All positional args must be str, got {args!r}")
    env = os.environ.copy()
    if extra_env:
        env.update(extra_env)
    return subprocess.run(
        [sys.executable, str(script_path), *args],
        capture_output=True,
        text=True,
        cwd=cwd,
        env=env,
    )


def load_module_from_path(name: str, path: Path):
    """Import a script file as a module (for unit-testing its functions).

    Centralises the spec_from_file_location -> module_from_spec ->
    exec_module boilerplate. Six pre-existing test files carry local copies
    under various names (_load, _load_lint, _load_module) — they can migrate
    at next edit; new test files should call this.
    """
    import importlib.util

    spec = importlib.util.spec_from_file_location(name, path)
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    return module


def run_skill_linter(script_path: Path, root: Path) -> subprocess.CompletedProcess[str]:
    """Invoke a SKILL.md linter (--path arg + PYTHONPATH=scripts/)."""
    return run_script(
        script_path,
        "--path",
        str(root),
        extra_env={"PYTHONPATH": str(script_path.parent)},
    )


def write_skill(root: Path, name: str, frontmatter_body: str) -> None:
    """Write a synthetic <root>/<name>/SKILL.md with the given frontmatter.

    Single-sourced here (previously copied verbatim in
    test_check_task_type.py and test_check_data_access_level.py, per this
    module's migrate-at-next-edit convention)."""
    skill_dir = root / name
    skill_dir.mkdir(parents=True, exist_ok=True)
    (skill_dir / "SKILL.md").write_text(
        f"---\n{frontmatter_body}---\n\n# {name}\n",
        encoding="utf-8",
    )


def load_json_schema(path: Path) -> dict[str, Any]:
    """Load a JSON Schema file and verify it parses as Draft 2020-12.

    Centralised so test files don't re-implement the same load + check_schema
    pair. Mirrors the helper at scripts/check_literature_corpus_schema.py
    (which predates this module) — that script can migrate at next edit.
    """
    from jsonschema import Draft202012Validator

    with path.open("r", encoding="utf-8") as f:
        schema = json.load(f)
    Draft202012Validator.check_schema(schema)
    return schema


def build_schema_validator(schema: dict[str, Any]):
    """Construct a Draft 2020-12 validator with format checking enabled.

    Without format_checker, format keywords (date-time on verified_at /
    acknowledged_at / generated_at) validate silently — a hole codex flagged
    in T3 review of literature_corpus_entry.schema.json.
    """
    from jsonschema import Draft202012Validator

    return Draft202012Validator(
        schema, format_checker=Draft202012Validator.FORMAT_CHECKER
    )