Files
multica/server/internal/handler/comment_thread_queue_test.go
Jiayuan Zhang 028f3b4437 feat(issues): show agent execution inline in comment threads (#8131)
* feat(issues): show agent runs inline with comment replies

* fix(issues): unify run comment projection and preserve disclosure focus

* refactor(issues): replace nested run cards with inline status rows

* fix(issues): align thread roots with reply content

* style(issues): remove thread toggle background

* style(issues): group reaction picker with comment actions

* feat(issues): show readable issue references in run summaries

* style(issues): move completed run logs into comment headers

* style(issues): balance comment block vertical spacing

* style(issues): shimmer live agent progress text

* style(issues): unify comment action hover feedback

* style(issues): reveal comment actions on block hover

* fix(issues): move published reply activity into the header

* fix(issues): settle comment UI as soon as replies are published

* fix(issues): reveal comment log buttons on hover

* fix(issues): defer run placement until trigger comments arrive

* fix(issues): isolate agent comment queues by thread

* fix(issues): display pending runs in execution order

* fix(ci): align integration tests with thread-scoped queues

* fix(issues): fit run actions to narrow comment columns
2026-09-08 12:58:16 +08:00

145 lines
5.8 KiB
Go

package handler
import (
"context"
"errors"
"testing"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
"github.com/multica-ai/multica/server/internal/testutil"
db "github.com/multica-ai/multica/server/pkg/db/generated"
)
func TestCommentThreadQueuesMergeAndExecuteIndependently(t *testing.T) {
ctx := context.Background()
runtimeID := dbfx.Runtime(t, "thread queue runtime")
agentID := dbfx.Agent(t, "thread queue agent", runtimeID)
issueID := dbfx.Issue(t, "thread queue issue", testutil.Cols{"assignee_type": "agent", "assignee_id": agentID})
rootA := dbfx.Comment(t, issueID, "thread A")
rootB := dbfx.Comment(t, issueID, "thread B")
replyA := dbfx.Comment(t, issueID, "A supplement", testutil.Cols{"parent_id": rootA})
nestedA := dbfx.Comment(t, issueID, "A nested supplement", testutil.Cols{"parent_id": replyA})
t.Cleanup(func() { dbfx.Exec(t, "DELETE FROM agent_task_queue WHERE issue_id = $1", issueID) })
issue, err := testHandler.Queries.GetIssue(ctx, parseUUID(issueID))
if err != nil {
t.Fatal(err)
}
agent, err := testHandler.Queries.GetAgent(ctx, parseUUID(agentID))
if err != nil {
t.Fatal(err)
}
enqueue := func(commentID string, pending bool, want DispatchStatus) {
t.Helper()
result := testHandler.enqueueCommentAgentTriggers(ctx, issue, parseUUID(commentID),
[]commentAgentTrigger{{Agent: agent, Source: commentTriggerSourceMentionAgent, AlreadyPending: pending}})
if result[agentID].status != want {
t.Fatalf("enqueue %s: got %+v, want %s", commentID, result[agentID], want)
}
}
enqueue(rootA, false, DispatchQueued)
// Even a stale pending hint from another thread must not merge or defer B.
enqueue(rootB, true, DispatchQueued)
enqueue(replyA, true, DispatchCoalesced)
enqueue(nestedA, true, DispatchCoalesced)
tasks, err := testHandler.Queries.ListTasksByIssue(ctx, parseUUID(issueID))
if err != nil {
t.Fatal(err)
}
if len(tasks) != 2 {
t.Fatalf("got %d runs, want one per thread", len(tasks))
}
var a, b db.AgentTaskQueue
for _, task := range tasks {
switch uuidToString(task.CommentThreadID) {
case rootA:
a = task
case rootB:
b = task
default:
t.Fatalf("wrong thread scope: %s", uuidToString(task.CommentThreadID))
}
}
if uuidToString(a.TriggerCommentID) != nestedA || len(a.CoalescedCommentIds) != 2 || len(b.CoalescedCommentIds) != 0 {
t.Fatalf("wrong input batches: A=%+v B=%+v", a.TriggerCommentID, b.CoalescedCommentIds)
}
claim := db.ClaimAgentTaskParams{AgentID: parseUUID(agentID), RuntimeID: parseUUID(runtimeID), PrepareLeaseSecs: 60, RuntimeStaleSecs: 120}
claimed, err := testHandler.Queries.ClaimAgentTask(ctx, claim)
if err != nil {
t.Fatal(err)
}
if claimed.ID != a.ID {
t.Fatal("oldest thread should execute first")
}
if _, err := testHandler.Queries.ClaimAgentTask(ctx, claim); !errors.Is(err, pgx.ErrNoRows) {
t.Fatalf("another thread ran concurrently on the same issue: %v", err)
}
// A new instruction after execution starts belongs to a successor run.
dbfx.Exec(t, "UPDATE agent_task_queue SET status='running', started_at=now(), delivered_comment_ids=ARRAY[$2::uuid,$3::uuid,$4::uuid] WHERE id=$1", uuidToString(a.ID), rootA, replyA, nestedA)
followupA := dbfx.Comment(t, issueID, "A follow-up", testutil.Cols{"parent_id": rootA})
enqueue(followupA, false, DispatchQueued)
dbfx.Exec(t, "UPDATE agent_task_queue SET status='completed', completed_at=now() WHERE id=$1", uuidToString(a.ID))
completed, err := testHandler.Queries.GetAgentTask(ctx, a.ID)
if err != nil {
t.Fatal(err)
}
testHandler.reconcileCommentsOnCompletion(ctx, &completed)
tasks, err = testHandler.Queries.ListTasksByIssue(ctx, parseUUID(issueID))
if err != nil {
t.Fatal(err)
}
if len(tasks) != 3 {
t.Fatalf("completion duplicated another thread: got %d tasks", len(tasks))
}
// A row-specific rerun replaces only A's pending successor.
rerun, err := testHandler.TaskService.RerunIssue(ctx, issue.ID, a.ID, pgtype.UUID{}, parseUUID(testUserID), nil)
if err != nil {
t.Fatal(err)
}
other, err := testHandler.Queries.GetAgentTask(ctx, b.ID)
if err != nil || other.Status != "queued" {
t.Fatalf("rerun disturbed thread B: %s / %v", other.Status, err)
}
if rerun.CommentThreadID != a.CommentThreadID {
t.Fatal("rerun lost thread scope")
}
if _, err := testHandler.TaskService.CancelTask(ctx, rerun.ID); err != nil {
t.Fatal(err)
}
other, err = testHandler.Queries.GetAgentTask(ctx, b.ID)
if err != nil || other.Status != "queued" {
t.Fatalf("stop disturbed thread B: %s / %v", other.Status, err)
}
}
func TestDeferredCommentThreadsPromoteIndependently(t *testing.T) {
ctx := context.Background()
runtimeID := dbfx.Runtime(t, "thread promotion runtime")
agentID := dbfx.Agent(t, "thread promotion agent", runtimeID)
issueID := dbfx.Issue(t, "thread promotion issue")
rootA := dbfx.Comment(t, issueID, "A")
rootB := dbfx.Comment(t, issueID, "B")
create := func(root string) string {
return dbfx.Task(t, agentID, testutil.Cols{"runtime_id": runtimeID, "issue_id": issueID, "trigger_comment_id": root, "status": "deferred", "fire_at": testutil.Raw("now()-interval '1 minute'")})
}
a, b := create(rootA), create(rootB)
anotherA := create(rootA)
promoted, err := testHandler.Queries.PromoteDueDeferredTasksForRuntime(ctx, db.PromoteDueDeferredTasksForRuntimeParams{RuntimeID: parseUUID(runtimeID), RuntimeStaleSecs: 120})
if err != nil {
t.Fatal(err)
}
if len(promoted) != 2 {
t.Fatalf("promoted %d runs, want one per thread", len(promoted))
}
for _, id := range []string{a, b} {
row, err := testHandler.Queries.GetAgentTask(ctx, parseUUID(id))
if err != nil || row.Status != "queued" {
t.Fatalf("thread did not promote: %s / %v", row.Status, err)
}
}
row, err := testHandler.Queries.GetAgentTask(ctx, parseUUID(anotherA))
if err != nil || row.Status != "deferred" {
t.Fatalf("second run in A must stay deferred: %s / %v", row.Status, err)
}
}