Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

All notable changes to this project will be documented here.

## [Unreleased]

### Added
- Opt-in integration test suite (`tests/test_integration.py`) that
exercises real clipboard tools. Skipped by default; run with
`uv run pytest -m integration`. Covers text round-trip, unicode,
multiline, special characters, format listing, and unavailable
MIME types. Closes #23.

## [2.1.1] - 2026-04-12

### Fixed
Expand Down
5 changes: 4 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ mcp-clipboard is an MCP (Model Context Protocol) server that reads and writes th
# Install dependencies
uv sync

# Run all tests
# Run all tests (mocked, no clipboard needed)
uv run pytest

# Run a single test file
Expand All @@ -21,6 +21,9 @@ uv run pytest tests/test_parser.py
# Run a single test function
uv run pytest tests/test_parser.py::test_parse_google_sheets_html

# Run integration tests (requires real clipboard daemon)
uv run pytest -m integration

# Run the MCP server (stdio mode)
uv run mcp-clipboard

Expand Down
5 changes: 4 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,14 @@ For anything bigger than a one-line fix:

```bash
uv sync --extra dev # install runtime + dev dependencies
uv run pytest # run the full test suite
uv run pytest # run the full test suite (mocked, no clipboard needed)
uv run pytest tests/test_parser.py # single test file
uv run pytest -k "test_format_html" # single test by name
uv run pytest -m integration # integration tests (real clipboard)
```

**Integration tests** (`tests/test_integration.py`) exercise real clipboard tools (`wl-paste`, `xclip`, `pbpaste`, etc.) and are skipped by default. Run them with `-m integration` if you have a clipboard daemon available. These are especially useful for platform testers (#5, #10).

Requires **Python 3.11+**. See the [`README → Development`](README.md#development) section for additional commands (build, debug logging, MCP Inspector).

## PR requirements
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ artifacts = ["src/mcp_clipboard/instructions/*.md", "src/mcp_clipboard/icons/*.s
pythonpath = ["src"]
testpaths = ["tests"]
asyncio_mode = "auto"
markers = ["integration: requires real clipboard tools (skipped by default)"]
addopts = "-m 'not integration'"

[tool.ruff]
target-version = "py311"
Expand Down
77 changes: 77 additions & 0 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""Integration tests that exercise real clipboard tools.

Skipped by default. Run with:

uv run pytest -m integration

Requires a running clipboard daemon (Wayland compositor, X11 server,
or macOS/Windows desktop session). These tests write to and read from
the actual system clipboard.
"""

from __future__ import annotations

import pytest

from mcp_clipboard.clipboard import (
list_clipboard_formats,
read_clipboard,
write_clipboard,
)

pytestmark = pytest.mark.integration


@pytest.mark.asyncio
async def test_round_trip_plain_text():
"""Write plain text and read it back."""
test_value = "mcp-clipboard integration test"
await write_clipboard(test_value)
result = await read_clipboard("text/plain")
assert result.strip() == test_value


@pytest.mark.asyncio
async def test_round_trip_unicode():
"""Write unicode text and read it back."""
test_value = "Hello \U0001f30d \u4f60\u597d"
await write_clipboard(test_value)
result = await read_clipboard("text/plain")
assert result.strip() == test_value


@pytest.mark.asyncio
async def test_round_trip_multiline():
"""Write multiline text and read it back."""
test_value = "line one\nline two\nline three"
await write_clipboard(test_value)
result = await read_clipboard("text/plain")
assert result.strip() == test_value


@pytest.mark.asyncio
async def test_round_trip_special_chars():
"""Write text with special characters and read it back."""
test_value = 'pipes | and "quotes" and <angles> & ampersands'
await write_clipboard(test_value)
result = await read_clipboard("text/plain")
assert result.strip() == test_value


@pytest.mark.asyncio
async def test_list_formats_includes_text():
"""After writing text, list_clipboard_formats should include a text type."""
await write_clipboard("format check")
formats = await list_clipboard_formats()
assert isinstance(formats, list)
assert len(formats) > 0
# At least one text format should be present
text_formats = [f for f in formats if "text" in f.lower() or "string" in f.lower()]
assert text_formats, f"No text format found in: {formats}"


@pytest.mark.asyncio
async def test_read_empty_mime_returns_empty():
"""Reading an unavailable MIME type should return an empty string."""
result = await read_clipboard("application/x-nonexistent-format")
assert result == ""
Loading