Skip to content

structwalk: pin the shadowed-embed double-visit bug in unit tests - #6478

Draft
denik wants to merge 5 commits into
mainfrom
denik/structwalk-shadow-test
Draft

structwalk: pin the shadowed-embed double-visit bug in unit tests#6478
denik wants to merge 5 commits into
mainfrom
denik/structwalk-shadow-test

Conversation

@denik

@denik denik commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Two unit tests in libs/structs/structwalk/shadow_test.go that pin the current (buggy) behaviour and must be updated when the bug is fixed.

TestShadowedEmbedIsVisitedTwice
Walks a resources.Cluster with both BaseResource.Lifecycle and Cluster.Lifecycle *LifecycleWithStarted populated. Both resolve to lifecycle.prevent_destroy; structwalk visits that path twice while encoding/json uses only the shallower (direct named) field. The test asserts structwalk and json agree — they don't, and the assertion records the mismatch as the expected current state.

TestShadowedEmbedCausesStructdiffDuplicate
Diffs two resources.Pipeline values with both BaseResource.ID and CreatePipeline.Id non-zero ("old-base-id" vs "new-base-id", "old-sdk-id" vs "new-sdk-id"). Both fields carry json:"id" at the same embedding depth: ambiguous to encoding/json (neither serialized), visited twice by structdiff. The raw diff slice contains id twice; prepareChanges in the direct engine silently overwrites the first entry with the second via a map, so which value wins is order-dependent. The test asserts id currently appears twice — the fix must make this list empty.

Note: lifecycle.prevent_destroy is intentionally absent from state (ClusterState only tracks lifecycle.started). An acceptance test asserting its absence in bundle plan -o json was testing correct behavior, not a bug, and was removed.

This pull request and its description were written by Isaac.

resources.Pipeline embeds BaseResource and pipelines.CreatePipeline, both
declaring json:"id". resources.Cluster embeds BaseResource.Lifecycle and its
own Lifecycle *LifecycleWithStarted, both resolving to json path
"lifecycle.prevent_destroy". encoding/json resolves this by using only the
shallower field (direct named field beats promoted embedded one); structwalk
visits both.

Unit test (libs/structs/structwalk/shadow_test.go):

  TestShadowedEmbedIsVisitedTwice — walks a resources.Cluster with both
  shadowed fields non-zero and logs that lifecycle.prevent_destroy is visited
  twice. Asserts structwalk and encoding/json agree about which paths exist
  (they don't, and the assertion records that mismatch as the current
  behaviour).

  TestShadowedEmbedCausesStructdiffDuplicate — diffs two resources.Pipeline
  values with both id fields non-zero. structdiff receives the id path twice
  (once from each shadowed embed). prepareChanges in the direct engine maps
  changes by path string, so the second entry silently overwrites the first;
  which value wins is implementation-order-dependent. The test asserts that
  "id" currently appears twice in the raw diff output, so a fix must remove it.

Acceptance test (acceptance/bundle/resources/clusters/shadow_lifecycle/):

  bundle plan -o json on a cluster with lifecycle.prevent_destroy: true
  shows prevent_destroy_in_state: "ABSENT" — the field is not in ClusterState
  (PrepareState only copies lifecycle.started), so the direct engine cannot
  detect future changes to it. The golden pins that absence; when the bug is
  fixed, prevent_destroy would appear in the state and this golden needs
  updating.

Both tests are meant to FAIL when the bug is fixed, forcing an update.

Co-authored-by: Isaac
The previous script used jq to extract specific fields and format them as
a custom object. That obscured the actual plan output and made it hard to
see what was and wasn't tracked. Now the script feeds the full cluster plan
entry straight into the golden, so it is clear what new_state.value carries.

lifecycle.prevent_destroy is absent from new_state.value because ClusterState
only tracks lifecycle.started; the golden makes that visible at a glance.
lifecycle.prevent_destroy is intentionally absent from state — it is a
deploy-time guard that tells the CLI to refuse to destroy the resource, not
a field the remote API tracks. An acceptance test asserting its absence was
testing correct behavior, not a bug.

The two unit tests remain: they pin the actual double-visit behavior in
structwalk and structdiff over config types that have shadowed embedded fields.
@denik denik changed the title structwalk: pin the shadowed-embed double-visit bug in tests structwalk: pin the shadowed-embed double-visit bug in unit tests Sep 2, 2026
- slices.Sort instead of sort.Strings (repo convention)
- nolint:staticcheck on the explicit embedded-field selectors —
  the test intentionally addresses the shadowed fields by their
  full qualified names to set both independently; the shorter form
  would silently hit only one of them
The test previously imported bundle/config/resources and the SDK's
pipelines package to get the real embedding shapes. A change to those
types could break this test for unrelated reasons.

Replaced with local exported struct types (Shadow*) that mirror the exact
embedding patterns, with comments pointing to the originals:

  ShadowPipeline  ← resources.Pipeline: ShadowBase + ShadowSDKPipeline,
                    both with json:"id" → ambiguous, dropped by encoding/json
  ShadowCluster   ← resources.Cluster: ShadowBase.Lifecycle + named
                    Cluster.Lifecycle, both at json:"lifecycle.prevent_destroy"

Also strengthened the first test: the assertions now record the exact
buggy visit counts (2 for both cases), so the tests pass with the bug
present and must be updated when the bug is fixed, matching the intent of
TestShadowedEmbedCausesStructdiffDuplicate.
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