mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-02 18:13:27 +02:00
A pending-task merge previously reported success (coalesced) even when it was refused or failed: attribution fail-closed and unknown DB errors both returned handled=true, so the caller recorded coalesced for a merge that never happened. mergeCommentIntoPendingTask now returns a distinguishable commentMergeResult; commentMergeTerminalOutcome maps a real merge to coalesced, a fail-closed refusal to blocked/attribution_blocked, and any other failure to blocked/internal_error. Only "no queued task to fold" falls through to the active-task decision. Adds pure coverage of the mapping plus a fail-closed regression asserting the non-success outcome and unchanged task count. Co-authored-by: multica-agent <github@multica.ai>
106 lines
4.7 KiB
Go
106 lines
4.7 KiB
Go
package handler
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
)
|
|
|
|
// TestDecidePostMergeMiss is Elon round-4 must-fix: the active-task check governs
|
|
// what happens after a comment merge misses. A query FAILURE must fail closed —
|
|
// never enqueue a fresh task (a duplicate concurrent-run risk) and never report a
|
|
// success. Only a confirmed active task defers; a confirmed-none enqueues fresh.
|
|
// Unit-tested here because a real DB fault cannot be forced through valid inputs,
|
|
// and this decision — not the query call — is what actually governs the branch.
|
|
func TestDecidePostMergeMiss(t *testing.T) {
|
|
t.Run("query error: fail closed, non-success internal_error", func(t *testing.T) {
|
|
status, reason, enqueueFresh := decidePostMergeMiss(false, errors.New("db down"))
|
|
if enqueueFresh {
|
|
t.Error("enqueueFresh = true on query error; must fail closed to avoid a duplicate run")
|
|
}
|
|
if status != DispatchBlocked || reason != ReasonInternalError {
|
|
t.Errorf("got %s/%s, want blocked/internal_error", status, reason)
|
|
}
|
|
})
|
|
t.Run("query error dominates a stale active=true", func(t *testing.T) {
|
|
status, _, enqueueFresh := decidePostMergeMiss(true, errors.New("db down"))
|
|
if enqueueFresh || status != DispatchBlocked {
|
|
t.Errorf("got status %s enqueueFresh %v, want blocked + no fresh enqueue", status, enqueueFresh)
|
|
}
|
|
})
|
|
t.Run("active task: defer, no fresh enqueue", func(t *testing.T) {
|
|
status, reason, enqueueFresh := decidePostMergeMiss(true, nil)
|
|
if enqueueFresh || status != DispatchDeferred || reason != ReasonDeferred {
|
|
t.Errorf("got %s/%s enqueueFresh %v, want deferred + no fresh enqueue", status, reason, enqueueFresh)
|
|
}
|
|
})
|
|
t.Run("no active task: enqueue a fresh follow-up", func(t *testing.T) {
|
|
_, _, enqueueFresh := decidePostMergeMiss(false, nil)
|
|
if !enqueueFresh {
|
|
t.Error("enqueueFresh = false with no active task; a fresh follow-up must run")
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestCommentMergeTerminalOutcome is Elon round-5 must-fix: a pending-task merge
|
|
// must report an HONEST public outcome. Only a real merge is coalesced; a
|
|
// fail-closed refusal is blocked/attribution_blocked and any other failure is
|
|
// blocked/internal_error — never a fabricated coalesced success. Only
|
|
// "no queued task to fold into" is non-terminal (the caller runs the active-task
|
|
// decision). Pure-tested because a real DB/attribution fault cannot be forced
|
|
// through valid handler inputs, and this mapping is what governs the outcome.
|
|
func TestCommentMergeTerminalOutcome(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
result commentMergeResult
|
|
wantTerminal bool
|
|
wantStatus DispatchStatus
|
|
wantReason DispatchReasonCode
|
|
}{
|
|
{"real merge coalesces", commentMergeSucceeded, true, DispatchCoalesced, ReasonCoalesced},
|
|
{"attribution fail-closed is blocked, not success", commentMergeAttributionBlocked, true, DispatchBlocked, ReasonAttributionBlocked},
|
|
{"unknown error is blocked, not success", commentMergeError, true, DispatchBlocked, ReasonInternalError},
|
|
{"no pending task defers to active-task decision", commentMergeNoPendingTask, false, "", ""},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
status, reason, terminal := commentMergeTerminalOutcome(tc.result)
|
|
if terminal != tc.wantTerminal {
|
|
t.Errorf("terminal = %v, want %v", terminal, tc.wantTerminal)
|
|
}
|
|
if status != tc.wantStatus || reason != tc.wantReason {
|
|
t.Errorf("got %q/%q, want %q/%q", status, reason, tc.wantStatus, tc.wantReason)
|
|
}
|
|
if tc.wantTerminal && status != DispatchCoalesced && status != DispatchBlocked {
|
|
t.Errorf("terminal outcome %q is neither the coalesced success nor a blocked failure", status)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDecideSuppressedLeaderOutcome: the self-trigger-suppressed squad leader's
|
|
// active-task check must never fake success — a query error is a non-success
|
|
// internal_error, a confirmed active run defers, and a confirmed-none is
|
|
// self_trigger_suppressed (MUL-4525, Elon round 4).
|
|
func TestDecideSuppressedLeaderOutcome(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
active bool
|
|
err error
|
|
wantStatus DispatchStatus
|
|
wantReason DispatchReasonCode
|
|
}{
|
|
{"query error", false, errors.New("db down"), DispatchBlocked, ReasonInternalError},
|
|
{"query error dominates stale active", true, errors.New("db down"), DispatchBlocked, ReasonInternalError},
|
|
{"active run", true, nil, DispatchDeferred, ReasonAlreadyActive},
|
|
{"no active run", false, nil, DispatchBlocked, ReasonSelfTriggerSuppressed},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
status, reason := decideSuppressedLeaderOutcome(tc.active, tc.err)
|
|
if status != tc.wantStatus || reason != tc.wantReason {
|
|
t.Errorf("got %s/%s, want %s/%s", status, reason, tc.wantStatus, tc.wantReason)
|
|
}
|
|
})
|
|
}
|
|
}
|