mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-03-30 13:54:01 +00:00
198 lines
5.9 KiB
TypeScript
198 lines
5.9 KiB
TypeScript
import { test, expect, describe } from "bun:test"
|
|
import { extractResponseText, formatPromptTooLargeError } from "../../src/cli/cmd/github"
|
|
import type { MessageV2 } from "../../src/session/message-v2"
|
|
|
|
// Helper to create minimal valid parts
|
|
function createTextPart(text: string): MessageV2.Part {
|
|
return {
|
|
id: "1",
|
|
sessionID: "s",
|
|
messageID: "m",
|
|
type: "text" as const,
|
|
text,
|
|
}
|
|
}
|
|
|
|
function createReasoningPart(text: string): MessageV2.Part {
|
|
return {
|
|
id: "1",
|
|
sessionID: "s",
|
|
messageID: "m",
|
|
type: "reasoning" as const,
|
|
text,
|
|
time: { start: 0 },
|
|
}
|
|
}
|
|
|
|
function createToolPart(tool: string, title: string, status: "completed" | "running" = "completed"): MessageV2.Part {
|
|
if (status === "completed") {
|
|
return {
|
|
id: "1",
|
|
sessionID: "s",
|
|
messageID: "m",
|
|
type: "tool" as const,
|
|
callID: "c1",
|
|
tool,
|
|
state: {
|
|
status: "completed",
|
|
input: {},
|
|
output: "",
|
|
title,
|
|
metadata: {},
|
|
time: { start: 0, end: 1 },
|
|
},
|
|
}
|
|
}
|
|
return {
|
|
id: "1",
|
|
sessionID: "s",
|
|
messageID: "m",
|
|
type: "tool" as const,
|
|
callID: "c1",
|
|
tool,
|
|
state: {
|
|
status: "running",
|
|
input: {},
|
|
time: { start: 0 },
|
|
},
|
|
}
|
|
}
|
|
|
|
function createStepStartPart(): MessageV2.Part {
|
|
return {
|
|
id: "1",
|
|
sessionID: "s",
|
|
messageID: "m",
|
|
type: "step-start" as const,
|
|
}
|
|
}
|
|
|
|
function createStepFinishPart(): MessageV2.Part {
|
|
return {
|
|
id: "1",
|
|
sessionID: "s",
|
|
messageID: "m",
|
|
type: "step-finish" as const,
|
|
reason: "done",
|
|
cost: 0,
|
|
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
|
}
|
|
}
|
|
|
|
describe("extractResponseText", () => {
|
|
test("returns text from text part", () => {
|
|
const parts = [createTextPart("Hello world")]
|
|
expect(extractResponseText(parts)).toBe("Hello world")
|
|
})
|
|
|
|
test("returns last text part when multiple exist", () => {
|
|
const parts = [createTextPart("First"), createTextPart("Last")]
|
|
expect(extractResponseText(parts)).toBe("Last")
|
|
})
|
|
|
|
test("returns text even when tool parts follow", () => {
|
|
const parts = [createTextPart("I'll help with that."), createToolPart("todowrite", "3 todos")]
|
|
expect(extractResponseText(parts)).toBe("I'll help with that.")
|
|
})
|
|
|
|
test("returns null for reasoning-only response (signals summary needed)", () => {
|
|
const parts = [createReasoningPart("Let me think about this...")]
|
|
expect(extractResponseText(parts)).toBeNull()
|
|
})
|
|
|
|
test("returns null for tool-only response (signals summary needed)", () => {
|
|
// This is the exact scenario from the bug report - todowrite with no text
|
|
const parts = [createToolPart("todowrite", "8 todos")]
|
|
expect(extractResponseText(parts)).toBeNull()
|
|
})
|
|
|
|
test("returns null for multiple completed tools", () => {
|
|
const parts = [
|
|
createToolPart("read", "src/file.ts"),
|
|
createToolPart("edit", "src/file.ts"),
|
|
createToolPart("bash", "bun test"),
|
|
]
|
|
expect(extractResponseText(parts)).toBeNull()
|
|
})
|
|
|
|
test("returns null for running tool parts (signals summary needed)", () => {
|
|
const parts = [createToolPart("bash", "", "running")]
|
|
expect(extractResponseText(parts)).toBeNull()
|
|
})
|
|
|
|
test("throws on empty array", () => {
|
|
expect(() => extractResponseText([])).toThrow("no parts returned")
|
|
})
|
|
|
|
test("returns null for step-start only", () => {
|
|
const parts = [createStepStartPart()]
|
|
expect(extractResponseText(parts)).toBeNull()
|
|
})
|
|
|
|
test("returns null for step-finish only", () => {
|
|
const parts = [createStepFinishPart()]
|
|
expect(extractResponseText(parts)).toBeNull()
|
|
})
|
|
|
|
test("returns null for step-start and step-finish", () => {
|
|
const parts = [createStepStartPart(), createStepFinishPart()]
|
|
expect(extractResponseText(parts)).toBeNull()
|
|
})
|
|
|
|
test("returns text from multi-step response", () => {
|
|
const parts = [
|
|
createStepStartPart(),
|
|
createToolPart("read", "src/file.ts"),
|
|
createTextPart("Done"),
|
|
createStepFinishPart(),
|
|
]
|
|
expect(extractResponseText(parts)).toBe("Done")
|
|
})
|
|
|
|
test("prefers text over reasoning when both present", () => {
|
|
const parts = [createReasoningPart("Internal thinking..."), createTextPart("Final answer")]
|
|
expect(extractResponseText(parts)).toBe("Final answer")
|
|
})
|
|
|
|
test("prefers text over tools when both present", () => {
|
|
const parts = [createToolPart("read", "src/file.ts"), createTextPart("Here's what I found")]
|
|
expect(extractResponseText(parts)).toBe("Here's what I found")
|
|
})
|
|
})
|
|
|
|
describe("formatPromptTooLargeError", () => {
|
|
test("formats error without files", () => {
|
|
const result = formatPromptTooLargeError([])
|
|
expect(result).toBe("PROMPT_TOO_LARGE: The prompt exceeds the model's context limit.")
|
|
})
|
|
|
|
test("formats error with files (base64 content)", () => {
|
|
// Base64 is ~33% larger than original, so we multiply by 0.75 to get original size
|
|
// 400 KB base64 = 300 KB original, 200 KB base64 = 150 KB original
|
|
const files = [
|
|
{ filename: "screenshot.png", content: "a".repeat(400 * 1024) },
|
|
{ filename: "diagram.png", content: "b".repeat(200 * 1024) },
|
|
]
|
|
const result = formatPromptTooLargeError(files)
|
|
|
|
expect(result).toStartWith("PROMPT_TOO_LARGE: The prompt exceeds the model's context limit.")
|
|
expect(result).toInclude("Files in prompt:")
|
|
expect(result).toInclude("screenshot.png (300 KB)")
|
|
expect(result).toInclude("diagram.png (150 KB)")
|
|
})
|
|
|
|
test("lists all files when multiple present", () => {
|
|
// Base64 sizes: 4KB -> 3KB, 8KB -> 6KB, 12KB -> 9KB
|
|
const files = [
|
|
{ filename: "img1.png", content: "x".repeat(4 * 1024) },
|
|
{ filename: "img2.jpg", content: "y".repeat(8 * 1024) },
|
|
{ filename: "img3.gif", content: "z".repeat(12 * 1024) },
|
|
]
|
|
const result = formatPromptTooLargeError(files)
|
|
|
|
expect(result).toInclude("img1.png (3 KB)")
|
|
expect(result).toInclude("img2.jpg (6 KB)")
|
|
expect(result).toInclude("img3.gif (9 KB)")
|
|
})
|
|
})
|