bug_bunny 4.8.0 → 4.9.0

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 (63) 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 +116 -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 +293 -0
  9. data/.agents/skills/skill-manager/SKILL.md +225 -0
  10. data/.agents/skills/skill-manager/scripts/sync.rb +356 -0
  11. data/.agents/skills/yard/SKILL.md +311 -0
  12. data/.agents/skills/yard/references/tipos.md +144 -0
  13. data/CHANGELOG.md +14 -0
  14. data/CLAUDE.md +28 -225
  15. data/README.md +5 -3
  16. data/lib/bug_bunny/consumer.rb +21 -5
  17. data/lib/bug_bunny/otel.rb +47 -0
  18. data/lib/bug_bunny/producer.rb +13 -4
  19. data/lib/bug_bunny/request.rb +14 -2
  20. data/lib/bug_bunny/version.rb +1 -1
  21. data/lib/bug_bunny.rb +1 -0
  22. data/skill/SKILL.md +253 -0
  23. data/skill/references/client-middleware.md +161 -0
  24. data/skill/references/consumer.md +122 -0
  25. data/skill/references/controller.md +105 -0
  26. data/skill/references/errores.md +97 -0
  27. data/skill/references/resource.md +116 -0
  28. data/skill/references/routing.md +82 -0
  29. data/skill/references/testing.md +138 -0
  30. data/skills.lock +30 -0
  31. data/skills.yml +40 -0
  32. data/spec/integration/consumer_middleware_spec.rb +23 -2
  33. data/spec/unit/consumer_spec.rb +138 -6
  34. data/spec/unit/otel_spec.rb +54 -0
  35. data/spec/unit/producer_spec.rb +187 -0
  36. data/spec/unit/request_spec.rb +51 -0
  37. metadata +28 -29
  38. data/.agents/skills/rabbitmq-expert/SKILL.md +0 -1555
  39. data/.claude/commands/gem-ai-setup.md +0 -174
  40. data/.claude/commands/pr.md +0 -53
  41. data/.claude/commands/release.md +0 -52
  42. data/.claude/commands/rubocop.md +0 -22
  43. data/.claude/commands/service-ai-setup.md +0 -168
  44. data/.claude/commands/test.md +0 -28
  45. data/.claude/commands/yard.md +0 -46
  46. data/docs/_index.md +0 -50
  47. data/docs/ai/_index.md +0 -56
  48. data/docs/ai/antipatterns.md +0 -166
  49. data/docs/ai/api.md +0 -251
  50. data/docs/ai/architecture.md +0 -92
  51. data/docs/ai/errors.md +0 -158
  52. data/docs/ai/faq_external.md +0 -133
  53. data/docs/ai/faq_internal.md +0 -86
  54. data/docs/ai/glossary.md +0 -45
  55. data/docs/concepts.md +0 -140
  56. data/docs/howto/controller.md +0 -194
  57. data/docs/howto/middleware_client.md +0 -119
  58. data/docs/howto/middleware_consumer.md +0 -127
  59. data/docs/howto/rails.md +0 -214
  60. data/docs/howto/resource.md +0 -200
  61. data/docs/howto/routing.md +0 -133
  62. data/docs/howto/testing.md +0 -259
  63. data/docs/howto/tracing.md +0 -119
@@ -0,0 +1,97 @@
1
+ # Catálogo de Errores
2
+
3
+ ## Jerarquía
4
+
5
+ ```
6
+ StandardError
7
+ └── BugBunny::Error
8
+ ├── BugBunny::CommunicationError
9
+ ├── BugBunny::ConfigurationError
10
+ ├── BugBunny::SecurityError
11
+ ├── BugBunny::ClientError (4xx)
12
+ │ ├── BugBunny::BadRequest (400)
13
+ │ ├── BugBunny::NotFound (404)
14
+ │ ├── BugBunny::NotAcceptable (406)
15
+ │ ├── BugBunny::RequestTimeout (408)
16
+ │ ├── BugBunny::Conflict (409)
17
+ │ └── BugBunny::UnprocessableEntity (422)
18
+ └── BugBunny::ServerError (5xx)
19
+ └── BugBunny::InternalServerError (500+)
20
+ ```
21
+
22
+ ## Errores de Infraestructura
23
+
24
+ ### BugBunny::CommunicationError
25
+ **Causa:** Fallo de conexión TCP/AMQP o reconexión agotada (`max_reconnect_attempts`).
26
+ **Cuándo:** Al intentar publicar o consumir sin conexión activa, o tras agotar intentos de reconexión.
27
+ **Resolución:** Verificar conectividad a RabbitMQ (host, port, firewall). Revisar logs `event=session.reconnect_failed`. Ajustar `max_reconnect_attempts` y `max_reconnect_interval`.
28
+
29
+ ### BugBunny::ConfigurationError
30
+ **Causa:** Campo requerido faltante o valor fuera de rango en `BugBunny.configure`.
31
+ **Validaciones:** host (String no vacío), port (1-65535), username/password (no nil), heartbeat (0-3600), rpc_timeout (>0), channel_prefetch (1-10000).
32
+ **Resolución:** Revisar el bloque `BugBunny.configure` y corregir valores.
33
+
34
+ ### BugBunny::SecurityError
35
+ **Causa:** Un mensaje intenta ejecutar un controlador que no hereda de `BugBunny::Controller`.
36
+ **Cuándo:** El consumer resuelve la clase pero falla la validación `is_a?(BugBunny::Controller)`.
37
+ **Resolución:** Verificar que el controlador herede de `BugBunny::Controller` y que `config.controller_namespace` sea correcto.
38
+
39
+ ## Errores de Cliente (4xx)
40
+
41
+ ### BugBunny::BadRequest (400)
42
+ **Causa:** Request malformado o sintaxis inválida.
43
+ **Resolución:** Verificar formato del body y headers.
44
+
45
+ ### BugBunny::NotFound (404)
46
+ **Causa:** El recurso solicitado no existe en el servicio remoto.
47
+ **Resolución:** Verificar ID del recurso y que el endpoint exista.
48
+
49
+ ### BugBunny::NotAcceptable (406)
50
+ **Causa:** Negociación de contenido falló.
51
+ **Resolución:** Verificar `content_type` del request.
52
+
53
+ ### BugBunny::RequestTimeout (408)
54
+ **Causa:** No hubo respuesta en `config.rpc_timeout` segundos.
55
+ **Cuándo:** El `Concurrent::IVar` expira sin recibir reply.
56
+ **Resolución:** Verificar que el worker esté activo. Revisar saturación de prefetch. Aumentar `rpc_timeout` si el procesamiento es legítimamente lento.
57
+
58
+ ### BugBunny::Conflict (409)
59
+ **Causa:** Conflicto de regla de negocio (ej: recurso ya existe, versión desactualizada).
60
+ **Resolución:** Reintentar tras refrescar el estado del recurso.
61
+
62
+ ### BugBunny::UnprocessableEntity (422)
63
+ **Causa:** Fallo de validación en el servicio remoto.
64
+ **Acceso a errores:**
65
+ ```ruby
66
+ begin
67
+ order.save
68
+ rescue BugBunny::UnprocessableEntity => e
69
+ e.error_messages # Hash, Array o String con detalles
70
+ e.raw_response # Response original completo
71
+ end
72
+ ```
73
+ **Smart extraction:** Busca `errors`, `error`, `detail`, `message` en el body. Formatea como Hash descriptivo si no encuentra convención.
74
+ **En Resource:** `save` captura 422 automáticamente, carga `resource.errors` y retorna `false`.
75
+
76
+ ## Errores de Servidor (5xx)
77
+
78
+ ### BugBunny::InternalServerError (500+)
79
+ **Causa:** Error no controlado en el servicio remoto.
80
+ **Resolución:** Revisar logs del servicio remoto. Verificar backtrace en `event=controller.unhandled_exception`.
81
+
82
+ ### BugBunny::ServerError (base 5xx)
83
+ **Causa:** Cualquier error de servidor no mapeado a InternalServerError.
84
+ **Resolución:** Similar a InternalServerError.
85
+
86
+ ## Formato de Mensajes de Error
87
+
88
+ El middleware `RaiseError` construye el mensaje así:
89
+ 1. Busca `{ "error": "...", "detail": "..." }` en el body.
90
+ 2. Si no encuentra, usa el Hash completo como JSON.
91
+ 3. Si el body está vacío, usa `"Unknown Error"`.
92
+
93
+ ## Connection Pool Missing
94
+
95
+ **No es una excepción BugBunny**, pero es un error común:
96
+ **Causa:** Se intentó usar un Resource sin asignar el pool global.
97
+ **Resolución:** Asegurar que `BugBunny::Resource.connection_pool = MY_POOL` se ejecute en el arranque.
@@ -0,0 +1,116 @@
1
+ # Resources
2
+
3
+ ## Definición
4
+
5
+ ```ruby
6
+ class Order < BugBunny::Resource
7
+ # Infraestructura AMQP
8
+ @connection_pool = MY_POOL
9
+ @exchange = 'orders_ex'
10
+ @exchange_type = 'topic'
11
+ @resource_name = 'orders' # path en la URL
12
+ @routing_key = 'orders.#'
13
+ @param_key = 'order' # wrapper key en payloads
14
+ @exchange_options = { durable: true }
15
+ @queue_options = { auto_delete: false }
16
+
17
+ # Atributos tipados (ActiveModel::Attributes)
18
+ attribute :id, :integer
19
+ attribute :status, :string
20
+ attribute :total, :decimal
21
+ attribute :active, :boolean
22
+
23
+ # Validaciones (ActiveModel::Validations)
24
+ validates :status, presence: true
25
+
26
+ # Callbacks
27
+ before_save :normalize_status
28
+ after_create :notify_warehouse
29
+ around_destroy :audit_deletion
30
+
31
+ # Middleware client-side
32
+ client_middleware do |stack|
33
+ stack.use BugBunny::Middleware::RaiseError
34
+ stack.use BugBunny::Middleware::JsonResponse
35
+ end
36
+ end
37
+ ```
38
+
39
+ ## Operaciones CRUD
40
+
41
+ ### Class Methods
42
+
43
+ ```ruby
44
+ Order.find(42) # GET orders/42 → Order
45
+ Order.where(status: 'active') # GET orders?status=active → [Order, ...]
46
+ Order.all # GET orders → [Order, ...]
47
+ Order.create(status: 'pending', total: 100) # POST orders → Order
48
+ ```
49
+
50
+ ### Instance Methods
51
+
52
+ ```ruby
53
+ order = Order.new(status: 'pending')
54
+ order.save # POST orders (nuevo) o PUT orders/42 (existente)
55
+ order.update(status: 'shipped') # assign + save
56
+ order.destroy # DELETE orders/42
57
+ order.persisted? # true si fue guardado
58
+ order.changed? # true si tiene cambios sin guardar
59
+ order.errors # ActiveModel::Errors
60
+ ```
61
+
62
+ ### Save: Create vs Update
63
+
64
+ - **Nuevo** (`persisted? == false`): Envía POST con todos los atributos.
65
+ - **Existente** (`persisted? == true`): Envía PUT solo con atributos cambiados (`changes_to_send`).
66
+ - Captura `BugBunny::UnprocessableEntity` (422) y carga `resource.errors`. Retorna `false`.
67
+
68
+ ## Contexto Dinámico (.with)
69
+
70
+ ### Forma de bloque (recomendada)
71
+
72
+ ```ruby
73
+ Order.with(exchange: 'priority_ex', routing_key: 'priority.orders') do
74
+ Order.all # Usa config temporal
75
+ Order.find(1) # También usa config temporal
76
+ end
77
+ # Config restaurada automáticamente
78
+ ```
79
+
80
+ ### Forma de cadena (single use)
81
+
82
+ ```ruby
83
+ order = Order.with(pool: special_pool).find(42)
84
+ # Siguiente llamada requiere nuevo .with()
85
+ ```
86
+
87
+ **Antipatrón:** No guardar el proxy en variable para múltiples llamadas → lanza error.
88
+
89
+ ## Change Tracking
90
+
91
+ Combina `ActiveModel::Dirty` con atributos dinámicos:
92
+
93
+ ```ruby
94
+ order = Order.find(42)
95
+ order.name = 'New Name' # Atributo definido
96
+ order.custom_field = 'value' # Atributo dinámico
97
+ order.changed # → ['name', 'custom_field']
98
+ order.changes_to_send # → { 'name' => 'New Name', 'custom_field' => 'value' }
99
+ ```
100
+
101
+ ## Callbacks Disponibles
102
+
103
+ Definidos con `define_model_callbacks`:
104
+ - `:save` — before/after/around save (create o update)
105
+ - `:create` — before/after/around create (recurso nuevo)
106
+ - `:update` — before/after/around update (recurso existente)
107
+ - `:destroy` — before/after/around destroy
108
+
109
+ ## Coerción de Tipos
110
+
111
+ Los atributos tipados usan `ActiveModel::Attributes`:
112
+ - `'25.50'` → `BigDecimal` (con `:decimal`)
113
+ - `'1'` / `'true'` → `true` (con `:boolean`)
114
+ - `'2026-04-01T...'` → `Time` (con `:time`)
115
+
116
+ Los atributos dinámicos (no declarados) se almacenan sin coerción.
@@ -0,0 +1,82 @@
1
+ # Routing
2
+
3
+ ## DSL Completo
4
+
5
+ ```ruby
6
+ BugBunny.routes.draw do
7
+ # Verbos HTTP individuales
8
+ get 'health', to: 'health#check'
9
+ post 'events', to: 'events#create'
10
+ put 'settings', to: 'settings#update'
11
+ patch 'settings', to: 'settings#patch'
12
+ delete 'cache', to: 'cache#clear'
13
+
14
+ # Resources genera 5 rutas REST
15
+ resources :users
16
+ # GET users → UsersController#index
17
+ # GET users/:id → UsersController#show
18
+ # POST users → UsersController#create
19
+ # PUT users/:id → UsersController#update
20
+ # DELETE users/:id → UsersController#destroy
21
+
22
+ # Filtros
23
+ resources :orders, only: [:index, :show]
24
+ resources :products, except: [:destroy]
25
+
26
+ # Namespaces (anidables)
27
+ namespace :admin do
28
+ namespace :v2 do
29
+ resources :reports # → Admin::V2::ReportsController
30
+ end
31
+ end
32
+
33
+ # Member y Collection
34
+ resources :nodes do
35
+ member do
36
+ put :drain # PUT nodes/:id/drain → NodesController#drain
37
+ post :restart # POST nodes/:id/restart → NodesController#restart
38
+ end
39
+ collection do
40
+ get :stats # GET nodes/stats → NodesController#stats
41
+ get :health # GET nodes/health → NodesController#health
42
+ end
43
+ end
44
+ end
45
+ ```
46
+
47
+ ## Route Matching
48
+
49
+ El `RouteSet#recognize(method, path)` busca la primera ruta que matchee:
50
+
51
+ - **Normalización:** Strips leading/trailing slashes del path.
52
+ - **Parámetros:** `:id` se compila a regex `(?<id>[^/]+)`. Los valores se extraen como Hash.
53
+ - **No match:** Retorna `nil` → el consumer responde 404.
54
+
55
+ ```ruby
56
+ route = BugBunny.routes.recognize('GET', 'users/42')
57
+ route.controller # => "users"
58
+ route.action # => "show"
59
+ route.namespace # => nil
60
+ route.params # => { 'id' => '42' }
61
+ ```
62
+
63
+ ## Resolución de Controller
64
+
65
+ El consumer resuelve el controlador concatenando:
66
+ 1. `config.controller_namespace` (default: `BugBunny::Controllers`)
67
+ 2. `route.namespace` (si existe)
68
+ 3. `route.controller.classify + "Controller"`
69
+
70
+ Ejemplo: namespace `:admin`, controller `:reports` → `BugBunny::Controllers::Admin::ReportsController`
71
+
72
+ Valida que el controlador sea subclase de `BugBunny::Controller`. Si no, lanza `SecurityError`.
73
+
74
+ ## Route Object
75
+
76
+ ```ruby
77
+ route.http_method # String: "GET", "POST", etc.
78
+ route.path_pattern # String: "users/:id"
79
+ route.controller # String: "users"
80
+ route.action # String: "show"
81
+ route.namespace # String o nil: "Admin::V2"
82
+ ```
@@ -0,0 +1,138 @@
1
+ # Testing
2
+
3
+ ## Estructura
4
+
5
+ ```
6
+ spec/
7
+ ├── spec_helper.rb
8
+ ├── support/
9
+ │ ├── bunny_mocks.rb # Stubs para unit tests
10
+ │ └── integration_helper.rb # Helpers para integration tests
11
+ ├── unit/ # Sin RabbitMQ real
12
+ │ ├── configuration_spec.rb
13
+ │ ├── client_session_pool_spec.rb
14
+ │ ├── consumer_spec.rb
15
+ │ ├── session_spec.rb
16
+ │ ├── consumer_middleware_spec.rb
17
+ │ ├── controller_after_action_spec.rb
18
+ │ ├── observability_spec.rb
19
+ │ └── resource_attributes_spec.rb
20
+ └── integration/ # Requiere RabbitMQ
21
+ ├── client_spec.rb
22
+ ├── consumer_middleware_spec.rb
23
+ ├── controller_spec.rb
24
+ ├── error_handling_spec.rb
25
+ ├── infrastructure_spec.rb
26
+ └── resource_spec.rb
27
+ ```
28
+
29
+ ## Unit Tests — Mocking de Bunny
30
+
31
+ Los unit tests usan `BunnyMocks` para evitar dependencia de RabbitMQ:
32
+
33
+ ```ruby
34
+ # spec/support/bunny_mocks.rb
35
+ BunnyMocks::FakeChannel # Simula canal Bunny
36
+ BunnyMocks::FakeConnection # Simula conexión Bunny
37
+ ```
38
+
39
+ Patrón de uso:
40
+
41
+ ```ruby
42
+ let(:connection) { BunnyMocks::FakeConnection.new }
43
+ let(:session) { BugBunny::Session.new(connection) }
44
+ ```
45
+
46
+ Para Producer: `allow_any_instance_of(BugBunny::Producer).to receive(:rpc).and_return(response)`
47
+
48
+ ## Integration Tests — Helpers
49
+
50
+ ### with_running_worker
51
+
52
+ Levanta un consumer real en un thread:
53
+
54
+ ```ruby
55
+ with_running_worker(
56
+ queue: unique('test_q'),
57
+ exchange: unique('test_ex'),
58
+ exchange_type: 'topic',
59
+ routing_key: 'users.#'
60
+ ) do
61
+ response = client.request('users/1', method: :get)
62
+ expect(response['status']).to eq(200)
63
+ end
64
+ # Worker se detiene automáticamente al salir del bloque
65
+ ```
66
+
67
+ ### with_spy_worker
68
+
69
+ Captura mensajes sin procesarlos:
70
+
71
+ ```ruby
72
+ with_spy_worker(queue:, exchange:) do |messages|
73
+ client.publish('events', body: { type: 'test' })
74
+ msg = wait_for_message(messages, 5)
75
+ expect(msg[:body]).to include('type' => 'test')
76
+ end
77
+ ```
78
+
79
+ ### unique(name)
80
+
81
+ Genera nombres únicos para evitar colisiones entre tests:
82
+
83
+ ```ruby
84
+ unique('my_queue') # → "my_queue_a3f1b2c4"
85
+ # Usa SecureRandom.hex(4)
86
+ ```
87
+
88
+ ## Thread Safety Testing
89
+
90
+ Patrón con `Concurrent::CyclicBarrier`:
91
+
92
+ ```ruby
93
+ barrier = Concurrent::CyclicBarrier.new(10)
94
+ counter = Concurrent::AtomicFixnum.new(0)
95
+
96
+ threads = 10.times.map do
97
+ Thread.new do
98
+ barrier.wait # Sincroniza inicio
99
+ session.channel
100
+ counter.increment
101
+ end
102
+ end
103
+
104
+ threads.each(&:join)
105
+ expect(counter.value).to eq(10)
106
+ ```
107
+
108
+ ## Skip de Integration Tests
109
+
110
+ Los tests `:integration` se skipean automáticamente si RabbitMQ no está disponible:
111
+
112
+ ```ruby
113
+ # spec_helper.rb
114
+ config.before(:each, :integration) do
115
+ skip 'RabbitMQ not available' unless rabbitmq_available?
116
+ end
117
+ ```
118
+
119
+ ## Configuración de Test
120
+
121
+ ```ruby
122
+ BugBunny.configure do |config|
123
+ config.host = ENV.fetch('RABBITMQ_HOST', 'localhost')
124
+ config.username = ENV.fetch('RABBITMQ_USER', 'guest')
125
+ config.password = ENV.fetch('RABBITMQ_PASS', 'guest')
126
+ end
127
+
128
+ TEST_POOL = ConnectionPool.new(size: 5) { BugBunny.create_connection }
129
+ ```
130
+
131
+ ## Ejecutar Tests
132
+
133
+ ```bash
134
+ bundle exec rspec # Todos
135
+ bundle exec rspec spec/unit/ # Solo unit
136
+ bundle exec rspec spec/integration/ # Solo integration (requiere RabbitMQ)
137
+ bundle exec rspec spec/unit/session_spec.rb # Archivo específico
138
+ ```
data/skills.lock ADDED
@@ -0,0 +1,30 @@
1
+ ---
2
+ synced_at: '2026-04-05 13:40:39'
3
+ skills:
4
+ - name: agent-review
5
+ scope: local
6
+ path: "/Users/gabriel/src/gems/bug_bunny/.agents/skills/agent-review"
7
+ - name: ai-reports
8
+ scope: local
9
+ path: "/Users/gabriel/src/gems/bug_bunny/.agents/skills/ai-reports"
10
+ - name: documentation-writer
11
+ scope: local
12
+ path: "/Users/gabriel/src/gems/bug_bunny/.agents/skills/documentation-writer"
13
+ - name: find-skills
14
+ scope: local
15
+ path: "/Users/gabriel/src/gems/bug_bunny/.agents/skills/find-skills"
16
+ - name: gem-release
17
+ scope: local
18
+ path: "/Users/gabriel/src/gems/bug_bunny/.agents/skills/gem-release"
19
+ - name: quality-code
20
+ scope: local
21
+ path: "/Users/gabriel/src/gems/bug_bunny/.agents/skills/quality-code"
22
+ - name: skill-builder
23
+ scope: local
24
+ path: "/Users/gabriel/src/gems/bug_bunny/.agents/skills/skill-builder"
25
+ - name: skill-manager
26
+ scope: local
27
+ path: "/Users/gabriel/src/gems/bug_bunny/.agents/skills/skill-manager"
28
+ - name: yard
29
+ scope: local
30
+ path: "/Users/gabriel/src/gems/bug_bunny/.agents/skills/yard"
data/skills.yml ADDED
@@ -0,0 +1,40 @@
1
+ mcps:
2
+ - github
3
+ - clickup
4
+ skills:
5
+ skill-manager:
6
+ repo: sequre/ai_knowledge
7
+ scope: local
8
+ yard:
9
+ repo: sequre/ai_knowledge
10
+ scope: local
11
+ quality-code:
12
+ repo: sequre/ai_knowledge
13
+ scope: local
14
+ gem-release:
15
+ repo: sequre/ai_knowledge
16
+ scope: local
17
+ skill-builder:
18
+ repo: sequre/ai_knowledge
19
+ scope: local
20
+ ai-reports:
21
+ repo: sequre/ai_knowledge
22
+ scope: local
23
+ environment:
24
+ space_id: "${AI_REPORTS_SPACE_ID}"
25
+ bug_reports_list_id: "${AI_REPORTS_BUG_REPORTS_LIST_ID}"
26
+ improvements_list_id: "${AI_REPORTS_IMPROVEMENTS_LIST_ID}"
27
+ agent-review:
28
+ repo: sequre/ai_knowledge
29
+ scope: local
30
+ environment:
31
+ space_id: "${AGENT_REVIEW_SAPCE_ID}"
32
+ list_id: "${AGENT_LIST_ID}"
33
+ documentation-writer:
34
+ repo: github/awesome-copilot
35
+ path: skills/documentation-writer
36
+ scope: local
37
+ find-skills:
38
+ repo: vercel-labs/skills
39
+ path: skills/find-skills
40
+ scope: local
@@ -24,7 +24,7 @@ class TrackingMiddleware < BugBunny::ConsumerMiddleware::Base
24
24
  def call(delivery_info, properties, body)
25
25
  self.class.calls << {
26
26
  routing_key: delivery_info.routing_key,
27
- headers: properties.headers
27
+ headers: properties.headers
28
28
  }
29
29
  @app.call(delivery_info, properties, body)
30
30
  end
@@ -45,7 +45,7 @@ RSpec.describe 'Consumer Middleware Stack', :integration do
45
45
  BugBunny.configure { |c| c.controller_namespace = 'BugBunny::Controllers' }
46
46
  # Limpiamos el middleware para no afectar otros specs
47
47
  BugBunny.configuration.instance_variable_set(:@consumer_middlewares,
48
- BugBunny::ConsumerMiddleware::Stack.new)
48
+ BugBunny::ConsumerMiddleware::Stack.new)
49
49
  end
50
50
 
51
51
  it 'ejecuta el middleware antes de process_message' do
@@ -82,5 +82,26 @@ RSpec.describe 'Consumer Middleware Stack', :integration do
82
82
  BugBunny.configuration.rpc_reply_headers = nil
83
83
  BugBunny.configuration.on_rpc_reply = nil
84
84
  end
85
+
86
+ it 'incluye los campos OTel semantic conventions en el reply' do
87
+ received_headers = nil
88
+
89
+ BugBunny.configuration.rpc_reply_headers = -> { { 'X-Test-Header' => 'from-consumer' } }
90
+ BugBunny.configuration.on_rpc_reply = ->(headers) { received_headers = headers }
91
+
92
+ with_running_worker(queue: queue, exchange: exchange, routing_key: 'ping') do
93
+ client.request('ping', method: :get, exchange: exchange, exchange_type: 'topic', routing_key: 'ping')
94
+ end
95
+
96
+ expect(received_headers).to include(
97
+ 'messaging_system' => 'rabbitmq',
98
+ 'messaging_operation' => 'publish',
99
+ 'X-Test-Header' => 'from-consumer'
100
+ )
101
+ expect(received_headers['messaging_message_id']).not_to be_nil
102
+ ensure
103
+ BugBunny.configuration.rpc_reply_headers = nil
104
+ BugBunny.configuration.on_rpc_reply = nil
105
+ end
85
106
  end
86
107
  end