| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385 |
- import { describe, expect, test } from "bun:test"
- import { MessageV2 } from "../../src/session/message-v2"
- import { SessionPrompt } from "../../src/session/prompt"
- describe("structured-output.OutputFormat", () => {
- test("parses text format", () => {
- const result = MessageV2.Format.safeParse({ type: "text" })
- expect(result.success).toBe(true)
- if (result.success) {
- expect(result.data.type).toBe("text")
- }
- })
- test("parses json_schema format with defaults", () => {
- const result = MessageV2.Format.safeParse({
- type: "json_schema",
- schema: { type: "object", properties: { name: { type: "string" } } },
- })
- expect(result.success).toBe(true)
- if (result.success) {
- expect(result.data.type).toBe("json_schema")
- if (result.data.type === "json_schema") {
- expect(result.data.retryCount).toBe(2) // default value
- }
- }
- })
- test("parses json_schema format with custom retryCount", () => {
- const result = MessageV2.Format.safeParse({
- type: "json_schema",
- schema: { type: "object" },
- retryCount: 5,
- })
- expect(result.success).toBe(true)
- if (result.success && result.data.type === "json_schema") {
- expect(result.data.retryCount).toBe(5)
- }
- })
- test("rejects invalid type", () => {
- const result = MessageV2.Format.safeParse({ type: "invalid" })
- expect(result.success).toBe(false)
- })
- test("rejects json_schema without schema", () => {
- const result = MessageV2.Format.safeParse({ type: "json_schema" })
- expect(result.success).toBe(false)
- })
- test("rejects negative retryCount", () => {
- const result = MessageV2.Format.safeParse({
- type: "json_schema",
- schema: { type: "object" },
- retryCount: -1,
- })
- expect(result.success).toBe(false)
- })
- })
- describe("structured-output.StructuredOutputError", () => {
- test("creates error with message and retries", () => {
- const error = new MessageV2.StructuredOutputError({
- message: "Failed to validate",
- retries: 3,
- })
- expect(error.name).toBe("StructuredOutputError")
- expect(error.data.message).toBe("Failed to validate")
- expect(error.data.retries).toBe(3)
- })
- test("converts to object correctly", () => {
- const error = new MessageV2.StructuredOutputError({
- message: "Test error",
- retries: 2,
- })
- const obj = error.toObject()
- expect(obj.name).toBe("StructuredOutputError")
- expect(obj.data.message).toBe("Test error")
- expect(obj.data.retries).toBe(2)
- })
- test("isInstance correctly identifies error", () => {
- const error = new MessageV2.StructuredOutputError({
- message: "Test",
- retries: 1,
- })
- expect(MessageV2.StructuredOutputError.isInstance(error)).toBe(true)
- expect(MessageV2.StructuredOutputError.isInstance({ name: "other" })).toBe(false)
- })
- })
- describe("structured-output.UserMessage", () => {
- test("user message accepts outputFormat", () => {
- const result = MessageV2.User.safeParse({
- id: "test-id",
- sessionID: "test-session",
- role: "user",
- time: { created: Date.now() },
- agent: "default",
- model: { providerID: "anthropic", modelID: "claude-3" },
- outputFormat: {
- type: "json_schema",
- schema: { type: "object" },
- },
- })
- expect(result.success).toBe(true)
- })
- test("user message works without outputFormat (optional)", () => {
- const result = MessageV2.User.safeParse({
- id: "test-id",
- sessionID: "test-session",
- role: "user",
- time: { created: Date.now() },
- agent: "default",
- model: { providerID: "anthropic", modelID: "claude-3" },
- })
- expect(result.success).toBe(true)
- })
- })
- describe("structured-output.AssistantMessage", () => {
- const baseAssistantMessage = {
- id: "test-id",
- sessionID: "test-session",
- role: "assistant" as const,
- parentID: "parent-id",
- modelID: "claude-3",
- providerID: "anthropic",
- mode: "default",
- agent: "default",
- path: { cwd: "/test", root: "/test" },
- cost: 0.001,
- tokens: { input: 100, output: 50, reasoning: 0, cache: { read: 0, write: 0 } },
- time: { created: Date.now() },
- }
- test("assistant message accepts structured", () => {
- const result = MessageV2.Assistant.safeParse({
- ...baseAssistantMessage,
- structured: { company: "Anthropic", founded: 2021 },
- })
- expect(result.success).toBe(true)
- if (result.success) {
- expect(result.data.structured).toEqual({ company: "Anthropic", founded: 2021 })
- }
- })
- test("assistant message works without structured_output (optional)", () => {
- const result = MessageV2.Assistant.safeParse(baseAssistantMessage)
- expect(result.success).toBe(true)
- })
- })
- describe("structured-output.createStructuredOutputTool", () => {
- test("creates tool with correct id", () => {
- const tool = SessionPrompt.createStructuredOutputTool({
- schema: { type: "object", properties: { name: { type: "string" } } },
- onSuccess: () => {},
- })
- // AI SDK tool type doesn't expose id, but we set it internally
- expect((tool as any).id).toBe("StructuredOutput")
- })
- test("creates tool with description", () => {
- const tool = SessionPrompt.createStructuredOutputTool({
- schema: { type: "object" },
- onSuccess: () => {},
- })
- expect(tool.description).toContain("structured format")
- })
- test("creates tool with schema as inputSchema", () => {
- const schema = {
- type: "object",
- properties: {
- company: { type: "string" },
- founded: { type: "number" },
- },
- required: ["company"],
- }
- const tool = SessionPrompt.createStructuredOutputTool({
- schema,
- onSuccess: () => {},
- })
- // AI SDK wraps schema in { jsonSchema: {...} }
- expect(tool.inputSchema).toBeDefined()
- const inputSchema = tool.inputSchema as any
- expect(inputSchema.jsonSchema?.properties?.company).toBeDefined()
- expect(inputSchema.jsonSchema?.properties?.founded).toBeDefined()
- })
- test("strips $schema property from inputSchema", () => {
- const schema = {
- $schema: "http://json-schema.org/draft-07/schema#",
- type: "object",
- properties: { name: { type: "string" } },
- }
- const tool = SessionPrompt.createStructuredOutputTool({
- schema,
- onSuccess: () => {},
- })
- // AI SDK wraps schema in { jsonSchema: {...} }
- const inputSchema = tool.inputSchema as any
- expect(inputSchema.jsonSchema?.$schema).toBeUndefined()
- })
- test("execute calls onSuccess with valid args", async () => {
- let capturedOutput: unknown
- const tool = SessionPrompt.createStructuredOutputTool({
- schema: { type: "object", properties: { name: { type: "string" } } },
- onSuccess: (output) => {
- capturedOutput = output
- },
- })
- expect(tool.execute).toBeDefined()
- const testArgs = { name: "Test Company" }
- const result = await tool.execute!(testArgs, {
- toolCallId: "test-call-id",
- messages: [],
- abortSignal: undefined as any,
- })
- expect(capturedOutput).toEqual(testArgs)
- expect(result.output).toBe("Structured output captured successfully.")
- expect(result.metadata.valid).toBe(true)
- })
- test("AI SDK validates schema before execute - missing required field", async () => {
- // Note: The AI SDK validates the input against the schema BEFORE calling execute()
- // So invalid inputs never reach the tool's execute function
- // This test documents the expected schema behavior
- const tool = SessionPrompt.createStructuredOutputTool({
- schema: {
- type: "object",
- properties: {
- name: { type: "string" },
- age: { type: "number" },
- },
- required: ["name", "age"],
- },
- onSuccess: () => {},
- })
- // The schema requires both 'name' and 'age'
- expect(tool.inputSchema).toBeDefined()
- const inputSchema = tool.inputSchema as any
- expect(inputSchema.jsonSchema?.required).toContain("name")
- expect(inputSchema.jsonSchema?.required).toContain("age")
- })
- test("AI SDK validates schema types before execute - wrong type", async () => {
- // Note: The AI SDK validates the input against the schema BEFORE calling execute()
- // So invalid inputs never reach the tool's execute function
- // This test documents the expected schema behavior
- const tool = SessionPrompt.createStructuredOutputTool({
- schema: {
- type: "object",
- properties: {
- count: { type: "number" },
- },
- required: ["count"],
- },
- onSuccess: () => {},
- })
- // The schema defines 'count' as a number
- expect(tool.inputSchema).toBeDefined()
- const inputSchema = tool.inputSchema as any
- expect(inputSchema.jsonSchema?.properties?.count?.type).toBe("number")
- })
- test("execute handles nested objects", async () => {
- let capturedOutput: unknown
- const tool = SessionPrompt.createStructuredOutputTool({
- schema: {
- type: "object",
- properties: {
- user: {
- type: "object",
- properties: {
- name: { type: "string" },
- email: { type: "string" },
- },
- required: ["name"],
- },
- },
- required: ["user"],
- },
- onSuccess: (output) => {
- capturedOutput = output
- },
- })
- // Valid nested object - AI SDK validates before calling execute()
- const validResult = await tool.execute!(
- { user: { name: "John", email: "[email protected]" } },
- {
- toolCallId: "test-call-id",
- messages: [],
- abortSignal: undefined as any,
- },
- )
- expect(capturedOutput).toEqual({ user: { name: "John", email: "[email protected]" } })
- expect(validResult.metadata.valid).toBe(true)
- // Verify schema has correct nested structure
- const inputSchema = tool.inputSchema as any
- expect(inputSchema.jsonSchema?.properties?.user?.type).toBe("object")
- expect(inputSchema.jsonSchema?.properties?.user?.properties?.name?.type).toBe("string")
- expect(inputSchema.jsonSchema?.properties?.user?.required).toContain("name")
- })
- test("execute handles arrays", async () => {
- let capturedOutput: unknown
- const tool = SessionPrompt.createStructuredOutputTool({
- schema: {
- type: "object",
- properties: {
- tags: {
- type: "array",
- items: { type: "string" },
- },
- },
- required: ["tags"],
- },
- onSuccess: (output) => {
- capturedOutput = output
- },
- })
- // Valid array - AI SDK validates before calling execute()
- const validResult = await tool.execute!(
- { tags: ["a", "b", "c"] },
- {
- toolCallId: "test-call-id",
- messages: [],
- abortSignal: undefined as any,
- },
- )
- expect(capturedOutput).toEqual({ tags: ["a", "b", "c"] })
- expect(validResult.metadata.valid).toBe(true)
- // Verify schema has correct array structure
- const inputSchema = tool.inputSchema as any
- expect(inputSchema.jsonSchema?.properties?.tags?.type).toBe("array")
- expect(inputSchema.jsonSchema?.properties?.tags?.items?.type).toBe("string")
- })
- test("toModelOutput returns text value", () => {
- const tool = SessionPrompt.createStructuredOutputTool({
- schema: { type: "object" },
- onSuccess: () => {},
- })
- expect(tool.toModelOutput).toBeDefined()
- const modelOutput = tool.toModelOutput!({
- output: "Test output",
- title: "Test",
- metadata: { valid: true },
- })
- expect(modelOutput.type).toBe("text")
- expect(modelOutput.value).toBe("Test output")
- })
- // Note: Retry behavior is handled by the AI SDK and the prompt loop, not the tool itself
- // The tool simply calls onSuccess when execute() is called with valid args
- // See prompt.ts loop() for actual retry logic
- })
|