Build immutable policy publication artifact
This commit is contained in:
parent
cac0301866
commit
e8035f3887
22 changed files with 2375 additions and 452 deletions
197
tests/test_publication.py
Normal file
197
tests/test_publication.py
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import datetime as dt
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
|
||||
ROOT = Path(__file__).parents[1]
|
||||
sys.path.insert(0, str(ROOT / "tools"))
|
||||
SPEC = importlib.util.spec_from_file_location("build_site", ROOT / "tools/build_site.py")
|
||||
build_site = importlib.util.module_from_spec(SPEC)
|
||||
assert SPEC.loader is not None
|
||||
SPEC.loader.exec_module(build_site)
|
||||
|
||||
|
||||
def _fixture(tmp_path: Path) -> Path:
|
||||
repo = tmp_path / "canon-repo"
|
||||
source = repo / "canon/standards/example.md"
|
||||
source.parent.mkdir(parents=True)
|
||||
source.write_text(
|
||||
"""---
|
||||
id: example
|
||||
title: "Example Standard"
|
||||
status: proposed
|
||||
revision: "draft-1"
|
||||
owner: example-owner
|
||||
last_reviewed: "2026-08-18"
|
||||
review_interval: 6m
|
||||
---
|
||||
|
||||
# Example
|
||||
|
||||
## 1. Rule
|
||||
|
||||
| Level | Meaning |
|
||||
| --- | --- |
|
||||
| **V0** | No position. |
|
||||
| **V1** | Restart recovery. |
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
manifest = tmp_path / "publication.json"
|
||||
manifest.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": 1,
|
||||
"site": {"title": "Test", "base_url": "https://example.invalid"},
|
||||
"repositories": {"canon": {"path": "canon-repo"}},
|
||||
"documents": [
|
||||
{
|
||||
"id": "example",
|
||||
"source_repo": "canon",
|
||||
"source_path": "canon/standards/example.md",
|
||||
"canonical_path": "standards/example/v1/index.html",
|
||||
"revision_path": "standards/example/v1/revisions/{revision}/index.html",
|
||||
"legacy_paths": ["example.html"],
|
||||
"review_interval": "6m",
|
||||
}
|
||||
],
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
return manifest
|
||||
|
||||
|
||||
class PublicationTest(unittest.TestCase):
|
||||
def test_archive_build_accepts_only_exact_source_revision_override(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
repo = root / "net-kingdom"
|
||||
source = repo / "canon/example.md"
|
||||
source.parent.mkdir(parents=True)
|
||||
source.write_text("example", encoding="utf-8")
|
||||
key = "POLICY_NEXUS_SOURCE_REVISION_NET_KINGDOM"
|
||||
with mock.patch.dict(os.environ, {key: "a" * 40}):
|
||||
self.assertEqual("a" * 40, build_site._source_revision(repo, source))
|
||||
with mock.patch.dict(os.environ, {key: "main"}):
|
||||
with self.assertRaisesRegex(ValueError, "clean 40-hex Git commit"):
|
||||
build_site._source_revision(repo, source)
|
||||
|
||||
def test_manifest_builds_index_current_revision_and_legacy_alias(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
manifest = _fixture(root)
|
||||
output = root / "site"
|
||||
records = build_site.build(manifest, output, as_of=dt.date(2026, 8, 18))
|
||||
|
||||
self.assertEqual("2027-02-18", records[0]["review_due"])
|
||||
self.assertTrue((output / "index.html").is_file())
|
||||
current = (output / "standards/example/v1/index.html").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
revision = output / "standards/example/v1/revisions/draft-1/index.html"
|
||||
self.assertIn("Availability", current)
|
||||
self.assertIn("policy-source-revision", current)
|
||||
self.assertIn("policy-source-digest", current)
|
||||
self.assertIn("Review due: 2027-02-18", current)
|
||||
self.assertEqual(revision.read_text(encoding="utf-8"), current)
|
||||
self.assertIn(
|
||||
"/standards/example/v1/index.html",
|
||||
(output / "example.html").read_text(encoding="utf-8"),
|
||||
)
|
||||
|
||||
def test_revision_address_refuses_changed_source_under_same_revision(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
manifest = _fixture(root)
|
||||
output = root / "site"
|
||||
build_site.build(manifest, output, as_of=dt.date(2026, 8, 18))
|
||||
source = root / "canon-repo/canon/standards/example.md"
|
||||
source.write_text(
|
||||
source.read_text(encoding="utf-8") + "\nChanged.\n", encoding="utf-8"
|
||||
)
|
||||
|
||||
with self.assertRaisesRegex(RuntimeError, "immutable revision"):
|
||||
build_site.build(manifest, output, as_of=dt.date(2026, 8, 18))
|
||||
|
||||
def test_same_content_can_move_from_worktree_to_commit_provenance(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
manifest = _fixture(root)
|
||||
output = root / "site"
|
||||
build_site.build(manifest, output, as_of=dt.date(2026, 8, 18))
|
||||
revision = output / "standards/example/v1/revisions/draft-1/index.html"
|
||||
original = revision.read_text(encoding="utf-8")
|
||||
|
||||
original_source_revision = build_site._source_revision
|
||||
try:
|
||||
build_site._source_revision = lambda _repo, _source: "new-commit"
|
||||
build_site.build(manifest, output, as_of=dt.date(2026, 8, 18))
|
||||
finally:
|
||||
build_site._source_revision = original_source_revision
|
||||
|
||||
self.assertEqual(original, revision.read_text(encoding="utf-8"))
|
||||
current = (output / "standards/example/v1/index.html").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
self.assertIn('policy-source-revision" content="new-commit', current)
|
||||
|
||||
def test_stale_and_superseded_notices_do_not_mutate_revision_page(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
manifest = _fixture(root)
|
||||
document = json.loads(manifest.read_text(encoding="utf-8"))
|
||||
document["documents"][0]["lifecycle"] = "superseded"
|
||||
document["documents"][0]["successor"] = "/standards/example/v2/"
|
||||
manifest.write_text(json.dumps(document), encoding="utf-8")
|
||||
output = root / "site"
|
||||
|
||||
build_site.build(manifest, output, as_of=dt.date(2027, 2, 19))
|
||||
|
||||
current = (output / "standards/example/v1/index.html").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
revision = (
|
||||
output / "standards/example/v1/revisions/draft-1/index.html"
|
||||
).read_text(encoding="utf-8")
|
||||
self.assertIn("Superseded.", current)
|
||||
self.assertIn("Review overdue.", current)
|
||||
self.assertNotIn("Superseded.", revision)
|
||||
self.assertNotIn("Review overdue.", revision)
|
||||
|
||||
def test_manifest_rejects_unsafe_publication_path(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
manifest = _fixture(root)
|
||||
document = json.loads(manifest.read_text(encoding="utf-8"))
|
||||
document["documents"][0]["canonical_path"] = "../escape.html"
|
||||
manifest.write_text(json.dumps(document), encoding="utf-8")
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "unsafe publication path"):
|
||||
build_site.load_manifest(manifest)
|
||||
|
||||
def test_build_rejects_missing_publication_owner(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
manifest = _fixture(root)
|
||||
source = root / "canon-repo/canon/standards/example.md"
|
||||
source.write_text(
|
||||
source.read_text(encoding="utf-8").replace(
|
||||
"owner: example-owner\n", ""
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "owner is required"):
|
||||
build_site.build(manifest, root / "site")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
106
tests/test_release.py
Normal file
106
tests/test_release.py
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).parents[1]
|
||||
sys.path.insert(0, str(ROOT / "tools"))
|
||||
from verify_release import verify
|
||||
|
||||
|
||||
COMMIT = "1" * 40
|
||||
SOURCE_DIGEST = "2" * 64
|
||||
|
||||
|
||||
def _page(revision: str = COMMIT, digest: str = SOURCE_DIGEST) -> str:
|
||||
return (
|
||||
'<!doctype html><meta name="policy-source-revision" '
|
||||
f'content="{revision}"><meta name="policy-source-digest" content="{digest}">'
|
||||
)
|
||||
|
||||
|
||||
def _release(root: Path) -> Path:
|
||||
build = root / "build"
|
||||
canonical = build / "standards/example/v1/index.html"
|
||||
revision = build / "standards/example/v1/revisions/draft-1/index.html"
|
||||
canonical.parent.mkdir(parents=True)
|
||||
revision.parent.mkdir(parents=True)
|
||||
canonical.write_text(_page(), encoding="utf-8")
|
||||
revision.write_text(_page(), encoding="utf-8")
|
||||
(build / "index.html").write_text("<!doctype html>", encoding="utf-8")
|
||||
manifest = {
|
||||
"schema_version": 1,
|
||||
"generated_as_of": "2026-08-18",
|
||||
"documents": [
|
||||
{
|
||||
"id": "example",
|
||||
"title": "Example Standard",
|
||||
"status": "proposed",
|
||||
"revision": "draft-1",
|
||||
"owner": "example-owner",
|
||||
"last_reviewed": "2026-08-18",
|
||||
"review_due": "2027-02-18",
|
||||
"canonical_path": "standards/example/v1/index.html",
|
||||
"revision_path": "standards/example/v1/revisions/draft-1/index.html",
|
||||
"source_revision": COMMIT,
|
||||
"source_digest": SOURCE_DIGEST,
|
||||
}
|
||||
],
|
||||
}
|
||||
(build / "publication-manifest.json").write_text(
|
||||
json.dumps(manifest, sort_keys=True) + "\n", encoding="utf-8"
|
||||
)
|
||||
return build
|
||||
|
||||
|
||||
class ReleaseVerificationTest(unittest.TestCase):
|
||||
def test_accepts_clean_provenance_and_returns_manifest_digest(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
build = _release(Path(directory))
|
||||
evidence = verify(build)
|
||||
expected = hashlib.sha256(
|
||||
(build / "publication-manifest.json").read_bytes()
|
||||
).hexdigest()
|
||||
self.assertEqual(expected, evidence["publication_manifest_digest"])
|
||||
self.assertEqual(["example"], evidence["documents"])
|
||||
|
||||
def test_rejects_working_tree_source_revision(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
build = _release(Path(directory))
|
||||
manifest_path = build / "publication-manifest.json"
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
dirty = COMMIT + "+working-tree." + SOURCE_DIGEST[:12]
|
||||
manifest["documents"][0]["source_revision"] = dirty
|
||||
manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
|
||||
canonical = build / manifest["documents"][0]["canonical_path"]
|
||||
canonical.write_text(_page(revision=dirty), encoding="utf-8")
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "clean 40-hex Git commit"):
|
||||
verify(build)
|
||||
|
||||
def test_rejects_immutable_revision_built_from_dirty_source(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
build = _release(Path(directory))
|
||||
revision = build / "standards/example/v1/revisions/draft-1/index.html"
|
||||
revision.write_text(
|
||||
_page(revision=COMMIT + "+working-tree." + SOURCE_DIGEST[:12]),
|
||||
encoding="utf-8",
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "immutable revision page"):
|
||||
verify(build)
|
||||
|
||||
def test_rejects_page_digest_mismatch(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
build = _release(Path(directory))
|
||||
canonical = build / "standards/example/v1/index.html"
|
||||
canonical.write_text(_page(digest="3" * 64), encoding="utf-8")
|
||||
with self.assertRaisesRegex(ValueError, "provenance differs"):
|
||||
verify(build)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue