mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 21:26:39 +00:00
* fix(tui): keep every word of an unquoted --print/--json message --print and --json took a single optional value and never read the positional words, so `deerflow --print summarize this repo` sent only "summarize", `deerflow summarize this repo --print` exited 2 asking for a message on a terminal, and with piped stdin the typed words were ignored. Both flags now take every following word and fall back to words given before them, as --cli does; a typed message wins over piped stdin. A message split around the flag is refused with exit 2, because argparse loses the word order between the two parts. * docs(changelog): reference #6636 * fix(tui): refuse --print and --json together Only one headless mode runs, so the other flag's words were dropped: '--print hello --json world' ran with 'hello', and '--print --json hello' asked for a message. Put both flags in a mutually exclusive group so argparse exits 2 for every combination. --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
216 lines
6.1 KiB
Python
216 lines
6.1 KiB
Python
"""Tests for TUI launch-mode planning + arg parsing (pure, no Textual)."""
|
|
|
|
import pytest
|
|
|
|
from deerflow.tui.cli import LaunchPlan, build_parser, plan_launch
|
|
|
|
|
|
def plan(argv, *, stdin_tty=True, stdout_tty=True, env=None):
|
|
return plan_launch(argv, stdin_isatty=stdin_tty, stdout_isatty=stdout_tty, env=env or {})
|
|
|
|
|
|
def test_top_level_help_points_to_extension_management():
|
|
assert "deerflow extensions --help" in build_parser().format_help()
|
|
|
|
|
|
def test_bare_command_on_tty_launches_tui():
|
|
p = plan([])
|
|
assert p.mode == "tui"
|
|
assert p.forced_tui is False
|
|
assert p.transparent is False
|
|
|
|
|
|
def test_non_tty_with_no_message_falls_back_to_headless_help():
|
|
p = plan([], stdin_tty=False, stdout_tty=False)
|
|
assert p.mode == "headless-help"
|
|
assert p.reason
|
|
|
|
|
|
def test_print_with_message():
|
|
p = plan(["--print", "summarize this repo"])
|
|
assert p.mode == "print"
|
|
assert p.message == "summarize this repo"
|
|
assert p.read_stdin is False
|
|
|
|
|
|
@pytest.mark.parametrize("mode", ["--print", "--json"])
|
|
def test_headless_recursion_limit(mode):
|
|
p = plan(["--recursion-limit", "250", mode, "hello"])
|
|
assert p.recursion_limit == 250
|
|
|
|
|
|
def test_headless_recursion_limit_is_optional():
|
|
p = plan(["--print", "hello"])
|
|
assert p.recursion_limit is None
|
|
|
|
|
|
@pytest.mark.parametrize("value", ["0", "-1", "not-an-integer"])
|
|
def test_headless_recursion_limit_must_be_positive(value):
|
|
with pytest.raises(SystemExit):
|
|
plan(["--recursion-limit", value, "--print", "hello"])
|
|
|
|
|
|
def test_recursion_limit_is_rejected_for_tui_mode():
|
|
with pytest.raises(SystemExit):
|
|
plan(["--recursion-limit", "250"])
|
|
|
|
|
|
def test_print_without_value_reads_stdin_when_piped():
|
|
p = plan(["--print"], stdin_tty=False)
|
|
assert p.mode == "print"
|
|
assert p.read_stdin is True
|
|
|
|
|
|
def test_json_mode():
|
|
p = plan(["--json", "hello"])
|
|
assert p.mode == "json"
|
|
assert p.message == "hello"
|
|
|
|
|
|
@pytest.mark.parametrize("mode", ["--print", "--json"])
|
|
def test_headless_unquoted_message_keeps_every_word(mode):
|
|
"""An unquoted message must not be truncated to its first word."""
|
|
p = plan([mode, "summarize", "this", "repo"])
|
|
assert p.mode == mode.removeprefix("--")
|
|
assert p.message == "summarize this repo"
|
|
assert p.read_stdin is False
|
|
|
|
|
|
@pytest.mark.parametrize("mode", ["--print", "--json"])
|
|
def test_headless_message_before_the_flag_is_used(mode):
|
|
"""Words before a bare --print/--json are the message, as with --cli."""
|
|
p = plan(["summarize", "this", "repo", mode])
|
|
assert p.mode == mode.removeprefix("--")
|
|
assert p.message == "summarize this repo"
|
|
assert p.read_stdin is False
|
|
|
|
|
|
@pytest.mark.parametrize("mode", ["--print", "--json"])
|
|
def test_headless_message_wins_over_piped_stdin(mode):
|
|
p = plan(["hello", mode], stdin_tty=False)
|
|
assert p.message == "hello"
|
|
assert p.read_stdin is False
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"argv",
|
|
[
|
|
["hello", "--print", "there"],
|
|
["--print", "hello", "--continue", "there"],
|
|
["hello", "--json", "there"],
|
|
],
|
|
)
|
|
def test_headless_message_split_around_the_flag_is_rejected(argv, capsys):
|
|
"""Word order across the flag is lost by argparse, so refuse rather than guess."""
|
|
with pytest.raises(SystemExit) as exc:
|
|
plan(argv)
|
|
assert exc.value.code == 2
|
|
assert "pass the message in one place" in capsys.readouterr().err
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"argv",
|
|
[
|
|
["--print", "hello", "--json", "world"],
|
|
["--print", "--json", "hello"],
|
|
["--json", "hello", "--print"],
|
|
["hello", "--print", "--json"],
|
|
],
|
|
)
|
|
def test_print_and_json_together_are_rejected(argv, capsys):
|
|
"""Only one headless mode runs, so the other flag's message must not vanish."""
|
|
with pytest.raises(SystemExit) as exc:
|
|
plan(argv, stdin_tty=False)
|
|
assert exc.value.code == 2
|
|
assert "not allowed with argument" in capsys.readouterr().err
|
|
|
|
|
|
@pytest.mark.parametrize("mode", ["--print", "--json"])
|
|
def test_headless_blank_message_is_treated_as_missing(mode):
|
|
p = plan([mode, " "])
|
|
assert p.mode == "headless-help"
|
|
|
|
|
|
def test_headless_help_documents_multi_word_messages():
|
|
assert "--print [MESSAGE ...]" in build_parser().format_help()
|
|
|
|
|
|
def test_force_tui_even_without_tty():
|
|
p = plan(["--tui"], stdin_tty=False, stdout_tty=False)
|
|
assert p.mode == "tui"
|
|
assert p.forced_tui is True
|
|
|
|
|
|
def test_env_var_forces_tui():
|
|
p = plan([], stdin_tty=False, stdout_tty=False, env={"DEER_FLOW_TUI": "1"})
|
|
assert p.mode == "tui"
|
|
|
|
|
|
def test_transparent_flag_is_carried_to_tui_plan():
|
|
p = plan(["--tui-transparent"])
|
|
assert p.mode == "tui"
|
|
assert p.transparent is True
|
|
|
|
|
|
def test_transparent_env_is_carried_to_tui_plan():
|
|
p = plan([], env={"DEER_FLOW_TUI_TRANSPARENT": "yes"})
|
|
assert p.mode == "tui"
|
|
assert p.transparent is True
|
|
|
|
|
|
def test_cli_flag_with_message_runs_print():
|
|
p = plan(["--cli", "do", "this", "thing"])
|
|
assert p.mode == "print"
|
|
assert p.message == "do this thing"
|
|
|
|
|
|
def test_cli_flag_without_message_is_headless_help():
|
|
p = plan(["--cli"])
|
|
assert p.mode == "headless-help"
|
|
|
|
|
|
def test_cli_continue_runs_headless_reading_stdin():
|
|
p = plan(["--cli", "--continue"], stdin_tty=False)
|
|
assert p.mode == "print"
|
|
assert p.read_stdin is True
|
|
assert p.continue_recent is True
|
|
|
|
|
|
def test_cli_with_piped_stdin_runs_headless():
|
|
p = plan(["--cli"], stdin_tty=False)
|
|
assert p.mode == "print"
|
|
assert p.read_stdin is True
|
|
|
|
|
|
def test_continue_recent_flag():
|
|
p = plan(["--continue"])
|
|
assert p.mode == "tui"
|
|
assert p.continue_recent is True
|
|
|
|
|
|
def test_resume_specific_thread():
|
|
p = plan(["--resume", "thread-abc"])
|
|
assert p.mode == "tui"
|
|
assert p.thread_id == "thread-abc"
|
|
|
|
|
|
def test_chat_subcommand_is_accepted():
|
|
p = plan(["chat"])
|
|
assert p.mode == "tui"
|
|
|
|
|
|
def test_chat_subcommand_with_print():
|
|
p = plan(["chat", "--print", "hi"])
|
|
assert p.mode == "print"
|
|
assert p.message == "hi"
|
|
|
|
|
|
def test_positional_message_becomes_initial_tui_prompt():
|
|
p = plan(["explain", "the", "codebase"])
|
|
assert p.mode == "tui"
|
|
assert p.message == "explain the codebase"
|
|
|
|
|
|
def test_plan_is_a_launch_plan_instance():
|
|
assert isinstance(plan([]), LaunchPlan)
|