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

153 lines
6.6 KiB
Python

"""신규 문서 생성이 3요소를 모두 계획하는지 지킨다.
canonical 모드에서 문서 하나를 새로 만들려면 정본 파일 · 호환 심링크 · manifest 2행이
*동시에* 필요하다(하나라도 빠지면 layout_check 가 CANONICAL_OWNER_MISSING /
MIGRATION_ENTRY_MISSING 으로 FAIL). 기존 문서가 그냥 되는 건 권한이 있어서가 아니라
심링크가 이미 있어 ``resolve()`` 가 번역기 노릇을 하기 때문이고, 신규 문서에는 그
번역기가 없어 **문서 생성 자체가 막혀 있었다**.
이 테스트는 저장소를 쓰지 않는다 — 계획만 계산해 검사한다.
"""
from __future__ import annotations
import json
from pathlib import Path
import sys
import unittest
REPO_ROOT = Path(__file__).resolve().parents[2]
RUNTIME = REPO_ROOT / "harness/runtime"
sys.path.insert(0, str(RUNTIME))
import vault_migrate # noqa: E402
DOC = b"""---
title: branch / feature-plan-probe
source_type: branch-note
status: raw
branch: feature-plan-probe
project: ca-skeleton-frontend-operational-contract
tags: [branch]
created: 2026-07-22
status_label: in-progress
---
# branch: feature-plan-probe
"""
def _canonical_mode() -> bool:
layout = REPO_ROOT / "harness/source/vault-layout.json"
if not layout.is_file():
return False
try:
return json.loads(layout.read_text(encoding="utf-8")).get("mode") == "canonical"
except (OSError, ValueError):
return False
class PlanNewDocumentTest(unittest.TestCase):
def setUp(self) -> None:
if not _canonical_mode():
self.skipTest("canonical 모드가 아님 — cutover 이전")
self.legacy = REPO_ROOT / "raw/branch-notes/feature-plan-probe.md"
if self.legacy.exists() or self.legacy.is_symlink():
self.skipTest("probe 슬러그가 이미 존재")
def test_plan_covers_canonical_symlink_and_manifest(self) -> None:
changes = vault_migrate.plan_new_document(REPO_ROOT, self.legacy, DOC)
rels = {p.relative_to(REPO_ROOT).as_posix(): v for p, v in changes.items()}
canonical = [r for r in rels if r.startswith("vault/")]
self.assertEqual(len(canonical), 1, f"정본이 정확히 1개여야 한다: {sorted(rels)}")
self.assertIn("ca-skeleton-frontend-operational-contract", canonical[0],
"frontmatter 의 project 로 라우팅돼야 한다")
self.assertTrue(canonical[0].endswith("/branch-notes/feature-plan-probe.md"))
self.assertEqual(rels[canonical[0]], DOC)
legacy_rel = "raw/branch-notes/feature-plan-probe.md"
self.assertIsInstance(rels[legacy_rel], vault_migrate.SymlinkValue,
"legacy 경로에는 실파일이 아니라 심링크가 놓여야 한다")
self.assertTrue((self.legacy.parent / rels[legacy_rel].target).resolve()
== (REPO_ROOT / canonical[0]))
self.assertIn("harness/source/vault-layout.json", rels,
"manifest 갱신이 같은 트랜잭션에 없으면 layout_check 가 FAIL 한다")
manifest = json.loads(rels["harness/source/vault-layout.json"].decode("utf-8"))
for key in ("migration_manifest", "rollback_mapping"):
paths = {row["legacy_path"] for row in manifest[key]["entries"]}
self.assertIn(legacy_rel, paths, f"{key} 에 신규 문서 행이 없다")
def test_existing_document_is_rejected(self) -> None:
existing = REPO_ROOT / "raw/branch-notes"
candidates = [p for p in sorted(existing.glob("*.md")) if p.is_symlink()]
if not candidates:
self.skipTest("심링크 레이아웃이 아님")
with self.assertRaises(vault_migrate.MigrationError) as ctx:
vault_migrate.plan_new_document(REPO_ROOT, candidates[0], DOC)
self.assertEqual(ctx.exception.code, "LEGACY_ALREADY_EXISTS")
EXPAND_DOC = b"""---
title: branch / feature-expand-probe
source_type: branch-note
status: raw
branch: feature-expand-probe
project: ca-skeleton-frontend-operational-contract
tags: [branch]
created: 2026-07-23
status_label: in-progress
---
# branch: feature-expand-probe
"""
class ExpandNewDocumentTest(unittest.TestCase):
"""계획 3요소가 write-root 집행에 살해당하지 않는지 지킨다.
``plan_new_documents`` 가 3요소를 올바르게 계산해도,
``expand_authoritative_changes`` 가 그 결과 *전부* 를 ``enforce_write_paths`` 에
넣으면 ②호환 심링크(raw/…)와 ③manifest(harness/source/…) 가 canonical write
root('vault') 밖이라 트랜잭션 전체가 WRITE_ROOT_VIOLATION 으로 죽는다 —
2026-07-23 branch_from_project WI-019 실측. 집행 대상은 caller 가 고른 목적지이지,
planner 가 layout 계약(①~③ 동시 성립)을 위해 스스로 도출한 산출물이 아니다.
이 테스트도 저장소를 쓰지 않는다 — expand 는 계산만 한다.
"""
def setUp(self) -> None:
if not _canonical_mode():
self.skipTest("canonical 모드가 아님 — cutover 이전")
self.legacy = REPO_ROOT / "raw/branch-notes/feature-expand-probe.md"
if self.legacy.exists() or self.legacy.is_symlink():
self.skipTest("probe 슬러그가 이미 존재")
def test_expand_admits_planned_three_elements(self) -> None:
expanded, authority = vault_migrate.expand_authoritative_changes(
REPO_ROOT, {self.legacy: EXPAND_DOC}
)
self.assertEqual(authority["mode"], "canonical")
rels = {p.relative_to(REPO_ROOT).as_posix(): v for p, v in expanded.items()}
canonical = [r for r in rels if r.startswith("vault/")]
self.assertEqual(len(canonical), 1, f"정본이 정확히 1개여야 한다: {sorted(rels)}")
self.assertEqual(rels[canonical[0]], EXPAND_DOC)
legacy_rel = "raw/branch-notes/feature-expand-probe.md"
self.assertIsInstance(rels.get(legacy_rel), vault_migrate.SymlinkValue,
"호환 심링크가 트랜잭션에서 빠지면 CANONICAL_OWNER_MISSING")
self.assertIn("harness/source/vault-layout.json", rels,
"manifest 갱신이 트랜잭션에서 빠지면 MIGRATION_ENTRY_MISSING")
def test_non_legacy_write_is_still_rejected(self) -> None:
stray = REPO_ROOT / "docs/feature-expand-probe-stray.md"
with self.assertRaises(vault_migrate.MigrationError) as ctx:
vault_migrate.expand_authoritative_changes(REPO_ROOT, {stray: EXPAND_DOC})
self.assertEqual(ctx.exception.code, "WRITE_ROOT_VIOLATION",
"planner 면제가 legacy-content 밖 경로까지 새면 안 된다")
if __name__ == "__main__":
unittest.main()