Files
mizan/cores/mizan-python/tests/test_ir.py
Ryth Azhur 3aafec6dd4 A channel's message slots are named from the client, on every backend
The IR called them react-message and django-message, so a FastAPI channel had
to declare a DjangoMessage. They are client-message and server-message now,
and the direction words hold wherever a channel is declared: Params /
ClientMessage / ServerMessage, with mizan-core deriving <Pascal>Params and
friends so no backend names a type itself. Django's ReactChannel and
FastAPI's ReactChannel are both Channel.

mizan-fastapi never registered a channels extension, so build_ir() emitted no
channel at all and every payload type was invisible to codegen. It registers
one now. RegistryExtension is an ABC requiring all(), which is what the IR
reads — an extension that cannot enumerate its registrations no longer exists.

The gate that should have caught the rename could not: tests/afi registered no
channel because mizan-rust had no channel registry to register one in, so a
five-package rename of the wire contract passed byte-parity without a channel
byte crossing it. mizan-rust grows ChannelSlotKind, a CHANNELS slice, a
#[mizan::channel] macro, and KDL emission whose wire_to_pascal matches Python's
split; the AFI fixture now carries a channel with every slot and one with a
single slot, so all three backends prove the contract byte for byte.

MizanChannel held three Option<String> beside three has_*() predicates and
unwrapped them with defaults; it holds an ordered slot vector, so an absent
slot is absent rather than defaulted. The channels target emitted a React
hooks file that a stage1-only consumer could not compile — react emits that
now. The codegen's parity tests byte-compared emitted source against baselines
without ever compiling it: they compile the generated crate and run its tests,
import the generated Python package and call every method, and typecheck each
TypeScript target against a consumer.

Also fixed at source: app_visitor printed its import diagnostic to stdout, the
stream export_mizan_ir writes KDL to, so a failed import silently corrupted the
IR; the apps root was hardcoded to "apps"; _default_literal crashed build_ir on
any non-JSON-serializable field default; Django and mizan-core derived Pascal
names two different ways, disagreeing on every dotted channel name.

ir.py builds a document and renders templates/ir/document.kdl.j2 rather than
appending KDL strings with hand-tracked indentation, and named types resolve to
a fixed point — a model reachable only through a union branch was referenced by
a ref that no type block ever defined.

The rest is the write-gate's own classifiers run over the standing tree:
relative imports, silent swallows, Protocol contracts that should be ABCs,
emitters hand-rendering target source, catch-all arms over closed enums, and
comments narrating the project rather than the code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 14:03:19 -04:00

108 lines
3.8 KiB
Python

"""Unit tests for KDL IR emission. Every assertion runs on a real KDL parse tree."""
from unittest import TestCase
import ckdl
from pydantic import BaseModel
from mizan_core.client.function import client
from mizan_core.ir import build_ir
from mizan_core.registry import clear_registry, register
class Prefs(BaseModel):
live: bool = True
class Settings(BaseModel):
meta: Prefs = Prefs()
label: str = "plain"
quoted: str = 'a "b" \\ c\nd\te'
retries: int = 3
ratio: float = 0.5
enabled: bool = False
tags: list[str] = []
mapping: dict[str, str] = {}
note: str | None = None
who: str
def _struct_fields(document: ckdl.Document, type_name: str) -> dict[str, ckdl.Node]:
"""Field nodes of the named struct, keyed by field name."""
for node in document.nodes:
if node.name == "type" and node.args[0] == type_name:
for child in node.children:
if child.name == "struct":
return {field.args[0]: field for field in child.children}
raise AssertionError(f"no struct type {type_name!r} in:\n{document}")
class EmptyDocumentTests(TestCase):
"""The IR of an empty registry."""
def setUp(self):
clear_registry()
def tearDown(self):
clear_registry()
def test_empty_registry_emits_zero_bytes(self):
"""Nothing registered renders the empty document, not a blank line."""
self.assertEqual(build_ir(), "")
def test_empty_document_parses_to_zero_nodes(self):
"""The empty document is valid KDL carrying no nodes."""
self.assertEqual(len(ckdl.parse(build_ir()).nodes), 0)
class FieldDefaultTests(TestCase):
"""Which Pydantic field defaults reach the document as KDL literals."""
def setUp(self):
clear_registry()
@client
def get_settings(request) -> Settings:
return Settings(who="anyone")
register(get_settings, "get_settings")
self.fields = _struct_fields(
ckdl.parse(build_ir()), "getSettingsOutput"
)
def tearDown(self):
clear_registry()
def test_scalar_defaults_survive_the_round_trip(self):
"""bool, int, float and str defaults parse back to the Python values."""
self.assertEqual(self.fields["label"].properties["default"], "plain")
self.assertEqual(self.fields["retries"].properties["default"], 3)
self.assertEqual(self.fields["ratio"].properties["default"], 0.5)
self.assertEqual(self.fields["enabled"].properties["default"], False)
def test_string_default_escapes_round_trip(self):
"""Quotes, backslashes and control characters survive KDL escaping."""
self.assertEqual(
self.fields["quoted"].properties["default"], Settings.model_fields["quoted"].default
)
def test_model_valued_default_carries_no_literal(self):
"""A nested-model default has no KDL scalar form, so no `default` is emitted."""
meta = self.fields["meta"]
self.assertNotIn("default", meta.properties)
self.assertIs(meta.properties["required"], False)
def test_container_defaults_carry_no_literal(self):
"""List and dict defaults have no KDL scalar form either."""
self.assertNotIn("default", self.fields["tags"].properties)
self.assertNotIn("default", self.fields["mapping"].properties)
def test_none_default_carries_no_literal(self):
"""`= None` leaves the optional shape to say the field may be absent."""
self.assertNotIn("default", self.fields["note"].properties)
def test_required_field_has_no_required_property(self):
"""A required field is the default, so the property is left off entirely."""
self.assertNotIn("required", self.fields["who"].properties)
self.assertNotIn("default", self.fields["who"].properties)