1
0
Fork 0
Code Issues Pull requests Projects Releases 2 Packages Wiki Activity Actions Pages
DocForge2/tests/test_projection_contract.py

486 lines
19 KiB
Python

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"<!DOCTYPE html>", prepared.output.splitlines()[0])
self.assertTrue(prepared.output.endswith(b"</html>\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 (
"<script>alert(1)</script>{{ docforge_content }}",
'<main onclick="alert(1)">{{ docforge_content }}</main>',
'<a href="javascript:alert(1)">{{ docforge_content }}</a>',
'<iframe src="https://example.invalid"></iframe>{{ docforge_content }}',
'<meta http-equiv="refresh" content="0">{{ 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()