Add versioned independent projection contracts
This commit is contained in:
parent
4c5773c865
commit
96e3965855
22 changed files with 3561 additions and 133 deletions
404
tests/test_graph_projection.py
Normal file
404
tests/test_graph_projection.py
Normal file
|
|
@ -0,0 +1,404 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
|
||||
from docforge.errors import DocForgeError
|
||||
from docforge.graph_projection import (
|
||||
GraphViewRequestV1,
|
||||
build_graph_view_plan,
|
||||
)
|
||||
from docforge.models import Edge, Limits, Node, ProjectDescriptor, ProjectSnapshot
|
||||
from docforge.projection_contract import GraphViewPlanV1
|
||||
|
||||
|
||||
def _document(plan: GraphViewPlanV1) -> dict[str, Any]:
|
||||
return cast(dict[str, Any], plan.as_dict())
|
||||
|
||||
|
||||
def _node(
|
||||
node_id: str,
|
||||
*,
|
||||
title: str | None = None,
|
||||
family: str = "code",
|
||||
authority: str = "derived",
|
||||
status: str = "active",
|
||||
tags: tuple[str, ...] = (),
|
||||
) -> Node:
|
||||
return Node(
|
||||
node_id=node_id,
|
||||
title=title or node_id,
|
||||
family=family,
|
||||
authority=authority,
|
||||
status=status,
|
||||
tags=tags,
|
||||
summary=f"Summary for {node_id}",
|
||||
content=f"SECRET SOURCE BODY {node_id}",
|
||||
source_path=f"/private/source/{node_id}.py",
|
||||
source_anchor=f"line-{len(node_id)}",
|
||||
content_hash=(node_id.encode("utf-8").hex() + "0" * 64)[:64],
|
||||
)
|
||||
|
||||
|
||||
def _snapshot(
|
||||
root: Path,
|
||||
nodes: tuple[Node, ...],
|
||||
edges: tuple[Edge, ...],
|
||||
) -> ProjectSnapshot:
|
||||
descriptor = ProjectDescriptor(
|
||||
schema_version=1,
|
||||
project_id="graph-project",
|
||||
title="Graph project",
|
||||
adapter="generic",
|
||||
root=root,
|
||||
descriptor_path=root / ".docforge" / "project.toml",
|
||||
descriptor_hash="d" * 64,
|
||||
content_roots=(root / "docs",),
|
||||
authority_files=(),
|
||||
cache_root=root / ".docforge" / "cache",
|
||||
index_path=root / ".docforge" / "cache" / "index.sqlite3",
|
||||
changeset_root=root / ".docforge" / "changesets",
|
||||
proposal_writers=(),
|
||||
render=None,
|
||||
allowed_relations=tuple(sorted({edge.relation for edge in edges})),
|
||||
profiles=(),
|
||||
limits=Limits(),
|
||||
)
|
||||
return ProjectSnapshot(
|
||||
descriptor=descriptor,
|
||||
nodes=nodes,
|
||||
edges=edges,
|
||||
revision="revision-1",
|
||||
source_hash="a" * 64,
|
||||
)
|
||||
|
||||
|
||||
class GraphProjectionTests(unittest.TestCase):
|
||||
def test_exact_root_plan_is_deterministic_sorted_and_path_free(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
nodes = (
|
||||
_node("c", family="docs", tags=("python",)),
|
||||
_node("a", tags=("python", "callable")),
|
||||
_node("b", tags=("python",)),
|
||||
_node("unrelated"),
|
||||
)
|
||||
edges = (
|
||||
Edge("b", "calls", "c"),
|
||||
Edge("a", "calls", "b"),
|
||||
Edge("unrelated", "calls", "c"),
|
||||
)
|
||||
request = GraphViewRequestV1(
|
||||
view_id="architecture",
|
||||
title="Architecture",
|
||||
root_node_id="a",
|
||||
depth=2,
|
||||
max_nodes=10,
|
||||
max_edges=10,
|
||||
max_work=100,
|
||||
)
|
||||
first = build_graph_view_plan(_snapshot(root, nodes, edges), request, True)
|
||||
second = build_graph_view_plan(
|
||||
_snapshot(root, tuple(reversed(nodes)), tuple(reversed(edges))),
|
||||
request,
|
||||
True,
|
||||
)
|
||||
self.assertEqual(first.as_dict(), second.as_dict())
|
||||
GraphViewPlanV1.from_dict(first.as_dict())
|
||||
document = _document(first)
|
||||
self.assertEqual(
|
||||
["a", "b", "c"], [node["node_id"] for node in document["graph"]["nodes"]]
|
||||
)
|
||||
self.assertEqual(
|
||||
[
|
||||
{"source_id": "a", "relation": "calls", "target_id": "b"},
|
||||
{"source_id": "b", "relation": "calls", "target_id": "c"},
|
||||
],
|
||||
document["graph"]["edges"],
|
||||
)
|
||||
encoded = json.dumps(document, sort_keys=True)
|
||||
self.assertNotIn(str(root), encoded)
|
||||
self.assertNotIn("SECRET SOURCE BODY", encoded)
|
||||
self.assertNotIn("/private/source/", encoded)
|
||||
self.assertNotIn("line-1", encoded)
|
||||
self.assertEqual("excluded", document["policy"]["source_paths"])
|
||||
self.assertEqual("excluded", document["policy"]["source_bodies"])
|
||||
self.assertEqual("allowed", document["policy"]["logic"])
|
||||
self.assertEqual("exact_root", document["view"]["scope"]["kind"])
|
||||
|
||||
def test_lexical_scope_uses_metadata_only_and_closed_filters(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
nodes = (
|
||||
_node(
|
||||
"api.handler",
|
||||
title="Request handler",
|
||||
family="code",
|
||||
authority="derived",
|
||||
tags=("python", "route"),
|
||||
),
|
||||
_node(
|
||||
"api.test",
|
||||
title="Handler proof",
|
||||
family="test",
|
||||
authority="approved_plan",
|
||||
tags=("python", "test"),
|
||||
),
|
||||
replace(
|
||||
_node("hidden.body", family="code", tags=("python",)),
|
||||
content="request handler appears only in the forbidden source body",
|
||||
),
|
||||
)
|
||||
edges = (
|
||||
Edge("api.handler", "tested_by", "api.test"),
|
||||
Edge("hidden.body", "relates_to", "api.handler"),
|
||||
)
|
||||
request = GraphViewRequestV1(
|
||||
view_id="routes",
|
||||
title="Routes",
|
||||
query="request handler",
|
||||
families=("code",),
|
||||
authorities=("derived",),
|
||||
tags=("python", "route"),
|
||||
relations=("tested_by",),
|
||||
max_nodes=10,
|
||||
max_edges=10,
|
||||
max_work=100,
|
||||
)
|
||||
plan = build_graph_view_plan(_snapshot(root, nodes, edges), request, False)
|
||||
document = _document(plan)
|
||||
self.assertEqual(
|
||||
["api.handler"],
|
||||
[node["node_id"] for node in document["graph"]["nodes"]],
|
||||
)
|
||||
self.assertEqual([], document["graph"]["edges"])
|
||||
self.assertEqual(
|
||||
{
|
||||
"families": ["code"],
|
||||
"relations": ["tested_by"],
|
||||
"authorities": ["derived"],
|
||||
"statuses": [],
|
||||
"tags": ["python", "route"],
|
||||
},
|
||||
document["view"]["filters"],
|
||||
)
|
||||
self.assertEqual("lexical", document["view"]["scope"]["kind"])
|
||||
|
||||
def test_result_and_work_limits_emit_explicit_omissions(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
nodes = tuple(_node(value) for value in ("a", "b", "c", "d"))
|
||||
edges = (
|
||||
Edge("a", "calls", "b"),
|
||||
Edge("a", "calls", "c"),
|
||||
Edge("a", "calls", "d"),
|
||||
Edge("b", "calls", "c"),
|
||||
)
|
||||
result_limited = build_graph_view_plan(
|
||||
_snapshot(root, nodes, edges),
|
||||
GraphViewRequestV1(
|
||||
view_id="limited",
|
||||
title="Limited",
|
||||
root_node_id="a",
|
||||
max_nodes=2,
|
||||
max_edges=0,
|
||||
max_work=100,
|
||||
),
|
||||
False,
|
||||
)
|
||||
result_limited = _document(result_limited)
|
||||
self.assertEqual(
|
||||
["a", "b"], [node["node_id"] for node in result_limited["graph"]["nodes"]]
|
||||
)
|
||||
self.assertEqual([], result_limited["graph"]["edges"])
|
||||
self.assertEqual(
|
||||
["edge_result_limit", "node_result_limit"],
|
||||
[item["code"] for item in result_limited["omissions"]],
|
||||
)
|
||||
|
||||
work_limited = build_graph_view_plan(
|
||||
_snapshot(root, nodes, edges),
|
||||
GraphViewRequestV1(
|
||||
view_id="work",
|
||||
title="Work",
|
||||
root_node_id="a",
|
||||
max_nodes=10,
|
||||
max_edges=10,
|
||||
max_work=1,
|
||||
),
|
||||
False,
|
||||
)
|
||||
work_limited = _document(work_limited)
|
||||
self.assertIn(
|
||||
"work_limit",
|
||||
[item["code"] for item in work_limited["omissions"]],
|
||||
)
|
||||
self.assertEqual(
|
||||
1,
|
||||
work_limited["diagnostics"]["examined_work_units"],
|
||||
)
|
||||
self.assertTrue(work_limited["diagnostics"]["truncated"])
|
||||
|
||||
def test_no_ast_policy_excludes_requested_logic(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
snapshot = _snapshot(root, (_node("a"),), ())
|
||||
request = GraphViewRequestV1(
|
||||
view_id="logic",
|
||||
title="Logic",
|
||||
root_node_id="a",
|
||||
initial_mode="logic",
|
||||
include_logic=True,
|
||||
)
|
||||
blocked = _document(build_graph_view_plan(snapshot, request, False))
|
||||
self.assertEqual("forbidden", blocked["policy"]["logic"])
|
||||
self.assertEqual([], blocked["graph"]["logic_projections"])
|
||||
self.assertIn(
|
||||
"logic_forbidden",
|
||||
[item["code"] for item in blocked["omissions"]],
|
||||
)
|
||||
allowed = _document(build_graph_view_plan(snapshot, request, True))
|
||||
self.assertEqual("allowed", allowed["policy"]["logic"])
|
||||
self.assertNotIn(
|
||||
"logic_forbidden",
|
||||
[item["code"] for item in allowed["omissions"]],
|
||||
)
|
||||
|
||||
def test_edge_and_node_filters_constrain_exact_root_bfs(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
nodes = (
|
||||
_node("root", family="code", status="active"),
|
||||
_node("code-child", family="code", status="active"),
|
||||
_node("doc-child", family="docs", status="active"),
|
||||
_node("old-child", family="code", status="historical"),
|
||||
)
|
||||
edges = (
|
||||
Edge("root", "calls", "code-child"),
|
||||
Edge("root", "documents", "doc-child"),
|
||||
Edge("root", "calls", "old-child"),
|
||||
)
|
||||
plan = build_graph_view_plan(
|
||||
_snapshot(root, nodes, edges),
|
||||
GraphViewRequestV1(
|
||||
view_id="filtered",
|
||||
title="Filtered",
|
||||
root_node_id="root",
|
||||
families=("code",),
|
||||
statuses=("active",),
|
||||
relations=("calls",),
|
||||
max_work=100,
|
||||
),
|
||||
False,
|
||||
)
|
||||
plan = _document(plan)
|
||||
self.assertEqual(
|
||||
["code-child", "root"], [node["node_id"] for node in plan["graph"]["nodes"]]
|
||||
)
|
||||
self.assertEqual(
|
||||
[{"source_id": "root", "relation": "calls", "target_id": "code-child"}],
|
||||
plan["graph"]["edges"],
|
||||
)
|
||||
|
||||
def test_invalid_requests_and_graphs_fail_closed(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
snapshot = _snapshot(root, (_node("a"),), ())
|
||||
invalid_requests = (
|
||||
GraphViewRequestV1(view_id="v", title="V"),
|
||||
GraphViewRequestV1(
|
||||
view_id="v",
|
||||
title="V",
|
||||
root_node_id="a",
|
||||
query="a",
|
||||
),
|
||||
GraphViewRequestV1(
|
||||
view_id="v",
|
||||
title="V",
|
||||
root_node_id="a",
|
||||
max_nodes=0,
|
||||
),
|
||||
GraphViewRequestV1(
|
||||
view_id="v",
|
||||
title="V",
|
||||
query="***",
|
||||
),
|
||||
GraphViewRequestV1(
|
||||
view_id="v",
|
||||
title="V",
|
||||
root_node_id="a",
|
||||
families=("code", "code"),
|
||||
),
|
||||
)
|
||||
for request in invalid_requests:
|
||||
with self.subTest(request=request), self.assertRaises(DocForgeError) as error:
|
||||
build_graph_view_plan(snapshot, request, False)
|
||||
self.assertEqual("invalid_graph_view_request", error.exception.code)
|
||||
|
||||
with self.assertRaises(DocForgeError) as missing:
|
||||
build_graph_view_plan(
|
||||
snapshot,
|
||||
GraphViewRequestV1(
|
||||
view_id="v",
|
||||
title="V",
|
||||
root_node_id="missing",
|
||||
),
|
||||
False,
|
||||
)
|
||||
self.assertEqual("missing_node", missing.exception.code)
|
||||
|
||||
duplicate = _snapshot(root, (_node("a"), _node("a")), ())
|
||||
with self.assertRaises(DocForgeError) as invalid:
|
||||
build_graph_view_plan(
|
||||
duplicate,
|
||||
GraphViewRequestV1(
|
||||
view_id="v",
|
||||
title="V",
|
||||
root_node_id="a",
|
||||
),
|
||||
False,
|
||||
)
|
||||
self.assertEqual("invalid_projection", invalid.exception.code)
|
||||
|
||||
def test_plan_identity_changes_with_generation_request_and_policy(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
snapshot = _snapshot(root, (_node("a"),), ())
|
||||
request = GraphViewRequestV1(
|
||||
view_id="v",
|
||||
title="V",
|
||||
root_node_id="a",
|
||||
)
|
||||
base = build_graph_view_plan(snapshot, request, False)
|
||||
self.assertEqual(
|
||||
base.plan_id,
|
||||
build_graph_view_plan(snapshot, request, False).plan_id,
|
||||
)
|
||||
self.assertNotEqual(
|
||||
base.plan_id,
|
||||
build_graph_view_plan(
|
||||
replace(snapshot, source_hash="b" * 64),
|
||||
request,
|
||||
False,
|
||||
).plan_id,
|
||||
)
|
||||
self.assertNotEqual(
|
||||
base.plan_id,
|
||||
build_graph_view_plan(
|
||||
snapshot,
|
||||
replace(request, title="Other"),
|
||||
False,
|
||||
).plan_id,
|
||||
)
|
||||
self.assertNotEqual(
|
||||
base.plan_id,
|
||||
build_graph_view_plan(snapshot, request, True).plan_id,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
486
tests/test_projection_contract.py
Normal file
486
tests/test_projection_contract.py
Normal file
|
|
@ -0,0 +1,486 @@
|
|||
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()
|
||||
314
tests/test_projection_schemas.py
Normal file
314
tests/test_projection_schemas.py
Normal file
|
|
@ -0,0 +1,314 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from jsonschema import Draft202012Validator
|
||||
|
||||
from docforge.errors import DocForgeError
|
||||
from docforge.projection_contract import (
|
||||
GraphViewPlanV1,
|
||||
ManualRenderPlanV1,
|
||||
ProjectionPackageV1,
|
||||
ProjectionReceiptV1,
|
||||
)
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SCHEMAS = ROOT / "schemas"
|
||||
|
||||
|
||||
class ProjectionSchemaTests(unittest.TestCase):
|
||||
@staticmethod
|
||||
def schema(name: str) -> dict[str, object]:
|
||||
return json.loads((SCHEMAS / name).read_text(encoding="utf-8"))
|
||||
|
||||
@classmethod
|
||||
def validator(cls, name: str) -> Draft202012Validator:
|
||||
return Draft202012Validator(cls.schema(name))
|
||||
|
||||
@staticmethod
|
||||
def project_identity() -> dict[str, object]:
|
||||
return {
|
||||
"project_id": "schema-fixture",
|
||||
"project_root_fingerprint": "a" * 16,
|
||||
"adapter": "generic",
|
||||
"revision": "fixture-revision",
|
||||
"source_hash": "b" * 64,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def manual_plan(cls) -> ManualRenderPlanV1:
|
||||
return ManualRenderPlanV1.create(
|
||||
{
|
||||
"project": cls.project_identity(),
|
||||
"view": {
|
||||
"view_id": "manual",
|
||||
"title": "Schema Manual",
|
||||
"families": ["guide"],
|
||||
"renderer": "generic_html",
|
||||
},
|
||||
"changeset_hash": None,
|
||||
"pages": [
|
||||
{
|
||||
"node_id": "guide.schema",
|
||||
"title": "Projection schema",
|
||||
"family": "guide",
|
||||
"authority": "authoritative",
|
||||
"status": "approved",
|
||||
"tags": ["schema"],
|
||||
"summary": "Defines the projection schema fixture.",
|
||||
"content": "The schema fixture is deterministic.",
|
||||
"content_hash": "c" * 64,
|
||||
"components": ["manual.commonmark@1"],
|
||||
"breadcrumbs": [],
|
||||
"cross_references": [],
|
||||
"backlinks": [],
|
||||
}
|
||||
],
|
||||
"navigation": [
|
||||
{
|
||||
"node_id": "guide.schema",
|
||||
"title": "Projection schema",
|
||||
}
|
||||
],
|
||||
"search_documents": [
|
||||
{
|
||||
"node_id": "guide.schema",
|
||||
"title": "Projection schema",
|
||||
"summary": "Defines the projection schema fixture.",
|
||||
"family": "guide",
|
||||
"status": "approved",
|
||||
"tags": ["schema"],
|
||||
}
|
||||
],
|
||||
"diagnostics": {"orphans": ["guide.schema"], "cycles": []},
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def graph_plan(cls) -> GraphViewPlanV1:
|
||||
return GraphViewPlanV1.create(
|
||||
{
|
||||
"project": cls.project_identity(),
|
||||
"view": {
|
||||
"view_id": "portable",
|
||||
"title": "Portable graph",
|
||||
"initial_mode": "nodes",
|
||||
"scope": {
|
||||
"kind": "exact_root",
|
||||
"root_node_id": "guide.schema",
|
||||
"depth": 2,
|
||||
},
|
||||
"filters": {
|
||||
"families": [],
|
||||
"relations": [],
|
||||
"authorities": [],
|
||||
"statuses": [],
|
||||
"tags": [],
|
||||
},
|
||||
"detail_fields": [
|
||||
"node_id",
|
||||
"title",
|
||||
"family",
|
||||
"authority",
|
||||
"status",
|
||||
"tags",
|
||||
"summary",
|
||||
"content_hash",
|
||||
],
|
||||
},
|
||||
"bounds": {
|
||||
"depth": 2,
|
||||
"max_nodes": 100,
|
||||
"max_edges": 400,
|
||||
"max_work": 100000,
|
||||
},
|
||||
"policy": {
|
||||
"visibility": "selected_graph_only",
|
||||
"source_paths": "excluded",
|
||||
"source_bodies": "excluded",
|
||||
"database_queries": "forbidden",
|
||||
"executable_content": "forbidden",
|
||||
"logic": "forbidden",
|
||||
"logic_requested": False,
|
||||
},
|
||||
"graph": {
|
||||
"root_node_id": "guide.schema",
|
||||
"nodes": [],
|
||||
"edges": [],
|
||||
"logic_projections": [],
|
||||
},
|
||||
"omissions": [],
|
||||
"diagnostics": {
|
||||
"selection": "exact_root",
|
||||
"returned_nodes": 0,
|
||||
"returned_edges": 0,
|
||||
"examined_work_units": 0,
|
||||
"truncated": False,
|
||||
"ordering": "node_id;source_id,relation,target_id",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def package(
|
||||
cls,
|
||||
plan: ManualRenderPlanV1 | GraphViewPlanV1 | None = None,
|
||||
) -> ProjectionPackageV1:
|
||||
selected = plan or cls.manual_plan()
|
||||
kind = "manual" if isinstance(selected, ManualRenderPlanV1) else "graph"
|
||||
return ProjectionPackageV1.create(
|
||||
kind=kind,
|
||||
plan=selected,
|
||||
renderer={
|
||||
"renderer_id": "generic_html",
|
||||
"renderer_version": "1",
|
||||
},
|
||||
components=[{"component_id": "projection.document@1"}],
|
||||
assets=[
|
||||
{
|
||||
"asset_id": "projection.template",
|
||||
"media_type": "text/plain; charset=utf-8",
|
||||
"sha256": "d" * 64,
|
||||
"text": "fixture",
|
||||
}
|
||||
],
|
||||
output_policy={
|
||||
"artifact_ids": ["projection.html"],
|
||||
"max_total_bytes": 1000000,
|
||||
},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def receipt(cls) -> ProjectionReceiptV1:
|
||||
package = cls.package()
|
||||
return ProjectionReceiptV1.create(
|
||||
kind="manual",
|
||||
package_id=package.package_id,
|
||||
plan_id=package.document["plan_id"], # type: ignore[arg-type]
|
||||
renderer={
|
||||
"renderer_id": "generic_html",
|
||||
"renderer_version": "1",
|
||||
},
|
||||
artifacts=[
|
||||
{
|
||||
"artifact_id": "manual.html",
|
||||
"media_type": "text/html; charset=utf-8",
|
||||
"sha256": "e" * 64,
|
||||
"bytes": 123,
|
||||
}
|
||||
],
|
||||
diagnostics={"warnings": []},
|
||||
timing={"elapsed_ns": 123456},
|
||||
peak_memory_bytes=None,
|
||||
)
|
||||
|
||||
def test_schemas_are_valid_and_accept_current_documents(self) -> None:
|
||||
documents = {
|
||||
"manual-render-plan.schema.json": self.manual_plan().as_dict(),
|
||||
"graph-view-plan.schema.json": self.graph_plan().as_dict(),
|
||||
"projection-package.schema.json": self.package().as_dict(),
|
||||
"projection-receipt.schema.json": self.receipt().as_dict(),
|
||||
}
|
||||
for name, document in documents.items():
|
||||
with self.subTest(schema=name):
|
||||
schema = self.schema(name)
|
||||
Draft202012Validator.check_schema(schema)
|
||||
Draft202012Validator(schema).validate(document)
|
||||
|
||||
graph_package = self.package(self.graph_plan()).as_dict()
|
||||
self.validator("projection-package.schema.json").validate(graph_package)
|
||||
|
||||
def test_unknown_fields_are_rejected_at_contract_boundaries(self) -> None:
|
||||
cases = (
|
||||
(
|
||||
"manual-render-plan.schema.json",
|
||||
self.manual_plan().as_dict(),
|
||||
),
|
||||
(
|
||||
"graph-view-plan.schema.json",
|
||||
self.graph_plan().as_dict(),
|
||||
),
|
||||
(
|
||||
"projection-package.schema.json",
|
||||
self.package().as_dict(),
|
||||
),
|
||||
(
|
||||
"projection-receipt.schema.json",
|
||||
self.receipt().as_dict(),
|
||||
),
|
||||
)
|
||||
for name, document in cases:
|
||||
with self.subTest(schema=name):
|
||||
document["unexpected"] = True
|
||||
self.assertFalse(self.validator(name).is_valid(document))
|
||||
|
||||
manual = self.manual_plan().as_dict()
|
||||
manual["pages"][0]["unexpected"] = True # type: ignore[index]
|
||||
self.assertFalse(self.validator("manual-render-plan.schema.json").is_valid(manual))
|
||||
|
||||
package = self.package().as_dict()
|
||||
package["assets"][0]["path"] = "/tmp/escape" # type: ignore[index]
|
||||
self.assertFalse(self.validator("projection-package.schema.json").is_valid(package))
|
||||
|
||||
receipt = self.receipt().as_dict()
|
||||
receipt["artifacts"][0]["content"] = "not receipt evidence" # type: ignore[index]
|
||||
self.assertFalse(self.validator("projection-receipt.schema.json").is_valid(receipt))
|
||||
|
||||
def test_obviously_malformed_identities_and_structures_are_rejected(self) -> None:
|
||||
manual = self.manual_plan().as_dict()
|
||||
manual["plan_id"] = "not-a-sha256"
|
||||
self.assertFalse(self.validator("manual-render-plan.schema.json").is_valid(manual))
|
||||
|
||||
graph = self.graph_plan().as_dict()
|
||||
graph["project"]["project_root_fingerprint"] = "wrong" # type: ignore[index]
|
||||
self.assertFalse(self.validator("graph-view-plan.schema.json").is_valid(graph))
|
||||
graph = self.graph_plan().as_dict()
|
||||
graph["bounds"] = -1
|
||||
self.assertFalse(self.validator("graph-view-plan.schema.json").is_valid(graph))
|
||||
|
||||
package = self.package().as_dict()
|
||||
package["assets"][0].pop("sha256") # type: ignore[index]
|
||||
self.assertFalse(self.validator("projection-package.schema.json").is_valid(package))
|
||||
package = self.package().as_dict()
|
||||
package["output_policy"]["max_total_bytes"] = 0 # type: ignore[index]
|
||||
self.assertFalse(self.validator("projection-package.schema.json").is_valid(package))
|
||||
package = self.package().as_dict()
|
||||
package["kind"] = "graph"
|
||||
self.assertFalse(self.validator("projection-package.schema.json").is_valid(package))
|
||||
|
||||
receipt = self.receipt().as_dict()
|
||||
receipt["artifacts"][0]["artifact_id"] = "../manual.html" # type: ignore[index]
|
||||
self.assertFalse(self.validator("projection-receipt.schema.json").is_valid(receipt))
|
||||
receipt = self.receipt().as_dict()
|
||||
receipt["artifacts"][0]["bytes"] = -1 # type: ignore[index]
|
||||
self.assertFalse(self.validator("projection-receipt.schema.json").is_valid(receipt))
|
||||
receipt = self.receipt().as_dict()
|
||||
receipt["peak_memory_bytes"] = True
|
||||
self.assertFalse(self.validator("projection-receipt.schema.json").is_valid(receipt))
|
||||
|
||||
def test_assets_and_artifacts_enforce_fixed_collection_bounds(self) -> None:
|
||||
package = self.package().as_dict()
|
||||
package["assets"] = [copy.deepcopy(package["assets"][0]) for _ in range(33)] # type: ignore[index]
|
||||
self.assertFalse(self.validator("projection-package.schema.json").is_valid(package))
|
||||
|
||||
receipt = self.receipt().as_dict()
|
||||
receipt["artifacts"] = [
|
||||
copy.deepcopy(receipt["artifacts"][0])
|
||||
for _ in range(33) # type: ignore[index]
|
||||
]
|
||||
self.assertFalse(self.validator("projection-receipt.schema.json").is_valid(receipt))
|
||||
|
||||
def test_canonical_identity_equality_remains_a_runtime_check(self) -> None:
|
||||
document = self.manual_plan().as_dict()
|
||||
document["plan_id"] = "f" * 64
|
||||
self.validator("manual-render-plan.schema.json").validate(document)
|
||||
with self.assertRaises(DocForgeError) as raised:
|
||||
ManualRenderPlanV1.from_dict(document)
|
||||
self.assertEqual("invalid_projection", raised.exception.code)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
@ -64,6 +64,14 @@ PUBLIC_IMPORTS = {
|
|||
"docforge.client_config": ("generate_client_configuration",),
|
||||
"docforge.doctor": ("run_doctor",),
|
||||
"docforge.index": ("ProjectIndex",),
|
||||
"docforge.graph_projection": (
|
||||
"GraphViewRequestV1",
|
||||
"build_graph_view_plan",
|
||||
),
|
||||
"docforge.manual_projection": (
|
||||
"build_manual_projection_package",
|
||||
"build_manual_render_plan",
|
||||
),
|
||||
"docforge.mcp_server": (
|
||||
"create_project_server",
|
||||
"create_read_only_server",
|
||||
|
|
@ -84,6 +92,16 @@ PUBLIC_IMPORTS = {
|
|||
"capability_mode",
|
||||
"compose_effective_policy",
|
||||
),
|
||||
"docforge.projection_contract": (
|
||||
"GraphViewPlanV1",
|
||||
"ManualRenderPlanV1",
|
||||
"ProjectionArtifact",
|
||||
"ProjectionPackageV1",
|
||||
"ProjectionReceiptV1",
|
||||
"ProjectionRenderResult",
|
||||
"canonical_projection_bytes",
|
||||
"projection_hash",
|
||||
),
|
||||
"docforge.retrieval": (
|
||||
"ContextCapsuleV1",
|
||||
"RetrievalPlanV1",
|
||||
|
|
@ -97,6 +115,7 @@ PUBLIC_IMPORTS = {
|
|||
"Renderer",
|
||||
"renderer_for",
|
||||
),
|
||||
"docforge_renderers.manual": ("ManualHtmlRenderer",),
|
||||
}
|
||||
|
||||
EXPECTED_ENTRY_POINTS = {
|
||||
|
|
|
|||
|
|
@ -386,6 +386,25 @@ class VisualizationTests(unittest.TestCase):
|
|||
with self.assertRaisesRegex(DocForgeError, "category is unsupported"):
|
||||
snapshot.filter_nodes(category="relation", value="depends_on", limit=2)
|
||||
|
||||
def test_snapshot_source_never_mixes_pinned_graph_with_newer_canonical_text(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = self.copy_fixture("alpha", Path(directory))
|
||||
index = ProjectIndex(Project.open(root))
|
||||
index.build()
|
||||
snapshot = VisualizationIndexSnapshot(index, index.check())
|
||||
before = snapshot.source("guide.workflow")
|
||||
|
||||
source = root / "docs/content/workflow.md"
|
||||
source.write_text(
|
||||
source.read_text(encoding="utf-8") + "\nNewer unindexed source text.\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
after = snapshot.source("guide.workflow")
|
||||
|
||||
self.assertEqual(before["content"], after["content"])
|
||||
self.assertNotIn("Newer unindexed source text", after["content"])
|
||||
self.assertEqual("index_snapshot", after["source_provenance"])
|
||||
|
||||
def test_flow_reverses_imports_into_a_complete_structural_path(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = self.copy_fixture("alpha", Path(directory))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue