Jelajahi Sumber

fix: update test mocks for ImageConversionResult return type

- Update image-utils.test.ts to expect { images: [], errors: [] } format
- Update useStdinJsonHandler.test.ts mock to return ImageConversionResult
- All 2040 CLI tests now pass
marius-kilocode 3 minggu lalu
induk
melakukan
fbdd20e55e

+ 12 - 8
cli/src/media/__tests__/image-utils.test.ts

@@ -46,36 +46,40 @@ describe("convertImagesToDataUrls", () => {
 		vi.clearAllMocks()
 	})
 
-	it("should return undefined for undefined input", async () => {
+	it("should return empty result for undefined input", async () => {
 		const result = await convertImagesToDataUrls(undefined)
-		expect(result).toBeUndefined()
+		expect(result).toEqual({ images: [], errors: [] })
 	})
 
-	it("should return undefined for empty array", async () => {
+	it("should return empty result for empty array", async () => {
 		const result = await convertImagesToDataUrls([])
-		expect(result).toBeUndefined()
+		expect(result).toEqual({ images: [], errors: [] })
 	})
 
 	it("should pass through data URLs unchanged", async () => {
 		const dataUrl =
 			"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
 		const result = await convertImagesToDataUrls([dataUrl])
-		expect(result).toEqual([dataUrl])
+		expect(result.images).toEqual([dataUrl])
+		expect(result.errors).toEqual([])
 	})
 
 	it("should convert file paths to data URLs", async () => {
 		const result = await convertImagesToDataUrls(["/tmp/image.png"])
-		expect(result).toEqual(["data:image/png;base64,mock-tmpimagepng"])
+		expect(result.images).toEqual(["data:image/png;base64,mock-tmpimagepng"])
+		expect(result.errors).toEqual([])
 	})
 
 	it("should handle mixed data URLs and file paths", async () => {
 		const dataUrl = "data:image/png;base64,existing"
 		const result = await convertImagesToDataUrls([dataUrl, "/tmp/new.png"])
-		expect(result).toEqual([dataUrl, "data:image/png;base64,mock-tmpnewpng"])
+		expect(result.images).toEqual([dataUrl, "data:image/png;base64,mock-tmpnewpng"])
+		expect(result.errors).toEqual([])
 	})
 
 	it("should handle multiple file paths", async () => {
 		const result = await convertImagesToDataUrls(["/tmp/a.png", "/tmp/b.png"])
-		expect(result).toEqual(["data:image/png;base64,mock-tmpapng", "data:image/png;base64,mock-tmpbpng"])
+		expect(result.images).toEqual(["data:image/png;base64,mock-tmpapng", "data:image/png;base64,mock-tmpbpng"])
+		expect(result.errors).toEqual([])
 	})
 })

+ 15 - 5
cli/src/media/image-utils.ts

@@ -13,6 +13,16 @@ export function isDataUrl(str: string): boolean {
 	return str.startsWith("data:")
 }
 
+/**
+ * Result of image conversion, including both successful conversions and errors.
+ */
+export interface ImageConversionResult {
+	/** Successfully converted data URLs */
+	images: string[]
+	/** Errors for images that failed to convert */
+	errors: Array<{ path: string; error: string }>
+}
+
 /**
  * Convert image paths to data URLs if needed.
  * If images are already data URLs, they are passed through unchanged.
@@ -20,14 +30,14 @@ export function isDataUrl(str: string): boolean {
  *
  * @param images Array of image paths or data URLs
  * @param logContext Optional context string for error logging
- * @returns Array of data URLs (or undefined if no valid images)
+ * @returns Object containing successful images and any errors
  */
 export async function convertImagesToDataUrls(
 	images: string[] | undefined,
 	logContext: string = "image-utils",
-): Promise<string[] | undefined> {
+): Promise<ImageConversionResult> {
 	if (!images || images.length === 0) {
-		return undefined
+		return { images: [], errors: [] }
 	}
 
 	// Separate data URLs from file paths
@@ -44,7 +54,7 @@ export async function convertImagesToDataUrls(
 
 	// If all images are already data URLs, return them directly
 	if (filePaths.length === 0) {
-		return dataUrls.length > 0 ? dataUrls : undefined
+		return { images: dataUrls, errors: [] }
 	}
 
 	// Convert file paths to data URLs
@@ -58,5 +68,5 @@ export async function convertImagesToDataUrls(
 
 	// Combine existing data URLs with newly converted ones
 	const allDataUrls = [...dataUrls, ...result.images]
-	return allDataUrls.length > 0 ? allDataUrls : undefined
+	return { images: allDataUrls, errors: result.errors }
 }

+ 3 - 2
cli/src/state/hooks/__tests__/useStdinJsonHandler.test.ts

@@ -11,10 +11,11 @@ import { handleStdinMessage, type StdinMessage, type StdinMessageHandlers } from
 // Mock the image-utils module which is used by useStdinJsonHandler
 vi.mock("../../../media/image-utils.js", () => ({
 	convertImagesToDataUrls: vi.fn().mockImplementation(async (images: string[] | undefined) => {
-		if (!images || images.length === 0) return undefined
-		return images.map((img) =>
+		if (!images || images.length === 0) return { images: [], errors: [] }
+		const convertedImages = images.map((img) =>
 			img.startsWith("data:") ? img : `data:image/png;base64,mock-${img.replace(/[^a-zA-Z0-9]/g, "")}`,
 		)
+		return { images: convertedImages, errors: [] }
 	}),
 }))
 

+ 32 - 3
cli/src/state/hooks/useStdinJsonHandler.ts

@@ -39,6 +39,14 @@ export interface StdinMessageHandlers {
  *
  * File paths are automatically converted to data URLs before being sent.
  */
+/**
+ * Output a JSON message to stdout for the Agent Manager to consume.
+ * Used for error notifications and other structured output.
+ */
+function outputJsonMessage(message: Record<string, unknown>): void {
+	console.log(JSON.stringify(message))
+}
+
 export async function handleStdinMessage(
 	message: StdinMessage,
 	handlers: StdinMessageHandlers,
@@ -49,10 +57,20 @@ export async function handleStdinMessage(
 			// This allows the Agent Manager to send the initial prompt via stdin
 			// instead of CLI args, enabling images to be included with the first message
 			// Images are converted from file paths to data URLs if needed
-			const images = await convertImagesToDataUrls(message.images)
+			const result = await convertImagesToDataUrls(message.images)
+
+			// Notify if some images failed to load
+			if (result.errors.length > 0) {
+				outputJsonMessage({
+					type: "image_load_error",
+					errors: result.errors,
+					message: `Failed to load ${result.errors.length} image(s): ${result.errors.map((e) => e.path).join(", ")}`,
+				})
+			}
+
 			await handlers.sendTask({
 				text: message.text || "",
-				...(images && { images }),
+				...(result.images.length > 0 && { images: result.images }),
 			})
 			return { handled: true }
 		}
@@ -60,7 +78,18 @@ export async function handleStdinMessage(
 		case "askResponse": {
 			// Handle ask response (user message, approval response, etc.)
 			// Images are converted from file paths to data URLs if needed
-			const images = await convertImagesToDataUrls(message.images)
+			const result = await convertImagesToDataUrls(message.images)
+
+			// Notify if some images failed to load
+			if (result.errors.length > 0) {
+				outputJsonMessage({
+					type: "image_load_error",
+					errors: result.errors,
+					message: `Failed to load ${result.errors.length} image(s): ${result.errors.map((e) => e.path).join(", ")}`,
+				})
+			}
+
+			const images = result.images.length > 0 ? result.images : undefined
 			if (message.askResponse === "yesButtonClicked" || message.askResponse === "noButtonClicked") {
 				await handlers.respondToTool({
 					response: message.askResponse,