mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +00:00
* fix(tui): accept whitespace separators in slash commands * fix(tui): align slash palette with Unicode whitespace Close the slash palette on every whitespace separator accepted by the command resolver, including pasted tabs and Unicode spaces. Document the accepted delimiters and cover parser arguments, palette closure, Enter submission and Tab completion. Validation: 352 focused tests, 34,089 offline backend tests and 262 blocking-I/O tests passed. Backend formatting, frontend check and agent guidance passed. --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
198 lines
6.3 KiB
Python
198 lines
6.3 KiB
Python
"""Tests for the slash-command registry (pure)."""
|
|
|
|
import pytest
|
|
|
|
from deerflow.skills.slash import RESERVED_SLASH_SKILL_NAMES
|
|
from deerflow.tui.command_registry import (
|
|
BUILTIN_COMMANDS,
|
|
build_registry,
|
|
filter_commands,
|
|
format_command_help,
|
|
resolve,
|
|
)
|
|
|
|
|
|
def _skills():
|
|
return [
|
|
{"name": "brainstorming", "description": "Explore ideas", "enabled": True},
|
|
{"name": "tdd", "description": "Test driven dev", "enabled": True},
|
|
{"name": "disabled-one", "description": "off", "enabled": False},
|
|
]
|
|
|
|
|
|
def test_build_registry_includes_all_builtins():
|
|
registry = build_registry([])
|
|
names = {c.name for c in registry}
|
|
for builtin in BUILTIN_COMMANDS:
|
|
assert builtin.name in names
|
|
|
|
|
|
def test_build_registry_adds_enabled_skill_commands_only():
|
|
registry = build_registry(_skills())
|
|
skill_names = {c.name for c in registry if c.category == "skill"}
|
|
assert "brainstorming" in skill_names
|
|
assert "tdd" in skill_names
|
|
assert "disabled-one" not in skill_names
|
|
|
|
|
|
def test_filter_empty_query_returns_all():
|
|
registry = build_registry([])
|
|
assert filter_commands(registry, "") == registry
|
|
|
|
|
|
def test_filter_matches_name_substring_case_insensitive():
|
|
registry = build_registry([])
|
|
results = filter_commands(registry, "MOD")
|
|
assert any(c.name == "model" for c in results)
|
|
|
|
|
|
def test_filter_matches_description():
|
|
registry = build_registry(_skills())
|
|
results = filter_commands(registry, "explore")
|
|
assert any(c.name == "brainstorming" for c in results)
|
|
|
|
|
|
def test_filter_ranks_prefix_matches_before_substring():
|
|
registry = build_registry([])
|
|
results = filter_commands(registry, "me")
|
|
# "memory" (prefix) should rank above a command that only contains "me"
|
|
names = [c.name for c in results]
|
|
assert "memory" in names
|
|
assert names.index("memory") == 0
|
|
|
|
|
|
def test_resolve_plain_text_is_message():
|
|
res = resolve("hello there")
|
|
assert res.kind == "message"
|
|
assert res.text == "hello there"
|
|
|
|
|
|
def test_resolve_builtin_command():
|
|
res = resolve("/model")
|
|
assert res.kind == "builtin"
|
|
assert res.name == "model"
|
|
|
|
|
|
def test_resolve_builtin_with_args():
|
|
res = resolve("/resume thread-123")
|
|
assert res.kind == "builtin"
|
|
assert res.name == "resume"
|
|
assert res.args == "thread-123"
|
|
|
|
|
|
@pytest.mark.parametrize("separator", [" ", "\t", "\n", "\r\n", "\v", "\f", "\u0085", "\u00a0", "\u2003", "\u2028", "\u202f", "\u3000"])
|
|
@pytest.mark.parametrize(("name", "kind"), [("resume", "builtin"), ("goal", "builtin"), ("tdd", "skill")])
|
|
def test_resolve_whitespace_separated_command_preserves_multiline_args(separator, name, kind):
|
|
args = "First line\n indented detail\nLast line"
|
|
|
|
res = resolve(f"/{name}{separator}{args}", skills=["tdd", "goal"])
|
|
|
|
assert res.kind == kind
|
|
assert res.name == name
|
|
assert res.args == args
|
|
|
|
|
|
@pytest.mark.parametrize("text", ["/ tdd task", "/\ttdd task", "/\ntdd task", "/context\ncompact", "/missing\ntask"])
|
|
def test_whitespace_does_not_admit_invalid_or_reserved_skill_commands(text):
|
|
assert resolve(text, skills=["tdd", "context"]).kind == "unknown"
|
|
|
|
|
|
def test_resolve_skill_activation():
|
|
res = resolve("/tdd write the test first", skills=["tdd"])
|
|
assert res.kind == "skill"
|
|
assert res.name == "tdd"
|
|
assert res.args == "write the test first"
|
|
|
|
|
|
def test_resolve_unknown_command():
|
|
res = resolve("/definitely-not-a-command", skills=["tdd"])
|
|
assert res.kind == "unknown"
|
|
assert res.name == "definitely-not-a-command"
|
|
|
|
|
|
def test_resolve_bare_slash_is_unknown_empty():
|
|
res = resolve("/")
|
|
assert res.kind == "unknown"
|
|
|
|
|
|
def test_goal_is_builtin_command():
|
|
resolved = resolve("/goal finish the implementation")
|
|
|
|
assert resolved.kind == "builtin"
|
|
assert resolved.name == "goal"
|
|
assert resolved.args == "finish the implementation"
|
|
|
|
|
|
def test_clear_is_builtin_command():
|
|
resolved = resolve("/clear")
|
|
|
|
assert resolved.kind == "builtin"
|
|
assert resolved.name == "clear"
|
|
assert resolved.args == ""
|
|
|
|
|
|
def test_goal_builtin_takes_precedence_over_skill():
|
|
registry = build_registry([{"name": "goal", "description": "skill", "enabled": True}])
|
|
|
|
assert [command.name for command in registry].count("goal") == 1
|
|
assert resolve("/goal finish", skills=["goal"]).kind == "builtin"
|
|
|
|
|
|
def test_build_registry_never_exposes_reserved_commands_as_skills():
|
|
registry = build_registry([{"name": name, "description": "reserved", "enabled": True} for name in RESERVED_SLASH_SKILL_NAMES])
|
|
|
|
skill_names = {command.name for command in registry if command.category == "skill"}
|
|
assert skill_names == {"context"}
|
|
|
|
|
|
def test_resolve_never_classifies_reserved_commands_as_skills():
|
|
reserved_names = sorted(RESERVED_SLASH_SKILL_NAMES)
|
|
|
|
for name in reserved_names:
|
|
resolved = resolve(f"/{name} task", skills=reserved_names)
|
|
if name == "context":
|
|
assert resolved.kind == "skill"
|
|
else:
|
|
assert resolved.kind != "skill", name
|
|
|
|
|
|
def test_context_compact_alias_is_not_a_tui_skill():
|
|
resolved = resolve("/context compact", skills=["context"])
|
|
|
|
assert resolved.kind == "unknown"
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# /help text <-> registry parity
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
def test_help_lists_every_builtin_command():
|
|
help_line = format_command_help()
|
|
# Every registered built-in must be advertised in /help; this is the guard
|
|
# against the help text silently drifting from BUILTIN_COMMANDS.
|
|
for command in BUILTIN_COMMANDS:
|
|
assert f"/{command.name}" in help_line
|
|
|
|
|
|
def test_help_lists_only_builtin_commands():
|
|
help_line = format_command_help()
|
|
slugs = [token[1:] for token in help_line.split() if token.startswith("/")]
|
|
assert set(slugs) == {command.name for command in BUILTIN_COMMANDS}
|
|
|
|
|
|
def test_help_preserves_registry_order():
|
|
help_line = format_command_help()
|
|
slugs = [token[1:] for token in help_line.split() if token.startswith("/")]
|
|
assert slugs == [command.name for command in BUILTIN_COMMANDS]
|
|
|
|
|
|
def test_help_has_no_duplicate_commands():
|
|
help_line = format_command_help()
|
|
slugs = [token[1:] for token in help_line.split() if token.startswith("/")]
|
|
assert len(slugs) == len(set(slugs))
|
|
|
|
|
|
def test_help_starts_with_commands_label():
|
|
assert format_command_help().startswith("Commands: /")
|