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

test: prove unrelated project isolation

This commit is contained in:
Andraxion 2026-07-22 05:01:07 -04:00
parent 1c4aa79d1c
commit 1943c36ef3
12 changed files with 492 additions and 12 deletions

View file

@ -0,0 +1,54 @@
schema_version = 1
project_id = "awesome-ski-game"
title = "Awesome Ski Game"
adapter = "generic"
[sources]
content_roots = ["handbook"]
authority_files = ["DOCUMENTATION_POLICY.md"]
[derived]
cache_root = ".docforge/cache"
index = ".docforge/cache/index.sqlite3"
[changesets]
root = ".docforge/changesets"
[[changesets.writers]]
id = "ski-editor"
families = ["proof", "riding", "safety", "session", "trail"]
operations = ["create", "update", "move", "delete"]
[render]
template_root = "site/templates"
preview_root = ".docforge/previews"
[[render.views]]
id = "field-guide"
renderer = "generic_html"
template = "field-guide.html"
output = ".docforge/rendered/field-guide.html"
title = "Awesome Ski Game Field Guide"
families = ["proof", "riding", "safety", "session", "trail"]
[graph]
allowed_relations = ["depends_on", "informs", "proves", "relates_to"]
[limits]
max_source_bytes = 100000
max_nodes = 100
max_query_chars = 200
max_results = 20
max_traversal_depth = 4
max_context_tokens = 2000
max_changesets = 20
max_changeset_operations = 20
max_changeset_bytes = 100000
[[profiles]]
id = "ride-day"
families = ["proof", "riding", "safety", "session", "trail"]
statuses = ["active", "current", "open", "ready", "verified"]
required_nodes = ["session.first-descent"]
token_budget = 900
dependency_depth = 3

View file

@ -0,0 +1,4 @@
# Documentation policy
Files under `handbook/` own Awesome Ski Game documentation facts. DocForge indexes, contexts,
changesets, previews, and rendered pages are derived and may be rebuilt.

View file

@ -0,0 +1,11 @@
[[nodes]]
schema_version = 1
id = "proof.carve-playtest"
title = "Carve playtest proof"
family = "proof"
authority = "derived"
status = "verified"
tags = ["carving", "playtest", "proof"]
summary = "Records that the edge-control fixture has a bounded test target."
content = "The test target records grip, carve angle, skid state, and the route segment for each sample."
proves = ["riding.edge-control"]

View file

@ -0,0 +1,14 @@
+++
schema_version = 1
id = "riding.edge-control"
title = "Edge control"
family = "riding"
authority = "approved_plan"
status = "ready"
tags = ["carving", "controls", "snow"]
summary = "Defines how player input produces grip, carve angle, and controlled skidding."
depends_on = ["trail.glacier-bowl"]
+++
Edge input changes ski grip continuously. Higher edge angles create tighter carved turns when the
surface supports them; excessive angle or poor snow contact produces a controlled skid.

View file

@ -0,0 +1,14 @@
+++
schema_version = 1
id = "safety.avalanche-window"
title = "Avalanche window"
family = "safety"
authority = "authoritative"
status = "current"
tags = ["conditions", "risk", "weather"]
summary = "Defines the conditions that close exposed terrain during the test session."
informs = ["session.first-descent"]
+++
The session closes exposed terrain when wind loading crosses the configured safety threshold. A
closure changes route availability but does not rewrite the terrain definition.

View file

@ -0,0 +1,14 @@
+++
schema_version = 1
id = "session.first-descent"
title = "First descent session"
family = "session"
authority = "approved_plan"
status = "active"
tags = ["playtest", "route", "session"]
summary = "Combines one route, one riding mechanic, and current safety conditions for a playtest."
depends_on = ["riding.edge-control", "safety.avalanche-window", "trail.glacier-bowl"]
+++
The first descent starts on the sheltered practice face, crosses the bowl, and exits through the
marked lower route. The session records completion time, falls, controlled skids, and route closure.

View file

@ -0,0 +1,13 @@
+++
schema_version = 1
id = "trail.glacier-bowl"
title = "Glacier Bowl"
family = "trail"
authority = "authoritative"
status = "open"
tags = ["alpine", "route", "snow"]
summary = "Defines the terrain and route constraints for the first playable descent."
+++
Glacier Bowl is a wide alpine route with one narrow entry, a sheltered practice face, and a marked
exit. The playable route must keep the entry and exit readable in flat light.

View file

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="docforge-render" content="{{ docforge_render_identity }}">
<title>{{ docforge_title }}</title>
</head>
<body data-project="{{ docforge_project_id }}" data-view="{{ docforge_view_id }}">
<header><h1>{{ docforge_title }}</h1></header>
<main>{{ docforge_content }}</main>
</body>
</html>

View file

@ -0,0 +1,306 @@
from __future__ import annotations
import asyncio
import hashlib
import re
import shutil
import sys
import tempfile
import unittest
from contextlib import AsyncExitStack
from pathlib import Path
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from docforge.changesets import ChangesetStore
from docforge.context import compile_context
from docforge.index import ProjectIndex
from docforge.project import Project
from docforge.rendering import RenderService
ROOT = Path(__file__).resolve().parents[1]
FIXTURES = ROOT / "tests" / "fixtures"
def copy_fixture(name: str, destination: Path) -> Path:
root = destination / name
shutil.copytree(FIXTURES / name, root)
return root
def canonical_digest(root: Path) -> str:
paths = [
root / ".docforge/project.toml",
*(path for path in root.rglob("*") if path.is_file() and ".docforge" not in path.parts),
]
digest = hashlib.sha256()
for path in sorted(set(paths), key=lambda item: item.relative_to(root).as_posix()):
digest.update(path.relative_to(root).as_posix().encode())
digest.update(path.read_bytes())
return digest.hexdigest()
def node_hash(project: Project, node_id: str) -> str:
return next(node.content_hash for node in project.load().nodes if node.node_id == node_id)
class UnrelatedProjectProofTests(unittest.TestCase):
def test_generic_ski_project_completes_the_full_workflow(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = copy_fixture("awesome-ski-game", Path(directory))
before = canonical_digest(root)
project = Project.open(root)
snapshot = project.load()
self.assertEqual("generic", project.descriptor.adapter)
self.assertEqual("awesome-ski-game", snapshot.descriptor.project_id)
self.assertEqual(
{"proof", "riding", "safety", "session", "trail"},
{node.family for node in snapshot.nodes},
)
self.assertEqual(5, len(snapshot.nodes))
self.assertEqual(6, len(snapshot.edges))
index = ProjectIndex(project)
built = index.build()
self.assertEqual(built, index.check())
self.assertEqual(
["proof.carve-playtest", "riding.edge-control"],
[item["node_id"] for item in index.search("grip carve", limit=5)["results"]],
)
self.assertEqual(
["safety.avalanche-window"],
[
item["node_id"]
for item in index.filter_nodes(family="safety", status="current")["results"]
],
)
self.assertEqual(
{
"riding.edge-control",
"safety.avalanche-window",
"trail.glacier-bowl",
},
{
item["node_id"]
for item in index.dependencies("session.first-descent", depth=3)["results"]
},
)
first_context = compile_context(index, "ride-day", budget=300)
second_context = compile_context(index, "ride-day", budget=300)
self.assertEqual(first_context, second_context)
self.assertLessEqual(first_context["estimated_tokens"], 300)
self.assertEqual("session.first-descent", first_context["entries"][0]["node_id"])
self.assertTrue(first_context["omissions"])
changesets = ChangesetStore(project, "ski-editor")
created = changesets.create("edge-tuning")
updated = changesets.propose_update(
changeset_id="edge-tuning",
expected_changeset_hash=created["changeset_hash"],
node_id="riding.edge-control",
expected_content_hash=node_hash(project, "riding.edge-control"),
metadata={"summary": "Proposes a narrower grip transition for review."},
content="<script>unsafe()</script>\n\n**Previewed edge-tuning proposal.**",
relationship_changes=[],
rationale="Exercise an unrelated project's isolated proposal path.",
)
validated = changesets.validate("edge-tuning")
diff = changesets.diff("edge-tuning")
self.assertTrue(validated["valid"])
self.assertEqual(updated["changeset_hash"], validated["changeset_hash"])
self.assertEqual("update", diff["changes"][0]["operation"])
rendering = RenderService(project, changesets)
first_preview = rendering.preview("edge-tuning", "field-guide")
preview_path = root / ".docforge/previews/edge-tuning/field-guide.html"
preview_bytes = preview_path.read_bytes()
second_preview = rendering.preview("edge-tuning", "field-guide")
self.assertEqual(first_preview["preview_identity"], second_preview["preview_identity"])
self.assertEqual(preview_bytes, preview_path.read_bytes())
preview = preview_bytes.decode()
self.assertIn("&lt;script&gt;unsafe()&lt;/script&gt;", preview)
self.assertIn("<strong>Previewed edge-tuning proposal.</strong>", preview)
self.assertFalse((root / ".docforge/rendered/field-guide.html").exists())
self.assertEqual(before, canonical_digest(root))
def test_generic_core_contains_no_project_specific_vocabulary(self) -> None:
banned = re.compile(r"\b(worldforge|assetforge|phase|villager)\b", re.IGNORECASE)
leaks = []
for path in sorted((ROOT / "src/docforge").rglob("*.py")):
for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
if banned.search(line):
leaks.append(f"{path.name}:{line_number}:{line.strip()}")
self.assertEqual([], leaks)
class UnrelatedProjectIsolationTests(unittest.IsolatedAsyncioTestCase):
async def test_two_live_servers_remain_project_isolated(self) -> None:
with tempfile.TemporaryDirectory() as directory:
parent = Path(directory)
ski_root = copy_fixture("awesome-ski-game", parent / "ski")
alpha_root = copy_fixture("alpha", parent / "alpha")
ski_before = canonical_digest(ski_root)
alpha_before = canonical_digest(alpha_root)
ski_project = Project.open(ski_root)
alpha_project = Project.open(alpha_root)
ProjectIndex(ski_project).build()
ProjectIndex(alpha_project).build()
ski_parameters = StdioServerParameters(
command=sys.executable,
args=[
"-m",
"docforge.mcp_server",
"--project-root",
str(ski_root),
"--proposal-writer",
"ski-editor",
],
)
alpha_parameters = StdioServerParameters(
command=sys.executable,
args=[
"-m",
"docforge.mcp_server",
"--project-root",
str(alpha_root),
"--proposal-writer",
"alpha-editor",
],
)
async with AsyncExitStack() as stack:
ski_read, ski_write = await stack.enter_async_context(stdio_client(ski_parameters))
alpha_read, alpha_write = await stack.enter_async_context(
stdio_client(alpha_parameters)
)
ski_session = await stack.enter_async_context(ClientSession(ski_read, ski_write))
alpha_session = await stack.enter_async_context(
ClientSession(alpha_read, alpha_write)
)
await asyncio.gather(ski_session.initialize(), alpha_session.initialize())
ski_info, alpha_info = await asyncio.gather(
ski_session.call_tool("docforge_project_info", {}),
alpha_session.call_tool("docforge_project_info", {}),
)
ski_own, alpha_own, ski_cross, alpha_cross = await asyncio.gather(
ski_session.call_tool(
"docforge_get_node", {"node_id": "session.first-descent"}
),
alpha_session.call_tool("docforge_get_node", {"node_id": "guide.workflow"}),
ski_session.call_tool("docforge_get_node", {"node_id": "guide.workflow"}),
alpha_session.call_tool(
"docforge_get_node", {"node_id": "session.first-descent"}
),
)
ski_result, alpha_result = await asyncio.gather(
self._propose_and_preview(
ski_session,
node_id="riding.edge-control",
content_hash=node_hash(ski_project, "riding.edge-control"),
view_id="field-guide",
summary="A ski-only proposal from its bound server.",
),
self._propose_and_preview(
alpha_session,
node_id="guide.workflow",
content_hash=node_hash(alpha_project, "guide.workflow"),
view_id="manual",
summary="An Alpha-only proposal from its bound server.",
),
)
self.assertEqual("awesome-ski-game", ski_info.structuredContent["project_id"])
self.assertEqual("alpha-docs", alpha_info.structuredContent["project_id"])
self.assertEqual("current", ski_info.structuredContent["index_health"]["state"])
self.assertEqual("current", alpha_info.structuredContent["index_health"]["state"])
self.assertNotEqual(
ski_info.structuredContent["project_root_fingerprint"],
alpha_info.structuredContent["project_root_fingerprint"],
)
self.assertNotEqual(
ski_info.structuredContent["source_hash"],
alpha_info.structuredContent["source_hash"],
)
self.assertEqual("session.first-descent", ski_own.structuredContent["node"]["node_id"])
self.assertEqual("guide.workflow", alpha_own.structuredContent["node"]["node_id"])
self.assertEqual("missing_node", ski_cross.structuredContent["error"]["code"])
self.assertEqual("awesome-ski-game", ski_cross.structuredContent["project_id"])
self.assertEqual("missing_node", alpha_cross.structuredContent["error"]["code"])
self.assertEqual("alpha-docs", alpha_cross.structuredContent["project_id"])
self.assertEqual("ski-editor", ski_result["creator"])
self.assertEqual("alpha-editor", alpha_result["creator"])
ski_changeset = ski_root / ".docforge/changesets/parallel-edit.json"
alpha_changeset = alpha_root / ".docforge/changesets/parallel-edit.json"
ski_preview = ski_root / ".docforge/previews/parallel-edit/field-guide.html"
alpha_preview = alpha_root / ".docforge/previews/parallel-edit/manual.html"
ski_index = ski_root / ".docforge/cache/index.sqlite3"
alpha_index = alpha_root / ".docforge/cache/index.sqlite3"
self.assertTrue(ski_index.is_file())
self.assertTrue(alpha_index.is_file())
self.assertTrue(ski_index.resolve().is_relative_to(ski_root))
self.assertTrue(alpha_index.resolve().is_relative_to(alpha_root))
self.assertTrue(ski_changeset.is_file())
self.assertTrue(alpha_changeset.is_file())
self.assertTrue(ski_preview.is_file())
self.assertTrue(alpha_preview.is_file())
ski_html = ski_preview.read_text(encoding="utf-8")
alpha_html = alpha_preview.read_text(encoding="utf-8")
self.assertIn('data-project="awesome-ski-game"', ski_html)
self.assertNotIn('data-project="alpha-docs"', ski_html)
self.assertIn('data-project="alpha-docs"', alpha_html)
self.assertNotIn('data-project="awesome-ski-game"', alpha_html)
self.assertFalse((ski_root / ".docforge/rendered/field-guide.html").exists())
self.assertFalse((alpha_root / ".docforge/rendered/manual.html").exists())
self.assertEqual(ski_before, canonical_digest(ski_root))
self.assertEqual(alpha_before, canonical_digest(alpha_root))
async def _propose_and_preview(
self,
session: ClientSession,
*,
node_id: str,
content_hash: str,
view_id: str,
summary: str,
) -> dict[str, object]:
created = await session.call_tool(
"docforge_create_changeset", {"changeset_id": "parallel-edit"}
)
updated = await session.call_tool(
"docforge_propose_node_update",
{
"changeset_id": "parallel-edit",
"expected_changeset_hash": created.structuredContent["changeset_hash"],
"node_id": node_id,
"expected_content_hash": content_hash,
"metadata": {"summary": summary},
"content": None,
"relationship_changes": [],
"rationale": "Prove simultaneous project-bound proposal isolation.",
},
)
validated = await session.call_tool(
"docforge_validate_changeset", {"changeset_id": "parallel-edit"}
)
preview = await session.call_tool(
"docforge_preview_changeset",
{"changeset_id": "parallel-edit", "view_id": view_id},
)
self.assertEqual("ok", updated.structuredContent["status"])
self.assertTrue(validated.structuredContent["valid"])
self.assertEqual("current", preview.structuredContent["state"])
return {
"creator": created.structuredContent["creator"],
"changeset_hash": updated.structuredContent["changeset_hash"],
"preview_identity": preview.structuredContent["preview_identity"],
}
if __name__ == "__main__":
unittest.main()