Files
multica/server/cmd/server/quick_create_subscriber_test.go
Bohan Jiang c8ab73d38d MUL-3244: Bind quick-create attachments to created issues (#4062)
* fix: bind quick-create attachments to created issues

Co-authored-by: multica-agent <github@multica.ai>

* test: use real image markdown in quick-create attachment test

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-12 16:45:38 +08:00

155 lines
4.8 KiB
Go

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),
pgtype.UUID{},
"please file a bug",
pgtype.UUID{},
pgtype.UUID{},
nil,
)
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),
pgtype.UUID{},
"another bug",
pgtype.UUID{},
pgtype.UUID{},
nil,
)
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)
}
}