mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-09-05 22:16:59 +00:00
Extract TeamToolBackend interface from concrete *TeamToolManager to enable unit testing of the 17 team task action handlers without DB or message bus. - Add TeamToolBackend interface (19 methods) in team_tool_backend.go - Add exported wrappers on TeamToolManager (cache, helpers, dispatch, manager) - Migrate 133 call sites across 5 action handler files (pure casing rename) - Change TeamTasksTool.manager field from *TeamToolManager to TeamToolBackend - Add mock backend infrastructure (baseNoopTeamStore + mockTaskStore + mockBackend) - Add 94 unit subtests across 6 test files covering all actions: lifecycle (claim/complete/cancel/review/approve/reject), create validation, blocker escalation, mutations (comment/progress/attach/update), followup (ask_user/clear_ask_user/retry), read actions (notification/policy/list/search/get) WorkspaceInterceptor and PostTurnProcessor remain on concrete *TeamToolManager. No behavioral changes — all existing logic preserved.
146 lines
3.8 KiB
Go
146 lines
3.8 KiB
Go
package tools
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
)
|
|
|
|
func TestExecute_NotificationRunBlocked(t *testing.T) {
|
|
mb, tool, _, _, ctx := newTestTeamSetup()
|
|
_ = mb
|
|
|
|
notifCtx := WithRunKind(ctx, RunKindNotification)
|
|
|
|
tests := []struct {
|
|
action string
|
|
wantErr bool
|
|
}{
|
|
{"list", false},
|
|
{"get", false},
|
|
{"search", false},
|
|
{"create", true},
|
|
{"claim", true},
|
|
{"complete", true},
|
|
{"cancel", true},
|
|
{"approve", true},
|
|
{"reject", true},
|
|
{"comment", true},
|
|
{"progress", true},
|
|
{"update", true},
|
|
{"retry", true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.action, func(t *testing.T) {
|
|
result := tool.Execute(notifCtx, map[string]any{"action": tt.action})
|
|
if tt.wantErr && !result.IsError {
|
|
t.Errorf("expected error for action %q during notification run", tt.action)
|
|
}
|
|
if !tt.wantErr && result.IsError {
|
|
if strings.Contains(result.ForLLM, "notification run") {
|
|
t.Errorf("action %q should be allowed during notification run, got: %s", tt.action, result.ForLLM)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExecute_PolicyBlocked(t *testing.T) {
|
|
mb, _, _, _, ctx := newTestTeamSetup()
|
|
|
|
liteTool := NewTeamTasksTool(mb, LiteTeamPolicy{})
|
|
|
|
blocked := []string{"comment", "review", "approve", "reject", "attach", "ask_user", "clear_ask_user"}
|
|
for _, action := range blocked {
|
|
t.Run(action, func(t *testing.T) {
|
|
result := liteTool.Execute(ctx, map[string]any{"action": action})
|
|
if !result.IsError {
|
|
t.Errorf("expected policy block for action %q in lite edition", action)
|
|
}
|
|
if !strings.Contains(result.ForLLM, "not available in this edition") {
|
|
t.Errorf("expected edition error, got: %s", result.ForLLM)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExecute_UnknownAction(t *testing.T) {
|
|
_, tool, _, _, ctx := newTestTeamSetup()
|
|
|
|
result := tool.Execute(ctx, map[string]any{"action": "nonexistent"})
|
|
if !result.IsError {
|
|
t.Fatal("expected error for unknown action")
|
|
}
|
|
if !strings.Contains(result.ForLLM, "unknown action") {
|
|
t.Errorf("expected 'unknown action' error, got: %s", result.ForLLM)
|
|
}
|
|
}
|
|
|
|
func TestList_FiltersByUserForExternalChannels(t *testing.T) {
|
|
_, tool, _, _, ctx := newTestTeamSetup()
|
|
|
|
extCtx := WithToolChannel(ctx, "telegram")
|
|
extCtx = store.WithUserID(extCtx, "user123")
|
|
|
|
result := tool.Execute(extCtx, map[string]any{"action": "list"})
|
|
if result.IsError {
|
|
t.Fatalf("unexpected error: %s", result.ForLLM)
|
|
}
|
|
}
|
|
|
|
func TestList_NoFilterForTeammate(t *testing.T) {
|
|
_, tool, _, _, ctx := newTestTeamSetup()
|
|
|
|
teamCtx := WithToolChannel(ctx, ChannelTeammate)
|
|
|
|
result := tool.Execute(teamCtx, map[string]any{"action": "list"})
|
|
if result.IsError {
|
|
t.Fatalf("unexpected error: %s", result.ForLLM)
|
|
}
|
|
}
|
|
|
|
func TestSearch_SatisfiesCreateGate(t *testing.T) {
|
|
_, tool, _, _, ctx := newTestTeamSetup()
|
|
|
|
ptd := NewPendingTeamDispatch()
|
|
ctx = WithPendingTeamDispatch(ctx, ptd)
|
|
|
|
if ptd.HasListed() {
|
|
t.Fatal("expected HasListed=false before search")
|
|
}
|
|
|
|
tool.Execute(ctx, map[string]any{"action": "search", "query": "test"})
|
|
|
|
if !ptd.HasListed() {
|
|
t.Error("expected HasListed=true after search")
|
|
}
|
|
}
|
|
|
|
func TestGet_CrossTeamBlocked(t *testing.T) {
|
|
mb, tool, _, _, ctx := newTestTeamSetup()
|
|
|
|
otherTeamID := uuid.New()
|
|
taskID := uuid.New()
|
|
mb.taskStore.mu.Lock()
|
|
mb.taskStore.tasks[taskID] = &store.TeamTaskData{
|
|
BaseModel: store.BaseModel{ID: taskID, CreatedAt: time.Now(), UpdatedAt: time.Now()},
|
|
TeamID: otherTeamID,
|
|
Subject: "Other team task",
|
|
Status: store.TeamTaskStatusPending,
|
|
}
|
|
mb.taskStore.mu.Unlock()
|
|
|
|
result := tool.Execute(ctx, map[string]any{"action": "get", "task_id": taskID.String()})
|
|
if !result.IsError {
|
|
t.Fatal("expected error for cross-team get")
|
|
}
|
|
if !strings.Contains(result.ForLLM, "does not belong") {
|
|
t.Errorf("expected team ownership error, got: %s", result.ForLLM)
|
|
}
|
|
}
|