197 lines
9.2 KiB
Python
197 lines
9.2 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import io
|
|
import json
|
|
from pathlib import Path
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from contextlib import redirect_stdout
|
|
|
|
|
|
RUNTIME = Path(__file__).resolve().parents[1] / "runtime"
|
|
if str(RUNTIME) not in sys.path:
|
|
sys.path.insert(0, str(RUNTIME))
|
|
|
|
import semantic_audit # noqa: E402
|
|
import semantic_candidate_builder as builder # noqa: E402
|
|
import semantic_certificate as certificate # noqa: E402
|
|
import semantic_surface_extractor as extractor # noqa: E402
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
|
|
|
|
class SemanticCertificateTest(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.tempdir = tempfile.TemporaryDirectory()
|
|
self.root = Path(self.tempdir.name)
|
|
for relative in (
|
|
"harness/source/typed-contracts.json",
|
|
str(builder.DEFAULT_ONTOLOGY),
|
|
str(extractor.DEFAULT_POLICY),
|
|
str(certificate.DEFAULT_AGENT_METADATA),
|
|
str(certificate.DEFAULT_AGENT_BODY),
|
|
"harness/source/execution-profiles.json",
|
|
):
|
|
target = self.root / relative
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copyfile(REPO_ROOT / relative, target)
|
|
(self.root / "raw/branch-notes").mkdir(parents=True)
|
|
(self.root / "raw/project-notes").mkdir(parents=True)
|
|
self.path = self.root / "raw/branch-notes/feature-certificate.md"
|
|
self.path.write_text(
|
|
"---\n"
|
|
"title: certificate fixture\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",
|
|
)
|
|
policy = extractor.load_policy(self.root)
|
|
extraction = extractor.extract_document(self.root, self.path, policy)
|
|
lines = self.path.read_text(encoding="utf-8").splitlines()
|
|
assertions = []
|
|
for index, surface in enumerate(extraction["surfaces"]):
|
|
line = surface["line_start"] + 1
|
|
quote = lines[line - 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": quote,
|
|
"line_start": line,
|
|
"line_end": line,
|
|
})
|
|
assertion_result = {
|
|
"schema_version": builder.ASSERTION_SCHEMA,
|
|
"subject": extraction["path"],
|
|
"mode": "local",
|
|
"surface_manifest_sha256": hashlib.sha256(extractor.canonical_json_bytes(extraction)).hexdigest(),
|
|
"assertions": assertions,
|
|
}
|
|
candidate_result = builder.build(self.root, extraction, assertion_result)
|
|
self.assertEqual(candidate_result["candidates"], [])
|
|
self.audit_request = semantic_audit.build_verdict_request(candidate_result)
|
|
self.audit_result = {
|
|
"schema_version": semantic_audit.AUDIT_RESULT_SCHEMA,
|
|
"request_sha256": hashlib.sha256(semantic_audit.canonical_json_bytes(self.audit_request)).hexdigest(),
|
|
"subject": self.audit_request["subject"],
|
|
"mode": self.audit_request["mode"],
|
|
"auditor": {"contract_version": "semantic-coherence/v1", "model_id": "test-model", "run_id": "run-1"},
|
|
"verdicts": [],
|
|
}
|
|
self.validated = semantic_audit.validate_result(self.root, self.audit_request, self.audit_result)
|
|
|
|
def tearDown(self) -> None:
|
|
self.tempdir.cleanup()
|
|
|
|
def _write_certificate(self) -> Path:
|
|
path, content, _document = certificate.prepare_certificate(
|
|
self.root,
|
|
self.validated,
|
|
audit_request=self.audit_request,
|
|
audit_result=self.audit_result,
|
|
)
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_bytes(content)
|
|
return path
|
|
|
|
def test_path_is_hash_of_canonical_subject_and_current_certificate_passes(self) -> None:
|
|
path = self._write_certificate()
|
|
expected_id = hashlib.sha256("raw/branch-notes/feature-certificate.md".encode("utf-8")).hexdigest()
|
|
self.assertEqual(path.parent.name, expected_id)
|
|
validated = certificate.validate_certificate(self.root, path)
|
|
self.assertEqual(validated["verdict"], "PASS")
|
|
self.assertNotIn("issued_at", validated)
|
|
result = certificate.check(self.root, mode="local")
|
|
self.assertEqual(result["status"], "PASS")
|
|
self.assertEqual(result["coverage"], {"required": 1, "current": 1, "missing_or_stale": 0})
|
|
self.assertEqual(certificate.check(self.root, mode="hub")["status"], "PASS")
|
|
output = io.StringIO()
|
|
with redirect_stdout(output):
|
|
code = certificate.main([
|
|
"--root", str(self.root), "--check", "--mode", "local",
|
|
"--path", "raw/branch-notes/feature-certificate.md",
|
|
])
|
|
self.assertEqual(code, 0, output.getvalue())
|
|
self.assertEqual(json.loads(output.getvalue())["required_documents"], 1)
|
|
|
|
def test_document_policy_graph_and_auditor_bytes_make_certificate_stale(self) -> None:
|
|
path = self._write_certificate()
|
|
self.path.write_text(self.path.read_text(encoding="utf-8") + "changed\n", encoding="utf-8")
|
|
with self.assertRaises(certificate.SemanticCertificateError) as raised:
|
|
certificate.validate_certificate(self.root, path)
|
|
self.assertEqual(raised.exception.code, "SEMANTIC_CERTIFICATE_STALE")
|
|
|
|
self.path.write_text(self.path.read_text(encoding="utf-8").removesuffix("changed\n"), encoding="utf-8")
|
|
(self.root / certificate.DEFAULT_AGENT_BODY).write_text("changed contract\n", encoding="utf-8")
|
|
with self.assertRaises(certificate.SemanticCertificateError) as raised:
|
|
certificate.validate_certificate(self.root, path)
|
|
self.assertEqual(raised.exception.code, "SEMANTIC_CERTIFICATE_STALE")
|
|
|
|
def test_missing_certificate_fails_required_document(self) -> None:
|
|
result = certificate.check(self.root)
|
|
self.assertEqual(result["status"], "FAIL")
|
|
self.assertEqual(result["findings"][0]["code"], "SEMANTIC_CERTIFICATE_MISSING")
|
|
|
|
def test_embedded_audit_artifact_tampering_invalidates_certificate(self) -> None:
|
|
path = self._write_certificate()
|
|
document = json.loads(path.read_text(encoding="utf-8"))
|
|
document["audit_result"]["auditor"]["run_id"] = "tampered-run"
|
|
path.write_text(json.dumps(document), encoding="utf-8")
|
|
|
|
with self.assertRaises(certificate.SemanticCertificateError) as raised:
|
|
certificate.validate_certificate(self.root, path)
|
|
self.assertEqual(raised.exception.code, "SEMANTIC_CERTIFICATE_STALE")
|
|
|
|
def test_canonical_layout_requires_certificate_for_vault_subject_not_legacy_stub(self) -> None:
|
|
layout = self.root / "harness/source/vault-layout.json"
|
|
layout.write_text(json.dumps({"schema_version": "vault-layout/v1", "mode": "canonical", "vault_root": "vault"}), encoding="utf-8")
|
|
canonical = self.root / "vault/10-projects/sample/branch-notes/feature-certificate.md"
|
|
canonical.parent.mkdir(parents=True)
|
|
canonical.write_bytes(self.path.read_bytes())
|
|
result = certificate.check(self.root, mode="local")
|
|
self.assertEqual(result["status"], "FAIL")
|
|
self.assertEqual(result["required_documents"], 1)
|
|
self.assertEqual(result["findings"][0]["path"], "vault/10-projects/sample/branch-notes/feature-certificate.md")
|
|
|
|
def test_canonical_mapping_keeps_stable_logical_subject(self) -> None:
|
|
canonical = self.root / "vault/10-projects/sample/branch-notes/feature-certificate.md"
|
|
canonical.parent.mkdir(parents=True)
|
|
canonical.write_bytes(self.path.read_bytes())
|
|
layout = self.root / "harness/source/vault-layout.json"
|
|
layout.write_text(json.dumps({
|
|
"schema_version": "vault-layout/v1",
|
|
"mode": "canonical",
|
|
"migration_manifest": {
|
|
"schema_version": "vault-migration/v1",
|
|
"entries": [{
|
|
"legacy_path": "raw/branch-notes/feature-certificate.md",
|
|
"canonical_path": "vault/10-projects/sample/branch-notes/feature-certificate.md",
|
|
"sha256": hashlib.sha256(canonical.read_bytes()).hexdigest(),
|
|
}],
|
|
},
|
|
}), encoding="utf-8")
|
|
self.assertEqual(
|
|
certificate.logical_subject(self.root, canonical),
|
|
"raw/branch-notes/feature-certificate.md",
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|