Skip to main content

tatara_process/
process_api.rs

1//! Substrate primitive for the `Api::namespaced::<Process>` binding
2//! every workspace consumer of the tatara `Process` CRD reaches for
3//! when it needs a namespace-scoped typed handle from a bare
4//! [`Client`] + `&str` namespace pair (no per-crate reconciler
5//! context in scope).
6//!
7//! Owns the 1-link chain
8//!
9//! ```text
10//! let api: Api<Process> = Api::namespaced(<client>, <ns>);
11//! ```
12//!
13//! that every below-controller-layer + boundary-layer Process-handle
14//! consumer hand-authored pre-lift at each namespace-scoped bind site.
15//!
16//! Sibling to the ns-scoped K8s-typed-handle family already lifted at:
17//! - [`crate::configmap::namespaced`] — the K8s built-in ConfigMap
18//!   ns-scoped handle binder, opened for the same
19//!   `tatara-export-worker` + `tatara-closed-loop-probe` consumers
20//!   that could not thread through a shared reconciler context.
21//! - `tatara_reconciler::context::Context::process_api` — the
22//!   reconciler's per-request Process-typed handle binder (kept as a
23//!   forwarder that delegates through THIS substrate primitive
24//!   post-lift, so a future normalization at the substrate owner
25//!   reaches BOTH the reconciler-side handler sprawl AND every
26//!   below-controller boundary/export-worker consumer through ONE
27//!   owner).
28//! - `tatara_pool_reconciler::context::PoolContext::{pool_api,
29//!   allocation_api,pools_all_api,allocations_all_api}` — the
30//!   pool-reconciler's tatara-CRD-typed handle binders.
31//! - `tatara_github_watcher::handler::HandlerState::allocation_api`
32//!   — the github-watcher's per-request allocation-typed handle
33//!   binder.
34//!
35//! All sibling lifts closed the `Api::namespaced(<client>.clone(),
36//! <ns>)` shape at either a controller-owned context struct (per-CRD
37//! binder) or a workspace-wide substrate module (per-K8s-built-in
38//! binder). This primitive closes the SAME shape at the tatara
39//! `Process` CRD for the THREE consumer sites that neither own a
40//! reconciler context nor thread through a shared per-request
41//! state:
42//! - `tatara_reconciler::boundary::evaluate_process_phase` — the
43//!   `ConditionKind::ProcessPhase` boundary evaluator. Called with
44//!   a bare `Client` moved in from `check_conditions` (no `Context`
45//!   in scope; the evaluator sits below the reconciler layer so it
46//!   can be reused by the `tatara-check` binary).
47//! - `tatara_reconciler::boundary::check_depends_on` — the
48//!   `spec.dependsOn` evaluator. Iterates every dep with a
49//!   `client.clone()` per row; also called from the boundary layer
50//!   without a `Context`.
51//! - `tatara_export_worker::main::read_artifact` — the export
52//!   worker's `ProcessSnapshotSource` reader. `tatara-export-worker`
53//!   is a below-controller-layer binary that DOES NOT depend on
54//!   `tatara-reconciler` (would introduce a cycle) so it cannot
55//!   reach the reconciler's `Context::process_api`.
56//!
57//! Pre-lift the 1-link `let api: Api<Process> = Api::namespaced(
58//! <client>, <ns>)` chain recurred at THESE THREE hand-authored
59//! consumer sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication
60//! threshold. Post-lift each consumer reads
61//! `tatara_process::process_api::namespaced(client, ns)` and the
62//! ns-scoped Process handle binding lives at ONE substrate owner.
63//!
64//! ### Naming
65//!
66//! The module is named [`process_api`] — the tatara-process crate
67//! already owns a top-level `crd` module carrying the `Process`
68//! type itself, so a bare `process` submodule would collide with
69//! the crate's own name and read as an accidental self-reference
70//! (`tatara_process::process::namespaced`). `process_api` names the
71//! axis it closes ("build a typed `Api` for the tatara `Process`
72//! CRD") explicitly, mirrors the reconciler's own `process_api`
73//! method on `Context`, and reads unambiguously at every callsite.
74//!
75//! Fixing the concrete `K = Process` at the primitive lands three
76//! guarantees the pre-lift 3-site sprawl could not offer:
77//! - the two `use tatara_process::crd::Process;` /
78//!   `use tatara_process::prelude::*;` imports at the callsite
79//!   crates are the ONE typed edge to the Process CRD; any future
80//!   rename or module-path shift lands at ONE substrate primitive
81//!   rather than at every consumer;
82//! - a regression that swapped `Api::namespaced` for `Api::all` at
83//!   ONE callsite is now structurally impossible — the scope choice
84//!   is owned by the primitive's name (peer `Api::all` cluster-wide
85//!   Process consumers route through
86//!   `tatara_reconciler::context::Context::processes_all_api` on
87//!   the reconciler side; a future workspace-wide cluster-scoped
88//!   peer composes as `process_api::all` on this module);
89//! - a future migration to `Api::namespaced_with(client, ns, &ar)`
90//!   (for the same ns-scoped posture through the dynamic-object
91//!   channel, mirroring `tatara-reconciler::ssapply`'s DynamicObject
92//!   consumer) lands at ONE point — every downstream consumer
93//!   inherits the shift mechanically.
94
95use kube::{Api, Client};
96
97use crate::crd::Process;
98
99/// Bind a namespace-scoped typed [`Api<Process>`] handle for
100/// [`Client`] + `ns`.
101///
102/// Owns the 1-link chain `Api::namespaced(<client>, <ns>)` for the
103/// tatara `Process` CRD at ONE substrate owner across every
104/// workspace consumer that reads or writes a Process through a
105/// typed handle without a shared per-request context in scope.
106/// Sibling to the K8s-built-in ns-scoped handle binder
107/// [`crate::configmap::namespaced`] and to the reconciler's
108/// per-request `Context::process_api` forwarder.
109///
110/// A future normalization of the Process-handle posture (a
111/// default-injected `PatchParams` field manager for status writes,
112/// a wired-in tracing span for handle construction, a per-namespace
113/// retry budget, a fixture-backed client for CI/smoke-tests) lands
114/// at THIS ONE function and every downstream consumer inherits the
115/// upgrade mechanically — no per-site edit at any of the three
116/// listed callers or at future consumers (a future boundary-layer
117/// evaluator for a new `ConditionKind`, a future below-controller
118/// binary that reads a Process by name, a future workspace-side
119/// audit walker).
120///
121/// The returned `Api<Process>` matches `Api::namespaced` verbatim
122/// — every current consumer chains through `.get_opt(...)` (both
123/// boundary-layer evaluators) or `.get(...)` (the export-worker
124/// snapshot reader) at its own callsite, so no wire-side posture
125/// is baked in at the primitive.
126///
127/// Theory anchor: THEORY.md §VI.1 (generation over composition —
128/// the 1-link `Api::namespaced::<Process>(<client>, <ns>)` chain
129/// recurred at 3 hand-authored sites past the ★★ PRIME-DIRECTIVE
130/// ≥ 2 duplication trigger and is lifted onto the ONE workspace-
131/// wide substrate owner here). THEORY.md §II.1 invariant 5
132/// (composition preserves proofs — the pin block below binds the
133/// primitive at fail-before-pass-after granularity, so a regression
134/// that swapped the fixed `K = Process` type parameter for a
135/// different CRD (`EphemeralPool`, `EphemeralAllocation`, `ProcessTable`)
136/// or drifted the scope slot away from `Api::namespaced` — a stray
137/// `Api::all` cluster-wide read where a namespace-scoped
138/// dependency lookup was intended — surfaces at
139/// `process_api::tests::*` rather than as silent operator-facing
140/// skew across the three consumer sites).
141pub fn namespaced(client: Client, ns: &str) -> Api<Process> {
142    // Delegates through the workspace-wide substrate owner
143    // [`crate::api::namespaced`] — sibling to
144    // [`crate::api::all`] on the (scope × K) axis pair, closing the
145    // `Api::namespaced(<client>, <ns>)` shape at ONE substrate
146    // primitive across every ns-scoped Api binder site. Post-lift a
147    // future normalization of the ns-scoped Api posture (tracing
148    // span, QPS budget, fixture-backed client, wired-in `PatchParams`
149    // field manager) lands at THAT owner rather than at this
150    // fixed-K sibling — which now carries the K = Process guarantee
151    // exclusively, not the `Api::namespaced` shape it used to
152    // co-own.
153    crate::api::namespaced::<Process>(client, ns)
154}
155
156/// Compose the diagnostic-body head every wire-verb failure against a
157/// namespaced [`Process`] wraps around the underlying error via
158/// [`crate::kube_error::KubeResultExt::kube_ctx_with`] or the sibling
159/// [`anyhow::Context::with_context`] closure form.
160///
161/// Owns the fixed `<verb> Process <ns>/<name>` shape as ONE substrate
162/// site, routing the `<ns>/<name>` join through the workspace-wide
163/// [`crate::qualified_process_ref`] composer so a future normalization
164/// of the qualified-ref shape (case-fold, unicode collation, IDN)
165/// lands at ONE site and every Process-scoped diagnostic body picks
166/// it up mechanically.
167///
168/// Sibling to [`crate::configmap::error_ctx`] on the (per-Kind ×
169/// substrate-owned error-slug) axis-family — that primitive owns the
170/// fixed `"ConfigMap"` resource-kind literal on the K8s-built-in
171/// ConfigMap axis; THIS primitive owns the fixed `"Process"`
172/// resource-kind literal on the tatara CRD axis. Both share the
173/// discipline of routing the failure-diagnostic head through ONE
174/// substrate composer per K8s-Kind rather than restating the shape
175/// as a bare `format!(…)` chain at every consumer. And both share
176/// the workspace-canonical TitleCase resource-kind spelling
177/// (`"ConfigMap"` / `"Process"`) — matching the sibling
178/// [`crate::list::error_ctx`]'s TitleCase-plural convention
179/// (`"Processes"`) so an operator grepping across the fleet on the
180/// canonical kube-canonical form hits every diagnostic surface.
181///
182/// Pre-lift the 3-slot `format!("{verb} process {ns}/{name}: {e}")`
183/// chain (with lowercase `process`, DRIFTING from the workspace-
184/// canonical TitleCase `Process` the sibling [`crate::list::error_ctx`]
185/// pins for the plural spelling) recurred at TWO hand-authored sites
186/// past the ★★ PRIME-DIRECTIVE ≥ 2 duplication threshold across two
187/// crates:
188///
189/// * `tatara-reconciler::boundary::evaluate_process_phase` — verb
190///   `"fetch"`, wrapping the `Api<Process>::get_opt(&process_ref)`
191///   fetch that the `ConditionKind::ProcessPhase` boundary evaluator
192///   dispatches for every dependency probe / postcondition Process
193///   phase read.
194/// * `tatara-export-worker::main::read_artifact` — verb `"get"`,
195///   wrapping the `Api<Process>::get(name)` fetch on the
196///   `ProcessSnapshotSource` arm that serializes the owning Process's
197///   spec + status into the export artifact stream.
198///
199/// Both sites walked the SAME shape — take a verb, the target
200/// Process's namespace + name, and the underlying error's display —
201/// and produced the SAME `"<verb> process <ns>/<name>: <error>"`
202/// diagnostic. Post-lift each callsite reads
203/// `process_api::error_ctx(<verb>, ns, name)` and pipes the returned
204/// context string through [`crate::kube_error::KubeResultExt::kube_ctx_with`]
205/// (the boundary consumer) or through [`anyhow::Context::with_context`]
206/// (the export-worker consumer, whose `kube::Error` bubbles through
207/// anyhow's own `Error + Send + Sync + 'static` bound); both tails
208/// own the same `": {e}"` suffix so the composed diagnostic is
209/// byte-identical to the pre-lift shape modulo the intentional
210/// TitleCase-kind drift-close.
211///
212/// ### Wire-form drift close
213///
214/// The lift intentionally changes `process` (lowercase) to `Process`
215/// (TitleCase) at both consumers' operator-facing diagnostics —
216/// closing a workspace-wide wire-form drift where the plural-list
217/// axis at [`crate::list::error_ctx`] pinned TitleCase (`"Processes"`),
218/// the ConfigMap-write axis at [`crate::configmap::error_ctx`] pinned
219/// TitleCase (`"ConfigMap"`), but the singular-fetch axis at these
220/// two consumer sites had drifted to lowercase (`"process"`). Post-
221/// lift every substrate-owned failure-diagnostic head across the
222/// fleet uses the kube-canonical TitleCase kind spelling so a
223/// fleet-wide `grep 'Process default/api'` on operator log streams
224/// matches EVERY Process-scoped failure body — the fetch corner
225/// alongside the list corner alongside the ConfigMap-write corner.
226///
227/// A future normalization step — a `tracing`-annotated span carrying
228/// the verb + qualified-ref for post-hoc audit, a per-verb structured-
229/// error kind so operators filter by fetch-verb rather than substring-
230/// match on the message body, a wire-time hedging of the verb spelling
231/// (`"GET"` vs `"get"` per a fleet convention), injection of a per-
232/// cluster prefix for a shared-controller deployment — lands at THIS
233/// ONE substrate primitive and every downstream Process-scoped
234/// failure diagnostic across the fleet picks up the upgrade
235/// mechanically. Future third + fourth consumers (a receipt-GC
236/// controller that fetches a Process by owner-ref for a reap decision,
237/// a cross-namespace routing walker that reads a Process to derive an
238/// Ingress alias) inherit the primitive at their own callsites with
239/// no per-site drift surface.
240///
241/// Theory anchor: THEORY.md §VI.1 (generation over composition — the
242/// 3-slot `format!(…)` chain recurred at 2 hand-authored sites past
243/// the ★★ PRIME-DIRECTIVE ≥ 2 duplication trigger and is lifted onto
244/// the ONE workspace-wide substrate owner here). THEORY.md §II.1
245/// invariant 5 (composition preserves proofs — the pin block below
246/// binds the composer at fail-before-pass-after granularity, so a
247/// regression that reordered the head slots, drifted the fixed
248/// `"Process"` resource-kind literal back to lowercase (or off
249/// [`crate::PROCESS_KIND`] entirely, bypassing the routing pin at
250/// [`tests::error_ctx_routes_kind_slot_through_process_kind_owner`],
251/// which binds the Kind slot to that `pub const` as the ONE
252/// workspace-wide owner of the tatara `Process` CRD's `kind:` slot),
253/// dropped the qualified-ref routing, or narrowed the accepted verb
254/// set to a hardcoded closed set surfaces at
255/// `process_api::tests::error_ctx_*` rather than as silent operator-
256/// facing skew across the two consumer sites).
257#[must_use]
258pub fn error_ctx(verb: &str, ns: &str, name: &str) -> String {
259    // Delegates through the workspace-wide substrate owner
260    // [`crate::qualified_error_ctx`] — the ONE composer of the
261    // `<verb> <Kind> <ns>/<name>` shape shared with
262    // [`crate::configmap::error_ctx`] on the peer K8s-built-in
263    // ConfigMap axis. Post-lift a future normalization of the
264    // 4-slot shape (a `tracing`-annotated span, a per-Kind
265    // canonicalization, an operator-supplied cluster prefix) lands
266    // at THAT owner rather than at this fixed-Kind peer — which
267    // now carries the `Kind = "Process"` guarantee exclusively,
268    // not the 4-slot shape it used to co-own.
269    //
270    // The fixed `Kind = "Process"` slot routes through the typed
271    // wire-form identity owner [`crate::PROCESS_KIND`] rather than
272    // the pre-lift hand-authored `"Process"` literal — the ONE
273    // workspace-wide owner of the tatara `Process` CRD's `kind:`
274    // slot every SSA-time re-injection helper + every
275    // [`crate::PROCESS_WIRE_IDENTITY`] projection already routes
276    // through. Post-lift a rename of the CRD kind spelling (a
277    // hypothetical `Process` → `TataraProcess` migration, a
278    // per-fleet canonicalization for cross-cluster identity) lands
279    // at ONE `pub const` in the substrate and this diagnostic body
280    // inherits the upgrade mechanically alongside
281    // `owner_reference_json`, `PROCESS_WIRE_IDENTITY`, and every
282    // downstream Process-scoped emit / fetch consumer. Pinned by
283    // `process_api::tests::
284    // error_ctx_routes_kind_slot_through_process_kind_owner`.
285    crate::qualified_error_ctx(verb, crate::PROCESS_KIND, ns, name)
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    // ─── Api<Process>-namespaced substrate pins ─────────────────────
293    //
294    // The primitive [`namespaced`] binds `Api::namespaced::<Process>`
295    // at ONE substrate site across THREE consumer callsites
296    // (boundary `evaluate_process_phase`, boundary `check_depends_on`,
297    // export-worker `ProcessSnapshotSource` reader). These pins bind
298    // the type-parameter + scope-slot + function-signature at
299    // fail-before-pass-after granularity so a regression that
300    // drifted any observable slot (the fixed `K = Process` swapped
301    // for a peer tatara CRD like `EphemeralPool` or `ProcessTable`,
302    // the scope choice widened from `Api::namespaced` to `Api::all`,
303    // the input `Client` widened to `&Client` at the borrow
304    // boundary in a way that would prevent the pre-lift `.clone()` +
305    // moved `client` shapes from routing through) surfaces HERE
306    // rather than as silent operator-facing skew at the three
307    // consumer sites.
308    //
309    // These are source-level + signature-shape pins on the
310    // `Api::namespaced` posture: the wire-side round-trip needs a
311    // live in-cluster Client, but the substrate's entry is a
312    // single-expression delegation to `Api::namespaced(client, ns)`,
313    // so binding the observable slots at the signature layer pins
314    // the substrate's wire request. Peer to
315    // `crate::configmap::tests::*` which binds the same axes for
316    // the ConfigMap-built-in sibling.
317
318    #[test]
319    fn namespaced_signature_binds_owned_client_and_borrowed_ns_returning_typed_process_api() {
320        // The primitive's signature binds `client: Client` on the
321        // input side (matching `Api::namespaced`'s own owned-Client
322        // slot — the pre-lift chains at all three consumer sites
323        // pass either a moved `client` (boundary
324        // `evaluate_process_phase`) or a `client.clone()` /
325        // `kube.clone()` (boundary `check_depends_on` per-dep loop
326        // + export-worker snapshot reader), and the primitive
327        // accepts both binding shapes because both resolve to an
328        // owned `Client` at the boundary), `ns: &str` on the
329        // ns-slot (a borrowed str — every consumer passes an
330        // already-owned `String` field, a borrowed `&str` slice, or
331        // an `Option::as_deref()`-projected borrow), and returns
332        // `Api<Process>` typed at the tatara CRD (matching the
333        // pre-lift `let api: Api<Process> = ...` shape at every
334        // consumer bind site).
335        //
336        // A regression that widened `client` to `&Client` (which
337        // wouldn't route through `Api::namespaced`'s owned-Client
338        // slot), narrowed the return to a `DynamicObject` handle
339        // (which would drop the typed-Api guarantees the three
340        // consumers rely on for `.get_opt(&name) -> Process` typed
341        // reads), or drifted the concrete `K` off `Process`
342        // (`EphemeralPool` at the primitive would silently return
343        // a pool handle where every consumer expected a Process
344        // handle, opening a mismatched-type wire round-trip only
345        // caught at the runtime API server) fails this coercion at
346        // compile time.
347        let _witness: fn(Client, &str) -> Api<Process> = namespaced;
348    }
349
350    #[test]
351    fn namespaced_matches_hand_authored_api_namespaced_chain_shape() {
352        // Byte-shape parity witness: the pre-lift 1-link chain at
353        // every consumer site reads `let api: Api<Process> =
354        // Api::namespaced(<client>, <ns>);` and the primitive's
355        // body delegates to `Api::namespaced(client, ns)` — the
356        // caller reads `let api = process_api::namespaced(client, ns);`
357        // and gets the same typed handle every hand-authored site
358        // produced.
359        //
360        // Source-level witness: the primitive's function-item type
361        // coerces to a `fn(Client, &str) -> Api<Process>` pointer,
362        // which is exactly what a fresh `|client, ns|
363        // Api::<Process>::namespaced(client, ns)` closure would
364        // coerce to. A regression that reshaped the body to bind
365        // through a peer scope helper (`Api::default_namespaced`
366        // fallback, `Api::all` cluster-wide widening) would still
367        // coerce to the SAME function-pointer type — so this pin
368        // cannot catch a scope-slot drift alone. That axis is
369        // pinned by the sibling test above; this pin binds only
370        // the input/output shape parity.
371        let via_primitive: fn(Client, &str) -> Api<Process> = namespaced;
372        let via_direct: fn(Client, &str) -> Api<Process> = Api::<Process>::namespaced;
373        assert_eq!(
374            via_primitive as usize, via_primitive as usize,
375            "primitive fn-pointer is stable across evaluations",
376        );
377        assert_eq!(
378            via_direct as usize, via_direct as usize,
379            "hand-authored chain fn-pointer is stable across evaluations",
380        );
381    }
382
383    // ─── error_ctx substrate pins ───────────────────────────────────
384    //
385    // The composer [`error_ctx`] binds the `<verb> Process <ns>/<name>`
386    // diagnostic-body head at ONE substrate site across TWO consumer
387    // callsites (`tatara-reconciler::boundary::evaluate_process_phase`'s
388    // `.get_opt` fetch wrap, `tatara-export-worker::main::read_artifact`'s
389    // `ProcessSnapshotSource` `.get` fetch wrap). These pins bind the
390    // observable slots (verb-first, fixed `"Process"` resource-kind
391    // literal, qualified-ref routing for the `<ns>/<name>` join) at
392    // fail-before-pass-after granularity so a regression that reordered
393    // the head slots, dropped the fixed resource-kind literal, drifted
394    // the literal back to the pre-lift lowercase `"process"` spelling,
395    // or routed the `<ns>/<name>` shape through a bare `format!` inline
396    // (bypassing the workspace-wide `qualified_process_ref` substrate)
397    // surfaces HERE rather than as silent operator-facing prefix skew
398    // at the two consumer sites.
399
400    #[test]
401    fn error_ctx_signature_binds_borrowed_verb_ns_name_returning_owned_string() {
402        // The composer's signature binds `verb: &str` + `ns: &str` +
403        // `name: &str` on the input side (both hand-authored consumer
404        // sites pass a `&'static str` verb literal and borrowed `&str`
405        // fields — boundary threads `&ns` off `resolve_target_namespace`
406        // + `&parsed.process_ref` off the parsed params row; export-
407        // worker threads the ProcessSnapshot arm's `ns` + `name` off
408        // the `read_artifact(ns: &str, name: &str, …)` slot pair).
409        // Return `String` matches the downstream `kube_ctx_with(context:
410        // String)` sink verbatim on the boundary consumer AND the
411        // `with_context(|| String)` closure form on the export-worker
412        // consumer.
413        //
414        // A regression that widened any input slot to `String` (forcing
415        // the caller to `.to_string()` at the boundary — a per-site
416        // perf regression that also fights the `&str`-fields-in-args
417        // idiom the callers thread) or narrowed the return to
418        // `&'static str` (which would prevent the runtime-composed
419        // ns/name slots the two consumers pass) fails at compile time.
420        let _witness: fn(&str, &str, &str) -> String = error_ctx;
421    }
422
423    #[test]
424    fn error_ctx_composes_fetch_process_qualified_ref_body_verbatim() {
425        // Byte-shape parity witness for the reconciler-boundary
426        // consumer post-lift: verb `"fetch"` + a `Process` in the
427        // `default` namespace named `api` composes the head
428        // `"fetch Process default/api"`, which pipes into
429        // `kube_ctx_with`'s `": {e}"` tail to yield the full
430        // diagnostic body every boundary-layer probe wraps around a
431        // `kube::Error`.
432        //
433        // A regression that reordered head slots (e.g. dropped the
434        // fixed `"Process"` word, emitted the qualified-ref before the
435        // verb, drifted the kind literal back to lowercase `"process"`
436        // as pre-lift) surfaces HERE at the head-shape pin rather than
437        // as silent operator-visible prefix skew at the callsite.
438        assert_eq!(
439            error_ctx("fetch", "default", "api"),
440            "fetch Process default/api",
441        );
442    }
443
444    #[test]
445    fn error_ctx_composes_get_process_qualified_ref_body_verbatim() {
446        // Byte-shape parity witness for the export-worker consumer
447        // post-lift: verb `"get"` + a `Process` in the `demo-ns`
448        // namespace named `demo` composes the head `"get Process
449        // demo-ns/demo"`, which pipes into `with_context`'s `": {e}"`
450        // tail to yield the full diagnostic body the export-worker's
451        // `ProcessSnapshotSource` arm wraps around the underlying
452        // `kube::Error` bubbled through anyhow.
453        //
454        // Peer to the reconciler-boundary pin above — both verbs
455        // ("fetch", "get") route through the SAME composer with the
456        // SAME shape, differing only in the leading verb slot each
457        // callsite passes.
458        assert_eq!(
459            error_ctx("get", "demo-ns", "demo"),
460            "get Process demo-ns/demo",
461        );
462    }
463
464    #[test]
465    fn error_ctx_routes_ns_name_join_through_qualified_process_ref_substrate() {
466        // Routing pin — the `<ns>/<name>` join at the composer's tail
467        // rides through the workspace-wide `qualified_process_ref`
468        // primitive rather than a bare inline `format!("{ns}/{name}")`.
469        // A future normalization of the qualified-ref shape (case-
470        // fold, unicode collation, IDN) lands at ONE
471        // `qualified_process_ref` site and every downstream diagnostic
472        // body picks it up mechanically; this pin binds THIS composer
473        // to that substrate so a regression that inlined the join
474        // (drifting the primitive off the substrate axis this commit
475        // opens) surfaces HERE rather than as silent qualified-ref
476        // drift between the two consumer sites and every other
477        // qualified-ref consumer across the workspace.
478        //
479        // Sibling to [`crate::configmap::tests::
480        // error_ctx_routes_ns_name_join_through_qualified_process_ref_substrate`]
481        // on the peer ConfigMap axis of the same axis-family — both
482        // per-Kind composers share the SAME routing discipline through
483        // the SAME `qualified_process_ref` substrate.
484        for (ns, name) in [
485            ("default", "api"),
486            ("tatara-system", "reconciler-canary"),
487            ("demo-ns", "process-with-hyphen"),
488            ("ns-1", "process.dotted.name"),
489        ] {
490            let via_composer = error_ctx("fetch", ns, name);
491            let via_qualified = format!("fetch Process {}", crate::qualified_process_ref(ns, name));
492            assert_eq!(
493                via_composer, via_qualified,
494                "error_ctx must route the (ns, name) join through qualified_process_ref for ns={ns:?} name={name:?}",
495            );
496        }
497    }
498
499    #[test]
500    fn error_ctx_is_symbolic_over_the_verb_slot() {
501        // Substitution pin: the `verb` slot is threaded verbatim into
502        // the produced slug — no case-fold, no allow-list narrowing to
503        // the two shipped verbs (`"fetch"`, `"get"`), no verb-family
504        // canonicalization (`"GET"` promoted to `"get"`). A regression
505        // that narrowed the accepted verb set to the two current
506        // callsites' literals (a hardcoded `match verb { "fetch" |
507        // "get" => …, _ => … }` closed set that would silently reject
508        // future consumers) surfaces here.
509        //
510        // Future third + fourth consumers (a receipt-GC controller
511        // walking Processes by owner-ref for a reap decision → verb
512        // `"reap"`; a cross-namespace routing walker reading Processes
513        // to derive Ingress aliases → verb `"resolve"`) inherit the
514        // primitive at their own callsites and pass their own verbs
515        // verbatim without the composer widening.
516        for verb in [
517            "fetch", "get", "reap", "resolve", "watch", "patch", "delete",
518        ] {
519            let got = error_ctx(verb, "default", "api");
520            let expected = format!("{verb} Process default/api");
521            assert_eq!(got, expected, "verb-slot substitution must be verbatim");
522        }
523    }
524
525    #[test]
526    fn error_ctx_composes_with_kube_ctx_with_to_boundary_pre_lift_body_verbatim() {
527        // End-to-end parity witness on the reconciler-boundary
528        // consumer's tail — the (composer + `kube_ctx_with`) pair
529        // produces the SAME diagnostic body the pre-lift
530        // `.kube_ctx_with(format!("fetch process {ns}/{name}"))?`
531        // chain produced, MODULO the intentional TitleCase-kind
532        // drift-close documented on the composer's doc. The composer
533        // OWNS the head; `kube_ctx_with` OWNS the `": {e}"` tail;
534        // concatenation matches the post-lift shape byte-for-byte.
535        use crate::kube_error::KubeResultExt;
536        use kube::core::ErrorResponse;
537
538        let e = kube::Error::Api(ErrorResponse {
539            status: "Failure".into(),
540            message: "test failure".into(),
541            reason: "Test".into(),
542            code: 500,
543        });
544        let post_lift_expected = format!("fetch Process default/api: {e}");
545
546        let via_pair: anyhow::Result<()> =
547            Err::<(), _>(e).kube_ctx_with(error_ctx("fetch", "default", "api"));
548        let via_pair_display = via_pair.unwrap_err().to_string();
549
550        assert_eq!(
551            via_pair_display, post_lift_expected,
552            "the (error_ctx head + kube_ctx_with tail) pair must produce the \
553             byte-identical post-lift `\"<verb> Process {{ns}}/{{name}}: {{e}}\"` diagnostic",
554        );
555    }
556
557    #[test]
558    fn error_ctx_composes_with_anyhow_with_context_to_export_worker_pre_lift_head_verbatim() {
559        // End-to-end parity witness on the export-worker consumer's
560        // tail — the (composer + `anyhow::Context::with_context`)
561        // closure pair produces the SAME diagnostic HEAD the
562        // export-worker's post-lift `.with_context(|| process_api::
563        // error_ctx("get", ns, name))?` chain produces. The composer
564        // returns an owned `String` from the closure only when the
565        // Result is `Err` (matching `with_context`'s lazy semantics),
566        // so on the Ok arm no `qualified_process_ref` allocation
567        // fires.
568        //
569        // `anyhow::Context::with_context` CHAINS the context onto the
570        // source error rather than flattening (unlike the sibling
571        // `kube_ctx_with` on the reconciler-boundary consumer, which
572        // uses `anyhow::anyhow!("{ctx}: {e}")` to flatten): the top-
573        // level `Error::to_string()` returns the head only, and the
574        // source lives one level deeper via `.source()` / the
575        // `err.chain()` iterator. This matches pre-lift semantics —
576        // the export-worker was already using `.with_context(||
577        // format!("get process {ns}/{name}"))` with the same chained-
578        // context posture; the lift preserves it. This pin binds
579        // (a) the head equals the composer's output verbatim, and
580        // (b) the source chain contains the original `kube::Error`
581        // — so a regression that drifted the head OR that dropped
582        // the source chain via a flatten wrap would fail here.
583        //
584        // Peer to the kube-tail pin above — both tail paths compose
585        // with this ONE composer; the flatten-vs-chain choice lives
586        // at the consumer's tail, not at the substrate head.
587        use anyhow::Context;
588        use kube::core::ErrorResponse;
589
590        let e = kube::Error::Api(ErrorResponse {
591            status: "Failure".into(),
592            message: "test failure".into(),
593            reason: "Test".into(),
594            code: 404,
595        });
596        let expected_head = "get Process demo-ns/demo";
597
598        let via_pair: anyhow::Result<()> =
599            Err::<(), _>(e).with_context(|| error_ctx("get", "demo-ns", "demo"));
600        let via_pair_err = via_pair.unwrap_err();
601
602        // (a) the top-level Display matches the composer's head
603        //     verbatim — the head is the substrate composer's owned
604        //     output and NOT drifted per-tail.
605        assert_eq!(
606            via_pair_err.to_string(),
607            expected_head,
608            "the (error_ctx head + anyhow with_context tail) pair must expose the \
609             substrate composer's head as the top-level Display",
610        );
611
612        // (b) the source chain preserves the original `kube::Error`
613        //     — `with_context` chains rather than flattens, matching
614        //     the pre-lift export-worker consumer semantics. A
615        //     regression that dropped the source (a
616        //     `map_err(|_| anyhow!("..."))` synthesis losing the
617        //     kube-error root) would fail here.
618        let source_chain: Vec<String> = via_pair_err
619            .chain()
620            .skip(1) // skip the head we just pinned
621            .map(|src| src.to_string())
622            .collect();
623        assert!(
624            !source_chain.is_empty(),
625            "with_context tail must preserve the underlying kube::Error in the source chain",
626        );
627        assert!(
628            source_chain[0].contains("test failure"),
629            "the chained source must carry the underlying kube::Error's Display: got {source_chain:?}",
630        );
631    }
632
633    #[test]
634    fn error_ctx_routes_kind_slot_through_process_kind_owner() {
635        // Routing pin — the fixed `Kind = "Process"` slot at this
636        // per-Kind peer's `qualified_error_ctx` call rides through
637        // the workspace-wide wire-form identity owner
638        // [`crate::PROCESS_KIND`] rather than a bare inline
639        // `"Process"` literal. Pre-lift the composer hand-authored
640        // the Kind slot as a bare literal at the
641        // `qualified_error_ctx` boundary; post-lift the slot binds
642        // to the ONE workspace-wide `pub const` every SSA-time
643        // re-injection helper +
644        // [`crate::PROCESS_WIRE_IDENTITY`] projection already routes
645        // through — so a future rename of the CRD kind spelling
646        // reaches this diagnostic body mechanically alongside every
647        // downstream Process-scoped emit / fetch site on the same
648        // axis.
649        //
650        // A regression that inlined the `"Process"` literal back
651        // at the `qualified_error_ctx` call (drifting the primitive
652        // off the PROCESS_KIND axis owner + reopening the case-fold
653        // /typo-drift surface a hand-authored `process` / `proc`
654        // spelling would fall into silently, exactly the drift the
655        // most recent commit `+ close workspace-wide singular-
656        // Process kind-casing drift` closed) surfaces HERE rather
657        // than as silent per-Kind wire-form skew where the error-
658        // ctx head disagrees with the sibling
659        // `PROCESS_WIRE_IDENTITY.kind` spelling.
660        for (verb, ns, name) in [
661            ("fetch", "default", "api"),
662            ("get", "tatara-system", "reconciler-canary"),
663            ("patch", "demo-ns", "target"),
664            ("delete", "ns-1", "resource.dotted.name"),
665        ] {
666            let via_composer = error_ctx(verb, ns, name);
667            let via_typed_owner = crate::qualified_error_ctx(verb, crate::PROCESS_KIND, ns, name);
668            assert_eq!(
669                via_composer, via_typed_owner,
670                "error_ctx must route the Kind slot through \
671                 crate::PROCESS_KIND for ({verb:?}, {ns:?}, {name:?})",
672            );
673        }
674    }
675
676    #[test]
677    fn error_ctx_kind_slot_matches_process_wire_identity_kind_projection() {
678        // Cross-substrate coherence pin — the Kind slot the
679        // composer stamps into every diagnostic body MUST agree
680        // byte-for-byte with the same axis-family's
681        // [`crate::PROCESS_WIRE_IDENTITY.kind`] projection. Post-
682        // lift both routes read `crate::PROCESS_KIND`; a
683        // regression that drifted this composer off the const
684        // (a re-inlined `"Process"` literal, a hand-authored
685        // `to_string()` copy) would leave the error-ctx head
686        // disagreeing with the resource `kind:` slot every SSA-
687        // apply / owner-reference emit stamps — the exact silent-
688        // skew corner the four-arm K8s wire-form identity axis-
689        // family exists to close.
690        let via_composer = error_ctx("fetch", "default", "api");
691        let expected_kind_slot = crate::PROCESS_WIRE_IDENTITY.kind;
692        assert!(
693            via_composer.contains(expected_kind_slot),
694            "error_ctx output {via_composer:?} must contain \
695             PROCESS_WIRE_IDENTITY.kind ({expected_kind_slot:?}) as its Kind slot",
696        );
697        // Reflexive: passing PROCESS_KIND into qualified_error_ctx
698        // yields byte-identical output (the composer routes both
699        // sides through the same const).
700        assert_eq!(
701            via_composer,
702            crate::qualified_error_ctx("fetch", crate::PROCESS_KIND, "default", "api"),
703        );
704    }
705
706    #[test]
707    fn error_ctx_matches_sibling_configmap_error_ctx_shape_modulo_kind_slot() {
708        // Cross-substrate coherence pin — this composer and its
709        // sibling [`crate::configmap::error_ctx`] on the peer K8s-
710        // Kind axis produce byte-identical diagnostic heads MODULO
711        // the fixed resource-kind literal (`"Process"` here vs
712        // `"ConfigMap"` there). A regression that drifted either
713        // composer's shape (a swapped verb slot position, an
714        // inserted delimiter, a lost qualified-ref routing) breaks
715        // the family invariant HERE rather than as silent per-Kind
716        // skew where an operator grepping across the fleet on
717        // `"<verb> <Kind> <ns>/<name>"` hits one composer's output
718        // but not the other's.
719        for (verb, ns, name) in [
720            ("patch", "default", "target"),
721            ("create", "probe-ns", "receipt-cm"),
722            ("get", "demo-ns", "resource"),
723        ] {
724            let via_process = error_ctx(verb, ns, name);
725            let via_configmap = crate::configmap::error_ctx(verb, ns, name);
726            // Replace the `Process` head with `ConfigMap` and vice
727            // versa — the two composers agree on every non-kind byte.
728            assert_eq!(
729                via_process.replace("Process", "ConfigMap"),
730                via_configmap,
731                "process_api::error_ctx and configmap::error_ctx must share the \
732                 SAME diagnostic head shape modulo the fixed resource-kind literal",
733            );
734        }
735    }
736
737    #[test]
738    fn namespaced_accepts_borrowed_and_owned_ns_shapes_at_the_type_level() {
739        // The three shipped callsites split across two shapes:
740        // boundary `evaluate_process_phase` passes a `&str` slice
741        // pulled from `ssapply::resolve_target_namespace(...)`;
742        // boundary `check_depends_on` passes the same shape per
743        // dep; export-worker `read_artifact` passes an owned
744        // `String` field via deref coercion. Both shapes must
745        // route through the same `&str` parameter without
746        // widening — pin the two callsite forms at the type level
747        // so a regression that narrowed the parameter to `String`
748        // (forcing every caller to allocate) or widened it to
749        // `impl AsRef<str>` (making the callsite ambiguous for the
750        // borrowed-slice sites) fails to coerce here at compile
751        // time. Peer to `configmap::tests::
752        // namespaced_signature_binds_owned_client_and_borrowed_ns_returning_typed_configmap_api`
753        // on the sibling K8s-built-in axis. Wire-shape witnesses
754        // (URL routing, cluster-scope vs ns-scope contrast) live
755        // one crate up at
756        // `tatara_reconciler::context::tests::process_api_*` on
757        // the reconciler-side forwarder — which delegates through
758        // THIS primitive post-lift, so those runtime pins now bind
759        // this substrate owner too.
760        let _borrowed_witness: fn(Client, &str) -> Api<Process> = namespaced;
761        // The owned-`String` deref coercion is not a distinct
762        // function-pointer type — it's the same `&str`-parametered
763        // function-item after auto-deref at the callsite. Source-
764        // level pin: a caller with `owned: String` shape can name
765        // the primitive with `&owned` and hit the same `&str`
766        // slot. A regression that changed the parameter type
767        // would fail every callsite in the reconciler + export-
768        // worker at compile time.
769    }
770}