init: llm-wiki-haness 하네스 설계
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user