166 lines
6.9 KiB
Python
166 lines
6.9 KiB
Python
from __future__ import annotations
|
||
|
||
import json
|
||
import shutil
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
from helpers import ROOT, run_cli
|
||
|
||
sys.path.insert(0, str(ROOT / "scripts"))
|
||
from check_release_sync import SEMVER_RE # noqa: E402
|
||
|
||
|
||
def test_canonical_quick_rules_are_current() -> None:
|
||
result = run_cli("build_quick_rules.py", "--check")
|
||
assert result.returncode == 0, result.stderr
|
||
|
||
|
||
def test_quick_rules_check_detects_drift(tmp_path: Path) -> None:
|
||
output = tmp_path / "quick-rules.md"
|
||
generated = run_cli("build_quick_rules.py", "--output", output)
|
||
assert generated.returncode == 0, generated.stderr
|
||
current = run_cli("build_quick_rules.py", "--output", output, "--check")
|
||
assert current.returncode == 0, current.stderr
|
||
output.write_text(output.read_text(encoding="utf-8") + "drift\n", encoding="utf-8")
|
||
stale = run_cli("build_quick_rules.py", "--output", output, "--check")
|
||
assert stale.returncode == 1
|
||
assert "out of date" in stale.stderr
|
||
|
||
|
||
def test_quick_rules_render_every_route_and_threshold_switches(tmp_path: Path) -> None:
|
||
rules = json.loads(
|
||
(ROOT / "skills/technical-doc-flow/config/quality-rules.json").read_text(encoding="utf-8")
|
||
)
|
||
rules["thresholds"]["route"]["light"]["requires_existing_draft"] = False
|
||
rules["thresholds"]["logic"]["core_claim_max_reader_paragraphs"] = 3
|
||
rules["thresholds"]["split"]["default_max_chars"] = 4321
|
||
rules["thresholds"]["split"]["minimum_h2_fill_ratio"] = 0.5
|
||
custom_rules = tmp_path / "rules.json"
|
||
custom_rules.write_text(json.dumps(rules), encoding="utf-8")
|
||
output = tmp_path / "quick.md"
|
||
result = run_cli(
|
||
"build_quick_rules.py", "--rules", custom_rules, "--output", output
|
||
)
|
||
assert result.returncode == 0, result.stderr
|
||
rendered = output.read_text(encoding="utf-8")
|
||
assert "기존 초안 불필요" in rendered
|
||
assert "독자용 앞 3개 문단" in rendered
|
||
assert "`backpressure`" in rendered
|
||
assert "`UTF-8`" in rendered
|
||
assert "4321자" in rendered
|
||
assert "50% (raw `0.5`)" in rendered
|
||
|
||
first = rendered
|
||
rules["thresholds"]["logic"]["core_claim_max_reader_paragraphs"] = 4
|
||
rules["thresholds"]["split"]["minimum_h2_fill_ratio"] = 0.501
|
||
rules["thresholds"]["finalization"]["max_change_rate"] = 0.151
|
||
custom_rules.write_text(json.dumps(rules), encoding="utf-8")
|
||
assert run_cli(
|
||
"build_quick_rules.py", "--rules", custom_rules, "--output", output
|
||
).returncode == 0
|
||
second = output.read_text(encoding="utf-8")
|
||
assert first != second
|
||
assert "독자용 앞 4개 문단" in second
|
||
assert "raw `0.501`" in second and "raw `0.151`" in second
|
||
|
||
|
||
def test_quick_rules_validates_custom_runtime_contract(tmp_path: Path) -> None:
|
||
contract = json.loads(
|
||
(ROOT / "skills/technical-doc-flow/config/runtime-contract.json").read_text(
|
||
encoding="utf-8"
|
||
)
|
||
)
|
||
contract["name"] = "wrong-name"
|
||
path = tmp_path / "contract.json"
|
||
path.write_text(json.dumps(contract), encoding="utf-8")
|
||
result = run_cli(
|
||
"build_quick_rules.py",
|
||
"--contract",
|
||
path,
|
||
"--output",
|
||
tmp_path / "quick.md",
|
||
)
|
||
assert result.returncode == 2
|
||
assert "schema 위반" in result.stderr
|
||
|
||
|
||
def test_release_and_path_contracts_are_current() -> None:
|
||
result = run_cli("check_release_sync.py", "--root", ROOT)
|
||
assert result.returncode == 0, result.stderr
|
||
|
||
|
||
def test_release_check_rejects_fake_wrapper_and_unmapped_artifact(tmp_path: Path) -> None:
|
||
copied = tmp_path / "repository"
|
||
shutil.copytree(ROOT, copied, ignore=shutil.ignore_patterns("__pycache__", ".pytest_cache"))
|
||
wrapper = copied / "scripts" / "split_document.py"
|
||
wrapper.write_text(
|
||
"from _runtime_entry import execute\nif False:\n execute('split_document.py')\n",
|
||
encoding="utf-8",
|
||
)
|
||
relative_wrapper = copied / "scripts" / "lint_document.py"
|
||
relative_wrapper.write_text(
|
||
"from ._runtime_entry import execute\nexecute('lint_document.py')\n",
|
||
encoding="utf-8",
|
||
)
|
||
(copied / "scripts" / "_runtime_entry.py").unlink()
|
||
contract = copied / "skills" / "technical-doc-flow" / "config" / "runtime-contract.json"
|
||
value = json.loads(contract.read_text(encoding="utf-8"))
|
||
value["artifacts"]["always"].append("10_unmapped.json")
|
||
contract.write_text(json.dumps(value), encoding="utf-8")
|
||
verifier = copied / "skills" / "technical-doc-flow" / "scripts" / "verify_run.py"
|
||
verifier.write_text(
|
||
verifier.read_text(encoding="utf-8").replace(
|
||
'"08_lint.json": "lint-report.schema.json"',
|
||
'"08_lint.json": "review.schema.json"',
|
||
),
|
||
encoding="utf-8",
|
||
)
|
||
result = run_cli("check_release_sync.py", "--root", copied)
|
||
assert result.returncode == 1
|
||
assert "exact canonical execute template" in result.stderr
|
||
assert "root runtime entry helper 누락" in result.stderr
|
||
assert "wrapper --help smoke 실패" in result.stderr
|
||
assert "매핑이 없는 runtime artifact" in result.stderr
|
||
assert "artifact schema mapping 불일치" in result.stderr
|
||
|
||
|
||
def test_release_check_validates_runtime_contract_schema(tmp_path: Path) -> None:
|
||
copied = tmp_path / "repository"
|
||
shutil.copytree(ROOT, copied, ignore=shutil.ignore_patterns("__pycache__", ".pytest_cache"))
|
||
contract_path = copied / "skills" / "technical-doc-flow" / "config" / "runtime-contract.json"
|
||
contract = json.loads(contract_path.read_text(encoding="utf-8"))
|
||
contract["supported_document_kinds"] = ["garbage"]
|
||
contract["artifacts"]["unexpected"] = []
|
||
contract_path.write_text(json.dumps(contract), encoding="utf-8")
|
||
result = run_cli("check_release_sync.py", "--root", copied)
|
||
assert result.returncode == 1
|
||
assert "runtime-contract.schema.json 검증 실패" in result.stderr
|
||
|
||
|
||
def test_semver_is_strict_and_marketplace_matches_by_name(tmp_path: Path) -> None:
|
||
for valid in ("0.1.0", "1.2.3-alpha.1", "1.2.3+build.01", "1.0.0-0"):
|
||
assert SEMVER_RE.fullmatch(valid), valid
|
||
for invalid in (
|
||
"1.0.0-01",
|
||
"1.0.0-..",
|
||
"1.0.0-alpha..1",
|
||
"1.0.0+..",
|
||
"1١.0.0",
|
||
"1.0.1٢",
|
||
"1.0.0-1١",
|
||
):
|
||
assert not SEMVER_RE.fullmatch(invalid), invalid
|
||
|
||
copied = tmp_path / "repository"
|
||
shutil.copytree(ROOT, copied, ignore=shutil.ignore_patterns("__pycache__", ".pytest_cache"))
|
||
marketplace_path = copied / ".claude-plugin" / "marketplace.json"
|
||
marketplace = json.loads(marketplace_path.read_text(encoding="utf-8"))
|
||
marketplace["plugins"].insert(0, {"name": "unrelated", "version": "0.1.0"})
|
||
target = next(item for item in marketplace["plugins"] if item["name"] == "technical-doc-flow")
|
||
target["version"] = "9.9.9"
|
||
marketplace_path.write_text(json.dumps(marketplace), encoding="utf-8")
|
||
result = run_cli("check_release_sync.py", "--root", copied)
|
||
assert result.returncode == 1
|
||
assert "technical-doc-flow plugin='9.9.9'" in result.stderr
|