/** * KDL IR byte-parity — the mizan-ts `buildIr()` against the canonical Python * `build_ir()` (`cores/mizan-python/src/mizan_core/ir.py`). * * The IR is the codegen contract. A TypeScript backend can only feed * `protocol/mizan-codegen` if it emits the same KDL the Python/Rust backends * emit for the same registry. This test reconstructs the AFI fixture in both * languages, subprocesses the live Python emitter, and asserts byte-equality — * the same discipline `protocol/mizan-codegen/tests/python_parity.rs` applies. */ import { describe, test, expect, beforeEach } from 'bun:test' import { execFileSync } from 'child_process' import { existsSync } from 'fs' import { resolve } from 'path' import { buildIr, clearRegistry } from '../src' import { registerFixture } from './ir-fixture' const REPO_ROOT = resolve(import.meta.dir, '../../..') const MIZAN_PYTHON = resolve(REPO_ROOT, 'cores/mizan-python') /** * Reconstruct the AFI fixture in Python via `mizan_core` only (no backend * adapter dependency) and emit `build_ir()`. This is the cross-language oracle: * the same registrations the TS fixture makes, run through the reference * emitter. */ const PY_FIXTURE = String.raw` import sys from typing import Optional from pydantic import BaseModel from mizan_core.client.function import client from mizan_core import registry as reg from mizan_core.ir import build_ir reg.clear_registry() class EchoOutput(BaseModel): message: str class WhoamiOutput(BaseModel): email: str authenticated: bool class ProfileOutput(BaseModel): user_id: int name: str class OrderOutput(BaseModel): id: int user_id: int total: int class StatusOutput(BaseModel): ok: bool @client def echo(request, text: str) -> EchoOutput: ... @client def whoami(request) -> WhoamiOutput: ... @client(context="user") def user_profile(request, user_id: int) -> ProfileOutput: ... @client(context="user") def user_orders(request, user_id: int) -> list[OrderOutput]: ... @client(affects="user") def update_profile(request, user_id: int, name: str) -> StatusOutput: ... @client def find_user(request, user_id: int) -> Optional[ProfileOutput]: ... @client(merge="user") def rename_user(request, user_id: int, name: str) -> ProfileOutput: ... for f in [echo, whoami, user_profile, user_orders, update_profile, find_user, rename_user]: reg.register(f, f.__name__) sys.stdout.write(build_ir()) ` function pythonBuildIr(): string { return execFileSync( 'uv', ['run', '--project', MIZAN_PYTHON, 'python', '-c', PY_FIXTURE], { encoding: 'utf-8' }, ) } const UV_AVAILABLE = (() => { try { execFileSync('uv', ['--version'], { stdio: 'ignore' }) return existsSync(resolve(MIZAN_PYTHON, 'pyproject.toml')) } catch { return false } })() describe('KDL IR — buildIr()', () => { beforeEach(() => clearRegistry()) test('emits the canonical type / function / context sections', () => { registerFixture() const kdl = buildIr() // Types are alphabetical; output structs renamed to Output. expect(kdl).toContain('type "OrderOutput" {') expect(kdl).toContain('type "echoInput" {') expect(kdl).toContain('type "findUserOutput" {') expect(kdl).toContain('type "userOrdersOutput" {') // Functions alphabetical, with transport + context/affects/merge leaves. expect(kdl).toContain('function "echo" {') expect(kdl).toContain(' camel "echo"') expect(kdl).toContain(' has-input #true') expect(kdl).toContain(' output-nullable #true') // find_user expect(kdl).toContain(' affects "user"') // update_profile expect(kdl).toContain(' merge "user"') // rename_user // Context section with shared param elevation. expect(kdl).toContain('context "user" {') expect(kdl).toContain(' shared-by "user_orders"') expect(kdl).toContain(' shared-by "user_profile"') }) test('has-input #false for a no-arg function', () => { registerFixture() const kdl = buildIr() const whoami = kdl.slice(kdl.indexOf('function "whoami" {')) expect(whoami).toContain('has-input #false') expect(whoami).not.toContain('input "whoamiInput"') }) test.skipIf(!UV_AVAILABLE)( 'byte-identical to the Python build_ir() (cores/mizan-python)', () => { registerFixture() const tsKdl = buildIr() const pyKdl = pythonBuildIr() // Line-by-line first so a divergence names the offending line. const tsLines = tsKdl.split('\n') const pyLines = pyKdl.split('\n') const n = Math.max(tsLines.length, pyLines.length) for (let i = 0; i < n; i++) { if (tsLines[i] !== pyLines[i]) { throw new Error( `KDL diverges at line ${i + 1}:\n` + ` python: ${JSON.stringify(pyLines[i])}\n` + ` ts: ${JSON.stringify(tsLines[i])}`, ) } } expect(tsKdl).toBe(pyKdl) }, ) })