|
148 | 148 | { "name": "native-equals-body", "body_attempts": 3, "redelivery_count": 3, "expected_attempts": 3 } |
149 | 149 | ] |
150 | 150 | } |
| 151 | + }, |
| 152 | + "kafka": { |
| 153 | + "description": "Apache Kafka binding conformance (broker-bindings.md §6). Every SDK that ships a Kafka transport must satisfy these. The record value is the byte-identical envelope (the 'cases' above); these lock the bq- header projection + the bq-attempts-authoritative reconciliation the binding adds. Per-message values reuse fixtures/order-created.json so the expected projection is deterministic.", |
| 154 | + "property_projection": { |
| 155 | + "description": "On produce, the transport MUST project these native Kafka record headers from the envelope, all UTF-8 byte strings (Kafka headers are bytes, so integers are stringified): bq-job = job (the URN), bq-trace-id = trace_id, bq-message-id = meta.id, bq-schema-version = str(meta.schema_version), bq-source-lang = meta.lang, bq-attempts = str(attempts). The record value is the byte-identical envelope and the record timestamp mirrors meta.created_at (Unix ms). Applies to every Kafka-producing SDK.", |
| 156 | + "envelope_file": "fixtures/order-created.json", |
| 157 | + "headers": { |
| 158 | + "bq-job": "urn:babel:orders:created", |
| 159 | + "bq-trace-id": "7b3f9c2a-e41d-4f88-9b2a-1c0d5e6f7a8b", |
| 160 | + "bq-message-id": "f1e2d3c4-b5a6-4789-90ab-cdef01234567", |
| 161 | + "bq-schema-version": "1", |
| 162 | + "bq-source-lang": "php", |
| 163 | + "bq-attempts": "0" |
| 164 | + } |
| 165 | + }, |
| 166 | + "attempts_reconciliation": { |
| 167 | + "description": "On consume, attempts = the bq-attempts header when present (AUTHORITATIVE — Kafka has no native delivery count), else the body's own attempts (the fallback for a non-BabelQueue producer). This is NOT a max: the header overrides the body even when lower. A null header_attempts means the header is absent. The rule is identical across all five Kafka SDKs.", |
| 168 | + "cases": [ |
| 169 | + { "name": "header-matches-body", "body_attempts": 0, "header_attempts": 0, "expected_attempts": 0 }, |
| 170 | + { "name": "header-present", "body_attempts": 0, "header_attempts": 2, "expected_attempts": 2 }, |
| 171 | + { "name": "header-absent-falls-back", "body_attempts": 3, "header_attempts": null, "expected_attempts": 3 }, |
| 172 | + { "name": "header-authoritative-overrides", "body_attempts": 5, "header_attempts": 1, "expected_attempts": 1 }, |
| 173 | + { "name": "header-higher-than-body", "body_attempts": 1, "header_attempts": 4, "expected_attempts": 4 } |
| 174 | + ] |
| 175 | + } |
| 176 | + }, |
| 177 | + "artemis": { |
| 178 | + "description": "Apache ActiveMQ Artemis binding conformance (broker-bindings.md §7). Every SDK that ships an Artemis transport must satisfy these. The message body stays byte-identical (the 'cases' above); these lock the AMQP-1.0 projection + reconciliation the binding adds. Per-message values reuse fixtures/order-created.json so the expected projection is deterministic.", |
| 179 | + "property_projection": { |
| 180 | + "description": "On produce, the transport MUST project the envelope onto the AMQP a JMS peer reads: the x-opt-jms-type message annotation = job (the URN, the AMQP-JMS mapping of JMSType), correlation-id = trace_id (JMSCorrelationID), creation-time = meta.created_at (JMSTimestamp, Unix ms), plus the bq- application properties, all string-valued (matching the Java JMS setStringProperty projection): bq-schema-version = str(meta.schema_version), bq-source-lang = meta.lang, bq-attempts = str(attempts), bq-app-id = 'babelqueue'. The body is the byte-identical envelope. Unlike Kafka/Pulsar, the URN / trace_id / message-id are NOT bq- properties — they ride the JMS-native slots. Applies to every Artemis-producing SDK.", |
| 181 | + "envelope_file": "fixtures/order-created.json", |
| 182 | + "jms_type_annotation": "x-opt-jms-type", |
| 183 | + "jms_type": "urn:babel:orders:created", |
| 184 | + "correlation_id": "7b3f9c2a-e41d-4f88-9b2a-1c0d5e6f7a8b", |
| 185 | + "properties": { |
| 186 | + "bq-schema-version": "1", |
| 187 | + "bq-source-lang": "php", |
| 188 | + "bq-attempts": "0", |
| 189 | + "bq-app-id": "babelqueue" |
| 190 | + } |
| 191 | + }, |
| 192 | + "attempts_reconciliation": { |
| 193 | + "description": "On consume, attempts = max(body.attempts, delivery-count): the AMQP delivery-count header is 0-based (0 on first delivery) so it maps directly with NO -1, a runtime-incremented body count is never lowered, and delivery-count 0 leaves the body's own count untouched (the runtime/App retries by republishing with attempts+1, which resets the broker's delivery-count to 0). The Java JMS binding reads the 1-based JMSXDeliveryCount and subtracts 1, arriving at the same 0-based attempts. The rule is identical across the native-consumer SDKs (.NET/Java/Node) and the Transport+App SDKs (Python/Go).", |
| 194 | + "cases": [ |
| 195 | + { "name": "first-delivery", "body_attempts": 0, "delivery_count": 0, "expected_attempts": 0 }, |
| 196 | + { "name": "third-delivery", "body_attempts": 0, "delivery_count": 2, "expected_attempts": 2 }, |
| 197 | + { "name": "native-exceeds-body", "body_attempts": 2, "delivery_count": 5, "expected_attempts": 5 }, |
| 198 | + { "name": "never-lower-runtime", "body_attempts": 5, "delivery_count": 1, "expected_attempts": 5 }, |
| 199 | + { "name": "first-delivery-keeps-body", "body_attempts": 4, "delivery_count": 0, "expected_attempts": 4 }, |
| 200 | + { "name": "native-equals-body", "body_attempts": 3, "delivery_count": 3, "expected_attempts": 3 } |
| 201 | + ] |
| 202 | + } |
151 | 203 | } |
152 | 204 | } |
0 commit comments