Files
llm-wiki/harness/tests/test_layout_check.py
T

145 lines
6.7 KiB
Python

from __future__ import annotations
import json
import hashlib
import sys
from pathlib import Path
import tempfile
import unittest
RUNTIME = Path(__file__).resolve().parents[1] / "runtime"
sys.path.insert(0, str(RUNTIME))
from layout_check import LayoutContractError, check_layout, enforce_write_paths, resolve_authority # noqa: E402
class LayoutCheckTests(unittest.TestCase):
def _fixture(self, root: Path) -> None:
for path in (
"raw/branch-notes",
"wiki/concepts",
"harness/source",
"harness/adapters",
"harness/runtime",
"harness/tests",
"vault/10-projects",
"vault/30-knowledge",
):
(root / path).mkdir(parents=True, exist_ok=True)
(root / "harness/source/vault-layout.json").write_text(
json.dumps(
{
"schema_version": "vault-layout/v1",
"mode": "compatibility",
"vault_root": "vault",
"write_roots": {
"compatibility": ["raw", "wiki"],
"shadow": ["raw", "wiki"],
"canonical": ["vault"],
},
"migration_manifest": {"schema_version": "vault-migration/v1", "entries": []},
"rollback_mapping": {"schema_version": "vault-rollback/v1", "entries": []},
"areas": {
"10-projects": ["raw/branch-notes"],
"30-knowledge": ["wiki/concepts"],
},
}
),
encoding="utf-8",
)
def test_complete_unique_mapping_passes(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
self._fixture(root)
self.assertEqual(check_layout(root)["status"], "PASS")
def test_unassigned_root_fails(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
self._fixture(root)
(root / "raw/orphan").mkdir()
result = check_layout(root)
self.assertEqual(result["status"], "FAIL")
self.assertIn("UNASSIGNED_CONTENT_ROOT", {item["code"] for item in result["findings"]})
def _configure_cutover(self, root: Path, mode: str, *, canonical_bytes: bytes, legacy_bytes: bytes) -> None:
legacy = root / "raw/branch-notes/feature-example.md"
canonical = root / "vault/10-projects/feature-example.md"
legacy.write_bytes(legacy_bytes)
canonical.write_bytes(canonical_bytes)
manifest_path = root / "harness/source/vault-layout.json"
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
manifest["mode"] = mode
manifest["migration_manifest"]["entries"] = [{
"legacy_path": "raw/branch-notes/feature-example.md",
"canonical_path": "vault/10-projects/feature-example.md",
"sha256": hashlib.sha256(canonical_bytes).hexdigest(),
}]
manifest["rollback_mapping"]["entries"] = [{
"canonical_path": "vault/10-projects/feature-example.md",
"legacy_path": "raw/branch-notes/feature-example.md",
}]
manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
def test_shadow_requires_byte_identical_mirror(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
self._fixture(root)
content = b"---\ntitle: example\n---\n# Example\n"
self._configure_cutover(root, "shadow", canonical_bytes=content, legacy_bytes=content)
self.assertEqual(check_layout(root)["status"], "PASS")
(root / "vault/10-projects/feature-example.md").write_text("drift\n", encoding="utf-8")
result = check_layout(root)
codes = {item["code"] for item in result["findings"]}
self.assertIn("SHADOW_MIRROR_DRIFT", codes)
self.assertIn("MIGRATION_HASH_MISMATCH", codes)
def test_shadow_mirror_symlink_is_rejected(self) -> None:
"""shadow 미러가 심링크면 read_bytes 가 정본을 따라가 drift 를 못 잡는다(symlink-blind).
예전엔 legacy 가 정본을 가리키는 심링크여도 바이트가 같아 PASS 했다 — shadow 불변식
(독립 실파일 미러)을 위반하는데도 조용했다. 이제 심링크 자체를 loud 하게 잡는다.
"""
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
self._fixture(root)
content = b"---\ntitle: example\n---\n# Example\n"
self._configure_cutover(root, "shadow", canonical_bytes=content, legacy_bytes=content)
self.assertEqual(check_layout(root)["status"], "PASS")
# legacy 실파일을 정본을 가리키는 심링크로 바꾼다 → 바이트는 여전히 동일.
legacy = root / "raw/branch-notes/feature-example.md"
legacy.unlink()
legacy.symlink_to(Path("../../vault/10-projects/feature-example.md"))
result = check_layout(root)
codes = {item["code"] for item in result["findings"]}
self.assertEqual(result["status"], "FAIL")
self.assertIn("SHADOW_MIRROR_SYMLINK", codes)
self.assertNotIn("SHADOW_MIRROR_DRIFT", codes) # 심링크가 바이트 비교를 가리기 전에 잡힘
def test_canonical_requires_stub_owner_and_rollback_mapping(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
self._fixture(root)
canonical = b"---\ntitle: example\n---\n# Example\n"
stub = b"---\ntitle: moved\ncanonical_path: vault/10-projects/feature-example.md\n---\n# Moved\n"
self._configure_cutover(root, "canonical", canonical_bytes=canonical, legacy_bytes=stub)
result = check_layout(root)
self.assertEqual(result["status"], "PASS", result["findings"])
self.assertEqual(result["authority"], "vault")
self.assertEqual(result["write_roots"], ["vault"])
authority = resolve_authority(root)
enforce_write_paths(root, [root / "vault/10-projects/new.md"], authority)
with self.assertRaises(LayoutContractError) as raised:
enforce_write_paths(root, [root / "raw/branch-notes/new.md"], authority)
self.assertEqual(raised.exception.code, "WRITE_ROOT_VIOLATION")
(root / "raw/branch-notes/feature-example.md").write_bytes(canonical)
result = check_layout(root)
self.assertIn("OLD_NEW_FULL_CONTENT_DUPLICATE", {item["code"] for item in result["findings"]})
if __name__ == "__main__":
unittest.main()