Files
goclaw/internal/tools/team_mock_backend_test.go
viettranx e62c027b7d feat(vault): task + delegation auto-linking in enrich worker
- Add enrich_auto_linking.go: deterministic auto-link logic for task/delegation contexts
- Add team_task_siblings.go (PG + SQLite): find all tasks in same delegation for batch link
- Update enrich_worker.go: call enrich_auto_linking Phase 2 hook + skip re-embed for binary
- Update team_store.go: add TaskSiblings interface method + TeamStore binding
- Add context_keys.go + test: define ContextDelegationIDKey + ContextTaskIDKey for propagation
- Update vault_interceptor.go: extract + inject delegation ID from request
- Update run_context.go: propagate DelegationID from request
- Update loop_context.go: inject DelegationID from run context
- Update teams_tasks.go: DeleteTask(s) → cleanup auto-links via vault_source_cleanup
- Update teams_tasks_activity.go: DetachFileFromTask → cleanup related auto-links
- Update vault_documents_enrichment.go: populate summary from media_summary
- Update cmd/gateway.go: wire TeamStore to handler bootstrap
2026-04-11 21:22:23 +07:00

894 lines
29 KiB
Go

package tools
import (
"context"
"fmt"
"strings"
"sync"
"time"
"github.com/google/uuid"
"github.com/nextlevelbuilder/goclaw/internal/bus"
"github.com/nextlevelbuilder/goclaw/internal/store"
)
// ============================================================
// baseNoopTeamStore — noop implementations for all TeamStore methods
// ============================================================
type baseNoopTeamStore struct{}
// TeamCRUDStore
func (b *baseNoopTeamStore) CreateTeam(_ context.Context, _ *store.TeamData) error {
return fmt.Errorf("not implemented: CreateTeam")
}
func (b *baseNoopTeamStore) GetTeam(_ context.Context, _ uuid.UUID) (*store.TeamData, error) {
return nil, fmt.Errorf("not implemented: GetTeam")
}
func (b *baseNoopTeamStore) GetTeamUnscoped(_ context.Context, _ uuid.UUID) (*store.TeamData, error) {
return nil, fmt.Errorf("not implemented: GetTeamUnscoped")
}
func (b *baseNoopTeamStore) UpdateTeam(_ context.Context, _ uuid.UUID, _ map[string]any) error {
return fmt.Errorf("not implemented: UpdateTeam")
}
func (b *baseNoopTeamStore) DeleteTeam(_ context.Context, _ uuid.UUID) error {
return fmt.Errorf("not implemented: DeleteTeam")
}
func (b *baseNoopTeamStore) ListTeams(_ context.Context) ([]store.TeamData, error) {
return nil, fmt.Errorf("not implemented: ListTeams")
}
func (b *baseNoopTeamStore) AddMember(_ context.Context, _, _ uuid.UUID, _ string) error {
return fmt.Errorf("not implemented: AddMember")
}
func (b *baseNoopTeamStore) RemoveMember(_ context.Context, _, _ uuid.UUID) error {
return fmt.Errorf("not implemented: RemoveMember")
}
func (b *baseNoopTeamStore) ListMembers(_ context.Context, _ uuid.UUID) ([]store.TeamMemberData, error) {
return nil, fmt.Errorf("not implemented: ListMembers")
}
func (b *baseNoopTeamStore) ListIdleMembers(_ context.Context, _ uuid.UUID) ([]store.TeamMemberData, error) {
return nil, fmt.Errorf("not implemented: ListIdleMembers")
}
func (b *baseNoopTeamStore) GetTeamForAgent(_ context.Context, _ uuid.UUID) (*store.TeamData, error) {
return nil, fmt.Errorf("not implemented: GetTeamForAgent")
}
func (b *baseNoopTeamStore) KnownUserIDs(_ context.Context, _ uuid.UUID, _ int) ([]string, error) {
return nil, fmt.Errorf("not implemented: KnownUserIDs")
}
func (b *baseNoopTeamStore) ListTaskScopes(_ context.Context, _ uuid.UUID) ([]store.ScopeEntry, error) {
return nil, fmt.Errorf("not implemented: ListTaskScopes")
}
// TaskStore
func (b *baseNoopTeamStore) CreateTask(_ context.Context, _ *store.TeamTaskData) error {
return fmt.Errorf("not implemented: CreateTask")
}
func (b *baseNoopTeamStore) UpdateTask(_ context.Context, _ uuid.UUID, _ map[string]any) error {
return fmt.Errorf("not implemented: UpdateTask")
}
func (b *baseNoopTeamStore) ListTasks(_ context.Context, _ uuid.UUID, _, _, _, _, _ string, _, _ int) ([]store.TeamTaskData, error) {
return nil, fmt.Errorf("not implemented: ListTasks")
}
func (b *baseNoopTeamStore) GetTask(_ context.Context, _ uuid.UUID) (*store.TeamTaskData, error) {
return nil, fmt.Errorf("not implemented: GetTask")
}
func (b *baseNoopTeamStore) GetTasksByIDs(_ context.Context, _ []uuid.UUID) ([]store.TeamTaskData, error) {
return nil, fmt.Errorf("not implemented: GetTasksByIDs")
}
func (b *baseNoopTeamStore) SearchTasks(_ context.Context, _ uuid.UUID, _ string, _ int, _ string) ([]store.TeamTaskData, error) {
return nil, fmt.Errorf("not implemented: SearchTasks")
}
func (b *baseNoopTeamStore) DeleteTask(_ context.Context, _, _ uuid.UUID) error {
return fmt.Errorf("not implemented: DeleteTask")
}
func (b *baseNoopTeamStore) DeleteTasks(_ context.Context, _ []uuid.UUID, _ uuid.UUID) ([]uuid.UUID, error) {
return nil, fmt.Errorf("not implemented: DeleteTasks")
}
func (b *baseNoopTeamStore) ClaimTask(_ context.Context, _, _, _ uuid.UUID) error {
return fmt.Errorf("not implemented: ClaimTask")
}
func (b *baseNoopTeamStore) AssignTask(_ context.Context, _, _, _ uuid.UUID) error {
return fmt.Errorf("not implemented: AssignTask")
}
func (b *baseNoopTeamStore) CompleteTask(_ context.Context, _, _ uuid.UUID, _ string) error {
return fmt.Errorf("not implemented: CompleteTask")
}
func (b *baseNoopTeamStore) CancelTask(_ context.Context, _, _ uuid.UUID, _ string) error {
return fmt.Errorf("not implemented: CancelTask")
}
func (b *baseNoopTeamStore) FailTask(_ context.Context, _, _ uuid.UUID, _ string) error {
return fmt.Errorf("not implemented: FailTask")
}
func (b *baseNoopTeamStore) FailPendingTask(_ context.Context, _, _ uuid.UUID, _ string) error {
return fmt.Errorf("not implemented: FailPendingTask")
}
func (b *baseNoopTeamStore) ReviewTask(_ context.Context, _, _ uuid.UUID) error {
return fmt.Errorf("not implemented: ReviewTask")
}
func (b *baseNoopTeamStore) ApproveTask(_ context.Context, _, _ uuid.UUID, _ string) error {
return fmt.Errorf("not implemented: ApproveTask")
}
func (b *baseNoopTeamStore) RejectTask(_ context.Context, _, _ uuid.UUID, _ string) error {
return fmt.Errorf("not implemented: RejectTask")
}
func (b *baseNoopTeamStore) UpdateTaskProgress(_ context.Context, _, _ uuid.UUID, _ int, _ string) error {
return fmt.Errorf("not implemented: UpdateTaskProgress")
}
func (b *baseNoopTeamStore) RenewTaskLock(_ context.Context, _, _ uuid.UUID) error {
return fmt.Errorf("not implemented: RenewTaskLock")
}
func (b *baseNoopTeamStore) ResetTaskStatus(_ context.Context, _, _ uuid.UUID) error {
return fmt.Errorf("not implemented: ResetTaskStatus")
}
func (b *baseNoopTeamStore) ListActiveTasksByChatID(_ context.Context, _ string) ([]store.TeamTaskData, error) {
return nil, fmt.Errorf("not implemented: ListActiveTasksByChatID")
}
// TaskCommentStore
func (b *baseNoopTeamStore) AddTaskComment(_ context.Context, _ *store.TeamTaskCommentData) error {
return fmt.Errorf("not implemented: AddTaskComment")
}
func (b *baseNoopTeamStore) ListTaskComments(_ context.Context, _ uuid.UUID) ([]store.TeamTaskCommentData, error) {
return nil, fmt.Errorf("not implemented: ListTaskComments")
}
func (b *baseNoopTeamStore) ListRecentTaskComments(_ context.Context, _ uuid.UUID, _ int) ([]store.TeamTaskCommentData, error) {
return nil, fmt.Errorf("not implemented: ListRecentTaskComments")
}
func (b *baseNoopTeamStore) RecordTaskEvent(_ context.Context, _ *store.TeamTaskEventData) error {
return fmt.Errorf("not implemented: RecordTaskEvent")
}
func (b *baseNoopTeamStore) ListTaskEvents(_ context.Context, _ uuid.UUID) ([]store.TeamTaskEventData, error) {
return nil, fmt.Errorf("not implemented: ListTaskEvents")
}
func (b *baseNoopTeamStore) ListTeamEvents(_ context.Context, _ uuid.UUID, _, _ int) ([]store.TeamTaskEventData, error) {
return nil, fmt.Errorf("not implemented: ListTeamEvents")
}
func (b *baseNoopTeamStore) AttachFileToTask(_ context.Context, _ *store.TeamTaskAttachmentData) error {
return fmt.Errorf("not implemented: AttachFileToTask")
}
func (b *baseNoopTeamStore) GetAttachment(_ context.Context, _ uuid.UUID) (*store.TeamTaskAttachmentData, error) {
return nil, fmt.Errorf("not implemented: GetAttachment")
}
func (b *baseNoopTeamStore) ListTaskAttachments(_ context.Context, _ uuid.UUID) ([]store.TeamTaskAttachmentData, error) {
return nil, fmt.Errorf("not implemented: ListTaskAttachments")
}
func (b *baseNoopTeamStore) DetachFileFromTask(_ context.Context, _ uuid.UUID, _ string) error {
return fmt.Errorf("not implemented: DetachFileFromTask")
}
func (b *baseNoopTeamStore) BatchGetTaskSiblingsByBasenames(_ context.Context, _ uuid.UUID, _ []string, _ int) (map[string][]store.TaskSibling, error) {
return nil, nil
}
// TaskRecoveryStore
func (b *baseNoopTeamStore) RecoverAllStaleTasks(_ context.Context) ([]store.RecoveredTaskInfo, error) {
return nil, fmt.Errorf("not implemented: RecoverAllStaleTasks")
}
func (b *baseNoopTeamStore) ForceRecoverAllTasks(_ context.Context) ([]store.RecoveredTaskInfo, error) {
return nil, fmt.Errorf("not implemented: ForceRecoverAllTasks")
}
func (b *baseNoopTeamStore) ListRecoverableTasks(_ context.Context, _ uuid.UUID) ([]store.TeamTaskData, error) {
return nil, fmt.Errorf("not implemented: ListRecoverableTasks")
}
func (b *baseNoopTeamStore) MarkAllStaleTasks(_ context.Context, _ time.Time) ([]store.RecoveredTaskInfo, error) {
return nil, fmt.Errorf("not implemented: MarkAllStaleTasks")
}
func (b *baseNoopTeamStore) MarkInReviewStaleTasks(_ context.Context, _ time.Time) ([]store.RecoveredTaskInfo, error) {
return nil, fmt.Errorf("not implemented: MarkInReviewStaleTasks")
}
func (b *baseNoopTeamStore) FixOrphanedBlockedTasks(_ context.Context) ([]store.RecoveredTaskInfo, error) {
return nil, fmt.Errorf("not implemented: FixOrphanedBlockedTasks")
}
// TaskFollowupStore
func (b *baseNoopTeamStore) SetTaskFollowup(_ context.Context, _, _ uuid.UUID, _ time.Time, _ int, _, _, _ string) error {
return fmt.Errorf("not implemented: SetTaskFollowup")
}
func (b *baseNoopTeamStore) ClearTaskFollowup(_ context.Context, _ uuid.UUID) error {
return fmt.Errorf("not implemented: ClearTaskFollowup")
}
func (b *baseNoopTeamStore) ListAllFollowupDueTasks(_ context.Context) ([]store.TeamTaskData, error) {
return nil, fmt.Errorf("not implemented: ListAllFollowupDueTasks")
}
func (b *baseNoopTeamStore) IncrementFollowupCount(_ context.Context, _ uuid.UUID, _ *time.Time) error {
return fmt.Errorf("not implemented: IncrementFollowupCount")
}
func (b *baseNoopTeamStore) ClearFollowupByScope(_ context.Context, _, _ string) (int, error) {
return 0, fmt.Errorf("not implemented: ClearFollowupByScope")
}
func (b *baseNoopTeamStore) SetFollowupForActiveTasks(_ context.Context, _ uuid.UUID, _, _ string, _ time.Time, _ int, _ string) (int, error) {
return 0, fmt.Errorf("not implemented: SetFollowupForActiveTasks")
}
func (b *baseNoopTeamStore) HasActiveMemberTasks(_ context.Context, _ uuid.UUID, _ uuid.UUID) (bool, error) {
return false, fmt.Errorf("not implemented: HasActiveMemberTasks")
}
// TeamAccessStore
func (b *baseNoopTeamStore) GrantTeamAccess(_ context.Context, _ uuid.UUID, _, _, _ string) error {
return fmt.Errorf("not implemented: GrantTeamAccess")
}
func (b *baseNoopTeamStore) RevokeTeamAccess(_ context.Context, _ uuid.UUID, _ string) error {
return fmt.Errorf("not implemented: RevokeTeamAccess")
}
func (b *baseNoopTeamStore) ListTeamGrants(_ context.Context, _ uuid.UUID) ([]store.TeamUserGrant, error) {
return nil, fmt.Errorf("not implemented: ListTeamGrants")
}
func (b *baseNoopTeamStore) ListUserTeams(_ context.Context, _ string) ([]store.TeamData, error) {
return nil, fmt.Errorf("not implemented: ListUserTeams")
}
func (b *baseNoopTeamStore) HasTeamAccess(_ context.Context, _ uuid.UUID, _ string) (bool, error) {
return false, fmt.Errorf("not implemented: HasTeamAccess")
}
// ============================================================
// mockTaskStore — in-memory TeamStore for unit tests
// ============================================================
type mockTaskStore struct {
baseNoopTeamStore
tasks map[uuid.UUID]*store.TeamTaskData
comments map[uuid.UUID][]store.TeamTaskCommentData
events map[uuid.UUID][]store.TeamTaskEventData
attachments map[uuid.UUID][]store.TeamTaskAttachmentData
team *store.TeamData
members []store.TeamMemberData
taskSeq int
mu sync.Mutex
}
func newMockTaskStore(team *store.TeamData, members []store.TeamMemberData) *mockTaskStore {
return &mockTaskStore{
tasks: make(map[uuid.UUID]*store.TeamTaskData),
comments: make(map[uuid.UUID][]store.TeamTaskCommentData),
events: make(map[uuid.UUID][]store.TeamTaskEventData),
attachments: make(map[uuid.UUID][]store.TeamTaskAttachmentData),
team: team,
members: members,
}
}
func (s *mockTaskStore) GetTeam(_ context.Context, teamID uuid.UUID) (*store.TeamData, error) {
s.mu.Lock()
defer s.mu.Unlock()
if s.team != nil && s.team.ID == teamID {
return s.team, nil
}
return nil, fmt.Errorf("team not found: %s", teamID)
}
func (s *mockTaskStore) GetTeamForAgent(_ context.Context, _ uuid.UUID) (*store.TeamData, error) {
s.mu.Lock()
defer s.mu.Unlock()
return s.team, nil
}
func (s *mockTaskStore) ListMembers(_ context.Context, _ uuid.UUID) ([]store.TeamMemberData, error) {
s.mu.Lock()
defer s.mu.Unlock()
return s.members, nil
}
func (s *mockTaskStore) GetTask(_ context.Context, taskID uuid.UUID) (*store.TeamTaskData, error) {
s.mu.Lock()
defer s.mu.Unlock()
t, ok := s.tasks[taskID]
if !ok {
return nil, store.ErrTaskNotFound
}
cp := *t
return &cp, nil
}
func (s *mockTaskStore) GetTasksByIDs(_ context.Context, ids []uuid.UUID) ([]store.TeamTaskData, error) {
s.mu.Lock()
defer s.mu.Unlock()
out := make([]store.TeamTaskData, 0, len(ids))
for _, id := range ids {
if t, ok := s.tasks[id]; ok {
out = append(out, *t)
}
}
return out, nil
}
func (s *mockTaskStore) CreateTask(_ context.Context, task *store.TeamTaskData) error {
s.mu.Lock()
defer s.mu.Unlock()
now := time.Now()
task.ID = uuid.New()
s.taskSeq++
task.TaskNumber = s.taskSeq
task.CreatedAt = now
task.UpdatedAt = now
if task.Identifier == "" {
task.Identifier = fmt.Sprintf("T-%d", task.TaskNumber)
}
cp := *task
s.tasks[task.ID] = &cp
return nil
}
func (s *mockTaskStore) UpdateTask(_ context.Context, taskID uuid.UUID, updates map[string]any) error {
s.mu.Lock()
defer s.mu.Unlock()
t, ok := s.tasks[taskID]
if !ok {
return store.ErrTaskNotFound
}
for k, v := range updates {
switch k {
case "status":
if sv, ok := v.(string); ok {
t.Status = sv
}
case "result":
if sv, ok := v.(string); ok {
t.Result = &sv
}
case "metadata":
if mv, ok := v.(map[string]any); ok {
t.Metadata = mv
}
case "description":
if sv, ok := v.(string); ok {
t.Description = sv
}
case "subject":
if sv, ok := v.(string); ok {
t.Subject = sv
}
case "blocked_by":
if ids, ok := v.([]uuid.UUID); ok {
t.BlockedBy = ids
}
case "owner_agent_id":
if id, ok := v.(uuid.UUID); ok {
t.OwnerAgentID = &id
}
case "locked_at":
if tv, ok := v.(time.Time); ok {
t.LockedAt = &tv
}
case "lock_expires_at":
if tv, ok := v.(time.Time); ok {
t.LockExpiresAt = &tv
}
case "progress_percent":
if iv, ok := v.(int); ok {
t.ProgressPercent = iv
}
case "progress_step":
if sv, ok := v.(string); ok {
t.ProgressStep = sv
}
case "followup_at":
if tv, ok := v.(time.Time); ok {
t.FollowupAt = &tv
}
case "followup_count":
if iv, ok := v.(int); ok {
t.FollowupCount = iv
}
case "followup_max":
if iv, ok := v.(int); ok {
t.FollowupMax = iv
}
case "followup_message":
if sv, ok := v.(string); ok {
t.FollowupMessage = sv
}
case "followup_channel":
if sv, ok := v.(string); ok {
t.FollowupChannel = sv
}
case "followup_chat_id":
if sv, ok := v.(string); ok {
t.FollowupChatID = sv
}
}
}
t.UpdatedAt = time.Now()
return nil
}
func (s *mockTaskStore) ClaimTask(_ context.Context, taskID, agentID, _ uuid.UUID) error {
s.mu.Lock()
defer s.mu.Unlock()
t, ok := s.tasks[taskID]
if !ok {
return store.ErrTaskNotFound
}
if t.Status != store.TeamTaskStatusPending {
return fmt.Errorf("cannot claim task with status %q", t.Status)
}
if t.OwnerAgentID != nil && *t.OwnerAgentID != agentID {
return fmt.Errorf("task already owned by another agent")
}
now := time.Now()
t.Status = store.TeamTaskStatusInProgress
t.OwnerAgentID = &agentID
t.LockedAt = &now
t.UpdatedAt = now
return nil
}
func (s *mockTaskStore) AssignTask(_ context.Context, taskID, agentID, _ uuid.UUID) error {
s.mu.Lock()
defer s.mu.Unlock()
t, ok := s.tasks[taskID]
if !ok {
return store.ErrTaskNotFound
}
if t.Status != store.TeamTaskStatusPending {
return fmt.Errorf("cannot assign task with status %q", t.Status)
}
now := time.Now()
t.Status = store.TeamTaskStatusInProgress
t.OwnerAgentID = &agentID
t.LockedAt = &now
t.UpdatedAt = now
return nil
}
func (s *mockTaskStore) CompleteTask(_ context.Context, taskID, _ uuid.UUID, result string) error {
s.mu.Lock()
defer s.mu.Unlock()
t, ok := s.tasks[taskID]
if !ok {
return store.ErrTaskNotFound
}
if t.Status != store.TeamTaskStatusInProgress {
return fmt.Errorf("cannot complete task with status %q", t.Status)
}
t.Status = store.TeamTaskStatusCompleted
t.Result = &result
t.UpdatedAt = time.Now()
return nil
}
func (s *mockTaskStore) CancelTask(_ context.Context, taskID, _ uuid.UUID, reason string) error {
s.mu.Lock()
defer s.mu.Unlock()
t, ok := s.tasks[taskID]
if !ok {
return store.ErrTaskNotFound
}
if t.Status == store.TeamTaskStatusCompleted || t.Status == store.TeamTaskStatusCancelled {
return fmt.Errorf("cannot cancel task with status %q", t.Status)
}
t.Status = store.TeamTaskStatusCancelled
t.Result = &reason
t.UpdatedAt = time.Now()
return nil
}
func (s *mockTaskStore) FailTask(_ context.Context, taskID, _ uuid.UUID, errMsg string) error {
s.mu.Lock()
defer s.mu.Unlock()
t, ok := s.tasks[taskID]
if !ok {
return store.ErrTaskNotFound
}
if t.Status != store.TeamTaskStatusInProgress {
return fmt.Errorf("cannot fail task with status %q", t.Status)
}
result := "FAILED: " + errMsg
t.Status = store.TeamTaskStatusFailed
t.Result = &result
t.UpdatedAt = time.Now()
return nil
}
func (s *mockTaskStore) FailPendingTask(_ context.Context, taskID, _ uuid.UUID, errMsg string) error {
s.mu.Lock()
defer s.mu.Unlock()
t, ok := s.tasks[taskID]
if !ok {
return store.ErrTaskNotFound
}
if t.Status != store.TeamTaskStatusPending && t.Status != store.TeamTaskStatusBlocked {
return fmt.Errorf("cannot fail-pending task with status %q", t.Status)
}
t.Status = store.TeamTaskStatusFailed
t.Result = &errMsg
t.UpdatedAt = time.Now()
return nil
}
func (s *mockTaskStore) ReviewTask(_ context.Context, taskID, _ uuid.UUID) error {
s.mu.Lock()
defer s.mu.Unlock()
t, ok := s.tasks[taskID]
if !ok {
return store.ErrTaskNotFound
}
if t.Status != store.TeamTaskStatusInProgress {
return fmt.Errorf("cannot review task with status %q", t.Status)
}
t.Status = store.TeamTaskStatusInReview
t.UpdatedAt = time.Now()
return nil
}
func (s *mockTaskStore) ApproveTask(_ context.Context, taskID, _ uuid.UUID, _ string) error {
s.mu.Lock()
defer s.mu.Unlock()
t, ok := s.tasks[taskID]
if !ok {
return store.ErrTaskNotFound
}
if t.Status != store.TeamTaskStatusInReview {
return fmt.Errorf("cannot approve task with status %q", t.Status)
}
t.Status = store.TeamTaskStatusCompleted
t.UpdatedAt = time.Now()
return nil
}
func (s *mockTaskStore) RejectTask(_ context.Context, taskID, _ uuid.UUID, reason string) error {
s.mu.Lock()
defer s.mu.Unlock()
t, ok := s.tasks[taskID]
if !ok {
return store.ErrTaskNotFound
}
if t.Status != store.TeamTaskStatusInReview {
return fmt.Errorf("cannot reject task with status %q", t.Status)
}
t.Status = store.TeamTaskStatusCancelled
t.Result = &reason
t.UpdatedAt = time.Now()
return nil
}
func (s *mockTaskStore) ResetTaskStatus(_ context.Context, taskID, _ uuid.UUID) error {
s.mu.Lock()
defer s.mu.Unlock()
t, ok := s.tasks[taskID]
if !ok {
return store.ErrTaskNotFound
}
t.Status = store.TeamTaskStatusPending
t.LockedAt = nil
t.LockExpiresAt = nil
t.UpdatedAt = time.Now()
return nil
}
func (s *mockTaskStore) UpdateTaskProgress(_ context.Context, taskID, _ uuid.UUID, percent int, step string) error {
s.mu.Lock()
defer s.mu.Unlock()
t, ok := s.tasks[taskID]
if !ok {
return store.ErrTaskNotFound
}
if t.Status != store.TeamTaskStatusInProgress {
return fmt.Errorf("cannot update progress for task with status %q", t.Status)
}
t.ProgressPercent = percent
t.ProgressStep = step
t.UpdatedAt = time.Now()
return nil
}
func (s *mockTaskStore) RenewTaskLock(_ context.Context, taskID, _ uuid.UUID) error {
s.mu.Lock()
defer s.mu.Unlock()
t, ok := s.tasks[taskID]
if !ok {
return store.ErrTaskNotFound
}
expires := time.Now().Add(10 * time.Minute)
t.LockExpiresAt = &expires
t.UpdatedAt = time.Now()
return nil
}
func (s *mockTaskStore) ListTasks(_ context.Context, teamID uuid.UUID, _, _, _, _, _ string, _, _ int) ([]store.TeamTaskData, error) {
s.mu.Lock()
defer s.mu.Unlock()
var out []store.TeamTaskData
for _, t := range s.tasks {
if t.TeamID == teamID {
out = append(out, *t)
}
}
return out, nil
}
func (s *mockTaskStore) SearchTasks(_ context.Context, teamID uuid.UUID, query string, limit int, _ string) ([]store.TeamTaskData, error) {
s.mu.Lock()
defer s.mu.Unlock()
q := strings.ToLower(query)
var out []store.TeamTaskData
for _, t := range s.tasks {
if t.TeamID == teamID && strings.Contains(strings.ToLower(t.Subject), q) {
out = append(out, *t)
if limit > 0 && len(out) >= limit {
break
}
}
}
return out, nil
}
func (s *mockTaskStore) AddTaskComment(_ context.Context, comment *store.TeamTaskCommentData) error {
s.mu.Lock()
defer s.mu.Unlock()
comment.ID = uuid.New()
comment.CreatedAt = time.Now()
cp := *comment
s.comments[comment.TaskID] = append(s.comments[comment.TaskID], cp)
return nil
}
func (s *mockTaskStore) ListTaskComments(_ context.Context, taskID uuid.UUID) ([]store.TeamTaskCommentData, error) {
s.mu.Lock()
defer s.mu.Unlock()
return s.comments[taskID], nil
}
func (s *mockTaskStore) ListRecentTaskComments(_ context.Context, taskID uuid.UUID, limit int) ([]store.TeamTaskCommentData, error) {
s.mu.Lock()
defer s.mu.Unlock()
all := s.comments[taskID]
if limit <= 0 || len(all) <= limit {
return all, nil
}
return all[len(all)-limit:], nil
}
func (s *mockTaskStore) ListTaskEvents(_ context.Context, taskID uuid.UUID) ([]store.TeamTaskEventData, error) {
s.mu.Lock()
defer s.mu.Unlock()
return s.events[taskID], nil
}
func (s *mockTaskStore) AttachFileToTask(_ context.Context, att *store.TeamTaskAttachmentData) error {
s.mu.Lock()
defer s.mu.Unlock()
att.ID = uuid.New()
att.CreatedAt = time.Now()
cp := *att
s.attachments[att.TaskID] = append(s.attachments[att.TaskID], cp)
return nil
}
func (s *mockTaskStore) ListTaskAttachments(_ context.Context, taskID uuid.UUID) ([]store.TeamTaskAttachmentData, error) {
s.mu.Lock()
defer s.mu.Unlock()
return s.attachments[taskID], nil
}
func (s *mockTaskStore) SetTaskFollowup(_ context.Context, taskID, _ uuid.UUID, followupAt time.Time, max int, message, channel, chatID string) error {
s.mu.Lock()
defer s.mu.Unlock()
t, ok := s.tasks[taskID]
if !ok {
return store.ErrTaskNotFound
}
t.FollowupAt = &followupAt
t.FollowupMax = max
t.FollowupMessage = message
t.FollowupChannel = channel
t.FollowupChatID = chatID
t.UpdatedAt = time.Now()
return nil
}
func (s *mockTaskStore) ClearTaskFollowup(_ context.Context, taskID uuid.UUID) error {
s.mu.Lock()
defer s.mu.Unlock()
t, ok := s.tasks[taskID]
if !ok {
return store.ErrTaskNotFound
}
t.FollowupAt = nil
t.FollowupCount = 0
t.FollowupMax = 0
t.FollowupMessage = ""
t.FollowupChannel = ""
t.FollowupChatID = ""
t.UpdatedAt = time.Now()
return nil
}
func (s *mockTaskStore) ListRecoverableTasks(_ context.Context, teamID uuid.UUID) ([]store.TeamTaskData, error) {
s.mu.Lock()
defer s.mu.Unlock()
var out []store.TeamTaskData
for _, t := range s.tasks {
if t.TeamID != teamID {
continue
}
if t.Status == store.TeamTaskStatusPending || t.Status == store.TeamTaskStatusStale {
out = append(out, *t)
}
}
return out, nil
}
// ============================================================
// capturedEvent / capturedDispatch
// ============================================================
type capturedEvent struct {
Name string
Payload any
}
type capturedDispatch struct {
TaskID uuid.UUID
AgentID uuid.UUID
}
// ============================================================
// mockBackend — implements TeamToolBackend for unit tests
// ============================================================
type mockBackend struct {
team *store.TeamData
members []store.TeamMemberData
agents map[uuid.UUID]*store.AgentData
agentKeys map[string]*store.AgentData
taskStore *mockTaskStore
events []capturedEvent
dispatches []capturedDispatch
inbound []bus.InboundMessage
mu sync.Mutex
}
func newMockBackend(team *store.TeamData, members []store.TeamMemberData, agents []*store.AgentData) *mockBackend {
mb := &mockBackend{
team: team,
members: members,
agents: make(map[uuid.UUID]*store.AgentData),
agentKeys: make(map[string]*store.AgentData),
taskStore: newMockTaskStore(team, members),
}
for _, ag := range agents {
mb.agents[ag.ID] = ag
mb.agentKeys[ag.AgentKey] = ag
}
return mb
}
func (mb *mockBackend) ResolveTeam(ctx context.Context) (*store.TeamData, uuid.UUID, error) {
if mb.team == nil {
return nil, uuid.Nil, fmt.Errorf("no team configured in mock")
}
agentID := store.AgentIDFromContext(ctx)
return mb.team, agentID, nil
}
func (mb *mockBackend) RequireLead(ctx context.Context, team *store.TeamData, agentID uuid.UUID) error {
channel := ToolChannelFromCtx(ctx)
if channel == ChannelTeammate || channel == ChannelSystem {
return nil
}
if agentID != team.LeadAgentID {
return fmt.Errorf("only the team lead can perform this action")
}
return nil
}
func (mb *mockBackend) Store() store.TeamStore { return mb.taskStore }
func (mb *mockBackend) ResolveAgentByKey(_ context.Context, key string) (uuid.UUID, error) {
if ag, ok := mb.agentKeys[key]; ok {
return ag.ID, nil
}
return uuid.Nil, fmt.Errorf("agent not found: %s", key)
}
func (mb *mockBackend) AgentKeyFromID(_ context.Context, id uuid.UUID) string {
if ag, ok := mb.agents[id]; ok {
return ag.AgentKey
}
return id.String()
}
func (mb *mockBackend) AgentDisplayName(_ context.Context, key string) string {
if ag, ok := mb.agentKeys[key]; ok {
return ag.DisplayName
}
return ""
}
func (mb *mockBackend) CachedListMembers(_ context.Context, _ uuid.UUID, _ uuid.UUID) ([]store.TeamMemberData, error) {
return mb.members, nil
}
func (mb *mockBackend) CachedGetAgentByID(_ context.Context, id uuid.UUID) (*store.AgentData, error) {
if ag, ok := mb.agents[id]; ok {
return ag, nil
}
return nil, fmt.Errorf("agent not found: %s", id)
}
func (mb *mockBackend) PreWarmAgentKeyCache(_ context.Context, _ []string) {}
func (mb *mockBackend) PreWarmAgentIDCache(_ context.Context, _ []uuid.UUID) {}
func (mb *mockBackend) BroadcastTeamEvent(_ context.Context, name string, payload any) {
mb.mu.Lock()
mb.events = append(mb.events, capturedEvent{Name: name, Payload: payload})
mb.mu.Unlock()
}
func (mb *mockBackend) DispatchTaskToAgent(_ context.Context, task *store.TeamTaskData, _ *store.TeamData, agentID uuid.UUID) {
mb.mu.Lock()
mb.dispatches = append(mb.dispatches, capturedDispatch{TaskID: task.ID, AgentID: agentID})
mb.mu.Unlock()
}
func (mb *mockBackend) TryPublishInbound(msg bus.InboundMessage) bool {
mb.mu.Lock()
mb.inbound = append(mb.inbound, msg)
mb.mu.Unlock()
return true
}
func (mb *mockBackend) BuildBlockerResultsSummary(_ context.Context, _ *store.TeamTaskData) string { return "" }
func (mb *mockBackend) BuildRecentCommentsSummary(_ context.Context, _ uuid.UUID) string { return "" }
func (mb *mockBackend) RestoreTraceContext(ctx context.Context, _ *store.TeamTaskData) context.Context {
return ctx
}
func (mb *mockBackend) FollowupDelayMinutes(_ *store.TeamData) int { return 30 }
func (mb *mockBackend) FollowupMaxReminders(_ *store.TeamData) int { return 0 }
func (mb *mockBackend) DataDir() string { return "/tmp/test" }
// ============================================================
// newTestTeamSetup — standard test fixture
// ============================================================
var (
testTeamID = uuid.MustParse("00000000-0000-0000-0000-000000000001")
testLeadID = uuid.MustParse("00000000-0000-0000-0000-000000000002")
testMemberID = uuid.MustParse("00000000-0000-0000-0000-000000000003")
testMember2ID = uuid.MustParse("00000000-0000-0000-0000-000000000004")
testTenantID = uuid.MustParse("00000000-0000-0000-0000-000000000099")
)
func newTestTeamSetup() (*mockBackend, *TeamTasksTool, uuid.UUID, uuid.UUID, context.Context) {
team := &store.TeamData{
BaseModel: store.BaseModel{ID: testTeamID},
Name: "test-team",
LeadAgentID: testLeadID,
Status: store.TeamStatusActive,
}
leadAgent := &store.AgentData{
BaseModel: store.BaseModel{ID: testLeadID},
AgentKey: "lead-agent",
DisplayName: "Lead Agent",
}
memberAgent := &store.AgentData{
BaseModel: store.BaseModel{ID: testMemberID},
AgentKey: "member-agent",
DisplayName: "Member Agent",
}
member2Agent := &store.AgentData{
BaseModel: store.BaseModel{ID: testMember2ID},
AgentKey: "member2-agent",
DisplayName: "Member 2 Agent",
}
members := []store.TeamMemberData{
{TeamID: testTeamID, AgentID: testLeadID, Role: store.TeamRoleLead, AgentKey: "lead-agent"},
{TeamID: testTeamID, AgentID: testMemberID, Role: store.TeamRoleMember, AgentKey: "member-agent"},
{TeamID: testTeamID, AgentID: testMember2ID, Role: store.TeamRoleMember, AgentKey: "member2-agent"},
}
mb := newMockBackend(team, members, []*store.AgentData{leadAgent, memberAgent, member2Agent})
tool := NewTeamTasksTool(mb, FullTeamPolicy{})
ctx := context.Background()
ctx = store.WithTenantID(ctx, testTenantID)
ctx = store.WithAgentID(ctx, testLeadID)
ctx = WithToolChannel(ctx, ChannelDashboard)
ctx = WithToolChatID(ctx, testTeamID.String())
ctx = WithTaskActionFlags(ctx, &TaskActionFlags{})
return mb, tool, testLeadID, testMemberID, ctx
}