The conformance board (tests/afi/test_capability_parity.py) is now fully green: 90 capability cells + 4 meta-locks + 3 codegen byte-parity = 97 passed. The gaps the prose table used to launder as "Django-only" / "out of scope" are wired, against the pinned-spec model (single-authored spec, byte-identical conformance across languages) — never per-language reimplementation. FastAPI — edge_manifest + PSR (logic single-sourced in mizan_core.manifest), WebSocket RPC (/ws/ through the shared dispatch), SSR (the framework-agnostic SSRBridge relocated to mizan_core.ssr; Django rides it from there), Shapes (SQLAlchemy projection, same declaration surface as django-readers), Forms (Pydantic schema/validate/submit). Rust (Axum + Tauri + cores/mizan-rust) — X-Mizan-Invalidate header, auth= enforcement, origin HMAC cache, edge manifest + PSR, WebSocket handler / IPC subscription channel, multipart upload, SSR bridge, Shapes, Forms; JWT/MWT mint+verify and cache-key derivation byte-pinned to the Python reference (cache_keys_pin, token_pin, invalidate_header_pin). TypeScript — a KDL IR emitter byte-identical to the Python build_ir (so a TS backend can feed the codegen — the largest gap), multipart upload, session-init, WebSocket transport, SSR bridge, JWT/MWT mint (pinned to Python), Shapes, Forms. Verified in the merged tree: core 25, fastapi 74, django 353/21-skip, mizan-rust (incl. cross-language pins) green, axum 10, tauri 8, mizan-ts 103/2-skip. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
102 lines
4.2 KiB
TypeScript
102 lines
4.2 KiB
TypeScript
/**
|
|
* SSR bridge tests — spawn + drive a JSON-RPC worker subprocess.
|
|
*
|
|
* The bridge's contract is the newline-delimited JSON-RPC protocol over a
|
|
* spawned worker (ready handshake, id-correlated render/ping, error frames,
|
|
* timeout, restart). Two peers exercise it:
|
|
*
|
|
* - a self-contained protocol stub (`stub-worker.mjs`, plain Node) — always
|
|
* runs, proving the full subprocess machinery independent of any install;
|
|
* - the REAL Bun worker (`workers/mizan-ssr/src/worker.tsx`) rendering an
|
|
* actual React component — runs when `bun` + the worker's deps are present.
|
|
*/
|
|
|
|
import { describe, test, expect, afterEach } from 'bun:test'
|
|
import { execFileSync } from 'child_process'
|
|
import { existsSync } from 'fs'
|
|
import { resolve } from 'path'
|
|
import { SSRBridge } from '../src'
|
|
|
|
const HERE = import.meta.dir
|
|
const REPO_ROOT = resolve(HERE, '../../..')
|
|
const STUB_WORKER = resolve(HERE, 'fixtures/stub-worker.mjs')
|
|
const HELLO_TSX = resolve(HERE, 'fixtures/Hello.tsx')
|
|
const REAL_WORKER = resolve(REPO_ROOT, 'workers/mizan-ssr/src/worker.tsx')
|
|
|
|
// The real worker renders an actual React component. bun resolves `react`
|
|
// from the COMPONENT file's tree, so the fixture resolves it via mizan-ts's
|
|
// own react devDependency (installed alongside this package).
|
|
const BUN_OK = (() => {
|
|
try {
|
|
execFileSync('bun', ['--version'], { stdio: 'ignore' })
|
|
return existsSync(resolve(HERE, '../node_modules/react/package.json'))
|
|
} catch {
|
|
return false
|
|
}
|
|
})()
|
|
|
|
let bridge: SSRBridge | null = null
|
|
afterEach(() => {
|
|
bridge?.shutdown()
|
|
bridge = null
|
|
})
|
|
|
|
describe('SSRBridge — stub worker (Node, no React)', () => {
|
|
test('waits for ready, then renders with id correlation', async () => {
|
|
bridge = new SSRBridge({ worker: STUB_WORKER, runtime: 'node', runtimeArgs: [] })
|
|
const r = await bridge.render('/abs/Card.tsx', { title: 'Hi', n: 3 })
|
|
expect(r.html).toBe('<div data-file="/abs/Card.tsx">{"title":"Hi","n":3}</div>')
|
|
})
|
|
|
|
test('ping health check', async () => {
|
|
bridge = new SSRBridge({ worker: STUB_WORKER, runtime: 'node', runtimeArgs: [] })
|
|
expect(await bridge.ping()).toBe(true)
|
|
})
|
|
|
|
test('concurrent renders stay correlated', async () => {
|
|
bridge = new SSRBridge({ worker: STUB_WORKER, runtime: 'node', runtimeArgs: [] })
|
|
const [a, b, c] = await Promise.all([
|
|
bridge.render('/a.tsx', { k: 'a' }),
|
|
bridge.render('/b.tsx', { k: 'b' }),
|
|
bridge.render('/c.tsx', { k: 'c' }),
|
|
])
|
|
expect(a.html).toContain('"k":"a"')
|
|
expect(b.html).toContain('"k":"b"')
|
|
expect(c.html).toContain('"k":"c"')
|
|
})
|
|
|
|
test('worker error frame surfaces as a thrown error', async () => {
|
|
bridge = new SSRBridge({ worker: STUB_WORKER, runtime: 'node', runtimeArgs: [] })
|
|
await expect(bridge.render('/boom.tsx', {})).rejects.toThrow('SSR render failed')
|
|
})
|
|
|
|
test('restarts after the worker exits', async () => {
|
|
bridge = new SSRBridge({ worker: STUB_WORKER, runtime: 'node', runtimeArgs: [] })
|
|
const first = await bridge.render('/one.tsx', { k: 1 })
|
|
expect(first.html).toContain('"k":1')
|
|
bridge.shutdown() // simulate a crashed/stopped worker
|
|
const second = await bridge.render('/two.tsx', { k: 2 })
|
|
expect(second.html).toContain('"k":2')
|
|
})
|
|
|
|
test('startup timeout when the worker never signals ready', async () => {
|
|
// `true` exits immediately without a ready frame → start times out.
|
|
bridge = new SSRBridge({ worker: '/dev/null', runtime: 'true', runtimeArgs: [], timeout: 0.3 })
|
|
await expect(bridge.render('/x.tsx', {})).rejects.toThrow()
|
|
})
|
|
})
|
|
|
|
describe('SSRBridge — real Bun worker (renderToString)', () => {
|
|
test.skipIf(!BUN_OK)('renders a React component to HTML', async () => {
|
|
bridge = new SSRBridge({ worker: REAL_WORKER, runtime: 'bun' })
|
|
const r = await bridge.render(HELLO_TSX, { name: 'Ryth' })
|
|
expect(r.html).toContain('Hello, Ryth!')
|
|
expect(r.html).toContain('class="greeting"')
|
|
})
|
|
|
|
test.skipIf(!BUN_OK)('ping on the real worker', async () => {
|
|
bridge = new SSRBridge({ worker: REAL_WORKER, runtime: 'bun' })
|
|
expect(await bridge.ping()).toBe(true)
|
|
})
|
|
})
|