Complete independent projection runtime
This commit is contained in:
parent
1134c2d375
commit
f1fabaf0ca
38 changed files with 4907 additions and 87 deletions
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
|
|
@ -18,6 +19,7 @@ from jsonschema import Draft202012Validator
|
|||
from mcp import ClientSession, StdioServerParameters
|
||||
from mcp.client.stdio import stdio_client
|
||||
|
||||
from docforge.changeset_contract import document_hash
|
||||
from docforge.cli import _parser, _run, main
|
||||
from docforge.client_config import (
|
||||
_read_existing,
|
||||
|
|
@ -39,6 +41,19 @@ CONFIGURATION_SCHEMA = json.loads(
|
|||
DOCTOR_SCHEMA = json.loads((SCHEMAS / "doctor-result.schema.json").read_text(encoding="utf-8"))
|
||||
POLICY_SCHEMA = json.loads((SCHEMAS / "policy.schema.json").read_text(encoding="utf-8"))
|
||||
|
||||
GRAPH_RENDER_CONFIG = """
|
||||
|
||||
[graph_render]
|
||||
output_root = ".docforge/portable-graph"
|
||||
|
||||
[[graph_render.views]]
|
||||
id = "architecture"
|
||||
renderer = "portable_graph_html"
|
||||
output = "architecture.html"
|
||||
title = "Alpha architecture"
|
||||
root = "guide.workflow"
|
||||
"""
|
||||
|
||||
|
||||
class ClientIntegrationTests(unittest.TestCase):
|
||||
def copy_fixture(self, destination: Path) -> Path:
|
||||
|
|
@ -156,6 +171,256 @@ class ClientIntegrationTests(unittest.TestCase):
|
|||
)
|
||||
self.assertEqual("invalid_capability_binding", escalated.exception.code)
|
||||
|
||||
def test_nondefault_projection_policy_selectors_serialize_and_validate(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = self.copy_fixture(Path(directory))
|
||||
descriptor = root / ".docforge" / "project.toml"
|
||||
descriptor.write_text(
|
||||
descriptor.read_text(encoding="utf-8") + GRAPH_RENDER_CONFIG,
|
||||
encoding="utf-8",
|
||||
)
|
||||
project = Project.open(root)
|
||||
expected_arguments = [
|
||||
"-I",
|
||||
"-m",
|
||||
"docforge.mcp_server",
|
||||
"--project-root",
|
||||
str(root),
|
||||
"--capability-mode",
|
||||
"read",
|
||||
"--manual-render-policy",
|
||||
"disabled",
|
||||
"--portable-graph-policy",
|
||||
"disabled",
|
||||
"--live-viewer-policy",
|
||||
"disabled",
|
||||
]
|
||||
for client in ("codex", "claude", "openclaw"):
|
||||
with self.subTest(client=client):
|
||||
result = generate_client_configuration(
|
||||
project,
|
||||
client,
|
||||
manual_render_policy="disabled",
|
||||
portable_graph_policy="disabled",
|
||||
live_viewer_policy="disabled",
|
||||
)
|
||||
Draft202012Validator(CONFIGURATION_SCHEMA).validate(result)
|
||||
_validate_configuration_result(result)
|
||||
self.assertEqual(
|
||||
{
|
||||
"schema_version": 2,
|
||||
"manual": "disabled",
|
||||
"portable_graph": "disabled",
|
||||
"live_viewer": "disabled",
|
||||
},
|
||||
result["projection_policy"],
|
||||
)
|
||||
self.assertEqual(expected_arguments, result["binding"]["args"])
|
||||
content = result["artifact"]["content"]
|
||||
if client == "codex":
|
||||
document = tomllib.loads(content)
|
||||
serialized = document["mcp_servers"][result["server_name"]]["args"]
|
||||
elif client == "claude":
|
||||
document = json.loads(content)
|
||||
serialized = document["mcpServers"][result["server_name"]]["args"]
|
||||
else:
|
||||
document = json.loads(content)
|
||||
serialized = document["mcp"]["servers"][result["server_name"]]["args"]
|
||||
self.assertEqual(expected_arguments, serialized)
|
||||
canonical = json.dumps(
|
||||
result["projection_policy"],
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
ensure_ascii=False,
|
||||
).encode("utf-8")
|
||||
self.assertEqual(
|
||||
hashlib.sha256(canonical).hexdigest(),
|
||||
result["projection_policy_hash"],
|
||||
)
|
||||
|
||||
def test_projection_policy_schema_and_configuration_validation_reject_drift(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
project = Project.open(self.copy_fixture(Path(directory)))
|
||||
result = generate_client_configuration(
|
||||
project,
|
||||
"codex",
|
||||
manual_render_policy="disabled",
|
||||
live_viewer_policy="disabled",
|
||||
)
|
||||
validator = Draft202012Validator(CONFIGURATION_SCHEMA)
|
||||
validator.validate(result)
|
||||
_validate_configuration_result(result)
|
||||
|
||||
for field, value in (
|
||||
("schema_version", 1),
|
||||
("manual", "on-demand"),
|
||||
("portable_graph", "auto"),
|
||||
("live_viewer", "explicit"),
|
||||
):
|
||||
with self.subTest(field=field):
|
||||
drifted = json.loads(json.dumps(result))
|
||||
drifted["projection_policy"][field] = value
|
||||
self.assertTrue(list(validator.iter_errors(drifted)))
|
||||
|
||||
missing = json.loads(json.dumps(result))
|
||||
missing.pop("projection_policy")
|
||||
self.assertTrue(list(validator.iter_errors(missing)))
|
||||
extra = json.loads(json.dumps(result))
|
||||
extra["projection_policy"]["project_path"] = "/private/project"
|
||||
self.assertTrue(list(validator.iter_errors(extra)))
|
||||
|
||||
mismatched = json.loads(json.dumps(result))
|
||||
mismatched["projection_policy"]["manual"] = "explicit"
|
||||
canonical = json.dumps(
|
||||
mismatched["projection_policy"],
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
ensure_ascii=False,
|
||||
).encode("utf-8")
|
||||
mismatched["projection_policy_hash"] = hashlib.sha256(canonical).hexdigest()
|
||||
with self.assertRaises(AssertionError):
|
||||
_validate_configuration_result(mismatched)
|
||||
|
||||
bad_hash = json.loads(json.dumps(result))
|
||||
bad_hash["projection_policy_hash"] = "0" * 64
|
||||
with self.assertRaises(AssertionError):
|
||||
_validate_configuration_result(bad_hash)
|
||||
|
||||
defaulted = generate_client_configuration(project, "codex")
|
||||
self.assertNotIn(
|
||||
"--manual-render-policy",
|
||||
defaulted["binding"]["args"],
|
||||
)
|
||||
defaulted["projection_policy"]["manual"] = "disabled"
|
||||
canonical = json.dumps(
|
||||
defaulted["projection_policy"],
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
ensure_ascii=False,
|
||||
).encode("utf-8")
|
||||
defaulted["projection_policy_hash"] = hashlib.sha256(canonical).hexdigest()
|
||||
with self.assertRaises(AssertionError):
|
||||
_validate_configuration_result(defaulted)
|
||||
|
||||
unavailable = json.loads(json.dumps(result))
|
||||
unavailable["projection_availability"]["manual_configured"] = False
|
||||
with self.assertRaises(AssertionError):
|
||||
_validate_configuration_result(unavailable)
|
||||
|
||||
descriptor = project.descriptor.descriptor_path
|
||||
descriptor.write_text(
|
||||
descriptor.read_text(encoding="utf-8") + GRAPH_RENDER_CONFIG,
|
||||
encoding="utf-8",
|
||||
)
|
||||
graph_project = Project.open(project.descriptor.root)
|
||||
graph_defaulted = generate_client_configuration(graph_project, "codex")
|
||||
self.assertNotIn(
|
||||
"--portable-graph-policy",
|
||||
graph_defaulted["binding"]["args"],
|
||||
)
|
||||
coordinated_graph_drift = json.loads(json.dumps(graph_defaulted))
|
||||
coordinated_graph_drift["projection_policy"]["portable_graph"] = "disabled"
|
||||
coordinated_graph_drift["projection_availability"]["portable_graph_configured"] = False
|
||||
canonical = json.dumps(
|
||||
coordinated_graph_drift["projection_policy"],
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
ensure_ascii=False,
|
||||
).encode("utf-8")
|
||||
coordinated_graph_drift["projection_policy_hash"] = hashlib.sha256(
|
||||
canonical
|
||||
).hexdigest()
|
||||
coordinated_graph_drift["configuration_hash"] = document_hash(
|
||||
{
|
||||
"schema_version": 1,
|
||||
"client": coordinated_graph_drift["client"],
|
||||
"server_name": coordinated_graph_drift["server_name"],
|
||||
"project": coordinated_graph_drift["project"],
|
||||
"binding": coordinated_graph_drift["binding"],
|
||||
"effective_policy": coordinated_graph_drift["effective_policy"],
|
||||
"projection_policy": coordinated_graph_drift["projection_policy"],
|
||||
"projection_policy_hash": coordinated_graph_drift["projection_policy_hash"],
|
||||
"projection_availability": coordinated_graph_drift["projection_availability"],
|
||||
"artifact_format": coordinated_graph_drift["artifact"]["format"],
|
||||
"artifact_content_sha256": coordinated_graph_drift["artifact"][
|
||||
"content_sha256"
|
||||
],
|
||||
}
|
||||
)
|
||||
with self.assertRaises(AssertionError):
|
||||
_validate_configuration_result(coordinated_graph_drift)
|
||||
|
||||
invalid_selections = (
|
||||
{"manual_render_policy": "sometimes"},
|
||||
{"portable_graph_policy": "auto"},
|
||||
{"live_viewer_policy": "always"},
|
||||
)
|
||||
for selection in invalid_selections:
|
||||
with self.subTest(selection=selection):
|
||||
with self.assertRaises(DocForgeError) as raised:
|
||||
generate_client_configuration(graph_project, "codex", **selection)
|
||||
self.assertEqual("invalid_projection_policy", raised.exception.code)
|
||||
|
||||
def test_doctor_round_trips_projection_selectors_and_rejects_invalid_modes(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
parent = Path(directory)
|
||||
project = Project.open(self.copy_fixture(parent))
|
||||
ProjectIndex(project).build()
|
||||
config = parent / "openclaw.json"
|
||||
generated = generate_client_configuration(
|
||||
project,
|
||||
"openclaw",
|
||||
manual_render_policy="disabled",
|
||||
live_viewer_policy="disabled",
|
||||
output=config,
|
||||
)
|
||||
healthy = run_doctor(
|
||||
project,
|
||||
"openclaw",
|
||||
config_path=config,
|
||||
server_name=generated["server_name"],
|
||||
)
|
||||
Draft202012Validator(DOCTOR_SCHEMA).validate(healthy)
|
||||
self.assertEqual("healthy", healthy["doctor_state"])
|
||||
self.assertIn(
|
||||
"effective_policy_valid",
|
||||
[check["code"] for check in healthy["checks"]],
|
||||
)
|
||||
|
||||
document = json.loads(config.read_text(encoding="utf-8"))
|
||||
entry = document["mcp"]["servers"][generated["server_name"]]
|
||||
arguments = entry["args"]
|
||||
manual_position = arguments.index("--manual-render-policy") + 1
|
||||
arguments[manual_position] = "sometimes"
|
||||
config.write_text(json.dumps(document, sort_keys=True), encoding="utf-8")
|
||||
invalid = run_doctor(
|
||||
project,
|
||||
"openclaw",
|
||||
config_path=config,
|
||||
server_name=generated["server_name"],
|
||||
)
|
||||
Draft202012Validator(DOCTOR_SCHEMA).validate(invalid)
|
||||
self.assertEqual("unhealthy", invalid["doctor_state"])
|
||||
policy_check = next(
|
||||
check for check in invalid["checks"] if check["check_id"] == "policy.effective"
|
||||
)
|
||||
self.assertEqual("invalid_projection_policy", policy_check["code"])
|
||||
|
||||
arguments[manual_position] = "auto"
|
||||
config.write_text(json.dumps(document, sort_keys=True), encoding="utf-8")
|
||||
unavailable = run_doctor(
|
||||
project,
|
||||
"openclaw",
|
||||
config_path=config,
|
||||
server_name=generated["server_name"],
|
||||
)
|
||||
Draft202012Validator(DOCTOR_SCHEMA).validate(unavailable)
|
||||
self.assertEqual("unhealthy", unavailable["doctor_state"])
|
||||
policy_check = next(
|
||||
check for check in unavailable["checks"] if check["check_id"] == "policy.effective"
|
||||
)
|
||||
self.assertEqual("projection_policy_unavailable", policy_check["code"])
|
||||
|
||||
def test_explicit_fragment_write_is_atomic_conflict_aware_and_private(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
parent = Path(directory)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue