Skip to content

Stop a fake team inheriting a real same-named team's plugin data - #36

Open
corsonknowles wants to merge 1 commit into
rubyatscale:mainfrom
corsonknowles:fix-testing-plugin-registry-leak
Open

Stop a fake team inheriting a real same-named team's plugin data#36
corsonknowles wants to merge 1 commit into
rubyatscale:mainfrom
corsonknowles:fix-testing-plugin-registry-leak

Conversation

@corsonknowles

Copy link
Copy Markdown

Problem

CodeTeams::Plugin's registry is keyed by team name (register_team uses team.raw_hash['name']), and the around hook installed by Testing.enable! only busts it after an example that built a fake team:

example.run
# Bust caches because plugins may hang onto stale data between examples.
if CodeTeams::Testing.code_teams.any?   # <- asymmetric
  CodeTeams.bust_caches!
  CodeTeams::Testing.reset!
end

An example that reads only real teams satisfies neither branch, so it leaves real-team plugin instances in the registry. A later code_team_with_config(name: 'Some Real Team', ...) then hits that cached entry and silently receives the real team's plugin data — files, slack, github, whatever the host app's plugins expose — instead of the config it just declared.

The failure mode is nasty to trace: the spec passes in isolation and fails only when some earlier example happened to touch a real team of the same name, so it presents as a seed-dependent flake with no visible connection to team config. We hit this in a downstream app where a fake Developer Productivity Rails team inherited the real team's protected-files list, silently protecting a file the example never declared.

Fix

Bust the plugin registry as each fake team is registered, so the fake always registers ahead of a same-named real team.

Only Plugin.bust_caches! is called, not CodeTeams.bust_caches!CodeTeams.all stays memoized and no team YAML is re-read, so this doesn't reintroduce the cost the .any? guard exists to avoid. The cost is paid only by suites that actually build fake teams.

Test

The added spec reproduces the leak deterministically inside a single example (no ordering dependency): write a real My Team with plugin data, read it through a plugin so the registry caches it, then build a fake My Team and assert it sees its own data.

Without the fix:

expected: "fake"
     got: "real"

bundle exec rspec (20 examples, 0 failures), bundle exec rubocop, and bundle exec srb tc are all green with the fix.

Plugin's registry is keyed by team name, and the around hook installed by
Testing.enable! only busts it after an example that built a fake team. An
example that reads only real teams therefore leaves real-team plugin instances
in the registry, and a later fake team sharing a real team's name silently
receives the real team's cached plugin data instead of its own.

This makes suites order-dependent in a way that is hard to trace: the spec
passes in isolation and fails only when some earlier example happened to touch
a real team of the same name.

Bust the plugin registry as each fake team is registered so the fake always
registers first. Only Plugin.bust_caches! is called, not CodeTeams.bust_caches!,
so CodeTeams.all stays memoized and no team YAML is re-read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@corsonknowles
corsonknowles requested a review from a team as a code owner August 14, 2026 09:37
@github-project-automation github-project-automation Bot moved this to Triage in Modularity Aug 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Triage

Development

Successfully merging this pull request may close these issues.

1 participant