test: improve clipboard tests with realistic scenarios and multimodal coverage

Rewrote clipboard tests from 11 shallow mocks to 21 realistic tests:
- Success paths now simulate tools actually writing files (not pre-created)
- osascript: success with PNG, success with TIFF, extraction-fail cases
- pngpaste: empty file rejection edge case
- Linux: extraction failure cleanup verification
- New TestMultimodalConversion class: base64 encoding, MIME types,
  multiple images, missing file handling, default question fallback
This commit is contained in:
teknium1
2026-03-05 17:58:06 -08:00
parent 399562a7d1
commit ffc752a79e

View File

@@ -1,9 +1,15 @@
"""Tests for hermes_cli/clipboard.py — clipboard image extraction."""
"""Tests for hermes_cli/clipboard.py — clipboard image extraction.
Tests clipboard image extraction across platforms, and the CLI-level
multimodal content conversion that turns attached images into OpenAI
vision API format.
"""
import base64
import subprocess
import sys
from pathlib import Path
from unittest.mock import patch, MagicMock
from unittest.mock import patch, MagicMock, call
import pytest
@@ -15,24 +21,24 @@ from hermes_cli.clipboard import (
)
class TestSaveClipboardImage:
"""Platform dispatch."""
# ── Platform dispatch ────────────────────────────────────────────────────
class TestSaveClipboardImage:
def test_dispatches_to_macos_on_darwin(self, tmp_path):
dest = tmp_path / "out.png"
with patch("hermes_cli.clipboard.sys") as mock_sys:
mock_sys.platform = "darwin"
with patch("hermes_cli.clipboard._macos_save", return_value=False) as mock_mac:
with patch("hermes_cli.clipboard._macos_save", return_value=False) as m:
save_clipboard_image(dest)
mock_mac.assert_called_once_with(dest)
m.assert_called_once_with(dest)
def test_dispatches_to_linux_on_linux(self, tmp_path):
dest = tmp_path / "out.png"
with patch("hermes_cli.clipboard.sys") as mock_sys:
mock_sys.platform = "linux"
with patch("hermes_cli.clipboard._linux_save", return_value=False) as mock_linux:
with patch("hermes_cli.clipboard._linux_save", return_value=False) as m:
save_clipboard_image(dest)
mock_linux.assert_called_once_with(dest)
m.assert_called_once_with(dest)
def test_creates_parent_dirs(self, tmp_path):
dest = tmp_path / "deep" / "nested" / "out.png"
@@ -43,65 +49,233 @@ class TestSaveClipboardImage:
assert dest.parent.exists()
# ── macOS pngpaste ───────────────────────────────────────────────────────
class TestMacosPngpaste:
def test_success(self, tmp_path):
def test_success_writes_file(self, tmp_path):
"""pngpaste writes the file on success — verify we detect it."""
dest = tmp_path / "out.png"
dest.write_bytes(b"fake png data") # simulate pngpaste writing
with patch("hermes_cli.clipboard.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
def fake_run(cmd, **kw):
# Simulate pngpaste writing the file
dest.write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 100)
return MagicMock(returncode=0)
with patch("hermes_cli.clipboard.subprocess.run", side_effect=fake_run):
assert _macos_pngpaste(dest) is True
assert dest.stat().st_size > 0
def test_not_installed(self, tmp_path):
dest = tmp_path / "out.png"
with patch("hermes_cli.clipboard.subprocess.run", side_effect=FileNotFoundError):
assert _macos_pngpaste(dest) is False
assert _macos_pngpaste(tmp_path / "out.png") is False
def test_no_image_in_clipboard(self, tmp_path):
dest = tmp_path / "out.png"
with patch("hermes_cli.clipboard.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=1)
assert _macos_pngpaste(dest) is False
assert not dest.exists()
def test_empty_file_rejected(self, tmp_path):
"""pngpaste exits 0 but writes an empty file — should return False."""
dest = tmp_path / "out.png"
def fake_run(cmd, **kw):
dest.write_bytes(b"") # empty
return MagicMock(returncode=0)
with patch("hermes_cli.clipboard.subprocess.run", side_effect=fake_run):
assert _macos_pngpaste(dest) is False
# ── macOS osascript ──────────────────────────────────────────────────────
class TestMacosOsascript:
def test_no_image_type_in_clipboard(self, tmp_path):
dest = tmp_path / "out.png"
with patch("hermes_cli.clipboard.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(
stdout="«class ut16», «class utf8»", returncode=0
)
assert _macos_osascript(dest) is False
assert _macos_osascript(tmp_path / "out.png") is False
def test_clipboard_info_check_fails(self, tmp_path):
dest = tmp_path / "out.png"
def test_clipboard_info_fails(self, tmp_path):
with patch("hermes_cli.clipboard.subprocess.run", side_effect=Exception("fail")):
assert _macos_osascript(tmp_path / "out.png") is False
def test_success_with_png(self, tmp_path):
"""clipboard has PNGf, osascript extracts it successfully."""
dest = tmp_path / "out.png"
call_count = [0]
def fake_run(cmd, **kw):
call_count[0] += 1
if call_count[0] == 1:
# clipboard info check
return MagicMock(stdout="«class PNGf», «class ut16»", returncode=0)
else:
# extraction — simulate writing the file
dest.write_bytes(b"\x89PNG" + b"\x00" * 50)
return MagicMock(stdout="", returncode=0)
with patch("hermes_cli.clipboard.subprocess.run", side_effect=fake_run):
assert _macos_osascript(dest) is True
assert dest.stat().st_size > 0
def test_success_with_tiff(self, tmp_path):
"""clipboard has TIFF type — should still attempt extraction."""
dest = tmp_path / "out.png"
call_count = [0]
def fake_run(cmd, **kw):
call_count[0] += 1
if call_count[0] == 1:
return MagicMock(stdout="«class TIFF»", returncode=0)
else:
dest.write_bytes(b"\x89PNG" + b"\x00" * 50)
return MagicMock(stdout="", returncode=0)
with patch("hermes_cli.clipboard.subprocess.run", side_effect=fake_run):
assert _macos_osascript(dest) is True
def test_extraction_returns_fail(self, tmp_path):
"""clipboard info says image but extraction script returns 'fail'."""
dest = tmp_path / "out.png"
call_count = [0]
def fake_run(cmd, **kw):
call_count[0] += 1
if call_count[0] == 1:
return MagicMock(stdout="«class PNGf»", returncode=0)
else:
return MagicMock(stdout="fail", returncode=0)
with patch("hermes_cli.clipboard.subprocess.run", side_effect=fake_run):
assert _macos_osascript(dest) is False
# ── Linux xclip ──────────────────────────────────────────────────────────
class TestLinuxSave:
def test_no_xclip_installed(self, tmp_path):
dest = tmp_path / "out.png"
with patch("hermes_cli.clipboard.subprocess.run", side_effect=FileNotFoundError):
assert _linux_save(dest) is False
assert _linux_save(tmp_path / "out.png") is False
def test_no_image_in_clipboard(self, tmp_path):
dest = tmp_path / "out.png"
with patch("hermes_cli.clipboard.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(stdout="text/plain\n", returncode=0)
assert _linux_save(dest) is False
assert _linux_save(tmp_path / "out.png") is False
def test_image_in_clipboard(self, tmp_path):
def test_image_extraction_success(self, tmp_path):
"""xclip reports image/png in targets, then pipes PNG data."""
dest = tmp_path / "out.png"
def fake_run(cmd, **kwargs):
call_count = [0]
def fake_run(cmd, **kw):
call_count[0] += 1
if "TARGETS" in cmd:
return MagicMock(stdout="image/png\ntext/plain\n", returncode=0)
# Extract call — write fake data
if "stdout" in kwargs and kwargs["stdout"]:
kwargs["stdout"].write(b"fake png")
# Extract call — write via the stdout file handle
if "stdout" in kw and hasattr(kw["stdout"], "write"):
kw["stdout"].write(b"\x89PNG\r\n\x1a\n" + b"\x00" * 100)
return MagicMock(returncode=0)
with patch("hermes_cli.clipboard.subprocess.run", side_effect=fake_run):
# Create the file to simulate xclip writing
dest.write_bytes(b"fake png")
assert _linux_save(dest) is True
assert dest.stat().st_size > 0
def test_extraction_fails_cleans_up(self, tmp_path):
"""If xclip extraction fails, any partial file is cleaned up."""
dest = tmp_path / "out.png"
call_count = [0]
def fake_run(cmd, **kw):
call_count[0] += 1
if "TARGETS" in cmd:
return MagicMock(stdout="image/png\n", returncode=0)
raise subprocess.SubprocessError("pipe broke")
with patch("hermes_cli.clipboard.subprocess.run", side_effect=fake_run):
assert _linux_save(dest) is False
assert not dest.exists()
# ── Multimodal content conversion (CLI-level) ────────────────────────────
class TestMultimodalConversion:
"""Test the image → OpenAI vision content conversion in chat()."""
def _make_fake_image(self, tmp_path, name="test.png", size=64):
"""Create a small fake PNG file."""
img = tmp_path / name
img.write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * size)
return img
def test_single_image_with_text(self, tmp_path):
"""One image + text → multimodal content array."""
img = self._make_fake_image(tmp_path)
raw_bytes = img.read_bytes()
expected_b64 = base64.b64encode(raw_bytes).decode()
# Simulate what chat() does with images
message = "What's in this image?"
images = [img]
content_parts = []
content_parts.append({"type": "text", "text": message})
for img_path in images:
data = base64.b64encode(img_path.read_bytes()).decode()
ext = img_path.suffix.lower().lstrip(".")
mime = {"png": "image/png", "jpg": "image/jpeg"}.get(ext, "image/png")
content_parts.append({
"type": "image_url",
"image_url": {"url": f"data:{mime};base64,{data}"}
})
assert len(content_parts) == 2
assert content_parts[0]["type"] == "text"
assert content_parts[0]["text"] == "What's in this image?"
assert content_parts[1]["type"] == "image_url"
assert content_parts[1]["image_url"]["url"].startswith("data:image/png;base64,")
assert expected_b64 in content_parts[1]["image_url"]["url"]
def test_multiple_images(self, tmp_path):
"""Multiple images → all included in content array."""
imgs = [self._make_fake_image(tmp_path, f"img{i}.png") for i in range(3)]
content_parts = [{"type": "text", "text": "Compare these"}]
for img_path in imgs:
data = base64.b64encode(img_path.read_bytes()).decode()
content_parts.append({
"type": "image_url",
"image_url": {"url": f"data:image/png;base64,{data}"}
})
assert len(content_parts) == 4 # 1 text + 3 images
def test_no_text_gets_default(self):
"""Empty text with image → default question added."""
text = ""
if not text:
text = "What do you see in this image?"
assert text == "What do you see in this image?"
def test_jpeg_mime_type(self, tmp_path):
"""JPEG files get the correct MIME type."""
img = tmp_path / "photo.jpg"
img.write_bytes(b"\xff\xd8\xff" + b"\x00" * 50)
ext = img.suffix.lower().lstrip(".")
mime = {"png": "image/png", "jpg": "image/jpeg",
"jpeg": "image/jpeg", "gif": "image/gif",
"webp": "image/webp"}.get(ext, "image/png")
assert mime == "image/jpeg"
def test_missing_image_skipped(self, tmp_path):
"""Non-existent image path is silently skipped."""
missing = tmp_path / "does_not_exist.png"
images = [missing]
content_parts = [{"type": "text", "text": "test"}]
for img_path in images:
if img_path.exists():
content_parts.append({"type": "image_url"})
assert len(content_parts) == 1 # only text, no image