claire/tests/test_chat_commands.py

233 lines
7.3 KiB
Python
Raw Permalink Normal View History

"""Slash-command parser + dispatcher tests."""
from __future__ import annotations
import uuid as _uuid
import pytest
from pydantic import ValidationError
from claire import events as ev
from claire.domain import ChatScope, TaskStatus
from claire.web.chat import commands as cmd
ORCH = cmd.ScopeCtx(scope=ChatScope.ORCHESTRATOR, scope_ref=None)
ALPHA = cmd.ScopeCtx(scope=ChatScope.PROJECT, scope_ref="alpha")
# ---------------------------------------------------------------------------
# is_slash / parse error paths
# ---------------------------------------------------------------------------
def test_is_slash_leading_whitespace() -> None:
assert cmd.is_slash(" /help")
assert not cmd.is_slash("hello")
def test_parse_empty_command_rejected() -> None:
with pytest.raises(cmd.ParseError):
cmd.parse("/", ORCH)
def test_parse_unknown_command_rejected() -> None:
with pytest.raises(cmd.ParseError):
cmd.parse("/nope", ORCH)
def test_parse_unterminated_quote_rejected() -> None:
with pytest.raises(cmd.ParseError):
cmd.parse('/project new "alpha', ORCH)
# ---------------------------------------------------------------------------
# /help, /pull, /status
# ---------------------------------------------------------------------------
def test_parse_help() -> None:
assert isinstance(cmd.parse("/help", ORCH), cmd.HelpCmd)
def test_parse_pull() -> None:
assert isinstance(cmd.parse("/pull", ORCH), cmd.PullCmd)
def test_parse_status() -> None:
assert isinstance(cmd.parse("/status", ORCH), cmd.StatusCmd)
# ---------------------------------------------------------------------------
# /project new
# ---------------------------------------------------------------------------
def test_parse_project_new_with_goal() -> None:
c = cmd.parse('/project new alpha --goal "build it"', ORCH)
assert isinstance(c, cmd.ProjectNewCmd)
assert c.name == "alpha"
assert c.goal == "build it"
def test_parse_project_new_equals_form() -> None:
c = cmd.parse('/project new alpha --goal=focused', ORCH)
assert isinstance(c, cmd.ProjectNewCmd)
assert c.goal == "focused"
def test_parse_project_new_requires_name() -> None:
with pytest.raises(cmd.ParseError):
cmd.parse("/project new", ORCH)
# ---------------------------------------------------------------------------
# /task new — scope-aware defaults
# ---------------------------------------------------------------------------
def test_parse_task_new_defaults_project_from_scope() -> None:
c = cmd.parse('/task new "first task" --priority 1', ALPHA)
assert isinstance(c, cmd.TaskNewCmd)
assert c.project == "alpha"
assert c.title == "first task"
assert c.priority == 1
def test_parse_task_new_requires_project_outside_project_chat() -> None:
with pytest.raises(cmd.ParseError):
cmd.parse('/task new "x"', ORCH)
def test_parse_task_new_explicit_project_wins() -> None:
c = cmd.parse('/task new "x" --project beta', ALPHA)
assert isinstance(c, cmd.TaskNewCmd)
assert c.project == "beta"
def test_parse_task_new_priority_out_of_range_rejected() -> None:
with pytest.raises(ValidationError):
cmd.parse('/task new "x" --priority 99', ALPHA)
def test_parse_task_new_priority_non_integer_rejected() -> None:
with pytest.raises(cmd.ParseError):
cmd.parse('/task new "x" --priority foo', ALPHA)
# ---------------------------------------------------------------------------
# /task list
# ---------------------------------------------------------------------------
def test_parse_task_list_status_filter() -> None:
c = cmd.parse("/task list --status todo", ALPHA)
assert isinstance(c, cmd.TaskListCmd)
assert c.status == TaskStatus.TODO
assert c.project == "alpha"
def test_parse_task_list_unknown_status_rejected() -> None:
with pytest.raises(cmd.ParseError):
cmd.parse("/task list --status garbage", ALPHA)
# ---------------------------------------------------------------------------
# /assign
# ---------------------------------------------------------------------------
def test_parse_assign_two_positionals() -> None:
c = cmd.parse("/assign abcd1234 efgh5678", ORCH)
assert isinstance(c, cmd.AssignCmd)
assert c.task_ref == "abcd1234"
assert c.session_ref == "efgh5678"
def test_parse_assign_requires_two_args() -> None:
with pytest.raises(cmd.ParseError):
cmd.parse("/assign abcd1234", ORCH)
# ---------------------------------------------------------------------------
# /broadcast — scope sensitivity
# ---------------------------------------------------------------------------
def test_parse_broadcast_uses_project_scope_as_target() -> None:
c = cmd.parse("/broadcast start now", ALPHA)
assert isinstance(c, cmd.BroadcastCmd)
assert c.target == "alpha"
assert c.text == "start now"
def test_parse_broadcast_orchestrator_rejected() -> None:
with pytest.raises(cmd.ParseError):
cmd.parse("/broadcast hi", ORCH)
def test_parse_broadcast_session_scope_uses_short_prefix() -> None:
sid = "abcd1234-5678-90ab-cdef-1234567890ab"
ctx = cmd.ScopeCtx(scope=ChatScope.SESSION, scope_ref=sid)
c = cmd.parse("/broadcast hello there", ctx)
assert isinstance(c, cmd.BroadcastCmd)
assert c.target == "@abcd1234"
# ---------------------------------------------------------------------------
# Dispatcher — runs against an in-memory DB
# ---------------------------------------------------------------------------
def test_dispatch_help(conn, gen) -> None:
result = cmd.dispatch(conn, gen, cmd.HelpCmd(), ORCH)
assert "Slash commands" in result.body
def test_dispatch_project_new_then_task_new(conn, gen) -> None:
cmd.dispatch(conn, gen, cmd.ProjectNewCmd(name="alpha"), ORCH)
result = cmd.dispatch(
conn, gen,
cmd.TaskNewCmd(project="alpha", title="first", priority=1),
ALPHA,
)
assert "first" in result.body
assert result.meta["kind"] == "task"
def test_dispatch_task_list_returns_cards(conn, gen) -> None:
cmd.dispatch(conn, gen, cmd.ProjectNewCmd(name="alpha"), ORCH)
cmd.dispatch(conn, gen, cmd.TaskNewCmd(project="alpha", title="t1"), ALPHA)
cmd.dispatch(conn, gen, cmd.TaskNewCmd(project="alpha", title="t2"), ALPHA)
result = cmd.dispatch(
conn, gen, cmd.TaskListCmd(project="alpha", status=None), ALPHA,
)
assert len(result.meta["tasks"]) == 2
titles = {t["title"] for t in result.meta["tasks"]}
assert titles == {"t1", "t2"}
def test_dispatch_assign_resolves_prefixes(conn, gen) -> None:
pid = _uuid.uuid4()
tid = _uuid.uuid4()
sid = _uuid.uuid4()
ev.append(conn, gen, ev.ProjectCreated(project_id=pid, name="alpha"))
ev.append(conn, gen, ev.TaskAdded(task_id=tid, project_id=pid, title="t"))
ev.append(conn, gen, ev.SessionObserved(session_uuid=sid, host="local"))
result = cmd.dispatch(
conn, gen,
cmd.AssignCmd(task_ref=str(tid)[:8], session_ref=str(sid)[:8]),
ALPHA,
)
assert "Assigned" in result.body
assert result.meta["task_id"] == str(tid)
def test_dispatch_status_summary(conn, gen) -> None:
cmd.dispatch(conn, gen, cmd.ProjectNewCmd(name="alpha"), ORCH)
cmd.dispatch(conn, gen, cmd.TaskNewCmd(project="alpha", title="t"), ALPHA)
result = cmd.dispatch(conn, gen, cmd.StatusCmd(), ORCH)
assert result.meta["projects"] == 1
assert result.meta["open_tasks"] == 1