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>
This commit is contained in:
131
backends/mizan-ts/tests/transport.test.ts
Normal file
131
backends/mizan-ts/tests/transport.test.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* 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')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user