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>
91 lines
3.3 KiB
Python
91 lines
3.3 KiB
Python
"""Notes CRUD driven over HTTP against a live Django server."""
|
|
|
|
from django.test import LiveServerTestCase
|
|
|
|
from tests.live_http import LiveRPCMixin
|
|
|
|
|
|
class NotesCRUDTests(LiveRPCMixin, LiveServerTestCase):
|
|
def setUp(self):
|
|
self.session_init()
|
|
|
|
def test_list_notes_empty(self):
|
|
result = self.result("list_notes")
|
|
|
|
self.assertEqual(result["notes"], [])
|
|
self.assertEqual(result["count"], 0)
|
|
|
|
def test_create_note(self):
|
|
result = self.result("create_note", {"title": "First Note", "content": "Hello!"})
|
|
|
|
self.assertEqual(result["title"], "First Note")
|
|
self.assertEqual(result["content"], "Hello!")
|
|
self.assertFalse(result["pinned"])
|
|
self.assertIn("id", result)
|
|
self.assertIn("created_at", result)
|
|
|
|
def test_create_and_list(self):
|
|
self.result("create_note", {"title": "Note A"})
|
|
self.result("create_note", {"title": "Note B"})
|
|
|
|
result = self.result("list_notes")
|
|
self.assertEqual(result["count"], 2)
|
|
titles = [n["title"] for n in result["notes"]]
|
|
self.assertIn("Note A", titles)
|
|
self.assertIn("Note B", titles)
|
|
|
|
def test_get_note_by_id(self):
|
|
note_id = self.result("create_note", {"title": "Get Me", "content": "Specific"})["id"]
|
|
|
|
result = self.result("get_note", {"id": note_id})
|
|
self.assertEqual(result["id"], note_id)
|
|
self.assertEqual(result["title"], "Get Me")
|
|
|
|
def test_update_note(self):
|
|
note_id = self.result("create_note", {"title": "Original"})["id"]
|
|
|
|
result = self.result("update_note", {"id": note_id, "title": "Updated"})
|
|
self.assertEqual(result["title"], "Updated")
|
|
|
|
def test_update_note_pin(self):
|
|
note_id = self.result("create_note", {"title": "Pin Me"})["id"]
|
|
|
|
result = self.result("update_note", {"id": note_id, "pinned": True})
|
|
self.assertTrue(result["pinned"])
|
|
|
|
def test_delete_note(self):
|
|
note_id = self.result("create_note", {"title": "Delete Me"})["id"]
|
|
|
|
self.assertTrue(self.result("delete_note", {"id": note_id})["deleted"])
|
|
|
|
# get_note raises ValueError for a missing row, which the executor maps
|
|
# onto the generic internal-error envelope rather than a 404.
|
|
reply = self.call("get_note", {"id": note_id})
|
|
self.assertEqual(reply.status, 500)
|
|
self.assertEqual(reply.body["code"], "INTERNAL_ERROR")
|
|
|
|
def test_delete_absent_note_reports_not_deleted(self):
|
|
result = self.result("delete_note", {"id": 424242})
|
|
|
|
self.assertFalse(result["deleted"])
|
|
|
|
def test_pinned_notes_sort_first(self):
|
|
self.result("create_note", {"title": "Unpinned"})
|
|
self.result("create_note", {"title": "Pinned", "pinned": True})
|
|
|
|
result = self.result("list_notes")
|
|
self.assertEqual(result["notes"][0]["title"], "Pinned")
|
|
|
|
def test_full_lifecycle(self):
|
|
note_id = self.result("create_note", {"title": "Lifecycle", "content": "v1"})["id"]
|
|
|
|
self.result("update_note", {"id": note_id, "content": "v2"})
|
|
self.result("update_note", {"id": note_id, "pinned": True})
|
|
|
|
fetched = self.result("get_note", {"id": note_id})
|
|
self.assertEqual(fetched["title"], "Lifecycle")
|
|
self.assertEqual(fetched["content"], "v2")
|
|
self.assertTrue(fetched["pinned"])
|
|
|
|
self.assertTrue(self.result("delete_note", {"id": note_id})["deleted"])
|