You can have your shell commands be idempotent. For instance, I have a number of places where I trigger a shell command only if an Ansible managed file changes.
It's only non-idempotent if the shell command runs every time, and the shell command itself isn't idempotent.
The reason for some of the "pointless" modules is that they integrate into Ansible better. I.e. for synchronize, you can run Ansible with the "--check" option, which won't change anything, but will report back what would be changed if you actually executed it. Running rsync in a shell command doesn't have that option because Ansible has no idea what that command does.
My bigger issue with Ansible is that the YAML is annoying. The looping behavior is frustrating, variable precedence is weird, IDE autocompletion is generally lacking. I wish they had designed a sane way to just write Python code.