wyrd-client 0.3.0

Shared Wyrd client: transport, authentication, and composed client capabilities.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
//! Concrete client error returned by config `validate()` methods, the
//! credential chain, and the auth token-exchange path, plus `pub`
//! server-response mappers.

use thiserror::Error;
use wyrd_spec::error::{WyrdError, WyrdProblem, WyrdStorageError};
use wyrd_spec::vala::error::BifrostError;
use wyrd_tonic::error::WYRD_ERROR_HEADER;

/// Concrete client-local error.
///
/// Produced by the config `validate()` methods, [`CredentialChain::resolve`],
/// and the [`AuthMiddleware`] token-exchange path. Server-side failures are
/// mapped separately via [`from_problem_json`] (HTTP) and [`from_grpc_status`]
/// (gRPC) into the unified [`WyrdError`] catalog; these client-local variants
/// are never echoed back by the server.
///
/// [`CredentialChain::resolve`]: crate::transport::credential::CredentialChain::resolve
/// [`AuthMiddleware`]: crate::auth::AuthMiddleware
#[derive(Debug, Error)]
pub enum WyrdClientError {
    /// Structural validation failed on a config struct. Maps to
    /// `WYRD_CLIENT_400_CONFIG_INVALID` at the public boundary. Not
    /// `WYRD_SPEC_400_VALIDATION`; that code is reserved for `wyrd-spec`
    /// contract violations only.
    #[error("config validation failed: {field}: {reason}")]
    Config {
        /// Dotted-path name of the offending config field, e.g.
        /// `"grpc_config.endpoint"` or `"http_config.timeout_ms"`. Echoed
        /// verbatim into the catalog payload's `field`.
        field: String,
        /// Short human reason, e.g. `"must not be empty"` or
        /// `"must be at least 1"`. Echoed verbatim into the catalog
        /// payload's `reason`.
        reason: String,
    },

    /// A request could not reach the server because the transport itself is
    /// unavailable — a DNS, TCP, TLS, or HTTP/gRPC connect failure. Produced by
    /// [`GrpcConnection::connect`], the token-exchange path in [`AuthMiddleware`],
    /// and [`MockTransport`] (`fail_on_drain`). Maps to
    /// `WYRD_CLIENT_503_TRANSPORT_DOWN` at the public boundary.
    ///
    /// [`GrpcConnection::connect`]: crate::transport::grpc::GrpcConnection::connect
    /// [`AuthMiddleware`]: crate::auth::AuthMiddleware
    /// [`MockTransport`]: crate::transport::mock::MockTransport
    #[error("transport unavailable ({transport}): {message}")]
    TransportDown {
        /// Transport tag (`"grpc"`, `"http"`, `"mock"`). Echoed into the
        /// catalog payload's `transport`.
        transport: String,
        /// Human-readable failure description. Echoed into the catalog
        /// payload's `message`.
        message: String,
    },

    /// The credential chain produced no usable credential. Configure at least
    /// one source: `WYRD_ACCESS_TOKEN`, `WYRD_WORKLOAD_TOKEN`+`WYRD_TENANT`,
    /// `WYRD_API_KEY`, an explicit `ClientConfig::credential`, or the
    /// `~/.config/wyrd/credentials.toml` `[default].api_key` floor. Maps to
    /// `WYRD_CLIENT_401_NO_CREDENTIALS` at the public boundary.
    ///
    /// `ClientConfig::credential` is the only credential field a caller sets by
    /// hand — one string classified by its own prefix — so this guidance must
    /// never name a per-grant field the config no longer has.
    #[error(
        "no credentials available; set WYRD_ACCESS_TOKEN, WYRD_WORKLOAD_TOKEN+WYRD_TENANT, \
         or WYRD_API_KEY, pass ClientConfig::credential, or add [default].api_key to \
         ~/.config/wyrd/credentials.toml"
    )]
    NoCredentials,

    /// The saved user login for this server and tenant cannot be used:
    /// the selector names another tenant, the local store is unsafe or
    /// corrupt, the login was logged out, or the server refused its renewal.
    /// Never falls through to another credential.
    /// Maps to `WYRD_CLIENT_401_SAVED_LOGIN_UNUSABLE` at the public boundary.
    #[error("saved user login cannot be used ({reason}): {message}")]
    SavedLogin {
        /// Stable machine reason: `"tenant_mismatch"`, `"unsafe_store"`,
        /// `"corrupt"`, `"io"`, `"logged_out"`, or `"refresh_refused"`.
        /// Echoed into the catalog payload's `reason`.
        reason: &'static str,
        /// Human-readable description; never carries a secret.
        message: String,
    },
}

impl WyrdClientError {
    /// Stable `WYRD_CLIENT_*` code for this client-local error.
    ///
    /// These codes are the client-boundary projection of the variants; the
    /// server never emits them. Server-reported failures carry their own
    /// catalog code via [`WyrdError::code`].
    #[must_use]
    pub fn code(&self) -> &'static str {
        match self {
            Self::Config { .. } => "WYRD_CLIENT_400_CONFIG_INVALID",
            Self::NoCredentials => "WYRD_CLIENT_401_NO_CREDENTIALS",
            Self::TransportDown { .. } => "WYRD_CLIENT_503_TRANSPORT_DOWN",
            Self::SavedLogin { .. } => "WYRD_CLIENT_401_SAVED_LOGIN_UNUSABLE",
        }
    }
}

impl From<&WyrdClientError> for WyrdError {
    /// Project one client-local failure onto its `WYRD_CLIENT_*` catalog variant.
    ///
    /// This is the single client-error projection shared by every facade, so a
    /// missing credential, invalid configuration, or unavailable transport keeps
    /// the same code and status whether it surfaced through `Cards` or
    /// `Bifrost`. The variant `Display` text becomes the catalog message, and
    /// only the safe structured metadata each variant already carries becomes
    /// `details`: `field` and `reason` for configuration, the `transport`
    /// discriminator for transport unavailability, and nothing for missing
    /// credentials. Raw transport failure text stays in the message only.
    fn from(error: &WyrdClientError) -> Self {
        let message = error.to_string();
        match error {
            WyrdClientError::Config { field, reason } => Self::ClientConfigInvalid {
                message,
                details: serde_json::json!({ "field": field, "reason": reason }),
            },
            WyrdClientError::NoCredentials => Self::ClientNoCredentials {
                message,
                details: serde_json::json!({}),
            },
            WyrdClientError::TransportDown { transport, .. } => Self::ClientTransportDown {
                message,
                details: serde_json::json!({ "transport": transport }),
            },
            WyrdClientError::SavedLogin { reason, .. } => Self::ClientSavedLoginUnusable {
                message,
                details: serde_json::json!({ "reason": reason }),
            },
        }
    }
}

impl From<WyrdClientError> for WyrdError {
    /// Project an owned client-local failure through the borrowed projection.
    fn from(error: WyrdClientError) -> Self {
        Self::from(&error)
    }
}

/// Map an HTTP `application/problem+json` response body into a [`WyrdError`].
///
/// Reads the stable `code` field and reconstructs the matching
/// [`WyrdError`] variant. Unknown codes are preserved in
/// [`WyrdError::UpstreamFailure`]`::details.original_code` so the original
/// code is never dropped. `pub` so other transport response sinks can reuse it.
///
/// # Arguments
/// * `body` - Decoded problem document; missing `code`, `detail`, or `details` fields default
///   to empty.
pub fn from_problem_json(body: &serde_json::Value) -> WyrdError {
    let code = body["code"].as_str().unwrap_or("");
    let message = body["detail"].as_str().unwrap_or("").to_owned();
    let details = body
        .get("details")
        .cloned()
        .unwrap_or(serde_json::json!({}));
    code_to_wyrd_error(code, message, details)
}

/// Rebuild the [`WyrdError`] a typed [`WyrdProblem`] carries.
///
/// A failed query terminal carries the same problem the server returns before
/// a stream starts, so both reach callers through this one reconstruction:
/// a `WYRD_VALA_*` problem rebuilds its exact typed error from `details`.
pub fn from_problem(problem: &WyrdProblem) -> WyrdError {
    code_to_wyrd_error(
        &problem.code,
        problem.detail.clone(),
        problem.details.clone(),
    )
}

/// Maps a gRPC [`wyrd_tonic::tonic::Status`] into a [`WyrdError`].
///
/// Prefers Wyrd's canonical problem document and accepts `google.rpc.ErrorInfo`
/// from older or external gRPC services.
///
/// Falls back to [`WyrdError::Internal`] when the status carries no
/// `ErrorInfo`.
///
/// # Arguments
/// * `status` - gRPC status whose metadata or details carry the Wyrd error.
pub fn from_grpc_status(status: &wyrd_tonic::tonic::Status) -> WyrdError {
    use wyrd_tonic::tonic_types::StatusExt as _;

    if let Some(problem) = status
        .metadata()
        .get_bin(WYRD_ERROR_HEADER)
        .and_then(|value| value.to_bytes().ok())
        .and_then(|bytes| serde_json::from_slice(&bytes).ok())
    {
        return from_problem_json(&problem);
    }
    if let Some(info) = status.get_details_error_info() {
        let details = if info.metadata.is_empty() {
            serde_json::json!({})
        } else {
            serde_json::Value::Object(
                info.metadata
                    .into_iter()
                    .map(|(k, v)| (k, serde_json::Value::String(v)))
                    .collect(),
            )
        };
        return code_to_wyrd_error(&info.reason, status.message().to_owned(), details);
    }
    WyrdError::Internal {
        message: status.message().to_owned(),
        details: serde_json::json!({}),
    }
}

/// Shared code→[`WyrdError`] reconstruction used by both transport mappers.
///
/// Both [`from_problem_json`] and [`from_grpc_status`] route through here so
/// identical codes always produce identical [`WyrdError`] values regardless of
/// which transport the response arrived on.
///
/// A Bifrost code is rebuilt exactly from the serialized [`BifrostError`] its
/// `details` carry — every server producer (the HTTP problem document, the
/// gRPC problem header, and the query terminal) serializes it — so every
/// catalog Bifrost variant keeps its code and fields without a per-code arm.
/// A fieldless Bifrost variant is rebuilt from its code alone, so a transport
/// that carries only the code, such as a gateway refusal in a provider's
/// error envelope, keeps it too.
///
/// Reconstruction goes through [`WyrdError::from_code`], which covers the whole
/// catalog (every `{ message, details }` variant), so a `404`/`403`/`429`
/// response keeps its real code **and** [`WyrdError::status`] instead of
/// collapsing onto `502`. Codes with no matching variant fall back to
/// [`WyrdError::UpstreamFailure`] with the original code preserved in
/// `details.original_code` rather than silently dropped.
pub(crate) fn code_to_wyrd_error(
    code: &str,
    message: String,
    details: serde_json::Value,
) -> WyrdError {
    if code.starts_with("WYRD_STORAGE_")
        && let Ok(error) = serde_json::from_value::<WyrdStorageError>(details.clone())
    {
        return error.into();
    }
    if code.starts_with("WYRD_VALA_")
        && let Ok(error) = serde_json::from_value::<BifrostError>(details.clone())
        && error.code() == code
    {
        return WyrdError::Vala { error };
    }
    if let Some(error) = BifrostError::from_unit_code(code) {
        return WyrdError::Vala { error };
    }
    if let Some(reconstructed) = WyrdError::from_code(code, message.clone(), details.clone()) {
        return reconstructed;
    }
    WyrdError::UpstreamFailure {
        message: format!("server error: {message}"),
        details: serde_json::json!({
            "original_code": code,
            "original_details": details,
        }),
    }
}

#[cfg(test)]
mod tests {
    use super::{BifrostError, WyrdClientError, code_to_wyrd_error, from_problem_json};
    use wyrd_spec::error::{WyrdError, WyrdStorageError};

    /// A fieldless Bifrost code with no `details`, as a gateway refusal in a
    /// provider error envelope carries it, rebuilds its exact variant.
    ///
    /// # Panics
    ///
    /// Panics when the code collapses onto an upstream failure.
    #[test]
    fn fieldless_bifrost_code_reconstructs_without_details() {
        let received = code_to_wyrd_error(
            "WYRD_VALA_403_BIFROST_CARD_SCOPE",
            "Wyrd gateway answered HTTP 403".to_owned(),
            serde_json::json!({}),
        );
        assert_eq!(received.code(), "WYRD_VALA_403_BIFROST_CARD_SCOPE");
        assert_eq!(received.status(), 403);
    }

    #[test]
    fn known_code_reconstructs_correct_variant() {
        let body = serde_json::json!({
            "code": "WYRD_SPEC_404_NOT_FOUND",
            "detail": "card not found",
            "details": {},
        });
        let err = from_problem_json(&body);
        assert_eq!(err.code(), "WYRD_SPEC_404_NOT_FOUND");
    }

    /// A Bifrost problem document round-trips to the exact variant and fields.
    ///
    /// The body is the server's own rendering, so a Variant query failure keeps
    /// its catalog code and location instead of collapsing to an upstream
    /// failure on the client.
    ///
    /// # Panics
    ///
    /// Panics when the reconstructed code or problem document drifts from the
    /// sent error.
    #[test]
    fn bifrost_problem_details_reconstruct_exact_variant() {
        let sent = WyrdError::Vala {
            error: BifrostError::VariantInvalidJson {
                field: "parse_json".to_owned(),
                row: 0,
                path: String::new(),
            },
        };
        let received = from_problem_json(&sent.as_problem_json());
        assert_eq!(received.code(), "WYRD_VALA_400_VARIANT_INVALID_JSON");
        assert_eq!(received.as_problem_json(), sent.as_problem_json());
    }

    #[test]
    fn unknown_code_preserved_in_catch_all() {
        let body = serde_json::json!({
            "code": "WYRD_CUSTOM_999_EXPERIMENTAL",
            "detail": "experimental error",
            "details": {},
        });
        let err = from_problem_json(&body);
        assert_eq!(
            err.code(),
            "WYRD_SPEC_502_UPSTREAM_FAILURE",
            "unknown code must fall through to UpstreamFailure catch-all"
        );
        let json = err.as_problem_json();
        assert_eq!(
            json["details"]["original_code"].as_str(),
            Some("WYRD_CUSTOM_999_EXPERIMENTAL"),
            "original code must be preserved in catch-all variant details"
        );
    }

    /// Every Bifrost problem the server renders reconstructs its exact variant.
    ///
    /// Each body is the server's own `as_problem_json` rendering, whose
    /// `details` carry the serialized `BifrostError`, so the client keeps the
    /// typed status, code, message, and fields instead of collapsing onto a
    /// retryable-looking 502 `UpstreamFailure`.
    ///
    /// # Panics
    ///
    /// Panics when any reconstructed problem drifts from the one sent.
    #[test]
    fn bifrost_problems_reconstruct_their_exact_variant() {
        let table = || "vala.datasets.events".to_owned();
        for (sent, status) in [
            (BifrostError::TableNotFound { table: table() }, 404),
            (BifrostError::FingerprintMismatch { table: table() }, 409),
            (
                BifrostError::CompactionTargetMismatch { table: table() },
                409,
            ),
            (BifrostError::CompactionTypeMismatch { table: table() }, 409),
            (
                BifrostError::SchemaParse {
                    detail: "too many leaves".to_owned(),
                },
                400,
            ),
            (
                BifrostError::InvalidCompactionTarget {
                    table: table(),
                    bytes: 1,
                },
                400,
            ),
            (
                BifrostError::QueryInvalidSql {
                    detail: "parser rejected SELECT FROM".to_owned(),
                },
                400,
            ),
            (BifrostError::QueryAdmissionRejected, 429),
            (BifrostError::QueryQueueFull, 429),
            (BifrostError::QueryMemoryRequestTooLarge, 422),
            (BifrostError::OracleRoleUnavailable, 503),
            (BifrostError::RunningQueryNotFound, 404),
            (BifrostError::RunningQueryConflict, 409),
            (BifrostError::RunningQueryControlUnavailable, 503),
        ] {
            let sent = WyrdError::from(sent);
            let received = from_problem_json(&sent.as_problem_json());
            assert_eq!(received.status(), status, "{}", sent.code());
            assert_eq!(received.as_problem_json(), sent.as_problem_json());
        }
    }

    #[test]
    fn previously_collapsed_code_now_keeps_its_status() {
        // A code that maps to a typed variant must reconstruct that variant and
        // report its real status, not collapse onto UpstreamFailure's 502. Here
        // a 403 code must surface as status 403.
        let body = serde_json::json!({
            "code": "WYRD_PERMISSION_403_DENIED_RBAC",
            "detail": "permission denied",
            "details": {},
        });
        let err = from_problem_json(&body);
        assert_eq!(err.code(), "WYRD_PERMISSION_403_DENIED_RBAC");
        assert_eq!(
            err.status(),
            403,
            "status must survive the round-trip, not collapse to 502"
        );
    }

    #[test]
    fn storage_problem_json_reconstructs_delegated_error() {
        let source: WyrdError = WyrdStorageError::InvalidUploadId {
            reason: "upload id must start with wyu_".to_owned(),
        }
        .into();

        let mapped = from_problem_json(&source.as_problem_json());

        assert_eq!(mapped.code(), "WYRD_STORAGE_400_INVALID_UPLOAD_ID");
        assert_eq!(mapped.status(), 400);
        assert_eq!(mapped.as_problem_json(), source.as_problem_json());
    }

    #[test]
    fn client_error_code_accessor_reports_stable_codes() {
        assert_eq!(
            WyrdClientError::Config {
                field: "f".to_owned(),
                reason: "r".to_owned(),
            }
            .code(),
            "WYRD_CLIENT_400_CONFIG_INVALID"
        );
        assert_eq!(
            WyrdClientError::NoCredentials.code(),
            "WYRD_CLIENT_401_NO_CREDENTIALS"
        );
        assert_eq!(
            WyrdClientError::TransportDown {
                transport: "http".to_owned(),
                message: "refused".to_owned(),
            }
            .code(),
            "WYRD_CLIENT_503_TRANSPORT_DOWN"
        );
    }

    /// The actionable half of the no-credentials error names the field that
    /// still exists.
    ///
    /// `ClientConfig` carries one `credential`; the former per-grant `api_key`
    /// field is gone, so telling a caller to pass it would send them to an API
    /// that no longer compiles.
    #[test]
    fn no_credentials_guidance_names_the_credential_field() {
        let message = WyrdClientError::NoCredentials.to_string();
        assert!(
            message.contains("ClientConfig::credential"),
            "guidance must name the field a caller can actually set: {message}"
        );
        assert!(
            !message.contains("ClientConfig::api_key"),
            "guidance must not name the removed per-grant field: {message}"
        );
    }

    #[test]
    fn client_error_has_only_expected_variants() {
        // Smoke test: verify Config, TransportDown, NoCredentials compile.
        // PayloadTooLarge and FlushTimeout are intentionally absent — those
        // variants moved to wyrd-queue.
        let _c = WyrdClientError::Config {
            field: "f".to_owned(),
            reason: "r".to_owned(),
        };
        let _d = WyrdClientError::TransportDown {
            transport: "grpc".to_owned(),
            message: "refused".to_owned(),
        };
        let _ = WyrdClientError::NoCredentials;
    }

    mod grpc_convergence {
        use std::collections::HashMap;

        use wyrd_tonic::tonic::Code;
        use wyrd_tonic::tonic_types::{ErrorDetails, StatusExt};

        use wyrd_spec::error::WyrdError;
        use wyrd_spec::vala::error::BifrostError;
        use wyrd_tonic::error::wyrd_error_to_status;

        use crate::error::{from_grpc_status, from_problem_json};

        /// Capacity and poison problems reconstruct identically across both transports.
        ///
        /// Each problem is rendered by the server's own HTTP and gRPC producers,
        /// so retryability is read from the same serialized `BifrostError`.
        ///
        /// # Panics
        ///
        /// Panics when either transport loses the code or status, or when the
        /// two reconstructions disagree.
        #[test]
        fn capacity_http_and_grpc_reconstruct_retryability_identically() {
            for (sent, status) in [
                (BifrostError::QueryAdmissionRejected, 429),
                (BifrostError::QueryQueueFull, 429),
                (BifrostError::QueryMemoryRequestTooLarge, 422),
                (BifrostError::QueryExecutionFailed, 500),
            ] {
                let sent = WyrdError::from(sent);
                let from_http = from_problem_json(&sent.as_problem_json());
                let from_grpc = from_grpc_status(&wyrd_error_to_status(sent.clone(), None));
                assert_eq!(from_grpc.code(), sent.code());
                assert_eq!(from_grpc.status(), status);
                assert_eq!(from_grpc.as_problem_json(), from_http.as_problem_json());
            }
        }

        #[test]
        fn grpc_and_http_same_code_produce_equal_wyrd_error() {
            let details = ErrorDetails::with_error_info(
                "WYRD_SPEC_404_NOT_FOUND",
                "wyrd.dev",
                HashMap::new(),
            );
            let status = wyrd_tonic::tonic::Status::with_error_details(
                Code::NotFound,
                "card not found",
                details,
            );

            let body = serde_json::json!({
                "code": "WYRD_SPEC_404_NOT_FOUND",
                "detail": "card not found",
                "details": {},
            });

            let from_grpc = from_grpc_status(&status);
            let from_http = from_problem_json(&body);

            assert_eq!(
                from_grpc.as_problem_json(),
                from_http.as_problem_json(),
                "gRPC and HTTP mappers must produce identical WyrdError for the same stable code"
            );
        }

        /// The enforced payload ceiling must reach the caller identically over
        /// both transports. A client that cannot read `limit` from the
        /// reconstructed error cannot resize its batch to fit; it can only
        /// guess at a ceiling the server never promised.
        ///
        /// # Panics
        ///
        /// Panics when either transport loses the code, status, measured bytes,
        /// enforced limit, detail, or remediation, or when the two disagree.
        #[test]
        fn payload_limit_reconstructs_identically_across_transports() {
            let sent = WyrdError::from(BifrostError::PayloadTooLarge {
                bytes: 41_943_040,
                limit: 8_388_608,
            });
            let detail = sent.to_string();
            let grpc = wyrd_error_to_status(sent.clone(), None);
            let http = sent.as_problem_json();

            let from_grpc = from_grpc_status(&grpc);
            let from_http = from_problem_json(&http);
            assert_eq!(
                from_grpc.as_problem_json(),
                from_http.as_problem_json(),
                "both transports must reconstruct the same payload-limit error"
            );

            for reconstructed in [&from_grpc, &from_http] {
                assert_eq!(reconstructed.code(), "WYRD_VALA_413_PAYLOAD_TOO_LARGE");
                assert_eq!(reconstructed.status(), 413);
                assert_eq!(reconstructed.to_string(), detail);
                let problem = reconstructed.as_problem_json();
                assert_eq!(
                    problem["details"]["data"]["bytes"], 41_943_040,
                    "measured bytes must survive the transport: {problem}"
                );
                assert_eq!(
                    problem["details"]["data"]["limit"], 8_388_608,
                    "the enforced limit must survive the transport: {problem}"
                );
                let remediation = problem["remediation"].as_str().unwrap_or_default();
                assert!(
                    !remediation.contains("32 MiB"),
                    "remediation must not contradict the supplied limit: {problem}"
                );
                assert!(
                    remediation.contains("limit"),
                    "remediation must point at the supplied limit: {problem}"
                );
            }
        }

        #[test]
        fn grpc_unknown_code_preserved_in_catch_all() {
            let details = ErrorDetails::with_error_info(
                "WYRD_CUSTOM_999_UNKNOWN",
                "wyrd.dev",
                HashMap::new(),
            );
            let status = wyrd_tonic::tonic::Status::with_error_details(
                Code::Unknown,
                "unknown server error",
                details,
            );
            let err = from_grpc_status(&status);
            assert_eq!(
                err.code(),
                "WYRD_SPEC_502_UPSTREAM_FAILURE",
                "unknown gRPC code must fall through to UpstreamFailure catch-all"
            );
            let json = err.as_problem_json();
            assert_eq!(
                json["details"]["original_code"].as_str(),
                Some("WYRD_CUSTOM_999_UNKNOWN"),
                "original code must be preserved in catch-all variant details"
            );
        }
    }
}