Make project MCP workflows self-synchronizing
This commit is contained in:
parent
a30f021a52
commit
73165c9f51
17 changed files with 1124 additions and 56 deletions
|
|
@ -69,7 +69,7 @@ class DocForgeMcpTests(unittest.IsolatedAsyncioTestCase):
|
|||
|
||||
names = tuple(tool.name for tool in response.tools)
|
||||
self.assertEqual(ALL_TOOLS, names)
|
||||
self.assertEqual(11, len(PROPOSAL_TOOLS))
|
||||
self.assertEqual(14, len(PROPOSAL_TOOLS))
|
||||
self.assertFalse(
|
||||
any(
|
||||
token in name
|
||||
|
|
@ -98,6 +98,8 @@ class DocForgeMcpTests(unittest.IsolatedAsyncioTestCase):
|
|||
("docforge_visualize", {"node_id": "guide.workflow", "depth": 1}),
|
||||
("docforge_stop_visualization", {}),
|
||||
("docforge_visualization_status", {}),
|
||||
("docforge_bootstrap", {}),
|
||||
("docforge_sync", {}),
|
||||
)
|
||||
with self.running_manager(Path(directory) / "viewer-manager.json"):
|
||||
service = DocForgeService(Project.open(root))
|
||||
|
|
@ -143,7 +145,113 @@ class DocForgeMcpTests(unittest.IsolatedAsyncioTestCase):
|
|||
self.assertLessEqual(context["estimated_tokens"], 180)
|
||||
self.assertTrue(context["omissions"])
|
||||
|
||||
async def test_missing_node_and_stale_index_are_structured_failures(self) -> None:
|
||||
async def test_sync_register_rebase_apply_and_lifecycle_are_one_bound_workflow(
|
||||
self,
|
||||
) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = self.copy_fixture("alpha", Path(directory))
|
||||
project = Project.open(root)
|
||||
ProjectIndex(project).build()
|
||||
async with create_connected_server_and_client_session(
|
||||
create_server(
|
||||
root,
|
||||
"alpha-editor",
|
||||
canonical_applier_id="alpha-editor",
|
||||
),
|
||||
raise_exceptions=True,
|
||||
) as session:
|
||||
bootstrap = await session.call_tool("docforge_bootstrap", {})
|
||||
self.assertEqual("current", bootstrap.structuredContent["staleness"])
|
||||
self.assertEqual(
|
||||
str(root),
|
||||
bootstrap.structuredContent["binding"]["project_root"],
|
||||
)
|
||||
|
||||
proof = root / "docs/content/proof.toml"
|
||||
proof.write_text(
|
||||
proof.read_text(encoding="utf-8") + "\n# Current validation evidence.\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
synchronized = await session.call_tool("docforge_sync", {})
|
||||
self.assertEqual(
|
||||
"rebuilt",
|
||||
synchronized.structuredContent["synchronization"]["action"],
|
||||
)
|
||||
|
||||
registered = await session.call_tool(
|
||||
"docforge_register_changes",
|
||||
{
|
||||
"changeset_id": "bound-workflow",
|
||||
"operations": [
|
||||
{
|
||||
"operation": "update",
|
||||
"node_id": "guide.workflow",
|
||||
"metadata": {
|
||||
"summary": "Registered and applied in one bound workflow."
|
||||
},
|
||||
"rationale": "Verify atomic registration without caller hashes.",
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
self.assertTrue(registered.structuredContent["ready_for_review"])
|
||||
self.assertEqual("ready", registered.structuredContent["lifecycle"])
|
||||
|
||||
foundation = root / "docs/content/foundation.md"
|
||||
foundation.write_text(
|
||||
foundation.read_text(encoding="utf-8") + "\nUnrelated current fact.\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
rebased = await session.call_tool(
|
||||
"docforge_rebase_changeset",
|
||||
{
|
||||
"changeset_id": "bound-workflow",
|
||||
"expected_changeset_hash": registered.structuredContent["changeset_hash"],
|
||||
},
|
||||
)
|
||||
self.assertTrue(rebased.structuredContent["rebased"])
|
||||
|
||||
difference = await session.call_tool(
|
||||
"docforge_get_changeset_diff",
|
||||
{"changeset_id": "bound-workflow"},
|
||||
)
|
||||
self.assertEqual("ok", difference.structuredContent["status"])
|
||||
applied = await session.call_tool(
|
||||
"docforge_apply_changeset",
|
||||
{
|
||||
"changeset_id": "bound-workflow",
|
||||
"expected_changeset_hash": rebased.structuredContent["changeset_hash"],
|
||||
},
|
||||
)
|
||||
self.assertEqual(
|
||||
"applied",
|
||||
applied.structuredContent["lifecycle"]["status"],
|
||||
)
|
||||
closed = await session.call_tool(
|
||||
"docforge_rebase_changeset",
|
||||
{
|
||||
"changeset_id": "bound-workflow",
|
||||
"expected_changeset_hash": rebased.structuredContent["changeset_hash"],
|
||||
},
|
||||
)
|
||||
active = await session.call_tool("docforge_list_changesets", {})
|
||||
history = await session.call_tool(
|
||||
"docforge_list_changesets",
|
||||
{"include_history": True, "status": "applied"},
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
"changeset_closed",
|
||||
closed.structuredContent["error"]["code"],
|
||||
)
|
||||
self.assertEqual(0, active.structuredContent["count"])
|
||||
self.assertEqual(1, history.structuredContent["count"])
|
||||
self.assertEqual(
|
||||
"applied",
|
||||
history.structuredContent["changesets"][0]["lifecycle"]["status"],
|
||||
)
|
||||
|
||||
async def test_missing_node_fails_and_stale_index_self_heals(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = self.copy_fixture("alpha", Path(directory))
|
||||
ProjectIndex(Project.open(root)).build()
|
||||
|
|
@ -159,16 +267,22 @@ class DocForgeMcpTests(unittest.IsolatedAsyncioTestCase):
|
|||
workflow.read_text(encoding="utf-8") + "\nChanged after startup.\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
stale = await session.call_tool("docforge_get_node", {"node_id": "guide.workflow"})
|
||||
repaired = await session.call_tool(
|
||||
"docforge_get_node", {"node_id": "guide.workflow"}
|
||||
)
|
||||
|
||||
self.assertEqual("missing_node", missing.structuredContent["error"]["code"])
|
||||
self.assertEqual("stale_index", stale.structuredContent["error"]["code"])
|
||||
self.assertEqual("ok", repaired.structuredContent["status"])
|
||||
self.assertEqual("current", missing.structuredContent["staleness"])
|
||||
self.assertEqual("stale", stale.structuredContent["staleness"])
|
||||
self.assertEqual("current", repaired.structuredContent["staleness"])
|
||||
self.assertTrue(missing.structuredContent["source_hash"])
|
||||
self.assertTrue(stale.structuredContent["source_hash"])
|
||||
self.assertTrue(repaired.structuredContent["source_hash"])
|
||||
self.assertEqual(
|
||||
"rebuilt",
|
||||
repaired.structuredContent["synchronization"]["action"],
|
||||
)
|
||||
self.assertFalse(missing.isError)
|
||||
self.assertFalse(stale.isError)
|
||||
self.assertFalse(repaired.isError)
|
||||
|
||||
async def test_output_limit_fails_without_returning_partial_content(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue