tatara_process/err_ctx.rs
1//! Substrate primitive over `Result<T, E>` for any `E: `[`std::fmt::
2//! Display`] — the ONE substrate owner of the generic `.map_err(|e|
3//! anyhow::anyhow!("<ctx>: {e}"))` display-prefix wrap-shape.
4//!
5//! Every one of the sibling per-error-type peers ([`crate::kube_error::
6//! KubeResultExt`], [`crate::anyhow_flatten::FlattenCtxExt`],
7//! [`crate::hostname::HostnameResultExt`]) delegates its body onto
8//! this trait's `err_ctx` / `err_ctx_with` — the specialized peers
9//! stay for the naming discipline (each name is deliberately DISTINCT
10//! from [`anyhow::Context::context`] to prevent silent resolution to
11//! the chain-wrap semantics that would drop the source error's
12//! `Display` output from every `tracing::error!(error = %e, ...)`
13//! log line), but the byte-shape body itself lives at ONE substrate
14//! owner here. Pre-lift each peer restated the SAME
15//! `.map_err(|e| anyhow::anyhow!("{ctx}: {e}"))` closure by hand,
16//! pinned equal only by peer-side byte-shape tests; post-delegation
17//! byte-shape agreement holds by CONSTRUCTION and the cross-peer
18//! agreement tests in this module ([`tests::err_ctx_agrees_with_kube_ctx_on_kube_error_result`],
19//! [`tests::err_ctx_agrees_with_flatten_ctx_on_anyhow_result`],
20//! [`tests::err_ctx_agrees_with_hostname_ctx_on_hostname_error_result`],
21//! plus the `_with` peers) route the delegation invariant through
22//! this substrate.
23//!
24//! Peer of the type-specific flatten-wrap trait trio already in this
25//! crate on the same display-prefix wrap axis, partitioning the space
26//! by SPECIFICITY:
27//!
28//! * [`crate::kube_error::KubeResultExt::kube_ctx`] — the specialized
29//! peer for `Result<T, kube::Error>`, kept because
30//! [`kube::Error`]'s `Display` composes the request URI + status
31//! line in a shape every reconciler-side callsite already greps on.
32//! * [`crate::hostname::HostnameResultExt::hostname_ctx`] — the
33//! specialized peer for `Result<T, `[`crate::hostname::HostnameError`]`>`,
34//! kept because the [`thiserror`]-derived `Display` output matches
35//! the pre-lift hand-authored render_routing log stream verbatim.
36//! * [`crate::anyhow_flatten::FlattenCtxExt::flatten_ctx`] — the
37//! specialized peer for `anyhow::Result<T>`, kept because it
38//! collides with [`anyhow::Context::context`]'s naming so the
39//! distinct-method-name discipline (flatten-prefix vs. chain-wrap
40//! semantics) is load-bearing at every phase-machine callsite.
41//! * [`ErrCtxExt::err_ctx`] (this trait) — the shared substrate body
42//! the three specialized peers delegate onto, AND the reachable
43//! surface for any new consumer whose source error is a fresh
44//! [`crate::tagged_union::declare_tagged_union_error`]-derived
45//! variant (e.g. [`crate::export::ArtifactError`],
46//! [`crate::intent::IntentError`],
47//! [`crate::lifetime::LifetimeError`]). Such a consumer reaches the
48//! display-prefix wrap-shape mechanically at THIS ONE substrate
49//! owner instead of opening a fourth per-error-type peer trait for
50//! every fresh [`thiserror`]-derived enum.
51//!
52//! Pre-lift the shape was hand-authored at TWO
53//! `tatara-export-worker/src/main.rs` sites past the ★★ PRIME-DIRECTIVE
54//! ≥ 2 duplication threshold, both restating the SAME closure —
55//! capture an [`crate::export::ArtifactError`] returned by the
56//! substrate primitive [`crate::export::ArtifactSource::variant`],
57//! prepend the identical static context slug `"source"`, delegate
58//! the tail to [`std::fmt::Display`] via the `{e}` slot — differing
59//! in NOTHING but their line numbers. Post-lift both callsites read
60//! `spec.source.variant().err_ctx("source")?` and the wrap-shape
61//! lives at ONE substrate owner here.
62//!
63//! ### Naming — `err_ctx`, not `context`
64//!
65//! Same discipline as the three specialized peers: the method name
66//! `err_ctx` is deliberately DISTINCT from [`anyhow::Context::context`]
67//! so a caller with [`anyhow::Context`] in scope can never resolve to
68//! the wrong method (which chain-wraps rather than display-prefix-
69//! flattens, and would silently drop the underlying error detail from
70//! every downstream `tracing::error!(error = %e, ...)` log line whose
71//! formatter interpolates `{e}` rather than the chain-walking `{e:#}`).
72//!
73//! ### Two flavors: `err_ctx` + `err_ctx_with`
74//!
75//! * [`ErrCtxExt::err_ctx`] takes a `&'static str` context — the
76//! most common shape (`"source"` at the two export-worker
77//! callsites). Static binding keeps the compile-time contract that
78//! the context slug is a bare literal, no allocation, no dynamic
79//! content leaking into an error stream downstream operators grep
80//! on.
81//! * [`ErrCtxExt::err_ctx_with`] takes an owned [`String`] context —
82//! the escape hatch for future consumers that compose the slug via
83//! [`format!`] (e.g. a dynamic per-variant-name slug the way
84//! [`crate::anyhow_flatten::FlattenCtxExt::flatten_ctx_with`]
85//! already services for `phase_machine::evaluate_conditions`).
86//!
87//! ### `#[must_use]`
88//!
89//! Every consumer threads the `?` short-circuit onto its handler's
90//! `anyhow::Result<_>` return — dropping the wrap swallows the
91//! underlying failure entirely, which is never the intended
92//! semantic.
93//!
94//! Theory anchor: THEORY.md §VI.1 (generation over composition — the
95//! generic display-prefix wrap-shape recurred at two byte-identical
96//! hand-authored sites in `tatara-export-worker/src/main.rs` past
97//! the ★★ PRIME-DIRECTIVE ≥ 2 duplication trigger, and is lifted to
98//! ONE substrate owner here). THEORY.md §II.1 invariant 5
99//! (composition preserves proofs — a regression that drifts the
100//! display-prefix separator or the byte-shape surfaces here at the
101//! substrate pin rather than as silent operator-facing skew across
102//! every downstream `err_ctx` consumer).
103
104/// Substrate extension trait over `Result<T, E>` for any `E: `
105/// [`std::fmt::Display`] — the ONE substrate owner of the generic
106/// `.map_err(|e| anyhow::anyhow!("<ctx>: {e}"))` display-prefix
107/// wrap-shape for consumers whose source error is a bare `Display`
108/// type NOT already covered by a per-error-type flatten-wrap peer.
109/// See the module docs for the specialized-peer partition + the
110/// naming rationale (why `err_ctx` and not `context`).
111pub trait ErrCtxExt<T>: Sized {
112 /// Wrap the source error (if any) with a static context prefix,
113 /// producing an [`anyhow::Result`] whose error `Display` reads
114 /// exactly `"<context>: <source display>"`.
115 #[must_use = "an error wrap that isn't threaded via `?` swallows the underlying failure"]
116 fn err_ctx(self, context: &'static str) -> anyhow::Result<T>;
117
118 /// Owned-string peer of [`Self::err_ctx`] — the escape hatch for
119 /// consumers that compose the context slug via [`format!`].
120 #[must_use = "an error wrap that isn't threaded via `?` swallows the underlying failure"]
121 fn err_ctx_with(self, context: String) -> anyhow::Result<T>;
122}
123
124impl<T, E> ErrCtxExt<T> for Result<T, E>
125where
126 E: std::fmt::Display,
127{
128 #[inline]
129 fn err_ctx(self, context: &'static str) -> anyhow::Result<T> {
130 self.map_err(|e| anyhow::anyhow!("{context}: {e}"))
131 }
132
133 #[inline]
134 fn err_ctx_with(self, context: String) -> anyhow::Result<T> {
135 self.map_err(|e| anyhow::anyhow!("{context}: {e}"))
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 // A representative bare-`Display` error type — matches the shape
144 // of [`crate::export::ArtifactError`] (the concrete
145 // [`thiserror`]-derived error whose two pre-lift callsites drove
146 // this lift) without pulling the whole export module into the
147 // test surface. A regression that promotes the trait bound to
148 // e.g. `E: std::error::Error` would surface HERE (this type
149 // doesn't impl `Error`) rather than as a silent narrowing of the
150 // substrate's admissibility surface.
151 #[derive(Debug)]
152 struct DisplayErr(&'static str);
153 impl std::fmt::Display for DisplayErr {
154 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155 f.write_str(self.0)
156 }
157 }
158
159 // ─── ErrCtxExt::err_ctx substrate pins ───────────────────────────
160 //
161 // Fail-before-pass-after granularity: the `ErrCtxExt::err_ctx`
162 // trait method did not exist before this commit, so each test
163 // below fails to compile pre-lift. Post-lift they collectively
164 // pin the display-prefix wrap-shape at ONE substrate owner — a
165 // regression that drifts the separator, swaps the two slots,
166 // narrows the trait bound (`E: Error` in place of `E: Display`,
167 // ruling out today's `ArtifactError` and similar bare-Display
168 // enums that don't derive `Error`), or promotes the pass-through
169 // arm to a synthesis surfaces HERE rather than as silent
170 // operator-facing skew across the two export-worker pre-lift
171 // consumers whose log output already encoded the flat
172 // `"source: <ArtifactError display>"` shape.
173
174 #[test]
175 fn err_ctx_static_str_context_matches_pre_lift_format_bytewise() {
176 // Byte-shape parity pin: the wrap output of
177 // `err_ctx("<slug>")` MUST be `Display`-identical to the
178 // pre-lift hand-authored `.map_err(|e| anyhow!("<slug>:
179 // {e}"))` chain. A regression that inserted a separator
180 // character (`"<slug>:: <err>"`), dropped the space after
181 // the colon, or swapped the two slots (`"<err>: <slug>"`)
182 // surfaces HERE rather than as silent drift at every
183 // downstream log-output consumer.
184 let raw: Result<(), DisplayErr> = Err(DisplayErr("bad slot"));
185 let via_trait = raw.err_ctx("source").unwrap_err();
186 assert_eq!(format!("{via_trait}"), "source: bad slot");
187 }
188
189 #[test]
190 fn err_ctx_ok_arm_is_a_pure_passthrough() {
191 // Ok-arm invariant: `err_ctx` on `Ok(t)` MUST return `Ok(t)`
192 // verbatim — no side-effect on the payload, no synthesis of
193 // a context-tagged error. Peer to the Err-arm byte-shape
194 // pin; a regression that promoted the Ok arm to ALWAYS
195 // produce a synthesis Error would silently break every
196 // successful downstream primitive call in the pre-lift
197 // consumer set.
198 let raw: Result<i32, DisplayErr> = Ok(42);
199 assert_eq!(raw.err_ctx("noop").unwrap(), 42);
200 }
201
202 #[test]
203 fn err_ctx_with_owned_string_matches_pre_lift_format_bytewise() {
204 // Owned-string peer's byte-shape pin — same discipline as
205 // the static-`&str` peer above. Future consumers that
206 // compose the context slug via `format!` route through this
207 // method and inherit the SAME display-prefix discipline as
208 // the static-slug peer, so mixing the two forms across a
209 // consumer's log stream never surfaces as a format-string
210 // skew.
211 let raw: Result<(), DisplayErr> = Err(DisplayErr("bad slot"));
212 let dynamic_slug = format!("evaluate {:?}", "SomeVariant");
213 let via_trait = raw.err_ctx_with(dynamic_slug.clone()).unwrap_err();
214 assert_eq!(format!("{via_trait}"), format!("{dynamic_slug}: bad slot"));
215 }
216
217 #[test]
218 fn err_ctx_with_ok_arm_is_a_pure_passthrough() {
219 // Ok-arm invariant on the owned-string peer — sibling to
220 // the static-slug pin above.
221 let raw: Result<&'static str, DisplayErr> = Ok("variant resolved");
222 assert_eq!(
223 raw.err_ctx_with("dynamic".to_string()).unwrap(),
224 "variant resolved"
225 );
226 }
227
228 #[test]
229 fn err_ctx_static_and_owned_peers_produce_identical_output_for_the_same_slug() {
230 // Cross-peer coherence pin: given the SAME context slug via
231 // both peers, the wrapped [`anyhow::Error`] MUST have
232 // byte-identical `Display` output.
233 let slug = "source";
234 let a: Result<(), DisplayErr> = Err(DisplayErr("bad slot"));
235 let b: Result<(), DisplayErr> = Err(DisplayErr("bad slot"));
236 assert_eq!(
237 format!("{}", a.err_ctx(slug).unwrap_err()),
238 format!("{}", b.err_ctx_with(slug.to_string()).unwrap_err()),
239 "static-str and owned-string peers must produce identical Display output"
240 );
241 }
242
243 #[test]
244 fn err_ctx_threads_the_underlying_display_verbatim() {
245 // Display-tail invariant: the wrapped [`anyhow::Error`]'s
246 // `Display` output MUST contain the source error's own
247 // `Display` output verbatim as the tail past `"<ctx>: "`.
248 let underlying_display = format!("{}", DisplayErr("bad slot"));
249 let raw: Result<(), DisplayErr> = Err(DisplayErr("bad slot"));
250 let wrapped = raw.err_ctx("source").unwrap_err();
251 let wrapped_display = format!("{wrapped}");
252 assert!(
253 wrapped_display.ends_with(&underlying_display),
254 "wrapped Display `{wrapped_display}` must end with underlying Display `{underlying_display}`"
255 );
256 assert!(
257 wrapped_display.starts_with("source: "),
258 "wrapped Display `{wrapped_display}` must start with `\"<ctx>: \"`"
259 );
260 }
261
262 #[test]
263 fn err_ctx_admits_thiserror_derived_tagged_union_error() {
264 // Substrate coverage pin: the trait's `E: Display` bound
265 // MUST admit the concrete error type both pre-lift
266 // export-worker callsites captured — a
267 // [`crate::tagged_union::declare_tagged_union_error`]-
268 // derived variant whose `Display` is [`thiserror`]-generated.
269 // This test exercises `ArtifactError` specifically (the two
270 // pre-lift consumers' source error) so a regression that
271 // dropped its `Display` impl, narrowed the trait bound, or
272 // otherwise made the primitive inapplicable to the very
273 // callsites it was opened for surfaces HERE.
274 use crate::export::{ArtifactError, ARTIFACT_KIND_LIST};
275 let raw: Result<(), ArtifactError> = Err(ArtifactError::Empty(ARTIFACT_KIND_LIST));
276 let wrapped = raw.err_ctx("source").unwrap_err();
277 let wrapped_display = format!("{wrapped}");
278 assert!(
279 wrapped_display.starts_with("source: "),
280 "wrapped Display `{wrapped_display}` must start with `\"source: \"`"
281 );
282 assert!(
283 wrapped_display.contains(ARTIFACT_KIND_LIST),
284 "wrapped Display `{wrapped_display}` must thread ArtifactError's Display body verbatim"
285 );
286 }
287
288 #[test]
289 fn err_ctx_agrees_with_flatten_ctx_on_anyhow_result() {
290 // Cross-substrate coherence pin: on the specific input shape
291 // `anyhow::Result<T>`, the generic `err_ctx` and the
292 // specialized peer
293 // [`crate::anyhow_flatten::FlattenCtxExt::flatten_ctx`] MUST
294 // produce byte-identical `Display` output. Post-delegation
295 // (both bodies routed onto this substrate owner) byte-shape
296 // agreement holds by CONSTRUCTION rather than by two
297 // independent hand-authored `.map_err(|e| anyhow!)` closures
298 // pinned equal by convention; a regression that re-open-coded
299 // the specialized peer's body surfaces HERE rather than as
300 // silent operator-facing skew between the consumers migrated
301 // onto the generic and consumers still routed through the
302 // specialized peer.
303 use crate::anyhow_flatten::FlattenCtxExt;
304 let a: anyhow::Result<()> = Err(anyhow::anyhow!("underlying failure"));
305 let b: anyhow::Result<()> = Err(anyhow::anyhow!("underlying failure"));
306 assert_eq!(
307 format!("{}", a.err_ctx("source").unwrap_err()),
308 format!("{}", b.flatten_ctx("source").unwrap_err()),
309 "generic err_ctx and specialized flatten_ctx must agree on anyhow::Result"
310 );
311 }
312
313 #[test]
314 fn err_ctx_with_agrees_with_flatten_ctx_with_on_dynamic_slug() {
315 // Owned-string peer coherence pin: sibling to the static-slug
316 // pin above, walking the `_with` peer instead. Pre-delegation
317 // the two owned-string bodies were byte-identical by
318 // convention (each restating the SAME
319 // `.map_err(|e| anyhow!("{ctx}: {e}"))` chain); post-
320 // delegation the specialized `flatten_ctx_with` routes onto
321 // this substrate's `err_ctx_with`, and byte-shape agreement
322 // holds by CONSTRUCTION. A regression that re-open-coded
323 // `flatten_ctx_with`'s body — dropping the delegation and
324 // restoring the pre-lift inline closure with a drifted
325 // separator or a swapped-slots typo — surfaces HERE rather
326 // than as silent skew between the four static-slug consumers
327 // and the one `format!`-slug consumer in the reconciler's
328 // phase-machine log stream.
329 use crate::anyhow_flatten::FlattenCtxExt;
330 let slug = format!("evaluate {:?}", "HelmReleaseReleased");
331 let a: anyhow::Result<()> = Err(anyhow::anyhow!("underlying failure"));
332 let b: anyhow::Result<()> = Err(anyhow::anyhow!("underlying failure"));
333 assert_eq!(
334 format!("{}", a.err_ctx_with(slug.clone()).unwrap_err()),
335 format!("{}", b.flatten_ctx_with(slug).unwrap_err()),
336 "generic err_ctx_with and specialized flatten_ctx_with must agree on anyhow::Result"
337 );
338 }
339
340 #[test]
341 fn err_ctx_agrees_with_kube_ctx_on_kube_error_result() {
342 // Cross-substrate coherence pin: on the specific input shape
343 // `Result<T, kube::Error>`, the generic `err_ctx` and the
344 // specialized peer
345 // [`crate::kube_error::KubeResultExt::kube_ctx`] MUST produce
346 // byte-identical `Display` output. Post-delegation (the
347 // specialized peer's body routes onto this substrate) byte-
348 // shape agreement holds by CONSTRUCTION — `kube::Error:
349 // Display` so the generic `err_ctx` impl applies to the same
350 // input type. A regression that re-open-coded `kube_ctx`'s
351 // body — dropping the delegation and restoring the pre-lift
352 // inline closure — surfaces HERE rather than as silent
353 // operator-facing skew between the 25+ K8s-round-trip
354 // consumers and the sibling peer families.
355 use crate::kube_error::KubeResultExt;
356 use kube::core::ErrorResponse;
357 let mk = || {
358 kube::Error::Api(ErrorResponse {
359 status: "Failure".into(),
360 message: "test code 404".into(),
361 reason: "NotFound".into(),
362 code: 404,
363 })
364 };
365 let a: Result<(), kube::Error> = Err(mk());
366 let b: Result<(), kube::Error> = Err(mk());
367 assert_eq!(
368 format!("{}", a.err_ctx("install finalizer").unwrap_err()),
369 format!("{}", b.kube_ctx("install finalizer").unwrap_err()),
370 "generic err_ctx and specialized kube_ctx must agree on Result<_, kube::Error>"
371 );
372 }
373
374 #[test]
375 fn err_ctx_with_agrees_with_kube_ctx_with_on_dynamic_slug() {
376 // Owned-string peer coherence pin on the Kube axis: sibling
377 // to the static-slug pin above, walking the `_with` peer
378 // instead. Post-delegation the specialized `kube_ctx_with`
379 // routes onto this substrate's `err_ctx_with`, and byte-
380 // shape agreement holds by CONSTRUCTION.
381 use crate::kube_error::KubeResultExt;
382 use kube::core::ErrorResponse;
383 let mk = || {
384 kube::Error::Api(ErrorResponse {
385 status: "Failure".into(),
386 message: "test code 409".into(),
387 reason: "AlreadyExists".into(),
388 code: 409,
389 })
390 };
391 let slug = format!("patch (releasing→{})", "Exiting");
392 let a: Result<(), kube::Error> = Err(mk());
393 let b: Result<(), kube::Error> = Err(mk());
394 assert_eq!(
395 format!("{}", a.err_ctx_with(slug.clone()).unwrap_err()),
396 format!("{}", b.kube_ctx_with(slug).unwrap_err()),
397 "generic err_ctx_with and specialized kube_ctx_with must agree on Result<_, kube::Error>"
398 );
399 }
400
401 #[test]
402 fn err_ctx_agrees_with_hostname_ctx_on_hostname_error_result() {
403 // Cross-substrate coherence pin: on the specific input shape
404 // `Result<T, HostnameError>`, the generic `err_ctx` and the
405 // specialized peer
406 // [`crate::hostname::HostnameResultExt::hostname_ctx`] MUST
407 // produce byte-identical `Display` output. Post-delegation
408 // the specialized peer's body routes onto this substrate;
409 // [`crate::hostname::HostnameError`] impls `Display` via
410 // `thiserror` so the generic `err_ctx` impl applies to the
411 // same input type. A regression that re-open-coded
412 // `hostname_ctx`'s body surfaces HERE rather than as silent
413 // operator-facing skew between `render_routing`'s
414 // hostname-format consumer and the sibling peer families.
415 use crate::hostname::{HostnameError, HostnameResultExt};
416 let mk = || HostnameError::ReservedApp("auth".to_string());
417 let a: Result<(), HostnameError> = Err(mk());
418 let b: Result<(), HostnameError> = Err(mk());
419 assert_eq!(
420 format!("{}", a.err_ctx("render routing").unwrap_err()),
421 format!("{}", b.hostname_ctx("render routing").unwrap_err()),
422 "generic err_ctx and specialized hostname_ctx must agree on Result<_, HostnameError>"
423 );
424 }
425}