Skip to content

Commit 8f9f449

Browse files
committed
Add comprehensive tests for content generation and translation features
- Introduced test types for mocking server responses and payloads. - Implemented translation coverage tests for OpenAI to Gemini response translation. - Added translation tests to validate tool configuration and content processing. - Created validation and routing tests to ensure proper error handling and request validation. - Enhanced existing tests to cover various edge cases and ensure robust functionality.
1 parent e6af851 commit 8f9f449

17 files changed

Lines changed: 2694 additions & 128 deletions

src/lib/debug-logger.ts

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import { existsSync, mkdirSync } from "node:fs"
2+
import { writeFile } from "node:fs/promises"
3+
import { join } from "node:path"
4+
5+
import type { GeminiRequest } from "~/routes/generate-content/types"
6+
import type {
7+
ChatCompletionsPayload,
8+
ChatCompletionResponse,
9+
} from "~/services/copilot/create-chat-completions"
10+
11+
interface DebugLogData {
12+
timestamp: string
13+
requestId: string
14+
originalGeminiPayload: GeminiRequest
15+
translatedOpenAIPayload: ChatCompletionsPayload | null
16+
error?: string
17+
processingTime?: number
18+
}
19+
20+
export class DebugLogger {
21+
private static instance: DebugLogger | undefined
22+
private logDir: string
23+
24+
private constructor() {
25+
this.logDir = process.env.DEBUG_LOG_DIR || join(process.cwd(), "debug-logs")
26+
this.ensureLogDir()
27+
}
28+
29+
static getInstance(): DebugLogger {
30+
if (!DebugLogger.instance) {
31+
DebugLogger.instance = new DebugLogger()
32+
}
33+
return DebugLogger.instance
34+
}
35+
36+
private ensureLogDir(): void {
37+
if (!existsSync(this.logDir)) {
38+
mkdirSync(this.logDir, { recursive: true })
39+
}
40+
}
41+
42+
private generateLogFileName(requestId: string): string {
43+
const timestamp = new Date().toISOString().replaceAll(/[:.]/g, "-")
44+
return join(this.logDir, `debug-gemini-${timestamp}-${requestId}.log`)
45+
}
46+
47+
async logRequest(data: {
48+
requestId: string
49+
geminiPayload: GeminiRequest
50+
openAIPayload?: ChatCompletionsPayload | null
51+
error?: string
52+
processingTime?: number
53+
}): Promise<void> {
54+
const logData: DebugLogData = {
55+
timestamp: new Date().toISOString(),
56+
requestId: data.requestId,
57+
originalGeminiPayload: data.geminiPayload,
58+
translatedOpenAIPayload: data.openAIPayload ?? null,
59+
error: data.error,
60+
processingTime: data.processingTime,
61+
}
62+
63+
const logPath = this.generateLogFileName(data.requestId)
64+
65+
try {
66+
await writeFile(logPath, JSON.stringify(logData, null, 2), "utf8")
67+
console.log(`[DEBUG] Logged request data to: ${logPath}`)
68+
} catch (writeError) {
69+
console.error(`[DEBUG] Failed to write log file ${logPath}:`, writeError)
70+
}
71+
}
72+
73+
// For backward compatibility during development
74+
static async logGeminiRequest(
75+
geminiPayload: GeminiRequest,
76+
openAIPayload?: ChatCompletionsPayload,
77+
error?: string,
78+
): Promise<void> {
79+
const logger = DebugLogger.getInstance()
80+
const requestId = Math.random().toString(36).slice(2, 8)
81+
await logger.logRequest({ requestId, geminiPayload, openAIPayload, error })
82+
}
83+
84+
// Log GitHub Copilot API Response
85+
static async logCopilotResponse(
86+
response: ChatCompletionResponse,
87+
context?: string,
88+
): Promise<void> {
89+
const logger = DebugLogger.getInstance()
90+
const requestId = Math.random().toString(36).slice(2, 8)
91+
const timestamp = new Date().toISOString().replaceAll(/[:.]/g, "-")
92+
const logPath = join(
93+
logger.logDir,
94+
`debug-copilot-response-${timestamp}-${requestId}.log`,
95+
)
96+
97+
const logData = {
98+
timestamp: new Date().toISOString(),
99+
context: context || "GitHub Copilot API Response",
100+
response,
101+
}
102+
103+
try {
104+
await writeFile(logPath, JSON.stringify(logData, null, 2), "utf8")
105+
console.log(`[DEBUG] Logged Copilot response to: ${logPath}`)
106+
} catch (writeError) {
107+
console.error(
108+
`[DEBUG] Failed to write Copilot response log file ${logPath}:`,
109+
writeError,
110+
)
111+
}
112+
}
113+
114+
// Log any object for debugging purposes
115+
static async logDebugData(
116+
data: unknown,
117+
context: string,
118+
filePrefix = "debug-data",
119+
): Promise<void> {
120+
const logger = DebugLogger.getInstance()
121+
const requestId = Math.random().toString(36).slice(2, 8)
122+
const timestamp = new Date().toISOString().replaceAll(/[:.]/g, "-")
123+
const logPath = join(
124+
logger.logDir,
125+
`${filePrefix}-${timestamp}-${requestId}.log`,
126+
)
127+
128+
const logData = {
129+
timestamp: new Date().toISOString(),
130+
context,
131+
data,
132+
}
133+
134+
try {
135+
await writeFile(logPath, JSON.stringify(logData, null, 2), "utf8")
136+
console.log(`[DEBUG] Logged ${context} to: ${logPath}`)
137+
} catch (writeError) {
138+
console.error(
139+
`[DEBUG] Failed to write debug log file ${logPath}:`,
140+
writeError,
141+
)
142+
}
143+
}
144+
145+
// Log original and translated response comparison
146+
static async logResponseComparison(
147+
originalResponse: unknown,
148+
translatedResponse: unknown,
149+
options: { context: string; filePrefix?: string } = {
150+
context: "Response Comparison",
151+
},
152+
): Promise<void> {
153+
const { context, filePrefix = "debug-comparison" } = options
154+
const logger = DebugLogger.getInstance()
155+
const requestId = Math.random().toString(36).slice(2, 8)
156+
const timestamp = new Date().toISOString().replaceAll(/[:.]/g, "-")
157+
const logPath = join(
158+
logger.logDir,
159+
`${filePrefix}-${timestamp}-${requestId}.log`,
160+
)
161+
162+
const logData = {
163+
timestamp: new Date().toISOString(),
164+
context,
165+
originalResponse,
166+
translatedResponse,
167+
}
168+
169+
try {
170+
await writeFile(logPath, JSON.stringify(logData, null, 2), "utf8")
171+
console.log(`[DEBUG] Logged ${context} comparison to: ${logPath}`)
172+
} catch (writeError) {
173+
console.error(
174+
`[DEBUG] Failed to write comparison log file ${logPath}:`,
175+
writeError,
176+
)
177+
}
178+
}
179+
}

src/routes/generate-content/handler.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { SSEStreamingApi } from "hono/streaming"
44
import { streamSSE } from "hono/streaming"
55

66
import { awaitApproval } from "~/lib/approval"
7+
import { DebugLogger } from "~/lib/debug-logger"
78
import { checkRateLimit } from "~/lib/rate-limit"
89
import { state } from "~/lib/state"
910
import { getTokenCount } from "~/lib/tokenizer"
@@ -295,6 +296,14 @@ export async function handleGeminiStreamGeneration(c: Context) {
295296

296297
const openAIPayload = translateGeminiToOpenAIStream(geminiPayload, model)
297298

299+
// Log request for debugging (async, non-blocking) - only if debug logging is enabled
300+
if (process.env.DEBUG_GEMINI_REQUESTS === "true") {
301+
DebugLogger.logGeminiRequest(geminiPayload, openAIPayload).catch(
302+
(error: unknown) => {
303+
console.error("[DEBUG] Failed to log request:", error)
304+
},
305+
)
306+
}
298307
if (state.manualApprove) {
299308
await awaitApproval()
300309
}

src/routes/generate-content/route.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,25 @@ import {
88
handleGeminiCountTokens,
99
} from "./handler"
1010

11+
function isStreamGenerate(url: string): boolean {
12+
return url.includes(":streamGenerateContent")
13+
}
14+
function isCountTokens(url: string): boolean {
15+
return url.includes(":countTokens")
16+
}
17+
function isGenerate(url: string): boolean {
18+
return (
19+
url.includes(":generateContent") && !url.includes(":streamGenerateContent")
20+
)
21+
}
22+
1123
const router = new Hono()
1224

1325
// Streaming generation endpoint
1426
// POST /v1beta/models/{model}:streamGenerateContent
1527
router.post("/v1beta/models/*", async (c, next) => {
1628
const url = c.req.url
17-
if (url.includes(":streamGenerateContent")) {
29+
if (isStreamGenerate(url)) {
1830
try {
1931
return await handleGeminiStreamGeneration(c)
2032
} catch (error) {
@@ -28,7 +40,7 @@ router.post("/v1beta/models/*", async (c, next) => {
2840
// POST /v1beta/models/{model}:countTokens
2941
router.post("/v1beta/models/*", async (c, next) => {
3042
const url = c.req.url
31-
if (url.includes(":countTokens")) {
43+
if (isCountTokens(url)) {
3244
try {
3345
return await handleGeminiCountTokens(c)
3446
} catch (error) {
@@ -42,10 +54,7 @@ router.post("/v1beta/models/*", async (c, next) => {
4254
// POST /v1beta/models/{model}:generateContent
4355
router.post("/v1beta/models/*", async (c, next) => {
4456
const url = c.req.url
45-
if (
46-
url.includes(":generateContent")
47-
&& !url.includes(":streamGenerateContent")
48-
) {
57+
if (isGenerate(url)) {
4958
try {
5059
return await handleGeminiGeneration(c)
5160
} catch (error) {

0 commit comments

Comments
 (0)