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

Make render status receipt based

This commit is contained in:
Andraxion 2026-07-29 04:42:55 -04:00
parent 21c4992f9c
commit 0fe968c475
10 changed files with 913 additions and 27 deletions

View file

@ -77,6 +77,171 @@ class DocForgeRenderingTests(unittest.TestCase):
self.assertEqual("stale", stale["outputs"][0]["state"])
self.assertEqual(first_bytes, output.read_bytes())
def test_warm_render_status_uses_only_publication_receipts(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = self.copy_fixture("alpha", Path(directory))
rendered = RenderService(Project.open(root)).render("manual")
self.assertEqual("current", rendered["receipt"]["state"])
project = Project.open(root)
service = RenderService(project)
with (
mock.patch.object(
project,
"load",
side_effect=AssertionError("receipt status must not load canonical source"),
),
mock.patch.object(
service,
"_prepare",
side_effect=AssertionError("receipt status must not render"),
),
):
current = service.status("manual")
self.assertEqual("current", current["state"])
self.assertEqual("receipt", current["verification"])
self.assertEqual("current", current["outputs"][0]["state"])
output = root / ".docforge/rendered/manual.html"
output.write_bytes(output.read_bytes() + b"\n")
changed_output = service.status("manual")
self.assertEqual("stale", changed_output["state"])
self.assertEqual("output_changed", changed_output["outputs"][0]["reason"])
RenderService(Project.open(root)).render("manual")
template = root / "docs/templates/manual.html"
template.write_text(
template.read_text(encoding="utf-8") + "\n",
encoding="utf-8",
)
changed_template = service.status("manual")
self.assertEqual("template_changed", changed_template["outputs"][0]["reason"])
def test_render_receipt_failures_are_degraded_after_output_publication(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = self.copy_fixture("alpha", Path(directory))
service = RenderService(Project.open(root))
with mock.patch.object(
service,
"_publish_receipt",
side_effect=DocForgeError(
"render_receipt_failure",
"Synthetic receipt failure",
),
):
result = service.render("manual")
self.assertEqual("degraded", result["state"])
self.assertEqual("published", result["publication"])
self.assertEqual("failed", result["receipt"]["state"])
self.assertTrue((root / ".docforge/rendered/manual.html").is_file())
def test_render_receipt_refuses_post_render_input_and_output_changes(self) -> None:
for changed in ("template", "output", "source"):
with self.subTest(changed=changed), tempfile.TemporaryDirectory() as directory:
root = self.copy_fixture("alpha", Path(directory))
service = RenderService(Project.open(root))
publish = service._publish_receipt
def mutate_then_publish(
snapshot,
view,
prepared,
*,
changed_kind=changed,
project_root=root,
publish_receipt=publish,
):
if changed_kind == "template":
target = project_root / "docs/templates/manual.html"
elif changed_kind == "output":
target = project_root / ".docforge/rendered/manual.html"
else:
target = project_root / "docs/content/workflow.md"
target.write_bytes(target.read_bytes() + b"\nChanged before receipt.\n")
return publish_receipt(snapshot, view, prepared)
with mock.patch.object(
service,
"_publish_receipt",
side_effect=mutate_then_publish,
):
result = service.render("manual")
self.assertEqual("degraded", result["state"])
self.assertEqual("published", result["publication"])
self.assertNotEqual("current", service.status("manual")["state"])
self.assertEqual("stale", service.deep_status("manual")["state"])
def test_missing_and_corrupt_render_receipts_are_conservative(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = self.copy_fixture("alpha", Path(directory))
service = RenderService(Project.open(root))
service.render("manual")
receipt = root / ".docforge/cache/render-receipts/manual.json"
receipt.unlink()
missing = service.status("manual")
self.assertEqual("unverified", missing["outputs"][0]["state"])
self.assertEqual("receipt_missing", missing["outputs"][0]["reason"])
receipt.write_text("{not-json", encoding="utf-8")
corrupt = service.status("manual")
self.assertEqual("unverified", corrupt["outputs"][0]["state"])
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"):
with self.subTest(mutation=mutation), tempfile.TemporaryDirectory() as directory:
root = self.copy_fixture("alpha", Path(directory))
service = RenderService(Project.open(root))
service.render("manual")
receipt_path = root / ".docforge/cache/render-receipts/manual.json"
receipt = json.loads(receipt_path.read_text(encoding="utf-8"))
if mutation == "missing_hash":
receipt.pop("output_hash")
elif mutation == "renderer_version":
receipt["renderer_version"] = "obsolete"
else:
receipt["output_file"].pop("ctime_ns")
receipt_path.write_text(
json.dumps(receipt, sort_keys=True, indent=2) + "\n",
encoding="utf-8",
)
status = service.status("manual")
self.assertEqual("unverified", status["outputs"][0]["state"])
self.assertEqual(
"foreign_or_incompatible_receipt",
status["outputs"][0]["reason"],
)
def test_render_status_detects_change_between_bounded_captures(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = self.copy_fixture("alpha", Path(directory))
service = RenderService(Project.open(root))
service.render("manual")
receipt_status = service._receipt_status
calls = 0
def mutate_between_captures(descriptor, view, current_state):
nonlocal calls
calls += 1
if calls == 2:
output = root / ".docforge/rendered/manual.html"
output.write_bytes(output.read_bytes() + b"\n")
return receipt_status(descriptor, view, current_state)
with mock.patch.object(
service,
"_receipt_status",
side_effect=mutate_between_captures,
):
result = service.status("manual")
self.assertEqual("stale", result["state"])
self.assertNotEqual("current", result["outputs"][0]["state"])
def test_changeset_preview_is_deterministic_escaped_and_isolated(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = self.copy_fixture("alpha", Path(directory))
@ -312,6 +477,7 @@ class DocForgeRenderingTests(unittest.TestCase):
("render", "manual"),
("render-status", "manual"),
("preview", "cli-preview", "manual"),
("render-status", "manual", "--deep"),
)
results: list[dict] = []
for command in commands:
@ -322,6 +488,8 @@ class DocForgeRenderingTests(unittest.TestCase):
self.assertEqual("current", results[0]["state"])
self.assertEqual("current", results[1]["state"])
self.assertEqual("current", results[2]["state"])
self.assertEqual("receipt", results[1]["verification"])
self.assertEqual("deep", results[3]["verification"])
self.assertTrue((root / ".docforge/rendered/manual.html").is_file())
self.assertTrue((root / ".docforge/previews/cli-preview/manual.html").is_file())