Add task sidebar workflow
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hjbdev/patterm/internal/mcp"
|
||||
"github.com/hjbdev/patterm/internal/preset"
|
||||
"github.com/hjbdev/patterm/internal/scratchpad"
|
||||
taskstore "github.com/hjbdev/patterm/internal/task"
|
||||
)
|
||||
|
||||
// mkChild builds a Child without starting a PTY. Use sparingly — the
|
||||
@@ -100,8 +103,8 @@ func TestClassifySendMessageNilCallerRejectsNonTopLevelTarget(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrapSubAgentPromptPrependsSystemBlockWhenParented(t *testing.T) {
|
||||
out := wrapSubAgentPrompt("ship feature X", true)
|
||||
func TestBuildAgentPromptPrependsSystemBlockWhenParented(t *testing.T) {
|
||||
out := buildAgentPrompt("ship feature X", true, nil)
|
||||
if !strings.HasPrefix(out, "[system:") {
|
||||
t.Fatalf("expected prepended [system: …] block, got %q", out)
|
||||
}
|
||||
@@ -119,18 +122,18 @@ func TestWrapSubAgentPromptPrependsSystemBlockWhenParented(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrapSubAgentPromptPassthroughWhenNoParent(t *testing.T) {
|
||||
out := wrapSubAgentPrompt("hello", false)
|
||||
func TestBuildAgentPromptPassthroughWhenNoParent(t *testing.T) {
|
||||
out := buildAgentPrompt("hello", false, nil)
|
||||
if out != "hello" {
|
||||
t.Fatalf("expected passthrough for top-level spawn, got %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrapSubAgentPromptEmptyStaysEmpty(t *testing.T) {
|
||||
func TestBuildAgentPromptEmptyStaysEmpty(t *testing.T) {
|
||||
// Empty instructions mean "no inject" upstream; we must not synthesize
|
||||
// content here or LaunchAgent would type the system block into an
|
||||
// otherwise-idle agent.
|
||||
if out := wrapSubAgentPrompt("", true); out != "" {
|
||||
if out := buildAgentPrompt("", true, nil); out != "" {
|
||||
t.Fatalf("empty instructions should stay empty, got %q", out)
|
||||
}
|
||||
}
|
||||
@@ -215,7 +218,7 @@ func TestAvailableToolsAdvertisesAllTimerTools(t *testing.T) {
|
||||
"timer_cancel", "timer_pause", "timer_resume", "timer_list",
|
||||
}
|
||||
for _, role := range []mcp.CallerRole{mcp.RoleOrchestrator, mcp.RoleSubAgent} {
|
||||
tools := availableToolsForRole(role)
|
||||
tools := availableToolsForRole(role, false)
|
||||
for _, w := range want {
|
||||
if !containsString(tools, w) {
|
||||
t.Fatalf("role %q missing %q in available tools: %v", role, w, tools)
|
||||
@@ -224,6 +227,95 @@ func TestAvailableToolsAdvertisesAllTimerTools(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhoAmITaskContextIsConditional(t *testing.T) {
|
||||
t.Setenv("XDG_DATA_HOME", t.TempDir())
|
||||
tasks, err := taskstore.Open("projkey")
|
||||
if err != nil {
|
||||
t.Fatalf("task open: %v", err)
|
||||
}
|
||||
task, err := tasks.Create("Fix sidebar")
|
||||
if err != nil {
|
||||
t.Fatalf("task create: %v", err)
|
||||
}
|
||||
sess := NewSession(t.TempDir(), "projkey")
|
||||
unbound := newChildEntry("p_unbound", "agent", KindAgent, []string{"sh"}, nil, "", "", "", "")
|
||||
bound := newChildEntry("p_bound", "agent", KindAgent, []string{"sh"}, nil, "", task.ID, "", "")
|
||||
addTestChild(sess, unbound)
|
||||
addTestChild(sess, bound)
|
||||
h := newToolHost(sess, nil, tasks, nil, preset.Set{}, nil, 80, 24)
|
||||
|
||||
if got := h.WhoAmI(unbound.ID, true); got.Task != nil || containsString(got.AvailableTools, "task_register_worktree") {
|
||||
t.Fatalf("unbound whoami leaked task context/tools: %+v", got)
|
||||
}
|
||||
got := h.WhoAmI(bound.ID, true)
|
||||
if got.Task == nil || got.Task.ID != task.ID || got.Task.Title != task.Title {
|
||||
t.Fatalf("bound whoami missing task: %+v", got)
|
||||
}
|
||||
if !containsString(got.AvailableTools, "task_register_worktree") {
|
||||
t.Fatalf("bound whoami missing task_register_worktree: %+v", got.AvailableTools)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterTaskWorktreeResolvesAgainstCallerWorkDir(t *testing.T) {
|
||||
t.Setenv("XDG_DATA_HOME", t.TempDir())
|
||||
tasks, err := taskstore.Open("projkey")
|
||||
if err != nil {
|
||||
t.Fatalf("task open: %v", err)
|
||||
}
|
||||
task, err := tasks.Create("Fix sidebar")
|
||||
if err != nil {
|
||||
t.Fatalf("task create: %v", err)
|
||||
}
|
||||
workDir := t.TempDir()
|
||||
sess := NewSession(t.TempDir(), "projkey")
|
||||
caller := newChildEntry("p_bound", "agent", KindAgent, []string{"sh"}, nil, "", task.ID, workDir, "")
|
||||
addTestChild(sess, caller)
|
||||
h := newToolHost(sess, nil, tasks, nil, preset.Set{}, nil, 80, 24)
|
||||
|
||||
info, err := h.RegisterTaskWorktree(caller.ID, mcp.TaskRegisterWorktreeArgs{Path: "../worktree", Branch: "task-branch"})
|
||||
if err != nil {
|
||||
t.Fatalf("register: %v", err)
|
||||
}
|
||||
wantPath := filepath.Clean(filepath.Join(workDir, "../worktree"))
|
||||
if len(info.Worktrees) != 1 || info.Worktrees[0].Path != wantPath || info.Worktrees[0].Branch != "task-branch" || info.Worktrees[0].CreatedByProcessID != caller.ID {
|
||||
t.Fatalf("registered worktree = %+v, want path %q branch/task", info.Worktrees, wantPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpawnProcessInheritsCallerTask(t *testing.T) {
|
||||
t.Setenv("XDG_DATA_HOME", t.TempDir())
|
||||
tasks, err := taskstore.Open("projkey")
|
||||
if err != nil {
|
||||
t.Fatalf("task open: %v", err)
|
||||
}
|
||||
task, err := tasks.Create("Fix sidebar")
|
||||
if err != nil {
|
||||
t.Fatalf("task create: %v", err)
|
||||
}
|
||||
sess := NewSession(t.TempDir(), "projkey")
|
||||
defer sess.Shutdown()
|
||||
caller := newChildEntry("p_bound", "agent", KindAgent, []string{"sh"}, nil, "", task.ID, "", "")
|
||||
addTestChild(sess, caller)
|
||||
launcher := NewLauncher(sess, "", 80, 24)
|
||||
h := newToolHost(sess, nil, tasks, launcher, preset.Set{}, nil, 80, 24)
|
||||
|
||||
info, err := h.SpawnProcess(caller.ID, mcp.SpawnProcessArgs{Argv: []string{"sh", "-lc", "exit 0"}})
|
||||
if err != nil {
|
||||
t.Fatalf("spawn process: %v", err)
|
||||
}
|
||||
spawned := sess.FindChild(info.ID)
|
||||
if spawned == nil || spawned.TaskID != task.ID {
|
||||
t.Fatalf("spawned child task = %+v, want %q", spawned, task.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func addTestChild(sess *Session, c *Child) {
|
||||
sess.mu.Lock()
|
||||
defer sess.mu.Unlock()
|
||||
sess.children[c.ID] = c
|
||||
sess.order = append(sess.order, c.ID)
|
||||
}
|
||||
|
||||
// TestHelpTimersDocumentsAllTools mirrors the whoami check for the
|
||||
// help("timers") topic — the related-tools list must enumerate every
|
||||
// timer_* tool so callers reading help can dispatch them.
|
||||
|
||||
Reference in New Issue
Block a user