mirror of
https://github.com/tw93/Kami.git
synced 2026-09-18 10:18:26 +08:00
c9617b6aad
Separate HTML visibility evidence from document checks and group tests by responsibility without changing the CLI. Restore demo scan coverage after the layout split, reject missing repository surfaces, and keep generated examples out of source control.
256 lines
9.8 KiB
Python
256 lines
9.8 KiB
Python
"""Test entry point: python3 scripts/tests/test_build.py. Discovers sibling suites."""
|
|
from __future__ import annotations
|
|
|
|
import support
|
|
from support import REPO_ROOT, check, run_build_args, silently, skip
|
|
|
|
import importlib
|
|
import importlib.util
|
|
import inspect
|
|
import os
|
|
import shared as shared_mod
|
|
import sys
|
|
import tempfile
|
|
from build import (
|
|
DIAGRAM_TARGETS,
|
|
HTML_TARGETS,
|
|
PPTX_TARGETS,
|
|
SCREEN_TARGETS,
|
|
)
|
|
from pathlib import Path
|
|
from shared import (
|
|
DIAGRAMS,
|
|
DIAGRAM_TEMPLATES,
|
|
HTML_TEMPLATES,
|
|
MARP_TEMPLATES,
|
|
PARCHMENT_RGB,
|
|
SCREEN_TEMPLATES,
|
|
TEMPLATES,
|
|
build_targets,
|
|
diagram_targets,
|
|
marp_targets,
|
|
pptx_targets,
|
|
screen_targets,
|
|
)
|
|
|
|
|
|
def test_registry_consistency() -> None:
|
|
check("HTML_TEMPLATES has 24 entries", len(HTML_TEMPLATES) == 24,
|
|
f"got {len(HTML_TEMPLATES)}")
|
|
check("SCREEN_TARGETS has 3 entries", len(SCREEN_TARGETS) == 3,
|
|
f"got {len(SCREEN_TARGETS)}")
|
|
check("build_targets matches HTML_TEMPLATES key set",
|
|
set(build_targets()) == set(HTML_TEMPLATES))
|
|
check("screen_targets matches SCREEN_TARGETS key set",
|
|
set(screen_targets()) == set(SCREEN_TARGETS))
|
|
check("HTML_TARGETS in build.py matches build_targets()",
|
|
dict(HTML_TARGETS) == build_targets())
|
|
check("DIAGRAM_TARGETS has 18 entries", len(DIAGRAM_TARGETS) == 18,
|
|
f"got {len(DIAGRAM_TARGETS)}")
|
|
check("DIAGRAM_TARGETS in build.py matches shared.diagram_targets()",
|
|
dict(DIAGRAM_TARGETS) == diagram_targets() == dict(DIAGRAM_TEMPLATES))
|
|
check("PPTX_TARGETS has 2 entries", len(PPTX_TARGETS) == 2,
|
|
f"got {len(PPTX_TARGETS)}")
|
|
check("PPTX_TARGETS in build.py matches shared.pptx_targets()",
|
|
dict(PPTX_TARGETS) == pptx_targets())
|
|
registered_sources = {
|
|
**{f"html:{name}": TEMPLATES / spec.source
|
|
for name, spec in HTML_TEMPLATES.items()},
|
|
**{f"screen:{name}": TEMPLATES / source
|
|
for name, source in SCREEN_TEMPLATES.items()},
|
|
**{f"pptx:{name}": TEMPLATES / source
|
|
for name, source in pptx_targets().items()},
|
|
**{f"diagram:{name}": DIAGRAMS / source
|
|
for name, source in DIAGRAM_TEMPLATES.items()},
|
|
**{f"marp:{name}": TEMPLATES / source
|
|
for name, source in MARP_TEMPLATES.items()},
|
|
}
|
|
missing_sources = sorted(
|
|
f"{name} -> {path.relative_to(REPO_ROOT)}"
|
|
for name, path in registered_sources.items()
|
|
if not path.is_file()
|
|
)
|
|
check("every registered template source exists",
|
|
missing_sources == [], ", ".join(missing_sources))
|
|
check("Marp registry maps authoring entries with matching CSS",
|
|
marp_targets() == MARP_TEMPLATES
|
|
and all(
|
|
(TEMPLATES / source).exists()
|
|
and (TEMPLATES / source).with_suffix(".css").exists()
|
|
for source in MARP_TEMPLATES.values()
|
|
),
|
|
str(MARP_TEMPLATES))
|
|
check("PARCHMENT_RGB is canonical", PARCHMENT_RGB == (0xF5, 0xF4, 0xED))
|
|
|
|
|
|
def test_ci_builds_registered_hard_page_and_pptx_targets() -> None:
|
|
workflow = (REPO_ROOT / ".github" / "workflows" / "check.yml").read_text(
|
|
encoding="utf-8")
|
|
check("CI installs the editable PPTX runtime dependency",
|
|
"python-pptx" in workflow)
|
|
check("CI derives hard-page verification from the shared registry",
|
|
"from shared import HTML_TEMPLATES" in workflow
|
|
and "if spec.build_max_pages" in workflow)
|
|
check("CI verifies both editable PPTX targets",
|
|
"--verify slides\n" in workflow and "--verify slides-en" in workflow)
|
|
|
|
|
|
def test_public_document_kinds_derive_new_registry_entries() -> None:
|
|
from shared import TemplateSpec, public_document_template_kinds
|
|
|
|
shared_mod.HTML_TEMPLATES["invoice"] = TemplateSpec("invoice.html", 1)
|
|
try:
|
|
kinds = public_document_template_kinds()
|
|
finally:
|
|
shared_mod.HTML_TEMPLATES.pop("invoice", None)
|
|
check("new registry kinds cannot disappear behind the public-kind allowlist",
|
|
"invoice" in kinds,
|
|
str(sorted(kinds)))
|
|
|
|
|
|
def test_threshold_fallback_includes_resume_balance() -> None:
|
|
original = shared_mod.CHECKS_THRESHOLDS_FILE
|
|
with tempfile.TemporaryDirectory() as d:
|
|
shared_mod.CHECKS_THRESHOLDS_FILE = Path(d) / "missing-thresholds.json"
|
|
shared_mod.load_checks_thresholds.cache_clear()
|
|
try:
|
|
resume = shared_mod.load_checks_thresholds().get("resume_balance")
|
|
finally:
|
|
shared_mod.CHECKS_THRESHOLDS_FILE = original
|
|
shared_mod.load_checks_thresholds.cache_clear()
|
|
check("threshold fallback keeps the resume balance contract",
|
|
resume == {
|
|
"min_fill_pct": 0.83,
|
|
"max_fill_pct": 0.95,
|
|
"max_gap_pct": 0.12,
|
|
"dpi": 36,
|
|
},
|
|
repr(resume))
|
|
|
|
|
|
def test_checkout_detection_does_not_depend_on_site_presence() -> None:
|
|
"""A missing site must not turn an incomplete checkout into an installed skill."""
|
|
with tempfile.TemporaryDirectory() as d:
|
|
root = Path(d).resolve()
|
|
module_path = root / "skills" / "kami" / "scripts" / "shared.py"
|
|
module_path.parent.mkdir(parents=True)
|
|
module_path.write_text(Path(shared_mod.__file__).read_text(encoding="utf-8"), encoding="utf-8")
|
|
marker = root / "scripts" / "package-skill.sh"
|
|
marker.parent.mkdir()
|
|
marker.write_text("#!/bin/sh\n", encoding="utf-8")
|
|
|
|
def load():
|
|
spec = importlib.util.spec_from_file_location("shared_layout_fixture", module_path)
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
checkout = load()
|
|
check("checkout keeps its site boundary when the site directory is missing",
|
|
checkout.REPO_ROOT == root and checkout.SITE_ROOT == root / "site")
|
|
marker.unlink()
|
|
installed = load()
|
|
check("standalone installed skill has no repository or site boundary",
|
|
installed.REPO_ROOT is None and installed.SITE_ROOT is None)
|
|
|
|
|
|
def test_runner_auto_discovers_tests() -> None:
|
|
names = [name for name, _ in _test_functions()]
|
|
check("test runner auto-discovers Codex update command test",
|
|
"test_check_update_uses_codex_plugin_update_command" in names)
|
|
check("test runner auto-discovers this test",
|
|
"test_runner_auto_discovers_tests" in names)
|
|
|
|
|
|
def test_build_cli_rejects_unexpected_flags() -> None:
|
|
rc, out = run_build_args(["resume", "--verify"])
|
|
check("build.py rejects flags after target",
|
|
rc == 2 and "ERROR: unexpected argument: --verify" in out,
|
|
out.strip())
|
|
|
|
rc, out = run_build_args(["--check-density", "-v"])
|
|
check("build.py rejects unknown flags for path-based checks",
|
|
rc == 2 and "ERROR: unexpected argument: -v" in out,
|
|
out.strip())
|
|
|
|
rc, out = run_build_args(["--verify", "-v"])
|
|
check("build.py rejects unknown --verify flags",
|
|
rc == 2 and "ERROR: unexpected argument: -v" in out,
|
|
out.strip())
|
|
|
|
rc, out = run_build_args(["--check-markdown", "-v"])
|
|
check("build.py rejects unknown --check-markdown flags",
|
|
rc == 2 and "ERROR: unexpected argument: -v" in out,
|
|
out.strip())
|
|
|
|
|
|
def test_ci_required_skip_is_a_failure_not_a_pass() -> None:
|
|
"""A missing heavy dependency in CI must turn the suite red."""
|
|
original_fail, original_skip = support._FAIL, support._SKIP
|
|
original_ci = os.environ.get("CI")
|
|
try:
|
|
os.environ["CI"] = "1"
|
|
silently(skip, "negative-control fixture", ci_required=True)
|
|
rejected = support._FAIL == original_fail + 1 and support._SKIP == original_skip + 1
|
|
finally:
|
|
support._FAIL, support._SKIP = original_fail, original_skip
|
|
if original_ci is None:
|
|
os.environ.pop("CI", None)
|
|
else:
|
|
os.environ["CI"] = original_ci
|
|
check("CI-required skip increments failure and skip counters", rejected)
|
|
|
|
|
|
def test_build_cli_dispatches_new_checks() -> None:
|
|
rc, out = run_build_args(["--check-content"])
|
|
check("build.py --check-content without args is a usage error",
|
|
rc == 2 and "usage" in out, out.strip()[:120])
|
|
rc, out = run_build_args(["--check-visual"])
|
|
check("build.py --check-visual without args is a usage error",
|
|
rc == 2 and "usage" in out, out.strip()[:120])
|
|
rc, out = run_build_args(["--doctor"])
|
|
check("build.py --doctor reports capability status",
|
|
rc in {0, 1} and "Kami doctor" in out
|
|
and "visual verification" in out,
|
|
out.strip()[:300])
|
|
|
|
|
|
def _test_functions():
|
|
"""Discover every sibling suite and this entry point without a manual test list."""
|
|
modules = [sys.modules[__name__]]
|
|
modules.extend(
|
|
importlib.import_module(path.stem)
|
|
for path in sorted(Path(__file__).parent.glob("test_*.py"))
|
|
if path.stem != "test_build"
|
|
)
|
|
tests = []
|
|
names = set()
|
|
for module in modules:
|
|
for name, func in vars(module).items():
|
|
if not name.startswith("test_") or not callable(func):
|
|
continue
|
|
if getattr(func, "__module__", None) != module.__name__:
|
|
continue
|
|
if name in names:
|
|
raise ValueError(f"duplicate test name: {name}")
|
|
names.add(name)
|
|
tests.append((name, func))
|
|
return tests
|
|
|
|
|
|
def main() -> int:
|
|
for name, func in _test_functions():
|
|
signature = inspect.signature(func)
|
|
if signature.parameters:
|
|
params = ", ".join(signature.parameters)
|
|
check(f"{name} has no parameters", False, f"parameters: {params}")
|
|
continue
|
|
func()
|
|
print()
|
|
print(f"Passed: {support._PASS} | Skipped: {support._SKIP} | Failed: {support._FAIL}")
|
|
return 0 if support._FAIL == 0 else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|