Files
multica/server/internal/handler/comment_merge_failclosed_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

140 lines
6.5 KiB
Go

package handler
import (
"context"
"testing"
"github.com/jackc/pgx/v5/pgtype"
db "github.com/multica-ai/multica/server/pkg/db/generated"
)
// TestMergeCommentIntoPendingTask_FailClosedKeepsOriginalSnapshot is Elon's must-fix
// regression exercising the REAL merge path (mergeCommentIntoPendingTask), not just the
// service helper. On a fail-closed workspace, folding a comment that resolves to no
// precise human must REFUSE the merge and leave the queued task's entire attribution
// snapshot (trigger comment, originator, accountable, source) intact — never re-stamped
// to a degraded owner_fallback. A fail-open control proves the same comment DOES merge
// (owner_fallback) when the workspace permits the degrade (MUL-4302).
func TestMergeCommentIntoPendingTask_FailClosedKeepsOriginalSnapshot(t *testing.T) {
ctx := context.Background()
agentID := createWebhookTestAgent(t, "MergeFailClosed Agent")
var runtimeID string
if err := testPool.QueryRow(ctx, `SELECT runtime_id FROM agent WHERE id = $1`, agentID).Scan(&runtimeID); err != nil {
t.Fatalf("load runtime: %v", err)
}
var issueID string
if err := testPool.QueryRow(ctx, `
INSERT INTO issue (workspace_id, title, creator_type, creator_id, assignee_type, assignee_id, priority)
VALUES ($1, 'merge fail-closed', 'member', $2, 'agent', $3, 'medium') RETURNING id`,
testWorkspaceID, testUserID, agentID).Scan(&issueID); err != nil {
t.Fatalf("seed issue: %v", err)
}
t.Cleanup(func() { testPool.Exec(context.Background(), `DELETE FROM issue WHERE id = $1`, issueID) })
// cidA: the precise member comment the queued task is attributed to.
var cidA string
if err := testPool.QueryRow(ctx, `
INSERT INTO comment (issue_id, workspace_id, author_type, author_id, content)
VALUES ($1, $2, 'member', $3, 'please do X') RETURNING id`,
issueID, testWorkspaceID, testUserID).Scan(&cidA); err != nil {
t.Fatalf("seed comment A: %v", err)
}
// cidB: an agent comment with no source-task lineage → resolves to no precise human.
var cidB string
if err := testPool.QueryRow(ctx, `
INSERT INTO comment (issue_id, workspace_id, author_type, author_id, content)
VALUES ($1, $2, 'agent', $3, 'autonomous follow-up') RETURNING id`,
issueID, testWorkspaceID, agentID).Scan(&cidB); err != nil {
t.Fatalf("seed comment B: %v", err)
}
dbfx.Exec(t, `UPDATE comment SET parent_id=$2 WHERE id=$1`, cidB, cidA)
// A QUEUED task with a fully precise snapshot triggered by cidA.
if _, err := testPool.Exec(ctx, `
INSERT INTO agent_task_queue
(agent_id, runtime_id, issue_id, trigger_comment_id, status, priority,
originator_user_id, accountable_user_id, originator_source,
trigger_evidence_kind, trigger_evidence_ref_id)
VALUES ($1, $2, $3, $4, 'queued', 0, $5, $5, 'comment_source', 'comment', $4)`,
agentID, runtimeID, issueID, cidA, testUserID); err != nil {
t.Fatalf("seed queued task: %v", err)
}
agent, err := testHandler.Queries.GetAgent(ctx, parseUUID(agentID))
if err != nil {
t.Fatalf("get agent: %v", err)
}
issue := db.Issue{ID: parseUUID(issueID), WorkspaceID: parseUUID(testWorkspaceID)}
trigger := commentAgentTrigger{Agent: agent, Source: commentTriggerSourceIssueAssignee}
readSnapshot := func() (triggerComment, originator, accountable, source string) {
if err := testPool.QueryRow(ctx, `
SELECT COALESCE(trigger_comment_id::text,''), COALESCE(originator_user_id::text,''),
COALESCE(accountable_user_id::text,''), COALESCE(originator_source,'')
FROM agent_task_queue WHERE issue_id = $1 AND agent_id = $2`,
issueID, agentID).Scan(&triggerComment, &originator, &accountable, &source); err != nil {
t.Fatalf("read snapshot: %v", err)
}
return
}
// ── Fail-closed: merge must be refused, snapshot preserved ──
if _, err := testPool.Exec(ctx, `UPDATE workspace SET attribution_fail_closed = true WHERE id = $1`, testWorkspaceID); err != nil {
t.Fatalf("set fail-closed: %v", err)
}
t.Cleanup(func() {
testPool.Exec(context.Background(), `UPDATE workspace SET attribution_fail_closed = false WHERE id = $1`, testWorkspaceID)
})
countTasks := func() int {
var n int
if err := testPool.QueryRow(ctx, `SELECT count(*) FROM agent_task_queue WHERE issue_id = $1 AND agent_id = $2`, issueID, agentID).Scan(&n); err != nil {
t.Fatalf("count tasks: %v", err)
}
return n
}
before := countTasks()
if result := testHandler.mergeCommentIntoPendingTask(ctx, issue, trigger, parseUUID(cidB), pgtype.Text{}); result != commentMergeAttributionBlocked {
t.Fatalf("fail-closed merge result = %d, want commentMergeAttributionBlocked (refused, non-success)", result)
}
if after := countTasks(); after != before {
t.Errorf("task count changed %d -> %d on refused merge; a fail-closed refusal must not spawn a task", before, after)
}
// The refused merge must surface as a non-success blocked/attribution_blocked
// outcome, never a fabricated coalesced success.
if status, reason, terminal := commentMergeTerminalOutcome(commentMergeAttributionBlocked); !terminal || status != DispatchBlocked || reason != ReasonAttributionBlocked {
t.Errorf("attribution-blocked outcome = %s/%s (terminal=%v), want blocked/attribution_blocked", status, reason, terminal)
}
tc, orig, acc, src := readSnapshot()
if tc != cidA {
t.Errorf("trigger_comment_id = %s, want unchanged %s (fail-closed must keep the original trigger)", tc, cidA)
}
if orig != testUserID || acc != testUserID {
t.Errorf("originator/accountable = %s/%s, want unchanged %s (fail-closed must not re-stamp)", orig, acc, testUserID)
}
if src != "comment_source" {
t.Errorf("originator_source = %s, want unchanged comment_source (fail-closed must not degrade)", src)
}
// ── Fail-open control: the same comment now merges to owner_fallback ──
if _, err := testPool.Exec(ctx, `UPDATE workspace SET attribution_fail_closed = false WHERE id = $1`, testWorkspaceID); err != nil {
t.Fatalf("clear fail-closed: %v", err)
}
if result := testHandler.mergeCommentIntoPendingTask(ctx, issue, trigger, parseUUID(cidB), pgtype.Text{}); result != commentMergeSucceeded {
t.Fatalf("fail-open merge result = %d, want commentMergeSucceeded", result)
}
tc2, orig2, _, src2 := readSnapshot()
if tc2 != cidB {
t.Errorf("fail-open: trigger_comment_id = %s, want repointed to %s (merge should proceed)", tc2, cidB)
}
if src2 != "owner_fallback" {
t.Errorf("fail-open: originator_source = %s, want owner_fallback (merge proceeded)", src2)
}
if orig2 != "" {
t.Errorf("fail-open owner_fallback: originator = %s, want NULL (authorization carries no human)", orig2)
}
}