bug_bunny 4.17.1 → 4.18.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 50bb70f17d783b52abeb208bc362953c9494b61853f836da84f73552e3a76966
4
- data.tar.gz: 37f9550b64942b1e544a6e7d593c1695f1293c6a1b488e1b0bec3179d1f3b7a4
3
+ metadata.gz: 030517babaaafccb4ac6b067e1c11ac1cf6d4a6a711919c8455c3dcfe6a79637
4
+ data.tar.gz: fb9a551e05882e05efc3cd23215773507ae6d5d2baab70681360c04100724d29
5
5
  SHA512:
6
- metadata.gz: 4733c50dcb6824c4e7d53bc0842e823b0674c13bf6cd9d963f99b6fa090166d083ca64cc6e65eba2fc29bff09be840772a6404fae19e62f3dd9b6dbcf56fc2d3
7
- data.tar.gz: e9613f65b4bce0ad19aa8c19f259c6a3069ba4dfe185e4146a5992ddfbd20b3428816ed882e9d637f2b4a85d1d5d307094763bf8f3082900e8c6d9d3fe8a1fb7
6
+ metadata.gz: a5c4fbaade4883c361a20e705155d2b60dc6b3e6e64905afe34b02a7a7016cd7913bfd9f1f2de09979d357f72b026dbdc3129ad871d128a1b9ee901628a096b6
7
+ data.tar.gz: fb7b8dd6f7b74dee3c6d13b789dd07ff3abaeda535680a1ab7ea4276269e40a238588403e5fd7c8d2cdb3188acd3b7889d867d4dffed6a19af45d91eefa68591
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [4.18.0] - 2026-05-26
4
+
5
+ > Behavior change menor: cualquier `Bunny::Exception` que escape en la frontera del gem (TCP fail, conn rota, canal cerrado, auth fail) ahora se envuelve como `BugBunny::CommunicationError`. La excepción original queda accesible vía `.cause`. Callers que rescatan `Bunny::TCPConnectionFailed` directo deben migrar a `BugBunny::CommunicationError`. Callers que ya rescatan `BugBunny::Error` / `CommunicationError`: sin cambios.
6
+
7
+ ### Correcciones
8
+ - **`Client#publish`/`#request`/`#send` leak de `Bunny::TCPConnectionFailedForAllHosts` (#49):** el TCP fail nacía en `ConnectionPool::TimedStack#try_create` dentro de `@pool.with`, antes de entrar al `Producer#confirmed` que ya envolvía con `rescue StandardError`. `Client#run_in_pool` ahora envuelve cualquier `Bunny::Exception` → `BugBunny::CommunicationError` (cubre también `Bunny::ConnectionClosedError` in-flight sobre conn rota mid-publish). `BugBunny.create_connection` también envuelve (Railtie, scripts, tests). YARD `@raise` actualizado. — @Gabriel
9
+
10
+ ### Mejoras internas
11
+ - `Producer#confirmed` rescue estrechado de `rescue StandardError` a `rescue Bunny::Exception` — no traga bugs internos (`NoMethodError`, `KeyError`) bajo la etiqueta de fallo de transporte. — @Gabriel
12
+ - `CommunicationError` docstring expandido: declara que envuelve cualquier `Bunny::Exception`, no solo TCP, y que `.cause` preserva la original. — @Gabriel
13
+ - `Client#run_in_pool` refactor: extraído `execute_in_pool` privado para mantener `Metrics/AbcSize`. — @Gabriel
14
+
15
+ ### Documentación
16
+ - `docs/behavior/behavior.md` refrescado scoped (RFC-001 §3.3.3 vía quality-code Paso 5): nota de contrato de error wrapping en flujos Fire-and-forget y Confirmed; nueva entrada en §3 Inferencias; verificación humana 2026-05-26. — @Gabriel
17
+ - `README.md` y `skill/SKILL.md` re-indexados vía `dev-compose`: jerarquía de excepciones refinada, gotcha nuevo sobre errores de transporte 4.18+, descripción de `BugBunny::CommunicationError` en errores comunes. — @Gabriel
18
+
3
19
  ## [4.17.1] - 2026-05-19
4
20
 
5
21
  > Release docs-only sobre 4.17.0 — sin cambios en `lib/` ni API pública. Incorpora la familia de skills `dev-*` (RFC-001) y los artefactos de detalle version-locked (`docs/glossary`, `docs/behavior`) que viajan en el `.gem`.
data/README.md CHANGED
@@ -377,7 +377,7 @@ BugBunny maps RabbitMQ responses to a semantic exception hierarchy, similar to h
377
377
 
378
378
  ```
379
379
  BugBunny::Error
380
- ├── CommunicationError (network / channel failure)
380
+ ├── CommunicationError (wraps any Bunny::Exception — TCP/auth/channel — at the gem boundary; .cause preserves the original)
381
381
  ├── ConfigurationError (invalid config attribute)
382
382
  ├── SecurityError (unauthorized controller resolution)
383
383
  ├── PublishNacked (broker basic.nack on :confirmed publish)
@@ -1,6 +1,6 @@
1
1
  # Comportamiento — bug_bunny
2
2
 
3
- > meta: artefacto `comportamiento` · RFC-007 (cadencia incremental default / completo on-demand) · generado dev-enrich 1.3.0 (backfill on-demand) · anclado a `a5cdb10` · cobertura: completa (6 flujos) · verificado por humano 2026-05-18
3
+ > meta: artefacto `comportamiento` · RFC-007 (cadencia incremental default / completo on-demand) · generado dev-enrich 1.3.0 (backfill on-demand) · anclado a `94de2b4` · cobertura: completa (6 flujos) · verificado por humano 2026-05-18 (base) · 2026-05-26 (refresco scoped: contrato de error wrapping post-#49)
4
4
 
5
5
  ## 1. Resumen
6
6
 
@@ -68,6 +68,8 @@ sequenceDiagram
68
68
  ```
69
69
  Contexto: `client.rb:129-134` → `producer.rb:47-51` → `publish_message producer.rb:146-161`.
70
70
 
71
+ **Errores (contrato cliente):** cualquier `Bunny::Exception` que escape durante `@pool.with` (TCP fail en `try_create`, `ConnectionClosedError` in-flight, `ChannelAlreadyClosed`, etc.) se envuelve como `BugBunny::CommunicationError` en `Client#run_in_pool` (`client.rb:155-167`). La excepción original queda en `.cause`. Los callers del `Client` no rescatan tipos `Bunny::*`.
72
+
71
73
  ### Flujo: Confirmed + basic.return bridge
72
74
  ACK del broker síncrono; `basic.return` (mandatory unroutable) llega en el reader thread de Bunny y se puentea al hilo de publish vía `@pending_returns` + `Concurrent::Event` con ventana de tolerancia GVL.
73
75
 
@@ -92,7 +94,7 @@ sequenceDiagram
92
94
  P-->>CL: PublishUnroutable (o éxito si slot vacío)
93
95
  Note over RT,S: on_return corre antes del raise · su excepción se loggea (no propaga)
94
96
  ```
95
- Contexto: `producer.rb:72-93,200-216,281-345`; `session.rb:70-74,204-250`. Branches: ack→ok · nack→`PublishNacked` (`producer.rb:235`) · return→`PublishUnroutable` (`producer.rb:325`) · timeout→`RequestTimeout` (`producer.rb:214-215`).
97
+ Contexto: `producer.rb:72-93,200-216,281-345`; `session.rb:70-74,204-250`. Branches: ack→ok · nack→`PublishNacked` (`producer.rb:235`) · return→`PublishUnroutable` (`producer.rb:325`) · timeout→`RequestTimeout` (`producer.rb:214-215`) · canal/conn AMQP rota (`Bunny::Exception`)→`CommunicationError` (envuelta por `Producer#confirmed` `producer.rb:87-90` y/o `Client#run_in_pool` `client.rb:155-167`; `.cause` preserva la original).
96
98
 
97
99
  ### Flujo: Consumer subscribe loop + reconnect + health
98
100
  Loop bloqueante infinito con backoff exponencial + jitter en error; health check en TimerTask separado, **acoplado flojo** vía estado de session.
@@ -166,6 +168,7 @@ Contexto: `middleware/stack.rb:31-47` (build L43-47, `reverse.inject`), `base.rb
166
168
  | Secuencias y `file:line` extraídos por el LLM del código a `a5cdb10`; 2ª pasada LLM corrigió 3 discrepancias (flujo middleware invertido, timeout RPC `producer.rb:124`, timeout confirmed `producer.rb:214-215`) | confirmed | **verificado por humano 2026-05-18** (invariante RFC-001 §3.3 satisfecho) |
167
169
  | Orden wire `basic.return → basic.ack` garantizado por AMQP; `RETURN_RACE_WINDOW_S` cubre GVL | declared (código) / inferred (garantía AMQP) | confirmar lectura de `producer.rb:299-308` + spec AMQP |
168
170
  | Health check acoplado flojo al loop vía cierre de session | inferred | confirmar `consumer.rb:340-361` vs `106-124` |
171
+ | Frontera de error del Client: cualquier `Bunny::Exception` durante `@pool.with` (try_create o in-flight) → `BugBunny::CommunicationError` con `.cause` preservada (`client.rb:155-167`). `Producer#confirmed` rescate estrechado a `Bunny::Exception` (`producer.rb:87-90`) — no traga bugs Ruby. `BugBunny.create_connection` también envuelve (`bug_bunny.rb:96-99`). | declared (código post-#49) | confirmar lectura de `client.rb:155-167`, `producer.rb:87-90`, `bug_bunny.rb:96-99` + specs `communication_error_wrapping_spec.rb` |
169
172
 
170
173
  ## 4. Cobertura y fronteras
171
174
 
@@ -173,4 +176,4 @@ Contexto: `middleware/stack.rb:31-47` (build L43-47, `reverse.inject`), `base.rb
173
176
  - **Lógica dispersa marcada (no fingida):** health check (thread aparte vía `Concurrent::TimerTask`, acople flojo con el loop sólo vía cierre de session). RFC-007: marcada, no diagramada como flujo limpio falso. (El orden onion del middleware NO es lógica dispersa: es mecanismo limpio y documentado en `stack.rb:37-39`.)
174
177
  - **Fuera de alcance:** estructura (operaciones/interfaz/topología) → dev-structure F2 (no implementado). Significado de términos → `glossary.md`. Datos → n/a (sin DB).
175
178
  - **Frescura:** PR que renombre/mueva un método citado → reviewer actualiza el diagrama y `file:line` en el mismo PR (RFC-001 §3.3).
176
- - **Verificación humana:** completada 2026-05-18 (invariante RFC-001 §3.3 satisfecho). Re-verificar en cada PR que toque un flujo citado (frescura).
179
+ - **Verificación humana:** base completada 2026-05-18; refresco scoped 2026-05-26 (contrato de error wrapping post-#49 en Fire-and-forget y Confirmed; invariante RFC-001 §3.3 satisfecho). Re-verificar en cada PR que toque un flujo citado (frescura).
@@ -138,9 +138,19 @@ module BugBunny
138
138
  # Ejecuta la lógica de envío dentro del contexto del Pool.
139
139
  # Mapea los argumentos al objeto Request y ejecuta la cadena de middlewares.
140
140
  #
141
+ # Cualquier `Bunny::Exception` que nazca durante la adquisición de la conexión
142
+ # (`@pool.with` → `try_create`) o durante operaciones sobre una conexión rota
143
+ # in-flight (canal cerrado, conn perdida) se envuelve en
144
+ # {BugBunny::CommunicationError}. Esto preserva el contrato de abstracción del
145
+ # gem: los callers del `Client` no deberían tener que rescatar tipos de
146
+ # `Bunny::*`. La excepción original queda accesible vía `.cause` (Ruby la
147
+ # preserva automáticamente al re-raisear dentro del `rescue`).
148
+ #
141
149
  # @param url [String] La ruta destino.
142
150
  # @param args [Hash] Argumentos pasados a los métodos públicos.
143
151
  # @yield [req] Bloque para configuración adicional del Request.
152
+ # @raise [BugBunny::CommunicationError] Si la conexión TCP/AMQP falla durante
153
+ # la adquisición del slot del pool o durante la operación.
144
154
  def run_in_pool(url, args)
145
155
  req = build_request(url, args)
146
156
 
@@ -151,8 +161,19 @@ module BugBunny
151
161
  # de los keyword args. Evaluamos el warning sobre el estado final del Request.
152
162
  warn_return_raise_misuse(req)
153
163
 
154
- # Ejecución dentro del Pool.
155
- # Session y Producer se reutilizan por slot de conexión (ver #session_for / #producer_for).
164
+ execute_in_pool(req)
165
+ rescue BugBunny::Error
166
+ raise
167
+ rescue Bunny::Exception => e
168
+ raise BugBunny::CommunicationError, "AMQP failure on path=#{req&.path}: #{e.class}: #{e.message}"
169
+ end
170
+
171
+ # Ejecuta el Request dentro del Pool. Session y Producer se reutilizan por
172
+ # slot de conexión (ver #session_for / #producer_for).
173
+ #
174
+ # @param req [BugBunny::Request]
175
+ # @return [Hash] Respuesta del producer.
176
+ def execute_in_pool(req)
156
177
  @pool.with do |conn|
157
178
  session = session_for(conn)
158
179
  producer = producer_for(conn, session)
@@ -7,8 +7,24 @@ module BugBunny
7
7
  # Permite capturar cualquier error de la librería con un `rescue BugBunny::Error`.
8
8
  class Error < ::StandardError; end
9
9
 
10
- # Error lanzado cuando ocurren problemas de red o conexión con RabbitMQ.
11
- # Suele envolver excepciones nativas de la gema `bunny` (ej: TCP connection failure).
10
+ # Error lanzado cuando ocurren problemas de red, conexión o protocolo AMQP con RabbitMQ.
11
+ #
12
+ # Envuelve cualquier `Bunny::Exception` (TCP fail, auth fail, canal cerrado,
13
+ # `PreconditionFailed`, `ConnectionClosedError`, etc.) en las fronteras de
14
+ # abstracción del gem — `BugBunny.create_connection`, `BugBunny::Client#publish` /
15
+ # `#request` / `#send`, y `BugBunny::Producer#confirmed`. Los callers no deberían
16
+ # rescatar tipos de `Bunny::*` directamente: con `rescue BugBunny::CommunicationError`
17
+ # alcanza para cubrir cualquier fallo de transporte/broker.
18
+ #
19
+ # La excepción original queda accesible vía `.cause` (Ruby la preserva
20
+ # automáticamente al re-raisear dentro del `rescue`).
21
+ #
22
+ # @example
23
+ # begin
24
+ # client.publish('evt', exchange: 'x', body: payload)
25
+ # rescue BugBunny::CommunicationError => e
26
+ # logger.error("publish failed: #{e.message} cause=#{e.cause&.class}")
27
+ # end
12
28
  class CommunicationError < Error; end
13
29
 
14
30
  # Error lanzado cuando la configuración de la gema es inválida.
@@ -86,8 +86,8 @@ module BugBunny
86
86
  { 'status' => 202, 'body' => nil }
87
87
  rescue BugBunny::Error
88
88
  raise
89
- rescue StandardError => e
90
- raise BugBunny::CommunicationError, "Publisher confirms failed: #{e.message}"
89
+ rescue Bunny::Exception => e
90
+ raise BugBunny::CommunicationError, "Publisher confirms failed: #{e.class}: #{e.message}"
91
91
  ensure
92
92
  teardown_return_listener(request, return_listener)
93
93
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BugBunny
4
- VERSION = '4.17.1'
4
+ VERSION = '4.18.0'
5
5
  end
data/lib/bug_bunny.rb CHANGED
@@ -80,7 +80,10 @@ module BugBunny
80
80
  # @option options [Integer] :continuation_timeout (15000) Timeout para operaciones RPC internas.
81
81
  #
82
82
  # @return [Bunny::Session] Una sesión de Bunny ya iniciada (`start` ya invocado).
83
- # @raise [Bunny::TCPConnectionFailed] Si no se puede conectar al servidor.
83
+ # @raise [BugBunny::CommunicationError] Si no se puede establecer la conexión
84
+ # (TCP fail, auth fail, vhost inválido, etc.). Envuelve cualquier
85
+ # `Bunny::Exception` para preservar la frontera de abstracción del gem.
86
+ # La excepción original queda en `.cause`.
84
87
  def self.create_connection(**options)
85
88
  conn_options = merge_connection_options(options)
86
89
  Bunny.new(conn_options).tap do |conn|
@@ -89,6 +92,10 @@ module BugBunny
89
92
  end
90
93
  conn.start
91
94
  end
95
+ rescue Bunny::Exception => e
96
+ raise BugBunny::CommunicationError,
97
+ "Failed to establish AMQP connection to #{conn_options[:host]}:#{conn_options[:port]}: " \
98
+ "#{e.class}: #{e.message}"
92
99
  end
93
100
 
94
101
  # Cierra la conexión global si existe.
data/skill/SKILL.md CHANGED
@@ -19,7 +19,7 @@ Gema Ruby: capa de routing RESTful sobre AMQP/RabbitMQ. Microservicios se comuni
19
19
 
20
20
  ## Contrato resumido (piso mínimo)
21
21
 
22
- > Resume el contrato de **`bug_bunny` 4.17.0**. Suficiente para el uso típico sin abrir el detalle; el detalle version-locked está en [`../docs/behavior/behavior.md`](../docs/behavior/behavior.md) (6 flujos) y [`../docs/glossary/glossary.md`](../docs/glossary/glossary.md) (símbolos→significado). Antipatrones/API completa: más abajo (embebido interim, ver Cobertura y fronteras).
22
+ > Resume el contrato de **`bug_bunny` 4.18.0**. Suficiente para el uso típico sin abrir el detalle; el detalle version-locked está en [`../docs/behavior/behavior.md`](../docs/behavior/behavior.md) (6 flujos) y [`../docs/glossary/glossary.md`](../docs/glossary/glossary.md) (símbolos→significado). Antipatrones/API completa: más abajo (embebido interim, ver Cobertura y fronteras).
23
23
 
24
24
  **Símbolos públicos clave**
25
25
 
@@ -31,7 +31,7 @@ Gema Ruby: capa de routing RESTful sobre AMQP/RabbitMQ. Microservicios se comuni
31
31
  | `BugBunny::Controller` | `before/around/after_action`, `rescue_from`, `render status:, json:` |
32
32
  | `BugBunny.routes.draw` | `resources :x` · `namespace` · `member`/`collection` |
33
33
  | `BugBunny.configure` | `host/port/username/password` · `rpc_timeout` (default 10) · `nack_raise`/`return_raise` (default `true`) · `on_return` |
34
- | Excepciones | `RemoteError` (500 con backtrace remoto) · `PublishNacked` · `PublishUnroutable` · `RequestTimeout` · `NotFound` · `UnprocessableEntity` |
34
+ | Excepciones | `RemoteError` (500 con backtrace remoto) · `PublishNacked` · `PublishUnroutable` · `RequestTimeout` · `NotFound` · `UnprocessableEntity` · `CommunicationError` (envuelve cualquier `Bunny::Exception` — TCP/conn/canal; `.cause` preserva original) |
35
35
 
36
36
  **Uso típico**
37
37
 
@@ -54,6 +54,7 @@ client.publish('events', body: { type: 'x' }) # => { 'status' => 202 }
54
54
  - `confirmed:true + mandatory:true` con `return_raise` (default `true`) → `PublishUnroutable` si no rutea.
55
55
  - `BugBunny::Consumer.subscribe` requiere `connection:`. No correr el Consumer en threads de Puma (loop bloqueante).
56
56
  - `exchange_options: { durable: true }` debe matchear la declaración del consumer, o `Bunny::PreconditionFailed`.
57
+ - **Errores de transporte (4.18+):** TCP fail, conn rota, canal cerrado → siempre `BugBunny::CommunicationError`. No rescatar `Bunny::TCPConnectionFailed`/`ConnectionClosedError` directo — quedó atrás de la frontera. La original sigue accesible vía `.cause`.
57
58
 
58
59
  ## Índice de artefactos (fuente de verdad)
59
60
 
@@ -431,8 +432,8 @@ Limitación de RSpec: `instance_double` valida que el método exista pero **no**
431
432
  **Resolución:** `rescue BugBunny::RemoteError => e` y acceder a `e.original_class`, `e.original_message`, `e.original_backtrace`. Revisar logs del consumer (`event=controller.unhandled_exception`).
432
433
 
433
434
  ### BugBunny::CommunicationError
434
- **Causa:** Fallo de conexión o reconexión agotada.
435
- **Resolución:** Verificar conectividad a RabbitMQ. Revisar `max_reconnect_attempts` y logs de reconexión.
435
+ **Causa:** Fallo de transporte AMQP envuelve cualquier `Bunny::Exception` que escape en la frontera del gem (`Client#publish`/`#request`/`#send`, `Producer#confirmed`, `BugBunny.create_connection`). Cubre TCP fail (`Bunny::TCPConnectionFailed`), conn rota in-flight (`ConnectionClosedError`), canal cerrado (`ChannelAlreadyClosed`), auth fail, etc. La excepción original queda en `.cause`.
436
+ **Resolución:** Verificar conectividad a RabbitMQ (host/port/auth/vhost). Inspeccionar `e.cause` para clasificar el fallo concreto. Revisar `max_reconnect_attempts` y logs de reconexión.
436
437
 
437
438
  Ver catálogo completo en [Errores](references/errores.md).
438
439
 
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'support/bunny_mocks'
5
+
6
+ # Specs para el contrato de envoltura definido en issue #49:
7
+ # cualquier `Bunny::Exception` que escape de las fronteras de abstracción del
8
+ # gem (`BugBunny.create_connection`, `BugBunny::Client#publish`/`#request`/`#send`,
9
+ # `BugBunny::Producer#confirmed`) debe re-raisearse como
10
+ # `BugBunny::CommunicationError`, preservando la excepción original en `.cause`.
11
+ RSpec.describe 'Bunny::Exception → BugBunny::CommunicationError wrapping' do
12
+ include BunnyMocks
13
+
14
+ describe 'BugBunny.create_connection' do
15
+ before { BugBunny.configuration ||= BugBunny::Configuration.new }
16
+
17
+ it 'envuelve Bunny::TCPConnectionFailedForAllHosts en CommunicationError' do
18
+ fake_session = instance_double(Bunny::Session)
19
+ allow(fake_session).to receive(:after_recovery_completed)
20
+ allow(fake_session).to receive(:start).and_raise(Bunny::TCPConnectionFailedForAllHosts)
21
+ allow(Bunny).to receive(:new).and_return(fake_session)
22
+
23
+ err = capture_error { BugBunny.create_connection(host: 'broker.invalid', port: 5672) }
24
+ expect(err).to be_a(BugBunny::CommunicationError)
25
+ expect(err.message).to match(/broker.invalid:5672/)
26
+ expect(err.cause).to be_a(Bunny::TCPConnectionFailed)
27
+ end
28
+
29
+ it 'envuelve cualquier Bunny::Exception, no solo TCP' do
30
+ fake_session = instance_double(Bunny::Session)
31
+ allow(fake_session).to receive(:after_recovery_completed)
32
+ allow(fake_session).to receive(:start).and_raise(Bunny::AuthenticationFailureError.new('guest', '/', 0))
33
+ allow(Bunny).to receive(:new).and_return(fake_session)
34
+
35
+ err = capture_error { BugBunny.create_connection }
36
+ expect(err).to be_a(BugBunny::CommunicationError)
37
+ expect(err.cause).to be_a(Bunny::AuthenticationFailureError)
38
+ end
39
+ end
40
+
41
+ describe 'Client#publish — TCP fail en try_create (issue #49 caso original)' do
42
+ it 'envuelve Bunny::TCPConnectionFailedForAllHosts levantado dentro de @pool.with' do
43
+ # Pool falso que levanta la excepción raw de bunny al adquirir slot —
44
+ # simula exactamente lo observado en producción en el stack trace del issue.
45
+ raising_pool = Object.new
46
+ raising_pool.define_singleton_method(:with) { |&_block| raise Bunny::TCPConnectionFailedForAllHosts }
47
+
48
+ client = BugBunny::Client.new(pool: raising_pool)
49
+
50
+ err = capture_error { client.publish('evt', exchange: 'x', exchange_type: 'direct', body: { a: 1 }) }
51
+ expect(err).to be_a(BugBunny::CommunicationError)
52
+ expect(err.message).to match(/AMQP failure on path=evt/)
53
+ expect(err.cause).to be_a(Bunny::TCPConnectionFailed)
54
+ end
55
+ end
56
+
57
+ describe 'Client — in-flight ConnectionClosedError' do
58
+ it 'envuelve Bunny::ConnectionClosedError levantado por el Producer' do
59
+ conn = BunnyMocks::FakeConnection.new(true, BunnyMocks::FakeChannel.new(true))
60
+ pool = Object.new.tap { |p| p.define_singleton_method(:with) { |&blk| blk.call(conn) } }
61
+ client = BugBunny::Client.new(pool: pool)
62
+
63
+ allow_any_instance_of(BugBunny::Producer).to receive(:fire)
64
+ .and_raise(Bunny::ConnectionClosedError.new('connection lost'))
65
+
66
+ err = capture_error { client.publish('evt', exchange: 'x', exchange_type: 'direct', body: { a: 1 }) }
67
+ expect(err).to be_a(BugBunny::CommunicationError)
68
+ expect(err.message).to match(/AMQP failure/)
69
+ expect(err.cause).to be_a(Bunny::ConnectionClosedError)
70
+ end
71
+ end
72
+
73
+ describe 'Client — pass-through de BugBunny::Error' do
74
+ it 'no re-envuelve errores propios del gem (RequestTimeout, PublishNacked, etc.)' do
75
+ conn = BunnyMocks::FakeConnection.new(true, BunnyMocks::FakeChannel.new(true))
76
+ pool = Object.new.tap { |p| p.define_singleton_method(:with) { |&blk| blk.call(conn) } }
77
+ client = BugBunny::Client.new(pool: pool)
78
+
79
+ allow_any_instance_of(BugBunny::Producer).to receive(:rpc)
80
+ .and_raise(BugBunny::RequestTimeout.new('timeout'))
81
+
82
+ expect { client.request('foo', method: :get, exchange: 'x', exchange_type: 'direct') }
83
+ .to raise_error(BugBunny::RequestTimeout)
84
+ end
85
+ end
86
+
87
+ describe 'Producer#confirmed — rescue limitado a Bunny::Exception' do
88
+ it 'no traga errores genéricos de Ruby (NoMethodError) como CommunicationError' do
89
+ producer = BugBunny::Producer.new(instance_double(BugBunny::Session))
90
+ allow(producer).to receive(:setup_return_listener).and_return(nil)
91
+ allow(producer).to receive(:teardown_return_listener)
92
+ allow(producer).to receive(:publish_message).and_raise(NoMethodError.new('bug interno'))
93
+
94
+ request = BugBunny::Request.new('evt').tap do |r|
95
+ r.exchange = 'x'
96
+ r.exchange_type = 'direct'
97
+ r.delivery_mode = :confirmed
98
+ end
99
+
100
+ expect { producer.confirmed(request) }.to raise_error(NoMethodError, 'bug interno')
101
+ end
102
+
103
+ it 'envuelve Bunny::Exception como CommunicationError' do
104
+ producer = BugBunny::Producer.new(instance_double(BugBunny::Session))
105
+ allow(producer).to receive(:setup_return_listener).and_return(nil)
106
+ allow(producer).to receive(:teardown_return_listener)
107
+ allow(producer).to receive(:publish_message)
108
+ .and_raise(Bunny::ChannelAlreadyClosed.new('closed', double(id: 1)))
109
+
110
+ request = BugBunny::Request.new('evt').tap do |r|
111
+ r.exchange = 'x'
112
+ r.exchange_type = 'direct'
113
+ r.delivery_mode = :confirmed
114
+ end
115
+
116
+ err = capture_error { producer.confirmed(request) }
117
+ expect(err).to be_a(BugBunny::CommunicationError)
118
+ expect(err.message).to match(/Publisher confirms failed/)
119
+ expect(err.cause).to be_a(Bunny::ChannelAlreadyClosed)
120
+ end
121
+ end
122
+
123
+ def capture_error
124
+ yield
125
+ nil
126
+ rescue StandardError => e
127
+ e
128
+ end
129
+ end
@@ -377,13 +377,21 @@ RSpec.describe BugBunny::Producer do
377
377
  expect { confirmed_producer.confirmed(req) }.to raise_error(BugBunny::RequestTimeout, /Timeout/)
378
378
  end
379
379
 
380
- it 'envuelve errores del canal como BugBunny::CommunicationError' do
381
- allow(mock_channel).to receive(:wait_for_confirms).and_raise(StandardError, 'boom')
380
+ it 'envuelve Bunny::Exception del canal como BugBunny::CommunicationError' do
381
+ allow(mock_channel).to receive(:wait_for_confirms)
382
+ .and_raise(Bunny::ChannelAlreadyClosed.new('boom', double(id: 1)))
382
383
 
383
384
  expect { confirmed_producer.confirmed(build_request) }
384
385
  .to raise_error(BugBunny::CommunicationError, /boom/)
385
386
  end
386
387
 
388
+ it 'no traga errores genéricos de Ruby (rescue es Bunny::Exception, no StandardError)' do
389
+ allow(mock_channel).to receive(:wait_for_confirms).and_raise(NoMethodError, 'bug interno')
390
+
391
+ expect { confirmed_producer.confirmed(build_request) }
392
+ .to raise_error(NoMethodError, 'bug interno')
393
+ end
394
+
387
395
  it 'propaga BugBunny::Error sin envolver' do
388
396
  allow(fake_exchange).to receive(:publish).and_raise(BugBunny::CommunicationError, 'chan dead')
389
397
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bug_bunny
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.17.1
4
+ version: 4.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - gabix
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-05-19 00:00:00.000000000 Z
11
+ date: 2026-05-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bunny
@@ -283,6 +283,7 @@ files:
283
283
  - spec/support/bunny_mocks.rb
284
284
  - spec/support/integration_helper.rb
285
285
  - spec/unit/client_session_pool_spec.rb
286
+ - spec/unit/communication_error_wrapping_spec.rb
286
287
  - spec/unit/configuration_spec.rb
287
288
  - spec/unit/consumer_middleware_spec.rb
288
289
  - spec/unit/consumer_spec.rb
@@ -306,7 +307,7 @@ metadata:
306
307
  homepage_uri: https://github.com/gedera/bug_bunny
307
308
  source_code_uri: https://github.com/gedera/bug_bunny
308
309
  changelog_uri: https://github.com/gedera/bug_bunny/blob/main/CHANGELOG.md
309
- documentation_uri: https://github.com/gedera/bug_bunny/blob/v4.17.1/skill
310
+ documentation_uri: https://github.com/gedera/bug_bunny/blob/v4.18.0/skill
310
311
  post_install_message:
311
312
  rdoc_options: []
312
313
  require_paths: