Skip to content

fix: picklist values corrupted in CustomField merge#175

Merged
scolladon merged 31 commits intomainfrom
fix/picklist-values-corrupted
Feb 26, 2026
Merged

fix: picklist values corrupted in CustomField merge#175
scolladon merged 31 commits intomainfrom
fix/picklist-values-corrupted

Conversation

@scolladon
Copy link
Owner

@scolladon scolladon commented Feb 20, 2026

Explain your changes


When merging a CustomField with inline picklist values (valueSet > valueSetDefinition > value), the merge driver produced completely broken XML output. The <valueSet> container was lost, <restricted> and <valueSetDefinition> appeared directly inside <CustomField>, and all value elements were empty/corrupted.

Root causes:

  1. ObjectMergeNode double-wrapped children and lost its own container. It re-wrapped each child's output with { [key]: item }, but child nodes (like TextMergeNode) already include the property key. Additionally, it never wrapped its combined output with its own attribute name (e.g. valueSet), so the container was lost.

  2. OrderedKeyedArrayMergeStrategy.wrapElement() wrapped raw parsed objects instead of converting them to the builder format expected by fast-xml-parser with preserveOrder: true, producing empty <value></value> elements.

Fixes:

  • Add attribute parameter to ObjectMergeNode, remove double-wrapping, wrap combined output with container name
  • Use toJsonArray() in wrapElement() and wrapKeys() to convert to builder format

Refactoring included in this PR:

  • Rename ObjectMergeNode to PropertyMergeNode, remove NestedObjectMergeNode
  • Rename local MergeContext to ArrayMergeState in KeyedArrayMergeNode
  • Use wrapWithRootKey in PropertyMergeNode
  • Remove unnecessary exports from internal strategy and factory classes
  • Replace positional parameters with parameter objects (TextMergeParams, GapKeys)
  • Document wrapping contract on MergeNode interface
  • Update DESIGN.md with PropertyMergeNode rename
  • Extract OrderedKeyedArrayMergeStrategy to its own file (~523 lines out of KeyedArrayMergeNode.ts)
  • Consolidate ScenarioStrategy classes into a single-file strategy map (following TextMergeStrategy pattern)
  • Add dedicated PropertyMergeNode unit tests (6 new tests)
  • Add dedicated OrderedKeyedArrayMergeStrategy test file with parameterized merge scenarios

Does this close any currently open issues?


closes #174

  • Jest tests added to cover the fix.
  • NUT tests added to cover the fix.
  • E2E tests added to cover the fix.

Any particular element that can be tested locally


Merge a CustomField XML with inline picklist valueSet where one branch modifies restricted and removes values while the other branch is unchanged. The output should preserve the <valueSet> container, take the modified <restricted> value, and correctly render remaining <value> elements.

… container

ObjectMergeNode iterated child properties and re-wrapped each child's
output with `{ [key]: item }`, but child nodes (e.g. TextMergeNode)
already include the property key. This caused double-wrapping.
Additionally, ObjectMergeNode never wrapped its combined output with
its own attribute name, losing the container (e.g. `valueSet`).

- Add `attribute` parameter to ObjectMergeNode constructor
- Push child results directly without re-wrapping
- Wrap combined output with `{ [this.attribute]: ... }`
- Add optional chaining for safety on ancestor/local/other access
- Pass attribute from MergeNodeFactory to ObjectMergeNode

Fixes #174
…ement

OrderedKeyedArrayMergeStrategy.wrapElement() and wrapKeys() wrapped raw
parsed objects directly, but fast-xml-parser XMLBuilder with
preserveOrder:true requires builder format. This produced empty
`<value></value>` elements in the XML output.

Use toJsonArray() to convert raw objects to builder format before
wrapping, consistent with how the unordered strategy handles output.

Fixes #174
- Update ObjectMergeNode tests to pass required `attribute` parameter
- Add assertion verifying no double-wrapping of child output
- Update extractLabels helper to handle builder format arrays
- Add end-to-end CustomField picklist merge test (issue #174 scenario)

Refs #174
This is an integration test exercising the full XmlMerger pipeline,
not a unit test.
@scolladon scolladon requested a review from yohanim as a code owner February 20, 2026 17:25
- @biomejs/biome 2.4.0 → 2.4.3
- @commitlint/config-conventional 20.4.1 → 20.4.2
- @salesforce/core 8.26.0 → 8.26.2
- fast-xml-parser 5.3.6 → 5.3.7
- knip 5.83.1 → 5.84.1
Copy link
Collaborator

@yohanim yohanim left a comment

Choose a reason for hiding this comment

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

If it solves the issue with all other tests ok it's fine by me stuck in travel.
We'll discuss it at a later time.

@scolladon
Copy link
Owner Author

If it solves the issue with all other tests ok it's fine by me stuck in travel. We'll discuss it at a later time.

Yes, it is just a fix at the marshalling level, zero change to the algo

@scolladon
Copy link
Owner Author

Hi @nvuillam !

Could you test on your laptop and help us validate this PR please ?
You can follow those steps to easily do it locally

@nvuillam
Copy link
Contributor

nvuillam commented Feb 20, 2026

Can't test, it's one of my Cloudity colleagues that reported me the bug ^^
But with your test class it should be ok ^^

…ergeNode

Avoid naming collision with the MergeContext type from ../MergeContext.ts.
The local interface represents ordered merge state, not the orchestrator context.
Replace manual wrapping with the existing wrapWithRootKey utility.
This deduplicates the wrapping logic and properly handles the empty
output case (returns noConflict instead of raw empty array).
Each merge() returns output already wrapped with its attribute key.
This implicit contract was the root cause of the double-wrapping bug.
ObjectMergeNode merges pure objects property-by-property and wraps with attribute.
NestedObjectMergeNode delegates to MergeOrchestrator with full scenario detection.
…ration test

Parse the merge output with XMLParser and assert on the DOM tree instead
of fragile string patterns. Consolidates 5 tests into 3 focused assertions:
conflict-free merge, correct tree structure, no leaked children.
When all children resolve to nothing during merge, return empty output
instead of producing an empty container element like <valueSet></valueSet>.
…bjectMergeNode

PropertyMergeNode better conveys it merges objects property-by-property.
NestedObjectMergeNode was unused in production code (only tests) and
not part of the public API, so it is removed entirely.
@biomejs/biome 2.4.3 → 2.4.4
@scolladon
Copy link
Owner Author

Can't test, it's one of my Cloudity colleagues that reported me the bug ^^ But with your test class it should be ok ^^

@nvuillam if your colleagues could test before we merge it is better, else if we all agree it should fix properly then let's merge and see, we'll fix further if needed

…ry classes

DefaultMergeNodeFactory and 8 TextMergeStrategy classes were exported but
never imported by production code. Tests now use the public API
(getTextMergeStrategy, defaultNodeFactory) instead of importing internals.
Introduce TextMergeParams for TextMergeStrategy.handle()
(7 positional params to 1 object) and GapKeys for
KeyedArrayMergeNode gap methods (5-6 params to 3-4 params),
following the existing MergeContext pattern.
@scolladon scolladon force-pushed the fix/picklist-values-corrupted branch from 458aa74 to e4e5625 Compare February 21, 2026 08:39
Remove duplicated toJsonArray + rootKey wrapping logic.
Replace duplicated output.length > 0 || hasConflict guard
in KeyedArrayMergeNode with shared isNonEmpty helper.
Rename installService.ts and uninstallService.ts to match
the PascalCase convention used by all other source files.
Move hasSameOrder and lcs to arrayUtils, setsEqual and
setsIntersect to new setUtils, filterEmptyTextNodes and
buildKeyedMap to nodeUtils. Add unit tests for all.
Reduces KeyedArrayMergeNode.ts from 737 to 117 lines by moving the
523-line OrderedKeyedArrayMergeStrategy class and its supporting types
to a dedicated file. Exports KeyExtractor from nodeUtils.ts to share
the type definition.
Merges 10 strategy files + factory into ScenarioStrategy.ts following
the TextMergeStrategy pattern. Consolidates 9 test files into
ScenarioStrategy.test.ts using getScenarioStrategy() factory function.
Updates MergeOrchestrator import path accordingly.
Adds 6 tests covering: multiple properties merged simultaneously,
property only in local/other, property deleted in both, mixed
conflict/no-conflict children propagation, and nested object
recursive delegation.
Move ordered-strategy tests from KeyedArrayMergeNode.test.ts into a
dedicated OrderedKeyedArrayMergeStrategy.test.ts that tests the strategy
directly with an explicit keyExtractor function.
Consolidate the two standalone "Diverged orderings" tests into the
gracefulMergeScenarios array as M11 and M12.
@github-actions
Copy link

Published under dev-175 npm channel.

$ sf plugins install sf-git-merge-driver@dev-175

@nvuillam
Copy link
Contributor

nvuillam commented Feb 21, 2026

@JulianAubert is the one that found the bug, he'll tell you if it's fixed :)

But meanwhile i think you should release a patch: can't be break more picklists that it already does :D

@scolladon
Copy link
Owner Author

I have been able to test locally the fix 👍
All good ready to be merged

@scolladon scolladon merged commit 365c7f7 into main Feb 26, 2026
13 of 15 checks passed
@scolladon scolladon deleted the fix/picklist-values-corrupted branch February 26, 2026 09:54
@github-actions
Copy link

Shipped in release v1.5.4.
Version v1.5.4 will be assigned to the latest npm channel soon
Install it using either v1.5.4 or the latest-rc npm channel

$ sf plugins install sf-git-merge-driver@latest-rc
# Or
$ sf plugins install sf-git-merge-driver@v1.5.4

💡 Enjoying sf-git-merge-driver?
Your contribution helps us provide fast support 🚀 and high quality features 🔥
Become a sponsor 💙
Happy conflict merge free!

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.

Butchering of picklist values :D

3 participants