Skip to content

feat(cli): guard and close request-field gaps in curated commands - #210

Merged
ysyneu merged 12 commits into
mainfrom
feat/incident-merge-remove-source
Oct 9, 2026
Merged

ysyneu merged 12 commits into
mainfrom
feat/incident-merge-remove-source

Conversation

@ysyneu

@ysyneu ysyneu commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Summary

A curated (hand-written) command that takes a generated command's name drops the generated twin (genAddLeaf). Every request field the curated command does not re-declare is then unreachable from the CLI. TestGeneratorTargetsFullSpec only checks coverage per operation, so these gaps were invisible. For example, incident merge could not send remove_source_incidents.

This PR adds a field-level guard and closes every existing gap.

Guard: TestCuratedCommandsCoverRequestFields

The test rebuilds the generated tree on a bare root and finds each curated command that shadows a generated twin (19 today). For every top-level request field of that operation, read from the spec and including allOf/oneOf composition, it requires one of the following:

  • a curated flag with the same kebab-case name, or
  • an entry in curatedFieldRoutes: "--flag" (a renamed flag), "args" (positional), or "omit: <reason>".

Routes are validated too. A routed flag must exist, the field must still be in the spec, there must be no redundant same-name routes, and the command must still shadow a twin. A field the API adds later now fails the test instead of silently disappearing.

Gaps closed

101 gaps across 19 commands. Each one was resolved by adding a flag, routing it to an existing flag or argument, or omitting it with a stated reason.

  • incident list: --incident-ids --acker-ids --closer-ids --creator-ids --responder-ids --team-ids --asc --ever-muted --is-my-channel --is-my-team --is-rare --is-snoozed
  • incident merge: --remove-source-incidents --title --comment-file
  • incident comment: --comment-type-id
  • incident feed: --types --asc
  • alert list: --alert-ids --alert-keys --asc --by-updated-at
  • alert-event list: --asc
  • audit search: --is-write --is-dangerous --request-id
  • change list: --orderby --asc --include-events --filters (JSON array of {key, oper, vals})
  • channel list: --channel-ids --channel-name --is-my-team --is-my-starred --is-my-managed --is-brief --orderby --asc --page --limit
  • field list: --query --creator-id --orderby --asc

Free-text fields an agent might write use --*-file (for example --comment-file), never an inline string. When a new flag is not set, the request body is byte-identical to before.

Behavior change: channel list --name

--name used to filter client-side on the returned page only, so with paging it missed channels on other pages. It now sends the escaped text as the server's query. The match is the same case-insensitive substring, now also over the description. The duplicate --query flag is gone, and the footer prints the server total.

Custom fields and assignment on incident create (go-flashduty v0.15.12)

go-flashduty used to generate CustomFieldValues as struct{}, which dropped every value. That is fixed in flashcatcloud/go-flashduty#90, released as v0.15.12. This PR bumps to that version and adds the following to incident create:

  • --field key=value
  • --assign-emails
  • --escalate-rule-id
  • --escalate-layer

--field values are typed from the field definitions returned by /field/list:

Field type Sent as
checkbox bool
multi_select string array (comma-separated or JSON array)
single_select, text string

Unknown field names are rejected.

Fix: incident update --field

incident update --field sent field_value as {"value": v}, but the server reads the raw value, so every custom-field update was rejected. It now sends the typed value directly. The generated incident ack / incident resolve commands also carry custom_fields after the bump.

Test plan

  • make check passes: fmt, lint (0 issues), test, build
  • make check-cards
  • Wire-body tests for every new flag. Each one checks that the default body is unchanged.
  • Live API: merge with and without --remove-source-incidents. The source is closed and kept without the flag, and deleted (Incident not found) with it.
  • Live API: incident create --field project=n9e,duty控制台 --assign-emails … stored project as an array, and the responder was resolved from the email. incident update --field test_checkbox=false --field afdasf='["123","567"]' --field abc=456 stored a bool, an array, and the string "456".
  • Live API smoke run of the new filters. All were accepted, and the results matched each filter (--incident-ids, --responder-ids, --creator-ids 0, feed --types i_merge, channel list --name page 2, and others).

ysyneu added 8 commits October 8, 2026 19:57
POST /incident/merge accepts remove_source_incidents (default false: the
source incidents are closed and kept with a merge timeline entry; true:
they are deleted). The hand-written `incident merge` command shadows the
generated one and only exposed --source, so the field was unreachable
from the CLI. Expose it as an opt-in bool flag; the default request body
is unchanged.
A curated command that takes a generated command's name drops the
generated twin, and with it every flag the curated one does not
re-declare. Check each shadowed operation's top-level request fields
against the curated flags, with explicit routes for renamed flags,
positional arguments, and deliberately omitted fields.
Flags added:
- alert list: --alert-ids, --alert-keys, --asc, --by-updated-at
- alert-event list: --asc

Remaining request fields are routed to the existing flags that already set
them (--severity, --channel, --integration, --since, --until, --page,
--active, --muted, --integration-type, --comment-file), or omitted with a
stated reason (page cursor, single-valued orderby).
…ands

Flags added:
- audit search: --is-write, --is-dangerous, --request-id
- change list: --orderby, --asc, --include-events, --filters (JSON array of key/oper/vals)
- channel list: --channel-ids, --channel-name, --query, --is-my-team, --is-my-starred, --is-my-managed, --is-brief, --orderby, --asc, --page, --limit
- field list: --query, --creator-id, --orderby, --asc

Every new flag is sent only when set, so default request bodies are unchanged. Differently named existing flags are routed to their wire fields for team list/delete, change list, audit search and channel list.
Flags added:
- incident list: --incident-ids, --acker-ids, --closer-ids, --creator-ids,
  --responder-ids, --team-ids, --asc, --ever-muted, --is-my-channel,
  --is-my-team, --is-rare, --is-snoozed
- incident feed: --asc, --types
- incident merge: --title, --comment-file
- incident comment: --comment-type-id

Fields already carried by an existing flag or positional argument are
declared in curatedFieldRoutes; the two fields that cannot be sent are
recorded there with the reason.
# Conflicts:
#	internal/cli/curated_fields_test.go
channel list --name matched names client-side on the returned page only,
so with paging it missed channels on other pages, and it duplicated the
server-side query filter. Send the escaped text as the server's query
regex instead (same case-insensitive substring semantics, now also over
the description), drop the separate --query flag, and print the server's
total. Also list the valid --orderby values, and state precisely why the
audit and alert-event cursor/order fields are not exposed.
@ysyneu ysyneu changed the title feat(incident): add --remove-source-incidents to incident merge feat(cli): guard and close request-field gaps in curated commands Oct 9, 2026
ysyneu added 4 commits October 8, 2026 21:04
- incident create: --field key=value (now sendable with go-flashduty
  v0.15.12), --assign-emails, --escalate-rule-id, --escalate-layer.
- --field values are converted by the field's type from /field/list:
  checkbox -> bool, multi_select -> string array (comma-separated or a
  JSON array), single_select/text -> string. Unknown names are rejected.
- incident update --field sent field_value as {"value": v}; the server
  takes the raw value, so every update was rejected. Send the typed
  value directly.
- Test helper: reset every slice-valued flag between runs, not only
  string slices, so int slices no longer leak across tests.
The layer index only applies to an escalation rule; on its own the flag
was silently ignored and the incident was created unassigned.
@ysyneu
ysyneu merged commit ff6f7d7 into main Oct 9, 2026
12 checks passed
@ysyneu
ysyneu deleted the feat/incident-merge-remove-source branch October 9, 2026 04:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant