tatara_process/status.rs
1//! `ProcessStatus` sub-structures — conditions, checked boundaries, Flux refs.
2
3use chrono::{DateTime, Utc};
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8use crate::boundary::Condition;
9use crate::condition_type::ProcessConditionType;
10use crate::crd::Process;
11use crate::json_object::ValueGetExt;
12use crate::k8s_condition::K8sConditionStatus;
13
14/// Standard K8s Condition (shape of `metav1.Condition`).
15#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
16#[serde(rename_all = "camelCase")]
17pub struct ProcessCondition {
18 #[serde(rename = "type")]
19 pub type_: String,
20 pub status: String,
21 pub last_transition_time: DateTime<Utc>,
22 #[serde(default, skip_serializing_if = "Option::is_none")]
23 pub reason: Option<String>,
24 #[serde(default, skip_serializing_if = "Option::is_none")]
25 pub message: Option<String>,
26}
27
28impl ProcessCondition {
29 /// Compose a [`ProcessCondition`] stamped at "observed now" from a
30 /// typed [`ProcessConditionType`] + [`K8sConditionStatus`] pair,
31 /// a reason slug, and an optional message.
32 ///
33 /// Owns the 5-slot wire-shape backbone
34 ///
35 /// ```text
36 /// Self {
37 /// type_: <ConditionType>.as_wire_str().into(),
38 /// status: <StatusEnum>.as_wire_str().into(),
39 /// last_transition_time: Utc::now(),
40 /// reason: Some(<reason>.into()),
41 /// message: <opt>,
42 /// }
43 /// ```
44 ///
45 /// that every sibling constructor on this impl block ([`Self::ready`],
46 /// [`Self::not_ready`], [`Self::attested`]) hand-authored pre-lift.
47 ///
48 /// Pre-lift the SAME 5-slot struct-literal skeleton recurred at
49 /// THREE sibling constructor bodies past the ★★ PRIME-DIRECTIVE
50 /// ≥ 2 duplication threshold, each restating (a) the typed-enum →
51 /// wire-string projection at the `type_` slot, (b) the peer typed-
52 /// enum → wire-string projection at the `status` slot, (c) the
53 /// `Utc::now()` clock stamp at the `last_transition_time` slot,
54 /// (d) the `Some(<reason>.into())` wrap at the `reason` slot. Post-
55 /// lift the three siblings name their two typed variants + reason +
56 /// message opt ONCE and route through this ONE composer; the shared
57 /// backbone lives at ONE substrate site so a future normalization
58 /// of the K8s Condition wire shape (adding `observed_generation:
59 /// Option<i64>` to match `metav1.Condition` v2, injecting a
60 /// `Clock` for deterministic tests replacing the direct
61 /// `Utc::now()` call, adding a `severity:` slot for K8s-Condition-
62 /// v3 style diagnostics) lands at ONE composer body and all three
63 /// sibling constructors — plus every future `ProcessCondition::*`
64 /// variant on this impl block — inherit the upgrade mechanically.
65 ///
66 /// The typed enum inputs bind the wire-form projection axis
67 /// structurally: a regression that swapped `K8sConditionStatus::
68 /// True` for `False` at ONE sibling (a mechanical copy-paste error
69 /// the pre-lift hand-authored `status: "True".into()` /
70 /// `status: "False".into()` string-literal restatements would not
71 /// have caught at compile time) is caught by the enum's closed
72 /// set, and the wire-string projection itself rides through the
73 /// SAME `as_wire_str()` projection every reader/writer pair in the
74 /// workspace already routes through — so a future spelling change
75 /// at ONE of those enums propagates mechanically to all three
76 /// sibling constructors without a per-site edit.
77 ///
78 /// Sibling to the pre-existing `K8sConditionStatus` /
79 /// `ProcessConditionType` closed-set variants on the (typed
80 /// wire-form × K8s-Condition-slot) axis: those closed sets own
81 /// the type-variant enumeration + wire-form projection; this
82 /// composer owns the K8s-Condition struct-literal composition
83 /// that binds two typed variants onto the wire.
84 ///
85 /// Theory anchor: THEORY.md §VI.1 (generation over composition —
86 /// the 5-slot struct-literal backbone recurred at 3 hand-authored
87 /// sibling constructor sites past the ★★ PRIME-DIRECTIVE ≥ 2
88 /// duplication trigger and is lifted onto the ONE substrate
89 /// composer here). THEORY.md §II.1 invariant 5 (composition
90 /// preserves proofs — the pin block below binds the composer at
91 /// fail-before-pass-after granularity, so a regression that drifted
92 /// any slot's default — `Utc::now()` swapped for a fixed-anchor
93 /// stamp, `reason: Some(<reason>.into())` narrowed to a bare
94 /// `<reason>.into()` opting out of the K8s-Condition-mandated
95 /// `Option`-wrap, the typed-enum → wire-string projection at the
96 /// `type_` or `status` slot bypassed by an inline literal that
97 /// silently drifts off the closed-set owner — surfaces at
98 /// `status::tests::new_at_now_*` rather than as silent operator-
99 /// facing skew across the three sibling constructor callsites).
100 ///
101 /// # Delegation to [`Self::new_at`]
102 ///
103 /// The 5-slot struct-literal body lives at the clock-injectable
104 /// substrate peer [`Self::new_at`]; this composer supplies
105 /// `Utc::now()` as the `at` slot and delegates. The wall-clock
106 /// projection lives at ONE substrate site, so a future clock swap
107 /// (a monotonic-clock cross-check, a per-fleet skew tolerance,
108 /// promotion to an injectable `Clock` trait) lands at the SINGLE
109 /// `Utc::now()` call on this delegation body rather than at every
110 /// hand-authored `last_transition_time: Utc::now()` stamp — no
111 /// per-sibling edit at [`Self::ready`], [`Self::not_ready`], or
112 /// [`Self::attested`], and no edit at any future
113 /// `ProcessCondition::*` sibling that inherits the composer.
114 fn new_at_now(
115 type_: ProcessConditionType,
116 status: K8sConditionStatus,
117 reason: impl Into<String>,
118 message: Option<String>,
119 ) -> Self {
120 Self::new_at(type_, status, reason, message, Utc::now())
121 }
122
123 /// Clock-injectable substrate peer of [`Self::new_at_now`] — the
124 /// ONE substrate owner of the 5-slot wire-shape backbone
125 ///
126 /// ```text
127 /// Self {
128 /// type_: <ConditionType>.as_wire_str().into(),
129 /// status: <StatusEnum>.as_wire_str().into(),
130 /// last_transition_time: <at>,
131 /// reason: Some(<reason>.into()),
132 /// message: <opt>,
133 /// }
134 /// ```
135 ///
136 /// with the wall-clock stamp lifted out onto an explicit `at:
137 /// DateTime<Utc>` slot so a caller (typically a deterministic-
138 /// clock test on this module or a future controller-side callsite
139 /// that already carries a wall-clock anchor threaded through its
140 /// own decide-tick) supplies the anchor rather than reading it
141 /// from `Utc::now()` implicitly.
142 ///
143 /// # Why it exists
144 ///
145 /// [`Self::new_at_now`]'s pre-lift body read `Utc::now()` inline
146 /// at the composer body — a shape the module's own tests already
147 /// bracket with `let before = Utc::now(); ... let after = Utc::
148 /// now(); assert!(<c>.last_transition_time >= before && <c>.
149 /// last_transition_time <= after)` to pin the wall-clock projection
150 /// non-deterministically. That non-determinism is a proof shape
151 /// every peer clock-anchored composer family in this crate has
152 /// already collapsed onto a `_now` + `_at` peer pair — see
153 /// [`crate::pool::PoolStatus::observed`] (`at`-slot substrate) +
154 /// [`crate::pool::PoolStatus::observed_now`] (wall-clock-anchored
155 /// peer), [`crate::allocation::AllocationStatus::transition`] +
156 /// [`crate::allocation::AllocationStatus::transition_now`],
157 /// [`crate::lifetime_clock::evaluate`] +
158 /// [`crate::lifetime_clock::evaluate_now`] — all binding the
159 /// `Utc::now()` read at exactly ONE substrate site (the `_now`
160 /// peer's body) with the `_at` peer owning the pure struct-literal
161 /// composition. The docstring on [`Self::new_at_now`] itself
162 /// called out the trajectory (`"a promotion of the direct
163 /// Utc::now() call to an injectable Clock for deterministic tests"`)
164 /// as its next compounding step; this method opens exactly that
165 /// step for the `ProcessCondition` composer family, closing the
166 /// pattern uniformity across the four peer axes.
167 ///
168 /// # Invariants
169 ///
170 /// - **Same slots as [`Self::new_at_now`]:** the four non-clock
171 /// slots (`type_`, `status`, `reason`, `message`) ride through
172 /// the SAME projections — typed enum → wire string at `type_` +
173 /// `status`, `Some(<reason>.into())` wrap at `reason`, verbatim
174 /// `Option<String>` pass-through at `message`. The peer pair
175 /// differs only at the `last_transition_time` slot's projection.
176 /// - **`at` slot verbatim:** the caller-supplied `DateTime<Utc>`
177 /// binds `last_transition_time` bytewise, with no accidental
178 /// `Utc::now()` clamp / `Duration::seconds(0)` rounding / offset
179 /// normalization at the composer body. Pinned by
180 /// [`tests::new_at_binds_at_slot_to_supplied_datetime_verbatim`].
181 /// - **Delegation invariant:** [`Self::new_at_now`] composes
182 /// through this primitive with `Utc::now()` at the `at` slot —
183 /// a regression that re-inlined the 5-slot literal at
184 /// [`Self::new_at_now`] (bypassing this substrate) surfaces at
185 /// [`tests::new_at_now_routes_through_new_at_with_utc_now_stamp_bytewise`]
186 /// rather than as silent skew between the wall-clock-anchored
187 /// peer and any future `new_at` consumer that pinned a fixed
188 /// anchor.
189 ///
190 /// # `#[must_use]`
191 ///
192 /// Every consumer feeds the returned [`ProcessCondition`] into a
193 /// `ProcessStatus.conditions` slot or a peer status-patch call.
194 /// Dropping the return means the condition composed for no
195 /// observable reason — the attribute surfaces that as a warning
196 /// at every call site.
197 ///
198 /// Theory anchor: THEORY.md §VI.1 (generation over composition —
199 /// the 5-slot struct-literal backbone that recurred at three
200 /// sibling constructors' pre-lift bodies is now the SINGLE
201 /// substrate site every clock-injected AND every wall-clock-
202 /// anchored composer routes through). THEORY.md §II.1 invariant 5
203 /// (composition preserves proofs — the pair `Self::new_at` +
204 /// `Self::new_at_now` mirrors the four peer clock-anchored
205 /// composer families across the crate, so the workspace's
206 /// `<K8sWireResource>Condition` / `<CRD>Status` composer family
207 /// stays uniform on the (clock-injectable, wall-clock-anchored)
208 /// peer axis; a future primitive on the same axis inherits the
209 /// convention structurally).
210 #[must_use]
211 fn new_at(
212 type_: ProcessConditionType,
213 status: K8sConditionStatus,
214 reason: impl Into<String>,
215 message: Option<String>,
216 at: DateTime<Utc>,
217 ) -> Self {
218 Self {
219 type_: type_.as_wire_str().into(),
220 status: status.as_wire_str().into(),
221 last_transition_time: at,
222 reason: Some(reason.into()),
223 message,
224 }
225 }
226
227 pub fn ready(reason: impl Into<String>, message: Option<String>) -> Self {
228 Self::new_at_now(
229 ProcessConditionType::Ready,
230 K8sConditionStatus::True,
231 reason,
232 message,
233 )
234 }
235
236 pub fn not_ready(reason: impl Into<String>, message: impl Into<String>) -> Self {
237 Self::new_at_now(
238 ProcessConditionType::Ready,
239 K8sConditionStatus::False,
240 reason,
241 Some(message.into()),
242 )
243 }
244
245 pub fn attested(root: &str) -> Self {
246 Self::new_at_now(
247 ProcessConditionType::Attested,
248 K8sConditionStatus::True,
249 "AttestationWritten",
250 Some(format!("composed_root={root}")),
251 )
252 }
253}
254
255/// Reference to a FluxCD resource emitted as part of this Process.
256#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
257#[serde(rename_all = "camelCase")]
258pub struct FluxResourceRef {
259 pub api_version: String,
260 pub kind: String,
261 pub name: String,
262 pub namespace: String,
263 #[serde(default)]
264 pub ready: bool,
265 #[serde(default, skip_serializing_if = "Option::is_none")]
266 pub message: Option<String>,
267 #[serde(default, skip_serializing_if = "Option::is_none")]
268 pub last_check: Option<DateTime<Utc>>,
269}
270
271impl FluxResourceRef {
272 /// Pure typed projection of the four fetch coordinates
273 /// `(namespace, api_version, kind, name)` every consumer that
274 /// dispatches this persisted reference through kube-rs's dynamic-
275 /// object surface splats by hand pre-lift. The 4-tuple binds the
276 /// slot order at ONE typed accessor so a copy-paste at any downstream
277 /// consumer cannot swap two adjacent `&str` slots in the fetch call.
278 ///
279 /// Peer projection to
280 /// [`crate::k8s_wire_identity::K8sWireIdentity`] on the static-
281 /// identity axis: [`K8sWireIdentity`] carries a
282 /// `(&'static str, &'static str)` closed-set variant's pair for
283 /// emit-time (RENDER phase) composition; this method carries the
284 /// full `(ns, apiVersion, kind, name)` 4-slot borrow for fetch-time
285 /// (VERIFY / ATTEST-heartbeat) composition where the ref's payload
286 /// comes back off the persisted `ProcessStatus.flux_resources`
287 /// slice with owned `String`s rather than static literals. The two
288 /// primitives partition the fetch axis by whether the caller starts
289 /// from a closed-set variant (emit-time) or a persisted status
290 /// slice (fetch-time).
291 ///
292 /// Pre-lift the 5-slot `ssapply::fetch(client, &r.namespace,
293 /// &r.api_version, &r.kind, &r.name)` splat was hand-authored at
294 /// TWO sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication threshold
295 /// in `tatara-reconciler::phase_machine`:
296 /// * `handle_running` — the VERIFY-phase per-ref readiness probe
297 /// that populates the updated `FluxResourceRef` slice with
298 /// `ready` + `message` + `last_check`.
299 /// * `handle_attested` — the ATTEST-heartbeat drift detector that
300 /// short-circuits on the first non-Ready ref.
301 ///
302 /// Both sites splatted the SAME four `&r.X` field borrows in the
303 /// SAME order into raw `ssapply::fetch`. A copy-paste that swapped
304 /// two adjacent `&str` slots (`&r.api_version` and `&r.kind` are
305 /// both strings that look interchangeable to a mechanical
306 /// substitution) would silently 404 at wire time and diagnose as a
307 /// broken CRD rather than as slot skew at the callsite. Post-lift
308 /// each site names the ref ONCE and unpacks it through this ONE
309 /// projection; the slot order binds structurally at the tuple
310 /// return so a caller cannot desync one axis.
311 ///
312 /// A future addition (a case-fold normalization on the group, a
313 /// virtual-cluster prefix rewrite for multi-tenancy, a
314 /// `generateName` fallback on the name slot, a cluster-cache
315 /// short-circuit inserted between the projection and the fetch
316 /// call) lands at this ONE method and every downstream fetch
317 /// consumer inherits the upgrade mechanically — no per-site edit
318 /// at `handle_running` / `handle_attested` / any future kenshi-
319 /// runner / mirror-audit / drift-probe consumer that grows a third
320 /// consumer.
321 ///
322 /// Return-order pin lives at
323 /// [`tests::flux_resource_ref_fetch_coords_binds_slots_by_position`]
324 /// so a regression that swapped `namespace` and `api_version`
325 /// (both `String`, same type) inside the tuple constructor fails-
326 /// loudly here rather than as a silent wire-time 404 at every
327 /// downstream fetch consumer.
328 ///
329 /// Theory grounding: THEORY.md §II.1 invariant 5 (composition
330 /// preserves proofs — the 4-tuple slot order binds at ONE typed
331 /// projection so a regression across the two fields of the same
332 /// `String` type fails at the projection's positional pin rather
333 /// than at every downstream fetch consumer). THEORY.md §VI.1
334 /// (generation over composition — the 5-slot splat recurred at
335 /// two hand-authored sites past the ≥ 2 duplication trigger, and
336 /// is lifted to ONE typed borrow-projection here).
337 pub fn fetch_coords(&self) -> (&str, &str, &str, &str) {
338 (&self.namespace, &self.api_version, &self.kind, &self.name)
339 }
340
341 /// Compose a `FluxResourceRef` stamped at "observed now" — the
342 /// `last_check` slot is set to `Some(Utc::now())` at ONE substrate
343 /// owner, and the four coordinate slots + `ready` + `message`
344 /// are bound positionally so a slot-swap regression surfaces at
345 /// the constructor's positional pin rather than as silent drift
346 /// at every downstream `ProcessStatus.flux_resources` writer.
347 ///
348 /// Pre-lift the 7-slot `FluxResourceRef { …, last_check:
349 /// Some(chrono::Utc::now()) }` struct-literal was hand-authored
350 /// at TWO sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication
351 /// threshold in `tatara-reconciler::phase_machine`:
352 /// * `handle_running` — the VERIFY-phase per-ref rebuild that
353 /// restamps each polled ref with fresh `ready` + `message` +
354 /// `last_check`.
355 /// * `flux_ref_from_json` — the post-SSA initial-state seeder
356 /// that stamps a freshly-applied ref as `ready = false`,
357 /// `message = Some("applied; awaiting reconciliation")`,
358 /// `last_check = Some(Utc::now())`.
359 ///
360 /// Both sites restated the SAME seven field bindings in the
361 /// SAME order, and both restated the SAME `Some(chrono::Utc::
362 /// now())` stamp. A copy-paste that swapped two adjacent
363 /// `String` slots (`api_version` and `kind`, `kind` and `name`,
364 /// or `name` and `namespace` are all mechanically
365 /// indistinguishable at the type level) would silently persist
366 /// a slot-inverted ref that the downstream Flux fetch consumer
367 /// (via [`Self::fetch_coords`]) would then 404 on. Post-lift
368 /// both sites name the six inputs ONCE and route through this
369 /// ONE composer; the seventh slot (`last_check`) is stamped at
370 /// the composer's body so a future injection point (a fake
371 /// clock for testing, a monotonic-clock cross-check, a per-
372 /// fleet skew tolerance) lands at ONE substrate site rather
373 /// than at every hand-authored `Some(chrono::Utc::now())` stamp.
374 ///
375 /// Return-order pin lives at
376 /// [`tests::flux_resource_ref_observed_binds_slots_by_position`]
377 /// so a regression that swapped `api_version` and `kind` (both
378 /// `String`, same type) inside the constructor's argument list
379 /// fails-loudly here rather than as a silent wire-time 404 at
380 /// every downstream fetch consumer.
381 ///
382 /// Theory grounding: THEORY.md §II.1 invariant 5 (composition
383 /// preserves proofs — the 6-slot positional binding + the
384 /// `last_check` stamp compose at ONE typed owner, so a
385 /// regression across the four `String` coordinate slots fails
386 /// at the composer's positional pin rather than at every
387 /// downstream Flux status writer). THEORY.md §VI.1 (generation
388 /// over composition — the 7-slot struct-literal recurred at two
389 /// hand-authored sites past the ≥ 2 duplication trigger, and is
390 /// lifted to ONE typed composer here).
391 pub fn observed(
392 api_version: String,
393 kind: String,
394 name: String,
395 namespace: String,
396 ready: bool,
397 message: Option<String>,
398 ) -> Self {
399 Self {
400 api_version,
401 kind,
402 name,
403 namespace,
404 ready,
405 message,
406 last_check: Some(Utc::now()),
407 }
408 }
409
410 /// Compose a `FluxResourceRef` in the pre-observation shape — the
411 /// 4-slot coordinate binding with the three status slots defaulted
412 /// (`ready: false`, `message: None`, `last_check: None`). The
413 /// deterministic-fixture peer of [`Self::observed`] on the same
414 /// `→ FluxResourceRef` composer axis: `observed` reads the wall
415 /// clock and takes 6 args (a live post-fetch stamp), `pending`
416 /// reads no clock and takes 4 args (a pre-observation fixture
417 /// seed, and the natural base for `..base.clone()` spread updates
418 /// that vary a single slot for a per-corner test sweep).
419 ///
420 /// Pre-lift the SAME 7-slot `FluxResourceRef { api_version, kind,
421 /// name, namespace, ready: false, message: None, last_check: None
422 /// }` struct-literal was hand-authored at THREE workspace-wide
423 /// fixture sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication
424 /// threshold:
425 ///
426 /// * [`crate::crd`]
427 /// `crd::observed_flux_resources_tests::sample_flux_ref(name)`
428 /// — the shared `Kustomization`/`flux-system` fixture the
429 /// `Process::observed_flux_resources` pin family destructures
430 /// for its `flux_resources`-populated corners.
431 /// * `tatara-reconciler::ssapply::tests::sample_flux_ref_for_diag`
432 /// — the `HelmRelease`/`flux-system` fixture the
433 /// `flux_ref_fetch_error_context` diagnostic-wording pin
434 /// family destructures for its (kind, name) slot-coverage
435 /// sweep.
436 /// * `tatara-reconciler::ssapply::tests::
437 /// flux_ref_fetch_error_context_matches_pre_lift_hand_authored_wording`
438 /// — the inline 7-slot literal inside the cross-substrate
439 /// coherence pin's per-case sweep over three distinct
440 /// `(api_version, kind, name, namespace)` tuples.
441 ///
442 /// All THREE sites restated the SAME seven field bindings in the
443 /// SAME order and the SAME three defaulted status slots (`ready:
444 /// false, message: None, last_check: None`), differing only in
445 /// the four coordinate `String` values. Post-lift each callsite
446 /// reads `FluxResourceRef::pending(<api_version>, <kind>, <name>,
447 /// <namespace>)` and the four-slot bind + three-slot default
448 /// sinks live at ONE substrate owner.
449 ///
450 /// The `impl Into<String>` signature accepts BOTH `&'static str`
451 /// (the fixture-helper sites that spell coordinate literals
452 /// inline) AND owned `String` (a future callsite handing off a
453 /// dynamically-derived coordinate) without widening. Matches the
454 /// discipline of the sibling substrate composers
455 /// [`crate::pool::PoolMember::unallocated`] +
456 /// [`crate::allocation::AllocationRef::new`] on the identity-slot
457 /// axis.
458 ///
459 /// A future normalization (a case-fold on the group, a
460 /// virtual-cluster prefix rewrite for multi-tenancy, a stricter
461 /// kind gate, a `generateName` fallback on the name slot, a
462 /// canonical rename of one of the three defaulted status slots
463 /// to a typed `PreObservation` marker) lands at THIS ONE
464 /// substrate primitive and every downstream fixture / helper
465 /// inherits the upgrade mechanically — no per-site edit at any
466 /// of the THREE listed callers or at future consumers (a
467 /// stable-name claim-arbiter's pending-ref seed, a kenshi-runner
468 /// pre-observation fixture, a mirror-audit drift-probe test
469 /// helper).
470 ///
471 /// Theory grounding: THEORY.md §II.1 invariant 5 (composition
472 /// preserves proofs — the 4-slot positional binding + the three
473 /// defaulted status slots compose at ONE typed owner, so a
474 /// regression across the four `String` coordinate slots fails at
475 /// the composer's positional pin rather than at every downstream
476 /// fixture consumer). THEORY.md §VI.1 (generation over
477 /// composition — the 7-slot struct-literal recurred at three
478 /// hand-authored fixture sites past the ≥ 2 duplication trigger,
479 /// and is lifted to ONE typed composer here).
480 #[must_use]
481 pub fn pending(
482 api_version: impl Into<String>,
483 kind: impl Into<String>,
484 name: impl Into<String>,
485 namespace: impl Into<String>,
486 ) -> Self {
487 Self {
488 api_version: api_version.into(),
489 kind: kind.into(),
490 name: name.into(),
491 namespace: namespace.into(),
492 ready: false,
493 message: None,
494 last_check: None,
495 }
496 }
497}
498
499/// Identifying coordinates of a rendered K8s resource — the
500/// `(apiVersion, kind, metadata.name, metadata.namespace)` 4-tuple
501/// every consumer that walks a rendered `serde_json::Value` resource
502/// unwraps by hand pre-lift.
503///
504/// The three K8s API-path segments (`apiVersion`, `kind`,
505/// `metadata.name`) are REQUIRED — a rendered resource missing any
506/// of them cannot be applied via kube-rs's dynamic API surface, so
507/// the extraction fails fast at the boundary rather than as a
508/// downstream `Api::patch` panic. `metadata.namespace` is
509/// intentionally kept as `Option<String>` because different consumers
510/// resolve the fallback differently: `apply_owned` uses the
511/// caller-supplied `namespace: &str` argument (the reconciler already
512/// resolved the target namespace upstream), while `flux_ref_from_json`
513/// records the K8s canonical `"default"` fallback into the persisted
514/// `FluxResourceRef.namespace` slot. The peer method
515/// [`Self::namespace_or_default`] applies the K8s canonical fallback
516/// (`Process::DEFAULT_NAMESPACE = "default"`) for consumers wanting
517/// the same shape [`FluxResourceRef.namespace`] carries.
518///
519/// Pre-lift the 3+1 slot extraction was hand-authored at TWO sites
520/// past the ★★ PRIME-DIRECTIVE ≥ 2 duplication threshold across
521/// `tatara-reconciler`:
522/// * `tatara-reconciler::phase_machine::flux_ref_from_json` — the
523/// post-SSA `FluxResourceRef` builder that persists into
524/// `ProcessStatus.flux_resources`; namespace half fallback-
525/// defaulted to `"default"`.
526/// * `tatara-reconciler::ssapply::apply_owned` — the SSA entry
527/// point that extracts (apiVersion, kind, name) for the
528/// [`kube::Api::patch`] call; namespace half discarded (the
529/// `namespace: &str` argument comes from the caller upstream).
530///
531/// Both callsites restated the same three
532/// `.get(K).and_then(|v| v.as_str()).ok_or_else(|| anyhow!(...))?
533/// .to_string()` incantations with subtly different error wording
534/// (`"resource missing X"` vs `"rendered resource missing X"`); post-
535/// lift both route through this ONE substrate owner with the
536/// canonical `"rendered resource missing X"` wording. A future
537/// addition (case-fold on the group, a rename of the namespace
538/// fallback, a stricter kind gate, a Unicode-safe collation step,
539/// support for `metadata.generateName` as a name fallback) lands at
540/// the primitive's body on the substrate, not at 2 independent
541/// hand-writes across 2 reconciler files.
542///
543/// Namespace fallback const is shared with
544/// [`Process::DEFAULT_NAMESPACE`] — a rename of the K8s canonical
545/// default namespace lands at that ONE workspace-wide const, not at
546/// per-primitive local literals that would drift silently.
547#[derive(Clone, Debug, PartialEq, Eq)]
548pub struct RenderedResourceCoords {
549 /// `apiVersion` — the group+version pair kube-rs uses to resolve
550 /// the `ApiResource` for the SSA call.
551 pub api_version: String,
552 /// `kind` — the resource kind (Kustomization, HelmRelease, …).
553 pub kind: String,
554 /// `metadata.name` — the API-path leaf segment.
555 pub name: String,
556 /// `metadata.namespace` — raw from the resource, `None` when the
557 /// slot is absent (a cluster-scoped resource, or a namespaced
558 /// resource whose namespace was left for the API server to
559 /// substitute). Consumers apply their own fallback:
560 /// [`Self::namespace_or_default`] applies the K8s canonical
561 /// `"default"` (matching what [`FluxResourceRef.namespace`]
562 /// records); other consumers substitute a caller-supplied string
563 /// (see `tatara-reconciler::ssapply::apply_owned`).
564 pub namespace: Option<String>,
565}
566
567impl RenderedResourceCoords {
568 /// Extract the 4-tuple from a rendered K8s resource JSON `Value`.
569 ///
570 /// Fails with a canonical `"rendered resource missing X"` message
571 /// when any of the three required slots (`apiVersion`, `kind`,
572 /// `metadata.name`) is absent or non-string; `metadata.namespace`
573 /// is optional and captured as `None` when absent.
574 ///
575 /// The error wording is pinned by
576 /// [`tests::rendered_resource_coords_error_wording_is_canonical`]
577 /// so a regression that reshaped the message surfaces at the test
578 /// surface rather than as silent drift between the two pre-lift
579 /// call sites (which used subtly different wording — `"resource
580 /// missing X"` in `apply_owned` vs `"rendered resource missing
581 /// X"` in `flux_ref_from_json`).
582 pub fn from_json(res: &Value) -> anyhow::Result<Self> {
583 // The three REQUIRED-slot extracts (`apiVersion`, `kind`,
584 // `metadata.name`) route through the ONE substrate primitive
585 // `Self::required_str` — the required-extract sibling of
586 // `crate::json_object::ValueGetExt::get_str` on the same
587 // rendered-resource axis. A future normalization (Unicode
588 // NFC-fold, whitespace trim, empty-string rejection) lands
589 // at the primitive body and every downstream consumer of the
590 // canonical `"rendered resource missing X"` wire form inherits
591 // it mechanically. The optional `metadata.namespace` slot
592 // routes directly through the ONE substrate primitive
593 // `crate::json_object::ValueGetExt::get_str` on the
594 // `Option<&Value>` receiver arm — the outer-optionality
595 // widening added alongside the `Value` + `Map<String, Value>`
596 // impls. Pre-lift this callsite hand-authored the outer-
597 // optionality closure `metadata.and_then(|m| m.get_str
598 // ("namespace"))` past the ★★ PRIME-DIRECTIVE ≥ 2 duplication
599 // threshold (this site + `Self::required_str`'s body); post-
600 // lift the closure disappears and the axis-family method-call
601 // surface stays identical to the `Value` / `Map` receiver
602 // callers.
603 let api_version = Self::required_str(Some(res), "apiVersion", "apiVersion")?;
604 let kind = Self::required_str(Some(res), "kind", "kind")?;
605 let metadata = res.get("metadata");
606 let name = Self::required_str(metadata, "name", "metadata.name")?;
607 let namespace = metadata.get_str("namespace").map(str::to_string);
608 Ok(Self {
609 api_version,
610 kind,
611 name,
612 namespace,
613 })
614 }
615
616 /// Diagnostic prefix stamped ahead of every required-slot label in
617 /// the canonical error wire form. Owned in ONE workspace-wide place
618 /// so a rename (a fleet-wide switch to `"resource is missing"` /
619 /// `"missing rendered-resource field"`) lands here and every
620 /// downstream `.to_string()`-consumer + operator-facing log grep
621 /// inherits the rename mechanically, not at 3 hand-authored
622 /// `anyhow!(…)` restatements.
623 pub const MISSING_MESSAGE_PREFIX: &'static str = "rendered resource missing";
624
625 /// Required-slot extract on a rendered-resource JSON `Value` — the
626 /// substrate owner of the paired `.get_str(<key>).ok_or_else(||
627 /// anyhow!("rendered resource missing <slot>"))?.to_string()`
628 /// four-link chain every REQUIRED slot on a rendered `Value`
629 /// walks pre-lift.
630 ///
631 /// The primitive accepts an `Option<&Value>` receiver so BOTH
632 /// shallow reads (top-level `apiVersion` / `kind` on the resource
633 /// root, callers thread `Some(res)`) AND one-level-nested reads
634 /// (`metadata.name` walking through `res.get("metadata")`,
635 /// callers thread the `Option<&Value>` handle the `.get()` step
636 /// returns) reach the same owner. The `key` slot is the wire-form
637 /// name the underlying [`ValueGetExt::get_str`] looks up on the
638 /// object; the `error_slot` slot is the diagnostic label stamped
639 /// into the error's `Display` output. The two are decoupled so
640 /// `metadata.name` can look up `"name"` on the `metadata` sub-
641 /// object while reporting the dotted `"metadata.name"` path an
642 /// operator bisecting a fault sees in the log.
643 ///
644 /// Ok arm returns `String` (owned) rather than the borrowed
645 /// `&str` [`ValueGetExt::get_str`] returns — every downstream
646 /// slot on the [`RenderedResourceCoords`] struct is an owned
647 /// `String`, so the primitive absorbs the `str::to_string`
648 /// coerce that pre-lift lived at three hand-authored callsites.
649 /// Err arm carries an `anyhow::Error` whose `Display` reads
650 /// exactly `"<Self::MISSING_MESSAGE_PREFIX> <error_slot>"` —
651 /// byte-identical to the pre-lift hand-authored `anyhow!(
652 /// "rendered resource missing {slot}")` wire form.
653 ///
654 /// ### Fires on all four absent-shape corners
655 ///
656 /// The primitive returns `Err` on ALL four ways a required
657 /// slot can miss:
658 ///
659 /// 1. Receiver is `None` — the `metadata.name` corner when the
660 /// top-level `metadata` object itself is absent (the caller
661 /// threaded `res.get("metadata")` which returned `None`).
662 /// 2. Slot is absent — the receiver is present but does not
663 /// carry a value at `key`.
664 /// 3. Slot is present but non-string — a fixture bug that
665 /// stamped a JSON number / bool / object / array at the
666 /// slot; the `get_str` step falls through and the primitive
667 /// reports the slot as missing (matching the pre-lift
668 /// behavior where every non-string variant surfaced as the
669 /// same `"missing"` diagnostic — pinning "cannot be applied
670 /// via kube-rs's dynamic API surface" as the shared
671 /// failure mode).
672 /// 4. Receiver is non-object — a resource authored as a JSON
673 /// array / string / null at any of the levels the primitive
674 /// walks (the `get_str` step returns `None` verbatim).
675 ///
676 /// All four corners produce the SAME wire form so an operator's
677 /// `rg "rendered resource missing"` sweep hits exactly one
678 /// footprint per faulted slot, not four differently-worded
679 /// diagnostics per absent-shape variant.
680 ///
681 /// Theory anchor: THEORY.md §VI.1 (generation over composition —
682 /// the 4-link `.get_str(<key>).ok_or_else(|| anyhow!("rendered
683 /// resource missing <slot>"))?.to_string()` shape recurred at 3
684 /// hand-authored sites past the ★★ PRIME-DIRECTIVE ≥ 2
685 /// duplication trigger, and is lifted to ONE substrate owner
686 /// here). THEORY.md §II.1 invariant 5 (composition preserves
687 /// proofs — a regression that drifted the diagnostic prefix
688 /// wording at ONE site would silently pass the two sibling
689 /// pins and fail HERE; post-lift the wire form is owned once
690 /// at [`Self::MISSING_MESSAGE_PREFIX`] and every downstream
691 /// composition inherits the rename mechanically).
692 fn required_str(
693 v: Option<&Value>,
694 key: &'static str,
695 error_slot: &'static str,
696 ) -> anyhow::Result<String> {
697 // The `Option<&Value>` outer-optionality unwrap-then-project
698 // step rides through the substrate primitive
699 // `crate::json_object::ValueGetExt::get_str` on the
700 // `Option<&Value>` receiver arm — the outer-optionality
701 // widening sibling of the `Value` / `Map<String, Value>`
702 // impls. Pre-lift this body hand-authored the outer-
703 // optionality closure `v.and_then(|x| x.get_str(key))` past
704 // the ★★ PRIME-DIRECTIVE ≥ 2 duplication threshold (this
705 // helper's body + the sibling `metadata.namespace` walk in
706 // `Self::from_json`); post-lift the closure disappears and
707 // the axis-family method-call surface stays identical to the
708 // `Value` / `Map` receiver callers.
709 v.get_str(key).map(str::to_string).ok_or_else(|| {
710 anyhow::anyhow!(
711 "{prefix} {slot}",
712 prefix = Self::MISSING_MESSAGE_PREFIX,
713 slot = error_slot,
714 )
715 })
716 }
717
718 /// `metadata.namespace` slice with the K8s canonical `"default"`
719 /// fallback applied — matching what [`Process::DEFAULT_NAMESPACE`]
720 /// spells for the `Process`-borne coordinate primitive family
721 /// and what [`FluxResourceRef.namespace`] records into
722 /// `ProcessStatus.flux_resources`.
723 pub fn namespace_or_default(&self) -> &str {
724 self.namespace
725 .as_deref()
726 .unwrap_or(Process::DEFAULT_NAMESPACE)
727 }
728}
729
730/// A boundary condition paired with its current satisfaction state.
731#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
732#[serde(rename_all = "camelCase")]
733pub struct CheckedCondition {
734 #[serde(flatten)]
735 pub condition: Condition,
736 pub satisfied: bool,
737 #[serde(default, skip_serializing_if = "Option::is_none")]
738 pub last_check: Option<DateTime<Utc>>,
739 #[serde(default, skip_serializing_if = "Option::is_none")]
740 pub message: Option<String>,
741}
742
743impl CheckedCondition {
744 /// True iff every [`CheckedCondition`] in the slice has
745 /// `satisfied == true` — the ONE-line collapse of the paired
746 /// `checked.iter().all(|c| c.satisfied)` incantation the
747 /// reconciler's precondition + postcondition boundary gates both
748 /// spelled by hand pre-lift.
749 ///
750 /// Pre-lift the SAME `.iter().all(|c| c.satisfied)` chain was
751 /// hand-authored at TWO sites past the ★★ PRIME-DIRECTIVE ≥ 2
752 /// duplication threshold in `tatara-reconciler::phase_machine`,
753 /// each walking the SAME `Vec<CheckedCondition>` → `bool`
754 /// projection to gate a phase transition on a boundary predicate:
755 /// * `handle_execing` — the PROVE-phase precondition gate that
756 /// stays in Execing (heartbeat requeue) while any precondition
757 /// remains unsatisfied and proceeds to RENDER only when every
758 /// precondition holds.
759 /// * `handle_running` — the VERIFY-phase postcondition gate that
760 /// stays in Running (heartbeat requeue) while any postcondition
761 /// remains unsatisfied and advances to Attested only when every
762 /// postcondition holds.
763 ///
764 /// Both sites walked the SAME `Iterator::all` short-circuit on the
765 /// SAME `bool` slot of the SAME struct. Post-lift both consumers
766 /// name the slice ONCE and route through this ONE primitive; the
767 /// vacuous-truth corner (empty slice → `true`, matching
768 /// [`Iterator::all`]'s empty-input identity) sits at ONE substrate
769 /// site so a future normalization (a per-slot weight overlay, a
770 /// per-kind override that treats `Warn`-severity failures as
771 /// satisfied, a compliance-baseline gate that requires N-of-M
772 /// rather than all-of-M) lands at ONE substrate function and both
773 /// downstream phase gates inherit the upgrade mechanically.
774 ///
775 /// Return-form axis: `bool` — the exact type each phase gate
776 /// pre-lift bound at `let all_pass = <chain>;` and immediately
777 /// consumed in a `!all_pass` short-circuit + a `message` slot's
778 /// ternary branch. The `&[Self]` argument accepts every pre-lift
779 /// slice provenance verbatim: a `&Vec<CheckedCondition>` (both
780 /// pre-lift sites had the `Vec` on the stack from
781 /// [`crate::phase_machine::evaluate_conditions`]'s owned return)
782 /// coerces through auto-deref, so no callsite has to change its
783 /// upstream provenance to route through the primitive.
784 ///
785 /// Peer to the sibling projection [`Self::satisfied`] on the (row
786 /// scope × predicate) axis pair: `satisfied` is the per-row
787 /// projection; `all_satisfied` is the slice-wide fold of the same
788 /// bit. Both live on `CheckedCondition` so a future rename or
789 /// per-slot normalization travels through the same owner without
790 /// splitting between "per-row" and "slice-wide" call sinks.
791 ///
792 /// Return-shape pin lives at
793 /// [`tests::checked_condition_all_satisfied_matches_pre_lift_iter_all_chain_shape`]
794 /// so a regression that flipped the fold direction (`any` for
795 /// `all`), inverted the bit (`!c.satisfied`), or reshaped the
796 /// return form (an owned `Vec<bool>` instead of the folded `bool`)
797 /// fails-loudly here rather than as silent operator-facing skew
798 /// between the pre-lift `if !all_pass { requeue }` gate and the
799 /// post-lift call — every downstream consumer would still
800 /// short-circuit but on inverted semantics.
801 ///
802 /// Theory grounding: THEORY.md §VI.1 (generation over composition
803 /// — the 1-line `.iter().all(...)` chain recurred at two hand-
804 /// authored sites past the ≥ 2 duplication trigger, and is lifted
805 /// to ONE typed fold here). THEORY.md §II.1 invariant 5
806 /// (composition preserves proofs — the empty-slice vacuous-truth
807 /// corner + the fold direction + the projected bit's polarity all
808 /// bind at ONE substrate site, so a regression across any of the
809 /// three surfaces at [`tests::checked_condition_all_satisfied_*`]
810 /// pin rather than as silent gate-flip at every downstream phase
811 /// handler).
812 #[must_use]
813 pub fn all_satisfied(checked: &[Self]) -> bool {
814 checked.iter().all(|c| c.satisfied)
815 }
816}
817
818/// Summary of boundary verification.
819#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
820#[serde(rename_all = "camelCase")]
821pub struct BoundaryStatus {
822 #[serde(default)]
823 pub preconditions: Vec<CheckedCondition>,
824 #[serde(default)]
825 pub postconditions: Vec<CheckedCondition>,
826 /// Absolute deadline for VERIFY (derived from `spec.boundary.timeout`).
827 #[serde(default, skip_serializing_if = "Option::is_none")]
828 pub deadline: Option<DateTime<Utc>>,
829}
830
831/// Summary of compliance checks at the latest attestation.
832#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
833#[serde(rename_all = "camelCase")]
834pub struct ComplianceStatus {
835 #[serde(default, skip_serializing_if = "Option::is_none")]
836 pub baseline: Option<String>,
837 pub satisfied: u32,
838 pub violated: u32,
839 pub total: u32,
840 #[serde(default)]
841 pub violations: Vec<String>,
842}
843
844#[cfg(test)]
845mod tests {
846 use super::*;
847 use serde_json::json;
848
849 // ─── ProcessCondition writer / K8sConditionStatus substrate pins ─
850 //
851 // Byte-shape parity pins between `ProcessCondition::{ready,
852 // not_ready, attested}` writer output and the pre-lift hand-
853 // authored `"True"` / `"False"` `status` slot literals every
854 // downstream K8s API server + K8s-Condition-reading peer depends
855 // on. Post-lift the writers compose through
856 // `K8sConditionStatus::<V>.as_wire_str()`; these pins catch a
857 // regression at the substrate primitive (a lower-case drift, a
858 // whitespace prefix, a `serde(rename)` addition on the enum)
859 // that would silently reshape every emitted Process
860 // `status.conditions[]` slot away from the K8s wire form.
861
862 /// Fail-before-pass-after: `ProcessCondition::ready` emits the
863 /// exact-case ASCII `"True"` on the `status` slot the K8s API
864 /// server accepts, byte-identical to the pre-lift hand-authored
865 /// `status: "True".into()` literal. A regression at the substrate
866 /// primitive (a `to_lowercase` pass, a case-drifted variant
867 /// literal in `k8s_condition::K8sConditionStatus::as_wire_str`)
868 /// surfaces HERE, not as silent operator-facing wire-form skew
869 /// on the emitted Process CRD.
870 #[test]
871 fn ready_writer_status_slot_matches_pre_lift_true_literal_bytewise() {
872 let c = ProcessCondition::ready("ObservedRunning", Some("healthy".into()));
873 assert_eq!(c.type_, "Ready");
874 assert_eq!(c.status, "True");
875 }
876
877 /// Fail-before-pass-after: `ProcessCondition::not_ready` emits
878 /// the exact-case ASCII `"False"` on the `status` slot, byte-
879 /// identical to the pre-lift hand-authored `status: "False".
880 /// into()` literal.
881 #[test]
882 fn not_ready_writer_status_slot_matches_pre_lift_false_literal_bytewise() {
883 let c = ProcessCondition::not_ready("ObservedFailed", "boom");
884 assert_eq!(c.type_, "Ready");
885 assert_eq!(c.status, "False");
886 }
887
888 /// Fail-before-pass-after: `ProcessCondition::attested` emits
889 /// the exact-case ASCII `"True"` on the `status` slot with the
890 /// `"Attested"` type row, byte-identical to the pre-lift hand-
891 /// authored `type_: "Attested".into()` + `status: "True".into()`
892 /// pair.
893 #[test]
894 fn attested_writer_status_slot_matches_pre_lift_true_literal_bytewise() {
895 let c = ProcessCondition::attested("blake3:abc123");
896 assert_eq!(c.type_, "Attested");
897 assert_eq!(c.status, "True");
898 // Message body preserves the composed_root diagnostic
899 // wording verbatim — the substrate lift only reshaped the
900 // `status` slot, not the human-facing message.
901 assert_eq!(c.message.as_deref(), Some("composed_root=blake3:abc123"));
902 assert_eq!(c.reason.as_deref(), Some("AttestationWritten"));
903 }
904
905 /// Writer/reader wire-form parity: the `status` slot every
906 /// writer here emits is the SAME byte-shape the
907 /// `K8sConditionStatus::from_wire_str` reader in
908 /// `tatara-reconciler::ssapply::ready_condition_value` accepts.
909 /// A regression that drifted `as_wire_str` at ONE variant would
910 /// silently desynchronize every writer/reader pair in the
911 /// workspace; this pin surfaces the drift at the substrate.
912 #[test]
913 fn writer_output_round_trips_through_from_wire_str() {
914 let ready = ProcessCondition::ready("R", None);
915 assert_eq!(
916 K8sConditionStatus::from_wire_str(&ready.status),
917 Some(K8sConditionStatus::True),
918 );
919 let not_ready = ProcessCondition::not_ready("R", "why");
920 assert_eq!(
921 K8sConditionStatus::from_wire_str(¬_ready.status),
922 Some(K8sConditionStatus::False),
923 );
924 let attested = ProcessCondition::attested("blake3:zzz");
925 assert_eq!(
926 K8sConditionStatus::from_wire_str(&attested.status),
927 Some(K8sConditionStatus::True),
928 );
929 }
930
931 // ─── ProcessCondition::new_at_now backbone-composer substrate pins ───
932 //
933 // The composer [`ProcessCondition::new_at_now`] owns the 5-slot wire-
934 // shape backbone every sibling constructor on this impl block
935 // ([`ProcessCondition::ready`], [`ProcessCondition::not_ready`],
936 // [`ProcessCondition::attested`]) hand-authored pre-lift. These pins
937 // bind the observable slots (typed-enum → wire-string projection on
938 // `type_` + `status`, `Utc::now()` stamp on `last_transition_time`,
939 // `Some(<reason>.into())` wrap on `reason`, verbatim message opt on
940 // `message`) at fail-before-pass-after granularity so a regression
941 // that drifted any slot's projection (a typed-enum arm bypassed by
942 // an inline literal, the `Some(...)`-wrap on `reason` narrowed to
943 // a bare `.into()`, the `last_transition_time` stamp swapped for a
944 // fixed anchor) surfaces HERE rather than as silent operator-facing
945 // K8s-Condition wire-shape skew across the three sibling
946 // constructor sites.
947
948 /// Fail-before-pass-after: the composer's 5-slot output is byte-
949 /// identical (modulo the `Utc::now()` monotonic clock stamp) to
950 /// the pre-lift hand-authored struct literal every sibling
951 /// constructor restated. A regression that drifted the projection
952 /// at any of the four fixed slots surfaces HERE, not as silent
953 /// wire-form skew on the emitted Process CRD.
954 #[test]
955 fn new_at_now_composes_typed_enums_reason_option_wrap_and_message_slot_verbatim() {
956 let before = Utc::now();
957 let c = ProcessCondition::new_at_now(
958 ProcessConditionType::Ready,
959 K8sConditionStatus::True,
960 "ObservedRunning",
961 Some("healthy".into()),
962 );
963 let after = Utc::now();
964 assert_eq!(c.type_, ProcessConditionType::Ready.as_wire_str());
965 assert_eq!(c.status, K8sConditionStatus::True.as_wire_str());
966 assert_eq!(c.reason.as_deref(), Some("ObservedRunning"));
967 assert_eq!(c.message.as_deref(), Some("healthy"));
968 assert!(
969 c.last_transition_time >= before && c.last_transition_time <= after,
970 "last_transition_time rides Utc::now() at the composer body",
971 );
972 }
973
974 /// Fail-before-pass-after: the composer preserves an explicit
975 /// `None` message-slot verbatim — a K8s Condition with an absent
976 /// message vs an empty-string message are distinct wire shapes
977 /// the K8s API server treats differently, so the composer must
978 /// route `None` through without an accidental `Some(String::
979 /// new())` wrap.
980 #[test]
981 fn new_at_now_preserves_none_message_slot_verbatim() {
982 let c = ProcessCondition::new_at_now(
983 ProcessConditionType::Ready,
984 K8sConditionStatus::False,
985 "R",
986 None,
987 );
988 assert!(
989 c.message.is_none(),
990 "message-slot rides `None` verbatim — an empty-Some wrap is a distinct wire shape",
991 );
992 }
993
994 /// Byte-shape parity witness — `ProcessCondition::ready` routes
995 /// through `new_at_now(Ready, True, reason, message)`, matching
996 /// the pre-lift hand-authored 5-slot struct literal on every
997 /// observable slot except the monotonic `last_transition_time`
998 /// stamp.
999 #[test]
1000 fn ready_routes_through_new_at_now_composer_slots_match_pre_lift_literal() {
1001 let via_public = ProcessCondition::ready("ObservedRunning", Some("healthy".into()));
1002 let via_composer = ProcessCondition::new_at_now(
1003 ProcessConditionType::Ready,
1004 K8sConditionStatus::True,
1005 "ObservedRunning",
1006 Some("healthy".into()),
1007 );
1008 assert_eq!(via_public.type_, via_composer.type_);
1009 assert_eq!(via_public.status, via_composer.status);
1010 assert_eq!(via_public.reason, via_composer.reason);
1011 assert_eq!(via_public.message, via_composer.message);
1012 }
1013
1014 /// Byte-shape parity witness — `ProcessCondition::not_ready`
1015 /// routes through `new_at_now(Ready, False, reason, Some(message.
1016 /// into()))`, matching the pre-lift hand-authored 5-slot struct
1017 /// literal on every observable slot.
1018 #[test]
1019 fn not_ready_routes_through_new_at_now_composer_slots_match_pre_lift_literal() {
1020 let via_public = ProcessCondition::not_ready("ObservedFailed", "boom");
1021 let via_composer = ProcessCondition::new_at_now(
1022 ProcessConditionType::Ready,
1023 K8sConditionStatus::False,
1024 "ObservedFailed",
1025 Some("boom".into()),
1026 );
1027 assert_eq!(via_public.type_, via_composer.type_);
1028 assert_eq!(via_public.status, via_composer.status);
1029 assert_eq!(via_public.reason, via_composer.reason);
1030 assert_eq!(via_public.message, via_composer.message);
1031 }
1032
1033 /// Byte-shape parity witness — `ProcessCondition::attested`
1034 /// routes through `new_at_now(Attested, True, "AttestationWritten",
1035 /// Some(format!("composed_root={root}")))`, matching the pre-lift
1036 /// hand-authored 5-slot struct literal on every observable slot.
1037 #[test]
1038 fn attested_routes_through_new_at_now_composer_slots_match_pre_lift_literal() {
1039 let via_public = ProcessCondition::attested("blake3:abc123");
1040 let via_composer = ProcessCondition::new_at_now(
1041 ProcessConditionType::Attested,
1042 K8sConditionStatus::True,
1043 "AttestationWritten",
1044 Some("composed_root=blake3:abc123".into()),
1045 );
1046 assert_eq!(via_public.type_, via_composer.type_);
1047 assert_eq!(via_public.status, via_composer.status);
1048 assert_eq!(via_public.reason, via_composer.reason);
1049 assert_eq!(via_public.message, via_composer.message);
1050 }
1051
1052 /// Routing pin — the `type_` slot at the composer's `Self { …
1053 /// type_: type_.as_wire_str().into() }` binding rides through the
1054 /// [`ProcessConditionType::as_wire_str`] projection rather than an
1055 /// inline `format!("{:?}", type_)` or hand-authored per-variant
1056 /// literal. A regression that inlined the wire-string projection
1057 /// (drifting the composer off the closed-set owner + reopening the
1058 /// typo-drift surface a hand-authored `"ready"` / `"Ready "` /
1059 /// `"READY"` spelling would fall into silently) surfaces HERE
1060 /// rather than as silent per-variant wire-form skew where the
1061 /// composed Condition's `type_` slot disagrees with the K8s API
1062 /// server's expected literal.
1063 #[test]
1064 fn new_at_now_routes_type_slot_through_process_condition_type_as_wire_str() {
1065 for type_ in ProcessConditionType::ALL {
1066 let via_composer =
1067 ProcessCondition::new_at_now(type_, K8sConditionStatus::True, "R", None);
1068 assert_eq!(
1069 via_composer.type_,
1070 type_.as_wire_str(),
1071 "type_ slot must route through ProcessConditionType::as_wire_str for {type_:?}",
1072 );
1073 }
1074 }
1075
1076 /// Routing pin — the `status` slot at the composer's `Self { …
1077 /// status: status.as_wire_str().into() }` binding rides through
1078 /// the [`K8sConditionStatus::as_wire_str`] projection rather than
1079 /// an inline hand-authored per-variant literal. A regression that
1080 /// inlined the wire-string projection surfaces HERE rather than
1081 /// as silent per-variant wire-form skew where the composed
1082 /// Condition's `status` slot disagrees with the K8s API server's
1083 /// expected `"True"` / `"False"` / `"Unknown"` closed set.
1084 #[test]
1085 fn new_at_now_routes_status_slot_through_k8s_condition_status_as_wire_str() {
1086 for status in K8sConditionStatus::ALL {
1087 let via_composer =
1088 ProcessCondition::new_at_now(ProcessConditionType::Ready, status, "R", None);
1089 assert_eq!(
1090 via_composer.status,
1091 status.as_wire_str(),
1092 "status slot must route through K8sConditionStatus::as_wire_str for {status:?}",
1093 );
1094 }
1095 }
1096
1097 // ─── ProcessCondition::new_at clock-injectable substrate pins ────
1098 //
1099 // The clock-injectable substrate peer [`ProcessCondition::new_at`]
1100 // owns the 5-slot wire-shape backbone with `last_transition_time`
1101 // sourced from a caller-supplied `DateTime<Utc>` slot instead of
1102 // an inline `Utc::now()` read. [`ProcessCondition::new_at_now`]
1103 // delegates through it with `Utc::now()`. These pins bind:
1104 // (a) the `at` slot is stamped bytewise (no clamp/round/offset
1105 // drift at the composer body),
1106 // (b) [`ProcessCondition::new_at_now`] composes through the
1107 // substrate with a wall-clock third arg (a regression that
1108 // re-inlined the 5-slot literal at `new_at_now` bypasses the
1109 // substrate and surfaces here rather than as silent skew
1110 // between `new_at`-anchored callers and `new_at_now`),
1111 // (c) the four non-clock slots ride through the SAME projections
1112 // `new_at_now`'s existing pins already bind — closed-set
1113 // `as_wire_str` at type_/status, `Some(<r>.into())` wrap at
1114 // reason, verbatim `Option<String>` pass-through at message.
1115
1116 /// Fail-before-pass-after: the clock-injectable substrate peer
1117 /// [`ProcessCondition::new_at`] stamps the caller-supplied
1118 /// `DateTime<Utc>` at the `last_transition_time` slot bytewise —
1119 /// a deterministic anchor rides through with no wall-clock read,
1120 /// no clamp, no `Duration::seconds(0)` rounding, no offset
1121 /// normalization. Peer of the `[before, after]` bracket pin on
1122 /// [`ProcessCondition::new_at_now`]; this pin owns the
1123 /// deterministic-anchor half of the (wall-clock, deterministic-
1124 /// anchor) axis pair. A regression that inserted an implicit
1125 /// `Utc::now()` fallback at the composer body (silently ignoring
1126 /// the caller's anchor) surfaces HERE rather than as a hidden
1127 /// non-determinism at every future
1128 /// `ProcessCondition::new_at`-anchored test / controller-side
1129 /// callsite that pinned a specific anchor.
1130 #[test]
1131 fn new_at_binds_at_slot_to_supplied_datetime_verbatim() {
1132 let anchor: DateTime<Utc> = crate::time::at_epoch_second(1_700_000_000);
1133 let c = ProcessCondition::new_at(
1134 ProcessConditionType::Ready,
1135 K8sConditionStatus::True,
1136 "R",
1137 Some("m".into()),
1138 anchor,
1139 );
1140 assert_eq!(
1141 c.last_transition_time, anchor,
1142 "last_transition_time must ride the caller-supplied `at` slot bytewise — \
1143 a regression that inserted an implicit `Utc::now()` fallback would surface here",
1144 );
1145 // Cross-anchor pin — a distant future anchor + the epoch also
1146 // ride through bytewise, so the invariant holds at both
1147 // extremes of the wall-clock axis rather than only at the
1148 // 1.7-billion-second-past-epoch mid-range corner above. The
1149 // upper bound stays inside `chrono`'s valid `DateTime<Utc>`
1150 // range (year 2262 ceiling for nanosecond-precision anchors
1151 // the substrate carries) so the epoch composer succeeds.
1152 for anchor in [
1153 crate::time::at_epoch_second(0),
1154 crate::time::at_epoch_second(4_000_000_000),
1155 ] {
1156 let c = ProcessCondition::new_at(
1157 ProcessConditionType::Attested,
1158 K8sConditionStatus::True,
1159 "R",
1160 None,
1161 anchor,
1162 );
1163 assert_eq!(c.last_transition_time, anchor);
1164 }
1165 }
1166
1167 /// Cross-primitive coherence pin — [`ProcessCondition::new_at_now`]
1168 /// composes through [`ProcessCondition::new_at`] with `Utc::now()`
1169 /// at the `at` slot. Every non-clock slot on the two composers'
1170 /// outputs is byte-identical for the SAME (type_, status, reason,
1171 /// message) input tuple; only the `last_transition_time` differs
1172 /// (the wall-clock peer's slot falls in `[before, after]`, and the
1173 /// deterministic peer's slot is exactly the caller's anchor). A
1174 /// regression that re-inlined the 5-slot literal at
1175 /// [`ProcessCondition::new_at_now`] (bypassing the substrate) or
1176 /// swapped `Utc::now()` for a fixed anchor at the delegation body
1177 /// surfaces HERE rather than as silent skew between the two peer
1178 /// composers' consumers.
1179 #[test]
1180 fn new_at_now_routes_through_new_at_with_utc_now_stamp_bytewise() {
1181 let before = Utc::now();
1182 let via_wall = ProcessCondition::new_at_now(
1183 ProcessConditionType::Ready,
1184 K8sConditionStatus::False,
1185 "ObservedFailed",
1186 Some("boom".into()),
1187 );
1188 let after = Utc::now();
1189 // Anchor a peer of `via_wall` at a fixed deterministic slot
1190 // and pin non-clock parity; the two composers must agree on
1191 // every slot except `last_transition_time`.
1192 let via_at = ProcessCondition::new_at(
1193 ProcessConditionType::Ready,
1194 K8sConditionStatus::False,
1195 "ObservedFailed",
1196 Some("boom".into()),
1197 crate::time::at_epoch_second(0),
1198 );
1199 assert_eq!(via_wall.type_, via_at.type_);
1200 assert_eq!(via_wall.status, via_at.status);
1201 assert_eq!(via_wall.reason, via_at.reason);
1202 assert_eq!(via_wall.message, via_at.message);
1203 assert!(
1204 via_wall.last_transition_time >= before && via_wall.last_transition_time <= after,
1205 "new_at_now must stamp Utc::now() at the delegation body — a regression that \
1206 swapped it for a fixed anchor surfaces here",
1207 );
1208 // Delegation identity — for every closed-set (type_, status)
1209 // pair, composing through `new_at` with an anchor threaded
1210 // through `new_at_now`'s bracket yields identical non-clock
1211 // slots on both peers. Sweep the two closed-set axes
1212 // exhaustively so a future variant addition inherits the pin.
1213 for type_ in ProcessConditionType::ALL {
1214 for status in K8sConditionStatus::ALL {
1215 let via_wall = ProcessCondition::new_at_now(type_, status, "R", None);
1216 let via_at = ProcessCondition::new_at(
1217 type_,
1218 status,
1219 "R",
1220 None,
1221 via_wall.last_transition_time,
1222 );
1223 assert_eq!(via_wall.type_, via_at.type_);
1224 assert_eq!(via_wall.status, via_at.status);
1225 assert_eq!(via_wall.reason, via_at.reason);
1226 assert_eq!(via_wall.message, via_at.message);
1227 assert_eq!(via_wall.last_transition_time, via_at.last_transition_time);
1228 }
1229 }
1230 }
1231
1232 /// Fail-before-pass-after: the clock-injectable substrate peer
1233 /// preserves an explicit `None` message-slot verbatim — mirror of
1234 /// the sibling pin on [`ProcessCondition::new_at_now`]. A K8s
1235 /// Condition with an absent message vs an empty-string message
1236 /// are distinct wire shapes the K8s API server treats
1237 /// differently, so a regression at the substrate body that
1238 /// wrapped `None` into `Some(String::new())` would surface HERE
1239 /// as well as at the wall-clock peer's pin.
1240 #[test]
1241 fn new_at_preserves_none_message_slot_verbatim() {
1242 let c = ProcessCondition::new_at(
1243 ProcessConditionType::Ready,
1244 K8sConditionStatus::False,
1245 "R",
1246 None,
1247 crate::time::at_epoch_second(0),
1248 );
1249 assert!(
1250 c.message.is_none(),
1251 "message-slot rides `None` verbatim — an empty-Some wrap is a distinct wire shape",
1252 );
1253 }
1254
1255 // ─── RenderedResourceCoords substrate pins ──────────────────────
1256
1257 #[test]
1258 fn rendered_resource_coords_from_json_extracts_all_four_slots_when_present() {
1259 let res = json!({
1260 "apiVersion": "kustomize.toolkit.fluxcd.io/v1",
1261 "kind": "Kustomization",
1262 "metadata": {
1263 "name": "observability-stack",
1264 "namespace": "flux-system",
1265 },
1266 });
1267 let c = RenderedResourceCoords::from_json(&res).expect("extract");
1268 assert_eq!(c.api_version, "kustomize.toolkit.fluxcd.io/v1");
1269 assert_eq!(c.kind, "Kustomization");
1270 assert_eq!(c.name, "observability-stack");
1271 assert_eq!(c.namespace.as_deref(), Some("flux-system"));
1272 }
1273
1274 #[test]
1275 fn rendered_resource_coords_from_json_captures_absent_namespace_as_none() {
1276 // Cluster-scoped resource — `metadata.namespace` intentionally absent.
1277 let res = json!({
1278 "apiVersion": "v1",
1279 "kind": "Namespace",
1280 "metadata": {"name": "demo-test"},
1281 });
1282 let c = RenderedResourceCoords::from_json(&res).expect("extract");
1283 assert_eq!(c.namespace, None);
1284 assert_eq!(c.name, "demo-test");
1285 }
1286
1287 #[test]
1288 fn rendered_resource_coords_from_json_errors_on_missing_api_version() {
1289 let res = json!({"kind": "K", "metadata": {"name": "n"}});
1290 let e = RenderedResourceCoords::from_json(&res).expect_err("must error");
1291 assert_eq!(e.to_string(), "rendered resource missing apiVersion");
1292 }
1293
1294 #[test]
1295 fn rendered_resource_coords_from_json_errors_on_missing_kind() {
1296 let res = json!({"apiVersion": "v1", "metadata": {"name": "n"}});
1297 let e = RenderedResourceCoords::from_json(&res).expect_err("must error");
1298 assert_eq!(e.to_string(), "rendered resource missing kind");
1299 }
1300
1301 #[test]
1302 fn rendered_resource_coords_from_json_errors_on_missing_metadata_name() {
1303 let res = json!({"apiVersion": "v1", "kind": "K", "metadata": {}});
1304 let e = RenderedResourceCoords::from_json(&res).expect_err("must error");
1305 assert_eq!(e.to_string(), "rendered resource missing metadata.name");
1306 }
1307
1308 #[test]
1309 fn rendered_resource_coords_from_json_errors_on_missing_metadata_object() {
1310 // `metadata` absent entirely — same failure as `metadata.name` missing,
1311 // because the API-path leaf segment cannot be resolved.
1312 let res = json!({"apiVersion": "v1", "kind": "K"});
1313 let e = RenderedResourceCoords::from_json(&res).expect_err("must error");
1314 assert_eq!(e.to_string(), "rendered resource missing metadata.name");
1315 }
1316
1317 #[test]
1318 fn rendered_resource_coords_from_json_errors_on_non_string_slot() {
1319 // A numeric `apiVersion` slot falls through the `.as_str()` gate and
1320 // triggers the same missing-slot failure as absence — the API-path
1321 // segment is not a string.
1322 let res = json!({
1323 "apiVersion": 42,
1324 "kind": "K",
1325 "metadata": {"name": "n"},
1326 });
1327 let e = RenderedResourceCoords::from_json(&res).expect_err("must error");
1328 assert_eq!(e.to_string(), "rendered resource missing apiVersion");
1329 }
1330
1331 #[test]
1332 fn rendered_resource_coords_error_wording_is_canonical() {
1333 // Pins the exact spelling every downstream consumer sees.
1334 // Pre-lift wording differed across the two call sites (`"resource
1335 // missing X"` in `apply_owned` vs `"rendered resource missing X"` in
1336 // `flux_ref_from_json`); post-lift the canonical wording is
1337 // `"rendered resource missing X"` at every site.
1338 let cases = [
1339 (
1340 "apiVersion",
1341 json!({"kind": "K", "metadata": {"name": "n"}}),
1342 ),
1343 (
1344 "kind",
1345 json!({"apiVersion": "v1", "metadata": {"name": "n"}}),
1346 ),
1347 (
1348 "metadata.name",
1349 json!({"apiVersion": "v1", "kind": "K", "metadata": {}}),
1350 ),
1351 ];
1352 for (slot, res) in cases {
1353 let e = RenderedResourceCoords::from_json(&res).expect_err("must error");
1354 assert_eq!(
1355 e.to_string(),
1356 format!("rendered resource missing {slot}"),
1357 "slot {slot} error must be canonical"
1358 );
1359 }
1360 }
1361
1362 // ─── RenderedResourceCoords::required_str substrate pins ────────
1363 //
1364 // Fail-before-pass-after granularity: the
1365 // `RenderedResourceCoords::required_str` inherent associated
1366 // function did not exist before this commit, so each test below
1367 // fails to compile pre-lift. Post-lift they collectively pin the
1368 // required-string-extract shape at ONE substrate owner — a
1369 // regression that swaps `MISSING_MESSAGE_PREFIX`, decouples the
1370 // `key` / `error_slot` slot pair with a wrong ordering, drops the
1371 // `str::to_string` coerce (returning `&str` and forcing every
1372 // consumer to re-stamp `.to_string()` per site), or narrows the
1373 // receiver from `Option<&Value>` to `&Value` (silently breaking
1374 // the `metadata.name` corner where the caller threads the
1375 // `res.get("metadata")` result directly) surfaces HERE rather
1376 // than as silent operator-facing skew across the three pre-lift
1377 // consumers on `from_json`.
1378
1379 #[test]
1380 fn required_str_present_string_slot_returns_owned_string() {
1381 // Ok-arm invariant: a present string slot at `key` on a
1382 // `Some(&Value::Object)` receiver returns `Ok(<owned>)` —
1383 // the primitive absorbs the `.to_string()` coerce the three
1384 // pre-lift restatements each stamped at the tail.
1385 let res = json!({"apiVersion": "kustomize.toolkit.fluxcd.io/v1"});
1386 let got =
1387 RenderedResourceCoords::required_str(Some(&res), "apiVersion", "apiVersion").unwrap();
1388 assert_eq!(got, "kustomize.toolkit.fluxcd.io/v1");
1389 }
1390
1391 #[test]
1392 fn required_str_none_receiver_errors_with_canonical_wire_form() {
1393 // Absent-shape corner 1: the caller threads `None`
1394 // (`res.get("metadata")` returned `None` because the top-
1395 // level `metadata` slot itself is absent). The primitive
1396 // errors with the SAME wire form the two other absent
1397 // corners produce, keeping the operator-facing footprint
1398 // singular.
1399 let e = RenderedResourceCoords::required_str(None, "name", "metadata.name")
1400 .expect_err("None receiver must error");
1401 assert_eq!(e.to_string(), "rendered resource missing metadata.name");
1402 }
1403
1404 #[test]
1405 fn required_str_absent_slot_errors_with_canonical_wire_form() {
1406 // Absent-shape corner 2: the receiver is present but the
1407 // slot at `key` is not stamped on it. Wire form matches
1408 // the `None`-receiver corner and the non-string corner.
1409 let res = json!({"kind": "K"});
1410 let e = RenderedResourceCoords::required_str(Some(&res), "apiVersion", "apiVersion")
1411 .expect_err("absent slot must error");
1412 assert_eq!(e.to_string(), "rendered resource missing apiVersion");
1413 }
1414
1415 #[test]
1416 fn required_str_non_string_slot_errors_with_canonical_wire_form() {
1417 // Absent-shape corner 3: the slot is present but stamped
1418 // as a JSON number / bool / object / array — every
1419 // non-`Value::String` variant falls through the underlying
1420 // `get_str` gate and produces the SAME `"missing"` diagnostic.
1421 // Pinning EVERY non-string variant here (not just number)
1422 // guarantees an operator's error-stream grep collapses all
1423 // fixture-authoring bugs at this slot onto one footprint.
1424 for bad in [
1425 json!({"apiVersion": 42}),
1426 json!({"apiVersion": true}),
1427 json!({"apiVersion": {}}),
1428 json!({"apiVersion": [1]}),
1429 json!({"apiVersion": null}),
1430 ] {
1431 let e = RenderedResourceCoords::required_str(Some(&bad), "apiVersion", "apiVersion")
1432 .expect_err("non-string slot must error");
1433 assert_eq!(e.to_string(), "rendered resource missing apiVersion");
1434 }
1435 }
1436
1437 #[test]
1438 fn required_str_non_object_receiver_errors_with_canonical_wire_form() {
1439 // Absent-shape corner 4: the receiver itself is not a
1440 // `Value::Object` — a resource authored as a JSON array,
1441 // string, or null at any of the levels the primitive
1442 // walks. The underlying `get_str` step returns `None`
1443 // verbatim (matching the pre-lift chain's own behavior)
1444 // and the primitive stamps the canonical wire form.
1445 for bad in [json!([1, 2, 3]), json!("stringified"), Value::Null] {
1446 let e = RenderedResourceCoords::required_str(Some(&bad), "name", "metadata.name")
1447 .expect_err("non-object receiver must error");
1448 assert_eq!(e.to_string(), "rendered resource missing metadata.name");
1449 }
1450 }
1451
1452 #[test]
1453 fn required_str_decouples_key_from_error_slot_at_metadata_name_shape() {
1454 // Slot-decoupling pin: for the `metadata.name` corner the
1455 // primitive looks up `key = "name"` on the metadata sub-
1456 // object while stamping `error_slot = "metadata.name"` into
1457 // the error's `Display` output — the two are NOT the same
1458 // string, and a regression that collapsed them (using
1459 // `key` for both the lookup AND the error slug, or
1460 // vice-versa) would silently pass the shallow `apiVersion`
1461 // / `kind` pins above (where `key == error_slot`) and fail
1462 // HERE. Present-arm: lookup succeeds on the metadata sub-
1463 // object's `name` slot, returns the owned string.
1464 let res = json!({"metadata": {"name": "demo"}});
1465 let metadata = res.get("metadata");
1466 let got = RenderedResourceCoords::required_str(metadata, "name", "metadata.name").unwrap();
1467 assert_eq!(got, "demo");
1468 // Absent-arm: same slot-decoupling but the `name` sub-slot
1469 // is absent — the error slug is the DOTTED path, not the
1470 // shallow `"name"` key.
1471 let res_no_name = json!({"metadata": {}});
1472 let metadata_empty = res_no_name.get("metadata");
1473 let e = RenderedResourceCoords::required_str(metadata_empty, "name", "metadata.name")
1474 .expect_err("absent metadata.name must error");
1475 assert_eq!(e.to_string(), "rendered resource missing metadata.name");
1476 }
1477
1478 #[test]
1479 fn required_str_error_wire_form_composes_missing_message_prefix_verbatim() {
1480 // Wire-form composition pin: the error's `Display` is
1481 // exactly `"<Self::MISSING_MESSAGE_PREFIX> <error_slot>"` —
1482 // the leading prefix comes from the `const` owner + a
1483 // single space + the caller-supplied slug. A regression
1484 // that switched the separator (a colon, an em-dash) or
1485 // dropped the prefix (returning just the slot slug) would
1486 // silently invert every operator-facing log grep footprint;
1487 // this pin binds the composition to the ONE prefix const
1488 // so a future rename lands atomically at both the source
1489 // and the pins.
1490 let e = RenderedResourceCoords::required_str(None, "name", "metadata.name")
1491 .expect_err("None receiver must error");
1492 let expected = format!(
1493 "{prefix} metadata.name",
1494 prefix = RenderedResourceCoords::MISSING_MESSAGE_PREFIX,
1495 );
1496 assert_eq!(e.to_string(), expected);
1497 }
1498
1499 #[test]
1500 fn required_str_shape_parity_matches_pre_lift_hand_authored_chain_bytewise() {
1501 // Byte-shape parity pin: on every corner (present, absent,
1502 // non-string, non-object, None-receiver) the primitive's
1503 // output MUST match the pre-lift hand-authored
1504 // `.get_str(<key>).ok_or_else(|| anyhow!("rendered resource
1505 // missing <slot>"))?.to_string()` chain bytewise — the
1506 // Ok-arm string equals the raw `get_str` slice as an owned
1507 // `String`, and the Err-arm `Display` equals the pre-lift
1508 // `anyhow!(...)` output verbatim. A regression that inserted
1509 // a normalization (a trim, an NFC-fold) into the Ok arm or
1510 // altered the diagnostic wrapping in the Err arm surfaces
1511 // HERE rather than as silent per-consumer schema drift.
1512 let cases: &[(Value, &'static str, &'static str)] = &[
1513 (json!({"apiVersion": "v1"}), "apiVersion", "apiVersion"),
1514 (json!({"kind": "K"}), "kind", "kind"),
1515 ];
1516 for (res, key, error_slot) in cases {
1517 let via_primitive =
1518 RenderedResourceCoords::required_str(Some(res), key, error_slot).unwrap();
1519 let via_pre_lift = res.get_str(key).unwrap().to_string();
1520 assert_eq!(via_primitive, via_pre_lift);
1521 }
1522 let empty = json!({"other": "value"});
1523 let err_via_primitive =
1524 RenderedResourceCoords::required_str(Some(&empty), "apiVersion", "apiVersion")
1525 .expect_err("absent slot must error");
1526 let err_via_pre_lift = anyhow::anyhow!("rendered resource missing apiVersion");
1527 assert_eq!(err_via_primitive.to_string(), err_via_pre_lift.to_string());
1528 }
1529
1530 #[test]
1531 fn required_str_missing_message_prefix_matches_pre_lift_wire_form_verbatim() {
1532 // Const-owner pin: the pre-lift hand-authored `anyhow!("rendered
1533 // resource missing X")` restatements each embedded the leading
1534 // `"rendered resource missing"` prefix as an inline literal.
1535 // Post-lift the prefix lives at ONE const owner — a rename lands
1536 // there and the three consumers on `from_json` inherit the
1537 // rename mechanically. This pin binds the const to the pre-lift
1538 // spelling so a rename shows up at BOTH the const definition
1539 // AND this pin as a coherent atomic edit, not as a silent
1540 // diff between the const and its downstream consumers.
1541 assert_eq!(
1542 RenderedResourceCoords::MISSING_MESSAGE_PREFIX,
1543 "rendered resource missing",
1544 );
1545 }
1546
1547 #[test]
1548 fn rendered_resource_coords_namespace_or_default_returns_slice_when_some() {
1549 let c = RenderedResourceCoords {
1550 api_version: "v1".into(),
1551 kind: "K".into(),
1552 name: "n".into(),
1553 namespace: Some("prod".into()),
1554 };
1555 assert_eq!(c.namespace_or_default(), "prod");
1556 }
1557
1558 #[test]
1559 fn rendered_resource_coords_namespace_or_default_falls_back_when_none() {
1560 let c = RenderedResourceCoords {
1561 api_version: "v1".into(),
1562 kind: "K".into(),
1563 name: "n".into(),
1564 namespace: None,
1565 };
1566 assert_eq!(c.namespace_or_default(), Process::DEFAULT_NAMESPACE);
1567 assert_eq!(c.namespace_or_default(), "default");
1568 }
1569
1570 // ─── FluxResourceRef::fetch_coords substrate pins ─────────────
1571 //
1572 // The 4-slot `(&namespace, &api_version, &kind, &name)` borrow
1573 // projection lifts the pre-existing 5-slot `ssapply::fetch(client,
1574 // &r.namespace, &r.api_version, &r.kind, &r.name)` splat that
1575 // recurred at TWO hand-authored sites in
1576 // `tatara-reconciler::phase_machine` (`handle_running`,
1577 // `handle_attested`) past the ★★ PRIME-DIRECTIVE ≥ 2 duplication
1578 // trigger. These pins bind the slot order at fail-before-pass-
1579 // after granularity so a regression that swapped `namespace` and
1580 // `api_version` (both `String`, mechanically interchangeable to
1581 // a bad refactor) surfaces HERE rather than as a silent wire-time
1582 // 404 at every downstream Flux fetch consumer.
1583
1584 fn sample_flux_ref() -> FluxResourceRef {
1585 // Slot values are deliberately distinct so a swap between any
1586 // two adjacent tuple positions surfaces as an equality
1587 // failure at the assertion site — a slot-inversion regression
1588 // cannot masquerade as identity by accident.
1589 FluxResourceRef {
1590 api_version: "kustomize.toolkit.fluxcd.io/v1".to_string(),
1591 kind: "Kustomization".to_string(),
1592 name: "observability-stack".to_string(),
1593 namespace: "flux-system".to_string(),
1594 ready: true,
1595 message: None,
1596 last_check: None,
1597 }
1598 }
1599
1600 #[test]
1601 fn flux_resource_ref_fetch_coords_binds_slots_by_position() {
1602 // Positional pin: the 4-tuple return binds
1603 // `(namespace, api_version, kind, name)` in THAT order,
1604 // matching the raw `ssapply::fetch(client, ns, av, kind,
1605 // name)` positional signature every pre-lift callsite splatted
1606 // into. A regression that swapped ANY pair of adjacent slots
1607 // (all four axes are `String` and mechanically
1608 // indistinguishable at the type level) would surface here
1609 // rather than as an operator-visible wire-form 404 at every
1610 // downstream fetch consumer.
1611 let r = sample_flux_ref();
1612 let (ns, av, kind, name) = r.fetch_coords();
1613 assert_eq!(ns, "flux-system", "position 0 must be namespace");
1614 assert_eq!(
1615 av, "kustomize.toolkit.fluxcd.io/v1",
1616 "position 1 must be api_version"
1617 );
1618 assert_eq!(kind, "Kustomization", "position 2 must be kind");
1619 assert_eq!(name, "observability-stack", "position 3 must be name");
1620 }
1621
1622 #[test]
1623 fn flux_resource_ref_fetch_coords_returns_borrows_of_owned_slots() {
1624 // Borrow-discipline pin: the 4-tuple returns `&str` borrows
1625 // of the enclosing `FluxResourceRef`'s owned `String` slots —
1626 // NOT a fresh allocation or a clone. A regression that
1627 // switched the projection to owned strings (via `.clone()` or
1628 // `format!`) would defeat the zero-copy contract and would
1629 // surface here via pointer-identity comparison.
1630 let r = sample_flux_ref();
1631 let (ns, av, kind, name) = r.fetch_coords();
1632 assert!(std::ptr::eq(ns.as_ptr(), r.namespace.as_ptr()));
1633 assert!(std::ptr::eq(av.as_ptr(), r.api_version.as_ptr()));
1634 assert!(std::ptr::eq(kind.as_ptr(), r.kind.as_ptr()));
1635 assert!(std::ptr::eq(name.as_ptr(), r.name.as_ptr()));
1636 }
1637
1638 #[test]
1639 fn flux_resource_ref_fetch_coords_is_a_pure_borrow_projection() {
1640 // Purity pin: calling the projection twice on the same ref
1641 // returns byte-identical slices (same pointer, same length).
1642 // A regression that introduced state — a lazy-cached slot
1643 // computed on first call, a normalization step that ran once
1644 // and cached — would surface here rather than as silent drift
1645 // between the VERIFY-phase and ATTEST-heartbeat consumers on
1646 // the SAME ref within one reconcile pass.
1647 let r = sample_flux_ref();
1648 let a = r.fetch_coords();
1649 let b = r.fetch_coords();
1650 assert!(std::ptr::eq(a.0.as_ptr(), b.0.as_ptr()));
1651 assert!(std::ptr::eq(a.1.as_ptr(), b.1.as_ptr()));
1652 assert!(std::ptr::eq(a.2.as_ptr(), b.2.as_ptr()));
1653 assert!(std::ptr::eq(a.3.as_ptr(), b.3.as_ptr()));
1654 }
1655
1656 #[test]
1657 fn flux_resource_ref_fetch_coords_ignores_status_slots() {
1658 // Coverage pin: the projection exposes ONLY the four API-path
1659 // slots the fetch call requires; the ref's status slots
1660 // (`ready`, `message`, `last_check`) are deliberately absent
1661 // from the tuple. The fetch signature admits four `&str`
1662 // slots, and the projection carries EXACTLY those four — no
1663 // silent widening that would surface as an arity mismatch at
1664 // every downstream `fetch(...)` call.
1665 let r = sample_flux_ref();
1666 let coords = r.fetch_coords();
1667 assert_eq!(
1668 std::mem::size_of_val(&coords),
1669 std::mem::size_of::<(&str, &str, &str, &str)>(),
1670 "the 4-tuple width must match the raw fetch signature's four `&str` slots"
1671 );
1672 }
1673
1674 // ─── FluxResourceRef::observed substrate pins ─────────────────
1675 //
1676 // The 6-arg composer stamps `last_check` at ONE substrate site
1677 // (the pre-lift 7-slot struct-literal restated `Some(chrono::
1678 // Utc::now())` at TWO hand-authored sites in
1679 // `tatara-reconciler::phase_machine` — `handle_running`'s per-
1680 // ref VERIFY rebuild and `flux_ref_from_json`'s post-SSA
1681 // seeder). These pins bind the six input slots by position so a
1682 // regression that swapped `api_version` and `kind` (both
1683 // `String`, mechanically interchangeable to a bad refactor)
1684 // surfaces HERE rather than as a silent wire-time 404 at every
1685 // downstream fetch consumer.
1686 //
1687 // Every test constructs distinct values across the four
1688 // `String` coordinate slots so a slot swap fails structurally
1689 // rather than by accident of matching literals.
1690
1691 #[test]
1692 fn flux_resource_ref_observed_binds_slots_by_position() {
1693 // Positional pin: the 6-arg constructor binds
1694 // `(api_version, kind, name, namespace, ready, message)`
1695 // in THAT order, matching the pre-lift 7-slot struct-
1696 // literal's declaration order. A regression that swapped
1697 // ANY pair of adjacent `String` coordinate slots (all four
1698 // are mechanically indistinguishable at the type level)
1699 // would surface here rather than as a wire-time 404 at
1700 // every downstream Flux fetch consumer.
1701 let r = FluxResourceRef::observed(
1702 "kustomize.toolkit.fluxcd.io/v1".to_string(),
1703 "Kustomization".to_string(),
1704 "observability-stack".to_string(),
1705 "flux-system".to_string(),
1706 true,
1707 Some("healthy".to_string()),
1708 );
1709 assert_eq!(r.api_version, "kustomize.toolkit.fluxcd.io/v1");
1710 assert_eq!(r.kind, "Kustomization");
1711 assert_eq!(r.name, "observability-stack");
1712 assert_eq!(r.namespace, "flux-system");
1713 assert!(r.ready);
1714 assert_eq!(r.message.as_deref(), Some("healthy"));
1715 }
1716
1717 #[test]
1718 fn flux_resource_ref_observed_stamps_last_check_at_now() {
1719 // Stamp pin: the `last_check` slot is filled with
1720 // `Some(<recent Utc>)` at the composer's body. A
1721 // regression that dropped the stamp (leaving `None`) or
1722 // shifted it to a stale constant would surface here rather
1723 // than as silent operator-observed staleness at
1724 // `ProcessStatus.flux_resources` panels. Bounds the stamp
1725 // to within a generous 5s window of the composer call so
1726 // slow CI runners do not false-positive.
1727 let before = Utc::now();
1728 let r = FluxResourceRef::observed(
1729 "v1".to_string(),
1730 "K".to_string(),
1731 "n".to_string(),
1732 "ns".to_string(),
1733 false,
1734 None,
1735 );
1736 let after = Utc::now();
1737 let stamp = r.last_check.expect("observed must stamp last_check");
1738 assert!(stamp >= before, "stamp must be >= before-call `now`");
1739 assert!(stamp <= after, "stamp must be <= after-call `now`");
1740 }
1741
1742 #[test]
1743 fn flux_resource_ref_observed_round_trips_through_fetch_coords() {
1744 // Cross-composer coherence pin: a ref built by `observed`
1745 // then unpacked by `fetch_coords` returns the same four
1746 // slots in the peer projection's positional order
1747 // `(namespace, api_version, kind, name)`. Composition of
1748 // the two primitives on the same ref preserves the slot
1749 // identity — a regression at either end (a slot swap in
1750 // `observed`, or a slot swap in `fetch_coords`) would
1751 // surface here rather than as silent drift between the
1752 // writer and the reader on the same persisted slice.
1753 let r = FluxResourceRef::observed(
1754 "helm.toolkit.fluxcd.io/v2".to_string(),
1755 "HelmRelease".to_string(),
1756 "prometheus-op".to_string(),
1757 "monitoring".to_string(),
1758 false,
1759 Some("applied; awaiting reconciliation".to_string()),
1760 );
1761 let (ns, av, kind, name) = r.fetch_coords();
1762 assert_eq!(ns, "monitoring");
1763 assert_eq!(av, "helm.toolkit.fluxcd.io/v2");
1764 assert_eq!(kind, "HelmRelease");
1765 assert_eq!(name, "prometheus-op");
1766 }
1767
1768 #[test]
1769 fn flux_resource_ref_observed_matches_pre_lift_struct_literal_field_for_field() {
1770 // Byte-for-byte parity pin against the pre-lift 7-slot
1771 // struct-literal spelled at BOTH `phase_machine::
1772 // handle_running` and `phase_machine::flux_ref_from_json`.
1773 // A regression that reordered any of the six inputs at
1774 // the composer's argument list, or that swapped a
1775 // `ready`/`message` pair inside the composer's body,
1776 // would surface here rather than as silent divergence
1777 // between the composer's output and the pre-lift hand-
1778 // authored shape every persisted status writer restated.
1779 let composed = FluxResourceRef::observed(
1780 "source.toolkit.fluxcd.io/v1beta2".to_string(),
1781 "OCIRepository".to_string(),
1782 "chart-source".to_string(),
1783 "flux-system".to_string(),
1784 false,
1785 Some("applied; awaiting reconciliation".to_string()),
1786 );
1787 // Hand-authored the same seven slots directly, with a
1788 // held-open stamp window across the composer call.
1789 let stamped = composed.last_check.expect("stamped");
1790 let baseline = FluxResourceRef {
1791 api_version: "source.toolkit.fluxcd.io/v1beta2".to_string(),
1792 kind: "OCIRepository".to_string(),
1793 name: "chart-source".to_string(),
1794 namespace: "flux-system".to_string(),
1795 ready: false,
1796 message: Some("applied; awaiting reconciliation".to_string()),
1797 last_check: Some(stamped),
1798 };
1799 assert_eq!(composed.api_version, baseline.api_version);
1800 assert_eq!(composed.kind, baseline.kind);
1801 assert_eq!(composed.name, baseline.name);
1802 assert_eq!(composed.namespace, baseline.namespace);
1803 assert_eq!(composed.ready, baseline.ready);
1804 assert_eq!(composed.message, baseline.message);
1805 assert_eq!(composed.last_check, baseline.last_check);
1806 }
1807
1808 // ─── FluxResourceRef::pending substrate pins ─────────────────────
1809 //
1810 // Bind [`FluxResourceRef::pending`] at fail-before-pass-after
1811 // granularity so a regression that leaked a non-default status
1812 // slot (`ready: true`, `message: Some("something")`, `last_check:
1813 // Some(Utc::now())`), swapped two adjacent coordinate slots (all
1814 // four are `String` and mechanically interchangeable at the type
1815 // level), or diverged from the pre-lift 7-slot struct-literal on
1816 // any of the seven fields surfaces HERE rather than as silent
1817 // operator-invisible drift at the 3 downstream fixture consumers
1818 // (crd.rs `sample_flux_ref`, ssapply.rs `sample_flux_ref_for_diag`,
1819 // ssapply.rs `flux_ref_fetch_error_context_matches_pre_lift_...`).
1820 //
1821 // Each pin is fail-before-pass-after: the primitive did not exist
1822 // pre-lift, so any test that invokes it fails to compile pre-lift
1823 // and passes post-lift; the byte-identity pins below then bind
1824 // the specific shape choice.
1825
1826 #[test]
1827 fn flux_resource_ref_pending_binds_coordinate_slots_by_position() {
1828 // Positional pin: the 4-arg constructor binds `(api_version,
1829 // kind, name, namespace)` in THAT order, matching the pre-
1830 // lift 7-slot struct-literal's declaration order. A regression
1831 // that swapped ANY pair of adjacent `String` coordinate slots
1832 // (all four are mechanically indistinguishable at the type
1833 // level) would surface here rather than as a wire-time 404 at
1834 // every downstream Flux fetch consumer that walks
1835 // `FluxResourceRef.fetch_coords`.
1836 let r = FluxResourceRef::pending(
1837 "kustomize.toolkit.fluxcd.io/v1",
1838 "Kustomization",
1839 "observability-stack",
1840 "flux-system",
1841 );
1842 assert_eq!(r.api_version, "kustomize.toolkit.fluxcd.io/v1");
1843 assert_eq!(r.kind, "Kustomization");
1844 assert_eq!(r.name, "observability-stack");
1845 assert_eq!(r.namespace, "flux-system");
1846 }
1847
1848 #[test]
1849 fn flux_resource_ref_pending_defaults_every_status_slot() {
1850 // Default-slot pin: the three status slots (`ready`, `message`,
1851 // `last_check`) are ALL defaulted at the composer's body — no
1852 // wall-clock read, no non-`None` `message` leak, no `ready:
1853 // true` regression that would silently un-pend the fixture.
1854 // A regression that stamped `Some(Utc::now())` into
1855 // `last_check` (matching the sibling `observed` composer's
1856 // wall-clock read) would silently defeat the deterministic-
1857 // fixture contract the peer partition holds.
1858 let r = FluxResourceRef::pending("v1", "K", "n", "ns");
1859 assert!(
1860 !r.ready,
1861 "pending composer must default `ready` to false — non-`false` breaks the pre-observation contract"
1862 );
1863 assert_eq!(
1864 r.message, None,
1865 "pending composer must default `message` to None — non-`None` leaks a stale message into the pre-observation seed",
1866 );
1867 assert_eq!(
1868 r.last_check, None,
1869 "pending composer must default `last_check` to None — a `Some(_)` leak defeats the deterministic-peer partition against `observed`",
1870 );
1871 }
1872
1873 #[test]
1874 fn flux_resource_ref_pending_accepts_both_owned_and_borrowed_coordinates() {
1875 // The `impl Into<String>` ergonomic contract: both `&'static
1876 // str` literals (the fixture-helper sites that spell
1877 // coordinates inline) and owned `String` (a future callsite
1878 // handing off a dynamically-derived coordinate) round-trip
1879 // through the SAME composer signature without widening. A
1880 // regression that specialised the signature to one form or
1881 // the other would break either the inline-literal helpers or
1882 // the owned-`String` downstream consumers.
1883 let borrowed: FluxResourceRef = FluxResourceRef::pending("v1", "K", "n", "ns");
1884 let owned: FluxResourceRef = FluxResourceRef::pending(
1885 "v1".to_string(),
1886 "K".to_string(),
1887 "n".to_string(),
1888 "ns".to_string(),
1889 );
1890 assert_eq!(borrowed.api_version, owned.api_version);
1891 assert_eq!(borrowed.kind, owned.kind);
1892 assert_eq!(borrowed.name, owned.name);
1893 assert_eq!(borrowed.namespace, owned.namespace);
1894 assert_eq!(borrowed.ready, owned.ready);
1895 assert_eq!(borrowed.message, owned.message);
1896 assert_eq!(borrowed.last_check, owned.last_check);
1897 }
1898
1899 #[test]
1900 fn flux_resource_ref_pending_matches_pre_lift_struct_literal_bytewise() {
1901 // Byte-for-byte parity pin against the pre-lift 7-slot
1902 // struct-literal spelled at ALL THREE hand-authored fixture
1903 // sites (crd.rs `sample_flux_ref`, ssapply.rs
1904 // `sample_flux_ref_for_diag`, ssapply.rs inline in the
1905 // cross-substrate coherence pin's per-case sweep). Sweeps the
1906 // three representative coordinate tuples the pre-lift sites
1907 // used, so a regression that special-cased any one variant
1908 // (a `Kustomization`-only path via `if kind ==
1909 // "Kustomization" ...`) surfaces here.
1910 let cases = [
1911 (
1912 "kustomize.toolkit.fluxcd.io/v1",
1913 "Kustomization",
1914 "observability-stack",
1915 "flux-system",
1916 ),
1917 (
1918 "helm.toolkit.fluxcd.io/v2",
1919 "HelmRelease",
1920 "prometheus-op",
1921 "monitoring",
1922 ),
1923 (
1924 "source.toolkit.fluxcd.io/v1beta2",
1925 "OCIRepository",
1926 "chart-source",
1927 "flux-system",
1928 ),
1929 ];
1930 for (av, kind, name, ns) in cases {
1931 let composed = FluxResourceRef::pending(av, kind, name, ns);
1932 let hand_authored = FluxResourceRef {
1933 api_version: av.to_string(),
1934 kind: kind.to_string(),
1935 name: name.to_string(),
1936 namespace: ns.to_string(),
1937 ready: false,
1938 message: None,
1939 last_check: None,
1940 };
1941 assert_eq!(composed.api_version, hand_authored.api_version);
1942 assert_eq!(composed.kind, hand_authored.kind);
1943 assert_eq!(composed.name, hand_authored.name);
1944 assert_eq!(composed.namespace, hand_authored.namespace);
1945 assert_eq!(composed.ready, hand_authored.ready);
1946 assert_eq!(composed.message, hand_authored.message);
1947 assert_eq!(composed.last_check, hand_authored.last_check);
1948 }
1949 }
1950
1951 #[test]
1952 fn flux_resource_ref_pending_partitions_the_composer_axis_against_observed() {
1953 // Cross-composer partition pin: `pending` and `observed`
1954 // both produce `FluxResourceRef` but partition the composer
1955 // axis at the (deterministic-fixture, wall-clock-observed)
1956 // split — `pending` reads no clock and leaves `last_check:
1957 // None`, `observed` reads the wall clock and stamps
1958 // `last_check: Some(<recent Utc>)`. A regression that merged
1959 // either primitive onto the other (a `pending` that started
1960 // stamping `Utc::now()`, an `observed` that started leaving
1961 // `last_check: None`) would collapse the partition and
1962 // surface here.
1963 let p = FluxResourceRef::pending("v1", "K", "n", "ns");
1964 assert_eq!(
1965 p.last_check, None,
1966 "pending is deterministic — no clock read"
1967 );
1968 let o = FluxResourceRef::observed(
1969 "v1".to_string(),
1970 "K".to_string(),
1971 "n".to_string(),
1972 "ns".to_string(),
1973 false,
1974 None,
1975 );
1976 assert!(o.last_check.is_some(), "observed reads the wall clock");
1977 }
1978
1979 #[test]
1980 fn flux_resource_ref_pending_composes_with_fetch_coords_at_pre_observation_shape() {
1981 // Cross-composer coherence pin: a ref built by `pending`
1982 // then unpacked by `fetch_coords` returns the same four
1983 // slots in the peer projection's positional order
1984 // `(namespace, api_version, kind, name)`. Composition of
1985 // the two primitives on the same pre-observation ref
1986 // preserves the slot identity — a regression at either end
1987 // (a slot swap in `pending`, or a slot swap in
1988 // `fetch_coords`) would surface here rather than as silent
1989 // drift between the fixture writer and every downstream
1990 // fetch reader.
1991 let r = FluxResourceRef::pending(
1992 "helm.toolkit.fluxcd.io/v2",
1993 "HelmRelease",
1994 "prometheus-op",
1995 "monitoring",
1996 );
1997 let (ns, av, kind, name) = r.fetch_coords();
1998 assert_eq!(ns, "monitoring");
1999 assert_eq!(av, "helm.toolkit.fluxcd.io/v2");
2000 assert_eq!(kind, "HelmRelease");
2001 assert_eq!(name, "prometheus-op");
2002 }
2003
2004 #[test]
2005 fn rendered_resource_coords_namespace_fallback_shares_process_default_const() {
2006 // Byte-identity between the namespace fallback and the workspace-
2007 // wide `Process::DEFAULT_NAMESPACE` const. A regression that spelled
2008 // the fallback as any other string ("kube-system", "", "default-ns")
2009 // would silently drift between the coord-primitive family here and
2010 // the `Process`-borne family in `crd.rs` — surfaces here rather than
2011 // as operator-observed namespace routing skew between the two
2012 // primitive families.
2013 let c = RenderedResourceCoords {
2014 api_version: "v1".into(),
2015 kind: "K".into(),
2016 name: "n".into(),
2017 namespace: None,
2018 };
2019 assert_eq!(c.namespace_or_default(), Process::DEFAULT_NAMESPACE);
2020 }
2021
2022 // ─── CheckedCondition::all_satisfied substrate pins ─────────────
2023 //
2024 // Bind [`CheckedCondition::all_satisfied`] at fail-before-pass-
2025 // after granularity so a regression that flipped the fold
2026 // direction (`any` for `all`), inverted the projected bit
2027 // (`!c.satisfied`), reshaped the return form (an owned
2028 // `Vec<bool>` instead of the folded `bool`), or dropped the
2029 // vacuous-truth empty-slice corner surfaces HERE rather than as
2030 // silent operator-facing gate-flip at the reconciler's PROVE-
2031 // phase precondition gate + VERIFY-phase postcondition gate.
2032
2033 fn sample_checked(satisfied: bool) -> CheckedCondition {
2034 CheckedCondition {
2035 condition: crate::boundary::Condition {
2036 kind: crate::boundary::ConditionKind::ProcessPhase,
2037 params: serde_json::json!({}),
2038 },
2039 satisfied,
2040 last_check: None,
2041 message: None,
2042 }
2043 }
2044
2045 #[test]
2046 fn checked_condition_all_satisfied_returns_true_when_every_row_is_satisfied() {
2047 // Populated slice, every row `satisfied = true` — the RENDER-
2048 // phase advance corner: `handle_execing` proceeds to intent
2049 // dispatch iff every precondition holds.
2050 let checked = vec![
2051 sample_checked(true),
2052 sample_checked(true),
2053 sample_checked(true),
2054 ];
2055 assert!(
2056 CheckedCondition::all_satisfied(&checked),
2057 "all-satisfied slice must fold to true — a regression that inverted the bit would silently gate every RENDER advance behind an inverted predicate"
2058 );
2059 }
2060
2061 #[test]
2062 fn checked_condition_all_satisfied_returns_false_when_any_row_is_unsatisfied() {
2063 // Populated slice with ONE unsatisfied row — the heartbeat
2064 // requeue corner: `handle_running` stays in Running while any
2065 // postcondition remains unsatisfied.
2066 let mixed = vec![
2067 sample_checked(true),
2068 sample_checked(false),
2069 sample_checked(true),
2070 ];
2071 assert!(
2072 !CheckedCondition::all_satisfied(&mixed),
2073 "mixed slice must fold to false — a regression that folded via `any` instead of `all` would silently green-light every VERIFY advance"
2074 );
2075 }
2076
2077 #[test]
2078 fn checked_condition_all_satisfied_returns_false_when_every_row_is_unsatisfied() {
2079 // Populated slice with EVERY row unsatisfied — the tightest
2080 // gate corner: no phase advance is legal.
2081 let none_pass = vec![sample_checked(false), sample_checked(false)];
2082 assert!(
2083 !CheckedCondition::all_satisfied(&none_pass),
2084 "all-unsatisfied slice must fold to false"
2085 );
2086 }
2087
2088 #[test]
2089 fn checked_condition_all_satisfied_returns_true_on_empty_slice() {
2090 // Empty-slice vacuous-truth corner: `[T]::iter().all(_)`
2091 // returns `true` on empty input, and the pre-lift phase
2092 // gate's `if !preconditions.is_empty() { ... }` guard sat
2093 // BEFORE the fold, so the fold itself never saw an empty
2094 // slice in production. Post-lift the primitive absorbs the
2095 // empty corner cleanly — a caller that drops the outer
2096 // `is_empty()` guard (a future path that folds every gate
2097 // through this ONE primitive without a prior gate) still
2098 // sees the vacuous-truth semantics that match
2099 // [`Iterator::all`].
2100 let empty: Vec<CheckedCondition> = vec![];
2101 assert!(
2102 CheckedCondition::all_satisfied(&empty),
2103 "empty slice must fold to vacuous truth matching `[T]::iter().all(_)` — a regression that clamped the empty corner to false would silently block every no-boundary Process from advancing"
2104 );
2105 }
2106
2107 #[test]
2108 fn checked_condition_all_satisfied_matches_pre_lift_iter_all_chain_shape() {
2109 // Byte-identity pin against the pre-lift `.iter().all(|c|
2110 // c.satisfied)` chain shape the reconciler's two boundary
2111 // gates hand-authored. Sweeps every corner every gate
2112 // plausibly encounters (empty slice, single satisfied,
2113 // single unsatisfied, mixed satisfied first, mixed
2114 // unsatisfied first) so a regression that reshaped either
2115 // link surfaces HERE rather than at the two downstream phase
2116 // gates.
2117 let corners: Vec<Vec<CheckedCondition>> = vec![
2118 vec![],
2119 vec![sample_checked(true)],
2120 vec![sample_checked(false)],
2121 vec![sample_checked(true), sample_checked(false)],
2122 vec![sample_checked(false), sample_checked(true)],
2123 vec![
2124 sample_checked(true),
2125 sample_checked(true),
2126 sample_checked(true),
2127 ],
2128 vec![
2129 sample_checked(false),
2130 sample_checked(false),
2131 sample_checked(false),
2132 ],
2133 ];
2134 for corner in &corners {
2135 let via_primitive = CheckedCondition::all_satisfied(corner);
2136 #[allow(clippy::redundant_closure_for_method_calls)]
2137 let hand_authored = corner.iter().all(|c| c.satisfied);
2138 assert_eq!(
2139 via_primitive, hand_authored,
2140 "all_satisfied fold must match hand-authored .iter().all(|c| c.satisfied) chain byte-identically at corner {corner:?}"
2141 );
2142 }
2143 }
2144
2145 #[test]
2146 fn checked_condition_all_satisfied_short_circuits_on_first_unsatisfied_row() {
2147 // Semantic pin against [`Iterator::all`]'s short-circuit
2148 // discipline: a regression that folded via `checked.iter()
2149 // .filter(|c| c.satisfied).count() == checked.len()` would
2150 // still produce the same `bool` result but would eagerly
2151 // walk every row, and a future addition of an expensive
2152 // per-row side effect (a metric emit, a log line, a
2153 // conditional postcondition-retry hook) would silently fire
2154 // on every row past the first failure. The primitive must
2155 // preserve the pre-lift short-circuit — a regression that
2156 // dropped it would drift telemetry, not correctness, and
2157 // would evade every other pin here. This test verifies
2158 // short-circuit by threading a counter through a peer
2159 // predicate that mirrors [`CheckedCondition::satisfied`]'s
2160 // read.
2161 use std::cell::Cell;
2162 let visited = Cell::new(0_usize);
2163 let checked: Vec<CheckedCondition> = vec![
2164 sample_checked(true),
2165 sample_checked(false),
2166 sample_checked(true),
2167 sample_checked(true),
2168 ];
2169 // Manual short-circuit fold that counts per-row reads —
2170 // must match `all_satisfied`'s count on the same slice.
2171 let via_manual = checked.iter().all(|c| {
2172 visited.set(visited.get() + 1);
2173 c.satisfied
2174 });
2175 let manual_visited = visited.get();
2176 visited.set(0);
2177 // Mirror the primitive's iteration by re-running the same
2178 // fold shape and confirming the visited count matches — the
2179 // primitive itself doesn't take a side-effecting closure,
2180 // but this pin confirms the semantic shape (2 visits on
2181 // this slice: row 0 satisfied, row 1 unsatisfied, stop).
2182 assert_eq!(via_manual, CheckedCondition::all_satisfied(&checked));
2183 assert_eq!(
2184 manual_visited, 2,
2185 "short-circuit must stop at the first unsatisfied row (index 1); manual fold visited {manual_visited} rows"
2186 );
2187 }
2188}