Some checks failed
Build and publish policy-nexus image / build-and-push (push) Failing after 1m41s
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a126e1-76f8-7643-8cf3-e93336539ade
412 lines
19 KiB
Python
412 lines
19 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
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 setUp(self) -> None:
|
|
self.source_root_env = mock.patch.dict(
|
|
os.environ, {"POLICY_NEXUS_SOURCE_ROOT": ""}, clear=False
|
|
)
|
|
self.source_root_env.start()
|
|
|
|
def tearDown(self) -> None:
|
|
self.source_root_env.stop()
|
|
|
|
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_archive_build_uses_fetched_source_lock_revision(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
source_root = root / "_sources"
|
|
source_root.mkdir()
|
|
repo = root / "policy-nexus"
|
|
source = repo / "canon/example.md"
|
|
source.parent.mkdir(parents=True)
|
|
source.write_text("example", encoding="utf-8")
|
|
revision = "b" * 40
|
|
(source_root / "source-lock.json").write_text(
|
|
json.dumps(
|
|
{
|
|
"schema_version": 1,
|
|
"repositories": {
|
|
"policy-nexus": {"revision": revision}
|
|
},
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
with mock.patch.dict(
|
|
os.environ,
|
|
{"POLICY_NEXUS_SOURCE_ROOT": str(source_root)},
|
|
clear=False,
|
|
):
|
|
self.assertEqual(revision, build_site._source_revision(repo, source))
|
|
|
|
def test_archive_build_rejects_invalid_fetched_source_lock_revision(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
repo = root / "the-custodian"
|
|
source = repo / "canon/example.md"
|
|
source.parent.mkdir(parents=True)
|
|
source.write_text("example", encoding="utf-8")
|
|
(root / "source-lock.json").write_text(
|
|
json.dumps(
|
|
{
|
|
"schema_version": 1,
|
|
"repositories": {
|
|
"the-custodian": {"revision": "sha256:" + "b" * 64}
|
|
},
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
with mock.patch.dict(
|
|
os.environ, {"POLICY_NEXUS_SOURCE_ROOT": str(root)}, clear=False
|
|
):
|
|
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")
|
|
|
|
def test_build_rejects_non_public_risk_record(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
manifest = _fixture(root)
|
|
value = json.loads(manifest.read_text(encoding="utf-8"))
|
|
value["repositories"] = {"risk-nexus": {"path": "canon-repo"}}
|
|
value["documents"][0]["source_repo"] = "risk-nexus"
|
|
manifest.write_text(json.dumps(value), encoding="utf-8")
|
|
source = root / "canon-repo/canon/standards/example.md"
|
|
source.write_text(
|
|
source.read_text(encoding="utf-8").replace(
|
|
"status: proposed\n", "status: proposed\ndisclosure: embargoed\n"
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
with self.assertRaisesRegex(ValueError, "require disclosure: public"):
|
|
build_site.build(manifest, root / "site")
|
|
|
|
|
|
class HistoricalRevisionTest(unittest.TestCase):
|
|
def prepare(self, root):
|
|
manifest = _fixture(root)
|
|
data = json.loads(manifest.read_text())
|
|
repo = root / "canon-repo"
|
|
source = repo / "canon/standards/example.md"
|
|
source.write_text(source.read_text().replace(
|
|
'owner: example-owner', 'owner: example-owner\ndecided_by: Original Founder\ndate: "2026-08-18"'))
|
|
with mock.patch.object(build_site, "_source_revision", return_value="a" * 40):
|
|
build_site.build(manifest, root / "served", as_of=dt.date(2026, 8, 18))
|
|
original_page = (root / "served/standards/example/v1/revisions/draft-1/index.html").read_bytes()
|
|
history = repo / "docs/evidence/original.md"
|
|
history.parent.mkdir(parents=True)
|
|
history.write_bytes(source.read_bytes())
|
|
page = history.with_suffix(".html")
|
|
page.write_bytes(original_page)
|
|
data["documents"][0]["historical_revisions"] = [{
|
|
"revision": "draft-1", "source_path": "docs/evidence/original.md",
|
|
"source_digest": hashlib.sha256(history.read_bytes()).hexdigest(),
|
|
"source_revision": "a" * 40,
|
|
"rendered_page_path": "docs/evidence/original.html",
|
|
"rendered_page_digest": hashlib.sha256(original_page).hexdigest(),
|
|
}]
|
|
manifest.write_text(json.dumps(data))
|
|
source.write_text(source.read_text().replace('revision: "draft-1"', 'revision: "draft-2"')
|
|
+ "\nDated factual annotation; no new authority.\n")
|
|
return manifest, source, page, original_page
|
|
|
|
def test_clean_build_preserves_exact_served_revision_and_new_annotation(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
manifest, source, _page, original = self.prepare(root)
|
|
output = root / "clean"
|
|
with mock.patch.object(build_site, "_source_revision", return_value="b" * 40):
|
|
build_site.build(manifest, output, as_of=dt.date(2026, 8, 18))
|
|
old = output / "standards/example/v1/revisions/draft-1/index.html"
|
|
self.assertEqual(original, old.read_bytes())
|
|
self.assertNotIn(b"Dated factual annotation", old.read_bytes())
|
|
self.assertIn("Dated factual annotation",
|
|
(output / "standards/example/v1/index.html").read_text())
|
|
self.assertTrue((output / "standards/example/v1/revisions/draft-2/index.html").is_file())
|
|
meta, _ = build_site.split_frontmatter(source.read_text())
|
|
self.assertEqual("Original Founder", meta["decided_by"])
|
|
self.assertEqual("2026-08-18", meta["date"])
|
|
|
|
def test_same_current_frozen_render_survives_new_head_and_superseded_notice(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
manifest, source, page, original = self.prepare(root)
|
|
source.write_bytes(page.with_suffix(".md").read_bytes())
|
|
data = json.loads(manifest.read_text())
|
|
data["documents"][0].update(lifecycle="superseded", successor="/standards/example/v2/")
|
|
manifest.write_text(json.dumps(data))
|
|
output = root / "clean"
|
|
with mock.patch.object(build_site, "_source_revision", return_value="b" * 40):
|
|
build_site.build(manifest, output, as_of=dt.date(2026, 8, 18))
|
|
frozen = output / "standards/example/v1/revisions/draft-1/index.html"
|
|
current = output / "standards/example/v1/index.html"
|
|
self.assertEqual(original, frozen.read_bytes())
|
|
self.assertIn('policy-source-revision" content="' + "a" * 40, frozen.read_text())
|
|
self.assertNotIn("Superseded.", frozen.read_text())
|
|
self.assertIn("Superseded.", current.read_text())
|
|
self.assertIn('policy-source-revision" content="' + "b" * 40, current.read_text())
|
|
self.assertEqual(source.read_bytes(), page.with_suffix(".md").read_bytes())
|
|
|
|
def test_same_current_frozen_render_rejects_current_source_change(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
manifest, source, page, _original = self.prepare(root)
|
|
source.write_bytes(page.with_suffix(".md").read_bytes() + b"Changed current bytes.")
|
|
with self.assertRaisesRegex(RuntimeError, "same-current historical source differs"):
|
|
build_site.build(manifest, root / "clean")
|
|
self.assertFalse((root / "clean").exists())
|
|
|
|
def test_frozen_current_revision_accepts_source_version_frontmatter(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
manifest, source, page, original = self.prepare(root)
|
|
history = page.with_suffix(".md")
|
|
history.write_bytes(history.read_bytes().replace(b'revision: "draft-1"', b'version: "draft-1"'))
|
|
source.write_bytes(history.read_bytes())
|
|
data = json.loads(manifest.read_text())
|
|
historical = data["documents"][0]["historical_revisions"][0]
|
|
previous_digest = historical["source_digest"]
|
|
historical["source_digest"] = hashlib.sha256(history.read_bytes()).hexdigest()
|
|
original = original.replace(previous_digest.encode(), historical["source_digest"].encode())
|
|
page.write_bytes(original)
|
|
historical["rendered_page_digest"] = hashlib.sha256(original).hexdigest()
|
|
manifest.write_text(json.dumps(data))
|
|
build_site.build(manifest, root / "clean")
|
|
self.assertEqual(original, (root / "clean/standards/example/v1/revisions/draft-1/index.html").read_bytes())
|
|
|
|
def test_changed_historical_page_fails_before_replacing_output(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
manifest, _source, page, _original = self.prepare(root)
|
|
page.write_bytes(page.read_bytes() + b"changed")
|
|
with self.assertRaisesRegex(RuntimeError, "rendered page digest mismatch"):
|
|
build_site.build(manifest, root / "clean")
|
|
self.assertFalse((root / "clean").exists())
|
|
|
|
def test_changed_historical_source_and_provenance_are_rejected(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
manifest, _source, page, _original = self.prepare(root)
|
|
history = page.with_suffix(".md")
|
|
history.write_bytes(history.read_bytes() + b"changed")
|
|
with self.assertRaisesRegex(RuntimeError, "source digest mismatch"):
|
|
build_site.build(manifest, root / "clean")
|
|
|
|
def test_existing_immutable_history_refuses_different_page_bytes(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
manifest, _source, _page, original = self.prepare(root)
|
|
old = root / "clean/standards/example/v1/revisions/draft-1/index.html"
|
|
old.parent.mkdir(parents=True)
|
|
old.write_bytes(original + b"changed")
|
|
with self.assertRaisesRegex(RuntimeError, "already has different bytes"):
|
|
build_site.build(manifest, root / "clean")
|
|
self.assertEqual(original + b"changed", old.read_bytes())
|
|
|
|
def test_historical_source_cannot_escape_owner_repository(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
manifest, _source, _page, _original = self.prepare(root)
|
|
data = json.loads(manifest.read_text())
|
|
data["documents"][0]["historical_revisions"][0]["source_path"] = "../publication.json"
|
|
manifest.write_text(json.dumps(data))
|
|
with self.assertRaisesRegex(ValueError, "inside source repository"):
|
|
build_site.build(manifest, root / "clean")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|