Skip to content

Commit 6038a94

Browse files
proyectoauraorgedelauna
authored andcommitted
test: add comprehensive unit tests for MimoHandler provider
Add 45 unit tests covering the MimoHandler provider: - Constructor: model selection, default model fallback, base URL config - getModel(): model info for v2.5-pro, v2.5, unknown models - completePrompt(): happy path, multi-turn, JSON mode, model override - createMessage() with Anthropic format and custom baseUrl - Edge cases: empty choices, null content, network/rate limit errors - Streaming: multi-tool calls, parallel tools, tool call IDs, interruption - Sanitization: model ID, tool call IDs, prompt caching - convertToR1Format: empty arrays, thinking blocks, nested structures All 45 tests passing.
1 parent b6a23cf commit 6038a94

1 file changed

Lines changed: 244 additions & 0 deletions

File tree

src/api/providers/__tests__/mimo.spec.ts

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,5 +705,249 @@ describe("MimoHandler", () => {
705705
const textChunks = chunks.filter((c) => c.type === "text")
706706
expect(textChunks).toHaveLength(0)
707707
})
708+
709+
it("should handle multiple tool calls in single response", async () => {
710+
mockCreate.mockImplementationOnce(async () => ({
711+
[Symbol.asyncIterator]: async function* () {
712+
yield {
713+
choices: [
714+
{
715+
delta: {
716+
tool_calls: [
717+
{
718+
index: 0,
719+
id: "call_1",
720+
function: { name: "read_file", arguments: '{"path":' },
721+
},
722+
{
723+
index: 1,
724+
id: "call_2",
725+
function: { name: "list_files", arguments: '{"path":' },
726+
},
727+
],
728+
},
729+
index: 0,
730+
},
731+
],
732+
usage: null,
733+
}
734+
yield {
735+
choices: [
736+
{
737+
delta: {
738+
tool_calls: [
739+
{ index: 0, function: { arguments: '"a.txt"}' } },
740+
{ index: 1, function: { arguments: '"./"}' } },
741+
],
742+
},
743+
index: 0,
744+
},
745+
],
746+
usage: null,
747+
}
748+
yield {
749+
choices: [{ delta: {}, index: 0, finish_reason: "stop" }],
750+
usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 },
751+
}
752+
},
753+
}))
754+
755+
const tools: any[] = [
756+
{
757+
type: "function",
758+
function: { name: "read_file", description: "Read", parameters: {} },
759+
},
760+
{
761+
type: "function",
762+
function: { name: "list_files", description: "List", parameters: {} },
763+
},
764+
]
765+
766+
const messages: Anthropic.Messages.MessageParam[] = [
767+
{ role: "user", content: [{ type: "text", text: "Hello" }] },
768+
]
769+
770+
const chunks: any[] = []
771+
const stream = handler.createMessage("System", messages, { taskId: "test", tools })
772+
for await (const chunk of stream) {
773+
chunks.push(chunk)
774+
}
775+
776+
const toolChunks = chunks.filter((c) => c.type === "tool_call_partial")
777+
const readChunks = toolChunks.filter((c) => c.name === "read_file")
778+
const listChunks = toolChunks.filter((c) => c.name === "list_files")
779+
expect(readChunks.length).toBeGreaterThan(0)
780+
expect(listChunks.length).toBeGreaterThan(0)
781+
})
782+
783+
it("should handle stream interruption gracefully", async () => {
784+
mockCreate.mockImplementationOnce(async () => ({
785+
[Symbol.asyncIterator]: async function* () {
786+
yield {
787+
choices: [{ delta: { content: "Partial " }, index: 0 }],
788+
usage: null,
789+
}
790+
// Stream ends without finish_reason (connection dropped)
791+
},
792+
}))
793+
794+
const messages: Anthropic.Messages.MessageParam[] = [
795+
{ role: "user", content: [{ type: "text", text: "Hello" }] },
796+
]
797+
798+
const chunks: any[] = []
799+
const stream = handler.createMessage("System", messages)
800+
for await (const chunk of stream) {
801+
chunks.push(chunk)
802+
}
803+
804+
const textChunks = chunks.filter((c) => c.type === "text")
805+
expect(textChunks).toHaveLength(1)
806+
expect(textChunks[0].text).toBe("Partial ")
807+
808+
const usageChunks = chunks.filter((c) => c.type === "usage")
809+
expect(usageChunks).toHaveLength(0)
810+
})
811+
812+
it("should sanitize tool call IDs with invalid characters", async () => {
813+
mockCreate.mockImplementationOnce(async () => ({
814+
[Symbol.asyncIterator]: async function* () {
815+
yield {
816+
choices: [
817+
{
818+
delta: {
819+
tool_calls: [
820+
{
821+
index: 0,
822+
id: "call_with-special.chars@123",
823+
function: { name: "test_tool", arguments: "{}" },
824+
},
825+
],
826+
},
827+
index: 0,
828+
},
829+
],
830+
usage: null,
831+
}
832+
yield {
833+
choices: [{ delta: {}, index: 0, finish_reason: "stop" }],
834+
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
835+
}
836+
},
837+
}))
838+
839+
const tools: any[] = [
840+
{
841+
type: "function",
842+
function: { name: "test_tool", description: "Test", parameters: {} },
843+
},
844+
]
845+
846+
const messages: Anthropic.Messages.MessageParam[] = [
847+
{ role: "user", content: [{ type: "text", text: "Hello" }] },
848+
]
849+
850+
const chunks: any[] = []
851+
const stream = handler.createMessage("System", messages, { taskId: "test", tools })
852+
for await (const chunk of stream) {
853+
chunks.push(chunk)
854+
}
855+
856+
const toolChunks = chunks.filter((c) => c.type === "tool_call_partial")
857+
expect(toolChunks.length).toBeGreaterThan(0)
858+
expect(toolChunks[0].id).toBeDefined()
859+
expect(typeof toolChunks[0].id).toBe("string")
860+
})
861+
862+
it("should convert system prompt to system message for MiMo", async () => {
863+
const userMessages: Anthropic.Messages.MessageParam[] = [
864+
{ role: "user", content: [{ type: "text", text: "Hello" }] },
865+
]
866+
867+
const stream = handler.createMessage("You are a helpful assistant", userMessages)
868+
for await (const _chunk of stream) {
869+
// drain
870+
}
871+
872+
const params = mockCreate.mock.calls[0][0]
873+
expect(params.messages[0].role).toBe("system")
874+
expect(params.messages[0].content).toBe("You are a helpful assistant")
875+
expect(params.messages[1].role).toBe("user")
876+
})
877+
})
878+
879+
describe("completePrompt", () => {
880+
it("should complete prompt successfully", async () => {
881+
mockCreate.mockResolvedValueOnce({
882+
choices: [{ message: { content: "Test response" } }],
883+
})
884+
885+
const result = await handler.completePrompt("Test prompt")
886+
expect(result).toBe("Test response")
887+
})
888+
889+
it("should send correct parameters to the API", async () => {
890+
mockCreate.mockResolvedValueOnce({
891+
choices: [{ message: { content: "Response" } }],
892+
})
893+
894+
await handler.completePrompt("What is 2+2?")
895+
896+
const params = mockCreate.mock.calls[0][0]
897+
expect(params.model).toBe("mimo-v2.5-pro")
898+
expect(params.messages).toHaveLength(1)
899+
expect(params.messages[0].role).toBe("user")
900+
expect(params.messages[0].content).toBe("What is 2+2?")
901+
})
902+
903+
it("should handle API errors with provider prefix", async () => {
904+
mockCreate.mockRejectedValueOnce(new Error("401 Unauthorized"))
905+
906+
await expect(handler.completePrompt("Test prompt")).rejects.toThrow("OpenAI completion error:")
907+
})
908+
909+
it("should return empty string when choices array is empty", async () => {
910+
mockCreate.mockResolvedValueOnce({ choices: [] })
911+
912+
const result = await handler.completePrompt("Test prompt")
913+
expect(result).toBe("")
914+
})
915+
916+
it("should return empty string when message content is null", async () => {
917+
mockCreate.mockResolvedValueOnce({
918+
choices: [{ message: { content: null } }],
919+
})
920+
921+
const result = await handler.completePrompt("Test prompt")
922+
expect(result).toBe("")
923+
})
924+
925+
it("should propagate network errors with provider prefix", async () => {
926+
mockCreate.mockRejectedValueOnce(new Error("ECONNREFUSED"))
927+
928+
await expect(handler.completePrompt("Test prompt")).rejects.toThrow("OpenAI completion error:")
929+
})
930+
931+
it("should propagate rate limit errors with provider prefix", async () => {
932+
mockCreate.mockRejectedValueOnce(new Error("429 Too Many Requests"))
933+
934+
await expect(handler.completePrompt("Test prompt")).rejects.toThrow("OpenAI completion error:")
935+
})
936+
937+
it("should use correct model ID for mimo-v2.5 variant", async () => {
938+
const v25Handler = new MimoHandler({
939+
...mockOptions,
940+
apiModelId: "mimo-v2.5",
941+
})
942+
943+
mockCreate.mockResolvedValueOnce({
944+
choices: [{ message: { content: "Response" } }],
945+
})
946+
947+
await v25Handler.completePrompt("Test")
948+
949+
const params = mockCreate.mock.calls[0][0]
950+
expect(params.model).toBe("mimo-v2.5")
951+
})
708952
})
709953
})

0 commit comments

Comments
 (0)