1import { assert, describe, it } from "@effect/vitest"
2import { Schema } from "effect"
3import { toCodecAnthropic } from "effect/ai/AnthropicStructuredOutput"
4import { TestSchema } from "effect/testing"
5
6describe("AnthropicStructuredOutput representation v2", () => {
7 it("projects the encoded side before JSON Schema generation", () => {
8 assert.strictEqual(toCodecAnthropic(Schema.FiniteFromString).jsonSchema.type, "string")
9 })
10
11 it("keeps supported custom JSON Schema filters with a description", () => {
12 const schema = Schema.String.check(Schema.makeFilter<string>((value) => value.startsWith("a"), {
13 description: "starts with a",
14 representation: {
15 id: "test/ai/anthropic/startsWithA",
16 payload: null
17 },
18 toJsonSchema: () => ({ pattern: "^a" })
19 }))
20
21 assert.deepStrictEqual(toCodecAnthropic(schema).jsonSchema, {
22 type: "string",
23 description: "starts with a",
24 pattern: "^a"
25 })
26 })
27
28 it("drops unsupported custom JSON Schema filters with a description", async () => {
29 const schema = Schema.String.check(Schema.makeFilter<string>((value) => value.length >= 2, {
30 description: "at least two characters",
31 representation: {
32 id: "test/ai/anthropic/minTwoCharactersWithDescription",
33 payload: null
34 },
35 toJsonSchema: () => ({ minLength: 2 })
36 }))
37
38 const result = toCodecAnthropic(schema)
39 assert.deepStrictEqual(result.jsonSchema, {
40 type: "string",
41 description: "at least two characters"
42 })
43 await new TestSchema.Asserts(result.codec).decoding().fail(
44 "a",
45 `Expected <filter>`
46 )
47 })
48
49 it("keeps supported custom JSON Schema filters without a description", () => {
50 const schema = Schema.String.check(Schema.makeFilter<string>((value) => value.startsWith("a"), {
51 representation: {
52 id: "test/ai/anthropic/startsWithAWithoutDescription",
53 payload: null
54 },
55 toJsonSchema: () => ({ pattern: "^a" })
56 }))
57
58 assert.deepStrictEqual(toCodecAnthropic(schema).jsonSchema, {
59 type: "string",
60 pattern: "^a"
61 })
62 })
63
64 it("drops unsupported custom JSON Schema filters without a description", () => {
65 const schema = Schema.String.check(Schema.makeFilter<string>((value) => value.length >= 2, {
66 representation: {
67 id: "test/ai/anthropic/minTwoCharactersWithoutDescription",
68 payload: null
69 },
70 toJsonSchema: () => ({ minLength: 2 })
71 }))
72
73 assert.deepStrictEqual(toCodecAnthropic(schema).jsonSchema, {
74 type: "string"
75 })
76 })
77
78 it("invokes custom JSON Schema callbacks only with compiler inputs", () => {
79 let invocations = 0
80 const schema = Schema.String.check(Schema.makeFilter<string>(() => true, {
81 representation: {
82 id: "test/ai/anthropic/compilerInputs",
83 payload: null,
84 schemas: [Schema.Finite.ast]
85 },
86 toJsonSchema: ({ type, schemas }) => {
87 invocations++
88 assert.strictEqual(type, "string")
89 assert.strictEqual(schemas.length, 1)
90 assert.strictEqual(schemas[0].type, "number")
91 return { pattern: "^a" }
92 }
93 }))
94
95 assert.deepStrictEqual(toCodecAnthropic(schema).jsonSchema, {
96 type: "string",
97 pattern: "^a"
98 })
99 assert.strictEqual(invocations, 1)
100 })
101
102 it("compiles custom callbacks after structural transformations", () => {
103 let invocations = 0
104 const schema = Schema.Record(Schema.String, Schema.Finite).check(
105 Schema.makeFilter<object>(() => true, {
106 representation: {
107 id: "test/ai/anthropic/structuralCompilerInput",
108 payload: null
109 },
110 toJsonSchema: ({ type }) => {
111 invocations++
112 assert.strictEqual(type, "array")
113 return { description: "compiled from the provider shape" }
114 }
115 })
116 )
117
118 const jsonSchema = toCodecAnthropic(schema).jsonSchema
119 assert.strictEqual(jsonSchema.type, "array")
120 assert.strictEqual(
121 jsonSchema.description,
122 "Object encoded as array of [key, value] pairs. Apply object constraints to the decoded object and compiled from the provider shape"
123 )
124 assert.strictEqual(invocations, 1)
125 })
126
127 it("keeps property names that match unsupported keywords", () => {
128 const unsupported = Schema.Any.check(Schema.makeFilter<unknown>(() => true, {
129 representation: {
130 id: "test/ai/anthropic/unsupportedPropertySchema",
131 payload: null
132 },
133 toJsonSchema: () => ({ minLength: 2 })
134 }))
135
136 assert.deepStrictEqual(toCodecAnthropic(Schema.Struct({ minLength: unsupported })).jsonSchema, {
137 type: "object",
138 properties: { minLength: {} },
139 required: ["minLength"],
140 additionalProperties: false
141 })
142 })
143
144 it("keeps the original codec on the unchanged fast path", () => {
145 assert.strictEqual(toCodecAnthropic(Schema.String).codec, Schema.String)
146 })
147})