Add explicit cross-identity proposal acceptance
This commit is contained in:
parent
377cca0531
commit
7b21541ab3
10 changed files with 247 additions and 8 deletions
|
|
@ -328,6 +328,103 @@ class DocForgeChangesetTests(unittest.TestCase):
|
|||
self.assertFalse((root / ".docforge/changesets/.state/update-race.json").exists())
|
||||
self.assertFalse(tuple(target.parent.glob(".docforge-apply-*")))
|
||||
|
||||
def test_explicit_applier_accepts_a_configured_contributor_changeset(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")
|
||||
+ """
|
||||
|
||||
[[changesets.writers]]
|
||||
id = "contributor"
|
||||
families = ["guide"]
|
||||
operations = ["update"]
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
project = Project.open(root)
|
||||
proposal = ChangesetStore(project, "contributor").register(
|
||||
"contributor-update",
|
||||
[
|
||||
{
|
||||
"operation": "update",
|
||||
"node_id": "guide.workflow",
|
||||
"metadata": {"summary": "Accepted from a configured contributor."},
|
||||
"rationale": "Prove explicit cross-identity acceptance.",
|
||||
}
|
||||
],
|
||||
)
|
||||
default_service = CanonicalApplicationService(
|
||||
project,
|
||||
applier_id="alpha-editor",
|
||||
applier=GenericCanonicalApplier(project),
|
||||
)
|
||||
|
||||
with self.assertRaises(DocForgeError) as denied:
|
||||
default_service.apply(
|
||||
"contributor-update",
|
||||
str(proposal["changeset_hash"]),
|
||||
)
|
||||
|
||||
self.assertEqual("changeset_owner_conflict", denied.exception.code)
|
||||
service = CanonicalApplicationService(
|
||||
project,
|
||||
applier_id="alpha-editor",
|
||||
applier=GenericCanonicalApplier(project),
|
||||
accepted_proposal_writers=("alpha-editor", "contributor"),
|
||||
)
|
||||
result = service.apply(
|
||||
"contributor-update",
|
||||
str(proposal["changeset_hash"]),
|
||||
)
|
||||
|
||||
self.assertTrue(result["applied"])
|
||||
self.assertEqual("contributor", result["proposal_creator"])
|
||||
self.assertEqual("alpha-editor", result["applied_by"])
|
||||
self.assertEqual(
|
||||
["alpha-editor", "contributor"],
|
||||
service.access()["accepted_proposal_writers"],
|
||||
)
|
||||
workflow = next(
|
||||
node for node in project.load().nodes if node.node_id == "guide.workflow"
|
||||
)
|
||||
self.assertEqual(
|
||||
"Accepted from a configured contributor.",
|
||||
workflow.summary,
|
||||
)
|
||||
|
||||
def test_application_rejects_unknown_or_duplicate_accepted_writers(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = self.copy_fixture(Path(directory))
|
||||
project = Project.open(root)
|
||||
|
||||
with self.assertRaises(DocForgeError) as unknown:
|
||||
CanonicalApplicationService(
|
||||
project,
|
||||
applier_id="alpha-editor",
|
||||
applier=GenericCanonicalApplier(project),
|
||||
accepted_proposal_writers=("missing-writer",),
|
||||
)
|
||||
with self.assertRaises(DocForgeError) as duplicate:
|
||||
CanonicalApplicationService(
|
||||
project,
|
||||
applier_id="alpha-editor",
|
||||
applier=GenericCanonicalApplier(project),
|
||||
accepted_proposal_writers=("alpha-editor", "alpha-editor"),
|
||||
)
|
||||
with self.assertRaises(DocForgeError) as disabled:
|
||||
CanonicalApplicationService(
|
||||
project,
|
||||
applier_id=None,
|
||||
applier=None,
|
||||
accepted_proposal_writers=("alpha-editor",),
|
||||
)
|
||||
|
||||
self.assertEqual("invalid_application_policy", unknown.exception.code)
|
||||
self.assertEqual("invalid_application_policy", duplicate.exception.code)
|
||||
self.assertEqual("invalid_application_policy", disabled.exception.code)
|
||||
|
||||
def test_canonical_create_and_delete_races_preserve_foreign_targets(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
parent = Path(directory)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ from mcp import ClientSession, StdioServerParameters
|
|||
from mcp.client.stdio import stdio_client
|
||||
from mcp.shared.memory import create_connected_server_and_client_session
|
||||
|
||||
from docforge.application import GenericCanonicalApplier
|
||||
from docforge.changesets import ChangesetStore
|
||||
from docforge.errors import DocForgeError
|
||||
from docforge.index import ProjectIndex
|
||||
|
|
@ -29,6 +30,7 @@ from docforge.mcp_server import (
|
|||
SERVER_VERSION,
|
||||
DocForgeService,
|
||||
_create_bound_server,
|
||||
create_project_server,
|
||||
create_server,
|
||||
)
|
||||
from docforge.project import Project, project_root_fingerprint
|
||||
|
|
@ -1446,6 +1448,83 @@ class DocForgeMcpTests(unittest.IsolatedAsyncioTestCase):
|
|||
self.assertEqual("Applied through the gated MCP tool.", workflow.summary)
|
||||
self.assertTrue((root / ".docforge/rendered/manual.html").is_file())
|
||||
|
||||
async def test_project_server_accepts_an_explicit_contributor_without_granting_apply(
|
||||
self,
|
||||
) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = self.copy_fixture("alpha", Path(directory))
|
||||
descriptor = root / ".docforge/project.toml"
|
||||
descriptor.write_text(
|
||||
descriptor.read_text(encoding="utf-8")
|
||||
+ """
|
||||
|
||||
[[changesets.writers]]
|
||||
id = "contributor"
|
||||
families = ["guide"]
|
||||
operations = ["update"]
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
project = Project.open(root)
|
||||
ProjectIndex(project).build()
|
||||
async with create_connected_server_and_client_session(
|
||||
create_server(
|
||||
root,
|
||||
"contributor",
|
||||
capability_mode="proposal",
|
||||
),
|
||||
raise_exceptions=True,
|
||||
) as contributor:
|
||||
contributor_tools = tuple(
|
||||
tool.name for tool in (await contributor.list_tools()).tools
|
||||
)
|
||||
registered = await contributor.call_tool(
|
||||
"docforge_register_changes",
|
||||
{
|
||||
"changeset_id": "accepted-contribution",
|
||||
"operations": [
|
||||
{
|
||||
"operation": "update",
|
||||
"node_id": "guide.workflow",
|
||||
"metadata": {"summary": "Accepted through a separate applier."},
|
||||
"rationale": "Prove explicit contributor acceptance over MCP.",
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
async with create_connected_server_and_client_session(
|
||||
create_project_server(
|
||||
project,
|
||||
proposal_writer="alpha-editor",
|
||||
canonical_applier_id="alpha-editor",
|
||||
canonical_applier=GenericCanonicalApplier(project),
|
||||
accepted_proposal_writers=("contributor",),
|
||||
capability_mode="application",
|
||||
),
|
||||
raise_exceptions=True,
|
||||
) as developer:
|
||||
contract = await developer.call_tool("docforge_get_contract", {})
|
||||
applied = await developer.call_tool(
|
||||
"docforge_apply_changeset",
|
||||
{
|
||||
"changeset_id": "accepted-contribution",
|
||||
"expected_changeset_hash": registered.structuredContent["changeset_hash"],
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(ALL_TOOLS, contributor_tools)
|
||||
self.assertNotIn("docforge_apply_changeset", contributor_tools)
|
||||
self.assertEqual(
|
||||
["contributor"],
|
||||
contract.structuredContent["canonical_application_access"][
|
||||
"accepted_proposal_writers"
|
||||
],
|
||||
)
|
||||
self.assertTrue(applied.structuredContent["applied"])
|
||||
self.assertEqual("contributor", applied.structuredContent["proposal_creator"])
|
||||
self.assertEqual("alpha-editor", applied.structuredContent["applied_by"])
|
||||
|
||||
async def test_stdio_transport_serves_the_same_project_bound_contract(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = self.copy_fixture("beta", Path(directory))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue