mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-10 10:26:46 +08:00
* 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
140 lines
6.5 KiB
Go
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)
|
|
}
|
|
}
|