Complete DFG-8 adapter proposal support
This commit is contained in:
parent
f1e31487c0
commit
591bf0ae7d
14 changed files with 525 additions and 75 deletions
|
|
@ -2,6 +2,7 @@ from __future__ import annotations
|
|||
|
||||
import tempfile
|
||||
import unittest
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
|
||||
|
|
@ -12,14 +13,27 @@ from docforge.adapter_contract import (
|
|||
AdapterNode,
|
||||
AdapterProject,
|
||||
AdapterProjection,
|
||||
AdapterProjectSettings,
|
||||
ShadowArtifact,
|
||||
compare_artifacts,
|
||||
validate_projection,
|
||||
)
|
||||
from docforge.errors import DocForgeError
|
||||
from docforge.index import ProjectIndex
|
||||
from docforge.mcp_server import READ_TOOLS, create_read_only_server
|
||||
from docforge.models import Edge, Node
|
||||
from docforge.mcp_server import (
|
||||
ALL_TOOLS,
|
||||
READ_TOOLS,
|
||||
create_project_server,
|
||||
create_read_only_server,
|
||||
)
|
||||
from docforge.models import (
|
||||
Edge,
|
||||
Node,
|
||||
ProjectSnapshot,
|
||||
ProposalWriter,
|
||||
RenderConfig,
|
||||
RenderView,
|
||||
)
|
||||
|
||||
|
||||
class Loader:
|
||||
|
|
@ -221,6 +235,110 @@ class AdapterReadOnlyMcpTests(unittest.IsolatedAsyncioTestCase):
|
|||
self.assertEqual([("fixture", 321)], calls)
|
||||
self.assertFalse(project.descriptor.changeset_root.exists())
|
||||
|
||||
async def test_adapter_project_proposals_require_explicit_policy_and_stay_isolated(
|
||||
self,
|
||||
) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory).resolve()
|
||||
(root / ".docforge").mkdir()
|
||||
descriptor = root / ".docforge" / "project.toml"
|
||||
descriptor.write_text("adapter fixture\n", encoding="utf-8")
|
||||
content_root = root / "docs"
|
||||
content_root.mkdir()
|
||||
sources = (content_root / "foundation.md", content_root / "workflow.md")
|
||||
for source in sources:
|
||||
source.write_text(f"canonical {source.stem}\n", encoding="utf-8")
|
||||
template_root = root / "templates"
|
||||
template_root.mkdir()
|
||||
template = template_root / "review.html"
|
||||
template.write_text(
|
||||
"<html><head><title>{{ docforge_title }}</title></head>"
|
||||
"<body>{{ docforge_content }}</body></html>\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
validation_calls: list[tuple[Mapping[str, object], ...]] = []
|
||||
|
||||
def validate_proposal(
|
||||
base: ProjectSnapshot,
|
||||
projected: ProjectSnapshot,
|
||||
operations: tuple[Mapping[str, object], ...],
|
||||
) -> None:
|
||||
self.assertEqual(base.edges, projected.edges)
|
||||
validation_calls.append(operations)
|
||||
|
||||
settings = AdapterProjectSettings(
|
||||
descriptor_path=descriptor,
|
||||
content_roots=(content_root,),
|
||||
canonical_sources=sources,
|
||||
changeset_root=root / ".cache" / "changesets",
|
||||
proposal_writers=(ProposalWriter("fixture-writer", ("guide",), ("update",)),),
|
||||
render=RenderConfig(
|
||||
template_root=template_root,
|
||||
preview_root=root / ".cache" / "previews",
|
||||
views=(
|
||||
RenderView(
|
||||
"review",
|
||||
"generic_html",
|
||||
template,
|
||||
root / ".cache" / "rendered" / "review.html",
|
||||
"Adapter review",
|
||||
("guide",),
|
||||
),
|
||||
),
|
||||
),
|
||||
proposal_validator=validate_proposal,
|
||||
)
|
||||
project = AdapterProject(
|
||||
Loader(AdapterContractTests().projection(root)),
|
||||
cache_root=root / ".cache" / "adapter",
|
||||
settings=settings,
|
||||
)
|
||||
ProjectIndex(project).build()
|
||||
before = {source: source.read_bytes() for source in sources}
|
||||
server = create_project_server(project, proposal_writer="fixture-writer")
|
||||
async with create_connected_server_and_client_session(
|
||||
server, raise_exceptions=True
|
||||
) as session:
|
||||
tools = await session.list_tools()
|
||||
created = await session.call_tool(
|
||||
"docforge_create_changeset", {"changeset_id": "adapter-update"}
|
||||
)
|
||||
node = await session.call_tool("docforge_get_node", {"node_id": "guide.workflow"})
|
||||
proposed = await session.call_tool(
|
||||
"docforge_propose_node_update",
|
||||
{
|
||||
"changeset_id": "adapter-update",
|
||||
"expected_changeset_hash": created.structuredContent["changeset_hash"],
|
||||
"node_id": "guide.workflow",
|
||||
"expected_content_hash": node.structuredContent["node"]["content_hash"],
|
||||
"metadata": None,
|
||||
"content": "Updated workflow content.",
|
||||
"relationship_changes": [],
|
||||
"rationale": "Prove adapter proposal policy.",
|
||||
},
|
||||
)
|
||||
validated = await session.call_tool(
|
||||
"docforge_validate_changeset",
|
||||
{"changeset_id": "adapter-update"},
|
||||
)
|
||||
diff = await session.call_tool(
|
||||
"docforge_get_changeset_diff",
|
||||
{"changeset_id": "adapter-update"},
|
||||
)
|
||||
preview = await session.call_tool(
|
||||
"docforge_preview_changeset",
|
||||
{"changeset_id": "adapter-update", "view_id": "review"},
|
||||
)
|
||||
|
||||
self.assertEqual(ALL_TOOLS, tuple(tool.name for tool in tools.tools))
|
||||
self.assertEqual("ok", proposed.structuredContent["status"])
|
||||
self.assertTrue(validated.structuredContent["valid"])
|
||||
self.assertEqual("update", diff.structuredContent["changes"][0]["operation"])
|
||||
preview_path = root / preview.structuredContent["preview"]["path"]
|
||||
self.assertTrue(preview_path.is_file())
|
||||
self.assertTrue(validation_calls)
|
||||
self.assertEqual(before, {source: source.read_bytes() for source in sources})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue