Files
mizan/backends/mizan-ts/tests/transport.test.ts
Ryth Azhur 6c5f6f1fba AFI parity: close all 35 gaps — every adapter wires every AFI-common capability
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>
2026-06-04 13:44:35 -04:00

132 lines
4.8 KiB
TypeScript

/**
* Session-init + WebSocket transport tests.
*
* session-init returns the `{ csrfToken }` no-store shape at parity with the
* Django/FastAPI/Axum session endpoint. The WebSocket transport drives the
* SAME dispatch core the HTTP path uses, so a function exposed over WS behaves
* identically — invalidation, auth, and not-found all carry through.
*/
import { describe, test, expect, beforeEach } from 'bun:test'
import {
ReactContext,
client,
clearRegistry,
handleSessionInit,
sessionInitRoute,
SESSION_INIT_PATH,
handleWebSocketMessage,
serveWebSocket,
type Identity,
type WebSocketLike,
} from '../src'
describe('session-init', () => {
test('returns { csrfToken: null } with no-store', () => {
const r = handleSessionInit()
expect(r.status).toBe(200)
expect(r.body).toEqual({ csrfToken: null })
expect(r.headers['Cache-Control']).toBe('no-store')
expect(r.headers['Content-Type']).toBe('application/json')
})
test('embeds a host-provided CSRF token', () => {
const r = handleSessionInit('tok-123')
expect(r.body).toEqual({ csrfToken: 'tok-123' })
})
test('route descriptor mounts GET /session/ (parity with Django/FastAPI/Axum)', () => {
expect(SESSION_INIT_PATH).toBe('/session/')
expect(sessionInitRoute.path).toBe('/session/')
expect(sessionInitRoute.method).toBe('GET')
// The wired handler returns the session shape.
expect(sessionInitRoute.handler().body).toEqual({ csrfToken: null })
})
})
describe('WebSocket transport', () => {
beforeEach(() => clearRegistry())
const UserCtx = new ReactContext('user')
function setup() {
client({ context: UserCtx, websocket: true }, async function user_profile(user_id: number) {
return { user_id, name: `user_${user_id}` }
})
client({ affects: UserCtx, websocket: true }, async function update_profile(user_id: number, name: string) {
return { ok: true, user_id, name }
})
}
test('call frame routes through mutation dispatch + carries invalidation', async () => {
setup()
const reply = await handleWebSocketMessage({
id: 1,
type: 'call',
fn: 'update_profile',
args: { user_id: 5, name: 'X' },
})
expect(reply.id).toBe(1)
expect(reply.result).toEqual({ ok: true, user_id: 5, name: 'X' })
expect(reply.invalidate).toBeDefined()
expect(reply.invalidate[0].context).toBe('user')
expect(reply.invalidate[0].params.user_id).toBe(5)
})
test('fetch frame routes through context bundle', async () => {
setup()
const reply = await handleWebSocketMessage({
id: 2,
type: 'fetch',
context: 'user',
params: { user_id: '7' },
})
expect(reply.id).toBe(2)
expect(reply.result.user_profile).toEqual({ user_id: '7', name: 'user_7' })
})
test('unknown function returns an error frame, not a throw', async () => {
const reply = await handleWebSocketMessage({ id: 3, type: 'call', fn: 'nope' })
expect(reply.error).toBeDefined()
expect(reply.error!.code).toBe('NOT_FOUND')
expect(reply.id).toBe(3)
})
test('auth enforcement carries over the WS transport', async () => {
client({ auth: true, websocket: true }, async function secret() {
return { ok: true }
})
const anon: Identity = { isAuthenticated: false, isStaff: false, isSuperuser: false, id: null }
const reply = await handleWebSocketMessage({ id: 4, type: 'call', fn: 'secret' }, anon)
expect(reply.error!.code).toBe('UNAUTHORIZED')
})
test('malformed JSON frame → error', async () => {
const reply = await handleWebSocketMessage('{not json')
expect(reply.error!.code).toBe('BAD_REQUEST')
})
test('serveWebSocket wires a connection and replies as JSON', async () => {
setup()
const sent: string[] = []
let listener: ((e: { data: any }) => void) | null = null
const ws: WebSocketLike = {
send: (d) => sent.push(d),
addEventListener: (_t, l) => {
listener = l
},
}
serveWebSocket(ws)
expect(listener).not.toBeNull()
// Drive a message through the wired listener.
await listener!({ data: JSON.stringify({ id: 9, type: 'fetch', context: 'user', params: { user_id: '3' } }) })
// Give the async handler a tick to resolve + send.
await new Promise((r) => setTimeout(r, 0))
expect(sent.length).toBe(1)
const reply = JSON.parse(sent[0])
expect(reply.id).toBe(9)
expect(reply.result.user_profile.name).toBe('user_3')
})
})