-
Notifications
You must be signed in to change notification settings - Fork 910
feat(slash): add /copy command to copy latest assistant response to clipboard #1726
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| """Tests for /copy slash command.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from pathlib import Path | ||
| from unittest.mock import patch | ||
|
|
||
| import pytest | ||
| from kosong.message import Message, TextPart, ThinkPart | ||
| from kosong.tooling.empty import EmptyToolset | ||
|
|
||
| from kimi_cli.soul.agent import Agent, Runtime | ||
| from kimi_cli.soul.context import Context | ||
| from kimi_cli.soul.kimisoul import KimiSoul | ||
| from kimi_cli.soul.slash import copy | ||
| from kimi_cli.wire.types import TextPart as WireTextPart | ||
|
|
||
|
|
||
| def _make_soul(runtime: Runtime, tmp_path: Path) -> KimiSoul: | ||
| agent = Agent( | ||
| name="Test Agent", | ||
| system_prompt="Test system prompt.", | ||
| toolset=EmptyToolset(), | ||
| runtime=runtime, | ||
| ) | ||
| return KimiSoul(agent, context=Context(file_backend=tmp_path / "history.jsonl")) | ||
|
|
||
|
|
||
| async def _run_copy(soul: KimiSoul) -> None: | ||
| result = copy(soul, "") | ||
| if result is not None: | ||
| await result | ||
|
|
||
|
|
||
| class TestCopySlashCommand: | ||
| async def test_copy_no_assistant_message( | ||
| self, runtime: Runtime, tmp_path: Path, monkeypatch: pytest.MonkeyPatch | ||
| ) -> None: | ||
| soul = _make_soul(runtime, tmp_path) | ||
| sent: list[WireTextPart] = [] | ||
| monkeypatch.setattr("kimi_cli.soul.slash.wire_send", lambda msg: sent.append(msg)) | ||
|
|
||
| await _run_copy(soul) | ||
|
|
||
| assert any("No assistant response to copy" in s.text for s in sent) | ||
|
|
||
| async def test_copy_text_only( | ||
| self, runtime: Runtime, tmp_path: Path, monkeypatch: pytest.MonkeyPatch | ||
| ) -> None: | ||
| soul = _make_soul(runtime, tmp_path) | ||
| await soul.context.append_message( | ||
| Message(role="assistant", content=[TextPart(text="Hello, world!")]) | ||
| ) | ||
|
|
||
| sent: list[WireTextPart] = [] | ||
| monkeypatch.setattr("kimi_cli.soul.slash.wire_send", lambda msg: sent.append(msg)) | ||
|
|
||
| with patch("kimi_cli.utils.clipboard.copy_text_to_clipboard") as mock_copy: | ||
| await _run_copy(soul) | ||
|
|
||
| mock_copy.assert_called_once_with("Hello, world!") | ||
| assert any("Copied the latest assistant response" in s.text for s in sent) | ||
|
Comment on lines
+47
to
+62
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Tests don't mock The tests Prompt for agentsWas this helpful? React with 👍 or 👎 to provide feedback. |
||
|
|
||
| async def test_copy_with_thinking( | ||
| self, runtime: Runtime, tmp_path: Path, monkeypatch: pytest.MonkeyPatch | ||
| ) -> None: | ||
| soul = _make_soul(runtime, tmp_path) | ||
| await soul.context.append_message( | ||
| Message( | ||
| role="assistant", | ||
| content=[ | ||
| ThinkPart(think="I should say hello."), | ||
| TextPart(text="Hello, world!"), | ||
| ], | ||
| ) | ||
| ) | ||
|
|
||
| sent: list[WireTextPart] = [] | ||
| monkeypatch.setattr("kimi_cli.soul.slash.wire_send", lambda msg: sent.append(msg)) | ||
|
|
||
| with patch("kimi_cli.utils.clipboard.copy_text_to_clipboard") as mock_copy: | ||
| await _run_copy(soul) | ||
|
|
||
| copied = mock_copy.call_args[0][0] | ||
| assert "I should say hello." in copied | ||
| assert "Hello, world!" in copied | ||
| assert any("Copied the latest assistant response" in s.text for s in sent) | ||
|
|
||
| async def test_copy_latest_assistant_only( | ||
| self, runtime: Runtime, tmp_path: Path, monkeypatch: pytest.MonkeyPatch | ||
| ) -> None: | ||
| soul = _make_soul(runtime, tmp_path) | ||
| await soul.context.append_message( | ||
| Message(role="assistant", content=[TextPart(text="First response")]) | ||
| ) | ||
| await soul.context.append_message( | ||
| Message(role="user", content=[TextPart(text="Follow up")]) | ||
| ) | ||
| await soul.context.append_message( | ||
| Message(role="assistant", content=[TextPart(text="Second response")]) | ||
| ) | ||
|
|
||
| sent: list[WireTextPart] = [] | ||
| monkeypatch.setattr("kimi_cli.soul.slash.wire_send", lambda msg: sent.append(msg)) | ||
|
|
||
| with patch("kimi_cli.utils.clipboard.copy_text_to_clipboard") as mock_copy: | ||
| await _run_copy(soul) | ||
|
|
||
| mock_copy.assert_called_once_with("Second response") | ||
| assert any("Copied the latest assistant response" in s.text for s in sent) | ||
|
|
||
| async def test_copy_clipboard_unavailable( | ||
| self, runtime: Runtime, tmp_path: Path, monkeypatch: pytest.MonkeyPatch | ||
| ) -> None: | ||
| soul = _make_soul(runtime, tmp_path) | ||
| await soul.context.append_message( | ||
| Message(role="assistant", content=[TextPart(text="Hello")]) | ||
| ) | ||
|
|
||
| sent: list[WireTextPart] = [] | ||
| monkeypatch.setattr("kimi_cli.soul.slash.wire_send", lambda msg: sent.append(msg)) | ||
| monkeypatch.setattr("kimi_cli.utils.clipboard.is_clipboard_available", lambda: False) | ||
|
|
||
| with patch("kimi_cli.utils.clipboard.copy_text_to_clipboard") as mock_copy: | ||
| await _run_copy(soul) | ||
|
|
||
| mock_copy.assert_not_called() | ||
| assert any("Clipboard is not available" in s.text for s in sent) | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new
/copytests (except the explicit unavailable case) call the command without stubbingis_clipboard_available(), so they depend on the host having a working system clipboard. In headless CI/Linux environments wherepyperclipreports no backend,/copyreturns early with the unavailable message and these assertions fail even though command logic is otherwise correct. Please force clipboard availability toTruein the success-path tests to make them deterministic.Useful? React with 👍 / 👎.