From 72d5135bf08136c641c5ec7bcfd04bda3a076d37 Mon Sep 17 00:00:00 2001 From: Bohan Jiang <52446949+Bohan-J@users.noreply.github.com> Date: Thu, 30 Apr 2026 13:19:34 +0800 Subject: [PATCH] fix(quick-create): subscribe requester to issues created via quick-create (#1924) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The agent runs the daemon CLI, so issue.creator_type is `agent` and the issue:created event listener only auto-subscribes the agent — not the human requester. Result: the requester gets a single completion inbox item but never sees follow-up comments or updates on their own issue. Subscribe the requester (reason=`creator`, the only matching value allowed by issue_subscriber's CHECK constraint without a migration) inside notifyQuickCreateCompleted, after the issue lookup succeeds and before the inbox write. Best-effort: log on failure, don't block the inbox. On success, publish subscriber:added so the UI stays in sync with manual subscribe and the listener-driven path. Adds two integration tests in cmd/server: success path subscribes the requester; failure path (agent finished without creating an issue) leaves no subscriber rows. Co-authored-by: multica-agent --- .../server/quick_create_subscriber_test.go | 146 ++++++++++++++++++ server/internal/service/task.go | 33 ++++ 2 files changed, 179 insertions(+) create mode 100644 server/cmd/server/quick_create_subscriber_test.go diff --git a/server/cmd/server/quick_create_subscriber_test.go b/server/cmd/server/quick_create_subscriber_test.go new file mode 100644 index 0000000000..d94325a761 --- /dev/null +++ b/server/cmd/server/quick_create_subscriber_test.go @@ -0,0 +1,146 @@ +package main + +import ( + "context" + "testing" + + "github.com/jackc/pgx/v5/pgtype" + "github.com/multica-ai/multica/server/internal/events" + "github.com/multica-ai/multica/server/internal/service" + "github.com/multica-ai/multica/server/internal/util" + db "github.com/multica-ai/multica/server/pkg/db/generated" +) + +// TestQuickCreateCompletion_SubscribesRequester locks in the fix for the +// quick-create requester not being subscribed to the issue: the agent runs +// the CLI and is recorded as the issue's creator, so the issue:created event +// only auto-subscribes the agent. The completion path must explicitly +// subscribe the human requester so they receive follow-up notifications. +func TestQuickCreateCompletion_SubscribesRequester(t *testing.T) { + ctx := context.Background() + queries := db.New(testPool) + bus := events.New() + taskSvc := service.NewTaskService(queries, testPool, nil, bus) + + var agentID string + if err := testPool.QueryRow(ctx, + `SELECT id::text FROM agent WHERE workspace_id = $1 ORDER BY created_at ASC LIMIT 1`, + testWorkspaceID, + ).Scan(&agentID); err != nil { + t.Fatalf("load fixture agent: %v", err) + } + + task, err := taskSvc.EnqueueQuickCreateTask(ctx, + parseUUID(testWorkspaceID), + parseUUID(testUserID), + parseUUID(agentID), + "please file a bug", + ) + if err != nil { + t.Fatalf("EnqueueQuickCreateTask: %v", err) + } + t.Cleanup(func() { + testPool.Exec(context.Background(), `DELETE FROM agent_task_queue WHERE id = $1`, task.ID) + }) + + if _, err := testPool.Exec(ctx, + `UPDATE agent_task_queue SET status = 'dispatched', dispatched_at = now() WHERE id = $1`, + task.ID, + ); err != nil { + t.Fatalf("dispatch task: %v", err) + } + if _, err := queries.StartAgentTask(ctx, task.ID); err != nil { + t.Fatalf("StartAgentTask: %v", err) + } + + number, err := queries.IncrementIssueCounter(ctx, parseUUID(testWorkspaceID)) + if err != nil { + t.Fatalf("IncrementIssueCounter: %v", err) + } + issue, err := queries.CreateIssueWithOrigin(ctx, db.CreateIssueWithOriginParams{ + WorkspaceID: parseUUID(testWorkspaceID), + Title: "agent-filed bug", + Status: "todo", + Priority: "none", + CreatorType: "agent", + CreatorID: parseUUID(agentID), + Number: number, + OriginType: pgtype.Text{String: "quick_create", Valid: true}, + OriginID: task.ID, + }) + if err != nil { + t.Fatalf("CreateIssueWithOrigin: %v", err) + } + t.Cleanup(func() { + testPool.Exec(context.Background(), `DELETE FROM issue WHERE id = $1`, issue.ID) + }) + + if _, err := taskSvc.CompleteTask(ctx, task.ID, []byte(`{"output":"done"}`), "", ""); err != nil { + t.Fatalf("CompleteTask: %v", err) + } + + if !isSubscribed(t, queries, util.UUIDToString(issue.ID), "member", testUserID) { + t.Fatal("expected requester to be subscribed after quick-create completion") + } +} + +// TestQuickCreateFailure_DoesNotSubscribeRequester confirms the failure path +// (agent finished without producing an issue) does not invent a subscriber +// row — there is nothing to subscribe to. +func TestQuickCreateFailure_DoesNotSubscribeRequester(t *testing.T) { + ctx := context.Background() + queries := db.New(testPool) + bus := events.New() + taskSvc := service.NewTaskService(queries, testPool, nil, bus) + + var agentID string + if err := testPool.QueryRow(ctx, + `SELECT id::text FROM agent WHERE workspace_id = $1 ORDER BY created_at ASC LIMIT 1`, + testWorkspaceID, + ).Scan(&agentID); err != nil { + t.Fatalf("load fixture agent: %v", err) + } + + task, err := taskSvc.EnqueueQuickCreateTask(ctx, + parseUUID(testWorkspaceID), + parseUUID(testUserID), + parseUUID(agentID), + "another bug", + ) + if err != nil { + t.Fatalf("EnqueueQuickCreateTask: %v", err) + } + t.Cleanup(func() { + testPool.Exec(context.Background(), `DELETE FROM agent_task_queue WHERE id = $1`, task.ID) + }) + + if _, err := testPool.Exec(ctx, + `UPDATE agent_task_queue SET status = 'dispatched', dispatched_at = now() WHERE id = $1`, + task.ID, + ); err != nil { + t.Fatalf("dispatch task: %v", err) + } + if _, err := queries.StartAgentTask(ctx, task.ID); err != nil { + t.Fatalf("StartAgentTask: %v", err) + } + + // No issue with origin_type=quick_create + this task id exists. Completion + // hits the failure branch and writes a failure inbox; no subscriber row. + if _, err := taskSvc.CompleteTask(ctx, task.ID, []byte(`{"output":"done"}`), "", ""); err != nil { + t.Fatalf("CompleteTask: %v", err) + } + + var leaked int + if err := testPool.QueryRow(ctx, ` + SELECT COUNT(*) + FROM issue_subscriber s + JOIN issue i ON i.id = s.issue_id + WHERE s.user_type = 'member' AND s.user_id = $1 + AND i.origin_type = 'quick_create' AND i.origin_id = $2 + `, testUserID, task.ID).Scan(&leaked); err != nil { + t.Fatalf("count leaked subscribers: %v", err) + } + if leaked != 0 { + t.Fatalf("expected no subscriber rows for failed quick-create, got %d", leaked) + } +} diff --git a/server/internal/service/task.go b/server/internal/service/task.go index 40272f4551..ea17ff7127 100644 --- a/server/internal/service/task.go +++ b/server/internal/service/task.go @@ -1421,6 +1421,39 @@ func (s *TaskService) notifyQuickCreateCompleted(ctx context.Context, task db.Ag "error", err, ) } + + // Subscribe the requester so they receive notifications for follow-up + // comments and updates. The DB row's creator_type/creator_id is the + // agent (it ran the CLI), but the human who triggered the quick-create + // is the semantic creator from a UX perspective — without this they + // only see the one-shot completion inbox and miss everything after. + // Best-effort: log on failure but don't block the inbox notification. + if err := s.Queries.AddIssueSubscriber(ctx, db.AddIssueSubscriberParams{ + IssueID: issue.ID, + UserType: "member", + UserID: requesterID, + Reason: "creator", + }); err != nil { + slog.Warn("quick-create completion: subscribe requester failed", + "task_id", util.UUIDToString(task.ID), + "issue_id", util.UUIDToString(issue.ID), + "requester_id", qc.RequesterID, + "error", err, + ) + } else { + s.Bus.Publish(events.Event{ + Type: protocol.EventSubscriberAdded, + WorkspaceID: qc.WorkspaceID, + ActorType: "agent", + ActorID: util.UUIDToString(task.AgentID), + Payload: map[string]any{ + "issue_id": util.UUIDToString(issue.ID), + "user_type": "member", + "user_id": qc.RequesterID, + "reason": "creator", + }, + }) + } prefix := s.getIssuePrefix(workspaceID) identifier := fmt.Sprintf("%s-%d", prefix, issue.Number) details, _ := json.Marshal(map[string]any{