fix: preserve operation dry-run semantics - #43
Conversation
apgiorgi
left a comment
There was a problem hiding this comment.
The mechanism here is sound and I verified the premise: the live spec declares dryRun on exactly two operations — resendInvite (openapi.yaml:3926) and cancelInvite (:3990). restish kebab-cases it (cli/param.go:99, cli/operation.go:147), it lands in sub.Flags() where it shadows dciCmd.PersistentFlags().Bool("dry-run", ...) (main.go:1370), and main.go:1414 therefore binds agent-dry-run from the operation's own flag. The LocalNonPersistentFlags() predicate is the correct discriminator — it includes an operation-local shadow and excludes the inherited root pflag, so non-native commands keep the local preview.
Two things block it.
1. Simulation becomes indistinguishable from a real call.
The early return at destructive_contract.go:131-133 emits no dry-run marker and leaves destructiveActionName empty, so installDestructiveActionSummaryGuard never wraps the response either. openapi.yaml:3993 explicitly says "The response shape is identical to a real execution."
So dci cancel-invite ID --dry-run --idempotency-key K and a real cancel produce byte-identical output. An agent cannot tell a simulation from a commit, and --yes is silently no longer required on either. Whatever we do here has to leave a marker in the output.
2. The docs become false, and the documented invocation now fails.
README.md:127 and skills/dci-cli/SKILL.md:43 both promise --dry-run shows what would happen "without sending the API request" / "without sending its request". That's now untrue for these two operations.
Worse: Idempotency-Key is required: true on both (openapi.yaml:3915, 3979) and restish never auto-supplies it. So an agent following SKILL.md runs dci cancel-invite ID --dry-run and gets an HTTP 400 where main returned exit 0. Needs the doc updates, and ideally a synthesized idempotency key on the dry-run path so the documented invocation actually works.
Nits
command_catalog.go:233— the dedup drops the "requires--yes" framing, and sincecommandCatalogFlaghas noRequiredfield,--idempotency-keyis still advertised as optional.- The new test lacks
t.Cleanup(resetDestructiveContractState)(the pattern elsewhere in that file) and never asserts thatdryRunactually reaches the query string — which is the whole point of the change.
Also worth knowing for sequencing: this early return bypasses ensureDestructiveOperations() entirely, so the caching in #44 gains nothing on this path.
Please run a live read-only check against both operations before this merges.
|
Addressed both blockers and the catalog/test nits. API-native simulations are visibly wrapped as |
…ry-run # Conflicts: # destructive_contract.go
|
All requested dry-run changes are present on current |
Summary
--dry-runflagaction.status: "simulated"anddry_run: trueWhy
Follow-up to Alfredo's review comment on #34.
cancel-inviteandresend-inviteexpose server-side simulation, but the generic preview shadowed them; simply deferring made simulated and real responses indistinguishable.Test methods
Both live API-native simulations completed successfully and returned
action.status: "simulated"withdry_run: true; no real invite mutation was requested. Unit tests also verify flag propagation, generated/preserved idempotency keys, the native execution path, and the response marker.Could this break things?
Risk: low-medium. Only operations declaring their own
dryRunparameter send a simulation request. All other commands retain the no-request preview. A real destructive execution still requires explicit confirmation.Jira
CMP-48647