1
0
Fork 0
Code Issues Pull requests Projects Releases 2 Packages Wiki Activity Actions Pages

Make project MCP workflows self-synchronizing

This commit is contained in:
Andraxion 2026-07-26 09:32:25 -04:00
parent a30f021a52
commit 73165c9f51
17 changed files with 1124 additions and 56 deletions

View file

@ -1,9 +1,11 @@
from __future__ import annotations
import hashlib
import sqlite3
import tempfile
import unittest
from collections.abc import Mapping
from contextlib import closing
from dataclasses import replace
from pathlib import Path
@ -350,6 +352,38 @@ class AdapterContractTests(unittest.TestCase):
index.get_node("guide.workflow")["node"]["source_path"],
)
def test_fast_incremental_reads_reverify_a_changed_index_file(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory).resolve()
loader = IncrementalLoader(root)
project = AdapterProject(loader, cache_root=root / ".cache" / "incremental")
index = ProjectIndex(project)
index.build()
index.synchronize()
self.assertTrue(index.attestation_path.is_file())
fresh = ProjectIndex(project)
self.assertEqual(
"current",
fresh.synchronize()["synchronization"]["action"],
)
with closing(sqlite3.connect(index.path)) as connection:
connection.execute(
"UPDATE nodes SET content = ? WHERE node_id = ?",
("tampered", "guide.foundation"),
)
connection.commit()
with self.assertRaisesRegex(DocForgeError, "rows do not match metadata"):
index.get_node("guide.foundation")
repaired = index.synchronize()
self.assertEqual("rebuilt", repaired["synchronization"]["action"])
self.assertEqual(
"Foundation content.",
index.get_node("guide.foundation")["node"]["content"],
)
def test_incremental_delete_failure_and_equivalence_are_safe(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory).resolve()

View file

@ -244,6 +244,8 @@ class DocForgeChangesetTests(unittest.TestCase):
node_ids = {node.node_id for node in snapshot.nodes}
workflow = next(node for node in snapshot.nodes if node.node_id == "guide.workflow")
self.assertTrue(result["applied"])
self.assertEqual("applied", result["lifecycle"]["status"])
self.assertEqual("ok", result["derived_refresh"]["status"])
self.assertEqual(
[
"docs/content/applied.md",
@ -265,8 +267,95 @@ class DocForgeChangesetTests(unittest.TestCase):
self.assertTrue((root / ".docforge/cache/index.sqlite3").is_file())
self.assertTrue((root / ".docforge/rendered/manual.html").is_file())
with self.assertRaisesRegex(DocForgeError, "Canonical project changed"):
with self.assertRaisesRegex(DocForgeError, "cannot be modified") as closed:
service.apply("apply-all", str(final["changeset_hash"]))
self.assertEqual("changeset_closed", closed.exception.code)
def test_abandoned_proposal_releases_overlap_and_stale_work_remains_active(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = self.copy_fixture(Path(directory))
project = Project.open(root)
store = ChangesetStore(project, "alpha-editor")
first = store.register(
"first",
[
{
"operation": "update",
"node_id": "guide.foundation",
"metadata": {"summary": "Abandoned proposal."},
"rationale": "Reserve then release this node.",
}
],
)
store.abandon(
"first",
str(first["changeset_hash"]),
"The proposal is no longer wanted.",
)
second = store.register(
"second",
[
{
"operation": "update",
"node_id": "guide.foundation",
"metadata": {"summary": "Replacement proposal."},
"rationale": "Verify terminal proposals release conflicts.",
}
],
)
workflow = root / "docs/content/workflow.md"
workflow.write_text(
workflow.read_text(encoding="utf-8") + "\nUnrelated current fact.\n",
encoding="utf-8",
)
active = store.list_changesets(include_history=False)
stale = store.list_changesets(include_history=False, status="stale")
self.assertEqual(0, active["count"])
self.assertEqual(["second"], [item["changeset_id"] for item in stale["changesets"]])
self.assertEqual("stale", stale["changesets"][0]["lifecycle"]["status"])
self.assertEqual("ready", second["lifecycle"])
def test_applied_receipt_survives_a_derived_refresh_failure(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = self.copy_fixture(Path(directory))
project = Project.open(root)
registered = ChangesetStore(project, "alpha-editor").register(
"degraded-refresh",
[
{
"operation": "update",
"node_id": "guide.workflow",
"metadata": {"summary": "Canonical even if refresh fails."},
"rationale": "Separate canonical success from disposable refresh.",
}
],
)
service = CanonicalApplicationService(
project,
applier_id="alpha-editor",
applier=GenericCanonicalApplier(project),
)
with mock.patch.object(
service.index,
"build",
side_effect=DocForgeError("index_failure", "Synthetic derived failure"),
):
result = service.apply(
"degraded-refresh",
str(registered["changeset_hash"]),
)
self.assertTrue(result["applied"])
self.assertEqual("applied", result["lifecycle"]["status"])
self.assertEqual("degraded", result["derived_refresh"]["status"])
self.assertEqual("index", result["derived_refresh"]["errors"][0]["component"])
with self.assertRaisesRegex(DocForgeError, "cannot be modified"):
service.apply(
"degraded-refresh",
str(registered["changeset_hash"]),
)
def test_relationship_only_update_is_hash_bound_and_does_not_rewrite_node(self) -> None:
with tempfile.TemporaryDirectory() as directory:

View file

@ -31,6 +31,8 @@ class DocForgeCliTests(unittest.TestCase):
parser = _parser()
reindexed = _run(parser.parse_args(["--project-root", str(root), "reindex"]))
self.assertTrue(reindexed["reindexed"])
synchronized = _run(parser.parse_args(["--project-root", str(root), "sync"]))
self.assertEqual("current", synchronized["synchronization"]["action"])
project = Project.open(root)
store = ChangesetStore(project, "alpha-editor")

View file

@ -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: