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.projection_fragments import FRAGMENT_CACHE_DIRECTORY, FragmentRecord from docforge.projection_worker import render_projection_in_worker 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_fragments_are_incremental_and_full_worker_output_is_the_oracle( self, ) -> None: renderer = GenericHtmlRenderer() with mock.patch( "docforge.render_contract.render_projection_in_worker", wraps=render_projection_in_worker, ) as detached: cold = renderer.prepare( self.snapshot, self.view, self.template, changeset_hash=None, ) self.assertEqual(2, detached.call_count) fragment_root = self.snapshot.descriptor.cache_root / FRAGMENT_CACHE_DIRECTORY self.assertEqual( len(self.plan.document["pages"]), len(list(fragment_root.glob("*.json"))), ) with mock.patch( "docforge.render_contract.render_projection_in_worker", wraps=render_projection_in_worker, ) as detached: warm = renderer.prepare( self.snapshot, self.view, self.template, changeset_hash=None, ) self.assertEqual(1, detached.call_count) self.assertEqual(cold.output, warm.output) with mock.patch( "docforge.render_contract.render_projection_in_worker", wraps=render_projection_in_worker, ) as detached: full = GenericHtmlRenderer(incremental=False).prepare( self.snapshot, self.view, self.template, changeset_hash=None, ) self.assertEqual(1, detached.call_count) self.assertEqual(full.output, warm.output) first_fragment = sorted(fragment_root.glob("*.json"))[0] first_fragment.write_bytes(b"{corrupt") with mock.patch( "docforge.render_contract.render_projection_in_worker", wraps=render_projection_in_worker, ) as detached: recovered = renderer.prepare( self.snapshot, self.view, self.template, changeset_hash=None, ) self.assertEqual(2, detached.call_count) self.assertEqual(full.output, recovered.output) original_record = FragmentRecord.from_bytes(first_fragment.read_bytes()) first_fragment.write_bytes( FragmentRecord.create( original_record.key, b"", ).to_bytes() ) with mock.patch( "docforge.render_contract.render_projection_in_worker", wraps=render_projection_in_worker, ) as detached: forged = renderer.prepare( self.snapshot, self.view, self.template, changeset_hash=None, ) self.assertEqual(2, detached.call_count) self.assertEqual(full.output, forged.output) self.assertNotIn(b"forged fragment", forged.output) def test_oversized_manual_fragment_bypasses_cache_and_uses_full_worker(self) -> None: content = "x" * 4_100_000 node = replace( self.snapshot.nodes[0], content=content, content_hash=hashlib.sha256(content.encode()).hexdigest(), ) snapshot = replace( self.snapshot, descriptor=replace( self.snapshot.descriptor, limits=replace( self.snapshot.descriptor.limits, max_render_bytes=20_000_000, ), ), nodes=(node,), edges=(), source_hash=hashlib.sha256(b"oversized-fragment").hexdigest(), ) with mock.patch( "docforge.render_contract.render_projection_in_worker", wraps=render_projection_in_worker, ) as detached: prepared = GenericHtmlRenderer().prepare( snapshot, self.view, self.template, changeset_hash=None, ) self.assertEqual(1, detached.call_count) full = GenericHtmlRenderer(incremental=False).prepare( snapshot, self.view, self.template, changeset_hash=None, ) self.assertEqual(full.output, prepared.output) def test_fragment_package_overflow_falls_back_to_full_worker(self) -> None: content = "x" * 2_700_000 base = self.snapshot.nodes[0] nodes = tuple( replace( base, node_id=f"guide.large-{index}", title=f"Large {index}", content=content, source_path=f"docs/content/large-{index}.md", content_hash=hashlib.sha256(f"{index}:{content}".encode()).hexdigest(), ) for index in range(4) ) descriptor = replace( self.snapshot.descriptor, limits=replace( self.snapshot.descriptor.limits, max_render_bytes=20_000_000, ), ) snapshot = replace( self.snapshot, descriptor=descriptor, nodes=nodes, edges=(), source_hash=hashlib.sha256(b"large-fragment-fixture").hexdigest(), ) incremental = GenericHtmlRenderer().prepare( snapshot, self.view, self.template, changeset_hash=None, ) full = GenericHtmlRenderer(incremental=False).prepare( snapshot, self.view, self.template, changeset_hash=None, ) self.assertGreater(len(full.output), 10_000_000) self.assertEqual(full.output, incremental.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()