Skip to content

Commit 1b5b6e6

Browse files
committed
fix: address self-review comments for image mentions feature
- Maintain backward compatibility for parseMentions() return type - Add error handling for corrupted images in processImageFile() - Use i18n for memory limit error message - Improve test clarity with @/path syntax - Add documentation for array flattening logic - Add test coverage for SVG, WebP, and AVIF formats
1 parent b2be7f4 commit 1b5b6e6

File tree

5 files changed

+278
-42
lines changed

5 files changed

+278
-42
lines changed

src/core/mentions/__tests__/imageMentions.spec.ts

Lines changed: 187 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,15 @@ describe("Image Mentions", () => {
140140
20,
141141
)
142142

143-
expect(result.images).toHaveLength(2)
144-
expect(result.images[0]).toBe(mockImageDataUrl1)
145-
expect(result.images[1]).toBe(mockImageDataUrl2)
146-
expect(result.text).toContain("'image1.png' (see below for image)")
147-
expect(result.text).toContain("'image2.jpg' (see below for image)")
143+
// Type guard for TypeScript
144+
expect(typeof result).toBe("object")
145+
if (typeof result === "object") {
146+
expect(result.images).toHaveLength(2)
147+
expect(result.images[0]).toBe(mockImageDataUrl1)
148+
expect(result.images[1]).toBe(mockImageDataUrl2)
149+
expect(result.text).toContain("'image1.png' (see below for image)")
150+
expect(result.text).toContain("'image2.jpg' (see below for image)")
151+
}
148152
})
149153

150154
it("should handle image size limit exceeded", async () => {
@@ -177,8 +181,11 @@ describe("Image Mentions", () => {
177181
20,
178182
)
179183

180-
expect(result.images).toHaveLength(0)
181-
expect(result.text).toContain("Image file is too large (10 MB). Maximum allowed size is 5 MB.")
184+
// When validation fails, we still get an object but with no images
185+
expect(typeof result).toBe("string")
186+
if (typeof result === "string") {
187+
expect(result).toContain("Image file is too large (10 MB). Maximum allowed size is 5 MB.")
188+
}
182189
})
183190

184191
it("should handle model that doesn't support images", async () => {
@@ -210,8 +217,11 @@ describe("Image Mentions", () => {
210217
20,
211218
)
212219

213-
expect(result.images).toHaveLength(0)
214-
expect(result.text).toContain("Image file detected but current model does not support images")
220+
// When model doesn't support images, result should be a string
221+
expect(typeof result).toBe("string")
222+
if (typeof result === "string") {
223+
expect(result).toContain("Image file detected but current model does not support images")
224+
}
215225
})
216226

217227
it("should handle mixed content with images and regular files", async () => {
@@ -263,12 +273,16 @@ describe("Image Mentions", () => {
263273
20,
264274
)
265275

266-
expect(result.images).toHaveLength(1)
267-
expect(result.images[0]).toBe(mockImageDataUrl)
268-
expect(result.text).toContain("'image.png' (see below for image)")
269-
// The script.js file will have an error because we're not fully mocking the file system
270-
// but that's okay for this test - we're mainly testing that images and non-images are handled differently
271-
expect(result.text).toContain("script.js")
276+
// Type guard for TypeScript
277+
expect(typeof result).toBe("object")
278+
if (typeof result === "object") {
279+
expect(result.images).toHaveLength(1)
280+
expect(result.images[0]).toBe(mockImageDataUrl)
281+
expect(result.text).toContain("'image.png' (see below for image)")
282+
// The script.js file will have an error because we're not fully mocking the file system
283+
// but that's okay for this test - we're mainly testing that images and non-images are handled differently
284+
expect(result.text).toContain("script.js")
285+
}
272286
})
273287

274288
it("should respect .rooignore for image files", async () => {
@@ -299,8 +313,11 @@ describe("Image Mentions", () => {
299313
20,
300314
)
301315

302-
expect(result.images).toHaveLength(0)
303-
expect(result.text).toContain("(Image ignored-image.png is ignored by .rooignore)")
316+
// When image is ignored, result should be a string
317+
expect(typeof result).toBe("string")
318+
if (typeof result === "string") {
319+
expect(result).toContain("(Image ignored-image.png is ignored by .rooignore)")
320+
}
304321
})
305322

306323
it("should handle total memory limit for multiple images", async () => {
@@ -348,8 +365,159 @@ describe("Image Mentions", () => {
348365
20, // maxTotalImageSize
349366
)
350367

351-
expect(result.images).toHaveLength(1)
352-
expect(result.text).toContain("Image skipped to avoid size limit")
368+
// Type guard for TypeScript
369+
expect(typeof result).toBe("object")
370+
if (typeof result === "object") {
371+
expect(result.images).toHaveLength(1)
372+
expect(result.text).toContain("Image skipped to avoid size limit")
373+
}
374+
})
375+
376+
it("should handle SVG image format", async () => {
377+
const mockSvgDataUrl =
378+
"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg=="
379+
380+
vi.mocked(imageHelpers.isSupportedImageFormat).mockImplementation((ext) => ext === ".svg")
381+
vi.mocked(imageHelpers.validateImageForProcessing).mockResolvedValue({
382+
isValid: true,
383+
sizeInMB: 0.1,
384+
})
385+
386+
vi.mocked(imageHelpers.processImageFile).mockResolvedValue({
387+
dataUrl: mockSvgDataUrl,
388+
buffer: Buffer.from('<svg xmlns="http://www.w3.org/2000/svg"></svg>'),
389+
sizeInKB: 100,
390+
sizeInMB: 0.1,
391+
notice: "Image (100 KB)",
392+
})
393+
394+
vi.mocked(fs.stat).mockResolvedValue({
395+
isFile: () => true,
396+
isDirectory: () => false,
397+
size: 102400,
398+
} as any)
399+
400+
const result = await parseMentions(
401+
"Check @/logo.svg",
402+
"/workspace",
403+
mockUrlContentFetcher,
404+
undefined,
405+
undefined,
406+
true,
407+
true,
408+
50,
409+
undefined,
410+
true,
411+
5,
412+
20,
413+
)
414+
415+
// Type guard for TypeScript
416+
expect(typeof result).toBe("object")
417+
if (typeof result === "object") {
418+
expect(result).toHaveProperty("images")
419+
expect(result.images).toHaveLength(1)
420+
expect(result.images[0]).toBe(mockSvgDataUrl)
421+
expect(result.text).toContain("'logo.svg' (see below for image)")
422+
}
423+
})
424+
425+
it("should handle WebP image format", async () => {
426+
const mockWebpDataUrl =
427+
"data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAAQAcJaQAA3AA/v3AgAA="
428+
429+
vi.mocked(imageHelpers.isSupportedImageFormat).mockImplementation((ext) => ext === ".webp")
430+
vi.mocked(imageHelpers.validateImageForProcessing).mockResolvedValue({
431+
isValid: true,
432+
sizeInMB: 0.2,
433+
})
434+
435+
vi.mocked(imageHelpers.processImageFile).mockResolvedValue({
436+
dataUrl: mockWebpDataUrl,
437+
buffer: Buffer.from("webp data"),
438+
sizeInKB: 200,
439+
sizeInMB: 0.2,
440+
notice: "Image (200 KB)",
441+
})
442+
443+
vi.mocked(fs.stat).mockResolvedValue({
444+
isFile: () => true,
445+
isDirectory: () => false,
446+
size: 204800,
447+
} as any)
448+
449+
const result = await parseMentions(
450+
"Check @/photo.webp",
451+
"/workspace",
452+
mockUrlContentFetcher,
453+
undefined,
454+
undefined,
455+
true,
456+
true,
457+
50,
458+
undefined,
459+
true,
460+
5,
461+
20,
462+
)
463+
464+
// Type guard for TypeScript
465+
expect(typeof result).toBe("object")
466+
if (typeof result === "object") {
467+
expect(result).toHaveProperty("images")
468+
expect(result.images).toHaveLength(1)
469+
expect(result.images[0]).toBe(mockWebpDataUrl)
470+
expect(result.text).toContain("'photo.webp' (see below for image)")
471+
}
472+
})
473+
474+
it("should handle AVIF image format", async () => {
475+
const mockAvifDataUrl =
476+
"data:image/avif;base64,AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAADybWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAGxpYmF2aWYAAAAADnBpdG0AAAAAAAEAAAAeaWxvYwAAAABEAAABAAEAAAABAAABGgAAAB0AAAAoaWluZgAAAAAAAQAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAamlwcnAAAABLaXBjbwAAABRpc3BlAAAAAAAAAAEAAAABAAAAEHBpeGkAAAAAAwgICAAAAAxhdjFDgQ0MAAAAABNjb2xybmNseAACAAIABoAAAAAXaXBtYQAAAAAAAAABAAEEAQKDBAAAACVtZGF0EgAKCBgANogQEAwgMg=="
477+
478+
vi.mocked(imageHelpers.isSupportedImageFormat).mockImplementation((ext) => ext === ".avif")
479+
vi.mocked(imageHelpers.validateImageForProcessing).mockResolvedValue({
480+
isValid: true,
481+
sizeInMB: 0.3,
482+
})
483+
484+
vi.mocked(imageHelpers.processImageFile).mockResolvedValue({
485+
dataUrl: mockAvifDataUrl,
486+
buffer: Buffer.from("avif data"),
487+
sizeInKB: 300,
488+
sizeInMB: 0.3,
489+
notice: "Image (300 KB)",
490+
})
491+
492+
vi.mocked(fs.stat).mockResolvedValue({
493+
isFile: () => true,
494+
isDirectory: () => false,
495+
size: 307200,
496+
} as any)
497+
498+
const result = await parseMentions(
499+
"Check @/modern.avif",
500+
"/workspace",
501+
mockUrlContentFetcher,
502+
undefined,
503+
undefined,
504+
true,
505+
true,
506+
50,
507+
undefined,
508+
true,
509+
5,
510+
20,
511+
)
512+
513+
// Type guard for TypeScript
514+
expect(typeof result).toBe("object")
515+
if (typeof result === "object") {
516+
expect(result).toHaveProperty("images")
517+
expect(result.images).toHaveLength(1)
518+
expect(result.images[0]).toBe(mockAvifDataUrl)
519+
expect(result.text).toContain("'modern.avif' (see below for image)")
520+
}
353521
})
354522
})
355523
})

src/core/mentions/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ export async function parseMentions(
9999
supportsImages: boolean = false,
100100
maxImageFileSize: number = DEFAULT_MAX_IMAGE_FILE_SIZE_MB,
101101
maxTotalImageSize: number = DEFAULT_MAX_TOTAL_IMAGE_SIZE_MB,
102-
): Promise<{ text: string; images: string[] }> {
102+
): Promise<string | { text: string; images: string[] }> {
103103
const mentions: Set<string> = new Set()
104104
const validCommands: Map<string, Command> = new Map()
105105
const imageDataUrls: string[] = []
@@ -327,6 +327,11 @@ export async function parseMentions(
327327
}
328328
}
329329

330+
// Maintain backward compatibility: if no images were found, return just the string
331+
// Otherwise, return the new format with text and images
332+
if (imageDataUrls.length === 0) {
333+
return parsedText
334+
}
330335
return { text: parsedText, images: imageDataUrls }
331336
}
332337

src/core/mentions/processUserContentMentions.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,15 @@ export async function processUserContentMentions({
6969
maxTotalImageSize,
7070
)
7171

72+
// Handle backward compatibility - result can be string or object
73+
if (typeof result === "string") {
74+
// No images found, just return the text block with updated text
75+
return {
76+
...block,
77+
text: result,
78+
}
79+
}
80+
7281
// If there are images, we need to add them as separate image blocks
7382
const blocks: Anthropic.Messages.ContentBlockParam[] = [
7483
{
@@ -116,10 +125,13 @@ export async function processUserContentMentions({
116125
maxTotalImageSize,
117126
)
118127

128+
// Handle backward compatibility - result can be string or object
129+
const textContent = typeof result === "string" ? result : result.text
130+
119131
// For tool_result, we can only return text content, not images
120132
return {
121133
...block,
122-
content: result.text,
134+
content: textContent,
123135
}
124136
}
125137

@@ -143,10 +155,13 @@ export async function processUserContentMentions({
143155
maxTotalImageSize,
144156
)
145157

158+
// Handle backward compatibility - result can be string or object
159+
const textContent = typeof result === "string" ? result : result.text
160+
146161
// For tool_result content blocks, we can only return text
147162
return {
148163
...contentBlock,
149-
text: result.text,
164+
text: textContent,
150165
}
151166
}
152167

@@ -163,7 +178,10 @@ export async function processUserContentMentions({
163178
return block
164179
}),
165180
).then((results) => {
166-
// Flatten any arrays that were returned (when images were added)
181+
// Flatten any arrays that were returned (when images were added).
182+
// This is necessary because when we process image mentions, we return an array
183+
// containing both the text block and separate image blocks. The flat() method
184+
// ensures all blocks are at the same level in the final array.
167185
return results.flat()
168186
})
169187
}

0 commit comments

Comments
 (0)