mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-06 14:00:09 +02:00
* Add comment trigger preview suppression Co-authored-by: multica-agent <github@multica.ai> * Use TanStack Query for trigger preview Co-authored-by: multica-agent <github@multica.ai> * Test note comments skip create triggers Co-authored-by: multica-agent <github@multica.ai> * feat(issues): redesign comment trigger chips as avatar chips Single agent renders as avatar + presence dot + full sentence; several agents collapse to an overlapping stack + active count, mirroring the header working chip. Per-agent skip moves into a click-opened popover (hover layers stay read-only tooltips); suppression reads as brightness, not a ban glyph. Loading and preview errors render nothing. Also: share one tooltip body across chip and popover rows, invalidate cached previews after a comment lands (the enqueued task changes the dedup answer), move the preview query key into issueKeys, and drop the now-unconsumed status field from useCommentTriggerPreview. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(server): drop comment trigger wrappers kept only for tests enqueueMentionedAgentTasks and shouldEnqueueSquadLeaderOnComment had no production callers after the compute/enqueue split — the comment path goes through computeCommentAgentTriggers. Tests now exercise the compute functions directly via package-local helpers, so the legacy adapters cannot drift from the real path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(skills): sync mentioning/squads source maps with shared trigger computation The squads source map still pointed the comment-trigger contract at the pre-refactor call chain (comment.go:940 -> shouldEnqueueSquadLeaderOnComment), and the mentioning skill referenced the deleted wrapper. Re-anchor both to computeCommentAgentTriggers / computeAssignedSquadLeaderCommentTrigger / computeMentionedAgentCommentTriggers with current line numbers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
323 lines
12 KiB
Go
323 lines
12 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
func createCommentTriggerPreviewIssue(t *testing.T, title string, assigneeType, assigneeID string) string {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
|
|
var number int
|
|
if err := testPool.QueryRow(ctx, `
|
|
UPDATE workspace
|
|
SET issue_counter = GREATEST(issue_counter, (SELECT COALESCE(MAX(number), 0) FROM issue WHERE workspace_id = $1)) + 1
|
|
WHERE id = $1 RETURNING issue_counter
|
|
`, testWorkspaceID).Scan(&number); err != nil {
|
|
t.Fatalf("next issue number: %v", err)
|
|
}
|
|
|
|
var assigneeTypeArg any
|
|
var assigneeIDArg any
|
|
if assigneeType != "" {
|
|
assigneeTypeArg = assigneeType
|
|
}
|
|
if assigneeID != "" {
|
|
assigneeIDArg = assigneeID
|
|
}
|
|
|
|
var issueID string
|
|
if err := testPool.QueryRow(ctx, `
|
|
INSERT INTO issue (workspace_id, creator_type, creator_id, title, assignee_type, assignee_id, number)
|
|
VALUES ($1, 'member', $2, $3, $4, $5, $6)
|
|
RETURNING id
|
|
`, testWorkspaceID, testUserID, title, assigneeTypeArg, assigneeIDArg, number).Scan(&issueID); err != nil {
|
|
t.Fatalf("create issue: %v", err)
|
|
}
|
|
|
|
t.Cleanup(func() {
|
|
testPool.Exec(context.Background(), `DELETE FROM agent_task_queue WHERE issue_id = $1`, issueID)
|
|
testPool.Exec(context.Background(), `DELETE FROM comment WHERE issue_id = $1`, issueID)
|
|
testPool.Exec(context.Background(), `DELETE FROM issue WHERE id = $1`, issueID)
|
|
})
|
|
|
|
return issueID
|
|
}
|
|
|
|
func previewCommentTriggersForTest(t *testing.T, issueID string, body map[string]any) CommentTriggerPreviewResponse {
|
|
t.Helper()
|
|
|
|
w := httptest.NewRecorder()
|
|
r := newRequest(http.MethodPost, "/api/issues/"+issueID+"/comments/trigger-preview", body)
|
|
r = withURLParam(r, "id", issueID)
|
|
testHandler.PreviewCommentTriggers(w, r)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("PreviewCommentTriggers: expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var resp CommentTriggerPreviewResponse
|
|
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
|
t.Fatalf("decode preview response: %v", err)
|
|
}
|
|
return resp
|
|
}
|
|
|
|
func postCommentForTriggerPreviewTest(t *testing.T, issueID string, body map[string]any) {
|
|
t.Helper()
|
|
|
|
w := httptest.NewRecorder()
|
|
r := newRequest(http.MethodPost, "/api/issues/"+issueID+"/comments", body)
|
|
r = withURLParam(r, "id", issueID)
|
|
testHandler.CreateComment(w, r)
|
|
if w.Code != http.StatusCreated {
|
|
t.Fatalf("CreateComment: expected 201, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func countQueuedCommentTriggerTasks(t *testing.T, issueID, agentID string) int {
|
|
t.Helper()
|
|
|
|
var n int
|
|
if err := testPool.QueryRow(context.Background(), `
|
|
SELECT count(*) FROM agent_task_queue
|
|
WHERE issue_id = $1 AND agent_id = $2 AND status = 'queued'
|
|
`, issueID, agentID).Scan(&n); err != nil {
|
|
t.Fatalf("count queued tasks: %v", err)
|
|
}
|
|
return n
|
|
}
|
|
|
|
func TestPreviewCommentTriggers_ReturnsMentionedAgentsAndSuppressFiltersCreate(t *testing.T) {
|
|
if testHandler == nil || testPool == nil {
|
|
t.Skip("database not available")
|
|
}
|
|
|
|
agentA := createHandlerTestAgent(t, "Preview Mention A", nil)
|
|
agentB := createHandlerTestAgent(t, "Preview Mention B", nil)
|
|
issueID := createCommentTriggerPreviewIssue(t, "comment trigger preview mentions", "", "")
|
|
content := fmt.Sprintf("[@A](mention://agent/%s) [@B](mention://agent/%s) please inspect", agentA, agentB)
|
|
|
|
preview := previewCommentTriggersForTest(t, issueID, map[string]any{"content": content})
|
|
if got := len(preview.Agents); got != 2 {
|
|
t.Fatalf("expected 2 preview agents, got %d: %+v", got, preview.Agents)
|
|
}
|
|
for _, agent := range preview.Agents {
|
|
if agent.Source != string(commentTriggerSourceMentionAgent) {
|
|
t.Fatalf("preview source = %q, want %q", agent.Source, commentTriggerSourceMentionAgent)
|
|
}
|
|
}
|
|
|
|
postCommentForTriggerPreviewTest(t, issueID, map[string]any{
|
|
"content": content,
|
|
"suppress_agent_ids": []string{agentB},
|
|
})
|
|
|
|
if got := countQueuedCommentTriggerTasks(t, issueID, agentA); got != 1 {
|
|
t.Fatalf("unsuppressed mentioned agent queued tasks = %d, want 1", got)
|
|
}
|
|
if got := countQueuedCommentTriggerTasks(t, issueID, agentB); got != 0 {
|
|
t.Fatalf("suppressed mentioned agent queued tasks = %d, want 0", got)
|
|
}
|
|
}
|
|
|
|
func TestCreateComment_SuppressUnknownAgentIDIsNoop(t *testing.T) {
|
|
if testHandler == nil || testPool == nil {
|
|
t.Skip("database not available")
|
|
}
|
|
|
|
agentID := createHandlerTestAgent(t, "Suppress Noop Agent", nil)
|
|
issueID := createCommentTriggerPreviewIssue(t, "comment trigger suppress noop", "", "")
|
|
content := fmt.Sprintf("[@Agent](mention://agent/%s) please inspect", agentID)
|
|
|
|
postCommentForTriggerPreviewTest(t, issueID, map[string]any{
|
|
"content": content,
|
|
"suppress_agent_ids": []string{
|
|
"00000000-0000-0000-0000-000000000001",
|
|
},
|
|
})
|
|
|
|
if got := countQueuedCommentTriggerTasks(t, issueID, agentID); got != 1 {
|
|
t.Fatalf("mentioned agent queued tasks = %d, want 1", got)
|
|
}
|
|
}
|
|
|
|
func TestPreviewCommentTriggers_NoteReturnsNoAgents(t *testing.T) {
|
|
if testHandler == nil || testPool == nil {
|
|
t.Skip("database not available")
|
|
}
|
|
|
|
agentID := createHandlerTestAgent(t, "Preview Note Agent", nil)
|
|
issueID := createCommentTriggerPreviewIssue(t, "comment trigger note", "agent", agentID)
|
|
content := fmt.Sprintf("/note [@Agent](mention://agent/%s) human-only context", agentID)
|
|
|
|
preview := previewCommentTriggersForTest(t, issueID, map[string]any{"content": content})
|
|
if got := len(preview.Agents); got != 0 {
|
|
t.Fatalf("note preview agents = %d, want 0: %+v", got, preview.Agents)
|
|
}
|
|
}
|
|
|
|
func TestCreateComment_NoteMentionDoesNotQueueAgent(t *testing.T) {
|
|
if testHandler == nil || testPool == nil {
|
|
t.Skip("database not available")
|
|
}
|
|
|
|
agentID := createHandlerTestAgent(t, "Create Note Agent", nil)
|
|
issueID := createCommentTriggerPreviewIssue(t, "comment trigger create note", "agent", agentID)
|
|
content := fmt.Sprintf("/note [@Agent](mention://agent/%s) human-only context", agentID)
|
|
|
|
postCommentForTriggerPreviewTest(t, issueID, map[string]any{"content": content})
|
|
|
|
if got := countQueuedCommentTriggerTasks(t, issueID, agentID); got != 0 {
|
|
t.Fatalf("note create queued tasks = %d, want 0", got)
|
|
}
|
|
}
|
|
|
|
func TestPreviewCommentTriggers_AssigneeAndSuppress(t *testing.T) {
|
|
if testHandler == nil || testPool == nil {
|
|
t.Skip("database not available")
|
|
}
|
|
|
|
agentID := createHandlerTestAgent(t, "Preview Assignee", nil)
|
|
issueID := createCommentTriggerPreviewIssue(t, "comment trigger assignee", "agent", agentID)
|
|
|
|
preview := previewCommentTriggersForTest(t, issueID, map[string]any{"content": "can you continue here?"})
|
|
if got := len(preview.Agents); got != 1 {
|
|
t.Fatalf("expected 1 assignee preview agent, got %d: %+v", got, preview.Agents)
|
|
}
|
|
if preview.Agents[0].ID != agentID {
|
|
t.Fatalf("preview agent id = %s, want %s", preview.Agents[0].ID, agentID)
|
|
}
|
|
if preview.Agents[0].Source != string(commentTriggerSourceIssueAssignee) {
|
|
t.Fatalf("preview source = %q, want %q", preview.Agents[0].Source, commentTriggerSourceIssueAssignee)
|
|
}
|
|
|
|
postCommentForTriggerPreviewTest(t, issueID, map[string]any{
|
|
"content": "can you continue here?",
|
|
"suppress_agent_ids": []string{agentID},
|
|
})
|
|
if got := countQueuedCommentTriggerTasks(t, issueID, agentID); got != 0 {
|
|
t.Fatalf("suppressed assignee queued tasks = %d, want 0", got)
|
|
}
|
|
}
|
|
|
|
func TestPreviewCommentTriggers_AllSuppressesAssigneeAndPendingDedupes(t *testing.T) {
|
|
if testHandler == nil || testPool == nil {
|
|
t.Skip("database not available")
|
|
}
|
|
|
|
ctx := context.Background()
|
|
agentID := createHandlerTestAgent(t, "Preview Dedup Assignee", nil)
|
|
issueID := createCommentTriggerPreviewIssue(t, "comment trigger all pending", "agent", agentID)
|
|
|
|
allPreview := previewCommentTriggersForTest(t, issueID, map[string]any{
|
|
"content": "FYI [@all](mention://all/all)",
|
|
})
|
|
if got := len(allPreview.Agents); got != 0 {
|
|
t.Fatalf("@all preview agents = %d, want 0: %+v", got, allPreview.Agents)
|
|
}
|
|
|
|
if _, err := testPool.Exec(ctx, `
|
|
INSERT INTO agent_task_queue (agent_id, runtime_id, issue_id, status)
|
|
VALUES ($1, $2, $3, 'queued')
|
|
`, agentID, handlerTestRuntimeID(t), issueID); err != nil {
|
|
t.Fatalf("seed queued task: %v", err)
|
|
}
|
|
|
|
pendingPreview := previewCommentTriggersForTest(t, issueID, map[string]any{
|
|
"content": "can you continue here?",
|
|
})
|
|
if got := len(pendingPreview.Agents); got != 0 {
|
|
t.Fatalf("pending preview agents = %d, want 0: %+v", got, pendingPreview.Agents)
|
|
}
|
|
}
|
|
|
|
func TestPreviewCommentTriggers_AssignedSquadLeaderAndSuppress(t *testing.T) {
|
|
if testHandler == nil || testPool == nil {
|
|
t.Skip("database not available")
|
|
}
|
|
|
|
ctx := context.Background()
|
|
leaderID := createHandlerTestAgent(t, "Preview Squad Leader", nil)
|
|
|
|
var squadID string
|
|
if err := testPool.QueryRow(ctx, `
|
|
INSERT INTO squad (workspace_id, name, description, leader_id, creator_id)
|
|
VALUES ($1, $2, '', $3, $4)
|
|
RETURNING id
|
|
`, testWorkspaceID, "Preview Trigger Squad", leaderID, testUserID).Scan(&squadID); err != nil {
|
|
t.Fatalf("create squad: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
testPool.Exec(context.Background(), `DELETE FROM squad WHERE id = $1`, squadID)
|
|
})
|
|
|
|
issueID := createCommentTriggerPreviewIssue(t, "comment trigger squad assignee", "squad", squadID)
|
|
|
|
preview := previewCommentTriggersForTest(t, issueID, map[string]any{"content": "please coordinate this"})
|
|
if got := len(preview.Agents); got != 1 {
|
|
t.Fatalf("expected 1 squad leader preview agent, got %d: %+v", got, preview.Agents)
|
|
}
|
|
if preview.Agents[0].ID != leaderID {
|
|
t.Fatalf("preview leader id = %s, want %s", preview.Agents[0].ID, leaderID)
|
|
}
|
|
if preview.Agents[0].Source != string(commentTriggerSourceIssueAssignee) {
|
|
t.Fatalf("preview source = %q, want %q", preview.Agents[0].Source, commentTriggerSourceIssueAssignee)
|
|
}
|
|
|
|
postCommentForTriggerPreviewTest(t, issueID, map[string]any{
|
|
"content": "please coordinate this",
|
|
"suppress_agent_ids": []string{leaderID},
|
|
})
|
|
if got := countQueuedCommentTriggerTasks(t, issueID, leaderID); got != 0 {
|
|
t.Fatalf("suppressed squad leader queued tasks = %d, want 0", got)
|
|
}
|
|
}
|
|
|
|
func TestPreviewCommentTriggers_MentionedSquadLeaderAndSuppress(t *testing.T) {
|
|
if testHandler == nil || testPool == nil {
|
|
t.Skip("database not available")
|
|
}
|
|
|
|
ctx := context.Background()
|
|
leaderID := createHandlerTestAgent(t, "Preview Mentioned Squad Leader", nil)
|
|
|
|
var squadID string
|
|
if err := testPool.QueryRow(ctx, `
|
|
INSERT INTO squad (workspace_id, name, description, leader_id, creator_id)
|
|
VALUES ($1, $2, '', $3, $4)
|
|
RETURNING id
|
|
`, testWorkspaceID, "Preview Mentioned Trigger Squad", leaderID, testUserID).Scan(&squadID); err != nil {
|
|
t.Fatalf("create squad: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
testPool.Exec(context.Background(), `DELETE FROM squad WHERE id = $1`, squadID)
|
|
})
|
|
|
|
issueID := createCommentTriggerPreviewIssue(t, "comment trigger mentioned squad", "", "")
|
|
content := fmt.Sprintf("[@Squad](mention://squad/%s) please take this", squadID)
|
|
|
|
preview := previewCommentTriggersForTest(t, issueID, map[string]any{"content": content})
|
|
if got := len(preview.Agents); got != 1 {
|
|
t.Fatalf("expected 1 mentioned squad leader preview agent, got %d: %+v", got, preview.Agents)
|
|
}
|
|
if preview.Agents[0].ID != leaderID {
|
|
t.Fatalf("preview leader id = %s, want %s", preview.Agents[0].ID, leaderID)
|
|
}
|
|
if preview.Agents[0].Source != string(commentTriggerSourceMentionSquadLeader) {
|
|
t.Fatalf("preview source = %q, want %q", preview.Agents[0].Source, commentTriggerSourceMentionSquadLeader)
|
|
}
|
|
|
|
postCommentForTriggerPreviewTest(t, issueID, map[string]any{
|
|
"content": content,
|
|
"suppress_agent_ids": []string{leaderID},
|
|
})
|
|
if got := countQueuedCommentTriggerTasks(t, issueID, leaderID); got != 0 {
|
|
t.Fatalf("suppressed mentioned squad leader queued tasks = %d, want 0", got)
|
|
}
|
|
}
|