mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-12 19:06:06 +02:00
* fix(server): surface real reason for failed quick-create (MUL-5268, #5885) When an agent's quick-create run finishes without producing an issue, the completion path wrote a fixed "agent finished without creating an issue" inbox, discarding the real reason — most often the active-duplicate guard rejecting the create. Users saw no actionable detail. notifyQuickCreateCompleted now: - distinguishes pgx.ErrNoRows (a confirmed no-issue → real failure) from a genuine lookup fault (DB/timeout), so a transient error no longer mislabels a run that may actually have created the issue; - on the real-failure branch, surfaces the agent's final output as the failure reason. The quick-create prompt already requires the agent to exit with the CLI error as its only output, so this carries the concrete cause (e.g. the existing issue's identifier + status), unescaped, bounded, and redacted. Empty output falls back to the existing generic message. No API/CLI contract or migration change. Co-authored-by: multica-agent <github@multica.ai> * fix(server): never end quick-create with no notification on lookup fault Review follow-up. The previous commit returned silently when the completion lookup failed with a non-ErrNoRows error, to avoid misreporting a failure that was never observed. But the task is already completed and nothing retries this reconciliation, so a single transient DB fault permanently stranded the requester with no inbox result at all. The indeterminate branch now writes a neutral, terminal notification: it does not assert failure (the agent may have created the issue), does not reuse the agent output as if it were the confirmed reason, and points at the one safe next step — check recent issues before retrying, so a retry cannot silently produce the duplicate the guard exists to prevent. notifyQuickCreateFailed / notifyQuickCreateUnconfirmed are now thin wrappers over a shared writer so both outcomes keep the identical row shape and the frontend's 'Edit as advanced form' recovery affordance. Tests: - TestQuickCreateLookupFault_WritesUnconfirmedInbox: fails against the previous commit with 'no rows in result set' (the exact silent-drop), passes now. Uses a DBTX wrapper that faults only GetIssueByOrigin so the inbox write still reaches the real DB. - TestQuickCreateFailure_RedactsAgentOutput: locks in that the newly-surfaced agent output is scrubbed before storage. Co-authored-by: multica-agent <github@multica.ai> * fix(inbox): render unverified quick-create outcome as neutral, not failed Review round 2. Three fixes. 1. Rebased onto main and updated the three CompleteTask call sites for the new sessionRolloutMissing parameter; the branch no longer compiles-fails on the merge ref. 2. The unverified outcome reused the quick_create_failed inbox type, so every client framed it as a failure regardless of the neutral title/body: web list rendered 'Failed: {detail}', web detail showed 'Create with agent failed', mobile rendered 'Failed: ...', and getInboxDisplayTitle replaced the neutral title with the original prompt. Users saw 'Failed: Couldn't confirm...' — asserting a failure never observed. Added a distinct quick_create_unconfirmed type end to end: core type union, web list label (no failure framing), web detail pane, the original-prompt box and 'Edit as advanced form' recovery affordance, mobile label + display title, and en / zh-Hans / ja / ko strings. Older clients hit their existing default branch and render the already-neutral title. 3. The terminal notification reused the caller's context, so a lookup that failed with context.Canceled / DeadlineExceeded failed the write for the same reason and still dropped the notification. The write is now detached via context.WithoutCancel with a bounded timeout. Tests (each verified to fail without its fix): - TestQuickCreateLookupCancelled_StillWritesUnconfirmedInbox: cancels the ctx at the lookup; without the detach, 'no rows in result set'. - inbox-detail-label.test.tsx: resolves accessors against the real en locale; pointing the unconfirmed case back at failed_with_detail reproduces 'Failed: Couldn't confirm...'. - inbox-display.test.ts: both outcomes stay recoverable rows. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai>
82 lines
2.9 KiB
Go
82 lines
2.9 KiB
Go
package service
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestQuickCreateFailureDetail pins the reason-extraction that turns the opaque
|
|
// "agent finished without creating an issue" inbox into the concrete CLI error
|
|
// the quick-create agent actually emitted (GH #5885).
|
|
func TestQuickCreateFailureDetail(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("real CLI error passes through", func(t *testing.T) {
|
|
t.Parallel()
|
|
out := "Error: an active issue already exists: JKY-30 (blocked)."
|
|
result := []byte(`{"output":"` + out + `"}`)
|
|
if got := quickCreateFailureDetail(result); got != out {
|
|
t.Fatalf("expected passthrough of the CLI error\n got: %q\nwant: %q", got, out)
|
|
}
|
|
})
|
|
|
|
t.Run("empty output yields empty so caller uses its generic default", func(t *testing.T) {
|
|
t.Parallel()
|
|
if got := quickCreateFailureDetail([]byte(`{"output":""}`)); got != "" {
|
|
t.Fatalf("expected empty detail for empty output, got %q", got)
|
|
}
|
|
})
|
|
|
|
t.Run("whitespace-only output yields empty", func(t *testing.T) {
|
|
t.Parallel()
|
|
if got := quickCreateFailureDetail([]byte(`{"output":" \n "}`)); got != "" {
|
|
t.Fatalf("expected empty detail for whitespace output, got %q", got)
|
|
}
|
|
})
|
|
|
|
t.Run("missing output field yields empty", func(t *testing.T) {
|
|
t.Parallel()
|
|
if got := quickCreateFailureDetail([]byte(`{"task_id":"abc"}`)); got != "" {
|
|
t.Fatalf("expected empty detail when output absent, got %q", got)
|
|
}
|
|
})
|
|
|
|
t.Run("malformed json yields empty", func(t *testing.T) {
|
|
t.Parallel()
|
|
if got := quickCreateFailureDetail([]byte(`not json`)); got != "" {
|
|
t.Fatalf("expected empty detail for malformed json, got %q", got)
|
|
}
|
|
})
|
|
|
|
t.Run("literal backslash-n is decoded to real newlines", func(t *testing.T) {
|
|
t.Parallel()
|
|
// The daemon may deliver an Output carrying 2-char `\n` sequences; they
|
|
// must render as real line breaks, matching the comment-fallback path.
|
|
result := []byte(`{"output":"line one\\nline two"}`)
|
|
got := quickCreateFailureDetail(result)
|
|
if got != "line one\nline two" {
|
|
t.Fatalf("expected decoded newline, got %q", got)
|
|
}
|
|
})
|
|
|
|
t.Run("oversized output is dropped to a safe generic notice", func(t *testing.T) {
|
|
t.Parallel()
|
|
// A runaway raw-stream dump must not have its tool-trace head surfaced
|
|
// as the failure reason (same concern as GH #5455).
|
|
huge := strings.Repeat("x", maxQuickCreateFailureDetailRunes+1)
|
|
result := []byte(`{"output":"` + huge + `"}`)
|
|
if got := quickCreateFailureDetail(result); got != quickCreateOversizedFailureDetail {
|
|
t.Fatalf("expected oversized notice, got %d-rune body", len(got))
|
|
}
|
|
})
|
|
|
|
t.Run("output exactly at the cap passes through", func(t *testing.T) {
|
|
t.Parallel()
|
|
body := strings.Repeat("x", maxQuickCreateFailureDetailRunes)
|
|
result := []byte(`{"output":"` + body + `"}`)
|
|
if got := quickCreateFailureDetail(result); got != body {
|
|
t.Fatalf("expected body at cap to pass through, got %d runes", len(got))
|
|
}
|
|
})
|
|
}
|