131 lines
5.2 KiB
Python
131 lines
5.2 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
RUNTIME = REPO_ROOT / "harness/runtime"
|
|
sys.path.insert(0, str(RUNTIME))
|
|
|
|
import branch_contract_check # noqa: E402
|
|
import branch_from_project # noqa: E402
|
|
|
|
|
|
PROJECT = """---
|
|
title: sample
|
|
source_type: project-note
|
|
id: sample-project
|
|
project_revision: 3
|
|
---
|
|
# Sample
|
|
<!-- section-id: project-decisions -->
|
|
## 결정
|
|
| Decision ID | Revision | Domain | Decision Summary | Status | Owner | Evidence |
|
|
|---|---|---|---|---|---|---|
|
|
| `DEC-SAMPLE-PROJECT-API-001` | 2 | api | canonical summary | active | project | C1 |
|
|
<!-- section-id: project-work-items -->
|
|
## 작업
|
|
| Work Item ID | branch slug | 완료 조건 (측정가능) | Applies Decisions | Dependencies | Status |
|
|
|---|---|---|---|---|---|
|
|
| `WI-SAMPLE-PROJECT-001` | `feature-sample-api-contract-runtime` | 검사가 통과한다 | `DEC-SAMPLE-PROJECT-API-001@2` | - | `planned` |
|
|
## 묶음
|
|
"""
|
|
|
|
|
|
class BranchContractCheckTest(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
temporary = tempfile.TemporaryDirectory()
|
|
self.addCleanup(temporary.cleanup)
|
|
self.root = Path(temporary.name)
|
|
for path in (
|
|
"raw/project-notes",
|
|
"raw/branch-notes",
|
|
"harness/source",
|
|
"harness/adapters",
|
|
"harness/runtime",
|
|
"harness/tests",
|
|
"vault/10-projects",
|
|
):
|
|
(self.root / path).mkdir(parents=True, exist_ok=True)
|
|
(self.root / "raw/project-notes/sample-project.md").write_text(PROJECT, encoding="utf-8")
|
|
(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",
|
|
)
|
|
changes, result = branch_from_project.prepare(
|
|
self.root, "sample-project", "WI-SAMPLE-PROJECT-001"
|
|
)
|
|
for path, content in changes.items():
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_bytes(content)
|
|
self.branch = self.root / result["target"]
|
|
|
|
def test_result_exposes_contract_and_generated_hashes(self) -> None:
|
|
result = branch_contract_check.validate(self.root, self.branch)
|
|
self.assertEqual(result["status"], "PASS", result)
|
|
self.assertEqual(result["project"], "sample-project")
|
|
self.assertEqual(result["work_item"], "WI-SAMPLE-PROJECT-001")
|
|
self.assertEqual(result["project_revision"], 3)
|
|
self.assertEqual(result["inherits"], ["DEC-SAMPLE-PROJECT-API-001@2"])
|
|
self.assertEqual(result["editable_sections"], ["branch-parent", "branch-goal", "branch-scope"])
|
|
self.assertEqual(
|
|
result["generated_hashes"]["declared_sha256"],
|
|
result["generated_hashes"]["observed_sha256"],
|
|
)
|
|
|
|
def test_failed_candidate_postflight_leaves_target_unchanged(self) -> None:
|
|
original = self.branch.read_bytes()
|
|
candidate = self.root / "candidate.md"
|
|
candidate.write_text(
|
|
original.decode("utf-8").replace("canonical summary", "tampered summary", 1),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = branch_contract_check.validate_candidate(
|
|
self.root, self.branch, candidate, postflight=True
|
|
)
|
|
|
|
self.assertEqual(result["status"], "FAIL")
|
|
self.assertIn("GENERATED_REGION_DRIFT", {item["code"] for item in result["findings"]})
|
|
self.assertEqual(self.branch.read_bytes(), original)
|
|
self.assertTrue(result["staged"])
|
|
|
|
def test_override_mismatch_exposes_legacy_and_alias_codes(self) -> None:
|
|
candidate = self.root / "candidate.md"
|
|
candidate.write_text(
|
|
self.branch.read_text(encoding="utf-8").replace(
|
|
"overrides: []", "overrides: [DEC-SAMPLE-PROJECT-API-001@2]", 1
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
result = branch_contract_check.validate_candidate(self.root, self.branch, candidate)
|
|
codes = {item["code"] for item in result["findings"]}
|
|
self.assertIn("OVERRIDE_APPROVAL_MISMATCH", codes)
|
|
self.assertIn("UNDECLARED_OVERRIDE", codes)
|
|
self.assertEqual(
|
|
result["failure_code_aliases"]["OVERRIDE_APPROVAL_MISMATCH"],
|
|
["UNDECLARED_OVERRIDE"],
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|