mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-05 21:39:54 +02:00
fix(issues): inherit trigger provenance + fix cross-issue test (MUL-2457)
Address review feedback on PR #2921: 1. RerunIssue now inherits TriggerCommentID from the source task when sourceTaskID is valid. Without this, a per-row rerun of a comment- or mention-triggered task degrades into a generic issue run because the daemon's buildCommentPrompt path keys on TriggerCommentID. The inherited summary is rebuilt naturally inside the enqueue helpers (buildCommentTriggerSummary derives it from the comment ID). 2. The new cross-issue rejection test inserted a second issue without `number`, hitting uq_issue_workspace_number on a same-workspace collision with the fixture's issue. Both inserts now claim the next available per-workspace number (MAX(number)+1) — matching the pattern used by notification_listeners_test. Added TestRerunIssueInheritsTriggerCommentFromSourceTask to lock the trigger provenance contract. Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -32,9 +32,13 @@ func setupRerunTestFixture(t *testing.T) (string, string, string) {
|
||||
}
|
||||
|
||||
var issueID string
|
||||
// Pick the next per-workspace number to avoid colliding with the
|
||||
// uq_issue_workspace_number unique constraint when multiple fixtures
|
||||
// coexist in the same test (e.g. TestRerunIssueRejectsCrossIssueTask).
|
||||
if err := testPool.QueryRow(ctx, `
|
||||
INSERT INTO issue (workspace_id, title, status, priority, creator_type, creator_id, assignee_type, assignee_id)
|
||||
SELECT $1, 'Rerun test issue', 'todo', 'none', 'member', m.user_id, 'agent', $2
|
||||
INSERT INTO issue (workspace_id, title, status, priority, creator_type, creator_id, assignee_type, assignee_id, number)
|
||||
SELECT $1, 'Rerun test issue', 'todo', 'none', 'member', m.user_id, 'agent', $2,
|
||||
(SELECT COALESCE(MAX(number), 0) + 1 FROM issue WHERE workspace_id = $1)
|
||||
FROM member m WHERE m.workspace_id = $1 LIMIT 1
|
||||
RETURNING id
|
||||
`, testWorkspaceID, agentID).Scan(&issueID); err != nil {
|
||||
@@ -383,11 +387,15 @@ func TestRerunIssueRejectsCrossIssueTask(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Second issue in the same workspace, with a task that does NOT belong
|
||||
// to issue A. The handler must reject this.
|
||||
// to issue A. The handler must reject this. Take the next available
|
||||
// per-workspace number so the uq_issue_workspace_number constraint
|
||||
// (both issues default to number=0 otherwise) doesn't fire before the
|
||||
// rerun assertion can.
|
||||
var issueBID string
|
||||
if err := testPool.QueryRow(ctx, `
|
||||
INSERT INTO issue (workspace_id, title, status, priority, creator_type, creator_id, assignee_type, assignee_id)
|
||||
SELECT $1, 'Rerun cross-issue test', 'todo', 'none', 'member', m.user_id, 'agent', $2
|
||||
INSERT INTO issue (workspace_id, title, status, priority, creator_type, creator_id, assignee_type, assignee_id, number)
|
||||
SELECT $1, 'Rerun cross-issue test', 'todo', 'none', 'member', m.user_id, 'agent', $2,
|
||||
(SELECT COALESCE(MAX(number), 0) + 1 FROM issue WHERE workspace_id = $1)
|
||||
FROM member m WHERE m.workspace_id = $1 LIMIT 1
|
||||
RETURNING id
|
||||
`, testWorkspaceID, agentID).Scan(&issueBID); err != nil {
|
||||
@@ -423,6 +431,78 @@ func TestRerunIssueRejectsCrossIssueTask(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestRerunIssueInheritsTriggerCommentFromSourceTask locks the trigger
|
||||
// provenance contract: a per-row rerun of a comment- or mention-triggered
|
||||
// task must carry the original trigger_comment_id through to the new task.
|
||||
// Otherwise the daemon's buildCommentPrompt path (which keys on
|
||||
// TriggerCommentID) is skipped and the rerun degrades into a generic
|
||||
// issue run that has lost the original comment context — see MUL-2457
|
||||
// review feedback.
|
||||
func TestRerunIssueInheritsTriggerCommentFromSourceTask(t *testing.T) {
|
||||
if testPool == nil {
|
||||
t.Skip("no database connection")
|
||||
}
|
||||
|
||||
issueID, agentID, runtimeID := setupRerunTestFixture(t)
|
||||
t.Cleanup(func() { cleanupRerunFixture(t, issueID) })
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Create a comment to stand in as the original mention / reply trigger.
|
||||
var triggerCommentID string
|
||||
if err := testPool.QueryRow(ctx, `
|
||||
INSERT INTO comment (issue_id, workspace_id, author_type, author_id, content, type)
|
||||
SELECT $1, $2, 'member', m.user_id, 'please retry this', 'comment'
|
||||
FROM member m WHERE m.workspace_id = $2 LIMIT 1
|
||||
RETURNING id
|
||||
`, issueID, testWorkspaceID).Scan(&triggerCommentID); err != nil {
|
||||
t.Fatalf("insert trigger comment: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
testPool.Exec(ctx, `DELETE FROM comment WHERE id = $1`, triggerCommentID)
|
||||
})
|
||||
|
||||
// Source task carries the trigger_comment_id — this is the row whose
|
||||
// retry button the user clicks in the execution log.
|
||||
var sourceTaskID string
|
||||
if err := testPool.QueryRow(ctx, `
|
||||
INSERT INTO agent_task_queue (agent_id, runtime_id, issue_id, status, priority,
|
||||
started_at, completed_at, failure_reason,
|
||||
trigger_comment_id)
|
||||
VALUES ($1, $2, $3, 'failed', 0,
|
||||
now() - interval '1 minute', now() - interval '30 seconds', 'agent_error',
|
||||
$4)
|
||||
RETURNING id
|
||||
`, agentID, runtimeID, issueID, triggerCommentID).Scan(&sourceTaskID); err != nil {
|
||||
t.Fatalf("insert source task: %v", err)
|
||||
}
|
||||
|
||||
queries := db.New(testPool)
|
||||
hub := realtime.NewHub()
|
||||
go hub.Run()
|
||||
bus := events.New()
|
||||
taskService := service.NewTaskService(queries, nil, hub, bus)
|
||||
|
||||
task, err := taskService.RerunIssue(
|
||||
ctx,
|
||||
pgtype.UUID{Bytes: parseUUIDBytes(issueID), Valid: true},
|
||||
pgtype.UUID{Bytes: parseUUIDBytes(sourceTaskID), Valid: true},
|
||||
pgtype.UUID{},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("RerunIssue failed: %v", err)
|
||||
}
|
||||
if task == nil {
|
||||
t.Fatal("RerunIssue returned nil task")
|
||||
}
|
||||
if !task.TriggerCommentID.Valid {
|
||||
t.Fatal("expected per-row rerun to inherit trigger_comment_id from source task, got NULL")
|
||||
}
|
||||
if got := util.UUIDToString(task.TriggerCommentID); got != triggerCommentID {
|
||||
t.Fatalf("trigger_comment_id mismatch: got %s, want %s", got, triggerCommentID)
|
||||
}
|
||||
}
|
||||
|
||||
// TestEnqueueTaskForIssueDoesNotForceFreshSession is the negative control
|
||||
// for the rerun flag: the normal enqueue path must leave the flag false so
|
||||
// auto-retry / comment-triggered tasks keep resuming the prior session
|
||||
|
||||
@@ -1309,7 +1309,12 @@ func (s *TaskService) MaybeRetryFailedTask(ctx context.Context, parent db.AgentT
|
||||
// - sourceTaskID Valid: rerun the agent that ran that task (and reuse its
|
||||
// leader/worker role). This is what the execution log retry button uses
|
||||
// so a per-row retry survives a subsequent assignee change and correctly
|
||||
// re-fires the squad worker or mention agent whose row was clicked.
|
||||
// re-fires the squad worker or mention agent whose row was clicked. The
|
||||
// source task's trigger_comment_id is also inherited (when the caller
|
||||
// didn't pass one) so a per-row rerun of a comment- or mention-triggered
|
||||
// task stays comment-triggered — the daemon's buildCommentPrompt path
|
||||
// keys on TriggerCommentID, and losing it would degrade the rerun into
|
||||
// a generic issue run that no longer carries the original comment.
|
||||
// - sourceTaskID empty: fall back to the issue's current assignee (agent
|
||||
// or squad leader). This preserves the CLI / API contract for callers
|
||||
// that have an issue ID but no specific task to target.
|
||||
@@ -1347,6 +1352,15 @@ func (s *TaskService) RerunIssue(ctx context.Context, issueID pgtype.UUID, sourc
|
||||
}
|
||||
agentID = sourceTask.AgentID
|
||||
isLeader = sourceTask.IsLeaderTask
|
||||
// Inherit trigger provenance so a per-row rerun of a comment- or
|
||||
// mention-triggered task stays a comment-triggered task. Without
|
||||
// this the daemon's buildCommentPrompt path is skipped (it keys on
|
||||
// TriggerCommentID) and the rerun degrades into a generic issue
|
||||
// run that has lost the original comment context. Only override
|
||||
// when the caller didn't pass one explicitly.
|
||||
if !triggerCommentID.Valid && sourceTask.TriggerCommentID.Valid {
|
||||
triggerCommentID = sourceTask.TriggerCommentID
|
||||
}
|
||||
} else {
|
||||
switch {
|
||||
case issue.AssigneeType.String == "agent" && issue.AssigneeID.Valid:
|
||||
|
||||
Reference in New Issue
Block a user