From 81d443cf403d871befe773f247ae9d800f7fa98c Mon Sep 17 00:00:00 2001 From: worldmozara Date: Mon, 2 Nov 2026 16:30:12 +0800 Subject: [PATCH] cli: answer a cancelled slash command at the boundary --- src/plyngent/cli/slash.py | 6 ++++++ tests/test_cli/test_repl_commands.py | 20 ++++++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/plyngent/cli/slash.py b/src/plyngent/cli/slash.py index 918affa..484c4f3 100644 --- a/src/plyngent/cli/slash.py +++ b/src/plyngent/cli/slash.py @@ -1,5 +1,6 @@ from __future__ import annotations +import asyncio import os import shlex from typing import TYPE_CHECKING, Any, Literal, cast, override @@ -1639,6 +1640,11 @@ async def handle_slash(state: ReplState, line: str) -> bool: args[0] = args[0].lower() try: await awaitlet.async_def(_run_slash_argv, args, state) + except asyncio.CancelledError: + # A command's own cancellable work was stopped (``/btw``'s side turn, a + # prompt): the interrupt belongs to that command. Letting it through + # would cancel this REPL task and end the session instead. + click.secho("cancelled", fg="yellow") except ReplExitError: return False except click.ClickException as exc: diff --git a/tests/test_cli/test_repl_commands.py b/tests/test_cli/test_repl_commands.py index daaed7d..1a976a0 100644 --- a/tests/test_cli/test_repl_commands.py +++ b/tests/test_cli/test_repl_commands.py @@ -1,7 +1,8 @@ from __future__ import annotations +import asyncio import base64 -from typing import TYPE_CHECKING, Literal, overload +from typing import TYPE_CHECKING, Literal, cast, overload import pytest import tomlkit @@ -28,7 +29,7 @@ from plyngent.tools import image_input_refusal from plyngent.tools.context import bind_instance if TYPE_CHECKING: - from collections.abc import AsyncIterator + from collections.abc import AsyncIterator, Sequence from pathlib import Path @@ -110,6 +111,21 @@ async def test_help(state: ReplState) -> None: assert await handle_slash(state, "/help") is True +async def test_a_cancelled_slash_command_keeps_the_repl( + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + """A command's own work being cancelled must not cancel the REPL's task.""" + from plyngent.cli import slash as slash_module + + def cancelled(_args: Sequence[str], _state: object) -> None: + raise asyncio.CancelledError + + monkeypatch.setattr(slash_module, "_run_slash_argv", cancelled) + assert await slash_module.handle_slash(cast("ReplState", cast("object", None)), "/btw hello") is True + assert "cancelled" in capsys.readouterr().out + + async def test_auto_continue_limits_installs_on_limit(tmp_path: Path) -> None: from plyngent.cli.limits import auto_continue_enabled, reset_auto_continue