-
Notifications
You must be signed in to change notification settings - Fork 2
feat: treat 'already in desired state' API errors as success (idempotent operations) #129
Description
Problem
When scripting with slck, operations like adding a reaction that already exists return errors from the Slack API (e.g. already_reacted). These aren't real failures — the desired state is already achieved. This causes unnecessary error noise in automation.
slck react :eyes: failed: Error: add reaction :eyes:: slack API error: already_reacted
Semantically, these operations are like PUT — "ensure this state exists." If the state already exists, that's success, not failure.
Operations to make idempotent
| Command | Slack Error Code | Current Behavior | Desired Behavior |
|---|---|---|---|
messages react |
already_reacted |
Returns error | Print Already reacted with :<emoji>: and exit 0 |
messages unreact |
no_reaction |
Returns error | Print No :<emoji>: reaction to remove and exit 0 |
channels archive |
already_archived |
Returns error with hint | Print Channel already archived: <channel> and exit 0 |
channels unarchive |
not_archived |
Returns error with hint | Print Channel not archived: <channel> and exit 0 |
channels invite |
already_in_channel |
Returns error | Print User(s) already in channel <channel> and exit 0 (single-user invites only) |
Excluded: messages delete + message_not_found — ambiguous (wrong timestamp vs already deleted).
Implementation plan
1. Add IsSlackError helper — internal/client/errors.go
Add a reusable helper to standardize error code checking (replaces ad-hoc strings.Contains usage):
func IsSlackError(err error, code string) bool {
if err == nil {
return false
}
return strings.Contains(err.Error(), code)
}Also add missing error hints for already_reacted, no_reaction, and already_in_channel to the errorHints map (useful if these errors surface in other contexts).
2. Update command files
Handle the idempotent error before calling WrapError, print a descriptive message, and return nil:
internal/cmd/messages/react.go— checkalready_reactedafterc.AddReaction()internal/cmd/messages/unreact.go— checkno_reactionafterc.RemoveReaction()internal/cmd/channels/archive.go— checkalready_archivedafterc.ArchiveChannel()internal/cmd/channels/unarchive.go— checknot_archivedafterc.UnarchiveChannel(). Also refactor existingnot_in_channelcheck to useIsSlackErrorinternal/cmd/channels/invite.go— checkalready_in_channelafterc.InviteToChannel()(only whenlen(userIDs) == 1). Also add missingWrapErrorcall for non-idempotent errors
3. Add tests
internal/cmd/messages/messages_test.go—TestRunReact_AlreadyReacted,TestRunUnreact_NoReactioninternal/cmd/channels/channels_test.go—TestRunArchive_AlreadyArchived,TestRunUnarchive_NotArchived,TestRunInvite_AlreadyInChannelinternal/client/errors_test.go—TestIsSlackError+ new hint entries
4. Update integration-tests.md
Update expected outputs for the idempotent operations.
Edge case: channels invite with multiple users
Slack's conversations.invite API is all-or-nothing. If you invite 2 users and one is already in the channel, Slack fails the entire batch with already_in_channel — the second user does NOT get invited.
Decision: Only treat as idempotent when len(userIDs) == 1. Multi-user invites still surface the error so callers know not everyone was added.
Verification
make test # All tests pass
make lint # No lint errors
make build # Builds cleanly