Skip to content

fix(store/memory): deduplicate AssignRoleIDs to match postgres#59

Merged
jmgilman merged 1 commit into
masterfrom
session-035/memory-uniquestrings
May 26, 2026
Merged

fix(store/memory): deduplicate AssignRoleIDs to match postgres#59
jmgilman merged 1 commit into
masterfrom
session-035/memory-uniquestrings

Conversation

@jmgilman
Copy link
Copy Markdown
Contributor

Summary

Closes a latent correctness divergence between store/memory and store/postgres in how provisioning rule role IDs are normalized at write time, and locks the deduplication behavior into the shared internal/storetest contract so both adapters must honor it going forward.

Postgres has always deduplicated AssignRoleIDs via uniqueStrings in normalizeProvisioningRule (and again at the DB layer via the (rule_id, role_id) unique constraint and on 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:

  • Adds uniqueStrings to store/memory/validation.go (verbatim from postgres, same godoc).
  • Adds a normalizeProvisioningRule helper to store/memory/provisioning.go mirroring postgres; routes provisioningRuleFromCreate and provisioningRuleFromUpdate through it.
  • Adds defensive uniqueStrings to store/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.
  • Extends internal/storetest/provisioning.go with two new assertions (...deduplicate AssignRoleIDs on create, ...on update) that lock the contract. assert.ElementsMatch is used for the post-update find because postgres returns role IDs ordered by array_agg(... order by rr.role_id) while memory returns first-occurrence order — the contract is "deduped set", not "deduped ordered list".
  • Adds provision identity tolerates duplicate initial role IDs as a regression net in internal/storetest/identity.go. Both adapters tolerate duplicates today (postgres via on conflict do nothing, memory via map set-semantics); this test makes the tolerance part of the contract.

Behavior change

Memory now deduplicates AssignRoleIDs on CreateProvisioningRule and UpdateProvisioningRule. 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/storerules package, keeping use-case-grain ports and not introducing a transaction port. PR 2 will lift SameIdentityBinding, UniqueStrings, ValidateNonEmptyStrings, ValidateRequiredStrings into internal/storerules and 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

  • TDD verified — new storetest assertions fail against memory with the exact diagnostic before the fix ([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; includes root:integration (Testcontainers postgres runs the shared suite including the new dedup assertions).
  • git diff --check — clean.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jmgilman jmgilman merged commit c15f247 into master May 26, 2026
2 checks passed
@jmgilman jmgilman deleted the session-035/memory-uniquestrings branch May 26, 2026 23:38
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