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:
2026-06-04 13:44:35 -04:00
parent 58d2cb2848
commit 6c5f6f1fba
81 changed files with 9893 additions and 463 deletions

View File

@@ -0,0 +1,163 @@
/**
* Upload tests — multipart File-part binding + constraint enforcement.
*
* Mirrors mizan-fastapi/tests/test_upload.py: a multipart call binds file parts
* into the function's Upload-typed inputs, and `File(...)` constraints
* (max-size, content-type) reject at dispatch with a 400.
*/
import { describe, test, expect, beforeEach } from 'bun:test'
import {
client,
clearRegistry,
handleMultipartCall,
parseSize,
validateUpload,
UploadedFile,
type StructField,
} from '../src'
const uploadField = (name: string, opts: { maxSize?: number; contentTypes?: string[]; optional?: boolean; list?: boolean } = {}): StructField => {
let shape: any = { kind: 'upload', maxSize: opts.maxSize, contentTypes: opts.contentTypes }
if (opts.list) shape = { kind: 'list', inner: shape }
if (opts.optional) shape = { kind: 'optional', inner: shape }
return { name, required: !opts.optional, shape }
}
const intField = (name: string): StructField => ({ name, required: true, shape: { kind: 'primitive', primitive: 'integer' } })
function multipart(fn: string, args: Record<string, any>, files: Record<string, Blob | Blob[]>): FormData {
const form = new FormData()
form.set('fn', fn)
form.set('args', JSON.stringify(args))
for (const [key, val] of Object.entries(files)) {
for (const f of Array.isArray(val) ? val : [val]) form.append(key, f)
}
return form
}
describe('parseSize', () => {
test('parses human sizes', () => {
expect(parseSize('5MB')).toBe(5 * 1024 * 1024)
expect(parseSize('1KB')).toBe(1024)
expect(parseSize('2GB')).toBe(2 * 1024 ** 3)
expect(parseSize(123)).toBe(123)
expect(parseSize('500')).toBe(500)
})
})
describe('validateUpload', () => {
test('max-size rejection', () => {
const f = new UploadedFile('a.bin', 'application/octet-stream', new Uint8Array(100))
expect(validateUpload(f, { maxSize: 50 })).toContain('exceeds max size')
expect(validateUpload(f, { maxSize: 200 })).toBeNull()
})
test('content-type allowlist + wildcard', () => {
const png = new UploadedFile('a.png', 'image/png', new Uint8Array(1))
expect(validateUpload(png, { contentTypes: ['image/png'] })).toBeNull()
expect(validateUpload(png, { contentTypes: ['image/*'] })).toBeNull()
expect(validateUpload(png, { contentTypes: ['application/pdf'] })).toContain('not allowed')
})
})
describe('multipart dispatch', () => {
beforeEach(() => clearRegistry())
test('binds a file part into the Upload input', async () => {
let received: UploadedFile | null = null
client(
{
affects: 'avatars',
ir: { input: [intField('user_id'), uploadField('avatar', { contentTypes: ['image/png'] })] },
},
async function set_avatar(user_id: number, avatar: UploadedFile) {
received = avatar
return { ok: true, name: avatar.filename, bytes: avatar.size }
},
)
const form = multipart('set_avatar', { user_id: 5 }, {
avatar: new File([new Uint8Array([1, 2, 3, 4])], 'face.png', { type: 'image/png' }),
})
const r = await handleMultipartCall(form)
expect(r.status).toBe(200)
expect(r.body.result).toEqual({ ok: true, name: 'face.png', bytes: 4 })
expect(received).not.toBeNull()
expect(received!.read()).toEqual(new Uint8Array([1, 2, 3, 4]))
})
test('max-size violation rejects with 400', async () => {
client(
{ affects: 'avatars', ir: { input: [uploadField('avatar', { maxSize: 3 })] } },
async function set_avatar(_avatar: UploadedFile) {
return { ok: true }
},
)
const form = multipart('set_avatar', {}, {
avatar: new File([new Uint8Array([1, 2, 3, 4, 5])], 'big.bin', { type: 'application/octet-stream' }),
})
const r = await handleMultipartCall(form)
expect(r.status).toBe(400)
expect(r.body.message).toContain('avatar:')
expect(r.body.message).toContain('exceeds max size')
})
test('content-type violation rejects with 400', async () => {
client(
{ affects: 'avatars', ir: { input: [uploadField('avatar', { contentTypes: ['image/png'] })] } },
async function set_avatar(_avatar: UploadedFile) {
return { ok: true }
},
)
const form = multipart('set_avatar', {}, {
avatar: new File([new Uint8Array([1])], 'doc.pdf', { type: 'application/pdf' }),
})
const r = await handleMultipartCall(form)
expect(r.status).toBe(400)
expect(r.body.message).toContain('not allowed')
})
test('list upload binds multiple parts', async () => {
let count = 0
client(
{ affects: 'gallery', ir: { input: [uploadField('photos', { list: true })] } },
async function add_photos(photos: UploadedFile[]) {
count = photos.length
return { ok: true, count: photos.length }
},
)
const form = multipart('add_photos', {}, {
photos: [
new File([new Uint8Array([1])], 'a.png', { type: 'image/png' }),
new File([new Uint8Array([2])], 'b.png', { type: 'image/png' }),
],
})
const r = await handleMultipartCall(form)
expect(r.status).toBe(200)
expect(r.body.result.count).toBe(2)
expect(count).toBe(2)
})
test('missing fn → 400', async () => {
const form = new FormData()
form.set('args', '{}')
const r = await handleMultipartCall(form)
expect(r.status).toBe(400)
expect(r.body.message).toContain("'fn'")
})
test('invalidation still emitted on multipart mutation', async () => {
client(
{ affects: 'avatars', ir: { input: [intField('user_id'), uploadField('avatar')] } },
async function set_avatar(_user_id: number, _avatar: UploadedFile) {
return { ok: true }
},
)
const form = multipart('set_avatar', { user_id: 9 }, {
avatar: new File([new Uint8Array([1])], 'a.bin', { type: 'application/octet-stream' }),
})
const r = await handleMultipartCall(form)
expect(r.status).toBe(200)
expect(r.headers['X-Mizan-Invalidate']).toContain('avatars')
})
})