Files
multica/server/internal/handler/comment_decision_test.go
J 300a4c6299 fix(comments): honest merge outcome — refused merge is blocked, not fake coalesced (MUL-4525)
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>
2026-07-15 01:56:12 +08:00

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)
}
})
}
}