Skip to content

fix(jtk): accumulate repeated --field flags for multi-value fields#186

Merged
rianjs merged 2 commits intomainfrom
fix/166-multi-value-fields
Mar 28, 2026
Merged

fix(jtk): accumulate repeated --field flags for multi-value fields#186
rianjs merged 2 commits intomainfrom
fix/166-multi-value-fields

Conversation

@rianjs
Copy link
Copy Markdown
Contributor

@rianjs rianjs commented Mar 28, 2026

Summary

  • Repeated --field flags with the same key now merge values into an array instead of overwriting
  • Adds MergeFieldValues() function that handles option arrays ([]map[string]string) and string arrays ([]string)
  • Fixes both issues update and issues create commands

Closes #166

Example

# Before: only "Monit Accounting" was set (last value wins)
# After: all three values are set
jtk issues update ON-483 \
  --field "customfield_10154=CheckSync" \
  --field "customfield_10154=MoniCore" \
  --field "customfield_10154=Monit Accounting"

Test plan

  • Unit tests for MergeFieldValues (option arrays, string arrays, non-array overwrite, type mismatch)
  • make build passes
  • make test passes
  • Integration test: update a multi-checkbox field with multiple --field flags

When the same field key is passed multiple times via --field, values are
now merged into an array instead of overwriting. This fixes multi-checkbox
and multi-select fields where multiple values need to be set:

  jtk issues update PROJ-123 \
    --field "customfield_10154=CheckSync" \
    --field "customfield_10154=MoniCore"

Applies to both issues update and issues create.

Closes #166
Copy link
Copy Markdown

@monit-reviewer monit-reviewer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated PR Review

Reviewed commit: 0b24c0a

Approved with 2 non-blocking suggestions below. Address at your discretion.

Summary

Reviewer Findings
harness-engineering:knowledge-reviewer 1
security:code-auditor 1
harness-engineering:knowledge-reviewer (1 findings)

💡 Suggestion - tools/jtk/api/fields.go:138

MergeFieldValues handles []map[string]string and []string but any other slice type (or cross-type mismatch like []map[string]string existing + []string new) silently falls through to replacement. In practice this can't happen because FormatFieldValue is deterministic for a given field, but a brief comment listing the two supported array types and noting the fallthrough contract would help future maintainers understand the design choice.

security:code-auditor (1 findings)

💡 Suggestion - tools/jtk/api/fields_test.go:500

The tests cover single-merge cases well, but the real usage pattern is 3+ --field flags for the same key (as shown in the PR example). Adding a test that chains multiple MergeFieldValues calls (e.g., accumulate three option values) would exercise the append-to-already-appended path and match the motivating use case.


Completed in 5m 50s | $0.78
Field Value
Reviewers hybrid-synthesis, database:sql-reviewer, security:code-auditor, harness-engineering:knowledge-reviewer, harness-engineering:enforcement-reviewer, harness-engineering:architecture-reviewer, harness-engineering:legibility-reviewer
Reviewed by pr-review-daemon · monit-pr-reviewer
Duration 5m 50s (Reviewers: 1m 35s · Synthesis: 1m 11s)
Cost $0.78
Tokens 289.8k in / 31.5k out
Turns 36

// MergeFieldValues merges a new formatted field value into an existing one.
// For array fields (e.g., multi-checkbox, labels), this appends to the existing
// array. For non-array fields, the new value replaces the existing one.
func MergeFieldValues(existing, newVal any) any {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Low (harness-engineering:knowledge-reviewer): MergeFieldValues handles []map[string]string and []string but any other slice type (or cross-type mismatch like []map[string]string existing + []string new) silently falls through to replacement. In practice this can't happen because FormatFieldValue is deterministic for a given field, but a brief comment listing the two supported array types and noting the fallthrough contract would help future maintainers understand the design choice.

})
}

func TestMergeFieldValues(t *testing.T) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Low (security:code-auditor): The tests cover single-merge cases well, but the real usage pattern is 3+ --field flags for the same key (as shown in the PR example). Adding a test that chains multiple MergeFieldValues calls (e.g., accumulate three option values) would exercise the append-to-already-appended path and match the motivating use case.

@rianjs
Copy link
Copy Markdown
Contributor Author

rianjs commented Mar 28, 2026

Addressed the 3+ values test case in 718b048. The comment about supported array types is fair but the function is 20 lines and self-documenting — adding a comment would be noise.

@rianjs
Copy link
Copy Markdown
Contributor Author

rianjs commented Mar 28, 2026

TDD Assessment — PR #186

What's covered well

  • Happy-path merges for both []map[string]string (option arrays) and []string (label arrays)
  • Scalar overwrite behavior
  • Type mismatch case (scalar existing + array new) — new value wins

Missing test cases for MergeFieldValues

Nil/empty inputs

  • existing = nil, newVal = []string{"x"} — hits the default branch, new value wins, but not explicitly asserted
  • existing = []string{}, newVal = []string{"x"} — zero-length append; append([]string{}, ...) works correctly but is unverified
  • existing = []string{"x"}, newVal = []string{} — appending nothing; could silently return the original

Reverse type mismatch (not tested)

  • existing = []string{"x"}, newVal = "scalar" — outer []string branch matches, inner newArr assertion fails, falls through to return newVal; the scalar wins. This is the opposite of the tested mismatch and has meaningfully different semantics worth documenting
  • existing = []map[string]string{...}, newVal = []string{"x"} — also silent fallthrough to return newVal; a reviewer would ask whether that's intentional

Multiple-element accumulation

  • Only one element on each side is tested. A three-call sequence (A → merge B → merge C) is not exercised, so growing-slice correctness across more than two inputs is unverified.

Gaps in create.go / update.go integration paths

The field-loop change in both files is not covered by any unit or integration test at the command level:

  • No test exercises runCreate or runUpdate with duplicate --field keys
  • The only coverage is the pure MergeFieldValues unit test; the if existing, ok := fields[fieldID] branch in the command code is never hit by the test suite
  • The test plan in the PR description notes "Integration test: update a multi-checkbox field…" is unchecked

Business logic concern

FormatFieldValue can also return map[string]string (for option, user, issuelink, priority, etc.) and float64 (for number). When existing is one of those non-array types and newVal is also one of those types, MergeFieldValues correctly returns newVal (scalar overwrite). That's fine for most fields — but for a user array field (schema type array, items user), FormatFieldValue returns []string{value}, so repeated user fields would merge correctly. This edge case is present in the code but absent from tests.

What a reviewer would flag

  1. No command-level test for the accumulation path — the fix to create.go and update.go is entirely untested; MergeFieldValues unit tests don't exercise the call sites
  2. Nil inputs to MergeFieldValues are unspecified — Go's type-switch will fall through to return newVal, but the function contract doesn't document it
  3. Reverse type-mismatch behavior (array existing + scalar new) silently discards the existing array; this likely matches the intent but no test or comment confirms it
  4. Unchecked integration test item in the PR description — should be checked or converted to a follow-up issue before merge

Overall the unit tests cover the documented happy paths but leave the call-site integration and the nil/reverse-mismatch edge cases unverified.

@rianjs rianjs merged commit 12b0978 into main Mar 28, 2026
7 checks passed
@rianjs rianjs deleted the fix/166-multi-value-fields branch March 28, 2026 18:16
rianjs added a commit that referenced this pull request Mar 28, 2026
Catches up documentation for recent features and fixes:
- README: document --fields flag, auto-pagination, fields command group,
  users get subcommand, --assignee none, multi-value --field, and
  escape sequences in comment --body
- CHANGELOG: add entries for PRs #178, #180, #182, #186-189
- integration-tests: add test cases for auto-pagination, --fields,
  users get, --assignee none, multi-value --field, and escape sequences

Closes #183
rianjs added a commit that referenced this pull request Mar 28, 2026
#190)

Catches up documentation for recent features and fixes:
- README: document --fields flag, auto-pagination, fields command group,
  users get subcommand, --assignee none, multi-value --field, and
  escape sequences in comment --body
- CHANGELOG: add entries for PRs #178, #180, #182, #186-189
- integration-tests: add test cases for auto-pagination, --fields,
  users get, --assignee none, multi-value --field, and escape sequences

Closes #183
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.

fix(jtk): multi-checkbox custom fields only set last value

2 participants