All checks were successful
Build and publish policy-nexus image / build-and-push (push) Successful in 1m25s
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a126e1-76f8-7643-8cf3-e93336539ade
89 lines
5.5 KiB
Python
89 lines
5.5 KiB
Python
from pathlib import Path
|
|
import hashlib
|
|
import json
|
|
import tempfile
|
|
import unittest
|
|
from unittest import mock
|
|
from test_publication import _fixture, build_site
|
|
|
|
class PublishedHistoryTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tmp = tempfile.TemporaryDirectory()
|
|
self.addCleanup(self.tmp.cleanup)
|
|
self.root = Path(self.tmp.name)
|
|
self.manifest = _fixture(self.root)
|
|
self.source = self.root/'canon-repo/canon/standards/example.md'
|
|
self.output = self.root/'build'
|
|
self.env = mock.patch.dict('os.environ', {'POLICY_NEXUS_SOURCE_ROOT':''})
|
|
self.env.start();self.addCleanup(self.env.stop)
|
|
with mock.patch.object(build_site, '_source_revision', return_value='a'*40):
|
|
build_site.build(self.manifest,self.output)
|
|
self.delivered = json.loads((self.output/'publication-manifest.json').read_text())
|
|
self.doc = self.delivered['documents'][0]
|
|
self.original = (self.output/self.doc['revision_path']).read_bytes()
|
|
self.archive = self.root/'archive';self.archive.mkdir()
|
|
(self.archive/'source.md').write_bytes(self.source.read_bytes())
|
|
(self.archive/'page.html').write_bytes(self.original)
|
|
delivered_bytes = (self.output/'publication-manifest.json').read_bytes()
|
|
(self.archive/'manifest.json').write_bytes(delivered_bytes)
|
|
record = {k:self.doc[k] for k in ['id','revision','revision_path','source_repo','source_path','source_revision','source_digest']}
|
|
record.update(archived_source_path='source.md',rendered_page_path='page.html',rendered_page_digest=hashlib.sha256(self.original).hexdigest())
|
|
self.receipt = {'schema_version':'policy-nexus-published-history/v1','publication_manifest_path':'manifest.json','publication_manifest_digest':hashlib.sha256(delivered_bytes).hexdigest(),'documents':[record]}
|
|
self.configure()
|
|
def configure(self):
|
|
data = json.dumps(self.receipt).encode();(self.archive/'receipt.json').write_bytes(data)
|
|
manifest = json.loads(self.manifest.read_text());manifest['published_history']=[{'receipt_path':'archive/receipt.json','receipt_digest':hashlib.sha256(data).hexdigest()}]
|
|
self.manifest.write_text(json.dumps(manifest))
|
|
def clean_build(self):
|
|
with mock.patch.object(build_site, '_source_revision', return_value='b'*40):
|
|
return build_site.build(self.manifest,self.root/'clean')
|
|
def test_clean_new_head_same_source_retains_actual_old_bytes(self):
|
|
self.clean_build()
|
|
self.assertEqual((self.root/'clean'/self.doc['revision_path']).read_bytes(),self.original)
|
|
self.assertIn(b'b'*40,(self.root/'clean'/self.doc['canonical_path']).read_bytes())
|
|
def test_new_revision_retains_previous_path_in_clean_build(self):
|
|
self.source.write_text(self.source.read_text().replace('draft-1','draft-2').replace('No position.','Updated position.'))
|
|
records=self.clean_build()
|
|
self.assertEqual((self.root/'clean'/self.doc['revision_path']).read_bytes(),self.original)
|
|
self.assertTrue((self.root/'clean'/records[0]['revision_path']).is_file())
|
|
def test_conflicting_existing_history_refuses_without_replacing_output(self):
|
|
path=self.output/self.doc['revision_path'];path.write_bytes(b'conflict')
|
|
with self.assertRaisesRegex(RuntimeError,'conflicts'):
|
|
build_site.build(self.manifest,self.output)
|
|
self.assertEqual(path.read_bytes(),b'conflict')
|
|
def test_missing_archived_old_page_refuses_clean_build(self):
|
|
(self.archive/'page.html').unlink()
|
|
with self.assertRaisesRegex(ValueError,'artifact'):self.clean_build()
|
|
self.assertFalse((self.root/'clean').exists())
|
|
def test_tampered_original_owner_source_refuses(self):
|
|
(self.archive/'source.md').write_bytes(b'changed')
|
|
with self.assertRaisesRegex(RuntimeError,'owner source digest'):self.clean_build()
|
|
def test_delivered_manifest_coverage_cannot_drop_document(self):
|
|
self.receipt['documents']=[];self.configure()
|
|
with self.assertRaisesRegex(ValueError,'every delivered'):self.clean_build()
|
|
def test_changed_source_under_same_revision_refuses(self):
|
|
self.source.write_text(self.source.read_text().replace('No position.','Changed position.'))
|
|
with self.assertRaisesRegex(RuntimeError,'Publish a new revision'):self.clean_build()
|
|
|
|
def test_canonical_cannot_overwrite_archived_revision(self):
|
|
manifest=json.loads(self.manifest.read_text())
|
|
manifest['documents'][0]['canonical_path']=self.doc['revision_path']
|
|
self.manifest.write_text(json.dumps(manifest))
|
|
# Bump current revision so the ordinary manifest path check cannot catch it.
|
|
self.source.write_text(self.source.read_text().replace('draft-1','draft-2'))
|
|
with self.assertRaisesRegex(ValueError,'archived immutable'):
|
|
self.clean_build()
|
|
|
|
def test_legacy_redirect_cannot_overwrite_archived_revision(self):
|
|
manifest=json.loads(self.manifest.read_text())
|
|
manifest['documents'][0]['legacy_paths']=[self.doc['revision_path']]
|
|
self.manifest.write_text(json.dumps(manifest))
|
|
self.source.write_text(self.source.read_text().replace('draft-1','draft-2'))
|
|
with self.assertRaisesRegex(ValueError,'archived immutable'):
|
|
self.clean_build()
|
|
|
|
def test_delivered_commit_field_must_match_captured_manifest(self):
|
|
self.receipt['documents'][0]['delivered_manifest_source_revision']='c'*40
|
|
self.configure()
|
|
with self.assertRaisesRegex(RuntimeError,'delivered source revision'):
|
|
self.clean_build()
|