Skip to content

Commit 141466b

Browse files
committed
feat: 添加多个测试用例以增强生成内容的覆盖率,包括回退响应、路由处理和翻译功能
1 parent bc10539 commit 141466b

5 files changed

Lines changed: 454 additions & 0 deletions
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { afterEach, expect, test, mock } from "bun:test"
2+
3+
afterEach(() => {
4+
mock.restore()
5+
})
6+
7+
test("streams fallback response when no text content in non-streaming to streaming conversion", async () => {
8+
// Mock createChatCompletions to return a non-streaming response with no text content
9+
await mock.module("~/services/copilot/create-chat-completions", () => ({
10+
createChatCompletions: (_: unknown) => ({
11+
id: "res-fallback",
12+
choices: [
13+
{
14+
index: 0,
15+
// No content, or content that doesn't have text
16+
message: { role: "assistant", content: null },
17+
finish_reason: "stop",
18+
},
19+
],
20+
usage: { prompt_tokens: 1, completion_tokens: 0, total_tokens: 1 },
21+
}),
22+
}))
23+
24+
await mock.module("~/lib/rate-limit", () => ({
25+
checkRateLimit: () => {},
26+
}))
27+
28+
const { server } = await import("~/server?fallback-response-no-text")
29+
30+
// Request streaming endpoint, but get non-streaming response with no text
31+
const res = await server.request(
32+
"/v1beta/models/gemini-pro:streamGenerateContent",
33+
{
34+
method: "POST",
35+
headers: { "content-type": "application/json" },
36+
body: JSON.stringify({
37+
contents: [{ role: "user", parts: [{ text: "test" }] }],
38+
}),
39+
},
40+
)
41+
42+
expect(res.status).toBe(200)
43+
const ct = res.headers.get("content-type") || ""
44+
expect(ct.includes("text/event-stream")).toBe(true)
45+
46+
const body = await res.text()
47+
48+
// Should contain data events
49+
expect(body.includes("data:")).toBe(true)
50+
// Should have the fallback response structure
51+
expect(body.includes('"candidates"')).toBe(true)
52+
expect(body.includes('"usageMetadata"')).toBe(true)
53+
})
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { afterEach, expect, test, mock } from "bun:test"
2+
3+
afterEach(() => {
4+
mock.restore()
5+
})
6+
7+
test("routes fallthrough when URL doesn't match any generate-content patterns", async () => {
8+
await mock.module("~/lib/rate-limit", () => ({
9+
checkRateLimit: () => {},
10+
}))
11+
12+
const { server } = await import("~/server?route-fallthrough")
13+
14+
// Test with a URL that doesn't match any of the patterns
15+
// Not :streamGenerateContent, not :countTokens, not :generateContent
16+
const res = await server.request(
17+
"/v1beta/models/gemini-pro:unknownOperation",
18+
{
19+
method: "POST",
20+
headers: { "content-type": "application/json" },
21+
body: JSON.stringify({
22+
contents: [{ role: "user", parts: [{ text: "test" }] }],
23+
}),
24+
},
25+
)
26+
27+
// Should get 404 or similar since no route matches
28+
expect(res.status).toBe(404)
29+
})
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { describe, it, expect } from "bun:test"
2+
3+
import type { GeminiContent } from "~/routes/generate-content/types"
4+
5+
import { translateGeminiContentsToOpenAI } from "~/routes/generate-content/translation"
6+
7+
describe("Translation Advanced Coverage Tests", () => {
8+
it("should handle system instruction in contents", () => {
9+
const contents: Array<GeminiContent> = [
10+
{
11+
role: "user",
12+
parts: [{ text: "Hello" }],
13+
},
14+
]
15+
const systemInstruction: GeminiContent = {
16+
parts: [{ text: "You are a helpful assistant" }],
17+
}
18+
19+
const messages = translateGeminiContentsToOpenAI(
20+
contents,
21+
systemInstruction,
22+
)
23+
24+
const systemMessage = messages.find((m) => m.role === "system")
25+
expect(systemMessage?.content).toBe("You are a helpful assistant")
26+
})
27+
28+
it("should handle empty user message content with fallback", () => {
29+
const contents: Array<GeminiContent> = [
30+
{
31+
role: "user",
32+
parts: [{ text: " " }], // Only whitespace
33+
},
34+
]
35+
36+
const messages = translateGeminiContentsToOpenAI(contents)
37+
38+
const userMessage = messages.find((m) => m.role === "user")
39+
expect(userMessage?.content).toBe(" ") // Fallback to minimal space
40+
})
41+
42+
it("should handle function responses without matching tool call IDs", () => {
43+
const contents: Array<GeminiContent> = [
44+
{
45+
role: "user",
46+
parts: [
47+
{
48+
functionResponse: {
49+
name: "unmatched_function",
50+
response: { result: "orphan response" },
51+
},
52+
},
53+
],
54+
},
55+
]
56+
57+
const messages = translateGeminiContentsToOpenAI(contents)
58+
59+
// Should not create tool messages for unmatched responses
60+
const toolMessages = messages.filter((m) => m.role === "tool")
61+
expect(toolMessages).toHaveLength(0)
62+
})
63+
64+
it("should handle complex content that cannot be merged", () => {
65+
// Create messages with complex content that can't be string-merged
66+
const contents: Array<GeminiContent> = [
67+
{
68+
role: "user",
69+
parts: [
70+
{
71+
inlineData: {
72+
mimeType: "image/png",
73+
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
74+
},
75+
},
76+
],
77+
},
78+
{
79+
role: "user",
80+
parts: [
81+
{
82+
inlineData: {
83+
mimeType: "image/jpeg",
84+
data: "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAhEAACAQMDBQAAAAAAAAAAAAABAgMABAUGIWGRkqGx0f/EABUBAQEAAAAAAAAAAAAAAAAAAAMF/8QAGhEAAgMBAQAAAAAAAAAAAAAAAAECEgMRkf/aAAwDAQACEQMRAD8A0XmX5OMlEw==",
85+
},
86+
},
87+
],
88+
},
89+
]
90+
91+
const messages = translateGeminiContentsToOpenAI(contents)
92+
93+
// Should create separate messages for complex content
94+
const userMessages = messages.filter((m) => m.role === "user")
95+
expect(userMessages).toHaveLength(2)
96+
expect(Array.isArray(userMessages[0].content)).toBe(true)
97+
expect(Array.isArray(userMessages[1].content)).toBe(true)
98+
})
99+
100+
it("should handle media content with mixed text and images", () => {
101+
const contents: Array<GeminiContent> = [
102+
{
103+
role: "user",
104+
parts: [
105+
{ text: "Look at this image:" },
106+
{
107+
inlineData: {
108+
mimeType: "image/png",
109+
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
110+
},
111+
},
112+
],
113+
},
114+
]
115+
116+
const messages = translateGeminiContentsToOpenAI(contents)
117+
118+
const userMessage = messages[0]
119+
expect(Array.isArray(userMessage.content)).toBe(true)
120+
const content = userMessage.content as Array<{
121+
type: string
122+
text?: string
123+
image_url?: { url: string }
124+
}>
125+
126+
expect(content).toHaveLength(2)
127+
expect(content[0].type).toBe("text")
128+
expect(content[0].text).toBe("Look at this image:")
129+
expect(content[1].type).toBe("image_url")
130+
expect(content[1].image_url?.url).toContain("data:image/png;base64,")
131+
})
132+
133+
it("should handle googleSearch tool", () => {
134+
const contents: Array<GeminiContent> = [
135+
{
136+
role: "user",
137+
parts: [{ text: "Search for information" }],
138+
},
139+
]
140+
141+
const messages = translateGeminiContentsToOpenAI(contents)
142+
143+
// Should be included in the messages array but tools conversion is internal
144+
expect(messages).toHaveLength(1)
145+
})
146+
147+
it("should handle NONE tool config mode", () => {
148+
const contents: Array<GeminiContent> = [
149+
{
150+
role: "user",
151+
parts: [{ text: "Hello" }],
152+
},
153+
]
154+
155+
const messages = translateGeminiContentsToOpenAI(contents)
156+
157+
expect(messages).toHaveLength(1)
158+
expect(messages[0].content).toBe("Hello")
159+
})
160+
})
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { describe, it, expect } from "bun:test"
2+
3+
import type { ChatCompletionResponse } from "~/services/copilot/create-chat-completions"
4+
5+
import { translateOpenAIToGemini } from "~/routes/generate-content/translation"
6+
7+
describe("OpenAI to Gemini Response Translation", () => {
8+
it("should handle assistant message with tool calls having arguments", () => {
9+
const openAIResponse: ChatCompletionResponse = {
10+
id: "chatcmpl-123",
11+
object: "chat.completion",
12+
created: Date.now(),
13+
model: "gpt-4",
14+
choices: [
15+
{
16+
index: 0,
17+
message: {
18+
role: "assistant",
19+
content: "I'll search for that",
20+
tool_calls: [
21+
{
22+
id: "call_123",
23+
type: "function",
24+
function: {
25+
name: "search",
26+
arguments: '{"query": "test query", "limit": 10}',
27+
},
28+
},
29+
],
30+
},
31+
finish_reason: "tool_calls",
32+
logprobs: null,
33+
},
34+
],
35+
usage: {
36+
prompt_tokens: 10,
37+
completion_tokens: 20,
38+
total_tokens: 30,
39+
},
40+
}
41+
42+
const result = translateOpenAIToGemini(openAIResponse)
43+
44+
expect(result.candidates).toHaveLength(1)
45+
expect(result.candidates[0]?.content.parts).toHaveLength(2)
46+
expect(result.candidates[0]?.content.parts[0]).toEqual({
47+
text: "I'll search for that",
48+
})
49+
expect(result.candidates[0]?.content.parts[1]).toEqual({
50+
functionCall: {
51+
name: "search",
52+
args: { query: "test query", limit: 10 },
53+
},
54+
})
55+
})
56+
57+
it("should handle assistant message with tool calls having empty arguments", () => {
58+
const openAIResponse: ChatCompletionResponse = {
59+
id: "chatcmpl-456",
60+
object: "chat.completion",
61+
created: Date.now(),
62+
model: "gpt-4",
63+
choices: [
64+
{
65+
index: 0,
66+
message: {
67+
role: "assistant",
68+
content: "Getting current time",
69+
tool_calls: [
70+
{
71+
id: "call_456",
72+
type: "function",
73+
function: {
74+
name: "get_current_time",
75+
arguments: "",
76+
},
77+
},
78+
],
79+
},
80+
finish_reason: "tool_calls",
81+
logprobs: null,
82+
},
83+
],
84+
usage: {
85+
prompt_tokens: 5,
86+
completion_tokens: 10,
87+
total_tokens: 15,
88+
},
89+
}
90+
91+
const result = translateOpenAIToGemini(openAIResponse)
92+
93+
expect(result.candidates[0]?.content.parts[1]).toEqual({
94+
functionCall: {
95+
name: "get_current_time",
96+
args: {},
97+
},
98+
})
99+
})
100+
101+
it("should handle assistant message with simple text content", () => {
102+
const openAIResponse: ChatCompletionResponse = {
103+
id: "chatcmpl-789",
104+
object: "chat.completion",
105+
created: Date.now(),
106+
model: "gpt-4",
107+
choices: [
108+
{
109+
index: 0,
110+
message: {
111+
role: "assistant",
112+
content: "Here's my response",
113+
},
114+
finish_reason: "stop",
115+
logprobs: null,
116+
},
117+
],
118+
usage: {
119+
prompt_tokens: 15,
120+
completion_tokens: 5,
121+
total_tokens: 20,
122+
},
123+
}
124+
125+
const result = translateOpenAIToGemini(openAIResponse)
126+
127+
expect(result.candidates[0]?.content.parts).toHaveLength(1)
128+
expect(result.candidates[0]?.content.parts[0]).toEqual({
129+
text: "Here's my response",
130+
})
131+
})
132+
})

0 commit comments

Comments
 (0)