Files
2026-09-22 13:20:58 +01:00

125 lines
4.4 KiB
Python

"""Temporary-repository support for architecture-lint tests."""
import json
import subprocess
import sys
import tempfile
import textwrap
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
LINTER = REPO_ROOT / "scripts" / "lint-architecture.py"
sys.path.insert(0, str(LINTER.parent))
from architecture_lint.rules import RULES # noqa: E402
RUNTIME_RULE = "ARCH-RUNTIME-IMPORT"
TASK_OFFICE_RULE = "ARCH-TASK-OFFICE-IMPORT"
ROOT_STATE_RULE = "ARCH-FRONTEND-ROOT-STATE-CAST"
INBOX_HISTORY_ISOLATION_RULE = "ARCH-INBOX-HISTORY-ISOLATION"
RUNTIME_IMPORT = "github.com/kandev/kandev/internal/agent/runtime/lifecycle"
OFFICE_IMPORT = "github.com/kandev/kandev/internal/office/models"
RULE_FILES = {rule.id: rule.baseline_path.name for rule in RULES}
class ArchitectureFixture(unittest.TestCase):
def setUp(self) -> None:
self.tempdir = tempfile.TemporaryDirectory()
self.repo = Path(self.tempdir.name)
self.test_hooks = self.repo / ".empty-git-hooks"
self.test_hooks.mkdir()
self.git("init", "-q")
self.git("config", "user.email", "test@example.com")
self.git("config", "user.name", "Architecture Test")
self.write_baseline()
self.write_ledger([])
def tearDown(self) -> None:
self.tempdir.cleanup()
def git(self, *args: str) -> subprocess.CompletedProcess[str]:
# Synthetic history must not depend on machine-wide Git hooks.
return subprocess.run(
["git", "-c", f"core.hooksPath={self.test_hooks}", *args],
cwd=self.repo,
text=True,
capture_output=True,
check=True,
)
def write(self, relative: str, content: str) -> None:
path = self.repo / relative
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(textwrap.dedent(content).lstrip(), encoding="utf-8")
def write_json(self, relative: str, content: object) -> None:
self.write(relative, json.dumps(content, indent=2) + "\n")
def write_baseline(
self,
*,
runtime: list[dict[str, object]] | None = None,
task_office: list[dict[str, object]] | None = None,
root_state: list[dict[str, object]] | None = None,
inbox_history_isolation: list[dict[str, object]] | None = None,
) -> None:
entries = {rule.id: [] for rule in RULES}
entries.update(
{
RUNTIME_RULE: runtime or [],
TASK_OFFICE_RULE: task_office or [],
ROOT_STATE_RULE: root_state or [],
INBOX_HISTORY_ISOLATION_RULE: inbox_history_isolation or [],
}
)
for rule in RULES:
self.write_json(
rule.baseline_path.as_posix(),
{"version": 1, "rule": rule.id, "entries": entries[rule.id]},
)
def write_ledger(self, entries: list[dict[str, object]]) -> None:
self.write_json(
"config/architecture-lint/compatibility-ledger.json",
{"version": 1, "entries": entries},
)
def track_all(self) -> None:
self.git("add", ".")
def run_cli(self, *args: str, cwd: Path | None = None) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[sys.executable, str(LINTER), *args],
cwd=cwd or self.repo,
text=True,
capture_output=True,
check=False,
)
def assert_diagnostic_location(self, result: subprocess.CompletedProcess[str], path: str, line: int) -> None:
self.assertIn(path, result.stdout)
self.assertTrue(
f"{path}:{line}" in result.stdout or f"line={line}" in result.stdout,
result.stdout,
)
@staticmethod
def runtime_entry(path: str) -> dict[str, str]:
return {"path": path, "import": RUNTIME_IMPORT}
def valid_ledger_entry(self, *, marker: str = "// compat: old-name") -> dict[str, object]:
return {
"id": "old-name-alias",
"locator": {
"path": "apps/backend/internal/example/compat.go",
"marker": marker,
},
"reason": "Older persisted callers still use the old name.",
"owner": "backend maintainers",
"introduced_on": "2026-01-15",
"removal_condition": "Remove after all persisted callers have migrated.",
"target_removal_date": "2099-12-31",
}