fix(pull): state-aware findExistingResourceId — prevent same-name clobber#29
Merged
Merged
Conversation
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.
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
pull'sfindExistingResourceIdhelper 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>.mdslug per UUID.Discovered in the gitops-mudflap customer working session 2026-05-13 (PRISM-737-adjacent) while investigating duplicate-resource accumulation.
What changes (behavior)
<name>-<bUuid8>.md; A stays intact ✓Files changed
src/pull.tsfindExistingResourceIdpromoted toexport function, newstateSectionarg + adoption filter; caller updated to pass{...state[resourceType], ...newStateSection}tests/find-existing-resource-id.test.tstests/pull-same-name-clobber.test.tsTest plan
npm run build— cleannpm test— 167/167 pass (156 prior + 11 new: 9 unit + 2 integration with parameterized ordering)npx @biomejs/biome check --write— cleanCode 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,newStateSectionwas empty when the new twin was processed, and the clobber happened anyway. Second commit on this branch (697a8dc) addresses that by mergingstate[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)
credentialsstate section is not consulted by this function — credentials follow a different reverse-map flow inpullCredentials. Not relevant to this fix.unparseable-claimdefensive handling.ResourceState.uuidis typedstring(non-optional). The filter checksclaim === undefined || claim === resource.id. An empty-stringuuidis theoretical and would block adoption (a strictly safer outcome than clobber). Not worth special-casing.findExistingResourceIdis now afindAdoptableResourceIdby behavior, but the existing name is recognizable and the docstring is updated. Renaming would churn the diff for no behavioral win.Related
npm run auditcommand, merged 2026-05-13)