402 lines
13 KiB
Python
402 lines
13 KiB
Python
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",
|
|
"AdapterConformanceReport",
|
|
"AdapterEdge",
|
|
"AdapterImplementation",
|
|
"AdapterLoader",
|
|
"AdapterManifest",
|
|
"AdapterNode",
|
|
"AdapterProject",
|
|
"AdapterProjection",
|
|
"AdapterProjectSettings",
|
|
"AdapterSource",
|
|
"AdapterSourceProjection",
|
|
"CompleteAdapterAssemblyLoader",
|
|
"IncrementalAdapterAssembler",
|
|
"IncrementalAdapterLoader",
|
|
"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",
|
|
),
|
|
"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",
|
|
"build_graph_projection_package",
|
|
"build_graph_view_plan",
|
|
),
|
|
"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",
|
|
),
|
|
"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",
|
|
),
|
|
"docforge.retrieval": (
|
|
"ContextCapsuleV1",
|
|
"RetrievalPlanV1",
|
|
"build_retrieval_plan",
|
|
"relation_category",
|
|
"validate_retrieval_plan",
|
|
),
|
|
"docforge.render_contract": (
|
|
"GenericHtmlRenderer",
|
|
"PreparedRender",
|
|
"Renderer",
|
|
"renderer_for",
|
|
),
|
|
"docforge_renderers.manual": ("ManualHtmlRenderer",),
|
|
"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",
|
|
"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",
|
|
"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:
|
|
configuration = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8"))
|
|
project = configuration["project"]
|
|
self.assertEqual("docforge", project["name"])
|
|
self.assertEqual(["version"], project["dynamic"])
|
|
self.assertEqual(
|
|
"src/docforge/_version.py",
|
|
configuration["tool"]["hatch"]["version"]["path"],
|
|
)
|
|
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))
|
|
|
|
version_surfaces = {
|
|
"docforge.cli": "docforge 2.0.0\n",
|
|
"docforge.mcp_server": "docforge-mcp 2.0.0\n",
|
|
"docforge.reference_mcp": "python -m docforge.reference_mcp 2.0.0\n",
|
|
"docforge.viewer_manager": "docforge-viewer-manager 2.0.0\n",
|
|
}
|
|
for module_name, expected in version_surfaces.items():
|
|
with self.subTest(module=module_name):
|
|
completed = subprocess.run(
|
|
[sys.executable, "-m", module_name, "--version"],
|
|
cwd=ROOT,
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
self.assertEqual(expected, completed.stdout)
|
|
|
|
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()
|