diff --git a/internal/shortcut/chatmsg/card_update.go b/internal/shortcut/chatmsg/card_update.go index 6aad864b..ac4dc6f1 100644 --- a/internal/shortcut/chatmsg/card_update.go +++ b/internal/shortcut/chatmsg/card_update.go @@ -131,9 +131,19 @@ func observeCardUpdateMap(value map[string]any, observation *cardUpdateObservati } } } + errorCode, hasErrorCode := value["errorCode"] + errorCodeEmpty := hasErrorCode && cardUpdateErrorCodeEmpty(errorCode) + if hasErrorCode && !errorCodeEmpty { + setNegativeCardUpdateEvidence(observation, "errorCode=non-empty") + } if success, ok := value["success"].(bool); ok { if success { - setPositiveCardUpdateEvidence(observation, "success=true") + // Record success=true only when the same response envelope explicitly + // includes its business-error field. A non-empty code is already + // negative evidence above, so the two signals reject the conflict. + if hasErrorCode { + setPositiveCardUpdateEvidence(observation, "success=true") + } } else { setNegativeCardUpdateEvidence(observation, "success=false") } @@ -149,6 +159,14 @@ func observeCardUpdateMap(value map[string]any, observation *cardUpdateObservati } } +func cardUpdateErrorCodeEmpty(value any) bool { + if value == nil { + return true + } + code, ok := value.(string) + return ok && strings.TrimSpace(code) == "" +} + func setPositiveCardUpdateEvidence(observation *cardUpdateObservation, evidence string) { if observation.positiveEvidence == "" { observation.positiveEvidence = evidence diff --git a/internal/shortcut/chatmsg/card_update_test.go b/internal/shortcut/chatmsg/card_update_test.go index 5b1f9d39..23afae54 100644 --- a/internal/shortcut/chatmsg/card_update_test.go +++ b/internal/shortcut/chatmsg/card_update_test.go @@ -63,6 +63,11 @@ func TestCrossPlatformCoverageVerifyStreamingCardUpdate(t *testing.T) { {name: "conflicting evidence", response: map[string]any{"updated": true, "applied": false}, wantErrIs: ErrCardUpdateUnverified}, {name: "zero affected", response: map[string]any{"affectedCount": 0}, wantErrIs: ErrCardUpdateNotApplied}, {name: "success acknowledgement", response: map[string]any{"success": true, "errorCode": nil}, wantProof: "success=true"}, + {name: "success acknowledgement with empty error code", response: map[string]any{"success": true, "errorCode": " "}, wantProof: "success=true"}, + {name: "success without explicit error code", response: map[string]any{"success": true}, wantErrIs: ErrCardUpdateUnverified}, + {name: "success conflicts with error code", response: map[string]any{"success": true, "errorCode": "InternalError"}, wantErrIs: ErrCardUpdateUnverified}, + {name: "success conflicts with numeric error code", response: map[string]any{"success": true, "errorCode": float64(500)}, wantErrIs: ErrCardUpdateUnverified}, + {name: "error code without success", response: map[string]any{"errorCode": "InternalError"}, wantErrIs: ErrCardUpdateNotApplied}, {name: "failed acknowledgement", response: map[string]any{"success": false}, wantErrIs: ErrCardUpdateNotApplied}, {name: "explicitly not updated", response: map[string]any{"result": map[string]any{"updated": false}}, wantErrIs: ErrCardUpdateNotApplied}, {name: "mismatched id", response: map[string]any{"result": map[string]any{"bizId": "biz-2", "updated": true}}, wantErrIs: ErrCardUpdateBizIDDrift},