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

Complete independent projection runtime

This commit is contained in:
Andraxion 2026-07-29 12:38:25 -04:00
parent 1134c2d375
commit f1fabaf0ca
38 changed files with 4907 additions and 87 deletions

View file

@ -0,0 +1,187 @@
import AxeBuilder from "@axe-core/playwright";
import { expect, test } from "@playwright/test";
import { spawn } from "node:child_process";
import { createInterface } from "node:readline";
const AXE_TAGS = [
"wcag2a",
"wcag2aa",
"wcag21a",
"wcag21aa",
"wcag22a",
"wcag22aa",
];
const MANUAL_AXE_TAGS = AXE_TAGS.filter((tag) => !tag.startsWith("wcag22"));
let fixtureProcess;
let surfaces;
function startFixture() {
const python = process.env.DOCFORGE_PYTHON || ".venv/bin/python";
const child = spawn(python, ["tools/accessibility_fixture.py"], {
cwd: process.cwd(),
stdio: ["pipe", "pipe", "pipe"],
});
let stderr = "";
child.stderr.setEncoding("utf8");
child.stderr.on("data", (chunk) => {
stderr += chunk;
});
const lines = createInterface({ input: child.stdout });
const ready = new Promise((resolve, reject) => {
let settled = false;
lines.once("line", (line) => {
settled = true;
try {
const payload = JSON.parse(line);
if (
payload.schema_version !== 1
|| typeof payload.manual_html !== "string"
|| typeof payload.portable_html !== "string"
|| typeof payload.live_url !== "string"
) {
throw new Error("Accessibility fixture returned an invalid payload");
}
resolve(payload);
} catch (error) {
reject(error);
} finally {
lines.close();
}
});
child.once("exit", (code, signal) => {
if (!settled) {
reject(
new Error(
`Accessibility fixture exited before readiness `
+ `(code=${code}, signal=${signal}):\n${stderr}`,
),
);
}
});
});
return { child, ready };
}
async function stopFixture(child) {
if (child.exitCode !== null || child.signalCode !== null) {
return;
}
const exited = new Promise((resolve) => child.once("exit", resolve));
child.stdin.end();
await Promise.race([
exited,
new Promise((_, reject) => {
setTimeout(() => reject(new Error("Accessibility fixture did not stop")), 5_000);
}),
]);
}
function violationReport(violations) {
return violations.map((violation) => {
const targets = violation.nodes
.flatMap((node) => node.target)
.join(", ");
return `${violation.id} (${violation.impact}): ${violation.help}\n ${targets}`;
}).join("\n");
}
async function expectNoAxeViolations(page, tags = AXE_TAGS) {
const results = await new AxeBuilder({ page }).withTags(tags).analyze();
expect(results.violations, violationReport(results.violations)).toEqual([]);
}
async function tabTo(page, selector, maximumTabs = 40) {
for (let count = 0; count < maximumTabs; count += 1) {
await page.keyboard.press("Tab");
if (await page.evaluate((target) => document.activeElement?.matches(target), selector)) {
return page.locator(selector).filter({ visible: true }).first();
}
}
throw new Error(`Keyboard focus did not reach ${selector}`);
}
test.beforeAll(async () => {
const fixture = startFixture();
fixtureProcess = fixture.child;
surfaces = await fixture.ready;
});
test.afterAll(async () => {
await stopFixture(fixtureProcess);
});
test("generated manual has no axe violations and its navigation works by keyboard", async ({
page,
}) => {
await page.setContent(surfaces.manual_html, { waitUntil: "load" });
await expect(page.locator("main section")).not.toHaveCount(0);
// The generic renderer owns structure, while this frozen project template owns target sizing.
await expectNoAxeViolations(page, MANUAL_AXE_TAGS);
await page.keyboard.press("Tab");
const firstNavigationLink = page.locator("nav[aria-label='Documentation'] a").first();
await expect(firstNavigationLink).toBeFocused();
const target = await firstNavigationLink.getAttribute("href");
expect(target).toMatch(/^#[A-Za-z0-9_.-]+$/);
await page.keyboard.press("Enter");
await expect.poll(() => page.evaluate(() => window.location.hash)).toBe(target);
});
test("portable graph supports skip, filter, view, and dialog keyboard flows", async ({ page }) => {
await page.goto("about:blank");
await page.setContent(surfaces.portable_html, { waitUntil: "load" });
await expect(page.locator("#status")).toContainText("nodes and");
await expectNoAxeViolations(page);
await page.keyboard.press("Tab");
await expect(page.locator("a.skip")).toBeFocused();
await page.keyboard.press("Enter");
await expect(page.locator("main#main")).toBeFocused();
await page.goto("about:blank");
await page.setContent(surfaces.portable_html, { waitUntil: "load" });
await page.keyboard.press("Tab");
await page.keyboard.press("Tab");
await expect(page.locator("#filter")).toBeFocused();
await page.keyboard.type("guide.workflow");
await expect(page.locator("#status")).toContainText("1 nodes and");
await page.keyboard.press("Tab");
await expect(page.locator("#mode")).toBeFocused();
await page.keyboard.press("ArrowDown");
await expect(page.locator("main#main")).toHaveAttribute("data-mode", "flow");
await page.keyboard.press("ArrowUp");
await expect(page.locator("main#main")).toHaveAttribute("data-mode", "nodes");
await page.keyboard.press("Tab");
const nodeButton = page.locator("#node-list button").first();
await expect(nodeButton).toBeFocused();
await page.keyboard.press("Enter");
await expect(page.locator("#node-dialog")).toHaveAttribute("open", "");
await expect(page.locator("#close-dialog")).toBeFocused();
await expectNoAxeViolations(page);
await page.keyboard.press("Escape");
await expect(page.locator("#node-dialog")).not.toHaveAttribute("open", "");
await expect(nodeButton).toBeFocused();
});
test("live viewer passes axe and exposes keyboard graph and resize controls", async ({ page }) => {
await page.goto(surfaces.live_url);
await expect(page.locator("#status")).toContainText("nodes ·");
await expect(page.locator("#graph g.node[role='button']").first()).toBeVisible();
await expectNoAxeViolations(page);
const resizer = await tabTo(page, "#left-resizer");
const originalWidth = Number(await resizer.getAttribute("aria-valuenow"));
await page.keyboard.press("ArrowRight");
await expect(resizer).toHaveAttribute("aria-valuenow", String(originalWidth + 16));
await tabTo(page, "#graph g.node[role='button']");
await page.keyboard.press("Shift+Enter");
await expect(page.locator("#node-dialog")).toHaveAttribute("open", "");
await expect(page.locator("#close-node-dialog")).toBeFocused();
await expectNoAxeViolations(page);
await page.keyboard.press("Escape");
await expect(page.locator("#node-dialog")).not.toHaveAttribute("open", "");
});

View file

@ -1,6 +1,7 @@
from __future__ import annotations
import contextlib
import hashlib
import io
import json
import os
@ -18,6 +19,7 @@ from jsonschema import Draft202012Validator
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from docforge.changeset_contract import document_hash
from docforge.cli import _parser, _run, main
from docforge.client_config import (
_read_existing,
@ -39,6 +41,19 @@ CONFIGURATION_SCHEMA = json.loads(
DOCTOR_SCHEMA = json.loads((SCHEMAS / "doctor-result.schema.json").read_text(encoding="utf-8"))
POLICY_SCHEMA = json.loads((SCHEMAS / "policy.schema.json").read_text(encoding="utf-8"))
GRAPH_RENDER_CONFIG = """
[graph_render]
output_root = ".docforge/portable-graph"
[[graph_render.views]]
id = "architecture"
renderer = "portable_graph_html"
output = "architecture.html"
title = "Alpha architecture"
root = "guide.workflow"
"""
class ClientIntegrationTests(unittest.TestCase):
def copy_fixture(self, destination: Path) -> Path:
@ -156,6 +171,256 @@ class ClientIntegrationTests(unittest.TestCase):
)
self.assertEqual("invalid_capability_binding", escalated.exception.code)
def test_nondefault_projection_policy_selectors_serialize_and_validate(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") + GRAPH_RENDER_CONFIG,
encoding="utf-8",
)
project = Project.open(root)
expected_arguments = [
"-I",
"-m",
"docforge.mcp_server",
"--project-root",
str(root),
"--capability-mode",
"read",
"--manual-render-policy",
"disabled",
"--portable-graph-policy",
"disabled",
"--live-viewer-policy",
"disabled",
]
for client in ("codex", "claude", "openclaw"):
with self.subTest(client=client):
result = generate_client_configuration(
project,
client,
manual_render_policy="disabled",
portable_graph_policy="disabled",
live_viewer_policy="disabled",
)
Draft202012Validator(CONFIGURATION_SCHEMA).validate(result)
_validate_configuration_result(result)
self.assertEqual(
{
"schema_version": 2,
"manual": "disabled",
"portable_graph": "disabled",
"live_viewer": "disabled",
},
result["projection_policy"],
)
self.assertEqual(expected_arguments, result["binding"]["args"])
content = result["artifact"]["content"]
if client == "codex":
document = tomllib.loads(content)
serialized = document["mcp_servers"][result["server_name"]]["args"]
elif client == "claude":
document = json.loads(content)
serialized = document["mcpServers"][result["server_name"]]["args"]
else:
document = json.loads(content)
serialized = document["mcp"]["servers"][result["server_name"]]["args"]
self.assertEqual(expected_arguments, serialized)
canonical = json.dumps(
result["projection_policy"],
sort_keys=True,
separators=(",", ":"),
ensure_ascii=False,
).encode("utf-8")
self.assertEqual(
hashlib.sha256(canonical).hexdigest(),
result["projection_policy_hash"],
)
def test_projection_policy_schema_and_configuration_validation_reject_drift(self) -> None:
with tempfile.TemporaryDirectory() as directory:
project = Project.open(self.copy_fixture(Path(directory)))
result = generate_client_configuration(
project,
"codex",
manual_render_policy="disabled",
live_viewer_policy="disabled",
)
validator = Draft202012Validator(CONFIGURATION_SCHEMA)
validator.validate(result)
_validate_configuration_result(result)
for field, value in (
("schema_version", 1),
("manual", "on-demand"),
("portable_graph", "auto"),
("live_viewer", "explicit"),
):
with self.subTest(field=field):
drifted = json.loads(json.dumps(result))
drifted["projection_policy"][field] = value
self.assertTrue(list(validator.iter_errors(drifted)))
missing = json.loads(json.dumps(result))
missing.pop("projection_policy")
self.assertTrue(list(validator.iter_errors(missing)))
extra = json.loads(json.dumps(result))
extra["projection_policy"]["project_path"] = "/private/project"
self.assertTrue(list(validator.iter_errors(extra)))
mismatched = json.loads(json.dumps(result))
mismatched["projection_policy"]["manual"] = "explicit"
canonical = json.dumps(
mismatched["projection_policy"],
sort_keys=True,
separators=(",", ":"),
ensure_ascii=False,
).encode("utf-8")
mismatched["projection_policy_hash"] = hashlib.sha256(canonical).hexdigest()
with self.assertRaises(AssertionError):
_validate_configuration_result(mismatched)
bad_hash = json.loads(json.dumps(result))
bad_hash["projection_policy_hash"] = "0" * 64
with self.assertRaises(AssertionError):
_validate_configuration_result(bad_hash)
defaulted = generate_client_configuration(project, "codex")
self.assertNotIn(
"--manual-render-policy",
defaulted["binding"]["args"],
)
defaulted["projection_policy"]["manual"] = "disabled"
canonical = json.dumps(
defaulted["projection_policy"],
sort_keys=True,
separators=(",", ":"),
ensure_ascii=False,
).encode("utf-8")
defaulted["projection_policy_hash"] = hashlib.sha256(canonical).hexdigest()
with self.assertRaises(AssertionError):
_validate_configuration_result(defaulted)
unavailable = json.loads(json.dumps(result))
unavailable["projection_availability"]["manual_configured"] = False
with self.assertRaises(AssertionError):
_validate_configuration_result(unavailable)
descriptor = project.descriptor.descriptor_path
descriptor.write_text(
descriptor.read_text(encoding="utf-8") + GRAPH_RENDER_CONFIG,
encoding="utf-8",
)
graph_project = Project.open(project.descriptor.root)
graph_defaulted = generate_client_configuration(graph_project, "codex")
self.assertNotIn(
"--portable-graph-policy",
graph_defaulted["binding"]["args"],
)
coordinated_graph_drift = json.loads(json.dumps(graph_defaulted))
coordinated_graph_drift["projection_policy"]["portable_graph"] = "disabled"
coordinated_graph_drift["projection_availability"]["portable_graph_configured"] = False
canonical = json.dumps(
coordinated_graph_drift["projection_policy"],
sort_keys=True,
separators=(",", ":"),
ensure_ascii=False,
).encode("utf-8")
coordinated_graph_drift["projection_policy_hash"] = hashlib.sha256(
canonical
).hexdigest()
coordinated_graph_drift["configuration_hash"] = document_hash(
{
"schema_version": 1,
"client": coordinated_graph_drift["client"],
"server_name": coordinated_graph_drift["server_name"],
"project": coordinated_graph_drift["project"],
"binding": coordinated_graph_drift["binding"],
"effective_policy": coordinated_graph_drift["effective_policy"],
"projection_policy": coordinated_graph_drift["projection_policy"],
"projection_policy_hash": coordinated_graph_drift["projection_policy_hash"],
"projection_availability": coordinated_graph_drift["projection_availability"],
"artifact_format": coordinated_graph_drift["artifact"]["format"],
"artifact_content_sha256": coordinated_graph_drift["artifact"][
"content_sha256"
],
}
)
with self.assertRaises(AssertionError):
_validate_configuration_result(coordinated_graph_drift)
invalid_selections = (
{"manual_render_policy": "sometimes"},
{"portable_graph_policy": "auto"},
{"live_viewer_policy": "always"},
)
for selection in invalid_selections:
with self.subTest(selection=selection):
with self.assertRaises(DocForgeError) as raised:
generate_client_configuration(graph_project, "codex", **selection)
self.assertEqual("invalid_projection_policy", raised.exception.code)
def test_doctor_round_trips_projection_selectors_and_rejects_invalid_modes(self) -> None:
with tempfile.TemporaryDirectory() as directory:
parent = Path(directory)
project = Project.open(self.copy_fixture(parent))
ProjectIndex(project).build()
config = parent / "openclaw.json"
generated = generate_client_configuration(
project,
"openclaw",
manual_render_policy="disabled",
live_viewer_policy="disabled",
output=config,
)
healthy = run_doctor(
project,
"openclaw",
config_path=config,
server_name=generated["server_name"],
)
Draft202012Validator(DOCTOR_SCHEMA).validate(healthy)
self.assertEqual("healthy", healthy["doctor_state"])
self.assertIn(
"effective_policy_valid",
[check["code"] for check in healthy["checks"]],
)
document = json.loads(config.read_text(encoding="utf-8"))
entry = document["mcp"]["servers"][generated["server_name"]]
arguments = entry["args"]
manual_position = arguments.index("--manual-render-policy") + 1
arguments[manual_position] = "sometimes"
config.write_text(json.dumps(document, sort_keys=True), encoding="utf-8")
invalid = run_doctor(
project,
"openclaw",
config_path=config,
server_name=generated["server_name"],
)
Draft202012Validator(DOCTOR_SCHEMA).validate(invalid)
self.assertEqual("unhealthy", invalid["doctor_state"])
policy_check = next(
check for check in invalid["checks"] if check["check_id"] == "policy.effective"
)
self.assertEqual("invalid_projection_policy", policy_check["code"])
arguments[manual_position] = "auto"
config.write_text(json.dumps(document, sort_keys=True), encoding="utf-8")
unavailable = run_doctor(
project,
"openclaw",
config_path=config,
server_name=generated["server_name"],
)
Draft202012Validator(DOCTOR_SCHEMA).validate(unavailable)
self.assertEqual("unhealthy", unavailable["doctor_state"])
policy_check = next(
check for check in unavailable["checks"] if check["check_id"] == "policy.effective"
)
self.assertEqual("projection_policy_unavailable", policy_check["code"])
def test_explicit_fragment_write_is_atomic_conflict_aware_and_private(self) -> None:
with tempfile.TemporaryDirectory() as directory:
parent = Path(directory)

View file

@ -3,6 +3,7 @@ from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from unittest import mock
from docforge.cli import _parser, _run
from docforge.errors import DocForgeError
@ -10,6 +11,28 @@ from docforge.project import Project
class DocForgeOnboardingTests(unittest.TestCase):
def test_scaffold_honors_disabled_manual_projection_policy(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory) / "disabled-render"
root.mkdir()
arguments = _parser().parse_args(
[
"--project-root",
str(root),
"--manual-render-policy",
"disabled",
"onboard",
"--scaffold",
]
)
with mock.patch(
"docforge.cli.RenderService.render",
side_effect=AssertionError("disabled onboarding must not render"),
):
result = _run(arguments)
self.assertEqual("skipped", result["render"]["state"])
self.assertFalse((root / ".docforge/rendered/manual.html").exists())
def test_assessment_detects_multiple_languages_without_writing(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory) / "polyglot"

View file

@ -30,6 +30,8 @@ from docforge.projection_contract import (
canonical_projection_bytes,
projection_hash,
)
from docforge.projection_fragments import FRAGMENT_CACHE_DIRECTORY, FragmentRecord
from docforge.projection_worker import render_projection_in_worker
from docforge.render_contract import GenericHtmlRenderer
from docforge_renderers.manual import ManualHtmlRenderer
@ -412,6 +414,171 @@ class ProjectionContractTests(unittest.TestCase):
prepared.output,
)
def test_manual_fragments_are_incremental_and_full_worker_output_is_the_oracle(
self,
) -> None:
renderer = GenericHtmlRenderer()
with mock.patch(
"docforge.render_contract.render_projection_in_worker",
wraps=render_projection_in_worker,
) as detached:
cold = renderer.prepare(
self.snapshot,
self.view,
self.template,
changeset_hash=None,
)
self.assertEqual(2, detached.call_count)
fragment_root = self.snapshot.descriptor.cache_root / FRAGMENT_CACHE_DIRECTORY
self.assertEqual(
len(self.plan.document["pages"]),
len(list(fragment_root.glob("*.json"))),
)
with mock.patch(
"docforge.render_contract.render_projection_in_worker",
wraps=render_projection_in_worker,
) as detached:
warm = renderer.prepare(
self.snapshot,
self.view,
self.template,
changeset_hash=None,
)
self.assertEqual(1, detached.call_count)
self.assertEqual(cold.output, warm.output)
with mock.patch(
"docforge.render_contract.render_projection_in_worker",
wraps=render_projection_in_worker,
) as detached:
full = GenericHtmlRenderer(incremental=False).prepare(
self.snapshot,
self.view,
self.template,
changeset_hash=None,
)
self.assertEqual(1, detached.call_count)
self.assertEqual(full.output, warm.output)
first_fragment = sorted(fragment_root.glob("*.json"))[0]
first_fragment.write_bytes(b"{corrupt")
with mock.patch(
"docforge.render_contract.render_projection_in_worker",
wraps=render_projection_in_worker,
) as detached:
recovered = renderer.prepare(
self.snapshot,
self.view,
self.template,
changeset_hash=None,
)
self.assertEqual(2, detached.call_count)
self.assertEqual(full.output, recovered.output)
original_record = FragmentRecord.from_bytes(first_fragment.read_bytes())
first_fragment.write_bytes(
FragmentRecord.create(
original_record.key,
b"<script>forged fragment</script>",
).to_bytes()
)
with mock.patch(
"docforge.render_contract.render_projection_in_worker",
wraps=render_projection_in_worker,
) as detached:
forged = renderer.prepare(
self.snapshot,
self.view,
self.template,
changeset_hash=None,
)
self.assertEqual(2, detached.call_count)
self.assertEqual(full.output, forged.output)
self.assertNotIn(b"forged fragment", forged.output)
def test_oversized_manual_fragment_bypasses_cache_and_uses_full_worker(self) -> None:
content = "x" * 4_100_000
node = replace(
self.snapshot.nodes[0],
content=content,
content_hash=hashlib.sha256(content.encode()).hexdigest(),
)
snapshot = replace(
self.snapshot,
descriptor=replace(
self.snapshot.descriptor,
limits=replace(
self.snapshot.descriptor.limits,
max_render_bytes=20_000_000,
),
),
nodes=(node,),
edges=(),
source_hash=hashlib.sha256(b"oversized-fragment").hexdigest(),
)
with mock.patch(
"docforge.render_contract.render_projection_in_worker",
wraps=render_projection_in_worker,
) as detached:
prepared = GenericHtmlRenderer().prepare(
snapshot,
self.view,
self.template,
changeset_hash=None,
)
self.assertEqual(1, detached.call_count)
full = GenericHtmlRenderer(incremental=False).prepare(
snapshot,
self.view,
self.template,
changeset_hash=None,
)
self.assertEqual(full.output, prepared.output)
def test_fragment_package_overflow_falls_back_to_full_worker(self) -> None:
content = "x" * 2_700_000
base = self.snapshot.nodes[0]
nodes = tuple(
replace(
base,
node_id=f"guide.large-{index}",
title=f"Large {index}",
content=content,
source_path=f"docs/content/large-{index}.md",
content_hash=hashlib.sha256(f"{index}:{content}".encode()).hexdigest(),
)
for index in range(4)
)
descriptor = replace(
self.snapshot.descriptor,
limits=replace(
self.snapshot.descriptor.limits,
max_render_bytes=20_000_000,
),
)
snapshot = replace(
self.snapshot,
descriptor=descriptor,
nodes=nodes,
edges=(),
source_hash=hashlib.sha256(b"large-fragment-fixture").hexdigest(),
)
incremental = GenericHtmlRenderer().prepare(
snapshot,
self.view,
self.template,
changeset_hash=None,
)
full = GenericHtmlRenderer(incremental=False).prepare(
snapshot,
self.view,
self.template,
changeset_hash=None,
)
self.assertGreater(len(full.output), 10_000_000)
self.assertEqual(full.output, incremental.output)
def test_manual_renderer_rejects_project_provided_active_content(self) -> None:
for active in (
"<script>alert(1)</script>{{ docforge_content }}",

View file

@ -0,0 +1,205 @@
from __future__ import annotations
import json
import tempfile
import unittest
from pathlib import Path
from unittest import mock
from docforge.projection_fragments import (
FRAGMENT_CACHE_DIRECTORY,
FRAGMENT_KEY_CONTRACT,
FRAGMENT_RECORD_CONTRACT,
FRAGMENT_SCHEMA_VERSION,
FragmentKey,
FragmentRecord,
ProjectionFragmentCache,
fragment_semantic_hash,
)
class ProjectionFragmentCacheTests(unittest.TestCase):
def setUp(self) -> None:
self.temporary = tempfile.TemporaryDirectory()
self.addCleanup(self.temporary.cleanup)
self.root = Path(self.temporary.name) / "project"
self.root.mkdir()
self.cache_root = self.root / ".docforge" / "cache"
self.cache = ProjectionFragmentCache(
self.root,
self.cache_root,
maximum_content_bytes=1_000,
)
self.key = self.make_key({"node": "guide.alpha", "summary": "Alpha"})
@staticmethod
def make_key(
semantics: object,
*,
renderer_version: str = "1",
component_version: str = "graph.node@1",
) -> FragmentKey:
return FragmentKey.create(
projection_kind="graph",
renderer_id="portable_graph_html",
renderer_version=renderer_version,
component_version=component_version,
semantic_input_hash=fragment_semantic_hash(semantics),
)
@property
def entry_path(self) -> Path:
return self.cache_root / FRAGMENT_CACHE_DIRECTORY / f"{self.key.key_id}.json"
def test_miss_hit_and_unchanged_reuse_are_exact(self) -> None:
self.assertIsNone(self.cache.get(self.key))
with mock.patch(
"docforge.projection_fragments.atomic_replace_bytes_at",
wraps=__import__(
"docforge.projection_fragments",
fromlist=["atomic_replace_bytes_at"],
).atomic_replace_bytes_at,
) as atomic:
stored = self.cache.put(self.key, b"<li>Alpha</li>")
self.assertIsNotNone(stored)
self.assertEqual(1, atomic.call_count)
before = self.entry_path.stat()
repeated = self.cache.put(self.key, b"<li>Alpha</li>")
self.assertEqual(1, atomic.call_count)
self.assertEqual(stored, repeated)
self.assertEqual(stored, self.cache.get(self.key))
after = self.entry_path.stat()
self.assertEqual((before.st_dev, before.st_ino), (after.st_dev, after.st_ino))
def test_key_versions_and_complete_semantics_invalidate_independently(self) -> None:
self.assertIsNotNone(self.cache.put(self.key, b"alpha"))
variants = (
self.make_key(
{"node": "guide.alpha", "summary": "Alpha"},
renderer_version="2",
),
self.make_key(
{"node": "guide.alpha", "summary": "Alpha"},
component_version="graph.node@2",
),
self.make_key({"node": "guide.alpha", "summary": "Changed"}),
)
for variant in variants:
with self.subTest(key=variant):
self.assertNotEqual(self.key.key_id, variant.key_id)
self.assertIsNone(self.cache.get(variant))
def test_add_delete_and_reorder_change_sequence_semantics(self) -> None:
base = ["a", "b"]
added = ["a", "b", "c"]
deleted = ["a"]
reordered = ["b", "a"]
keys = [self.make_key(value) for value in (base, added, deleted, reordered)]
self.assertEqual(4, len({key.key_id for key in keys}))
self.assertEqual(
fragment_semantic_hash({"a": 1, "b": 2}),
fragment_semantic_hash({"b": 2, "a": 1}),
)
def test_corrupt_oversized_incompatible_and_foreign_entries_are_misses(self) -> None:
self.assertIsNotNone(self.cache.put(self.key, b"alpha"))
self.entry_path.write_bytes(b"{bad-json")
self.assertIsNone(self.cache.get(self.key))
self.assertIsNotNone(self.cache.put(self.key, b"repaired"))
repaired = self.cache.get(self.key)
self.assertIsNotNone(repaired)
assert repaired is not None
self.assertEqual(b"repaired", repaired.content)
self.entry_path.write_bytes(b"x" * (self.cache.maximum_record_bytes + 1))
self.assertIsNone(self.cache.get(self.key))
incompatible = FragmentRecord.create(self.key, b"alpha").as_dict()
incompatible["schema_version"] = FRAGMENT_SCHEMA_VERSION + 1
self.entry_path.write_bytes(
json.dumps(incompatible, sort_keys=True, separators=(",", ":")).encode()
)
self.assertIsNone(self.cache.get(self.key))
foreign_key = self.make_key({"node": "foreign"})
foreign = FragmentRecord.create(foreign_key, b"foreign").to_bytes()
self.entry_path.write_bytes(foreign)
self.assertIsNone(self.cache.get(self.key))
def test_symlinked_entry_and_cache_root_fail_closed_without_outside_writes(self) -> None:
self.entry_path.parent.mkdir(parents=True)
outside = Path(self.temporary.name) / "outside.json"
outside.write_bytes(FragmentRecord.create(self.key, b"outside").to_bytes())
self.entry_path.symlink_to(outside)
self.assertIsNone(self.cache.get(self.key))
self.assertIsNone(self.cache.put(self.key, b"replacement"))
self.assertEqual(b"outside", FragmentRecord.from_bytes(outside.read_bytes()).content)
outside_cache = Path(self.temporary.name) / "outside-cache"
escaped = ProjectionFragmentCache(self.root, outside_cache)
self.assertIsNone(escaped.put(self.key, b"escaped"))
self.assertFalse(outside_cache.exists())
def test_keys_and_records_have_deterministic_path_free_serialization(self) -> None:
same_key = self.make_key({"summary": "Alpha", "node": "guide.alpha"})
self.assertEqual(self.key, same_key)
first = FragmentRecord.create(self.key, b"\x00fragment\xff")
second = FragmentRecord.create(same_key, b"\x00fragment\xff")
self.assertEqual(first, second)
self.assertEqual(first.to_bytes(), second.to_bytes())
self.assertEqual(first, FragmentRecord.from_bytes(first.to_bytes()))
document = json.loads(first.to_bytes())
self.assertEqual(FRAGMENT_SCHEMA_VERSION, document["schema_version"])
self.assertEqual(FRAGMENT_RECORD_CONTRACT, document["contract"])
self.assertEqual(FRAGMENT_KEY_CONTRACT, document["key"]["contract"])
self.assertNotIn(str(self.root), first.to_bytes().decode("utf-8"))
self.assertEqual(first.byte_count, len(first.content))
self.assertEqual(64, len(first.content_sha256))
def test_cache_write_failure_is_a_miss_and_does_not_mutate_canonical_files(self) -> None:
canonical = self.root / "canonical.md"
canonical.write_text("canonical", encoding="utf-8")
with mock.patch(
"docforge.projection_fragments.atomic_replace_bytes_at",
side_effect=OSError("synthetic cache failure"),
):
self.assertIsNone(self.cache.put(self.key, b"fragment"))
self.assertIsNone(self.cache.get(self.key))
self.assertEqual("canonical", canonical.read_text(encoding="utf-8"))
def test_record_rejects_noncanonical_serialization_and_invalid_evidence(self) -> None:
record = FragmentRecord.create(self.key, b"alpha")
pretty = json.dumps(record.as_dict(), sort_keys=True, indent=2).encode()
with self.assertRaisesRegex(Exception, "canonically serialized"):
FragmentRecord.from_bytes(pretty)
tampered = record.as_dict()
tampered["byte_count"] = record.byte_count + 1
canonical = json.dumps(tampered, sort_keys=True, separators=(",", ":")).encode()
with self.assertRaisesRegex(Exception, "byte evidence"):
FragmentRecord.from_bytes(canonical)
def test_prune_retains_only_the_exact_bounded_active_inventory(self) -> None:
retained = self.key
stale = self.make_key({"node": "stale"})
self.assertIsNotNone(self.cache.put(retained, b"retained"))
self.assertIsNotNone(self.cache.put(stale, b"stale"))
foreign = self.entry_path.parent / "foreign.tmp"
foreign.write_bytes(b"foreign")
self.assertTrue(self.cache.prune((retained,)))
retained_record = self.cache.get(retained)
self.assertIsNotNone(retained_record)
assert retained_record is not None
self.assertEqual(b"retained", retained_record.content)
self.assertIsNone(self.cache.get(stale))
self.assertFalse(foreign.exists())
with mock.patch(
"docforge.projection_fragments.MAX_FRAGMENT_CACHE_BYTES",
1,
):
self.assertFalse(self.cache.prune((retained,)))
if __name__ == "__main__":
unittest.main()

View file

@ -0,0 +1,266 @@
from __future__ import annotations
import copy
import hashlib
import json
import unittest
from dataclasses import FrozenInstanceError
from pathlib import Path
from jsonschema import Draft202012Validator
from docforge.errors import DocForgeError
from docforge.projection_policy import (
ProjectionPolicyV2,
compose_projection_policy,
)
ROOT = Path(__file__).resolve().parents[1]
POLICY_SCHEMA = json.loads(
(ROOT / "schemas" / "projection-policy.schema.json").read_text(encoding="utf-8")
)
class ProjectionPolicyV2Tests(unittest.TestCase):
def test_compatible_defaults_are_independent_and_schema_valid(self) -> None:
cases = (
(
{
"manual_configured": False,
"portable_graph_configured": False,
"application_enabled": False,
},
("disabled", "disabled", "on-demand"),
),
(
{
"manual_configured": True,
"portable_graph_configured": False,
"application_enabled": False,
},
("explicit", "disabled", "on-demand"),
),
(
{
"manual_configured": True,
"portable_graph_configured": True,
"application_enabled": True,
},
("auto", "explicit", "on-demand"),
),
(
{
"manual_configured": True,
"portable_graph_configured": True,
"application_enabled": False,
"live_viewer_available": False,
},
("explicit", "explicit", "disabled"),
),
)
validator = Draft202012Validator(POLICY_SCHEMA)
for arguments, expected in cases:
with self.subTest(arguments=arguments):
policy = compose_projection_policy(**arguments)
self.assertEqual(
expected, (policy.manual, policy.portable_graph, policy.live_viewer)
)
validator.validate(policy.as_dict())
def test_explicit_selections_can_narrow_each_consumer_independently(self) -> None:
policy = compose_projection_policy(
manual="explicit",
portable_graph="disabled",
live_viewer="disabled",
manual_configured=True,
portable_graph_configured=True,
application_enabled=True,
)
self.assertEqual(
{
"schema_version": 2,
"manual": "explicit",
"portable_graph": "disabled",
"live_viewer": "disabled",
},
policy.as_dict(),
)
fully_disabled = compose_projection_policy(
manual="disabled",
portable_graph="disabled",
live_viewer="disabled",
manual_configured=False,
portable_graph_configured=False,
application_enabled=False,
live_viewer_available=False,
)
self.assertEqual("disabled", fully_disabled.manual)
self.assertEqual("disabled", fully_disabled.portable_graph)
self.assertEqual("disabled", fully_disabled.live_viewer)
def test_policy_is_frozen_and_hashes_exact_canonical_payload(self) -> None:
first = compose_projection_policy(
manual_configured=True,
portable_graph_configured=True,
application_enabled=False,
)
second = compose_projection_policy(
manual_configured=True,
portable_graph_configured=True,
application_enabled=False,
)
canonical = json.dumps(
first.as_dict(),
sort_keys=True,
separators=(",", ":"),
ensure_ascii=False,
).encode("utf-8")
self.assertEqual(first.as_dict(), second.as_dict())
self.assertEqual(hashlib.sha256(canonical).hexdigest(), first.policy_hash)
self.assertEqual(first.policy_hash, second.policy_hash)
changed = compose_projection_policy(
manual="disabled",
manual_configured=True,
portable_graph_configured=True,
application_enabled=False,
)
self.assertNotEqual(first.policy_hash, changed.policy_hash)
with self.assertRaises(FrozenInstanceError):
first.manual = "disabled" # type: ignore[misc]
def test_invalid_modes_and_direct_construction_fail_closed(self) -> None:
cases = (
("manual", {"manual": "sometimes"}),
("portable_graph", {"portable_graph": "auto"}),
("live_viewer", {"live_viewer": "always"}),
("manual", {"manual": 1}),
)
for expected_projection, selection in cases:
with self.subTest(selection=selection):
with self.assertRaises(DocForgeError) as raised:
compose_projection_policy(
**selection,
manual_configured=True,
portable_graph_configured=True,
application_enabled=True,
)
self.assertEqual("invalid_projection_policy", raised.exception.code)
self.assertEqual(
expected_projection,
raised.exception.details["projection"],
)
with self.assertRaises(DocForgeError) as direct:
ProjectionPolicyV2(
manual="automatic", # type: ignore[arg-type]
portable_graph="explicit",
live_viewer="on-demand",
)
self.assertEqual("invalid_projection_policy", direct.exception.code)
def test_resource_and_capability_unavailability_are_distinct(self) -> None:
cases = (
(
{"manual": "explicit"},
{
"manual_configured": False,
"portable_graph_configured": False,
"application_enabled": False,
},
"manual",
"manual_render_config",
),
(
{"manual": "auto"},
{
"manual_configured": True,
"portable_graph_configured": False,
"application_enabled": False,
},
"manual",
"canonical_application",
),
(
{"portable_graph": "explicit"},
{
"manual_configured": False,
"portable_graph_configured": False,
"application_enabled": False,
},
"portable_graph",
"portable_graph_render_config",
),
(
{"live_viewer": "on-demand"},
{
"manual_configured": False,
"portable_graph_configured": False,
"application_enabled": False,
"live_viewer_available": False,
},
"live_viewer",
"live_viewer_runtime",
),
)
for selection, availability, projection, required in cases:
with self.subTest(selection=selection):
with self.assertRaises(DocForgeError) as raised:
compose_projection_policy(**selection, **availability)
self.assertEqual("projection_policy_unavailable", raised.exception.code)
self.assertEqual(projection, raised.exception.details["projection"])
self.assertEqual(required, raised.exception.details["required"])
def test_availability_inputs_must_be_real_booleans(self) -> None:
cases = (
{"manual_configured": 1},
{"portable_graph_configured": 0},
{"application_enabled": "yes"},
{"live_viewer_available": None},
)
for replacement in cases:
arguments = {
"manual_configured": False,
"portable_graph_configured": False,
"application_enabled": False,
"live_viewer_available": True,
**replacement,
}
with self.subTest(replacement=replacement):
with self.assertRaises(DocForgeError) as raised:
compose_projection_policy(**arguments)
self.assertEqual("invalid_projection_policy", raised.exception.code)
def test_schema_rejects_every_runtime_contract_drift(self) -> None:
validator = Draft202012Validator(POLICY_SCHEMA)
valid = compose_projection_policy(
manual_configured=True,
portable_graph_configured=True,
application_enabled=True,
).as_dict()
invalid_documents: list[dict[str, object]] = []
for field in ("schema_version", "manual", "portable_graph", "live_viewer"):
missing = copy.deepcopy(valid)
missing.pop(field)
invalid_documents.append(missing)
for field, value in (
("schema_version", 1),
("manual", "on-demand"),
("portable_graph", "auto"),
("live_viewer", "explicit"),
):
changed = copy.deepcopy(valid)
changed[field] = value
invalid_documents.append(changed)
extra = copy.deepcopy(valid)
extra["project_path"] = "/private/project"
invalid_documents.append(extra)
for document in invalid_documents:
with self.subTest(document=document):
self.assertFalse(validator.is_valid(document))
if __name__ == "__main__":
unittest.main()

View file

@ -0,0 +1,239 @@
from __future__ import annotations
import shutil
import tempfile
import unittest
from pathlib import Path
from unittest import mock
from docforge.application import CanonicalApplicationService
from docforge.errors import DocForgeError
from docforge.graph_rendering import GraphRenderService
from docforge.index import ProjectIndex
from docforge.mcp_server import DocForgeService
from docforge.project import Project
from docforge.rendering import RenderService
from docforge.viewer_manager import ViewerManagerClient
ROOT = Path(__file__).resolve().parents[1]
FIXTURES = ROOT / "tests" / "fixtures"
GRAPH_CONFIG = """
[graph_render]
output_root = ".docforge/portable-graph"
[[graph_render.views]]
id = "architecture"
renderer = "portable_graph_html"
output = "architecture.html"
title = "Alpha architecture"
root = "guide.workflow"
initial_mode = "nodes"
depth = 2
max_nodes = 20
max_edges = 40
max_work = 1000
include_logic = false
"""
class ProjectionPolicyIntegrationTests(unittest.TestCase):
def setUp(self) -> None:
self.temporary = tempfile.TemporaryDirectory()
self.addCleanup(self.temporary.cleanup)
self.root = Path(self.temporary.name) / "alpha"
shutil.copytree(FIXTURES / "alpha", self.root)
def project(self, *, graph: bool = False) -> Project:
if graph:
descriptor = self.root / ".docforge/project.toml"
descriptor.write_text(
descriptor.read_text(encoding="utf-8") + GRAPH_CONFIG,
encoding="utf-8",
)
return Project.open(self.root)
def test_disabled_manual_blocks_work_before_load_but_status_remains_receipt_only(self) -> None:
project = self.project()
service = RenderService(project, manual_policy="disabled")
with mock.patch.object(
project,
"load",
side_effect=AssertionError("disabled manual policy must fail before loading"),
):
status = service.status("manual")
self.assertEqual("stale", status["state"])
for operation in (
lambda: service.render("manual"),
lambda: service.deep_status("manual"),
lambda: service.preview("proposal", "manual"),
):
with self.subTest(operation=operation), self.assertRaises(DocForgeError) as raised:
operation()
self.assertEqual(
"projection_policy_forbids_operation",
raised.exception.code,
)
def test_disabled_graph_blocks_plan_and_render_but_status_remains_available(self) -> None:
project = self.project(graph=True)
service = GraphRenderService(project, portable_graph_policy="disabled")
with mock.patch.object(
project,
"load",
side_effect=AssertionError("disabled graph policy must fail before loading"),
):
status = service.status("architecture")
self.assertEqual("stale", status["state"])
for operation in (
lambda: service.plan("architecture"),
lambda: service.render("architecture"),
):
with self.subTest(operation=operation), self.assertRaises(DocForgeError) as raised:
operation()
self.assertEqual(
"projection_policy_forbids_operation",
raised.exception.code,
)
def test_live_viewer_disabled_blocks_start_before_index_work_but_allows_lifecycle(self) -> None:
project = self.project()
index = ProjectIndex(project)
client = ViewerManagerClient(index, live_viewer_policy="disabled")
with (
mock.patch.object(
index,
"check",
side_effect=AssertionError("disabled viewer must fail before index work"),
),
self.assertRaises(DocForgeError) as raised,
):
client.start(node_id="guide.workflow")
self.assertEqual("projection_policy_forbids_operation", raised.exception.code)
with mock.patch.object(
client,
"_lifecycle_request",
side_effect=(
{"status": "ok", "state": "not_running"},
{"status": "ok", "state": "stopped"},
),
):
self.assertEqual("not_running", client.status()["state"])
self.assertEqual("stopped", client.stop()["state"])
def test_application_auto_renders_and_explicit_or_disabled_skip_cleanly(self) -> None:
project = self.project()
for mode, expected_action, expected_calls in (
("auto", "rendered", 1),
("explicit", "skipped_explicit", 0),
("disabled", "skipped_disabled", 0),
):
with self.subTest(mode=mode):
service = CanonicalApplicationService(
project,
applier_id="alpha-editor",
applier=mock.Mock(),
manual_policy=mode, # type: ignore[arg-type]
)
with (
mock.patch.object(
service.changesets,
"apply",
return_value={"status": "ok", "applied": True},
),
mock.patch.object(
service.index,
"build",
return_value={"status": "ok"},
),
mock.patch.object(
service.index,
"check",
return_value={"status": "ok"},
),
mock.patch.object(
service.rendering,
"render",
return_value={"status": "ok", "state": "current"},
) as rendered,
):
result = service.apply("policy-application", "a" * 64)
refresh = result["derived_refresh"]
assert isinstance(refresh, dict)
self.assertEqual(
{"mode": mode, "action": expected_action},
refresh["render_policy"],
)
self.assertEqual(expected_calls, rendered.call_count)
self.assertEqual("ok", refresh["status"])
def test_mcp_exposes_v2_without_changing_v1_policy_or_legacy_render_projection(self) -> None:
project = self.project(graph=True)
baseline = DocForgeService(project, capability_mode_name="read")
narrowed = DocForgeService(
project,
capability_mode_name="read",
manual_projection_policy="disabled",
portable_graph_policy="disabled",
live_viewer_policy="disabled",
)
self.assertEqual(baseline.policy.as_dict(), narrowed.policy.as_dict())
result = narrowed.bootstrap()
self.assertEqual(narrowed.policy.as_dict(), result["effective_policy"])
self.assertEqual(
narrowed.projection_policy.as_dict(),
result["projection_policy"],
)
self.assertEqual(
narrowed.projection_policy.policy_hash,
result["projection_policy_hash"],
)
session = result["session_contract"]
assert isinstance(session, dict)
self.assertEqual(
{
"manual": narrowed.policy.manual_render,
"graph": narrowed.policy.graph_render,
"live_viewer": narrowed.policy.live_viewer,
},
session["render_policies"],
)
self.assertEqual(
"stale",
narrowed.graph_render_status("architecture")["state"],
)
blocked_plan = narrowed.graph_plan("architecture")
self.assertEqual(
"projection_policy_forbids_operation",
blocked_plan["error"]["code"],
)
def test_direct_service_policy_values_are_runtime_validated(self) -> None:
project = self.project(graph=True)
factories = (
lambda: RenderService(project, manual_policy="bogus"), # type: ignore[arg-type]
lambda: GraphRenderService(
project,
portable_graph_policy="bogus", # type: ignore[arg-type]
),
lambda: ViewerManagerClient(
ProjectIndex(project),
live_viewer_policy="bogus", # type: ignore[arg-type]
),
lambda: CanonicalApplicationService(
project,
applier_id=None,
applier=None,
manual_policy="bogus", # type: ignore[arg-type]
),
)
for factory in factories:
with self.subTest(factory=factory), self.assertRaises(DocForgeError) as raised:
factory()
self.assertEqual("invalid_projection_policy", raised.exception.code)
if __name__ == "__main__":
unittest.main()

View file

@ -0,0 +1,387 @@
from __future__ import annotations
import base64
import contextlib
import copy
import json
import os
import shutil
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock
from docforge.errors import DocForgeError
from docforge.graph_projection import (
GraphViewRequestV1,
build_graph_projection_package,
build_graph_view_plan,
)
from docforge.manual_projection import (
build_manual_projection_package,
build_manual_render_plan,
)
from docforge.project import Project
from docforge.projection_contract import (
ProjectionPackageV1,
ProjectionReceiptV1,
canonical_projection_bytes,
)
from docforge.projection_worker import (
MAX_WORKER_ARTIFACT_BYTES,
MAX_WORKER_RESPONSE_BYTES,
_child_response,
render_projection_in_worker,
)
from docforge.render_contract import GenericHtmlRenderer
from docforge_renderers.graph import PortableGraphHtmlRenderer
from docforge_renderers.manual import ManualHtmlRenderer
ROOT = Path(__file__).resolve().parents[1]
FIXTURES = ROOT / "tests" / "fixtures"
class ProjectionWorkerTests(unittest.TestCase):
def setUp(self) -> None:
self.temporary = tempfile.TemporaryDirectory()
self.addCleanup(self.temporary.cleanup)
self.root = Path(self.temporary.name) / "alpha"
shutil.copytree(FIXTURES / "alpha", self.root)
self.project = Project.open(self.root)
self.snapshot = self.project.load()
render = self.snapshot.descriptor.render
assert render is not None
self.manual_view = render.views[0]
self.manual_version = GenericHtmlRenderer().renderer_version
self.manual_plan = build_manual_render_plan(
self.snapshot,
self.manual_view,
changeset_hash=None,
)
self.manual_package = build_manual_projection_package(
self.manual_plan,
self.manual_view.template_path.read_bytes(),
renderer_id="generic_html",
renderer_version=self.manual_version,
max_output_bytes=self.snapshot.descriptor.limits.max_render_bytes,
)
self.graph_plan = build_graph_view_plan(
self.snapshot,
GraphViewRequestV1(
view_id="architecture",
title="Alpha architecture",
root_node_id="guide.workflow",
depth=2,
max_nodes=20,
max_edges=40,
max_work=1_000,
),
False,
)
self.graph_package = build_graph_projection_package(
self.graph_plan,
renderer_id=PortableGraphHtmlRenderer.renderer_id,
renderer_version=PortableGraphHtmlRenderer.renderer_version,
max_output_bytes=self.snapshot.descriptor.limits.max_render_bytes,
)
def test_manual_and_graph_workers_match_the_in_process_renderers(self) -> None:
cases = (
(
self.manual_package,
ManualHtmlRenderer(self.manual_version).render(self.manual_package),
),
(
self.graph_package,
PortableGraphHtmlRenderer().render(self.graph_package),
),
)
for package, expected in cases:
with self.subTest(kind=package.kind):
result = render_projection_in_worker(package)
self.assertEqual(expected.artifacts, result.artifacts)
receipt = result.receipt.as_dict()
self.assertEqual(package.package_id, receipt["package_id"])
self.assertEqual(package.document["plan_id"], receipt["plan_id"])
self.assertIs(type(receipt["peak_memory_bytes"]), int)
self.assertGreater(receipt["peak_memory_bytes"], 0)
def test_parent_launch_is_fixed_and_request_contains_no_runtime_authority(self) -> None:
original = subprocess.run
with mock.patch(
"docforge.projection_worker.subprocess.run",
wraps=original,
) as launched:
render_projection_in_worker(self.manual_package)
command = launched.call_args.args[0]
options = launched.call_args.kwargs
self.assertEqual(
[sys.executable, "-I", "-m", "docforge.projection_worker"],
command,
)
self.assertIs(options["shell"], False)
self.assertEqual(sys.prefix, options["cwd"])
self.assertEqual(
{"PYTHONIOENCODING": "utf-8", "PYTHONUTF8": "1"},
options["env"],
)
request = options["input"]
self.assertEqual(
canonical_projection_bytes(self.manual_package.as_dict()) + b"\n",
request,
)
for forbidden in (
str(self.root).encode(),
b"project_path",
b"index_path",
b"database_path",
b'"command"',
b'"module"',
b'"shell"',
b'"sql"',
):
self.assertNotIn(forbidden, request)
def test_worker_ignores_hostile_cwd_and_pythonpath_import_shadows(self) -> None:
shadow_root = Path(self.temporary.name) / "shadow"
shadow_package = shadow_root / "docforge"
shadow_package.mkdir(parents=True)
sentinel = shadow_root / "executed"
shadow_package.joinpath("__init__.py").write_text(
"from pathlib import Path\n"
f"Path({str(sentinel)!r}).write_text('executed', encoding='utf-8')\n",
encoding="utf-8",
)
with (
contextlib.chdir(shadow_root),
mock.patch.dict(
os.environ,
{"PYTHONPATH": str(shadow_root)},
clear=False,
),
):
result = render_projection_in_worker(self.manual_package)
self.assertEqual("manual", result.receipt.document["kind"])
self.assertFalse(sentinel.exists())
def test_parent_accepts_only_valid_packages_and_closed_renderer_versions(self) -> None:
with self.assertRaises(TypeError):
render_projection_in_worker({}) # type: ignore[arg-type]
unsupported = ProjectionPackageV1.create(
kind="manual",
plan=self.manual_plan,
renderer={"renderer_id": "generic_html", "renderer_version": "other"},
components=[{"component_id": "manual.document@1"}],
assets=copy.deepcopy(self.manual_package.document["assets"]),
output_policy=copy.deepcopy(self.manual_package.document["output_policy"]),
)
with (
mock.patch("docforge.projection_worker._invoke_worker") as invoke,
self.assertRaises(DocForgeError) as raised,
):
render_projection_in_worker(unsupported)
self.assertEqual("unsupported_renderer", raised.exception.code)
invoke.assert_not_called()
permissive_allowance = ProjectionPackageV1.create(
kind="manual",
plan=self.manual_plan,
renderer={
"renderer_id": "generic_html",
"renderer_version": self.manual_version,
},
components=[{"component_id": "manual.document@1"}],
assets=copy.deepcopy(self.manual_package.document["assets"]),
output_policy={
"artifact_ids": ["manual.html"],
"max_total_bytes": MAX_WORKER_ARTIFACT_BYTES + 1,
},
)
rendered = render_projection_in_worker(permissive_allowance)
self.assertEqual(
ManualHtmlRenderer(self.manual_version)
.render(permissive_allowance)
.artifacts[0]
.content,
rendered.artifacts[0].content,
)
def test_process_timeout_exit_and_signal_fail_closed(self) -> None:
failures = (
(
subprocess.TimeoutExpired(["worker"], 30),
"projection_worker_timeout",
),
(
subprocess.CompletedProcess(["worker"], 2, stdout=b""),
"projection_worker_failure",
),
(
subprocess.CompletedProcess(["worker"], -9, stdout=b""),
"projection_worker_failure",
),
)
for outcome, code in failures:
with self.subTest(outcome=type(outcome).__name__):
patch = (
mock.patch(
"docforge.projection_worker._invoke_worker",
side_effect=outcome,
)
if isinstance(outcome, BaseException)
else mock.patch(
"docforge.projection_worker._invoke_worker",
return_value=outcome,
)
)
with patch, self.assertRaises(DocForgeError) as raised:
render_projection_in_worker(self.manual_package)
self.assertEqual(code, raised.exception.code)
def test_malformed_trailing_and_oversized_responses_fail_closed(self) -> None:
valid = _child_response(self.manual_package)
malformed = (
b"",
b"{}",
b"not-json\n",
b'{ "schema_version": 1 }\n',
valid + b"{}\n",
b"x" * (MAX_WORKER_RESPONSE_BYTES + 1),
)
for response in malformed:
with (
self.subTest(length=len(response)),
mock.patch(
"docforge.projection_worker._invoke_worker",
return_value=subprocess.CompletedProcess(
["worker"],
0,
stdout=response,
),
),
self.assertRaises(DocForgeError) as raised,
):
render_projection_in_worker(self.manual_package)
self.assertEqual("projection_worker_failure", raised.exception.code)
def test_wrong_artifact_and_receipt_evidence_fail_closed(self) -> None:
valid = json.loads(_child_response(self.manual_package))
cases: dict[str, dict[str, object]] = {}
wrong_content = copy.deepcopy(valid)
wrong_content["artifacts"][0]["content_base64"] = base64.b64encode(b"changed").decode()
cases["content_hash"] = wrong_content
wrong_id = copy.deepcopy(valid)
wrong_id["artifacts"][0]["artifact_id"] = "other.html"
cases["artifact_id"] = wrong_id
missing_artifact = copy.deepcopy(valid)
missing_artifact["artifacts"] = []
cases["artifact_count"] = missing_artifact
wrong_media_type = copy.deepcopy(valid)
wrong_media_type["artifacts"][0]["media_type"] = "application/octet-stream"
cases["media_type"] = wrong_media_type
bad_base64 = copy.deepcopy(valid)
bad_base64["artifacts"][0]["content_base64"] = "***"
cases["base64"] = bad_base64
wrong_package = copy.deepcopy(valid)
receipt = wrong_package["receipt"]
replacement = ProjectionReceiptV1.create(
kind="manual",
package_id="0" * 64,
plan_id=receipt["plan_id"],
renderer=receipt["renderer"],
artifacts=receipt["artifacts"],
diagnostics=receipt["diagnostics"],
timing=receipt["timing"],
peak_memory_bytes=receipt["peak_memory_bytes"],
)
wrong_package["receipt"] = replacement.as_dict()
cases["package_id"] = wrong_package
zero_peak = copy.deepcopy(valid)
receipt = zero_peak["receipt"]
replacement = ProjectionReceiptV1.create(
kind="manual",
package_id=receipt["package_id"],
plan_id=receipt["plan_id"],
renderer=receipt["renderer"],
artifacts=receipt["artifacts"],
diagnostics=receipt["diagnostics"],
timing=receipt["timing"],
peak_memory_bytes=0,
)
zero_peak["receipt"] = replacement.as_dict()
cases["peak_memory"] = zero_peak
wrong_size = copy.deepcopy(valid)
receipt = wrong_size["receipt"]
artifacts = copy.deepcopy(receipt["artifacts"])
artifacts[0]["bytes"] += 1
replacement = ProjectionReceiptV1.create(
kind="manual",
package_id=receipt["package_id"],
plan_id=receipt["plan_id"],
renderer=receipt["renderer"],
artifacts=artifacts,
diagnostics=receipt["diagnostics"],
timing=receipt["timing"],
peak_memory_bytes=receipt["peak_memory_bytes"],
)
wrong_size["receipt"] = replacement.as_dict()
cases["artifact_size"] = wrong_size
configured_oversize = copy.deepcopy(valid)
maximum = self.manual_package.document["output_policy"]["max_total_bytes"]
configured_oversize["artifacts"][0]["content_base64"] = base64.b64encode(
b"x" * (maximum + 1)
).decode()
cases["configured_aggregate"] = configured_oversize
for name, document in cases.items():
response = canonical_projection_bytes(document) + b"\n"
with (
self.subTest(name=name),
mock.patch(
"docforge.projection_worker._invoke_worker",
return_value=subprocess.CompletedProcess(
["worker"],
0,
stdout=response,
),
),
self.assertRaises(DocForgeError) as raised,
):
render_projection_in_worker(self.manual_package)
self.assertEqual("projection_worker_failure", raised.exception.code)
def test_child_rejects_noncanonical_invalid_and_trailing_requests(self) -> None:
valid = canonical_projection_bytes(self.manual_package.as_dict()) + b"\n"
requests = (
b"{}\n",
b'{ "schema_version": 1 }\n',
valid + b"{}\n",
)
for request in requests:
with self.subTest(length=len(request)):
completed = subprocess.run(
[sys.executable, "-m", "docforge.projection_worker"],
cwd=ROOT,
input=request,
capture_output=True,
check=False,
timeout=10,
)
self.assertEqual(2, completed.returncode)
self.assertEqual(b"", completed.stdout)
if __name__ == "__main__":
unittest.main()

View file

@ -94,6 +94,19 @@ PUBLIC_IMPORTS = {
"capability_mode",
"compose_effective_policy",
),
"docforge.projection_policy": (
"ProjectionPolicyV2",
"compose_projection_policy",
"validate_live_viewer_projection_mode",
"validate_manual_projection_mode",
"validate_portable_graph_projection_mode",
),
"docforge.projection_fragments": (
"FragmentKey",
"FragmentRecord",
"ProjectionFragmentCache",
"fragment_semantic_hash",
),
"docforge.projection_contract": (
"GraphViewPlanV1",
"ManualRenderPlanV1",
@ -104,6 +117,7 @@ PUBLIC_IMPORTS = {
"canonical_projection_bytes",
"projection_hash",
),
"docforge.projection_worker": ("render_projection_in_worker",),
"docforge.retrieval": (
"ContextCapsuleV1",
"RetrievalPlanV1",
@ -186,6 +200,8 @@ EXPECTED_MCP_TOOLS = {
"docforge_rebase_changeset",
"docforge_register_changes",
"docforge_render_status",
"docforge_graph_plan",
"docforge_graph_render_status",
"docforge_search",
"docforge_stop_visualization",
"docforge_sync",
@ -242,6 +258,9 @@ class PublicContractTests(unittest.TestCase):
)
self.assertIn("--project-root", completed.stdout)
self.assertIn("--no-ast", completed.stdout)
self.assertIn("--manual-render-policy", completed.stdout)
self.assertIn("--portable-graph-policy", completed.stdout)
self.assertIn("--live-viewer-policy", completed.stdout)
def test_published_schemas_validate_their_current_contract_examples(self) -> None:
for path in sorted(SCHEMAS.glob("*.json")):

View file

@ -55,6 +55,14 @@ class DocForgeRenderingTests(unittest.TestCase):
self.assertEqual("missing", missing["outputs"][0]["state"])
first = service.render("manual")
projection_receipt = first["output"]["projection_receipt"]
self.assertIsInstance(projection_receipt, dict)
assert isinstance(projection_receipt, dict)
self.assertEqual("manual", projection_receipt["kind"])
self.assertEqual(
first["output"]["actual_output_hash"],
projection_receipt["artifacts"][0]["sha256"],
)
output = root / ".docforge/rendered/manual.html"
first_bytes = output.read_bytes()
second = service.render("manual")
@ -191,7 +199,12 @@ class DocForgeRenderingTests(unittest.TestCase):
self.assertEqual("receipt_corrupt", corrupt["outputs"][0]["reason"])
def test_render_receipt_schema_and_renderer_version_fail_closed(self) -> None:
for mutation in ("missing_hash", "renderer_version", "file_identity"):
for mutation in (
"missing_hash",
"renderer_version",
"file_identity",
"projection_artifact",
):
with self.subTest(mutation=mutation), tempfile.TemporaryDirectory() as directory:
root = self.copy_fixture("alpha", Path(directory))
service = RenderService(Project.open(root))
@ -202,6 +215,8 @@ class DocForgeRenderingTests(unittest.TestCase):
receipt.pop("output_hash")
elif mutation == "renderer_version":
receipt["renderer_version"] = "obsolete"
elif mutation == "projection_artifact":
receipt["projection_receipt"]["artifacts"][0]["sha256"] = "0" * 64
else:
receipt["output_file"].pop("ctime_ns")
receipt_path.write_text(
@ -401,6 +416,19 @@ class DocForgeRenderingTests(unittest.TestCase):
with self.assertRaisesRegex(DocForgeError, "configured limit") as limit_error:
limit_service.render("manual")
self.assertEqual("render_too_large", limit_error.exception.code)
excessive = self.copy_fixture("alpha", Path(directory) / "excessive")
excessive_descriptor = excessive / ".docforge/project.toml"
excessive_descriptor.write_text(
excessive_descriptor.read_text(encoding="utf-8").replace(
"max_changeset_bytes = 100000",
"max_changeset_bytes = 100000\nmax_render_bytes = 20000001",
),
encoding="utf-8",
)
permissive_limit = RenderService(Project.open(excessive)).render("manual")
self.assertEqual("current", permissive_limit["receipt"]["state"])
self.assertTrue((excessive / ".docforge/rendered/manual.html").is_file())
self.assertFalse((limit_root / ".docforge/rendered/manual.html").exists())
template_limit_root = self.copy_fixture("alpha", parent / "template-limit")