from __future__ import annotations import copy import hashlib import json import os import shutil import sqlite3 import tempfile import unittest from dataclasses import replace from pathlib import Path from unittest import mock from docforge.errors import DocForgeError from docforge.manual_projection import ( build_manual_projection_package, build_manual_render_plan, ) from docforge.models import Edge from docforge.project import Project from docforge.projection_contract import ( MANUAL_RENDER_PLAN_CONTRACT, PROJECTION_PACKAGE_CONTRACT, PROJECTION_RECEIPT_CONTRACT, ManualRenderPlanV1, ProjectionArtifact, ProjectionPackageV1, ProjectionReceiptV1, canonical_projection_bytes, projection_hash, ) from docforge.render_contract import GenericHtmlRenderer from docforge_renderers.manual import ManualHtmlRenderer ROOT = Path(__file__).resolve().parents[1] FIXTURES = ROOT / "tests" / "fixtures" ALPHA_RENDERER_VERSION = "1+markdown-it-py-4.2.0" ALPHA_RENDER_IDENTITY = "1c0a49c28ba3b0dabf94be36e75def197dee1be3cb73ac405b09875383c8dc5f" ALPHA_OUTPUT_HASH = "81656bb89debc7ad1fbe8bc290e9a3ba90664442b17a6d57e908d30d20c47f77" ALPHA_OUTPUT_BYTES = 2043 class ProjectionContractTests(unittest.TestCase): def setUp(self) -> None: self.temporary = tempfile.TemporaryDirectory() self.addCleanup(self.temporary.cleanup) self.root = Path(self.temporary.name) / "alpha" shutil.copytree(FIXTURES / "alpha", self.root) self.project = Project.open(self.root) self.snapshot = self.project.load() assert self.snapshot.descriptor.render is not None self.view = self.snapshot.descriptor.render.views[0] self.template = self.view.template_path.read_bytes() self.plan = build_manual_render_plan( self.snapshot, self.view, changeset_hash=None, ) self.package = build_manual_projection_package( self.plan, self.template, renderer_id="generic_html", renderer_version=ALPHA_RENDERER_VERSION, max_output_bytes=self.snapshot.descriptor.limits.max_render_bytes, ) def test_canonical_identity_is_stable_and_tampering_is_rejected(self) -> None: self.assertEqual( b'{"a":"\xc3\xa9","b":1}', canonical_projection_bytes({"b": 1, "a": "\N{LATIN SMALL LETTER E WITH ACUTE}"}), ) self.assertEqual( projection_hash( {key: value for key, value in self.plan.as_dict().items() if key != "plan_id"} ), self.plan.plan_id, ) self.assertEqual( self.plan.plan_id, build_manual_render_plan( self.snapshot, self.view, changeset_hash=None, ).plan_id, ) self.assertEqual( self.package.package_id, ProjectionPackageV1.from_dict( json.loads(json.dumps(self.package.as_dict())) ).package_id, ) tampered_plan = copy.deepcopy(self.plan.as_dict()) pages = tampered_plan["pages"] assert isinstance(pages, list) assert isinstance(pages[0], dict) pages[0]["title"] = "Tampered title" with self.assertRaises(DocForgeError) as plan_error: ManualRenderPlanV1.from_dict(tampered_plan) self.assertEqual("invalid_projection", plan_error.exception.code) tampered_package = copy.deepcopy(self.package.as_dict()) assets = tampered_package["assets"] assert isinstance(assets, list) assert isinstance(assets[0], dict) assets[0]["text"] = f"{assets[0]['text']}\nTampered" with self.assertRaises(DocForgeError) as package_error: ProjectionPackageV1.from_dict(tampered_package) self.assertEqual("invalid_projection", package_error.exception.code) def test_contract_documents_reject_unknown_or_malformed_fields(self) -> None: plan_with_extra = copy.deepcopy(self.plan.as_dict()) plan_with_extra["unexpected"] = True with self.assertRaises(DocForgeError) as extra_plan: ManualRenderPlanV1.from_dict(plan_with_extra) self.assertEqual("invalid_projection", extra_plan.exception.code) plan_with_foreign_project = copy.deepcopy(self.plan.as_dict()) project = plan_with_foreign_project["project"] assert isinstance(project, dict) project["absolute_root"] = str(self.root) with self.assertRaises(DocForgeError) as foreign_project: ManualRenderPlanV1.from_dict(plan_with_foreign_project) self.assertEqual("invalid_projection", foreign_project.exception.code) package_with_extra = copy.deepcopy(self.package.as_dict()) package_with_extra["unexpected"] = [] with self.assertRaises(DocForgeError) as extra_package: ProjectionPackageV1.from_dict(package_with_extra) self.assertEqual("invalid_projection", extra_package.exception.code) result = ManualHtmlRenderer(ALPHA_RENDERER_VERSION).render(self.package) receipt_with_extra = copy.deepcopy(result.receipt.as_dict()) receipt_with_extra["artifact_bytes"] = "forbidden" with self.assertRaises(DocForgeError) as extra_receipt: ProjectionReceiptV1.from_dict(receipt_with_extra) self.assertEqual("invalid_projection", extra_receipt.exception.code) with self.assertRaises(DocForgeError) as path_artifact: ProjectionReceiptV1.create( kind="manual", package_id=self.package.package_id, plan_id=self.plan.plan_id, renderer={ "renderer_id": "generic_html", "renderer_version": ALPHA_RENDERER_VERSION, }, artifacts=[ { "artifact_id": "../manual.html", "media_type": "text/html", "sha256": "0" * 64, "bytes": 1, } ], diagnostics={}, timing={"elapsed_ns": 0}, peak_memory_bytes=None, ) self.assertEqual("invalid_projection", path_artifact.exception.code) def test_projection_package_is_path_free_and_rejects_runtime_references(self) -> None: serialized = canonical_projection_bytes(self.package.as_dict()) self.assertNotIn(str(self.root).encode("utf-8"), serialized) self.assertNotIn(b"source_path", serialized) self.assertNotIn(b"sqlite", serialized.lower()) with self.assertRaises(DocForgeError) as absolute_path: ProjectionPackageV1.create( kind="manual", plan=self.plan, renderer={ "renderer_id": "generic_html", "renderer_version": ALPHA_RENDERER_VERSION, }, components=[], assets=[], output_policy={ "artifact_ids": ["manual.html"], "max_total_bytes": 1000, "template_path": "/home/example/private-template.html", }, ) self.assertEqual("invalid_projection", absolute_path.exception.code) with self.assertRaises(DocForgeError) as database_reference: ProjectionPackageV1.create( kind="manual", plan=self.plan, renderer={ "renderer_id": "generic_html", "renderer_version": ALPHA_RENDERER_VERSION, }, components=[], assets=[], output_policy={ "artifact_ids": ["manual.html"], "max_total_bytes": 1000, "database": "index.sqlite3", }, ) self.assertEqual("invalid_projection", database_reference.exception.code) def test_receipt_attests_artifacts_without_embedding_content(self) -> None: result = ManualHtmlRenderer(ALPHA_RENDERER_VERSION).render( self.package, render_identity=ALPHA_RENDER_IDENTITY, ) self.assertEqual(1, len(result.artifacts)) artifact = result.artifacts[0] evidence = artifact.evidence() receipt = result.receipt.as_dict() self.assertEqual(self.package.package_id, receipt["package_id"]) self.assertEqual(self.plan.plan_id, receipt["plan_id"]) self.assertEqual([evidence], receipt["artifacts"]) self.assertEqual(PROJECTION_RECEIPT_CONTRACT, receipt["contract"]) self.assertEqual( { "renderer_id": "generic_html", "renderer_version": ALPHA_RENDERER_VERSION, }, receipt["renderer"], ) self.assertEqual({"warnings": []}, receipt["diagnostics"]) self.assertIsNone(receipt["peak_memory_bytes"]) timing = receipt["timing"] assert isinstance(timing, dict) self.assertGreaterEqual(timing["elapsed_ns"], 0) self.assertNotIn("content", evidence) self.assertNotIn(artifact.content, canonical_projection_bytes(receipt)) self.assertEqual( receipt["receipt_id"], ProjectionReceiptV1.from_dict(copy.deepcopy(receipt)).receipt_id, ) tampered_receipt = copy.deepcopy(receipt) artifacts = tampered_receipt["artifacts"] assert isinstance(artifacts, list) assert isinstance(artifacts[0], dict) artifacts[0]["bytes"] = int(artifacts[0]["bytes"]) + 1 with self.assertRaises(DocForgeError) as tampered: ProjectionReceiptV1.from_dict(tampered_receipt) self.assertEqual("invalid_projection", tampered.exception.code) def test_manual_plan_is_deterministic_and_preserves_alpha_semantics(self) -> None: document = self.plan.as_dict() self.assertEqual(MANUAL_RENDER_PLAN_CONTRACT, document["contract"]) self.assertIsNone(document["changeset_hash"]) pages = document["pages"] navigation = document["navigation"] search_documents = document["search_documents"] diagnostics = document["diagnostics"] assert isinstance(pages, list) assert isinstance(navigation, list) assert isinstance(search_documents, list) assert isinstance(diagnostics, dict) self.assertEqual( ["guide.foundation", "guide.workflow", "proof.validation"], [page["node_id"] for page in pages], ) self.assertEqual( ["guide.foundation", "guide.workflow", "proof.validation"], [item["node_id"] for item in navigation], ) self.assertEqual( ["guide.foundation", "guide.workflow", "proof.validation"], [item["node_id"] for item in search_documents], ) self.assertEqual([], diagnostics["orphans"]) self.assertEqual([], diagnostics["cycles"]) page_by_id = {page["node_id"]: page for page in pages} self.assertEqual( [ { "source_id": "guide.workflow", "relation": "depends_on", "target_id": "guide.foundation", } ], page_by_id["guide.foundation"]["backlinks"], ) self.assertEqual( [ { "source_id": "guide.workflow", "relation": "depends_on", "target_id": "guide.foundation", } ], page_by_id["guide.workflow"]["cross_references"], ) self.assertEqual( [ { "source_id": "proof.validation", "relation": "proves", "target_id": "guide.workflow", } ], page_by_id["guide.workflow"]["backlinks"], ) self.assertEqual( [ { "source_id": "proof.validation", "relation": "proves", "target_id": "guide.workflow", } ], page_by_id["proof.validation"]["cross_references"], ) self.assertTrue( all( page["components"] == [ "manual.node-metadata@1", "manual.summary@1", "manual.commonmark@1", "manual.relationships@1", ] for page in pages ) ) proposed = build_manual_render_plan( self.snapshot, self.view, changeset_hash="a" * 64, ) self.assertNotEqual(self.plan.plan_id, proposed.plan_id) self.assertEqual("a" * 64, proposed.as_dict()["changeset_hash"]) def test_cycle_orphan_backlink_and_cross_reference_planning(self) -> None: edges = ( Edge("guide.foundation", "relates_to", "guide.workflow"), Edge("guide.workflow", "returns_to", "guide.foundation"), ) snapshot = replace(self.snapshot, edges=edges) first = build_manual_render_plan(snapshot, self.view, changeset_hash=None) second = build_manual_render_plan(snapshot, self.view, changeset_hash=None) self.assertEqual(first.plan_id, second.plan_id) document = first.as_dict() diagnostics = document["diagnostics"] pages = document["pages"] assert isinstance(diagnostics, dict) assert isinstance(pages, list) self.assertEqual(["proof.validation"], diagnostics["orphans"]) self.assertEqual( [["guide.foundation", "guide.workflow"]], diagnostics["cycles"], ) page_by_id = {page["node_id"]: page for page in pages} foundation = page_by_id["guide.foundation"] workflow = page_by_id["guide.workflow"] self.assertEqual( [ { "source_id": "guide.foundation", "relation": "relates_to", "target_id": "guide.workflow", } ], foundation["cross_references"], ) self.assertEqual( [ { "source_id": "guide.workflow", "relation": "returns_to", "target_id": "guide.foundation", } ], foundation["backlinks"], ) self.assertEqual( foundation["cross_references"], workflow["backlinks"], ) self.assertEqual( foundation["backlinks"], workflow["cross_references"], ) def test_alpha_compatibility_shim_preserves_legacy_identity_and_bytes(self) -> None: renderer = GenericHtmlRenderer() self.assertEqual(ALPHA_RENDERER_VERSION, renderer.renderer_version) prepared = renderer.prepare( self.snapshot, self.view, self.template, changeset_hash=None, ) self.assertEqual(ALPHA_RENDER_IDENTITY, prepared.render_identity) self.assertEqual(ALPHA_OUTPUT_HASH, prepared.output_hash) self.assertEqual(ALPHA_OUTPUT_BYTES, len(prepared.output)) self.assertEqual( ALPHA_OUTPUT_HASH, hashlib.sha256(prepared.output).hexdigest(), ) self.assertEqual(b"", prepared.output.splitlines()[0]) self.assertTrue(prepared.output.endswith(b"\n")) self.assertIn( f'content="{ALPHA_RENDER_IDENTITY}"'.encode(), prepared.output, ) def test_manual_renderer_rejects_project_provided_active_content(self) -> None: for active in ( "{{ docforge_content }}", '
{{ docforge_content }}
', '{{ docforge_content }}', '{{ docforge_content }}', '{{ docforge_content }}', ): with self.subTest(active=active): package = build_manual_projection_package( self.plan, active.encode("utf-8"), renderer_id="generic_html", renderer_version=ALPHA_RENDERER_VERSION, max_output_bytes=self.snapshot.descriptor.limits.max_render_bytes, ) with self.assertRaises(DocForgeError) as rejected: ManualHtmlRenderer(ALPHA_RENDERER_VERSION).render(package) self.assertEqual("invalid_template", rejected.exception.code) def test_manual_renderer_has_no_project_sqlite_or_path_write_capability(self) -> None: renderer = ManualHtmlRenderer(ALPHA_RENDERER_VERSION) forbidden = AssertionError("manual renderer crossed its capability boundary") with ( mock.patch.object(Project, "open", side_effect=forbidden), mock.patch.object(Project, "load", side_effect=forbidden), mock.patch.object(sqlite3, "connect", side_effect=forbidden), mock.patch.object(Path, "write_bytes", side_effect=forbidden), mock.patch.object(Path, "write_text", side_effect=forbidden), mock.patch.object(Path, "mkdir", side_effect=forbidden), mock.patch.object(Path, "touch", side_effect=forbidden), mock.patch.object(Path, "unlink", side_effect=forbidden), mock.patch.object(Path, "rename", side_effect=forbidden), mock.patch.object(Path, "replace", side_effect=forbidden), mock.patch.object(os, "mkdir", side_effect=forbidden), mock.patch.object(os, "makedirs", side_effect=forbidden), mock.patch.object(os, "rename", side_effect=forbidden), mock.patch.object(os, "replace", side_effect=forbidden), mock.patch.object(os, "unlink", side_effect=forbidden), ): result = renderer.render( self.package, render_identity=ALPHA_RENDER_IDENTITY, ) self.assertEqual(1, len(result.artifacts)) self.assertEqual("manual.html", result.artifacts[0].artifact_id) self.assertEqual(ALPHA_OUTPUT_HASH, result.artifacts[0].evidence()["sha256"]) def test_projection_artifact_evidence_is_canonical_and_content_free(self) -> None: artifact = ProjectionArtifact( artifact_id="manual.html", media_type="text/html; charset=utf-8", content=b"manual bytes", ) self.assertEqual( { "artifact_id": "manual.html", "media_type": "text/html; charset=utf-8", "sha256": hashlib.sha256(b"manual bytes").hexdigest(), "bytes": len(b"manual bytes"), }, artifact.evidence(), ) self.assertEqual( PROJECTION_PACKAGE_CONTRACT, self.package.as_dict()["contract"], ) if __name__ == "__main__": unittest.main()