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.
- checksums.yaml +4 -4
- data/.agents/skills/documentation-writer/SKILL.md +45 -0
- data/.agents/skills/gem-release/SKILL.md +116 -0
- data/.agents/skills/quality-code/SKILL.md +51 -0
- data/.agents/skills/sentry/SKILL.md +135 -0
- data/.agents/skills/sentry/references/api-endpoints.md +147 -0
- data/.agents/skills/sentry/scripts/sentry.rb +194 -0
- data/.agents/skills/skill-builder/SKILL.md +293 -0
- data/.agents/skills/skill-manager/SKILL.md +225 -0
- data/.agents/skills/skill-manager/scripts/sync.rb +356 -0
- data/.agents/skills/yard/SKILL.md +311 -0
- data/.agents/skills/yard/references/tipos.md +144 -0
- data/CHANGELOG.md +14 -0
- data/CLAUDE.md +28 -225
- data/README.md +5 -3
- data/lib/bug_bunny/consumer.rb +21 -5
- data/lib/bug_bunny/otel.rb +47 -0
- data/lib/bug_bunny/producer.rb +13 -4
- data/lib/bug_bunny/request.rb +14 -2
- data/lib/bug_bunny/version.rb +1 -1
- data/lib/bug_bunny.rb +1 -0
- data/skill/SKILL.md +253 -0
- data/skill/references/client-middleware.md +161 -0
- data/skill/references/consumer.md +122 -0
- data/skill/references/controller.md +105 -0
- data/skill/references/errores.md +97 -0
- data/skill/references/resource.md +116 -0
- data/skill/references/routing.md +82 -0
- data/skill/references/testing.md +138 -0
- data/skills.lock +30 -0
- data/skills.yml +40 -0
- data/spec/integration/consumer_middleware_spec.rb +23 -2
- data/spec/unit/consumer_spec.rb +138 -6
- data/spec/unit/otel_spec.rb +54 -0
- data/spec/unit/producer_spec.rb +187 -0
- data/spec/unit/request_spec.rb +51 -0
- metadata +28 -29
- data/.agents/skills/rabbitmq-expert/SKILL.md +0 -1555
- data/.claude/commands/gem-ai-setup.md +0 -174
- data/.claude/commands/pr.md +0 -53
- data/.claude/commands/release.md +0 -52
- data/.claude/commands/rubocop.md +0 -22
- data/.claude/commands/service-ai-setup.md +0 -168
- data/.claude/commands/test.md +0 -28
- data/.claude/commands/yard.md +0 -46
- data/docs/_index.md +0 -50
- data/docs/ai/_index.md +0 -56
- data/docs/ai/antipatterns.md +0 -166
- data/docs/ai/api.md +0 -251
- data/docs/ai/architecture.md +0 -92
- data/docs/ai/errors.md +0 -158
- data/docs/ai/faq_external.md +0 -133
- data/docs/ai/faq_internal.md +0 -86
- data/docs/ai/glossary.md +0 -45
- data/docs/concepts.md +0 -140
- data/docs/howto/controller.md +0 -194
- data/docs/howto/middleware_client.md +0 -119
- data/docs/howto/middleware_consumer.md +0 -127
- data/docs/howto/rails.md +0 -214
- data/docs/howto/resource.md +0 -200
- data/docs/howto/routing.md +0 -133
- data/docs/howto/testing.md +0 -259
- 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:
|
|
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
|
-
|
|
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
|