fix(store/memory): deduplicate AssignRoleIDs to match postgres#59
Merged
Conversation
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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
Closes a latent correctness divergence between
store/memoryandstore/postgresin how provisioning rule role IDs are normalized at write time, and locks the deduplication behavior into the sharedinternal/storetestcontract so both adapters must honor it going forward.Postgres has always deduplicated
AssignRoleIDsviauniqueStringsinnormalizeProvisioningRule(and again at the DB layer via the(rule_id, role_id)unique constraint andon conflict do nothing). Memory stored the slice verbatim, so reading back a rule created with[role-a, role-a, role-b]returned[role-a, role-a, role-b]from memory but[role-a, role-b]from postgres. The shared behavior suite was silent on this — neither adapter was caught.This PR:
uniqueStringstostore/memory/validation.go(verbatim from postgres, same godoc).normalizeProvisioningRulehelper tostore/memory/provisioning.gomirroring postgres; routesprovisioningRuleFromCreateandprovisioningRuleFromUpdatethrough it.uniqueStringstostore/memory/assignInitialRoles. Memory's map writes are already set-semantic so the observable behavior here is unchanged; the change is for parity with postgres at the code-shape level.internal/storetest/provisioning.gowith two new assertions (...deduplicate AssignRoleIDs on create,...on update) that lock the contract.assert.ElementsMatchis used for the post-update find because postgres returns role IDs ordered byarray_agg(... order by rr.role_id)while memory returns first-occurrence order — the contract is "deduped set", not "deduped ordered list".provision identity tolerates duplicate initial role IDsas a regression net ininternal/storetest/identity.go. Both adapters tolerate duplicates today (postgres viaon conflict do nothing, memory via map set-semantics); this test makes the tolerance part of the contract.Behavior change
Memory now deduplicates
AssignRoleIDsonCreateProvisioningRuleandUpdateProvisioningRule. Consumers that stored a rule with repeated role IDs in memory and read back the multiset will now read back the deduped slice. Initial-role-assignment semantics are already idempotent (a principal either has a role or doesn't), so this brings memory's observable contract into line with postgres without changing the meaning of any allowed input.Context
This is PR 1 of 3 in a focused refactor to lift pure cross-adapter rules into a new
internal/storerulespackage, keeping use-case-grain ports and not introducing a transaction port. PR 2 will liftSameIdentityBinding,UniqueStrings,ValidateNonEmptyStrings,ValidateRequiredStringsintointernal/storerulesand swap call sites. PR 3 will lift the passkey predicates. Landing the behavioral fix first keeps the subsequent extractions purely mechanical and reviewable in isolation.Plan: see project session 035.
Test plan
[notes-writer, notes-writer, notes-reader]vs. expected[notes-writer, notes-reader]); pass after.go test ./store/memory/...(in-process)go test ./store/postgres/...(unit, no integration tag)moon run root:check --summary minimal— green; includesroot:integration(Testcontainers postgres runs the shared suite including the new dedup assertions).git diff --check— clean.