init: llm-wiki-haness 하네스 설계

This commit is contained in:
DongHyeonka
2026-07-24 14:21:35 +09:00
parent 42bf3db4fd
commit 6c53ded9cb
2436 changed files with 194486 additions and 1 deletions
+305
View File
@@ -0,0 +1,305 @@
from __future__ import annotations
import importlib.util
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest
from unittest import mock
import json
REPO_ROOT = Path(__file__).resolve().parents[2]
RUNTIME = REPO_ROOT / "harness/runtime"
sys.path.insert(0, str(RUNTIME))
import branch_from_project # noqa: E402
import branch_contract_check # noqa: E402
import fs_transaction # noqa: E402
PROJECT = """---
title: sample
source_type: project-note
id: sample-project
project_revision: 3
---
# Sample
## Project Decision Registry
| Decision ID | Revision | Domain | Decision Summary | Status | Owner | Evidence |
|---|---|---|---|---|---|---|
| `DEC-SAMPLE-PROJECT-API-001` | 2 | api | canonical summary | active | project | C1 |
## Work Item Registry
| Work Item ID | branch slug | 완료 조건 (측정가능) | Applies Decisions | Dependencies | Status |
|---|---|---|---|---|---|
| `WI-SAMPLE-PROJECT-001` | `feature-sample-api-contract-runtime` | test command exits zero | `DEC-SAMPLE-PROJECT-API-001@2` | - | `planned` |
## 묶음
### Legacy manual links
- [[raw/errors/keep-me]]
"""
class BranchFromProjectTest(unittest.TestCase):
def setUp(self) -> None:
self.tempdir = tempfile.TemporaryDirectory()
self.addCleanup(self.tempdir.cleanup)
self.root = Path(self.tempdir.name)
project = self.root / "raw/project-notes/sample-project.md"
project.parent.mkdir(parents=True)
(self.root / "raw/branch-notes").mkdir(parents=True)
for path in ("harness/source", "harness/adapters", "harness/runtime", "harness/tests", "vault/10-projects"):
(self.root / path).mkdir(parents=True, exist_ok=True)
(self.root / "harness/source/vault-layout.json").write_text(
json.dumps({
"schema_version": "vault-layout/v1",
"mode": "compatibility",
"vault_root": "vault",
"canonical_mapping": {
"schema_version": "project-first-paths/v1",
"project_relation": "branch-to-project",
"project_member_pattern": "{vault_root}/{area}/{project}/{category}/{relative_path}",
"default_pattern": "{vault_root}/{area}/{category}/{relative_path}",
},
"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/project-notes", "raw/branch-notes"]},
}),
encoding="utf-8",
)
(self.root / "harness/source/typed-contracts.json").write_bytes(
(REPO_ROOT / "harness/source/typed-contracts.json").read_bytes()
)
project.write_text(PROJECT, encoding="utf-8")
def test_typed_contract_failure_blocks_branch_plan(self) -> None:
(self.root / "raw/branch-notes/feature-bad-contract.md").write_text(
"---\ntitle: bad\nimports: [MISSING-GATE-999@1]\n---\n# Bad\n",
encoding="utf-8",
)
with self.assertRaises(branch_from_project.BranchError) as raised:
branch_from_project.prepare(self.root, "sample-project", "WI-SAMPLE-PROJECT-001")
self.assertEqual(raised.exception.code, "TYPED_CONTRACT_FAILED")
def test_parent_hub_certificate_is_mandatory_when_semantic_policy_is_active(self) -> None:
policy = self.root / "harness/source/document-semantic-surfaces.json"
policy.write_bytes((REPO_ROOT / "harness/source/document-semantic-surfaces.json").read_bytes())
with self.assertRaises(branch_from_project.BranchError) as raised:
branch_from_project.prepare(self.root, "sample-project", "WI-SAMPLE-PROJECT-001")
self.assertEqual(raised.exception.code, "PARENT_SEMANTIC_CERTIFICATE_INVALID")
self.assertIn("SEMANTIC_CERTIFICATE_MISSING", str(raised.exception))
self.assertFalse(
(self.root / "raw/branch-notes/feature-sample-api-contract-runtime.md").exists()
)
def test_apply_updates_status_moc_and_passes_graph_checker(self) -> None:
result = subprocess.run(
[sys.executable, str(RUNTIME / "branch_from_project.py"), "sample-project", "WI-SAMPLE-PROJECT-001", "--root", str(self.root), "--apply"],
check=False, capture_output=True, text=True,
)
self.assertEqual(result.returncode, 0, result.stdout)
branch = self.root / "raw/branch-notes/feature-sample-api-contract-runtime.md"
self.assertTrue(branch.is_file())
branch_text = branch.read_text(encoding="utf-8")
self.assertIn("id: BR-SAMPLE-PROJECT-001", branch_text)
self.assertIn("contract_packet_sha256:", branch_text)
self.assertIn("<!-- GENERATED: branch-contract:start -->", branch_text)
self.assertIn("- [[raw/project-notes/sample-project]]", branch_text)
project_text = (self.root / "raw/project-notes/sample-project.md").read_text(encoding="utf-8")
self.assertIn("| `in-progress` |", project_text)
self.assertIn("[[raw/branch-notes/feature-sample-api-contract-runtime]]", project_text)
self.assertIn("[[raw/errors/keep-me]]", project_text)
check = subprocess.run(
[sys.executable, str(REPO_ROOT / ".claude/hooks/wiki_graph_contract_check.py"), "--all", "--root", str(self.root)],
check=False, capture_output=True, text=True,
)
self.assertEqual(check.returncode, 0, check.stdout)
def test_stale_decision_and_existing_target_fail_without_writes(self) -> None:
project = self.root / "raw/project-notes/sample-project.md"
original = project.read_text(encoding="utf-8")
project.write_text(original.replace("@2` | -", "@1` | -"), encoding="utf-8")
with self.assertRaises(branch_from_project.BranchError) as raised:
branch_from_project.prepare(self.root, "sample-project", "WI-SAMPLE-PROJECT-001")
self.assertEqual(raised.exception.code, "STALE_INHERITANCE_REVISION")
self.assertEqual(project.read_text(encoding="utf-8"), original.replace("@2` | -", "@1` | -"))
def test_dry_run_does_not_write_and_existing_target_fails(self) -> None:
target = self.root / "raw/branch-notes/feature-sample-api-contract-runtime.md"
result = subprocess.run(
[sys.executable, str(RUNTIME / "branch_from_project.py"), "sample-project", "WI-SAMPLE-PROJECT-001", "--root", str(self.root), "--dry-run"],
check=False, capture_output=True, text=True,
)
self.assertEqual(result.returncode, 0, result.stdout)
self.assertFalse(target.exists())
target.write_text("sentinel\n", encoding="utf-8")
with self.assertRaises(branch_from_project.BranchError) as raised:
branch_from_project.prepare(self.root, "sample-project", "WI-SAMPLE-PROJECT-001")
self.assertEqual(raised.exception.code, "TARGET_EXISTS")
self.assertEqual(target.read_text(encoding="utf-8"), "sentinel\n")
def test_dry_run_plan_hash_binds_apply_and_contract_preflight_passes(self) -> None:
command = [
sys.executable,
str(RUNTIME / "branch_from_project.py"),
"sample-project",
"WI-SAMPLE-PROJECT-001",
"--root",
str(self.root),
]
dry_run = subprocess.run([*command, "--dry-run"], check=False, capture_output=True, text=True)
self.assertEqual(dry_run.returncode, 0, dry_run.stdout)
plan = json.loads(dry_run.stdout)["plan_sha256"]
rejected = subprocess.run(
[*command, "--apply", "--expected-plan-sha256", "0" * 64],
check=False,
capture_output=True,
text=True,
)
self.assertEqual(rejected.returncode, 1, rejected.stdout)
self.assertFalse((self.root / "raw/branch-notes/feature-sample-api-contract-runtime.md").exists())
applied = subprocess.run(
[*command, "--apply", "--expected-plan-sha256", plan],
check=False,
capture_output=True,
text=True,
)
self.assertEqual(applied.returncode, 0, applied.stdout)
checked = branch_contract_check.validate(
self.root,
"raw/branch-notes/feature-sample-api-contract-runtime.md",
postflight=True,
)
self.assertEqual(checked["status"], "PASS", checked)
branch = self.root / "raw/branch-notes/feature-sample-api-contract-runtime.md"
branch.write_text(
branch.read_text(encoding="utf-8").replace("canonical summary", "tampered summary", 1),
encoding="utf-8",
)
drifted = branch_contract_check.validate(self.root, branch)
self.assertEqual(drifted["status"], "FAIL")
self.assertIn("GENERATED_REGION_DRIFT", {item["code"] for item in drifted["findings"]})
def test_repository_local_template_changes_rendered_plan(self) -> None:
baseline_changes, baseline = branch_from_project.prepare(
self.root, "sample-project", "WI-SAMPLE-PROJECT-001"
)
template = self.root / "templates/branch-note-template.md"
template.parent.mkdir(parents=True)
source = (REPO_ROOT / "templates/branch-note-template.md").read_text(encoding="utf-8")
template.write_text(source.replace("아직 없음.\n\n## 결정 사항", "template revision.\n\n## 결정 사항", 1), encoding="utf-8")
changed, result = branch_from_project.prepare(
self.root, "sample-project", "WI-SAMPLE-PROJECT-001"
)
target = self.root / "raw/branch-notes/feature-sample-api-contract-runtime.md"
self.assertNotEqual(baseline["plan_sha256"], result["plan_sha256"])
self.assertNotEqual(baseline_changes[target], changed[target])
def test_second_replace_failure_rolls_back_project_and_new_branch(self) -> None:
project = self.root / "raw/project-notes/sample-project.md"
original = project.read_bytes()
changes, result = branch_from_project.prepare(self.root, "sample-project", "WI-SAMPLE-PROJECT-001")
real_replace = fs_transaction.os.replace
calls = 0
def fail_second(source, target):
nonlocal calls
calls += 1
if calls == 2:
raise OSError("injected failure")
return real_replace(source, target)
with mock.patch("fs_transaction.os.replace", side_effect=fail_second):
with self.assertRaises(fs_transaction.TransactionError):
fs_transaction.replace_many(changes, must_not_exist={self.root / result["target"]})
self.assertEqual(project.read_bytes(), original)
self.assertFalse((self.root / result["target"]).exists())
def test_canonical_authority_refuses_legacy_project_and_branch_writes(self) -> None:
authority = {
"mode": "canonical",
"authority": "vault",
"write_roots": ["vault"],
"manifest_sha256": "0" * 64,
}
with mock.patch("branch_from_project.layout_check.resolve_authority", return_value=authority):
with self.assertRaises(branch_from_project.BranchError) as raised:
branch_from_project.prepare(self.root, "sample-project", "WI-SAMPLE-PROJECT-001")
self.assertEqual(raised.exception.code, "WRITE_ROOT_VIOLATION")
class MocStagingCoverageTest(BranchFromProjectTest):
"""stage 밖 자료로 파생된 generated region 이 재생성에서 비워지지 않는지 지킨다.
prepare 는 staging 사본 위에서 moc_indexer 를 돌린다. staging 이
raw/project-notes·raw/branch-notes 만 담으면 다른 child_root(raw/official-docs
등)가 stage 에 없어 sources 류 region 이 *빈 목록* 으로 재생성되고, 그 파괴적
결과가 실 저장소에 반영된다 — 2026-07-23 WI-019 apply 실측(무관 문서 100+개의
GENERATED region 소실). staging 은 relations 의 모든 root 를 담아야 한다.
"""
def test_moc_rebuild_preserves_regions_sourced_outside_old_staging(self) -> None:
manifest_path = self.root / "harness/source/vault-layout.json"
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
manifest["areas"]["10-projects"].append("raw/official-docs")
manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
evidence = self.root / "raw/official-docs/sample-evidence.md"
evidence.parent.mkdir(parents=True)
evidence.write_text(
"---\ntitle: evidence\nrelated_branches: [feature-existing-note]\n---\n# E\n",
encoding="utf-8",
)
existing = self.root / "raw/branch-notes/feature-existing-note.md"
existing.write_text(
# v2 binding marker 없는 legacy 노트 — graph checker 는 skip 하고
# moc_indexer 의 sources region 재생성 대상으로만 남는다.
"---\ntitle: branch / feature-existing-note\nsource_type: branch-note\n"
"status: raw\nbranch: feature-existing-note\nparent_branch:\n---\n"
"# branch: feature-existing-note\n\n## 묶음\n\n"
"<!-- GENERATED: sources:start -->\n"
"- [[raw/official-docs/sample-evidence]]\n"
"<!-- GENERATED: sources:end -->\n",
encoding="utf-8",
)
changes, _result = branch_from_project.prepare(
self.root, "sample-project", "WI-SAMPLE-PROJECT-001"
)
planned = changes.get(existing)
if planned is not None:
self.assertIn(
"[[raw/official-docs/sample-evidence]]", planned.decode("utf-8"),
"stage 에 없는 raw/official-docs 근거가 region 재생성에서 소실됐다",
)
class PlanHashSymlinkValueTest(unittest.TestCase):
"""계획 해시가 planner 산출 SymlinkValue 를 결정론적으로 소화하는지 지킨다.
canonical 모드 신규 branch 생성 시 expand 가 호환 심링크(SymlinkValue)를
changes 에 넣는다. ``_plan_sha256`` 이 bytes 만 가정하면 ``len(SymlinkValue)``
에서 TypeError — 2026-07-23 WI-019 실측(write-root 면제 이후 두 번째 다리).
"""
def test_plan_hash_digests_symlink_values(self) -> None:
root = Path("/repo")
changes = {
root / "vault/doc.md": b"content",
root / "raw/doc.md": fs_transaction.SymlinkValue("../vault/doc.md"),
}
first = branch_from_project._plan_sha256(root, changes)
self.assertRegex(first, r"^[0-9a-f]{64}$")
reordered = dict(reversed(list(changes.items())))
self.assertEqual(first, branch_from_project._plan_sha256(root, reordered),
"삽입 순서와 무관해야 한다")
retargeted = dict(changes)
retargeted[root / "raw/doc.md"] = fs_transaction.SymlinkValue("../vault/other.md")
self.assertNotEqual(first, branch_from_project._plan_sha256(root, retargeted),
"symlink target 변화가 해시에 반영돼야 한다")
if __name__ == "__main__":
unittest.main()