Compare commits

...

2 Commits

Author SHA1 Message Date
13de230f05 refactor: rename arktype schemas to match types
Co-authored-by: Ona <no-reply@ona.com>
2026-03-05 02:00:35 +00:00
64a03b253e test: add schema sync tests for arktype/JSON Schema drift
Validates reference payloads against both the arktype schema
(parseEnhancementResult) and the OpenRouter JSON Schema structure.
Catches field additions/removals or type changes in either schema.

Co-authored-by: Ona <no-reply@ona.com>
2026-03-05 01:57:17 +00:00
2 changed files with 95 additions and 8 deletions

View File

@@ -1,6 +1,10 @@
import { describe, expect, test } from "bun:test" import { describe, expect, test } from "bun:test"
import { emptyEnhancementResult, parseEnhancementResult } from "./schema.ts" import {
emptyEnhancementResult,
enhancementResultJsonSchema,
parseEnhancementResult,
} from "./schema.ts"
describe("parseEnhancementResult", () => { describe("parseEnhancementResult", () => {
test("parses valid result", () => { test("parses valid result", () => {
@@ -87,3 +91,86 @@ describe("emptyEnhancementResult", () => {
expect(result.syntheticItems).toEqual([]) expect(result.syntheticItems).toEqual([])
}) })
}) })
describe("schema sync", () => {
const referencePayloads = [
{
name: "full payload with null slot fill",
payload: {
slotFills: {
"weather-1": { insight: "Rain after 3pm", crossSource: null },
"cal-2": { summary: "Busy morning" },
},
syntheticItems: [
{ id: "briefing-morning", type: "briefing", text: "Light day ahead." },
{ id: "nudge-umbrella", type: "nudge", text: "Bring an umbrella." },
],
},
},
{
name: "empty collections",
payload: { slotFills: {}, syntheticItems: [] },
},
{
name: "slot fills only",
payload: {
slotFills: { "item-1": { slot: "filled" } },
syntheticItems: [],
},
},
{
name: "synthetic items only",
payload: {
slotFills: {},
syntheticItems: [{ id: "insight-1", type: "insight", text: "Something." }],
},
},
]
for (const { name, payload } of referencePayloads) {
test(`arktype and JSON Schema agree on: ${name}`, () => {
// arktype accepts it
const parsed = parseEnhancementResult(JSON.stringify(payload))
expect(parsed).not.toBeNull()
// JSON Schema structure matches
const jsonSchema = enhancementResultJsonSchema
expect(Object.keys(jsonSchema.properties).sort()).toEqual(
Object.keys(payload).sort(),
)
expect([...jsonSchema.required].sort()).toEqual(Object.keys(payload).sort())
// syntheticItems item schema has the right required fields
const itemSchema = jsonSchema.properties.syntheticItems.items
expect([...itemSchema.required].sort()).toEqual(["id", "text", "type"])
// Verify each synthetic item has exactly the fields the JSON Schema expects
for (const item of payload.syntheticItems) {
expect(Object.keys(item).sort()).toEqual([...itemSchema.required].sort())
}
})
}
test("JSON Schema rejects what arktype rejects: missing required field", () => {
// Missing syntheticItems
expect(parseEnhancementResult(JSON.stringify({ slotFills: {} }))).toBeNull()
// JSON Schema also requires it
expect(enhancementResultJsonSchema.required).toContain("syntheticItems")
})
test("JSON Schema rejects what arktype rejects: wrong slot fill value type", () => {
const bad = { slotFills: { "item-1": { slot: 42 } }, syntheticItems: [] }
// arktype rejects it
expect(parseEnhancementResult(JSON.stringify(bad))).toBeNull()
// JSON Schema only allows string or null for slot values
const slotValueTypes =
enhancementResultJsonSchema.properties.slotFills.additionalProperties
.additionalProperties.type
expect(slotValueTypes).toContain("string")
expect(slotValueTypes).toContain("null")
expect(slotValueTypes).not.toContain("number")
})
})

View File

@@ -1,24 +1,24 @@
import { type } from "arktype" import { type } from "arktype"
const syntheticItemSchema = type({ const SyntheticItem = type({
id: "string", id: "string",
type: "string", type: "string",
text: "string", text: "string",
}) })
const enhancementResultSchema = type({ const EnhancementResult = type({
slotFills: "Record<string, Record<string, string | null>>", slotFills: "Record<string, Record<string, string | null>>",
syntheticItems: syntheticItemSchema.array(), syntheticItems: SyntheticItem.array(),
}) })
export type SyntheticItem = typeof syntheticItemSchema.infer export type SyntheticItem = typeof SyntheticItem.infer
export type EnhancementResult = typeof enhancementResultSchema.infer export type EnhancementResult = typeof EnhancementResult.infer
/** /**
* JSON Schema passed to OpenRouter's structured output. * JSON Schema passed to OpenRouter's structured output.
* OpenRouter doesn't support arktype, so this is maintained separately. * OpenRouter doesn't support arktype, so this is maintained separately.
* *
* ⚠️ Must stay in sync with enhancementResultSchema above. * ⚠️ Must stay in sync with EnhancementResult above.
* If you add/remove fields, update both schemas. * If you add/remove fields, update both schemas.
*/ */
export const enhancementResultJsonSchema = { export const enhancementResultJsonSchema = {
@@ -76,7 +76,7 @@ export function parseEnhancementResult(json: string): EnhancementResult | null {
return null return null
} }
const result = enhancementResultSchema(parsed) const result = EnhancementResult(parsed)
if (result instanceof type.errors) { if (result instanceof type.errors) {
return null return null
} }