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

381 lines
12 KiB
Python
Raw Normal View History

from __future__ import annotations
import argparse
import hashlib
import importlib
import json
import shutil
import subprocess
import sys
import tempfile
import tomllib
import unittest
from pathlib import Path
from jsonschema import Draft202012Validator
import docforge
from docforge.changeset_contract import canonical_bytes, document_hash
from docforge.changesets import ChangesetStore
from docforge.cli import _parser
from docforge.index import ProjectIndex
from docforge.mcp_server import (
ALL_TOOLS,
APPLICATION_TOOLS,
PROPOSAL_TOOLS,
READ_TOOLS,
SERVER_VERSION,
DocForgeService,
)
from docforge.project import Project
ROOT = Path(__file__).resolve().parents[1]
FIXTURES = ROOT / "tests" / "fixtures"
SCHEMAS = ROOT / "schemas"
PUBLIC_IMPORTS = {
"docforge": (
"CanonicalApplier",
"CanonicalApplicationService",
"DocForgeError",
"GenericCanonicalApplier",
"Project",
),
"docforge.adapter_contract": (
"AdapterAssembly",
2026-07-29 14:23:29 -04:00
"AdapterConformanceReport",
"AdapterEdge",
"AdapterImplementation",
"AdapterLoader",
"AdapterManifest",
"AdapterNode",
"AdapterProject",
"AdapterProjection",
"AdapterProjectSettings",
"AdapterSource",
"AdapterSourceProjection",
2026-07-29 14:23:29 -04:00
"CompleteAdapterAssemblyLoader",
"IncrementalAdapterAssembler",
"IncrementalAdapterLoader",
2026-07-29 14:23:29 -04:00
"verify_adapter_conformance",
),
"docforge.adapter_sdk": (
"AdapterAssembly",
"AdapterConformanceReport",
"AdapterLoader",
"AdapterManifest",
"AdapterProject",
"AdapterProjection",
"AdapterSourceProjection",
"CompleteAdapterAssemblyLoader",
"IncrementalAdapterLoader",
"LogicProjection",
"Node",
"verify_adapter_conformance",
),
"docforge.application": (
"CanonicalApplier",
"CanonicalApplicationService",
"GenericCanonicalApplier",
),
2026-07-29 15:10:07 -04:00
"docforge.adapter_launcher": (
"ADAPTER_LAUNCHER_SCHEMA_VERSION",
"AdapterLauncherV1",
"AdapterSourceAvailabilityV1",
"adapter_source_availability",
"validate_adapter_launcher",
),
"docforge.client_config": (
"generate_adapter_client_configuration",
"generate_client_configuration",
),
"docforge.doctor": ("run_doctor",),
"docforge.index": ("ProjectIndex",),
"docforge.graph_projection": (
"GraphViewRequestV1",
2026-07-29 11:37:33 -04:00
"build_graph_projection_package",
"build_graph_view_plan",
),
2026-07-29 11:37:33 -04:00
"docforge.graph_rendering": ("GraphRenderService",),
"docforge.manual_projection": (
"build_manual_projection_package",
"build_manual_render_plan",
),
"docforge.mcp_server": (
"create_project_server",
"create_read_only_server",
"create_server",
),
"docforge.models": (
"Edge",
"LogicEdge",
"LogicNode",
"LogicProjection",
"Node",
"ProjectDescriptor",
"ProjectService",
"ProjectSnapshot",
),
2026-07-29 06:26:40 -04:00
"docforge.policy": (
"EffectivePolicyV1",
"capability_mode",
"compose_effective_policy",
),
"docforge.projection_policy": (
"ProjectionPolicyV2",
"compose_projection_policy",
"validate_live_viewer_projection_mode",
"validate_manual_projection_mode",
"validate_portable_graph_projection_mode",
),
"docforge.projection_fragments": (
"FragmentKey",
"FragmentRecord",
"ProjectionFragmentCache",
"fragment_semantic_hash",
),
"docforge.projection_contract": (
"GraphViewPlanV1",
"ManualRenderPlanV1",
"ProjectionArtifact",
"ProjectionPackageV1",
"ProjectionReceiptV1",
"ProjectionRenderResult",
"canonical_projection_bytes",
"projection_hash",
),
"docforge.projection_worker": ("render_projection_in_worker",),
"docforge.reference_config": (
"REFERENCE_ADAPTER_CONFIG",
"REFERENCE_ADAPTER_SCHEMA_VERSION",
"REFERENCE_LANGUAGES",
"ReferenceAdapterConfigV1",
"load_reference_adapter_config",
),
"docforge.reference_mcp": (
"REFERENCE_MCP_MODULE",
"create_reference_project",
"create_reference_server",
),
2026-07-29 07:10:18 -04:00
"docforge.retrieval": (
"ContextCapsuleV1",
"RetrievalPlanV1",
"build_retrieval_plan",
"relation_category",
"validate_retrieval_plan",
),
"docforge.render_contract": (
"GenericHtmlRenderer",
"PreparedRender",
"Renderer",
"renderer_for",
),
"docforge_renderers.manual": ("ManualHtmlRenderer",),
2026-07-29 11:37:33 -04:00
"docforge_renderers.graph": ("PortableGraphHtmlRenderer",),
}
EXPECTED_ENTRY_POINTS = {
"docforge": "docforge.cli:main",
"docforge-mcp": "docforge.mcp_server:main",
"docforge-viewer-manager": "docforge.viewer_manager:main",
}
EXPECTED_CLI_COMMANDS = {
"apply",
"backlinks",
"build",
"check",
"configure",
"context",
"dependencies",
"doctor",
"filter",
"generation-diff",
2026-07-29 11:37:33 -04:00
"graph-plan",
"graph-render",
"graph-render-status",
"impact",
"info",
"onboard",
"preview",
"reindex",
"render",
"render-status",
"search",
"show",
"sync",
"validate",
"validate-index",
"visualization-status",
"visualization-stop",
"visualize",
}
EXPECTED_MCP_TOOLS = {
"docforge_abandon_changeset",
"docforge_apply_changeset",
"docforge_backlinks",
"docforge_bootstrap",
"docforge_create_changeset",
"docforge_dependencies",
"docforge_filter_nodes",
"docforge_get_changeset",
"docforge_get_changeset_diff",
"docforge_get_context",
"docforge_get_generation_diff",
2026-07-29 07:10:18 -04:00
"docforge_get_task_context",
"docforge_get_contract",
"docforge_get_logic",
"docforge_get_node",
"docforge_impact",
"docforge_list_changesets",
"docforge_preview_changeset",
"docforge_project_info",
"docforge_propose_node_create",
"docforge_propose_node_delete",
"docforge_propose_node_move",
"docforge_propose_node_update",
"docforge_propose_relationship_update",
"docforge_rebase_changeset",
"docforge_register_changes",
"docforge_render_status",
"docforge_graph_plan",
"docforge_graph_render_status",
"docforge_search",
"docforge_stop_visualization",
"docforge_sync",
"docforge_validate_changeset",
"docforge_validate_project",
"docforge_visualization_status",
"docforge_visualize",
}
class PublicContractTests(unittest.TestCase):
def copy_fixture(self, destination: Path) -> Path:
root = destination / "alpha"
shutil.copytree(FIXTURES / "alpha", root)
return root
@staticmethod
def schema(name: str) -> dict[str, object]:
return json.loads((SCHEMAS / name).read_text(encoding="utf-8"))
def test_distribution_version_entry_points_and_imports_are_stable(self) -> None:
project = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8"))["project"]
self.assertEqual("docforge", project["name"])
self.assertEqual(docforge.__version__, project["version"])
self.assertEqual(docforge.__version__, SERVER_VERSION)
scripts = project["scripts"]
for name, target in EXPECTED_ENTRY_POINTS.items():
self.assertEqual(target, scripts[name])
for module_name, names in PUBLIC_IMPORTS.items():
module = importlib.import_module(module_name)
for name in names:
with self.subTest(module=module_name, name=name):
self.assertTrue(hasattr(module, name))
def test_cli_and_mcp_names_remain_additively_compatible(self) -> None:
parser = _parser()
commands = next(
action for action in parser._actions if isinstance(action, argparse._SubParsersAction)
)
self.assertLessEqual(EXPECTED_CLI_COMMANDS, set(commands.choices))
self.assertLessEqual(
EXPECTED_MCP_TOOLS,
set((*ALL_TOOLS, *APPLICATION_TOOLS)),
)
self.assertEqual((*READ_TOOLS, *PROPOSAL_TOOLS), ALL_TOOLS)
completed = subprocess.run(
[sys.executable, "-m", "docforge.mcp_server", "--help"],
cwd=ROOT,
check=True,
capture_output=True,
text=True,
)
self.assertIn("--project-root", completed.stdout)
self.assertIn("--no-ast", completed.stdout)
self.assertIn("--manual-render-policy", completed.stdout)
self.assertIn("--portable-graph-policy", completed.stdout)
self.assertIn("--live-viewer-policy", completed.stdout)
def test_published_schemas_validate_their_current_contract_examples(self) -> None:
for path in sorted(SCHEMAS.glob("*.json")):
with self.subTest(schema=path.name):
Draft202012Validator.check_schema(json.loads(path.read_text(encoding="utf-8")))
descriptor = tomllib.loads(
(FIXTURES / "alpha" / ".docforge" / "project.toml").read_text(encoding="utf-8")
)
Draft202012Validator(self.schema("project.schema.json")).validate(descriptor)
with tempfile.TemporaryDirectory() as directory:
root = self.copy_fixture(Path(directory))
project = Project.open(root)
snapshot = project.load()
node = snapshot.nodes[0]
node_metadata = {
"schema_version": 1,
"id": node.node_id,
"title": node.title,
"family": node.family,
"authority": node.authority,
"status": node.status,
"tags": list(node.tags),
"summary": node.summary,
}
Draft202012Validator(self.schema("node.schema.json")).validate(node_metadata)
edge = snapshot.edges[0]
Draft202012Validator(self.schema("edge.schema.json")).validate(
{
"source_id": edge.source_id,
"relation": edge.relation,
"target_id": edge.target_id,
}
)
store = ChangesetStore(project, "alpha-editor")
store.create("public-contract")
changeset = json.loads(
(root / ".docforge" / "changesets" / "public-contract.json").read_text(
encoding="utf-8"
)
)
Draft202012Validator(self.schema("changeset.schema.json")).validate(changeset)
service = DocForgeService(project)
ProjectIndex(project).build()
success = service.project_info()
error = service.invoke(lambda: service.index.get_node("missing.public-contract-node"))
result_validator = Draft202012Validator(self.schema("result.schema.json"))
result_validator.validate(success)
result_validator.validate(error)
generation_diff = service.generation_diff(limit=1)
result_validator.validate(generation_diff)
Draft202012Validator(self.schema("generation-diff-page.schema.json")).validate(
{
"generation_diff": generation_diff["generation_diff"],
"pagination": generation_diff["pagination"],
}
)
def test_changeset_hash_is_exact_canonical_json_sha256(self) -> None:
document = {
"schema_version": 1,
"changeset_id": "hash-contract",
"project_id": "alpha-docs",
"root_fingerprint": "0" * 16,
"base_revision": "unversioned",
"base_source_hash": "1" * 64,
"creator": "alpha-editor",
"operations": [],
}
expected = hashlib.sha256(canonical_bytes(document)).hexdigest()
self.assertEqual(expected, document_hash(document))
if __name__ == "__main__":
unittest.main()