init: llm-wiki-haness 하네스 설계
This commit is contained in:
@@ -0,0 +1,479 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest import mock
|
||||
from contextlib import redirect_stdout
|
||||
|
||||
|
||||
RUNTIME = Path(__file__).resolve().parents[1] / "runtime"
|
||||
sys.path.insert(0, str(RUNTIME))
|
||||
import document_commit # noqa: E402
|
||||
import fs_transaction # noqa: E402
|
||||
import proof_manifest # noqa: E402
|
||||
import semantic_audit # noqa: E402
|
||||
import semantic_candidate_builder # noqa: E402
|
||||
import semantic_certificate # noqa: E402
|
||||
import semantic_surface_extractor # noqa: E402
|
||||
|
||||
|
||||
class DocumentCommitTest(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.tempdir = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.tempdir.cleanup)
|
||||
self.root = Path(self.tempdir.name)
|
||||
(self.root / "raw/branch-notes").mkdir(parents=True)
|
||||
(self.root / "raw/errors").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/branch-notes", "raw/errors"]},
|
||||
}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(self.root / "harness/source/typed-contracts.json").write_bytes(
|
||||
(RUNTIME.parents[1] / "harness/source/typed-contracts.json").read_bytes()
|
||||
)
|
||||
self.parent = self.root / "raw/branch-notes/feature-sample-parent-contract.md"
|
||||
self.parent.write_text("---\ntitle: parent\n---\n# Parent\n## Cluster / 묶음\nmanual bytes\n", encoding="utf-8")
|
||||
self.candidate = self.root / "candidate.md"
|
||||
self.candidate.write_text(
|
||||
"---\ntitle: child\nsource_type: error-note\nstatus: raw\nrelated_branches: [feature-sample-parent-contract]\ntags: [error]\n---\n# Child\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
self.source = self.root / "source.md"
|
||||
self.source.write_text("evidence\n", encoding="utf-8")
|
||||
profiles = self.root / "profiles.json"
|
||||
profiles.write_text(json.dumps({"schema_version": "execution-profiles/v1", "profiles": {"capture": {}}}), encoding="utf-8")
|
||||
self.profiles = profiles
|
||||
proof = {
|
||||
"schema_version": proof_manifest.SCHEMA_VERSION,
|
||||
"run": {"id": "run-1", "profile": "capture"},
|
||||
"proofs": [{
|
||||
"finding": {"id": "F1", "role": "quote"},
|
||||
"source": {
|
||||
"path": "source.md",
|
||||
"sha256": hashlib.sha256(self.source.read_bytes()).hexdigest(),
|
||||
"line_start": 1,
|
||||
"line_end": 1,
|
||||
"quote_utf8": "evidence",
|
||||
},
|
||||
"execution": {
|
||||
"argv": ["proof-runner/exact-utf8-v1", "source.md", "1:1"],
|
||||
"exit_code": 0,
|
||||
"stdout_utf8": "evidence",
|
||||
"stdout_sha256": hashlib.sha256(b"evidence").hexdigest(),
|
||||
"exact_match": True,
|
||||
},
|
||||
}],
|
||||
}
|
||||
verified = proof_manifest.verify_manifest(proof, self.root, {"capture"})
|
||||
self.proof = self.root / "proof-manifest.json"
|
||||
self.proof.write_bytes(proof_manifest.manifest_bytes(verified))
|
||||
relations = {
|
||||
"schema_version": "document-relations/v1",
|
||||
"relations": [{
|
||||
"id": "error-to-branch",
|
||||
"child_roots": ["raw/errors"],
|
||||
"parent_field": "related_branches",
|
||||
"parent_roots": ["raw/branch-notes"],
|
||||
"marker": "errors",
|
||||
}],
|
||||
}
|
||||
self.relations = self.root / "relations.json"
|
||||
self.relations.write_text(json.dumps(relations), encoding="utf-8")
|
||||
self.request = {
|
||||
"schema_version": "document-commit/v1",
|
||||
"candidate": {"path": "candidate.md", "sha256": hashlib.sha256(self.candidate.read_bytes()).hexdigest()},
|
||||
"target": {"path": "raw/errors/child.md", "must_not_exist": True},
|
||||
"proof_manifest": {"path": "proof-manifest.json", "sha256": hashlib.sha256(self.proof.read_bytes()).hexdigest()},
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _pass_gate(_root, touched_paths, **_kwargs):
|
||||
return {"schema_version": "quality-gate-result/v1", "status": "PASS", "findings": [], "checked_paths": list(touched_paths)}
|
||||
|
||||
def _prepare(self):
|
||||
return document_commit.prepare(
|
||||
self.root,
|
||||
self.request,
|
||||
profiles_path=self.profiles,
|
||||
relations_path=self.relations,
|
||||
quality_runner=self._pass_gate,
|
||||
)
|
||||
|
||||
def test_prepare_is_read_only_and_commit_updates_child_and_parent_together(self) -> None:
|
||||
original_parent = self.parent.read_bytes()
|
||||
changes, result, forbidden = self._prepare()
|
||||
target = self.root / "raw/errors/child.md"
|
||||
self.assertFalse(target.exists())
|
||||
self.assertEqual(self.parent.read_bytes(), original_parent)
|
||||
self.assertEqual(result["proof_count"], 1)
|
||||
document_commit.commit(changes, forbidden)
|
||||
self.assertEqual(target.read_bytes(), self.candidate.read_bytes())
|
||||
parent_text = self.parent.read_text(encoding="utf-8")
|
||||
self.assertIn("<!-- GENERATED: errors:start -->", parent_text)
|
||||
self.assertIn("[[raw/errors/child]]", parent_text)
|
||||
self.assertIn("manual bytes", parent_text)
|
||||
|
||||
def test_quality_gate_receives_typed_and_projection_extensions(self) -> None:
|
||||
observed: list[str] = []
|
||||
|
||||
def gate(_root, touched_paths, **kwargs):
|
||||
observed.extend(item.name for item in kwargs["extensions"])
|
||||
return {
|
||||
"schema_version": "quality-gate-result/v1",
|
||||
"status": "PASS",
|
||||
"findings": [],
|
||||
"checked_paths": list(touched_paths),
|
||||
}
|
||||
|
||||
document_commit.prepare(
|
||||
self.root,
|
||||
self.request,
|
||||
profiles_path=self.profiles,
|
||||
relations_path=self.relations,
|
||||
quality_runner=gate,
|
||||
)
|
||||
self.assertEqual(observed, ["typed-contract", "contract-projection"])
|
||||
|
||||
def test_hash_or_quality_failure_writes_nothing(self) -> None:
|
||||
original_parent = self.parent.read_bytes()
|
||||
bad = json.loads(json.dumps(self.request))
|
||||
bad["candidate"]["sha256"] = "0" * 64
|
||||
with self.assertRaises(document_commit.DocumentCommitError) as raised:
|
||||
document_commit.prepare(self.root, bad, profiles_path=self.profiles, relations_path=self.relations, quality_runner=self._pass_gate)
|
||||
self.assertEqual(raised.exception.code, "CANDIDATE_HASH_MISMATCH")
|
||||
|
||||
def failing_gate(_root, touched_paths, **_kwargs):
|
||||
return {"schema_version": "quality-gate-result/v1", "status": "FAIL", "findings": [{"code": "INJECTED"}], "checked_paths": list(touched_paths)}
|
||||
|
||||
with self.assertRaises(document_commit.DocumentCommitError) as raised:
|
||||
document_commit.prepare(self.root, self.request, profiles_path=self.profiles, relations_path=self.relations, quality_runner=failing_gate)
|
||||
self.assertEqual(raised.exception.code, "QUALITY_GATE_FAILED")
|
||||
self.assertFalse((self.root / "raw/errors/child.md").exists())
|
||||
self.assertEqual(self.parent.read_bytes(), original_parent)
|
||||
|
||||
def test_failed_proof_manifest_blocks_completion(self) -> None:
|
||||
manifest = json.loads(self.proof.read_text(encoding="utf-8"))
|
||||
manifest["verification"]["status"] = "FAIL"
|
||||
manifest["verification"]["fail_count"] = 1
|
||||
self.proof.write_bytes(proof_manifest.manifest_bytes(manifest))
|
||||
self.request["proof_manifest"]["sha256"] = hashlib.sha256(self.proof.read_bytes()).hexdigest()
|
||||
with self.assertRaises(document_commit.DocumentCommitError) as raised:
|
||||
self._prepare()
|
||||
self.assertEqual(raised.exception.code, "PROOF_NOT_PASS")
|
||||
self.assertFalse((self.root / "raw/errors/child.md").exists())
|
||||
|
||||
def test_replace_failure_rolls_back_child_and_parent(self) -> None:
|
||||
changes, _result, forbidden = self._prepare()
|
||||
original_parent = self.parent.read_bytes()
|
||||
real_replace = fs_transaction.os.replace
|
||||
calls = 0
|
||||
|
||||
def fail_second(source, target):
|
||||
nonlocal calls
|
||||
calls += 1
|
||||
if calls == 2:
|
||||
raise OSError("injected replacement failure")
|
||||
return real_replace(source, target)
|
||||
|
||||
with mock.patch("fs_transaction.os.replace", side_effect=fail_second):
|
||||
with self.assertRaises(fs_transaction.TransactionError):
|
||||
document_commit.commit(changes, forbidden)
|
||||
self.assertFalse((self.root / "raw/errors/child.md").exists())
|
||||
self.assertEqual(self.parent.read_bytes(), original_parent)
|
||||
|
||||
def test_concurrent_parent_edit_blocks_commit_without_overwrite(self) -> None:
|
||||
changes, _result, forbidden = self._prepare()
|
||||
self.parent.write_text(self.parent.read_text(encoding="utf-8") + "concurrent\n", encoding="utf-8")
|
||||
concurrent = self.parent.read_bytes()
|
||||
with self.assertRaises(document_commit.DocumentCommitError) as raised:
|
||||
document_commit.commit(changes, forbidden)
|
||||
self.assertEqual(raised.exception.code, "CONCURRENT_MODIFICATION")
|
||||
self.assertEqual(self.parent.read_bytes(), concurrent)
|
||||
self.assertFalse((self.root / "raw/errors/child.md").exists())
|
||||
|
||||
def test_plan_hash_binds_apply_and_includes_active_layout(self) -> None:
|
||||
prepared = self._prepare()
|
||||
_changes, result, _forbidden = prepared
|
||||
self.assertRegex(result["plan_sha256"], r"^[0-9a-f]{64}$")
|
||||
self.assertEqual(result["active_layout"]["mode"], "compatibility")
|
||||
request_path = self.root / "request.json"
|
||||
request_path.write_text(json.dumps(self.request), encoding="utf-8")
|
||||
command = [
|
||||
str(request_path),
|
||||
"--root",
|
||||
str(self.root),
|
||||
"--profiles",
|
||||
str(self.profiles),
|
||||
"--relations",
|
||||
str(self.relations),
|
||||
]
|
||||
output = io.StringIO()
|
||||
with mock.patch("document_commit.prepare", return_value=prepared), redirect_stdout(output):
|
||||
exit_code = document_commit.main([*command, "--apply", "--expected-plan-sha256", "0" * 64])
|
||||
self.assertEqual(exit_code, 1, output.getvalue())
|
||||
self.assertEqual(json.loads(output.getvalue())["error"]["code"], "PLAN_HASH_MISMATCH")
|
||||
self.assertFalse((self.root / "raw/errors/child.md").exists())
|
||||
|
||||
def test_canonical_authority_refuses_legacy_target(self) -> None:
|
||||
"""canonical 모드의 신규 legacy 문서는 pre-check 가 아니라 planner 로 위임된다.
|
||||
|
||||
2026-07-23 cutover 수정: 신규 legacy 목적지는 expand 의 planner 가
|
||||
정본·심링크·manifest 를 계산하므로 pre-check 는 통과시킨다. 이 fixture 는
|
||||
planner 가 요구하는 project relation 설정이 없어 여전히 fail-closed 로
|
||||
거부된다(쓰기 0건) — 조용한 통과가 아님을 계속 보장한다.
|
||||
"""
|
||||
authority = {
|
||||
"mode": "canonical",
|
||||
"authority": "vault",
|
||||
"write_roots": ["vault"],
|
||||
"manifest_sha256": "0" * 64,
|
||||
}
|
||||
with mock.patch("document_commit.layout_check.resolve_authority", return_value=authority):
|
||||
with self.assertRaises(document_commit.DocumentCommitError) as raised:
|
||||
self._prepare()
|
||||
self.assertEqual(raised.exception.code, "PROJECT_RELATION_MISSING")
|
||||
self.assertFalse((self.root / "raw/errors/child.md").exists())
|
||||
|
||||
def test_semantic_commit_accepts_compatibility_symlink_target(self) -> None:
|
||||
"""canonical cutover 의 호환 심링크 target 에서 semantic commit 이 성립해야 한다.
|
||||
|
||||
target 을 resolve() 한 vault 철자와 인증서의 logical(raw/…) subject 를
|
||||
비교하면 영구 mismatch — branch_contract_check 가 이미 고친 pre-resolve
|
||||
판정 규율의 쌍둥이(2026-07-23 WI-019 hub 재인증 실측, SEMANTIC_AUDIT_SUBJECT_MISMATCH).
|
||||
"""
|
||||
repo_root = RUNTIME.parents[1]
|
||||
for relative in (
|
||||
semantic_surface_extractor.DEFAULT_POLICY,
|
||||
semantic_candidate_builder.DEFAULT_ONTOLOGY,
|
||||
semantic_certificate.DEFAULT_AGENT_METADATA,
|
||||
semantic_certificate.DEFAULT_AGENT_BODY,
|
||||
):
|
||||
target = self.root / relative
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_bytes((repo_root / relative).read_bytes())
|
||||
real = self.root / "raw/branch-notes/feature-symlink-child-real.md"
|
||||
real.parent.mkdir(parents=True, exist_ok=True)
|
||||
link = self.root / "raw/branch-notes/feature-symlink-child.md"
|
||||
self.candidate.write_text(
|
||||
"---\n"
|
||||
"title: symlink child\n"
|
||||
"source_type: branch-note\n"
|
||||
"status: verified\n"
|
||||
"---\n\n"
|
||||
"<!-- section-id: branch-contract-packet -->\n## 계약\ncontract assertion\n"
|
||||
"<!-- section-id: scope -->\n## 범위\nscope assertion\n"
|
||||
"<!-- section-id: decision-evidence -->\n## 근거\nevidence assertion\n"
|
||||
"<!-- section-id: implementation -->\n## 구현\nimplementation assertion\n"
|
||||
"<!-- section-id: edge-failure-dependency -->\n## 실패\nfailure assertion\n"
|
||||
"<!-- section-id: claims-to-verify -->\n## 주장\nclaims assertion\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
real.write_bytes(self.candidate.read_bytes())
|
||||
link.symlink_to(Path("feature-symlink-child-real.md"))
|
||||
policy = semantic_surface_extractor.load_policy(self.root)
|
||||
extraction = semantic_surface_extractor.extract_document(self.root, link, policy)
|
||||
self.assertEqual(extraction["path"], "raw/branch-notes/feature-symlink-child.md")
|
||||
lines = link.read_text(encoding="utf-8").splitlines()
|
||||
assertions = []
|
||||
for index, surface in enumerate(extraction["surfaces"]):
|
||||
line = surface["line_start"] + 1
|
||||
assertions.append({
|
||||
"assertion_id": f"A{index + 1}",
|
||||
"source_surface": surface["surface_id"],
|
||||
"subject": f"subject-{index}",
|
||||
"predicate": "other",
|
||||
"object": f"object-{index}",
|
||||
"condition": f"condition-{index}",
|
||||
"modality": "observed",
|
||||
"scope": "branch",
|
||||
"quote": lines[line - 1],
|
||||
"line_start": line,
|
||||
"line_end": line,
|
||||
})
|
||||
assertion_result = {
|
||||
"schema_version": semantic_candidate_builder.ASSERTION_SCHEMA,
|
||||
"subject": extraction["path"],
|
||||
"mode": extraction["mode"],
|
||||
"surface_manifest_sha256": hashlib.sha256(semantic_surface_extractor.canonical_json_bytes(extraction)).hexdigest(),
|
||||
"assertions": assertions,
|
||||
}
|
||||
candidates = semantic_candidate_builder.build(self.root, extraction, assertion_result)
|
||||
audit_request = semantic_audit.build_verdict_request(candidates)
|
||||
audit_result = {
|
||||
"schema_version": semantic_audit.AUDIT_RESULT_SCHEMA,
|
||||
"request_sha256": hashlib.sha256(semantic_audit.canonical_json_bytes(audit_request)).hexdigest(),
|
||||
"subject": extraction["path"],
|
||||
"mode": extraction["mode"],
|
||||
"auditor": {"contract_version": "semantic-coherence/v1", "model_id": "fixture", "run_id": "run-symlink"},
|
||||
"verdicts": [],
|
||||
}
|
||||
audit_request_path = self.root / "semantic-request.json"
|
||||
audit_result_path = self.root / "semantic-result.json"
|
||||
audit_request_path.write_bytes(semantic_audit.canonical_json_bytes(audit_request))
|
||||
audit_result_path.write_bytes(semantic_audit.canonical_json_bytes(audit_result))
|
||||
self.request["candidate"]["sha256"] = hashlib.sha256(self.candidate.read_bytes()).hexdigest()
|
||||
self.request["target"] = {"path": "raw/branch-notes/feature-symlink-child.md", "must_not_exist": False}
|
||||
self.request["semantic_audit"] = {
|
||||
"request": {"path": "semantic-request.json", "sha256": hashlib.sha256(audit_request_path.read_bytes()).hexdigest()},
|
||||
"result": {"path": "semantic-result.json", "sha256": hashlib.sha256(audit_result_path.read_bytes()).hexdigest()},
|
||||
}
|
||||
changes, result, forbidden = self._prepare()
|
||||
self.assertEqual(result["target"], "raw/branch-notes/feature-symlink-child.md")
|
||||
certificate_path = self.root / result["semantic_certificate"]
|
||||
document_commit.commit(changes, forbidden)
|
||||
certificate = json.loads(certificate_path.read_text(encoding="utf-8"))
|
||||
self.assertEqual(certificate["subject"], "raw/branch-notes/feature-symlink-child.md")
|
||||
self.assertTrue(link.is_symlink(), "호환 심링크가 실파일로 대체되면 안 된다")
|
||||
self.assertEqual(real.read_bytes(), self.candidate.read_bytes())
|
||||
|
||||
def test_semantic_certificate_and_document_share_one_rollback_boundary(self) -> None:
|
||||
repo_root = RUNTIME.parents[1]
|
||||
for relative in (
|
||||
semantic_surface_extractor.DEFAULT_POLICY,
|
||||
semantic_candidate_builder.DEFAULT_ONTOLOGY,
|
||||
semantic_certificate.DEFAULT_AGENT_METADATA,
|
||||
semantic_certificate.DEFAULT_AGENT_BODY,
|
||||
):
|
||||
target = self.root / relative
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_bytes((repo_root / relative).read_bytes())
|
||||
target = self.root / "raw/branch-notes/feature-semantic-child.md"
|
||||
self.candidate.write_text(
|
||||
"---\n"
|
||||
"title: semantic child\n"
|
||||
"source_type: branch-note\n"
|
||||
"status: verified\n"
|
||||
"---\n\n"
|
||||
"<!-- section-id: branch-contract-packet -->\n## 계약\ncontract assertion\n"
|
||||
"<!-- section-id: scope -->\n## 범위\nscope assertion\n"
|
||||
"<!-- section-id: decision-evidence -->\n## 근거\nevidence assertion\n"
|
||||
"<!-- section-id: implementation -->\n## 구현\nimplementation assertion\n"
|
||||
"<!-- section-id: edge-failure-dependency -->\n## 실패\nfailure assertion\n"
|
||||
"<!-- section-id: claims-to-verify -->\n## 주장\nclaims assertion\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
target.write_bytes(self.candidate.read_bytes())
|
||||
policy = semantic_surface_extractor.load_policy(self.root)
|
||||
extraction = semantic_surface_extractor.extract_document(self.root, target, policy)
|
||||
lines = target.read_text(encoding="utf-8").splitlines()
|
||||
assertions = []
|
||||
for index, surface in enumerate(extraction["surfaces"]):
|
||||
line = surface["line_start"] + 1
|
||||
assertions.append({
|
||||
"assertion_id": f"A{index + 1}",
|
||||
"source_surface": surface["surface_id"],
|
||||
"subject": f"subject-{index}",
|
||||
"predicate": "other",
|
||||
"object": f"object-{index}",
|
||||
"condition": f"condition-{index}",
|
||||
"modality": "observed",
|
||||
"scope": "branch",
|
||||
"quote": lines[line - 1],
|
||||
"line_start": line,
|
||||
"line_end": line,
|
||||
})
|
||||
assertion_result = {
|
||||
"schema_version": semantic_candidate_builder.ASSERTION_SCHEMA,
|
||||
"subject": extraction["path"],
|
||||
"mode": extraction["mode"],
|
||||
"surface_manifest_sha256": hashlib.sha256(semantic_surface_extractor.canonical_json_bytes(extraction)).hexdigest(),
|
||||
"assertions": assertions,
|
||||
}
|
||||
candidates = semantic_candidate_builder.build(self.root, extraction, assertion_result)
|
||||
audit_request = semantic_audit.build_verdict_request(candidates)
|
||||
audit_result = {
|
||||
"schema_version": semantic_audit.AUDIT_RESULT_SCHEMA,
|
||||
"request_sha256": hashlib.sha256(semantic_audit.canonical_json_bytes(audit_request)).hexdigest(),
|
||||
"subject": extraction["path"],
|
||||
"mode": extraction["mode"],
|
||||
"auditor": {"contract_version": "semantic-coherence/v1", "model_id": "fixture", "run_id": "run-1"},
|
||||
"verdicts": [],
|
||||
}
|
||||
audit_request_path = self.root / "semantic-request.json"
|
||||
audit_result_path = self.root / "semantic-result.json"
|
||||
audit_request_path.write_bytes(semantic_audit.canonical_json_bytes(audit_request))
|
||||
audit_result_path.write_bytes(semantic_audit.canonical_json_bytes(audit_result))
|
||||
target.unlink()
|
||||
self.request["candidate"]["sha256"] = hashlib.sha256(self.candidate.read_bytes()).hexdigest()
|
||||
self.request["target"] = {"path": "raw/branch-notes/feature-semantic-child.md", "must_not_exist": True}
|
||||
self.request["semantic_audit"] = {
|
||||
"request": {"path": "semantic-request.json", "sha256": hashlib.sha256(audit_request_path.read_bytes()).hexdigest()},
|
||||
"result": {"path": "semantic-result.json", "sha256": hashlib.sha256(audit_result_path.read_bytes()).hexdigest()},
|
||||
}
|
||||
|
||||
observed_extensions: list[str] = []
|
||||
|
||||
def gate(stage, touched_paths, **kwargs):
|
||||
findings = []
|
||||
for extension in kwargs["extensions"]:
|
||||
observed_extensions.append(extension.name)
|
||||
extension_result = extension.runner(stage)
|
||||
if extension_result["status"] != "PASS":
|
||||
findings.extend(extension_result.get("findings", []))
|
||||
return {
|
||||
"schema_version": "quality-gate-result/v1",
|
||||
"status": "FAIL" if findings else "PASS",
|
||||
"findings": findings,
|
||||
"checked_paths": list(touched_paths),
|
||||
}
|
||||
|
||||
changes, result, forbidden = document_commit.prepare(
|
||||
self.root,
|
||||
self.request,
|
||||
profiles_path=self.profiles,
|
||||
relations_path=self.relations,
|
||||
quality_runner=gate,
|
||||
)
|
||||
certificate_path = self.root / result["semantic_certificate"]
|
||||
self.assertIn("semantic-certificate", observed_extensions)
|
||||
self.assertFalse(target.exists())
|
||||
self.assertFalse(certificate_path.exists())
|
||||
|
||||
real_replace = fs_transaction.os.replace
|
||||
calls = 0
|
||||
|
||||
def fail_second(source, destination):
|
||||
nonlocal calls
|
||||
calls += 1
|
||||
if calls == 2:
|
||||
raise OSError("injected semantic transaction failure")
|
||||
return real_replace(source, destination)
|
||||
|
||||
with mock.patch("fs_transaction.os.replace", side_effect=fail_second):
|
||||
with self.assertRaises(fs_transaction.TransactionError):
|
||||
document_commit.commit(changes, forbidden)
|
||||
self.assertFalse(target.exists())
|
||||
self.assertFalse(certificate_path.exists())
|
||||
|
||||
document_commit.commit(changes, forbidden)
|
||||
self.assertTrue(target.is_file())
|
||||
self.assertEqual(semantic_certificate.validate_certificate(self.root, certificate_path)["verdict"], "PASS")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user