Repository navigation
fix(dynamic): send booleans the caller set to false instead of dropping them - #19
Merged
Merged
Conversation
…ng them
Dynamic commands only forwarded a boolean when it was true:
for propName, val := range boolFlags {
if val != nil && *val { toolArgs[propName] = true }
}
Cobra parses --flag=false and records it as Changed, but the value was
then discarded, making an explicit false byte-identical to omitting the
flag. False could not be transmitted at all, so any tool parameter whose
server-side default is true was stuck on. 15 such tool/flag pairs exist
across the Meta and Google caches, and they failed in both directions:
- delete_catalog_products --dry-run=false stayed in dry run, so the
tool could never execute a deletion through a dynamic command.
- publish_facebook_page_post --published=false, create_collection_ad
and create_instant_experience --is-published=false silently produced
a LIVE published object for a caller who asked for a draft.
- create_google_ads_campaign --target-youtube=false could not opt out
of a network.
Key off cmd.Flags().Changed() and forward the parsed value, so an unset
boolean is still omitted and the server default applies. Both the flag
registration and the change lookup now go through propNameToFlagName, so
the underscore-to-hyphen mapping cannot drift between them.
Argument building is extracted into buildToolArgs and flag registration
into registerToolFlags so the behaviour is unit-testable without network
I/O; the new tests fail against the previous implementation.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
Dynamic commands only forwarded a boolean when it was
true:Cobra parses
--flag=falseand records it asChanged, but the value was then discarded — so an explicitfalsewas byte-identical to omitting the flag.falsecould not be transmitted at all, which means any tool parameter whose server-side default istruewas permanently stuck on.I hit this validating pipeboard.co#2242:
pipeboard meta-ads delete-catalog-products --dry-run=falsecame backdry_run: true, and a deliberately wrong--expected-countproduced a warning instead of a rejection — both dry-run tells. The tool could not be executed through a dynamic command at all.Blast radius
15 tool/flag pairs across the cached Meta and Google schemas declare a
truedefault. They failed in both directions:delete_catalog_products --dry-run,--readbackpublish_facebook_page_post --published=false,create_collection_ad --is-published=false,create_instant_experience --is-published=falsecreate_google_ads_campaign --target-youtube=false,--target-google-search=falseThe unsafe row is the reason this is worth shipping promptly: someone creating a deliberately unpublished post got a live one, with no error.
The fix
Key off
cmd.Flags().Changed()and forward the parsed value. An unset boolean is still omitted, so server-side defaults keep applying — only explicitly-typed flags are now sent.Both flag registration and the change lookup go through a new
propNameToFlagName, so the underscore→hyphen mapping can't drift between them (that mismatch would silently drop every boolean again).Argument building is extracted into
buildToolArgsand flag registration intoregisterToolFlags, so the behavior is unit-testable without network I/O. No behavior change for strings, numbers, arrays, or objects.Note: a matching quirk remains for string flags —
if *val == "" { continue }means an explicit empty string can't be sent either. Left alone deliberately: unlike booleans it has no "default is non-empty" failure mode, and changing it would alter behavior for scripts that pass a possibly-empty shell variable expecting omission.Tests
TestBuildToolArgsBooleans(5 cases),TestBuildToolArgsHyphenatedBooleanFlag,TestBuildToolArgsTypeCoercion. They drive real cobra parsing through the real code path.Verified they actually catch the bug: reverting just the fix hunk makes 3 of them fail —
go build,go vet,gofmt,go test ./...all clean.End-to-end verification
Against a local Next dev server + Python MCP backend, on a Pipeboard-owned ad account, using a disposable
[PIPEBOARD TEST]catalog with one product:--dry-run=false --expected-count 1dry_run: true,deleted: 0— bug reproduced--dry-run)dry_run: true,deleted: 0— default preserved--dry-run=false --expected-count 7--dry-run=false --expected-count 1dry_run: false,deleted: 1, readbackconfirmed_absent: 1,still_present: []Test catalog deleted afterwards; the account's own pre-existing catalog was only ever read and still hashes to the manifest token recorded before the run.