Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/opencode/src/question/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export namespace Question {

export const Option = z
.object({
label: z.string().describe("Display text (1-5 words, concise)"),
label: z.string().max(30).describe("Display text (1-5 words, concise)"),
description: z.string().describe("Explanation of choice"),
})
.meta({
Expand All @@ -21,7 +21,7 @@ export namespace Question {
export const Info = z
.object({
question: z.string().describe("Complete question"),
header: z.string().max(12).describe("Very short label (max 12 chars)"),
header: z.string().max(30).describe("Very short label (max 30 chars)"),
options: z.array(Option).describe("Available choices"),
multiple: z.boolean().optional().describe("Allow selecting multiple choices"),
custom: z.boolean().optional().describe("Allow typing a custom answer (default: true)"),
Expand Down
107 changes: 107 additions & 0 deletions packages/opencode/test/tool/question.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { describe, expect, test, spyOn, beforeEach, afterEach } from "bun:test"
import { z } from "zod"
import { QuestionTool } from "../../src/tool/question"
import * as QuestionModule from "../../src/question"

const ctx = {
sessionID: "test-session",
messageID: "test-message",
callID: "test-call",
agent: "test-agent",
abort: AbortSignal.any([]),
metadata: () => {},
ask: async () => {},
}

describe("tool.question", () => {
let askSpy: any;

beforeEach(() => {
askSpy = spyOn(QuestionModule.Question, "ask").mockImplementation(async () => {
return []
})
})

afterEach(() => {
askSpy.mockRestore()
})

test("should successfully execute with valid question parameters", async () => {
const tool = await QuestionTool.init()
const questions = [
{
question: "What is your favorite color?",
header: "Color",
options: [
{ label: "Red", description: "The color of passion" },
{ label: "Blue", description: "The color of sky" },
],
multiple: false,
},
]

askSpy.mockResolvedValueOnce([["Red"]])

const result = await tool.execute(
{ questions },
ctx,
)
expect(askSpy).toHaveBeenCalledTimes(1)
expect(result.title).toBe("Asked 1 question")
})

test("should now pass with a header longer than 12 but less than 30 chars", async () => {
const tool = await QuestionTool.init()
const questions = [
{
question: "What is your favorite animal?",
header: "This Header is Over 12",
options: [{ label: "Dog", description: "Man's best friend" }],
},
]

askSpy.mockResolvedValueOnce([["Dog"]])

const result = await tool.execute({ questions }, ctx)
expect(result.output).toContain(`"What is your favorite animal?"="Dog"`)
})

test("should throw an Error for header exceeding 30 characters", async () => {
const tool = await QuestionTool.init()
const questions = [
{
question: "What is your favorite animal?",
header: "This Header is Definitely More Than Thirty Characters Long",
options: [{ label: "Dog", description: "Man's best friend" }],
},
]
try {
await tool.execute({ questions }, ctx)
// If it reaches here, the test should fail
expect(true).toBe(false)
} catch (e: any) {
expect(e).toBeInstanceOf(Error)
expect(e.cause).toBeInstanceOf(z.ZodError)
}
})

test("should throw an Error for label exceeding 30 characters", async () => {
const tool = await QuestionTool.init()
const questions = [
{
question: "A question with a very long label",
header: "Long Label",
options: [{ label: "This is a very, very, very long label that will exceed the limit", description: "A description" }],
},
]
try {
await tool.execute({ questions }, ctx)
// If it reaches here, the test should fail
expect(true).toBe(false)
} catch (e: any) {
expect(e).toBeInstanceOf(Error)
expect(e.cause).toBeInstanceOf(z.ZodError)
}
})
})