// Descriptor + executor contract pins — the shapes the client codegen and the // editor depend on. No database, no running server. Run with `voltro test`. import { describe, it, expect } from 'vitest' import { Schema } from 'effect' import { makeTestContext } from '@voltro/testing' import '../database/schema' // registers the core + derived content tables import { saveDraft } from '../mutations/content.saveDraft.mutation' import { listContent } from '../queries/content.list.query' import contentTypesExecute from '../actions/content.types.action.server' describe('content.saveDraft descriptor', () => { it('decodes an input carrying arbitrary per-type values', () => { const decode = Schema.decodeUnknownSync(saveDraft.input) const input = { type: 'blogPost', values: { title: 'Hi', body: 'x' } } expect(decode(input)).toEqual(input) }) it('encodes both arms of the result union', () => { const encode = Schema.encodeUnknownSync(saveDraft.output) expect(encode({ ok: true, id: 'blogPost_1', status: 'draft' })).toEqual({ ok: true, id: 'blogPost_1', status: 'draft' }) const fail = { ok: false, violations: [{ field: 'title', rule: 'required', message: 'missing' }] } expect(encode(fail)).toEqual(fail) }) }) describe('content.list descriptor', () => { it('accepts a type + optional status and pins the list projection shape', () => { const decodeIn = Schema.decodeUnknownSync(listContent.input) expect(decodeIn({ type: 'blogPost', status: 'draft' })).toEqual({ type: 'blogPost', status: 'draft' }) // The output carries only what a list view needs — id/status/title?/slug?/ // updatedAt; the runtime drops a row's other columns against this shape. // It is an ARRAY schema (a query delivers rows, not a row), so encode a // list and read the first element — passing a bare object here typechecked // as the array type and made `wire.id` a compile error that `voltro test` // never sees, because it transpiles without type-checking. const encode = Schema.encodeUnknownSync(listContent.output) const [wire] = encode([{ id: 'p1', status: 'published', title: 'T', slug: 't', updatedAt: new Date(0) }]) expect(wire?.id).toBe('p1') expect(wire?.status).toBe('published') expect(wire?.title).toBe('T') }) }) describe('content.types executor', () => { it('projects the registered types to a wire-safe, order-preserving descriptor', async () => { const ctx = makeTestContext({ subject: { type: 'user', id: 'u1', tenantId: 'acme' } }) const out = await contentTypesExecute({}, ctx) const names = out.types.map((t) => t.name) expect(names).toContain('blogPost') expect(names).toContain('page') const blog = out.types.find((t) => t.name === 'blogPost')! const fieldNames = blog.fields.map((f) => f.name) expect(fieldNames).toEqual(['title', 'slug', 'body', 'excerpt', 'publishedAt']) // The slug field is a string with a richer widget than plain text is fine — // and its derived transform (a function) is NOT on the wire shape. const slug = blog.fields.find((f) => f.name === 'slug')! expect(slug.kind).toBe('string') expect('transform' in slug).toBe(false) // The Literal `layout` on `page` carries its value set for the