Establish Milestone 0 compatibility and quality gates
This commit is contained in:
parent
15a913003c
commit
8ebb78a71d
15 changed files with 1114 additions and 27 deletions
270
tests/test_public_contract.py
Normal file
270
tests/test_public_contract.py
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
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",
|
||||
"AdapterEdge",
|
||||
"AdapterImplementation",
|
||||
"AdapterLoader",
|
||||
"AdapterManifest",
|
||||
"AdapterNode",
|
||||
"AdapterProject",
|
||||
"AdapterProjection",
|
||||
"AdapterProjectSettings",
|
||||
"AdapterSource",
|
||||
"AdapterSourceProjection",
|
||||
"IncrementalAdapterAssembler",
|
||||
"IncrementalAdapterLoader",
|
||||
),
|
||||
"docforge.application": (
|
||||
"CanonicalApplier",
|
||||
"CanonicalApplicationService",
|
||||
"GenericCanonicalApplier",
|
||||
),
|
||||
"docforge.index": ("ProjectIndex",),
|
||||
"docforge.mcp_server": (
|
||||
"create_project_server",
|
||||
"create_read_only_server",
|
||||
"create_server",
|
||||
),
|
||||
"docforge.models": (
|
||||
"Edge",
|
||||
"LogicEdge",
|
||||
"LogicNode",
|
||||
"LogicProjection",
|
||||
"Node",
|
||||
"ProjectDescriptor",
|
||||
"ProjectService",
|
||||
"ProjectSnapshot",
|
||||
),
|
||||
"docforge.render_contract": (
|
||||
"GenericHtmlRenderer",
|
||||
"PreparedRender",
|
||||
"Renderer",
|
||||
"renderer_for",
|
||||
),
|
||||
}
|
||||
|
||||
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",
|
||||
"context",
|
||||
"dependencies",
|
||||
"filter",
|
||||
"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_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_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)
|
||||
|
||||
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)
|
||||
|
||||
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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue