Skip to content

fix(pull): state-aware findExistingResourceId — prevent same-name clobber#29

Merged
dhruva-reddy merged 2 commits into
mainfrom
fix/findExistingResourceId-state-aware
May 13, 2026
Merged

fix(pull): state-aware findExistingResourceId — prevent same-name clobber#29
dhruva-reddy merged 2 commits into
mainfrom
fix/findExistingResourceId-state-aware

Conversation

@dhruva-reddy
Copy link
Copy Markdown
Contributor

Summary

pull's findExistingResourceId helper used to silently clobber an existing-tracked resource's file + state mapping whenever the dashboard had a second resource with the same name. The 1:1 name-slug match guard intended for cross-environment adoption ("dev's file with a different UUID-suffix — reuse it for the prod resource of the same name") didn't distinguish between "this on-disk file is free for adoption" vs "this file is already tracked in state by a DIFFERENT UUID."

The fix makes the function state-aware: it consults the merged view of prior-pull state + in-flight pull state to decide whether each candidate file is adoptable. Files already claimed by a different UUID are NOT adoptable — the caller falls through to generateResourceId(resource) which produces a deterministic <name>-<uuid8>.md slug per UUID.

Discovered in the gitops-mudflap customer working session 2026-05-13 (PRISM-737-adjacent) while investigating duplicate-resource accumulation.

What changes (behavior)

Scenario Before After
Single resource, single file on disk Reuses file Unchanged ✓
Cross-env pull: state empty, file exists with diff UUID-suffix Reuses file (adoption) Unchanged ✓
Re-pull of known UUID (reverseMap hit) Reuses via reverseMap (this function not called) Unchanged ✓
2 same-name dashboard resources, state has A's UUID, B is new CLOBBER: B's content overwrites A's file, A's state mapping is lost B gets new <name>-<bUuid8>.md; A stays intact ✓
N+ matches without a single adoptable candidate Returns undefined (0-or-2+ ambiguity guard) Unchanged ✓

Files changed

File Change LOC
src/pull.ts findExistingResourceId promoted to export function, new stateSection arg + adoption filter; caller updated to pass {...state[resourceType], ...newStateSection} ~45
tests/find-existing-resource-id.test.ts New file. 9 unit tests covering: cross-env adoption (regression), re-pull-same-UUID adoption, refusal-on-different-UUID claim (fix core), mixed-claim-with-1-adoptable adopt, N+ unclaimed → undefined, N+ all-claimed → undefined, name-absent, no-name-match, two-unclaimed-ambiguity ~190
tests/pull-same-name-clobber.test.ts New file. Spawn-fixture integration test with parameterized dashboard-response ordering — both A-first AND B-first orderings asserted, since the reviewer caught that the initial single-state-section fix was iteration-order-dependent ~290

Test plan

  • npm run build — clean
  • npm test167/167 pass (156 prior + 11 new: 9 unit + 2 integration with parameterized ordering)
  • npx @biomejs/biome check --write — clean

Code review

Reviewed twice during development. First pass surfaced a BLOCKING finding: the initial fix consulted only newStateSection, making it iteration-order-dependent. If the dashboard returned the new twin BEFORE the tracked one, newStateSection was empty when the new twin was processed, and the clobber happened anyway. Second commit on this branch (697a8dc) addresses that by merging state[resourceType] (prior-pull claims) into the adoption guard's view.

The B-first integration test in this PR fails on the first commit (d7c718f) and passes on the second (697a8dc) — it's the regression guard for the H1 finding.

Residual notes (non-blocking)

  • credentials state section is not consulted by this function — credentials follow a different reverse-map flow in pullCredentials. Not relevant to this fix.
  • No unparseable-claim defensive handling. ResourceState.uuid is typed string (non-optional). The filter checks claim === undefined || claim === resource.id. An empty-string uuid is theoretical and would block adoption (a strictly safer outcome than clobber). Not worth special-casing.
  • Function name unchanged. findExistingResourceId is now a findAdoptableResourceId by behavior, but the existing name is recognizable and the docstring is updated. Renaming would churn the diff for no behavioral win.

Related

When two dashboard resources share a name (e.g. via Duplicate Assistant
or any platform auto-seed) and one is already tracked in state, the
prior pull behavior was to silently reuse the existing file's slug for
the new UUID — clobbering the original's content and orphaning its
state mapping.

Fix: before adopting a matching file, check whether it's already
claimed in state by a different UUID. If so, refuse adoption and let
the caller fall through to `generateResourceId(resource)`, which
produces a deterministic `<name>-<uuid8>.md` slug per UUID. Cross-env
adoption (file exists, state empty for it) and re-pull (file exists,
state claims it for THIS UUID) paths are unchanged.

Promotes `findExistingResourceId` from `function` to `export function`
so tests can pin behavior directly.

Closes customer issue surfaced in gitops-mudflap working session
2026-05-13 (PRISM-737-adjacent).
… H1)

Addresses the BLOCKING finding from the code review of d7c718f. The
first fix passed only `newStateSection` (in-flight pull state) to
`findExistingResourceId`. That made the fix iteration-order-dependent:
if the dashboard's /assistant list returned the new same-name twin
BEFORE the tracked one, the new twin saw `newStateSection` empty (the
tracked one hadn't been processed yet), and clobbered the existing
file anyway.

Fix: pass a merged view `{...state[resourceType], ...newStateSection}`
to the adoption guard. The prior-pull state carries claims loaded from
disk; the in-flight section carries claims made earlier in this same
loop. Spreading `newStateSection` last preserves intra-pull precedence
when both have the same slug (e.g. an earlier iteration re-keyed it).

Adds a B-first integration test that fails under d7c718f and passes
under this fix. The original A-first test is preserved by extracting
both into a shared `runClobberScenario(testName, order)` helper.

Also tightens the same-UUID branch comment in `findExistingResourceId`
to clarify that it's defensive — the caller's `reverseMap.get` short-
circuits this case in production. Comment-only change.

Suite: 167/167 pass.
@dhruva-reddy dhruva-reddy merged commit 7e051fa into main May 13, 2026
@dhruva-reddy dhruva-reddy deleted the fix/findExistingResourceId-state-aware branch May 13, 2026 20:54
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