9381a6a9f7
* fix(scripts): launch dev-isolated backend via __backend subcommand The kandev binary became a bundle launcher with the restart supervisor (ADR 0019) and exits without KANDEV_BUNDLE_DIR when invoked bare; the isolated dev instance must run the backend entrypoint directly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(mcp): session spawning, sibling messaging, and renameable tabs - spawn_session_kandev spawns an additional agent session on an existing task (default: the caller's own) via LaunchSession(IntentStart), with optional agent_profile_id (defaults to the spawner's profile) and a session name; the initial prompt carries spawner attribution and reply instructions. - message_task_kandev accepts an optional session_id to target a specific session, enabling same-task sibling messaging. Explicit targets are pinned through dispatch so the idle-path primary-session re-resolution cannot reroute a sibling message back to its sender (regression-tested). - task_sessions gains a name column (migration ordered after the agent_execution_id-drop rebuild), a session.rename WS action, and inline tab rename UI; custom names take precedence over derived agent/model labels across the tab, agents dropdown, mobile list, chat header, and dockview panel titles. - Inter-session messages stamp sender_session_name into metadata so the sender badge shows which session sent the message. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(mcp): address review findings on session spawn/rename/messaging - include name in task-session DTOs (list + full, no omitempty) so persisted tab names survive list hydration and clears propagate - sanitize sender task/session ids embedded in <kandev-system> blocks - reject cross-workspace spawn targets; require sender_task_id - truncate session names by runes, not bytes - distinguish not-found from internal errors in message target lookup - guard TabRenameInput blur-commit after Enter/Escape resolution - cap optimistic rename length client-side and toast on rename failure - add tests: spawn launch-failure and cross-workspace paths, renameSession API client Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(mcp): round-two review fixes for spawn/rename lint and hardening - extract spawn validation/authorization helpers (cyclop/nestif limits) - make stripSystemTag replace until stable to block nested tag evasion - load session before rename so a durable rename can never skip its broadcast; surface lookup failures to the caller instead - drop redundant sessionLauncher nil check (guarded registration) - add maxLength to the rename input matching the server cap - add tests: nested sanitizer evasion, spawn rename-failure tolerance Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Kandev Agent <agent@kandev.dev> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
398 lines
16 KiB
Go
398 lines
16 KiB
Go
package mcp
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/kandev/kandev/internal/common/logger"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func newTestLogger(t *testing.T) *logger.Logger {
|
|
t.Helper()
|
|
log, err := logger.NewLogger(logger.LoggingConfig{Level: "error", Format: "console"})
|
|
require.NoError(t, err)
|
|
return log
|
|
}
|
|
|
|
// getRegisteredToolNames returns the names of all tools registered on the MCP server.
|
|
func getRegisteredToolNames(s *Server) []string {
|
|
toolsMap := s.mcpServer.ListTools()
|
|
names := make([]string, 0, len(toolsMap))
|
|
for name := range toolsMap {
|
|
names = append(names, name)
|
|
}
|
|
return names
|
|
}
|
|
|
|
func TestServerModeTask_RegistersCorrectTools(t *testing.T) {
|
|
log := newTestLogger(t)
|
|
backend := NewChannelBackendClient(log)
|
|
defer backend.Close()
|
|
|
|
s := New(backend, "test-session", "test-task", 10005, log, "", false, ModeTask)
|
|
require.NotNil(t, s)
|
|
|
|
tools := getRegisteredToolNames(s)
|
|
|
|
// Task mode should have kanban tools
|
|
assert.Contains(t, tools, "list_workspaces_kandev")
|
|
assert.Contains(t, tools, "list_workflows_kandev")
|
|
assert.Contains(t, tools, "list_workflow_steps_kandev")
|
|
assert.Contains(t, tools, "list_tasks_kandev")
|
|
assert.Contains(t, tools, "create_task_kandev")
|
|
assert.Contains(t, tools, "update_task_kandev")
|
|
assert.Contains(t, tools, "move_task_kandev")
|
|
assert.Contains(t, tools, "message_task_kandev")
|
|
assert.Contains(t, tools, "get_task_conversation_kandev")
|
|
|
|
// Task mode should have plan tools
|
|
assert.Contains(t, tools, "create_task_plan_kandev")
|
|
assert.Contains(t, tools, "get_task_plan_kandev")
|
|
assert.Contains(t, tools, "update_task_plan_kandev")
|
|
assert.Contains(t, tools, "delete_task_plan_kandev")
|
|
|
|
// Task mode should have interaction tools
|
|
assert.Contains(t, tools, "ask_user_question_kandev")
|
|
|
|
// Task mode should have profile listing tools (needed for create_task)
|
|
assert.Contains(t, tools, "list_agents_kandev")
|
|
assert.Contains(t, tools, "list_executor_profiles_kandev")
|
|
|
|
// Task mode keeps list_related_tasks_kandev (sibling discovery) but
|
|
// drops the task-document tools — those are office-only.
|
|
assert.Contains(t, tools, "list_related_tasks_kandev")
|
|
assert.NotContains(t, tools, "list_task_documents_kandev")
|
|
assert.NotContains(t, tools, "get_task_document_kandev")
|
|
assert.NotContains(t, tools, "write_task_document_kandev")
|
|
|
|
// Task mode exposes delete + archive so agents can clean up the tasks
|
|
// they fan out. Restore/unarchive is intentionally NOT exposed via MCP —
|
|
// it stays a user action in the UI.
|
|
assert.Contains(t, tools, "delete_task_kandev")
|
|
assert.Contains(t, tools, "archive_task_kandev")
|
|
assert.NotContains(t, tools, "restore_task_kandev")
|
|
|
|
// Task mode should NOT have config/mutation tools
|
|
assert.NotContains(t, tools, "create_workflow_kandev")
|
|
assert.NotContains(t, tools, "update_workflow_kandev")
|
|
assert.NotContains(t, tools, "delete_workflow_kandev")
|
|
assert.NotContains(t, tools, "create_workflow_step_kandev")
|
|
assert.NotContains(t, tools, "update_workflow_step_kandev")
|
|
assert.NotContains(t, tools, "update_agent_kandev")
|
|
assert.NotContains(t, tools, "create_agent_profile_kandev")
|
|
assert.NotContains(t, tools, "delete_agent_profile_kandev")
|
|
assert.NotContains(t, tools, "list_agent_profiles_kandev")
|
|
assert.NotContains(t, tools, "update_agent_profile_kandev")
|
|
assert.NotContains(t, tools, "get_mcp_config_kandev")
|
|
assert.NotContains(t, tools, "update_mcp_config_kandev")
|
|
assert.NotContains(t, tools, "list_executors_kandev")
|
|
assert.NotContains(t, tools, "create_executor_profile_kandev")
|
|
assert.NotContains(t, tools, "update_executor_profile_kandev")
|
|
assert.NotContains(t, tools, "delete_executor_profile_kandev")
|
|
assert.NotContains(t, tools, "update_task_state_kandev")
|
|
assert.NotContains(t, tools, "delete_workflow_step_kandev")
|
|
assert.NotContains(t, tools, "reorder_workflow_steps_kandev")
|
|
}
|
|
|
|
func TestServerModeConfig_RegistersCorrectTools(t *testing.T) {
|
|
log := newTestLogger(t)
|
|
backend := NewChannelBackendClient(log)
|
|
defer backend.Close()
|
|
|
|
s := New(backend, "test-session", "test-task", 10005, log, "", false, ModeConfig)
|
|
require.NotNil(t, s)
|
|
|
|
tools := getRegisteredToolNames(s)
|
|
|
|
// Config mode should have workflow config tools
|
|
assert.Contains(t, tools, "list_workspaces_kandev")
|
|
assert.Contains(t, tools, "list_workflows_kandev")
|
|
assert.Contains(t, tools, "list_repositories_kandev")
|
|
assert.Contains(t, tools, "create_workflow_kandev")
|
|
assert.Contains(t, tools, "update_workflow_kandev")
|
|
assert.Contains(t, tools, "delete_workflow_kandev")
|
|
assert.Contains(t, tools, "import_workflow_kandev")
|
|
assert.Contains(t, tools, "list_workflow_steps_kandev")
|
|
assert.Contains(t, tools, "create_workflow_step_kandev")
|
|
assert.Contains(t, tools, "update_workflow_step_kandev")
|
|
assert.Contains(t, tools, "delete_workflow_step_kandev")
|
|
assert.Contains(t, tools, "reorder_workflow_steps_kandev")
|
|
|
|
// Config mode should have agent tools
|
|
assert.Contains(t, tools, "list_agents_kandev")
|
|
assert.Contains(t, tools, "update_agent_kandev")
|
|
assert.Contains(t, tools, "create_agent_profile_kandev")
|
|
assert.Contains(t, tools, "delete_agent_profile_kandev")
|
|
|
|
// Config mode should have MCP config tools
|
|
assert.Contains(t, tools, "list_agent_profiles_kandev")
|
|
assert.Contains(t, tools, "update_agent_profile_kandev")
|
|
assert.Contains(t, tools, "get_mcp_config_kandev")
|
|
assert.Contains(t, tools, "update_mcp_config_kandev")
|
|
|
|
// Config mode should have executor profile tools
|
|
assert.Contains(t, tools, "list_executors_kandev")
|
|
assert.Contains(t, tools, "list_executor_profiles_kandev")
|
|
assert.Contains(t, tools, "create_executor_profile_kandev")
|
|
assert.Contains(t, tools, "update_executor_profile_kandev")
|
|
assert.Contains(t, tools, "delete_executor_profile_kandev")
|
|
|
|
// Config mode should have task tools
|
|
assert.Contains(t, tools, "list_tasks_kandev")
|
|
assert.Contains(t, tools, "move_task_kandev")
|
|
assert.Contains(t, tools, "delete_task_kandev")
|
|
assert.Contains(t, tools, "archive_task_kandev")
|
|
assert.Contains(t, tools, "update_task_state_kandev")
|
|
assert.Contains(t, tools, "get_task_conversation_kandev")
|
|
|
|
// Config mode should have interaction tools
|
|
assert.Contains(t, tools, "ask_user_question_kandev")
|
|
|
|
// Config mode should NOT have plan tools
|
|
assert.NotContains(t, tools, "create_task_plan_kandev")
|
|
assert.NotContains(t, tools, "get_task_plan_kandev")
|
|
assert.NotContains(t, tools, "update_task_plan_kandev")
|
|
assert.NotContains(t, tools, "delete_task_plan_kandev")
|
|
|
|
// Config mode should NOT have task-mode kanban create/update tools
|
|
assert.NotContains(t, tools, "create_task_kandev")
|
|
assert.NotContains(t, tools, "update_task_kandev")
|
|
}
|
|
|
|
func TestServerModeDefault_DefaultsToTask(t *testing.T) {
|
|
log := newTestLogger(t)
|
|
backend := NewChannelBackendClient(log)
|
|
defer backend.Close()
|
|
|
|
s := New(backend, "test-session", "test-task", 10005, log, "", false, "")
|
|
require.NotNil(t, s)
|
|
assert.Equal(t, ModeTask, s.mode)
|
|
|
|
tools := getRegisteredToolNames(s)
|
|
assert.Contains(t, tools, "create_task_kandev")
|
|
assert.Contains(t, tools, "create_task_plan_kandev")
|
|
assert.NotContains(t, tools, "create_workflow_step_kandev")
|
|
}
|
|
|
|
func TestServerModeConfig_DisableAskQuestion(t *testing.T) {
|
|
log := newTestLogger(t)
|
|
backend := NewChannelBackendClient(log)
|
|
defer backend.Close()
|
|
|
|
s := New(backend, "test-session", "test-task", 10005, log, "", true, ModeConfig)
|
|
require.NotNil(t, s)
|
|
|
|
tools := getRegisteredToolNames(s)
|
|
assert.NotContains(t, tools, "ask_user_question_kandev")
|
|
assert.Contains(t, tools, "list_agents_kandev")
|
|
assert.Contains(t, tools, "create_workflow_step_kandev")
|
|
}
|
|
|
|
func TestServerModeTask_DisableAskQuestion(t *testing.T) {
|
|
log := newTestLogger(t)
|
|
backend := NewChannelBackendClient(log)
|
|
defer backend.Close()
|
|
|
|
s := New(backend, "test-session", "test-task", 10005, log, "", true, ModeTask)
|
|
require.NotNil(t, s)
|
|
|
|
tools := getRegisteredToolNames(s)
|
|
assert.NotContains(t, tools, "ask_user_question_kandev")
|
|
assert.Contains(t, tools, "create_task_kandev")
|
|
assert.Contains(t, tools, "create_task_plan_kandev")
|
|
}
|
|
|
|
func TestServerModeTask_ToolCount(t *testing.T) {
|
|
log := newTestLogger(t)
|
|
backend := NewChannelBackendClient(log)
|
|
defer backend.Close()
|
|
|
|
s := New(backend, "test-session", "test-task", 10005, log, "", false, ModeTask)
|
|
tools := getRegisteredToolNames(s)
|
|
// 14 kanban (incl. delete + archive task + spawn_session) +
|
|
// 1 add_branch_to_task + 1 update_repository_base_branch +
|
|
// 1 step_complete (ADR 0015) + 1 interaction + 4 plan + 3 walkthrough +
|
|
// 1 related-tasks = 26.
|
|
// Task-document tools (list/get/write) are office-only.
|
|
assert.Contains(t, tools, "step_complete_kandev", "ADR 0015 explicit-completion signal must be registered in task mode")
|
|
assert.Contains(t, tools, "show_walkthrough_kandev", "walkthrough tool must be registered in task mode")
|
|
assert.Contains(t, tools, "spawn_session_kandev", "spawn_session must be registered in task mode")
|
|
assert.Equal(t, 26, len(tools))
|
|
}
|
|
|
|
func TestServerModeConfig_ToolCount(t *testing.T) {
|
|
log := newTestLogger(t)
|
|
backend := NewChannelBackendClient(log)
|
|
defer backend.Close()
|
|
|
|
s := New(backend, "test-session", "test-task", 10005, log, "", false, ModeConfig)
|
|
tools := getRegisteredToolNames(s)
|
|
// 12 workflow (incl. list_repositories + import_workflow) + 4 agent + 4 mcp + 5 executor + 6 task + 1 interaction = 32
|
|
assert.NotContains(t, tools, "step_complete_kandev", "step_complete_kandev requires a live task session; must NOT register in config mode")
|
|
assert.Equal(t, 32, len(tools))
|
|
}
|
|
|
|
func TestServerModeConfig_ToolDescriptions(t *testing.T) {
|
|
log := newTestLogger(t)
|
|
backend := NewChannelBackendClient(log)
|
|
defer backend.Close()
|
|
|
|
s := New(backend, "test-session", "test-task", 10005, log, "", false, ModeConfig)
|
|
|
|
toolsMap := s.mcpServer.ListTools()
|
|
|
|
assert.Contains(t, toolsMap["create_workflow_step_kandev"].Tool.Description, "Create a new workflow step")
|
|
assert.Contains(t, toolsMap["list_agents_kandev"].Tool.Description, "List all configured agents")
|
|
assert.Contains(t, toolsMap["get_mcp_config_kandev"].Tool.Description, "Get MCP server configuration")
|
|
}
|
|
|
|
func TestServerModeOffice_RegistersCorrectTools(t *testing.T) {
|
|
log := newTestLogger(t)
|
|
backend := NewChannelBackendClient(log)
|
|
defer backend.Close()
|
|
|
|
s := New(backend, "test-session", "test-task", 10005, log, "", false, ModeOffice)
|
|
require.NotNil(t, s)
|
|
|
|
tools := getRegisteredToolNames(s)
|
|
|
|
// Office mode should have plan tools
|
|
assert.Contains(t, tools, "create_task_plan_kandev")
|
|
assert.Contains(t, tools, "get_task_plan_kandev")
|
|
assert.Contains(t, tools, "update_task_plan_kandev")
|
|
assert.Contains(t, tools, "delete_task_plan_kandev")
|
|
|
|
// Office mode should have interaction tools
|
|
assert.Contains(t, tools, "ask_user_question_kandev")
|
|
|
|
// delegate_task_kandev was removed from ModeOffice when the
|
|
// agentctl CLI started covering task creation/delegation via
|
|
// `agentctl kandev task create --parent $KANDEV_TASK_ID …`.
|
|
assert.NotContains(t, tools, "delegate_task_kandev")
|
|
|
|
// Office mode exposes the cross-task handoff tools.
|
|
assert.Contains(t, tools, "list_related_tasks_kandev")
|
|
assert.Contains(t, tools, "list_task_documents_kandev")
|
|
assert.Contains(t, tools, "get_task_document_kandev")
|
|
assert.Contains(t, tools, "write_task_document_kandev")
|
|
|
|
// Office mode should NOT have kanban tools
|
|
assert.NotContains(t, tools, "create_task_kandev")
|
|
assert.NotContains(t, tools, "list_tasks_kandev")
|
|
assert.NotContains(t, tools, "update_task_kandev")
|
|
assert.NotContains(t, tools, "list_workspaces_kandev")
|
|
assert.NotContains(t, tools, "list_workflows_kandev")
|
|
assert.NotContains(t, tools, "list_workflow_steps_kandev")
|
|
assert.NotContains(t, tools, "list_agents_kandev")
|
|
assert.NotContains(t, tools, "list_executor_profiles_kandev")
|
|
|
|
// Office mode should NOT have config tools
|
|
assert.NotContains(t, tools, "create_workflow_kandev")
|
|
assert.NotContains(t, tools, "update_workflow_kandev")
|
|
assert.NotContains(t, tools, "update_agent_kandev")
|
|
}
|
|
|
|
func TestServerModeOffice_ToolCount(t *testing.T) {
|
|
log := newTestLogger(t)
|
|
backend := NewChannelBackendClient(log)
|
|
defer backend.Close()
|
|
|
|
s := New(backend, "test-session", "test-task", 10005, log, "", false, ModeOffice)
|
|
tools := getRegisteredToolNames(s)
|
|
// 4 plan + 1 interaction + 1 related-tasks + 3 task-documents = 9
|
|
// (delegate_task_kandev retired in favour of `agentctl kandev task create …`).
|
|
assert.NotContains(t, tools, "step_complete_kandev", "step_complete_kandev is kanban-task-only; office mode advances tasks via its own approval surface")
|
|
assert.Equal(t, 9, len(tools))
|
|
}
|
|
|
|
func TestServerModeOffice_DisableAskQuestion(t *testing.T) {
|
|
log := newTestLogger(t)
|
|
backend := NewChannelBackendClient(log)
|
|
defer backend.Close()
|
|
|
|
s := New(backend, "test-session", "test-task", 10005, log, "", true, ModeOffice)
|
|
require.NotNil(t, s)
|
|
|
|
tools := getRegisteredToolNames(s)
|
|
assert.NotContains(t, tools, "ask_user_question_kandev")
|
|
assert.Contains(t, tools, "create_task_plan_kandev")
|
|
// delegate_task_kandev was retired from ModeOffice (now lives in
|
|
// the agentctl CLI as `agentctl kandev task create --parent …`).
|
|
assert.NotContains(t, tools, "delegate_task_kandev")
|
|
// 4 plan + 1 related-tasks + 3 task-documents = 8 (no ask_user_question, no delegate)
|
|
assert.Equal(t, 8, len(tools))
|
|
}
|
|
|
|
func TestServerModeConstants(t *testing.T) {
|
|
assert.Equal(t, "task", ModeTask)
|
|
assert.Equal(t, "config", ModeConfig)
|
|
assert.Equal(t, "external", ModeExternal)
|
|
assert.Equal(t, "office", ModeOffice)
|
|
}
|
|
|
|
func TestServerModeExternal_RegistersCorrectTools(t *testing.T) {
|
|
log := newTestLogger(t)
|
|
backend := NewChannelBackendClient(log)
|
|
defer backend.Close()
|
|
|
|
s := New(backend, "", "", 0, log, "", true, ModeExternal)
|
|
require.NotNil(t, s)
|
|
|
|
tools := getRegisteredToolNames(s)
|
|
|
|
// External mode includes all config tools
|
|
assert.Contains(t, tools, "list_workspaces_kandev")
|
|
assert.Contains(t, tools, "list_repositories_kandev")
|
|
assert.Contains(t, tools, "create_workflow_kandev")
|
|
assert.Contains(t, tools, "list_agents_kandev")
|
|
assert.Contains(t, tools, "get_mcp_config_kandev")
|
|
assert.Contains(t, tools, "list_executors_kandev")
|
|
assert.Contains(t, tools, "move_task_kandev")
|
|
|
|
// External mode includes create_task_kandev so external agents can spawn tasks
|
|
assert.Contains(t, tools, "create_task_kandev")
|
|
|
|
// External mode does NOT include session-scoped tools
|
|
assert.NotContains(t, tools, "ask_user_question_kandev")
|
|
assert.NotContains(t, tools, "create_task_plan_kandev")
|
|
assert.NotContains(t, tools, "get_task_plan_kandev")
|
|
assert.NotContains(t, tools, "update_task_plan_kandev")
|
|
assert.NotContains(t, tools, "delete_task_plan_kandev")
|
|
|
|
// External mode does NOT include kanban update_task_kandev (config has its own update_task_state)
|
|
assert.NotContains(t, tools, "update_task_kandev")
|
|
|
|
// External mode does NOT include message_task_kandev (no live session context)
|
|
assert.NotContains(t, tools, "message_task_kandev")
|
|
}
|
|
|
|
func TestServerModeExternal_ToolCount(t *testing.T) {
|
|
log := newTestLogger(t)
|
|
backend := NewChannelBackendClient(log)
|
|
defer backend.Close()
|
|
|
|
s := New(backend, "", "", 0, log, "", true, ModeExternal)
|
|
tools := getRegisteredToolNames(s)
|
|
// 12 workflow (incl. list_repositories + import_workflow) + 4 agent + 4 mcp + 5 executor + 6 task + 1 create_task = 32.
|
|
// add_branch_to_task_kandev is task-mode only — external coding agents have no live session to attach a worktree to.
|
|
assert.Equal(t, 32, len(tools))
|
|
assert.NotContains(t, tools, "add_branch_to_task_kandev")
|
|
}
|
|
|
|
func TestNewExternal_Constructs(t *testing.T) {
|
|
log := newTestLogger(t)
|
|
backend := NewChannelBackendClient(log)
|
|
defer backend.Close()
|
|
|
|
s := NewExternal(backend, log, "")
|
|
require.NotNil(t, s)
|
|
assert.Equal(t, ModeExternal, s.mode)
|
|
assert.True(t, s.disableAskQuestion)
|
|
assert.Empty(t, s.sessionID)
|
|
assert.Empty(t, s.taskID)
|
|
assert.NotNil(t, s.sseServer)
|
|
assert.NotNil(t, s.httpServer)
|
|
assert.Equal(t, "/mcp/message?sessionId=session-1", s.sseServer.GetMessageEndpointForClient(nil, "session-1"))
|
|
}
|