from __future__ import annotations
import re
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
CI_WORKFLOW_PATH = ROOT / ".github" / "workflows" / "ci.yml"
CONTRACT_POLICY_DOC = ROOT / "docs" / "api-contract-validation.md"
FIELD_STRICT_DOMAINS = (
{
"name": "attendance",
"crate": "openlark-hr",
"biz_tag": "attendance",
"report_dir_fragment": "api_contract_fields/attendance",
},
{
"name": "docs",
"crate": "openlark-docs",
"biz_tag": None,
"report_dir_fragment": "api_contract_fields/docs",
},
)
def _api_contracts_job(workflow: str) -> str:
match = re.search(
r"(?ms)^ api-contracts:\n(.*?)(?=^ [a-z0-9_-]+:|\Z)",
workflow,
)
assert match is not None, "api-contracts job missing from ci.yml"
return match.group(0)
class ApiContractsCiGatesTests(unittest.TestCase):
def setUp(self) -> None:
self.workflow = CI_WORKFLOW_PATH.read_text(encoding="utf-8")
self.job = _api_contracts_job(self.workflow)
def test_endpoint_strict_covers_all_crates(self) -> None:
self.assertIn("--all-crates", self.job)
self.assertIn("--strict endpoint", self.job)
def test_token_strict_covers_security_and_auth(self) -> None:
self.assertIn("--crate openlark-security", self.job)
self.assertIn("--crate openlark-auth", self.job)
self.assertGreaterEqual(self.job.count("--strict tokens"), 2)
def test_attendance_field_strict_gate(self) -> None:
self.assertIn("--crate openlark-hr", self.job)
self.assertIn("--biz-tag attendance", self.job)
self.assertIn("--strict fields", self.job)
self.assertIn("api_contract_fields/attendance", self.job)
attendance_block = self._step_run_block_containing("--biz-tag attendance")
self.assertIn("--fields", attendance_block)
self.assertIn("--live-fields", attendance_block)
self.assertIn("--strict fields", attendance_block)
def test_docs_field_strict_gate(self) -> None:
self.assertIn("--crate openlark-docs", self.job)
self.assertIn("api_contract_fields/docs", self.job)
docs_block = self._step_run_block_containing("--crate openlark-docs")
self.assertIn("--fields", docs_block)
self.assertIn("--live-fields", docs_block)
self.assertIn("--strict fields", docs_block)
def test_field_strict_inventory_is_exactly_attendance_and_docs(self) -> None:
strict_fields_steps = [
block
for block in self._step_run_blocks()
if "--strict fields" in block
]
self.assertEqual(
len(strict_fields_steps),
len(FIELD_STRICT_DOMAINS),
"field-strict step count must match FIELD_STRICT_DOMAINS inventory",
)
for domain in FIELD_STRICT_DOMAINS:
self.assertIn(f"--crate {domain['crate']}", self.job)
self.assertIn(domain["report_dir_fragment"], self.job)
if domain["biz_tag"] is not None:
self.assertIn(f"--biz-tag {domain['biz_tag']}", self.job)
def test_no_monorepo_wide_field_strict(self) -> None:
for block in self._step_run_blocks():
if "--strict fields" not in block:
continue
self.assertNotIn(
"--all-crates",
block,
"field-strict must stay domain-scoped; do not pair "
"--all-crates with --strict fields",
)
def test_admission_policy_doc_defines_next_domain_slot(self) -> None:
self.assertTrue(
CONTRACT_POLICY_DOC.is_file(),
f"missing contract policy doc: {CONTRACT_POLICY_DOC}",
)
text = CONTRACT_POLICY_DOC.read_text(encoding="utf-8")
self.assertRegex(
text,
r"next field-strict domain|下一[个个]? field-strict 域|下一域 admission",
"docs must name the next field-strict domain admission slot",
)
self.assertRegex(
text,
r"0\s*ERROR|0 error|live baseline",
"docs must require live baseline 0 ERROR before flipping strict",
)
self.assertRegex(
text,
r"dual-edit|双改|workflow.*inventory|ci\.yml.*inventory|gate inventory",
"docs must state the dual-edit rule (workflow + inventory test)",
)
self.assertRegex(
text,
r"monorepo[- ]wide|--all-crates.*--strict fields|全仓.*strict fields",
"docs must restate non-goal: no monorepo-wide field strict",
)
self.assertRegex(
text,
r"threshold|阈值",
"docs must restate non-goal: no hard-gate threshold lowering",
)
self.assertIn("attendance", text)
self.assertIn("openlark-docs", text)
self.assertIn("test_validate_api_contracts_ci_gates", text)
def test_gate_inventory_verification_section_present(self) -> None:
text = CONTRACT_POLICY_DOC.read_text(encoding="utf-8")
self.assertRegex(
text,
r"gate inventory|门禁清单|trust gate",
"docs must include a written gate-inventory verification section",
)
self.assertIn("--strict endpoint", text)
self.assertIn("--strict tokens", text)
self.assertIn("--strict fields", text)
self.assertRegex(
text,
r"typed.coverage|typed_coverage_release|path_noise|true_gap",
"docs must reference typed-coverage / path_noise locks",
)
def _step_run_blocks(self) -> list[str]:
return re.split(r"(?m)^(?= - (?:name|uses):)", self.job)
def _step_run_block_containing(self, needle: str) -> str:
for block in self._step_run_blocks():
if needle in block:
return block
self.fail(f"no api-contracts step contains {needle!r}")
if __name__ == "__main__":
unittest.main()