@@ -6231,5 +6231,171 @@ events where the header text hasn't changed."
62316231 (goto-char (marker-position pi-coding-agent--hot-tail-start))
62326232 (should (looking-at "Assistant")))))
62336233
6234+ ;;; Inline Image Display
6235+
6236+ (ert-deftest pi-coding-agent-test-mime-to-image-type ()
6237+ "MIME type conversion covers backend types and rejects unknowns."
6238+ (should (eq (pi-coding-agent--mime-to-image-type "image/png") 'png))
6239+ (should (eq (pi-coding-agent--mime-to-image-type "image/jpeg") 'jpeg))
6240+ (should (eq (pi-coding-agent--mime-to-image-type "image/jpg") 'jpeg))
6241+ (should (eq (pi-coding-agent--mime-to-image-type "image/gif") 'gif))
6242+ (should (eq (pi-coding-agent--mime-to-image-type "image/webp") 'webp))
6243+ (should-not (pi-coding-agent--mime-to-image-type "image/tga"))
6244+ (should-not (pi-coding-agent--mime-to-image-type "text/plain")))
6245+
6246+ (ert-deftest pi-coding-agent-test-insert-inline-image-placeholder ()
6247+ "insert-inline-image produces a placeholder in batch mode."
6248+ ;; In batch mode display-images-p is nil, so we always get the placeholder.
6249+ (with-temp-buffer
6250+ (pi-coding-agent--insert-inline-image "iVBORw0KGgo=" "image/png")
6251+ (let ((content (buffer-substring-no-properties (point-min) (point-max))))
6252+ ;; Should contain MIME type and a size indicator
6253+ (should (string-match-p "Image:" content))
6254+ (should (string-match-p "image/png" content)))))
6255+
6256+ (ert-deftest pi-coding-agent-test-insert-inline-image-error-path ()
6257+ "insert-inline-image handles corrupt base64 gracefully."
6258+ (with-temp-buffer
6259+ (pi-coding-agent--insert-inline-image "!!!invalid!!!" "image/png")
6260+ (should (string-match-p "decode error"
6261+ (buffer-substring-no-properties
6262+ (point-min) (point-max))))))
6263+
6264+ (ert-deftest pi-coding-agent-test-tool-end-image-block-renders ()
6265+ "Image content block in tool result is displayed."
6266+ (with-temp-buffer
6267+ (pi-coding-agent-chat-mode)
6268+ (pi-coding-agent--display-tool-end
6269+ "read" '(:path "photo.png")
6270+ '((:type "text" :text "Read image file [image/png]")
6271+ (:type "image" :data "iVBORw0KGgo=" :mimeType "image/png"))
6272+ nil nil)
6273+ (let ((content (buffer-substring-no-properties (point-min) (point-max))))
6274+ (should (string-match-p "Read image file" content))
6275+ ;; In batch: placeholder text; in GUI: [image] fallback from insert-image
6276+ (should (or (string-match-p "\\[Image:" content)
6277+ (string-match-p "\\[image\\]" content))))))
6278+
6279+ (ert-deftest pi-coding-agent-test-tool-end-text-only-no-image ()
6280+ "Tool result with only text blocks has no image placeholder."
6281+ (with-temp-buffer
6282+ (pi-coding-agent-chat-mode)
6283+ (pi-coding-agent--display-tool-end
6284+ "bash" '(:command "ls")
6285+ '((:type "text" :text "file1\nfile2"))
6286+ nil nil)
6287+ (let ((content (buffer-substring-no-properties (point-min) (point-max))))
6288+ (should (string-match-p "file1" content))
6289+ (should-not (string-match-p "\\[Image:" content))
6290+ (should-not (string-match-p "\\[image\\]" content)))))
6291+
6292+ (ert-deftest pi-coding-agent-test-tool-end-image-inside-overlay ()
6293+ "Image content is contained within the tool block overlay."
6294+ (with-temp-buffer
6295+ (pi-coding-agent-chat-mode)
6296+ (let ((block (pi-coding-agent--display-tool-start
6297+ "read" '(:path "photo.png"))))
6298+ (pi-coding-agent--display-tool-end
6299+ "read" '(:path "photo.png")
6300+ '((:type "text" :text "Read image file [image/png]")
6301+ (:type "image" :data "iVBORw0KGgo=" :mimeType "image/png"))
6302+ nil nil block)
6303+ (let ((ov (seq-find (lambda (o) (overlay-get o 'pi-coding-agent-tool-block))
6304+ (overlays-in (point-min) (point-max)))))
6305+ (should ov)
6306+ (let ((ov-content (buffer-substring-no-properties
6307+ (overlay-start ov) (overlay-end ov))))
6308+ (should (or (string-match-p "\\[Image:" ov-content)
6309+ (string-match-p "\\[image\\]" ov-content))))))))
6310+
6311+ (ert-deftest pi-coding-agent-test-tool-end-image-blocks-stored ()
6312+ "Image content blocks are stored on the tool block struct."
6313+ (with-temp-buffer
6314+ (pi-coding-agent-chat-mode)
6315+ (let ((block (pi-coding-agent--display-tool-start
6316+ "read" '(:path "photo.png"))))
6317+ (pi-coding-agent--display-tool-end
6318+ "read" '(:path "photo.png")
6319+ '((:type "text" :text "Read image")
6320+ (:type "image" :data "iVBORw0KGgo=" :mimeType "image/png"))
6321+ nil nil block)
6322+ (should (pi-coding-agent--tool-block-image-blocks block))
6323+ ;; Content should include image block
6324+ (should (seq-some (lambda (b) (equal (plist-get b :type) "image"))
6325+ (pi-coding-agent--tool-block-image-blocks block))))))
6326+
6327+ (ert-deftest pi-coding-agent-test-tool-end-multiple-images ()
6328+ "Multiple image blocks each produce output."
6329+ (with-temp-buffer
6330+ (pi-coding-agent-chat-mode)
6331+ (pi-coding-agent--display-tool-end
6332+ "read" '(:path "photo.png")
6333+ '((:type "text" :text "Two images")
6334+ (:type "image" :data "iVBORw0KGgo=" :mimeType "image/png")
6335+ (:type "image" :data "/9j/4AAQ" :mimeType "image/jpeg"))
6336+ nil nil)
6337+ (let ((content (buffer-substring-no-properties (point-min) (point-max))))
6338+ ;; Both image indicators should be present
6339+ (should (or (>= (length (split-string content "\\[Image:\\|\\[image\\]" t)) 2)
6340+ ;; GUI mode: two [image] from insert-image
6341+ (string-match-p "\\[image\\].*\\[image\\]" content))))))
6342+
6343+ (ert-deftest pi-coding-agent-test-tool-end-image-only ()
6344+ "Tool result with only image blocks, no text, still renders."
6345+ (with-temp-buffer
6346+ (pi-coding-agent-chat-mode)
6347+ (pi-coding-agent--display-tool-end
6348+ "read" '(:path "photo.png")
6349+ '((:type "image" :data "iVBORw0KGgo=" :mimeType "image/png"))
6350+ nil nil)
6351+ (let ((content (buffer-substring-no-properties (point-min) (point-max))))
6352+ (should (or (string-match-p "\\[Image:" content)
6353+ (string-match-p "\\[image\\]" content))))))
6354+
6355+ (ert-deftest pi-coding-agent-test-tool-end-unknown-mime-type ()
6356+ "Unknown MIME type produces a placeholder, not an error."
6357+ (with-temp-buffer
6358+ (pi-coding-agent-chat-mode)
6359+ (pi-coding-agent--display-tool-end
6360+ "read" '(:path "file.tga")
6361+ '((:type "text" :text "Read image")
6362+ (:type "image" :data "AAAA" :mimeType "image/tga"))
6363+ nil nil)
6364+ (let ((content (buffer-substring-no-properties (point-min) (point-max))))
6365+ (should (string-match-p "image/tga" content)))))
6366+
6367+ (ert-deftest pi-coding-agent-test-history-tool-with-image ()
6368+ "History replay renders image from tool result."
6369+ (with-temp-buffer
6370+ (pi-coding-agent-chat-mode)
6371+ (let ((messages [(:role "assistant"
6372+ :content [(:type "toolCall" :id "tc1"
6373+ :name "read"
6374+ :arguments (:path "photo.png"))]
6375+ :timestamp 1704067200000)
6376+ (:role "toolResult" :toolCallId "tc1"
6377+ :toolName "read"
6378+ :content [(:type "text" :text "Read image file [image/png]")
6379+ (:type "image" :data "iVBORw0KGgo=" :mimeType "image/png")]
6380+ :isError :json-false
6381+ :timestamp 1704067201000)]))
6382+ (pi-coding-agent--display-history-messages messages))
6383+ (let ((content (buffer-substring-no-properties (point-min) (point-max))))
6384+ (should (or (string-match-p "\\[Image:.*image/png" content)
6385+ (string-match-p "\\[image\\]" content))))))
6386+
6387+ (ert-deftest pi-coding-agent-test-tool-end-image-with-vector-content ()
6388+ "Image blocks work when content is a vector (JSON parse format)."
6389+ (with-temp-buffer
6390+ (pi-coding-agent-chat-mode)
6391+ (pi-coding-agent--display-tool-end
6392+ "read" '(:path "photo.png")
6393+ [(:type "text" :text "Read image")
6394+ (:type "image" :data "iVBORw0KGgo=" :mimeType "image/png")]
6395+ nil nil)
6396+ (let ((content (buffer-substring-no-properties (point-min) (point-max))))
6397+ (should (or (string-match-p "\\[Image:" content)
6398+ (string-match-p "\\[image\\]" content))))))
6399+
62346400(provide 'pi-coding-agent-render-test)
62356401;;; pi-coding-agent-render-test.el ends here
0 commit comments