bug_bunny 4.8.0 → 4.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.agents/skills/documentation-writer/SKILL.md +45 -0
  3. data/.agents/skills/gem-release/SKILL.md +114 -0
  4. data/.agents/skills/quality-code/SKILL.md +51 -0
  5. data/.agents/skills/sentry/SKILL.md +135 -0
  6. data/.agents/skills/sentry/references/api-endpoints.md +147 -0
  7. data/.agents/skills/sentry/scripts/sentry.rb +194 -0
  8. data/.agents/skills/skill-builder/SKILL.md +232 -0
  9. data/.agents/skills/skill-manager/SKILL.md +172 -0
  10. data/.agents/skills/skill-manager/scripts/sync.rb +310 -0
  11. data/.agents/skills/yard/SKILL.md +311 -0
  12. data/.agents/skills/yard/references/tipos.md +144 -0
  13. data/CHANGELOG.md +8 -0
  14. data/CLAUDE.md +28 -231
  15. data/lib/bug_bunny/version.rb +1 -1
  16. data/skill/SKILL.md +230 -0
  17. data/skill/references/client-middleware.md +144 -0
  18. data/skill/references/consumer.md +104 -0
  19. data/skill/references/controller.md +105 -0
  20. data/skill/references/errores.md +97 -0
  21. data/skill/references/resource.md +116 -0
  22. data/skill/references/routing.md +82 -0
  23. data/skill/references/testing.md +138 -0
  24. data/skills.lock +24 -0
  25. data/skills.yml +19 -0
  26. metadata +24 -28
  27. data/.claude/commands/gem-ai-setup.md +0 -174
  28. data/.claude/commands/pr.md +0 -53
  29. data/.claude/commands/release.md +0 -52
  30. data/.claude/commands/rubocop.md +0 -22
  31. data/.claude/commands/service-ai-setup.md +0 -168
  32. data/.claude/commands/test.md +0 -28
  33. data/.claude/commands/yard.md +0 -46
  34. data/docs/_index.md +0 -50
  35. data/docs/ai/_index.md +0 -56
  36. data/docs/ai/antipatterns.md +0 -166
  37. data/docs/ai/api.md +0 -251
  38. data/docs/ai/architecture.md +0 -92
  39. data/docs/ai/errors.md +0 -158
  40. data/docs/ai/faq_external.md +0 -133
  41. data/docs/ai/faq_internal.md +0 -86
  42. data/docs/ai/glossary.md +0 -45
  43. data/docs/concepts.md +0 -140
  44. data/docs/howto/controller.md +0 -194
  45. data/docs/howto/middleware_client.md +0 -119
  46. data/docs/howto/middleware_consumer.md +0 -127
  47. data/docs/howto/rails.md +0 -214
  48. data/docs/howto/resource.md +0 -200
  49. data/docs/howto/routing.md +0 -133
  50. data/docs/howto/testing.md +0 -259
  51. data/docs/howto/tracing.md +0 -119
@@ -1,259 +0,0 @@
1
- # Testing
2
-
3
- BugBunny applications can be tested at two levels: **unit tests** (with Bunny doubles) and **integration tests** (with a full mocked AMQP stack). No real RabbitMQ server is required.
4
-
5
- ## Setup
6
-
7
- ```ruby
8
- # spec/spec_helper.rb
9
- require 'bug_bunny'
10
- require 'rspec'
11
-
12
- Dir[File.join(__dir__, 'support', '**', '*.rb')].each { |f| require f }
13
-
14
- RSpec.configure do |config|
15
- config.before(:each) do
16
- # Reset global state between tests
17
- BugBunny.instance_variable_set(:@consumer_middlewares, nil)
18
- BugBunny.instance_variable_set(:@routes, nil)
19
- end
20
- end
21
- ```
22
-
23
- ---
24
-
25
- ## Bunny Doubles
26
-
27
- Create lightweight doubles for Bunny objects so tests never touch the network:
28
-
29
- ```ruby
30
- # spec/support/bunny_mocks.rb
31
-
32
- def build_bunny_channel(opts = {})
33
- channel = instance_double(Bunny::Channel)
34
- allow(channel).to receive(:open?).and_return(true)
35
- allow(channel).to receive(:prefetch)
36
- allow(channel).to receive(:confirm_select)
37
- allow(channel).to receive(:ack)
38
- allow(channel).to receive(:reject)
39
- allow(channel).to receive(:close)
40
- allow(channel).to receive(:default_exchange).and_return(build_bunny_exchange)
41
- allow(channel).to receive(:topic).and_return(build_bunny_exchange)
42
- allow(channel).to receive(:direct).and_return(build_bunny_exchange)
43
- channel
44
- end
45
-
46
- def build_bunny_connection(opts = {})
47
- conn = instance_double(Bunny::Session)
48
- allow(conn).to receive(:open?).and_return(true)
49
- allow(conn).to receive(:start)
50
- allow(conn).to receive(:create_channel).and_return(build_bunny_channel)
51
- conn
52
- end
53
-
54
- def build_bunny_exchange
55
- exchange = instance_double(Bunny::Exchange)
56
- allow(exchange).to receive(:publish)
57
- exchange
58
- end
59
- ```
60
-
61
- ---
62
-
63
- ## Unit Testing Controllers
64
-
65
- Test controller actions directly via `Controller.call`, bypassing AMQP entirely:
66
-
67
- ```ruby
68
- RSpec.describe NodesController do
69
- let(:node) { Node.new(id: '42', name: 'web-01', status: 'active') }
70
-
71
- describe '#show' do
72
- it 'returns the node as JSON' do
73
- allow(Node).to receive(:find).with('42').and_return(node)
74
-
75
- response = NodesController.call(
76
- headers: { type: 'nodes/42', 'x-http-method' => 'GET' },
77
- body: ''
78
- )
79
-
80
- expect(response[:status]).to eq(200)
81
- expect(response[:body][:name]).to eq('web-01')
82
- end
83
- end
84
-
85
- describe '#show with missing node' do
86
- it 'returns 404' do
87
- allow(Node).to receive(:find).with('99').and_return(nil)
88
-
89
- response = NodesController.call(
90
- headers: { type: 'nodes/99', 'x-http-method' => 'GET' },
91
- body: ''
92
- )
93
-
94
- expect(response[:status]).to eq(404)
95
- end
96
- end
97
- end
98
- ```
99
-
100
- ---
101
-
102
- ## Unit Testing before_action / after_action
103
-
104
- ```ruby
105
- RSpec.describe NodesController do
106
- describe 'before_action :authenticate!' do
107
- it 'returns 401 when token is missing' do
108
- response = NodesController.call(
109
- headers: { type: 'nodes', 'x-http-method' => 'GET' },
110
- body: ''
111
- # no X-Service-Token header
112
- )
113
-
114
- expect(response[:status]).to eq(401)
115
- end
116
- end
117
-
118
- describe 'after_action :emit_audit_event' do
119
- it 'emits an audit event after create' do
120
- expect(AuditLog).to receive(:record).with(hash_including(action: 'create'))
121
-
122
- NodesController.call(
123
- headers: { type: 'nodes', 'x-http-method' => 'POST', 'X-Service-Token' => 'valid' },
124
- body: '{"node":{"name":"web-01","status":"pending"}}'
125
- )
126
- end
127
- end
128
- end
129
- ```
130
-
131
- ---
132
-
133
- ## Unit Testing Consumer Middleware
134
-
135
- ```ruby
136
- RSpec.describe TracingMiddleware do
137
- subject(:middleware) { described_class.new(terminal) }
138
-
139
- let(:terminal) { ->(di, props, body) { :processed } }
140
- let(:delivery_info) { instance_double(Bunny::DeliveryInfo, routing_key: 'nodes') }
141
- let(:properties) do
142
- instance_double(Bunny::MessageProperties,
143
- headers: { 'X-Trace-Id' => 'trace-123' },
144
- correlation_id: 'corr-456'
145
- )
146
- end
147
-
148
- it 'extracts the trace header and sets context' do
149
- expect(MyTracer).to receive(:with_trace).with('trace-123').and_yield
150
-
151
- middleware.call(delivery_info, properties, '{}')
152
- end
153
-
154
- it 'generates a new trace id when header is absent' do
155
- allow(properties).to receive(:headers).and_return({})
156
- expect(MyTracer).to receive(:with_trace).with(be_a(String)).and_yield
157
-
158
- middleware.call(delivery_info, properties, '{}')
159
- end
160
- end
161
- ```
162
-
163
- ---
164
-
165
- ## Integration Testing with a Mock Consumer
166
-
167
- For tests that exercise the full routing + controller stack, use an in-process integration helper:
168
-
169
- ```ruby
170
- # spec/support/integration_helper.rb
171
-
172
- module IntegrationHelper
173
- def process_message(method:, path:, body: '', headers: {})
174
- delivery_info = instance_double(Bunny::DeliveryInfo,
175
- delivery_tag: 1,
176
- routing_key: 'test'
177
- )
178
-
179
- properties = instance_double(Bunny::MessageProperties,
180
- type: path,
181
- reply_to: nil,
182
- correlation_id: SecureRandom.uuid,
183
- headers: { 'x-http-method' => method.to_s.upcase }.merge(headers)
184
- )
185
-
186
- consumer = BugBunny::Consumer.new
187
- # Allow channel operations on the mock session
188
- allow(consumer.session.channel).to receive(:ack)
189
- allow(consumer.session.channel).to receive(:reject)
190
- allow(consumer.session.channel).to receive(:default_exchange).and_return(build_bunny_exchange)
191
-
192
- consumer.send(:process_message, delivery_info, properties, body.to_json)
193
- end
194
- end
195
-
196
- RSpec.configure do |config|
197
- config.include IntegrationHelper, type: :integration
198
- end
199
- ```
200
-
201
- ```ruby
202
- # spec/integration/nodes_spec.rb, type: :integration
203
- RSpec.describe 'Nodes API', type: :integration do
204
- before do
205
- BugBunny.routes.draw { resources :nodes }
206
- end
207
-
208
- it 'routes GET nodes/:id to NodesController#show' do
209
- allow(Node).to receive(:find).with('42').and_return(Node.new(id: 42, name: 'web-01'))
210
-
211
- process_message(method: :get, path: 'nodes/42')
212
-
213
- expect(Node).to have_received(:find).with('42')
214
- end
215
-
216
- it 'returns 404 for unregistered routes' do
217
- response = process_message(method: :get, path: 'unknown/path')
218
- # response is captured via the consumer's handle_fatal_error path
219
- end
220
- end
221
- ```
222
-
223
- ---
224
-
225
- ## Unit Testing Configuration Validation
226
-
227
- ```ruby
228
- RSpec.describe BugBunny::Configuration do
229
- describe '#validate!' do
230
- it 'raises ConfigurationError when host is blank' do
231
- config = described_class.new
232
- config.host = ''
233
-
234
- expect { config.validate! }.to raise_error(BugBunny::ConfigurationError, /host is required/)
235
- end
236
-
237
- it 'raises ConfigurationError when port is out of range' do
238
- config = described_class.new
239
- config.host = 'localhost'
240
- config.port = 99_999
241
-
242
- expect { config.validate! }.to raise_error(BugBunny::ConfigurationError, /port must be in/)
243
- end
244
- end
245
- end
246
- ```
247
-
248
- ---
249
-
250
- ## Running Tests
251
-
252
- ```bash
253
- source /opt/homebrew/opt/chruby/share/chruby/chruby.sh && chruby ruby-3.3.8
254
-
255
- bundle exec rspec # all tests
256
- bundle exec rspec spec/unit/ # unit tests only
257
- bundle exec rspec spec/integration/ # integration tests only
258
- bundle exec rspec spec/unit/consumer_spec.rb # single file
259
- ```
@@ -1,119 +0,0 @@
1
- # Distributed Tracing
2
-
3
- BugBunny propagates trace context through the full RPC cycle, from the producer to the consumer and back. The mechanism is tracer-agnostic: BugBunny provides hooks and you supply the tracer-specific logic.
4
-
5
- ## What BugBunny Propagates by Default
6
-
7
- The `correlation_id` AMQP property travels automatically from producer to consumer on every request. The Consumer wraps the entire execution in a `logger.tagged(correlation_id)` block when the logger supports tagged logging (Rails' `ActiveSupport::TaggedLogging`).
8
-
9
- This alone connects producer and consumer log lines without any configuration.
10
-
11
- ---
12
-
13
- ## Full Bidirectional Propagation
14
-
15
- For distributed tracing systems (OpenTelemetry, AWS X-Ray, Datadog APM, etc.) you need to:
16
-
17
- 1. **Inject** the current trace header into outgoing requests (producer side).
18
- 2. **Extract** the trace header from incoming messages (consumer side) — done via Consumer Middleware.
19
- 3. **Re-inject** the updated trace header into the RPC reply (consumer side).
20
- 4. **Hydrate** the trace context from the reply headers back in the calling thread (producer side).
21
-
22
- Steps 3 and 4 handle the case where the consumer creates a child span — the parent needs to know about it.
23
-
24
- ---
25
-
26
- ## Configuration Hooks
27
-
28
- ### `rpc_reply_headers`
29
-
30
- A `Proc` called in the consumer thread just before sending the RPC reply. Its return value is merged into the reply AMQP headers.
31
-
32
- ```ruby
33
- BugBunny.configure do |config|
34
- config.rpc_reply_headers = -> {
35
- { 'X-Trace-Header' => MyTracer.outgoing_header }
36
- }
37
- end
38
- ```
39
-
40
- Zero overhead when not set.
41
-
42
- ### `on_rpc_reply`
43
-
44
- A `Proc` called in the producer thread after the RPC reply arrives, with the reply headers as argument. Use it to hydrate the trace context in the calling thread.
45
-
46
- ```ruby
47
- BugBunny.configure do |config|
48
- config.on_rpc_reply = ->(headers) {
49
- MyTracer.hydrate(headers['X-Trace-Header'])
50
- }
51
- end
52
- ```
53
-
54
- ---
55
-
56
- ## Full Example (tracer-agnostic)
57
-
58
- ```ruby
59
- # config/initializers/bug_bunny.rb
60
-
61
- BugBunny.configure do |config|
62
- # ... connection config ...
63
-
64
- # Step 3: inject updated trace header into RPC reply
65
- config.rpc_reply_headers = -> {
66
- { 'X-Trace-Header' => MyTracer.generate_outgoing_header }
67
- }
68
-
69
- # Step 4: hydrate trace context in the producer thread after reply
70
- config.on_rpc_reply = ->(headers) {
71
- MyTracer.hydrate_from_header(headers['X-Trace-Header'])
72
- }
73
- end
74
-
75
- # Step 1: inject trace header into outgoing requests (client middleware)
76
- class TraceInjectionMiddleware < BugBunny::Middleware::Base
77
- def call(request)
78
- request.headers['X-Trace-Header'] = MyTracer.generate_outgoing_header
79
- app.call(request)
80
- end
81
- end
82
-
83
- # Step 2: extract trace header from incoming messages (consumer middleware)
84
- class TraceExtractionMiddleware < BugBunny::ConsumerMiddleware::Base
85
- def call(delivery_info, properties, body)
86
- incoming_header = properties.headers&.dig('X-Trace-Header')
87
- MyTracer.with_trace_from_header(incoming_header) do
88
- @app.call(delivery_info, properties, body)
89
- end
90
- end
91
- end
92
-
93
- # Register both
94
- BugBunny::Resource.client_middleware { |s| s.use TraceInjectionMiddleware }
95
- BugBunny.consumer_middlewares.use TraceExtractionMiddleware
96
- ```
97
-
98
- ---
99
-
100
- ## Fire-and-Forget Tracing
101
-
102
- For `:publish` (fire-and-forget) calls, there is no reply, so `on_rpc_reply` and `rpc_reply_headers` do not apply. Use only the client middleware to inject the outgoing trace header:
103
-
104
- ```ruby
105
- class TraceInjectionMiddleware < BugBunny::Middleware::Base
106
- def call(request)
107
- request.headers['X-Trace-Header'] = MyTracer.generate_outgoing_header
108
- app.call(request)
109
- end
110
- end
111
- ```
112
-
113
- The consumer middleware extracts it on the other side regardless of whether the call was RPC or fire-and-forget.
114
-
115
- ---
116
-
117
- ## correlation_id
118
-
119
- BugBunny sets `correlation_id` automatically on every RPC request (used internally to match replies). It is also forwarded as a log tag. If your tracer uses a separate header (e.g., `traceparent`), use the `X-Trace-Header` pattern above. If you want to use `correlation_id` as the trace ID, read it from `properties.correlation_id` in your consumer middleware.