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::crd::Process;
10use crate::json_object::ValueGetExt;
11
12/// Standard K8s Condition (shape of `metav1.Condition`).
13#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
14#[serde(rename_all = "camelCase")]
15pub struct ProcessCondition {
16 #[serde(rename = "type")]
17 pub type_: String,
18 pub status: String,
19 pub last_transition_time: DateTime<Utc>,
20 #[serde(default, skip_serializing_if = "Option::is_none")]
21 pub reason: Option<String>,
22 #[serde(default, skip_serializing_if = "Option::is_none")]
23 pub message: Option<String>,
24}
25
26impl ProcessCondition {
27 pub fn ready(reason: impl Into<String>, message: Option<String>) -> Self {
28 Self {
29 type_: "Ready".into(),
30 status: "True".into(),
31 last_transition_time: Utc::now(),
32 reason: Some(reason.into()),
33 message,
34 }
35 }
36
37 pub fn not_ready(reason: impl Into<String>, message: impl Into<String>) -> Self {
38 Self {
39 type_: "Ready".into(),
40 status: "False".into(),
41 last_transition_time: Utc::now(),
42 reason: Some(reason.into()),
43 message: Some(message.into()),
44 }
45 }
46
47 pub fn attested(root: &str) -> Self {
48 Self {
49 type_: "Attested".into(),
50 status: "True".into(),
51 last_transition_time: Utc::now(),
52 reason: Some("AttestationWritten".into()),
53 message: Some(format!("composed_root={root}")),
54 }
55 }
56}
57
58/// Reference to a FluxCD resource emitted as part of this Process.
59#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
60#[serde(rename_all = "camelCase")]
61pub struct FluxResourceRef {
62 pub api_version: String,
63 pub kind: String,
64 pub name: String,
65 pub namespace: String,
66 #[serde(default)]
67 pub ready: bool,
68 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub message: Option<String>,
70 #[serde(default, skip_serializing_if = "Option::is_none")]
71 pub last_check: Option<DateTime<Utc>>,
72}
73
74impl FluxResourceRef {
75 /// Pure typed projection of the four fetch coordinates
76 /// `(namespace, api_version, kind, name)` every consumer that
77 /// dispatches this persisted reference through kube-rs's dynamic-
78 /// object surface splats by hand pre-lift. The 4-tuple binds the
79 /// slot order at ONE typed accessor so a copy-paste at any downstream
80 /// consumer cannot swap two adjacent `&str` slots in the fetch call.
81 ///
82 /// Peer projection to
83 /// [`crate::k8s_wire_identity::K8sWireIdentity`] on the static-
84 /// identity axis: [`K8sWireIdentity`] carries a
85 /// `(&'static str, &'static str)` closed-set variant's pair for
86 /// emit-time (RENDER phase) composition; this method carries the
87 /// full `(ns, apiVersion, kind, name)` 4-slot borrow for fetch-time
88 /// (VERIFY / ATTEST-heartbeat) composition where the ref's payload
89 /// comes back off the persisted `ProcessStatus.flux_resources`
90 /// slice with owned `String`s rather than static literals. The two
91 /// primitives partition the fetch axis by whether the caller starts
92 /// from a closed-set variant (emit-time) or a persisted status
93 /// slice (fetch-time).
94 ///
95 /// Pre-lift the 5-slot `ssapply::fetch(client, &r.namespace,
96 /// &r.api_version, &r.kind, &r.name)` splat was hand-authored at
97 /// TWO sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication threshold
98 /// in `tatara-reconciler::phase_machine`:
99 /// * `handle_running` — the VERIFY-phase per-ref readiness probe
100 /// that populates the updated `FluxResourceRef` slice with
101 /// `ready` + `message` + `last_check`.
102 /// * `handle_attested` — the ATTEST-heartbeat drift detector that
103 /// short-circuits on the first non-Ready ref.
104 ///
105 /// Both sites splatted the SAME four `&r.X` field borrows in the
106 /// SAME order into raw `ssapply::fetch`. A copy-paste that swapped
107 /// two adjacent `&str` slots (`&r.api_version` and `&r.kind` are
108 /// both strings that look interchangeable to a mechanical
109 /// substitution) would silently 404 at wire time and diagnose as a
110 /// broken CRD rather than as slot skew at the callsite. Post-lift
111 /// each site names the ref ONCE and unpacks it through this ONE
112 /// projection; the slot order binds structurally at the tuple
113 /// return so a caller cannot desync one axis.
114 ///
115 /// A future addition (a case-fold normalization on the group, a
116 /// virtual-cluster prefix rewrite for multi-tenancy, a
117 /// `generateName` fallback on the name slot, a cluster-cache
118 /// short-circuit inserted between the projection and the fetch
119 /// call) lands at this ONE method and every downstream fetch
120 /// consumer inherits the upgrade mechanically — no per-site edit
121 /// at `handle_running` / `handle_attested` / any future kenshi-
122 /// runner / mirror-audit / drift-probe consumer that grows a third
123 /// consumer.
124 ///
125 /// Return-order pin lives at
126 /// [`tests::flux_resource_ref_fetch_coords_binds_slots_by_position`]
127 /// so a regression that swapped `namespace` and `api_version`
128 /// (both `String`, same type) inside the tuple constructor fails-
129 /// loudly here rather than as a silent wire-time 404 at every
130 /// downstream fetch consumer.
131 ///
132 /// Theory grounding: THEORY.md §II.1 invariant 5 (composition
133 /// preserves proofs — the 4-tuple slot order binds at ONE typed
134 /// projection so a regression across the two fields of the same
135 /// `String` type fails at the projection's positional pin rather
136 /// than at every downstream fetch consumer). THEORY.md §VI.1
137 /// (generation over composition — the 5-slot splat recurred at
138 /// two hand-authored sites past the ≥ 2 duplication trigger, and
139 /// is lifted to ONE typed borrow-projection here).
140 pub fn fetch_coords(&self) -> (&str, &str, &str, &str) {
141 (&self.namespace, &self.api_version, &self.kind, &self.name)
142 }
143
144 /// Compose a `FluxResourceRef` stamped at "observed now" — the
145 /// `last_check` slot is set to `Some(Utc::now())` at ONE substrate
146 /// owner, and the four coordinate slots + `ready` + `message`
147 /// are bound positionally so a slot-swap regression surfaces at
148 /// the constructor's positional pin rather than as silent drift
149 /// at every downstream `ProcessStatus.flux_resources` writer.
150 ///
151 /// Pre-lift the 7-slot `FluxResourceRef { …, last_check:
152 /// Some(chrono::Utc::now()) }` struct-literal was hand-authored
153 /// at TWO sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication
154 /// threshold in `tatara-reconciler::phase_machine`:
155 /// * `handle_running` — the VERIFY-phase per-ref rebuild that
156 /// restamps each polled ref with fresh `ready` + `message` +
157 /// `last_check`.
158 /// * `flux_ref_from_json` — the post-SSA initial-state seeder
159 /// that stamps a freshly-applied ref as `ready = false`,
160 /// `message = Some("applied; awaiting reconciliation")`,
161 /// `last_check = Some(Utc::now())`.
162 ///
163 /// Both sites restated the SAME seven field bindings in the
164 /// SAME order, and both restated the SAME `Some(chrono::Utc::
165 /// now())` stamp. A copy-paste that swapped two adjacent
166 /// `String` slots (`api_version` and `kind`, `kind` and `name`,
167 /// or `name` and `namespace` are all mechanically
168 /// indistinguishable at the type level) would silently persist
169 /// a slot-inverted ref that the downstream Flux fetch consumer
170 /// (via [`Self::fetch_coords`]) would then 404 on. Post-lift
171 /// both sites name the six inputs ONCE and route through this
172 /// ONE composer; the seventh slot (`last_check`) is stamped at
173 /// the composer's body so a future injection point (a fake
174 /// clock for testing, a monotonic-clock cross-check, a per-
175 /// fleet skew tolerance) lands at ONE substrate site rather
176 /// than at every hand-authored `Some(chrono::Utc::now())` stamp.
177 ///
178 /// Return-order pin lives at
179 /// [`tests::flux_resource_ref_observed_binds_slots_by_position`]
180 /// so a regression that swapped `api_version` and `kind` (both
181 /// `String`, same type) inside the constructor's argument list
182 /// fails-loudly here rather than as a silent wire-time 404 at
183 /// every downstream fetch consumer.
184 ///
185 /// Theory grounding: THEORY.md §II.1 invariant 5 (composition
186 /// preserves proofs — the 6-slot positional binding + the
187 /// `last_check` stamp compose at ONE typed owner, so a
188 /// regression across the four `String` coordinate slots fails
189 /// at the composer's positional pin rather than at every
190 /// downstream Flux status writer). THEORY.md §VI.1 (generation
191 /// over composition — the 7-slot struct-literal recurred at two
192 /// hand-authored sites past the ≥ 2 duplication trigger, and is
193 /// lifted to ONE typed composer here).
194 pub fn observed(
195 api_version: String,
196 kind: String,
197 name: String,
198 namespace: String,
199 ready: bool,
200 message: Option<String>,
201 ) -> Self {
202 Self {
203 api_version,
204 kind,
205 name,
206 namespace,
207 ready,
208 message,
209 last_check: Some(Utc::now()),
210 }
211 }
212
213 /// Compose a `FluxResourceRef` in the pre-observation shape — the
214 /// 4-slot coordinate binding with the three status slots defaulted
215 /// (`ready: false`, `message: None`, `last_check: None`). The
216 /// deterministic-fixture peer of [`Self::observed`] on the same
217 /// `→ FluxResourceRef` composer axis: `observed` reads the wall
218 /// clock and takes 6 args (a live post-fetch stamp), `pending`
219 /// reads no clock and takes 4 args (a pre-observation fixture
220 /// seed, and the natural base for `..base.clone()` spread updates
221 /// that vary a single slot for a per-corner test sweep).
222 ///
223 /// Pre-lift the SAME 7-slot `FluxResourceRef { api_version, kind,
224 /// name, namespace, ready: false, message: None, last_check: None
225 /// }` struct-literal was hand-authored at THREE workspace-wide
226 /// fixture sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication
227 /// threshold:
228 ///
229 /// * [`crate::crd`]
230 /// `crd::observed_flux_resources_tests::sample_flux_ref(name)`
231 /// — the shared `Kustomization`/`flux-system` fixture the
232 /// `Process::observed_flux_resources` pin family destructures
233 /// for its `flux_resources`-populated corners.
234 /// * `tatara-reconciler::ssapply::tests::sample_flux_ref_for_diag`
235 /// — the `HelmRelease`/`flux-system` fixture the
236 /// `flux_ref_fetch_error_context` diagnostic-wording pin
237 /// family destructures for its (kind, name) slot-coverage
238 /// sweep.
239 /// * `tatara-reconciler::ssapply::tests::
240 /// flux_ref_fetch_error_context_matches_pre_lift_hand_authored_wording`
241 /// — the inline 7-slot literal inside the cross-substrate
242 /// coherence pin's per-case sweep over three distinct
243 /// `(api_version, kind, name, namespace)` tuples.
244 ///
245 /// All THREE sites restated the SAME seven field bindings in the
246 /// SAME order and the SAME three defaulted status slots (`ready:
247 /// false, message: None, last_check: None`), differing only in
248 /// the four coordinate `String` values. Post-lift each callsite
249 /// reads `FluxResourceRef::pending(<api_version>, <kind>, <name>,
250 /// <namespace>)` and the four-slot bind + three-slot default
251 /// sinks live at ONE substrate owner.
252 ///
253 /// The `impl Into<String>` signature accepts BOTH `&'static str`
254 /// (the fixture-helper sites that spell coordinate literals
255 /// inline) AND owned `String` (a future callsite handing off a
256 /// dynamically-derived coordinate) without widening. Matches the
257 /// discipline of the sibling substrate composers
258 /// [`crate::pool::PoolMember::unallocated`] +
259 /// [`crate::allocation::AllocationRef::new`] on the identity-slot
260 /// axis.
261 ///
262 /// A future normalization (a case-fold on the group, a
263 /// virtual-cluster prefix rewrite for multi-tenancy, a stricter
264 /// kind gate, a `generateName` fallback on the name slot, a
265 /// canonical rename of one of the three defaulted status slots
266 /// to a typed `PreObservation` marker) lands at THIS ONE
267 /// substrate primitive and every downstream fixture / helper
268 /// inherits the upgrade mechanically — no per-site edit at any
269 /// of the THREE listed callers or at future consumers (a
270 /// stable-name claim-arbiter's pending-ref seed, a kenshi-runner
271 /// pre-observation fixture, a mirror-audit drift-probe test
272 /// helper).
273 ///
274 /// Theory grounding: THEORY.md §II.1 invariant 5 (composition
275 /// preserves proofs — the 4-slot positional binding + the three
276 /// defaulted status slots compose at ONE typed owner, so a
277 /// regression across the four `String` coordinate slots fails at
278 /// the composer's positional pin rather than at every downstream
279 /// fixture consumer). THEORY.md §VI.1 (generation over
280 /// composition — the 7-slot struct-literal recurred at three
281 /// hand-authored fixture sites past the ≥ 2 duplication trigger,
282 /// and is lifted to ONE typed composer here).
283 #[must_use]
284 pub fn pending(
285 api_version: impl Into<String>,
286 kind: impl Into<String>,
287 name: impl Into<String>,
288 namespace: impl Into<String>,
289 ) -> Self {
290 Self {
291 api_version: api_version.into(),
292 kind: kind.into(),
293 name: name.into(),
294 namespace: namespace.into(),
295 ready: false,
296 message: None,
297 last_check: None,
298 }
299 }
300}
301
302/// Identifying coordinates of a rendered K8s resource — the
303/// `(apiVersion, kind, metadata.name, metadata.namespace)` 4-tuple
304/// every consumer that walks a rendered `serde_json::Value` resource
305/// unwraps by hand pre-lift.
306///
307/// The three K8s API-path segments (`apiVersion`, `kind`,
308/// `metadata.name`) are REQUIRED — a rendered resource missing any
309/// of them cannot be applied via kube-rs's dynamic API surface, so
310/// the extraction fails fast at the boundary rather than as a
311/// downstream `Api::patch` panic. `metadata.namespace` is
312/// intentionally kept as `Option<String>` because different consumers
313/// resolve the fallback differently: `apply_owned` uses the
314/// caller-supplied `namespace: &str` argument (the reconciler already
315/// resolved the target namespace upstream), while `flux_ref_from_json`
316/// records the K8s canonical `"default"` fallback into the persisted
317/// `FluxResourceRef.namespace` slot. The peer method
318/// [`Self::namespace_or_default`] applies the K8s canonical fallback
319/// (`Process::DEFAULT_NAMESPACE = "default"`) for consumers wanting
320/// the same shape [`FluxResourceRef.namespace`] carries.
321///
322/// Pre-lift the 3+1 slot extraction was hand-authored at TWO sites
323/// past the ★★ PRIME-DIRECTIVE ≥ 2 duplication threshold across
324/// `tatara-reconciler`:
325/// * `tatara-reconciler::phase_machine::flux_ref_from_json` — the
326/// post-SSA `FluxResourceRef` builder that persists into
327/// `ProcessStatus.flux_resources`; namespace half fallback-
328/// defaulted to `"default"`.
329/// * `tatara-reconciler::ssapply::apply_owned` — the SSA entry
330/// point that extracts (apiVersion, kind, name) for the
331/// [`kube::Api::patch`] call; namespace half discarded (the
332/// `namespace: &str` argument comes from the caller upstream).
333///
334/// Both callsites restated the same three
335/// `.get(K).and_then(|v| v.as_str()).ok_or_else(|| anyhow!(...))?
336/// .to_string()` incantations with subtly different error wording
337/// (`"resource missing X"` vs `"rendered resource missing X"`); post-
338/// lift both route through this ONE substrate owner with the
339/// canonical `"rendered resource missing X"` wording. A future
340/// addition (case-fold on the group, a rename of the namespace
341/// fallback, a stricter kind gate, a Unicode-safe collation step,
342/// support for `metadata.generateName` as a name fallback) lands at
343/// the primitive's body on the substrate, not at 2 independent
344/// hand-writes across 2 reconciler files.
345///
346/// Namespace fallback const is shared with
347/// [`Process::DEFAULT_NAMESPACE`] — a rename of the K8s canonical
348/// default namespace lands at that ONE workspace-wide const, not at
349/// per-primitive local literals that would drift silently.
350#[derive(Clone, Debug, PartialEq, Eq)]
351pub struct RenderedResourceCoords {
352 /// `apiVersion` — the group+version pair kube-rs uses to resolve
353 /// the `ApiResource` for the SSA call.
354 pub api_version: String,
355 /// `kind` — the resource kind (Kustomization, HelmRelease, …).
356 pub kind: String,
357 /// `metadata.name` — the API-path leaf segment.
358 pub name: String,
359 /// `metadata.namespace` — raw from the resource, `None` when the
360 /// slot is absent (a cluster-scoped resource, or a namespaced
361 /// resource whose namespace was left for the API server to
362 /// substitute). Consumers apply their own fallback:
363 /// [`Self::namespace_or_default`] applies the K8s canonical
364 /// `"default"` (matching what [`FluxResourceRef.namespace`]
365 /// records); other consumers substitute a caller-supplied string
366 /// (see `tatara-reconciler::ssapply::apply_owned`).
367 pub namespace: Option<String>,
368}
369
370impl RenderedResourceCoords {
371 /// Extract the 4-tuple from a rendered K8s resource JSON `Value`.
372 ///
373 /// Fails with a canonical `"rendered resource missing X"` message
374 /// when any of the three required slots (`apiVersion`, `kind`,
375 /// `metadata.name`) is absent or non-string; `metadata.namespace`
376 /// is optional and captured as `None` when absent.
377 ///
378 /// The error wording is pinned by
379 /// [`tests::rendered_resource_coords_error_wording_is_canonical`]
380 /// so a regression that reshaped the message surfaces at the test
381 /// surface rather than as silent drift between the two pre-lift
382 /// call sites (which used subtly different wording — `"resource
383 /// missing X"` in `apply_owned` vs `"rendered resource missing
384 /// X"` in `flux_ref_from_json`).
385 pub fn from_json(res: &Value) -> anyhow::Result<Self> {
386 // The three REQUIRED-slot extracts (`apiVersion`, `kind`,
387 // `metadata.name`) route through the ONE substrate primitive
388 // `Self::required_str` — the required-extract sibling of
389 // `crate::json_object::ValueGetExt::get_str` on the same
390 // rendered-resource axis. A future normalization (Unicode
391 // NFC-fold, whitespace trim, empty-string rejection) lands
392 // at the primitive body and every downstream consumer of the
393 // canonical `"rendered resource missing X"` wire form inherits
394 // it mechanically. The optional `metadata.namespace` slot
395 // continues to route through the pre-existing `get_str` READ
396 // primitive since its absent-arm is `None`, not an error.
397 let api_version = Self::required_str(Some(res), "apiVersion", "apiVersion")?;
398 let kind = Self::required_str(Some(res), "kind", "kind")?;
399 let metadata = res.get("metadata");
400 let name = Self::required_str(metadata, "name", "metadata.name")?;
401 let namespace = metadata
402 .and_then(|m| m.get_str("namespace"))
403 .map(str::to_string);
404 Ok(Self {
405 api_version,
406 kind,
407 name,
408 namespace,
409 })
410 }
411
412 /// Diagnostic prefix stamped ahead of every required-slot label in
413 /// the canonical error wire form. Owned in ONE workspace-wide place
414 /// so a rename (a fleet-wide switch to `"resource is missing"` /
415 /// `"missing rendered-resource field"`) lands here and every
416 /// downstream `.to_string()`-consumer + operator-facing log grep
417 /// inherits the rename mechanically, not at 3 hand-authored
418 /// `anyhow!(…)` restatements.
419 pub const MISSING_MESSAGE_PREFIX: &'static str = "rendered resource missing";
420
421 /// Required-slot extract on a rendered-resource JSON `Value` — the
422 /// substrate owner of the paired `.get_str(<key>).ok_or_else(||
423 /// anyhow!("rendered resource missing <slot>"))?.to_string()`
424 /// four-link chain every REQUIRED slot on a rendered `Value`
425 /// walks pre-lift.
426 ///
427 /// The primitive accepts an `Option<&Value>` receiver so BOTH
428 /// shallow reads (top-level `apiVersion` / `kind` on the resource
429 /// root, callers thread `Some(res)`) AND one-level-nested reads
430 /// (`metadata.name` walking through `res.get("metadata")`,
431 /// callers thread the `Option<&Value>` handle the `.get()` step
432 /// returns) reach the same owner. The `key` slot is the wire-form
433 /// name the underlying [`ValueGetExt::get_str`] looks up on the
434 /// object; the `error_slot` slot is the diagnostic label stamped
435 /// into the error's `Display` output. The two are decoupled so
436 /// `metadata.name` can look up `"name"` on the `metadata` sub-
437 /// object while reporting the dotted `"metadata.name"` path an
438 /// operator bisecting a fault sees in the log.
439 ///
440 /// Ok arm returns `String` (owned) rather than the borrowed
441 /// `&str` [`ValueGetExt::get_str`] returns — every downstream
442 /// slot on the [`RenderedResourceCoords`] struct is an owned
443 /// `String`, so the primitive absorbs the `str::to_string`
444 /// coerce that pre-lift lived at three hand-authored callsites.
445 /// Err arm carries an `anyhow::Error` whose `Display` reads
446 /// exactly `"<Self::MISSING_MESSAGE_PREFIX> <error_slot>"` —
447 /// byte-identical to the pre-lift hand-authored `anyhow!(
448 /// "rendered resource missing {slot}")` wire form.
449 ///
450 /// ### Fires on all four absent-shape corners
451 ///
452 /// The primitive returns `Err` on ALL four ways a required
453 /// slot can miss:
454 ///
455 /// 1. Receiver is `None` — the `metadata.name` corner when the
456 /// top-level `metadata` object itself is absent (the caller
457 /// threaded `res.get("metadata")` which returned `None`).
458 /// 2. Slot is absent — the receiver is present but does not
459 /// carry a value at `key`.
460 /// 3. Slot is present but non-string — a fixture bug that
461 /// stamped a JSON number / bool / object / array at the
462 /// slot; the `get_str` step falls through and the primitive
463 /// reports the slot as missing (matching the pre-lift
464 /// behavior where every non-string variant surfaced as the
465 /// same `"missing"` diagnostic — pinning "cannot be applied
466 /// via kube-rs's dynamic API surface" as the shared
467 /// failure mode).
468 /// 4. Receiver is non-object — a resource authored as a JSON
469 /// array / string / null at any of the levels the primitive
470 /// walks (the `get_str` step returns `None` verbatim).
471 ///
472 /// All four corners produce the SAME wire form so an operator's
473 /// `rg "rendered resource missing"` sweep hits exactly one
474 /// footprint per faulted slot, not four differently-worded
475 /// diagnostics per absent-shape variant.
476 ///
477 /// Theory anchor: THEORY.md §VI.1 (generation over composition —
478 /// the 4-link `.get_str(<key>).ok_or_else(|| anyhow!("rendered
479 /// resource missing <slot>"))?.to_string()` shape recurred at 3
480 /// hand-authored sites past the ★★ PRIME-DIRECTIVE ≥ 2
481 /// duplication trigger, and is lifted to ONE substrate owner
482 /// here). THEORY.md §II.1 invariant 5 (composition preserves
483 /// proofs — a regression that drifted the diagnostic prefix
484 /// wording at ONE site would silently pass the two sibling
485 /// pins and fail HERE; post-lift the wire form is owned once
486 /// at [`Self::MISSING_MESSAGE_PREFIX`] and every downstream
487 /// composition inherits the rename mechanically).
488 fn required_str(
489 v: Option<&Value>,
490 key: &'static str,
491 error_slot: &'static str,
492 ) -> anyhow::Result<String> {
493 v.and_then(|x| x.get_str(key))
494 .map(str::to_string)
495 .ok_or_else(|| {
496 anyhow::anyhow!(
497 "{prefix} {slot}",
498 prefix = Self::MISSING_MESSAGE_PREFIX,
499 slot = error_slot,
500 )
501 })
502 }
503
504 /// `metadata.namespace` slice with the K8s canonical `"default"`
505 /// fallback applied — matching what [`Process::DEFAULT_NAMESPACE`]
506 /// spells for the `Process`-borne coordinate primitive family
507 /// and what [`FluxResourceRef.namespace`] records into
508 /// `ProcessStatus.flux_resources`.
509 pub fn namespace_or_default(&self) -> &str {
510 self.namespace
511 .as_deref()
512 .unwrap_or(Process::DEFAULT_NAMESPACE)
513 }
514}
515
516/// A boundary condition paired with its current satisfaction state.
517#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
518#[serde(rename_all = "camelCase")]
519pub struct CheckedCondition {
520 #[serde(flatten)]
521 pub condition: Condition,
522 pub satisfied: bool,
523 #[serde(default, skip_serializing_if = "Option::is_none")]
524 pub last_check: Option<DateTime<Utc>>,
525 #[serde(default, skip_serializing_if = "Option::is_none")]
526 pub message: Option<String>,
527}
528
529impl CheckedCondition {
530 /// True iff every [`CheckedCondition`] in the slice has
531 /// `satisfied == true` — the ONE-line collapse of the paired
532 /// `checked.iter().all(|c| c.satisfied)` incantation the
533 /// reconciler's precondition + postcondition boundary gates both
534 /// spelled by hand pre-lift.
535 ///
536 /// Pre-lift the SAME `.iter().all(|c| c.satisfied)` chain was
537 /// hand-authored at TWO sites past the ★★ PRIME-DIRECTIVE ≥ 2
538 /// duplication threshold in `tatara-reconciler::phase_machine`,
539 /// each walking the SAME `Vec<CheckedCondition>` → `bool`
540 /// projection to gate a phase transition on a boundary predicate:
541 /// * `handle_execing` — the PROVE-phase precondition gate that
542 /// stays in Execing (heartbeat requeue) while any precondition
543 /// remains unsatisfied and proceeds to RENDER only when every
544 /// precondition holds.
545 /// * `handle_running` — the VERIFY-phase postcondition gate that
546 /// stays in Running (heartbeat requeue) while any postcondition
547 /// remains unsatisfied and advances to Attested only when every
548 /// postcondition holds.
549 ///
550 /// Both sites walked the SAME `Iterator::all` short-circuit on the
551 /// SAME `bool` slot of the SAME struct. Post-lift both consumers
552 /// name the slice ONCE and route through this ONE primitive; the
553 /// vacuous-truth corner (empty slice → `true`, matching
554 /// [`Iterator::all`]'s empty-input identity) sits at ONE substrate
555 /// site so a future normalization (a per-slot weight overlay, a
556 /// per-kind override that treats `Warn`-severity failures as
557 /// satisfied, a compliance-baseline gate that requires N-of-M
558 /// rather than all-of-M) lands at ONE substrate function and both
559 /// downstream phase gates inherit the upgrade mechanically.
560 ///
561 /// Return-form axis: `bool` — the exact type each phase gate
562 /// pre-lift bound at `let all_pass = <chain>;` and immediately
563 /// consumed in a `!all_pass` short-circuit + a `message` slot's
564 /// ternary branch. The `&[Self]` argument accepts every pre-lift
565 /// slice provenance verbatim: a `&Vec<CheckedCondition>` (both
566 /// pre-lift sites had the `Vec` on the stack from
567 /// [`crate::phase_machine::evaluate_conditions`]'s owned return)
568 /// coerces through auto-deref, so no callsite has to change its
569 /// upstream provenance to route through the primitive.
570 ///
571 /// Peer to the sibling projection [`Self::satisfied`] on the (row
572 /// scope × predicate) axis pair: `satisfied` is the per-row
573 /// projection; `all_satisfied` is the slice-wide fold of the same
574 /// bit. Both live on `CheckedCondition` so a future rename or
575 /// per-slot normalization travels through the same owner without
576 /// splitting between "per-row" and "slice-wide" call sinks.
577 ///
578 /// Return-shape pin lives at
579 /// [`tests::checked_condition_all_satisfied_matches_pre_lift_iter_all_chain_shape`]
580 /// so a regression that flipped the fold direction (`any` for
581 /// `all`), inverted the bit (`!c.satisfied`), or reshaped the
582 /// return form (an owned `Vec<bool>` instead of the folded `bool`)
583 /// fails-loudly here rather than as silent operator-facing skew
584 /// between the pre-lift `if !all_pass { requeue }` gate and the
585 /// post-lift call — every downstream consumer would still
586 /// short-circuit but on inverted semantics.
587 ///
588 /// Theory grounding: THEORY.md §VI.1 (generation over composition
589 /// — the 1-line `.iter().all(...)` chain recurred at two hand-
590 /// authored sites past the ≥ 2 duplication trigger, and is lifted
591 /// to ONE typed fold here). THEORY.md §II.1 invariant 5
592 /// (composition preserves proofs — the empty-slice vacuous-truth
593 /// corner + the fold direction + the projected bit's polarity all
594 /// bind at ONE substrate site, so a regression across any of the
595 /// three surfaces at [`tests::checked_condition_all_satisfied_*`]
596 /// pin rather than as silent gate-flip at every downstream phase
597 /// handler).
598 #[must_use]
599 pub fn all_satisfied(checked: &[Self]) -> bool {
600 checked.iter().all(|c| c.satisfied)
601 }
602}
603
604/// Summary of boundary verification.
605#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
606#[serde(rename_all = "camelCase")]
607pub struct BoundaryStatus {
608 #[serde(default)]
609 pub preconditions: Vec<CheckedCondition>,
610 #[serde(default)]
611 pub postconditions: Vec<CheckedCondition>,
612 /// Absolute deadline for VERIFY (derived from `spec.boundary.timeout`).
613 #[serde(default, skip_serializing_if = "Option::is_none")]
614 pub deadline: Option<DateTime<Utc>>,
615}
616
617/// Summary of compliance checks at the latest attestation.
618#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
619#[serde(rename_all = "camelCase")]
620pub struct ComplianceStatus {
621 #[serde(default, skip_serializing_if = "Option::is_none")]
622 pub baseline: Option<String>,
623 pub satisfied: u32,
624 pub violated: u32,
625 pub total: u32,
626 #[serde(default)]
627 pub violations: Vec<String>,
628}
629
630#[cfg(test)]
631mod tests {
632 use super::*;
633 use serde_json::json;
634
635 // ─── RenderedResourceCoords substrate pins ──────────────────────
636
637 #[test]
638 fn rendered_resource_coords_from_json_extracts_all_four_slots_when_present() {
639 let res = json!({
640 "apiVersion": "kustomize.toolkit.fluxcd.io/v1",
641 "kind": "Kustomization",
642 "metadata": {
643 "name": "observability-stack",
644 "namespace": "flux-system",
645 },
646 });
647 let c = RenderedResourceCoords::from_json(&res).expect("extract");
648 assert_eq!(c.api_version, "kustomize.toolkit.fluxcd.io/v1");
649 assert_eq!(c.kind, "Kustomization");
650 assert_eq!(c.name, "observability-stack");
651 assert_eq!(c.namespace.as_deref(), Some("flux-system"));
652 }
653
654 #[test]
655 fn rendered_resource_coords_from_json_captures_absent_namespace_as_none() {
656 // Cluster-scoped resource — `metadata.namespace` intentionally absent.
657 let res = json!({
658 "apiVersion": "v1",
659 "kind": "Namespace",
660 "metadata": {"name": "demo-test"},
661 });
662 let c = RenderedResourceCoords::from_json(&res).expect("extract");
663 assert_eq!(c.namespace, None);
664 assert_eq!(c.name, "demo-test");
665 }
666
667 #[test]
668 fn rendered_resource_coords_from_json_errors_on_missing_api_version() {
669 let res = json!({"kind": "K", "metadata": {"name": "n"}});
670 let e = RenderedResourceCoords::from_json(&res).expect_err("must error");
671 assert_eq!(e.to_string(), "rendered resource missing apiVersion");
672 }
673
674 #[test]
675 fn rendered_resource_coords_from_json_errors_on_missing_kind() {
676 let res = json!({"apiVersion": "v1", "metadata": {"name": "n"}});
677 let e = RenderedResourceCoords::from_json(&res).expect_err("must error");
678 assert_eq!(e.to_string(), "rendered resource missing kind");
679 }
680
681 #[test]
682 fn rendered_resource_coords_from_json_errors_on_missing_metadata_name() {
683 let res = json!({"apiVersion": "v1", "kind": "K", "metadata": {}});
684 let e = RenderedResourceCoords::from_json(&res).expect_err("must error");
685 assert_eq!(e.to_string(), "rendered resource missing metadata.name");
686 }
687
688 #[test]
689 fn rendered_resource_coords_from_json_errors_on_missing_metadata_object() {
690 // `metadata` absent entirely — same failure as `metadata.name` missing,
691 // because the API-path leaf segment cannot be resolved.
692 let res = json!({"apiVersion": "v1", "kind": "K"});
693 let e = RenderedResourceCoords::from_json(&res).expect_err("must error");
694 assert_eq!(e.to_string(), "rendered resource missing metadata.name");
695 }
696
697 #[test]
698 fn rendered_resource_coords_from_json_errors_on_non_string_slot() {
699 // A numeric `apiVersion` slot falls through the `.as_str()` gate and
700 // triggers the same missing-slot failure as absence — the API-path
701 // segment is not a string.
702 let res = json!({
703 "apiVersion": 42,
704 "kind": "K",
705 "metadata": {"name": "n"},
706 });
707 let e = RenderedResourceCoords::from_json(&res).expect_err("must error");
708 assert_eq!(e.to_string(), "rendered resource missing apiVersion");
709 }
710
711 #[test]
712 fn rendered_resource_coords_error_wording_is_canonical() {
713 // Pins the exact spelling every downstream consumer sees.
714 // Pre-lift wording differed across the two call sites (`"resource
715 // missing X"` in `apply_owned` vs `"rendered resource missing X"` in
716 // `flux_ref_from_json`); post-lift the canonical wording is
717 // `"rendered resource missing X"` at every site.
718 let cases = [
719 (
720 "apiVersion",
721 json!({"kind": "K", "metadata": {"name": "n"}}),
722 ),
723 (
724 "kind",
725 json!({"apiVersion": "v1", "metadata": {"name": "n"}}),
726 ),
727 (
728 "metadata.name",
729 json!({"apiVersion": "v1", "kind": "K", "metadata": {}}),
730 ),
731 ];
732 for (slot, res) in cases {
733 let e = RenderedResourceCoords::from_json(&res).expect_err("must error");
734 assert_eq!(
735 e.to_string(),
736 format!("rendered resource missing {slot}"),
737 "slot {slot} error must be canonical"
738 );
739 }
740 }
741
742 // ─── RenderedResourceCoords::required_str substrate pins ────────
743 //
744 // Fail-before-pass-after granularity: the
745 // `RenderedResourceCoords::required_str` inherent associated
746 // function did not exist before this commit, so each test below
747 // fails to compile pre-lift. Post-lift they collectively pin the
748 // required-string-extract shape at ONE substrate owner — a
749 // regression that swaps `MISSING_MESSAGE_PREFIX`, decouples the
750 // `key` / `error_slot` slot pair with a wrong ordering, drops the
751 // `str::to_string` coerce (returning `&str` and forcing every
752 // consumer to re-stamp `.to_string()` per site), or narrows the
753 // receiver from `Option<&Value>` to `&Value` (silently breaking
754 // the `metadata.name` corner where the caller threads the
755 // `res.get("metadata")` result directly) surfaces HERE rather
756 // than as silent operator-facing skew across the three pre-lift
757 // consumers on `from_json`.
758
759 #[test]
760 fn required_str_present_string_slot_returns_owned_string() {
761 // Ok-arm invariant: a present string slot at `key` on a
762 // `Some(&Value::Object)` receiver returns `Ok(<owned>)` —
763 // the primitive absorbs the `.to_string()` coerce the three
764 // pre-lift restatements each stamped at the tail.
765 let res = json!({"apiVersion": "kustomize.toolkit.fluxcd.io/v1"});
766 let got =
767 RenderedResourceCoords::required_str(Some(&res), "apiVersion", "apiVersion").unwrap();
768 assert_eq!(got, "kustomize.toolkit.fluxcd.io/v1");
769 }
770
771 #[test]
772 fn required_str_none_receiver_errors_with_canonical_wire_form() {
773 // Absent-shape corner 1: the caller threads `None`
774 // (`res.get("metadata")` returned `None` because the top-
775 // level `metadata` slot itself is absent). The primitive
776 // errors with the SAME wire form the two other absent
777 // corners produce, keeping the operator-facing footprint
778 // singular.
779 let e = RenderedResourceCoords::required_str(None, "name", "metadata.name")
780 .expect_err("None receiver must error");
781 assert_eq!(e.to_string(), "rendered resource missing metadata.name");
782 }
783
784 #[test]
785 fn required_str_absent_slot_errors_with_canonical_wire_form() {
786 // Absent-shape corner 2: the receiver is present but the
787 // slot at `key` is not stamped on it. Wire form matches
788 // the `None`-receiver corner and the non-string corner.
789 let res = json!({"kind": "K"});
790 let e = RenderedResourceCoords::required_str(Some(&res), "apiVersion", "apiVersion")
791 .expect_err("absent slot must error");
792 assert_eq!(e.to_string(), "rendered resource missing apiVersion");
793 }
794
795 #[test]
796 fn required_str_non_string_slot_errors_with_canonical_wire_form() {
797 // Absent-shape corner 3: the slot is present but stamped
798 // as a JSON number / bool / object / array — every
799 // non-`Value::String` variant falls through the underlying
800 // `get_str` gate and produces the SAME `"missing"` diagnostic.
801 // Pinning EVERY non-string variant here (not just number)
802 // guarantees an operator's error-stream grep collapses all
803 // fixture-authoring bugs at this slot onto one footprint.
804 for bad in [
805 json!({"apiVersion": 42}),
806 json!({"apiVersion": true}),
807 json!({"apiVersion": {}}),
808 json!({"apiVersion": [1]}),
809 json!({"apiVersion": null}),
810 ] {
811 let e = RenderedResourceCoords::required_str(Some(&bad), "apiVersion", "apiVersion")
812 .expect_err("non-string slot must error");
813 assert_eq!(e.to_string(), "rendered resource missing apiVersion");
814 }
815 }
816
817 #[test]
818 fn required_str_non_object_receiver_errors_with_canonical_wire_form() {
819 // Absent-shape corner 4: the receiver itself is not a
820 // `Value::Object` — a resource authored as a JSON array,
821 // string, or null at any of the levels the primitive
822 // walks. The underlying `get_str` step returns `None`
823 // verbatim (matching the pre-lift chain's own behavior)
824 // and the primitive stamps the canonical wire form.
825 for bad in [json!([1, 2, 3]), json!("stringified"), Value::Null] {
826 let e = RenderedResourceCoords::required_str(Some(&bad), "name", "metadata.name")
827 .expect_err("non-object receiver must error");
828 assert_eq!(e.to_string(), "rendered resource missing metadata.name");
829 }
830 }
831
832 #[test]
833 fn required_str_decouples_key_from_error_slot_at_metadata_name_shape() {
834 // Slot-decoupling pin: for the `metadata.name` corner the
835 // primitive looks up `key = "name"` on the metadata sub-
836 // object while stamping `error_slot = "metadata.name"` into
837 // the error's `Display` output — the two are NOT the same
838 // string, and a regression that collapsed them (using
839 // `key` for both the lookup AND the error slug, or
840 // vice-versa) would silently pass the shallow `apiVersion`
841 // / `kind` pins above (where `key == error_slot`) and fail
842 // HERE. Present-arm: lookup succeeds on the metadata sub-
843 // object's `name` slot, returns the owned string.
844 let res = json!({"metadata": {"name": "demo"}});
845 let metadata = res.get("metadata");
846 let got = RenderedResourceCoords::required_str(metadata, "name", "metadata.name").unwrap();
847 assert_eq!(got, "demo");
848 // Absent-arm: same slot-decoupling but the `name` sub-slot
849 // is absent — the error slug is the DOTTED path, not the
850 // shallow `"name"` key.
851 let res_no_name = json!({"metadata": {}});
852 let metadata_empty = res_no_name.get("metadata");
853 let e = RenderedResourceCoords::required_str(metadata_empty, "name", "metadata.name")
854 .expect_err("absent metadata.name must error");
855 assert_eq!(e.to_string(), "rendered resource missing metadata.name");
856 }
857
858 #[test]
859 fn required_str_error_wire_form_composes_missing_message_prefix_verbatim() {
860 // Wire-form composition pin: the error's `Display` is
861 // exactly `"<Self::MISSING_MESSAGE_PREFIX> <error_slot>"` —
862 // the leading prefix comes from the `const` owner + a
863 // single space + the caller-supplied slug. A regression
864 // that switched the separator (a colon, an em-dash) or
865 // dropped the prefix (returning just the slot slug) would
866 // silently invert every operator-facing log grep footprint;
867 // this pin binds the composition to the ONE prefix const
868 // so a future rename lands atomically at both the source
869 // and the pins.
870 let e = RenderedResourceCoords::required_str(None, "name", "metadata.name")
871 .expect_err("None receiver must error");
872 let expected = format!(
873 "{prefix} metadata.name",
874 prefix = RenderedResourceCoords::MISSING_MESSAGE_PREFIX,
875 );
876 assert_eq!(e.to_string(), expected);
877 }
878
879 #[test]
880 fn required_str_shape_parity_matches_pre_lift_hand_authored_chain_bytewise() {
881 // Byte-shape parity pin: on every corner (present, absent,
882 // non-string, non-object, None-receiver) the primitive's
883 // output MUST match the pre-lift hand-authored
884 // `.get_str(<key>).ok_or_else(|| anyhow!("rendered resource
885 // missing <slot>"))?.to_string()` chain bytewise — the
886 // Ok-arm string equals the raw `get_str` slice as an owned
887 // `String`, and the Err-arm `Display` equals the pre-lift
888 // `anyhow!(...)` output verbatim. A regression that inserted
889 // a normalization (a trim, an NFC-fold) into the Ok arm or
890 // altered the diagnostic wrapping in the Err arm surfaces
891 // HERE rather than as silent per-consumer schema drift.
892 let cases: &[(Value, &'static str, &'static str)] = &[
893 (json!({"apiVersion": "v1"}), "apiVersion", "apiVersion"),
894 (json!({"kind": "K"}), "kind", "kind"),
895 ];
896 for (res, key, error_slot) in cases {
897 let via_primitive =
898 RenderedResourceCoords::required_str(Some(res), key, error_slot).unwrap();
899 let via_pre_lift = res.get_str(key).unwrap().to_string();
900 assert_eq!(via_primitive, via_pre_lift);
901 }
902 let empty = json!({"other": "value"});
903 let err_via_primitive =
904 RenderedResourceCoords::required_str(Some(&empty), "apiVersion", "apiVersion")
905 .expect_err("absent slot must error");
906 let err_via_pre_lift = anyhow::anyhow!("rendered resource missing apiVersion");
907 assert_eq!(err_via_primitive.to_string(), err_via_pre_lift.to_string());
908 }
909
910 #[test]
911 fn required_str_missing_message_prefix_matches_pre_lift_wire_form_verbatim() {
912 // Const-owner pin: the pre-lift hand-authored `anyhow!("rendered
913 // resource missing X")` restatements each embedded the leading
914 // `"rendered resource missing"` prefix as an inline literal.
915 // Post-lift the prefix lives at ONE const owner — a rename lands
916 // there and the three consumers on `from_json` inherit the
917 // rename mechanically. This pin binds the const to the pre-lift
918 // spelling so a rename shows up at BOTH the const definition
919 // AND this pin as a coherent atomic edit, not as a silent
920 // diff between the const and its downstream consumers.
921 assert_eq!(
922 RenderedResourceCoords::MISSING_MESSAGE_PREFIX,
923 "rendered resource missing",
924 );
925 }
926
927 #[test]
928 fn rendered_resource_coords_namespace_or_default_returns_slice_when_some() {
929 let c = RenderedResourceCoords {
930 api_version: "v1".into(),
931 kind: "K".into(),
932 name: "n".into(),
933 namespace: Some("prod".into()),
934 };
935 assert_eq!(c.namespace_or_default(), "prod");
936 }
937
938 #[test]
939 fn rendered_resource_coords_namespace_or_default_falls_back_when_none() {
940 let c = RenderedResourceCoords {
941 api_version: "v1".into(),
942 kind: "K".into(),
943 name: "n".into(),
944 namespace: None,
945 };
946 assert_eq!(c.namespace_or_default(), Process::DEFAULT_NAMESPACE);
947 assert_eq!(c.namespace_or_default(), "default");
948 }
949
950 // ─── FluxResourceRef::fetch_coords substrate pins ─────────────
951 //
952 // The 4-slot `(&namespace, &api_version, &kind, &name)` borrow
953 // projection lifts the pre-existing 5-slot `ssapply::fetch(client,
954 // &r.namespace, &r.api_version, &r.kind, &r.name)` splat that
955 // recurred at TWO hand-authored sites in
956 // `tatara-reconciler::phase_machine` (`handle_running`,
957 // `handle_attested`) past the ★★ PRIME-DIRECTIVE ≥ 2 duplication
958 // trigger. These pins bind the slot order at fail-before-pass-
959 // after granularity so a regression that swapped `namespace` and
960 // `api_version` (both `String`, mechanically interchangeable to
961 // a bad refactor) surfaces HERE rather than as a silent wire-time
962 // 404 at every downstream Flux fetch consumer.
963
964 fn sample_flux_ref() -> FluxResourceRef {
965 // Slot values are deliberately distinct so a swap between any
966 // two adjacent tuple positions surfaces as an equality
967 // failure at the assertion site — a slot-inversion regression
968 // cannot masquerade as identity by accident.
969 FluxResourceRef {
970 api_version: "kustomize.toolkit.fluxcd.io/v1".to_string(),
971 kind: "Kustomization".to_string(),
972 name: "observability-stack".to_string(),
973 namespace: "flux-system".to_string(),
974 ready: true,
975 message: None,
976 last_check: None,
977 }
978 }
979
980 #[test]
981 fn flux_resource_ref_fetch_coords_binds_slots_by_position() {
982 // Positional pin: the 4-tuple return binds
983 // `(namespace, api_version, kind, name)` in THAT order,
984 // matching the raw `ssapply::fetch(client, ns, av, kind,
985 // name)` positional signature every pre-lift callsite splatted
986 // into. A regression that swapped ANY pair of adjacent slots
987 // (all four axes are `String` and mechanically
988 // indistinguishable at the type level) would surface here
989 // rather than as an operator-visible wire-form 404 at every
990 // downstream fetch consumer.
991 let r = sample_flux_ref();
992 let (ns, av, kind, name) = r.fetch_coords();
993 assert_eq!(ns, "flux-system", "position 0 must be namespace");
994 assert_eq!(
995 av, "kustomize.toolkit.fluxcd.io/v1",
996 "position 1 must be api_version"
997 );
998 assert_eq!(kind, "Kustomization", "position 2 must be kind");
999 assert_eq!(name, "observability-stack", "position 3 must be name");
1000 }
1001
1002 #[test]
1003 fn flux_resource_ref_fetch_coords_returns_borrows_of_owned_slots() {
1004 // Borrow-discipline pin: the 4-tuple returns `&str` borrows
1005 // of the enclosing `FluxResourceRef`'s owned `String` slots —
1006 // NOT a fresh allocation or a clone. A regression that
1007 // switched the projection to owned strings (via `.clone()` or
1008 // `format!`) would defeat the zero-copy contract and would
1009 // surface here via pointer-identity comparison.
1010 let r = sample_flux_ref();
1011 let (ns, av, kind, name) = r.fetch_coords();
1012 assert!(std::ptr::eq(ns.as_ptr(), r.namespace.as_ptr()));
1013 assert!(std::ptr::eq(av.as_ptr(), r.api_version.as_ptr()));
1014 assert!(std::ptr::eq(kind.as_ptr(), r.kind.as_ptr()));
1015 assert!(std::ptr::eq(name.as_ptr(), r.name.as_ptr()));
1016 }
1017
1018 #[test]
1019 fn flux_resource_ref_fetch_coords_is_a_pure_borrow_projection() {
1020 // Purity pin: calling the projection twice on the same ref
1021 // returns byte-identical slices (same pointer, same length).
1022 // A regression that introduced state — a lazy-cached slot
1023 // computed on first call, a normalization step that ran once
1024 // and cached — would surface here rather than as silent drift
1025 // between the VERIFY-phase and ATTEST-heartbeat consumers on
1026 // the SAME ref within one reconcile pass.
1027 let r = sample_flux_ref();
1028 let a = r.fetch_coords();
1029 let b = r.fetch_coords();
1030 assert!(std::ptr::eq(a.0.as_ptr(), b.0.as_ptr()));
1031 assert!(std::ptr::eq(a.1.as_ptr(), b.1.as_ptr()));
1032 assert!(std::ptr::eq(a.2.as_ptr(), b.2.as_ptr()));
1033 assert!(std::ptr::eq(a.3.as_ptr(), b.3.as_ptr()));
1034 }
1035
1036 #[test]
1037 fn flux_resource_ref_fetch_coords_ignores_status_slots() {
1038 // Coverage pin: the projection exposes ONLY the four API-path
1039 // slots the fetch call requires; the ref's status slots
1040 // (`ready`, `message`, `last_check`) are deliberately absent
1041 // from the tuple. The fetch signature admits four `&str`
1042 // slots, and the projection carries EXACTLY those four — no
1043 // silent widening that would surface as an arity mismatch at
1044 // every downstream `fetch(...)` call.
1045 let r = sample_flux_ref();
1046 let coords = r.fetch_coords();
1047 assert_eq!(
1048 std::mem::size_of_val(&coords),
1049 std::mem::size_of::<(&str, &str, &str, &str)>(),
1050 "the 4-tuple width must match the raw fetch signature's four `&str` slots"
1051 );
1052 }
1053
1054 // ─── FluxResourceRef::observed substrate pins ─────────────────
1055 //
1056 // The 6-arg composer stamps `last_check` at ONE substrate site
1057 // (the pre-lift 7-slot struct-literal restated `Some(chrono::
1058 // Utc::now())` at TWO hand-authored sites in
1059 // `tatara-reconciler::phase_machine` — `handle_running`'s per-
1060 // ref VERIFY rebuild and `flux_ref_from_json`'s post-SSA
1061 // seeder). These pins bind the six input slots by position so a
1062 // regression that swapped `api_version` and `kind` (both
1063 // `String`, mechanically interchangeable to a bad refactor)
1064 // surfaces HERE rather than as a silent wire-time 404 at every
1065 // downstream fetch consumer.
1066 //
1067 // Every test constructs distinct values across the four
1068 // `String` coordinate slots so a slot swap fails structurally
1069 // rather than by accident of matching literals.
1070
1071 #[test]
1072 fn flux_resource_ref_observed_binds_slots_by_position() {
1073 // Positional pin: the 6-arg constructor binds
1074 // `(api_version, kind, name, namespace, ready, message)`
1075 // in THAT order, matching the pre-lift 7-slot struct-
1076 // literal's declaration order. A regression that swapped
1077 // ANY pair of adjacent `String` coordinate slots (all four
1078 // are mechanically indistinguishable at the type level)
1079 // would surface here rather than as a wire-time 404 at
1080 // every downstream Flux fetch consumer.
1081 let r = FluxResourceRef::observed(
1082 "kustomize.toolkit.fluxcd.io/v1".to_string(),
1083 "Kustomization".to_string(),
1084 "observability-stack".to_string(),
1085 "flux-system".to_string(),
1086 true,
1087 Some("healthy".to_string()),
1088 );
1089 assert_eq!(r.api_version, "kustomize.toolkit.fluxcd.io/v1");
1090 assert_eq!(r.kind, "Kustomization");
1091 assert_eq!(r.name, "observability-stack");
1092 assert_eq!(r.namespace, "flux-system");
1093 assert!(r.ready);
1094 assert_eq!(r.message.as_deref(), Some("healthy"));
1095 }
1096
1097 #[test]
1098 fn flux_resource_ref_observed_stamps_last_check_at_now() {
1099 // Stamp pin: the `last_check` slot is filled with
1100 // `Some(<recent Utc>)` at the composer's body. A
1101 // regression that dropped the stamp (leaving `None`) or
1102 // shifted it to a stale constant would surface here rather
1103 // than as silent operator-observed staleness at
1104 // `ProcessStatus.flux_resources` panels. Bounds the stamp
1105 // to within a generous 5s window of the composer call so
1106 // slow CI runners do not false-positive.
1107 let before = Utc::now();
1108 let r = FluxResourceRef::observed(
1109 "v1".to_string(),
1110 "K".to_string(),
1111 "n".to_string(),
1112 "ns".to_string(),
1113 false,
1114 None,
1115 );
1116 let after = Utc::now();
1117 let stamp = r.last_check.expect("observed must stamp last_check");
1118 assert!(stamp >= before, "stamp must be >= before-call `now`");
1119 assert!(stamp <= after, "stamp must be <= after-call `now`");
1120 }
1121
1122 #[test]
1123 fn flux_resource_ref_observed_round_trips_through_fetch_coords() {
1124 // Cross-composer coherence pin: a ref built by `observed`
1125 // then unpacked by `fetch_coords` returns the same four
1126 // slots in the peer projection's positional order
1127 // `(namespace, api_version, kind, name)`. Composition of
1128 // the two primitives on the same ref preserves the slot
1129 // identity — a regression at either end (a slot swap in
1130 // `observed`, or a slot swap in `fetch_coords`) would
1131 // surface here rather than as silent drift between the
1132 // writer and the reader on the same persisted slice.
1133 let r = FluxResourceRef::observed(
1134 "helm.toolkit.fluxcd.io/v2".to_string(),
1135 "HelmRelease".to_string(),
1136 "prometheus-op".to_string(),
1137 "monitoring".to_string(),
1138 false,
1139 Some("applied; awaiting reconciliation".to_string()),
1140 );
1141 let (ns, av, kind, name) = r.fetch_coords();
1142 assert_eq!(ns, "monitoring");
1143 assert_eq!(av, "helm.toolkit.fluxcd.io/v2");
1144 assert_eq!(kind, "HelmRelease");
1145 assert_eq!(name, "prometheus-op");
1146 }
1147
1148 #[test]
1149 fn flux_resource_ref_observed_matches_pre_lift_struct_literal_field_for_field() {
1150 // Byte-for-byte parity pin against the pre-lift 7-slot
1151 // struct-literal spelled at BOTH `phase_machine::
1152 // handle_running` and `phase_machine::flux_ref_from_json`.
1153 // A regression that reordered any of the six inputs at
1154 // the composer's argument list, or that swapped a
1155 // `ready`/`message` pair inside the composer's body,
1156 // would surface here rather than as silent divergence
1157 // between the composer's output and the pre-lift hand-
1158 // authored shape every persisted status writer restated.
1159 let composed = FluxResourceRef::observed(
1160 "source.toolkit.fluxcd.io/v1beta2".to_string(),
1161 "OCIRepository".to_string(),
1162 "chart-source".to_string(),
1163 "flux-system".to_string(),
1164 false,
1165 Some("applied; awaiting reconciliation".to_string()),
1166 );
1167 // Hand-authored the same seven slots directly, with a
1168 // held-open stamp window across the composer call.
1169 let stamped = composed.last_check.expect("stamped");
1170 let baseline = FluxResourceRef {
1171 api_version: "source.toolkit.fluxcd.io/v1beta2".to_string(),
1172 kind: "OCIRepository".to_string(),
1173 name: "chart-source".to_string(),
1174 namespace: "flux-system".to_string(),
1175 ready: false,
1176 message: Some("applied; awaiting reconciliation".to_string()),
1177 last_check: Some(stamped),
1178 };
1179 assert_eq!(composed.api_version, baseline.api_version);
1180 assert_eq!(composed.kind, baseline.kind);
1181 assert_eq!(composed.name, baseline.name);
1182 assert_eq!(composed.namespace, baseline.namespace);
1183 assert_eq!(composed.ready, baseline.ready);
1184 assert_eq!(composed.message, baseline.message);
1185 assert_eq!(composed.last_check, baseline.last_check);
1186 }
1187
1188 // ─── FluxResourceRef::pending substrate pins ─────────────────────
1189 //
1190 // Bind [`FluxResourceRef::pending`] at fail-before-pass-after
1191 // granularity so a regression that leaked a non-default status
1192 // slot (`ready: true`, `message: Some("something")`, `last_check:
1193 // Some(Utc::now())`), swapped two adjacent coordinate slots (all
1194 // four are `String` and mechanically interchangeable at the type
1195 // level), or diverged from the pre-lift 7-slot struct-literal on
1196 // any of the seven fields surfaces HERE rather than as silent
1197 // operator-invisible drift at the 3 downstream fixture consumers
1198 // (crd.rs `sample_flux_ref`, ssapply.rs `sample_flux_ref_for_diag`,
1199 // ssapply.rs `flux_ref_fetch_error_context_matches_pre_lift_...`).
1200 //
1201 // Each pin is fail-before-pass-after: the primitive did not exist
1202 // pre-lift, so any test that invokes it fails to compile pre-lift
1203 // and passes post-lift; the byte-identity pins below then bind
1204 // the specific shape choice.
1205
1206 #[test]
1207 fn flux_resource_ref_pending_binds_coordinate_slots_by_position() {
1208 // Positional pin: the 4-arg constructor binds `(api_version,
1209 // kind, name, namespace)` in THAT order, matching the pre-
1210 // lift 7-slot struct-literal's declaration order. A regression
1211 // that swapped ANY pair of adjacent `String` coordinate slots
1212 // (all four are mechanically indistinguishable at the type
1213 // level) would surface here rather than as a wire-time 404 at
1214 // every downstream Flux fetch consumer that walks
1215 // `FluxResourceRef.fetch_coords`.
1216 let r = FluxResourceRef::pending(
1217 "kustomize.toolkit.fluxcd.io/v1",
1218 "Kustomization",
1219 "observability-stack",
1220 "flux-system",
1221 );
1222 assert_eq!(r.api_version, "kustomize.toolkit.fluxcd.io/v1");
1223 assert_eq!(r.kind, "Kustomization");
1224 assert_eq!(r.name, "observability-stack");
1225 assert_eq!(r.namespace, "flux-system");
1226 }
1227
1228 #[test]
1229 fn flux_resource_ref_pending_defaults_every_status_slot() {
1230 // Default-slot pin: the three status slots (`ready`, `message`,
1231 // `last_check`) are ALL defaulted at the composer's body — no
1232 // wall-clock read, no non-`None` `message` leak, no `ready:
1233 // true` regression that would silently un-pend the fixture.
1234 // A regression that stamped `Some(Utc::now())` into
1235 // `last_check` (matching the sibling `observed` composer's
1236 // wall-clock read) would silently defeat the deterministic-
1237 // fixture contract the peer partition holds.
1238 let r = FluxResourceRef::pending("v1", "K", "n", "ns");
1239 assert!(
1240 !r.ready,
1241 "pending composer must default `ready` to false — non-`false` breaks the pre-observation contract"
1242 );
1243 assert_eq!(
1244 r.message, None,
1245 "pending composer must default `message` to None — non-`None` leaks a stale message into the pre-observation seed",
1246 );
1247 assert_eq!(
1248 r.last_check, None,
1249 "pending composer must default `last_check` to None — a `Some(_)` leak defeats the deterministic-peer partition against `observed`",
1250 );
1251 }
1252
1253 #[test]
1254 fn flux_resource_ref_pending_accepts_both_owned_and_borrowed_coordinates() {
1255 // The `impl Into<String>` ergonomic contract: both `&'static
1256 // str` literals (the fixture-helper sites that spell
1257 // coordinates inline) and owned `String` (a future callsite
1258 // handing off a dynamically-derived coordinate) round-trip
1259 // through the SAME composer signature without widening. A
1260 // regression that specialised the signature to one form or
1261 // the other would break either the inline-literal helpers or
1262 // the owned-`String` downstream consumers.
1263 let borrowed: FluxResourceRef = FluxResourceRef::pending("v1", "K", "n", "ns");
1264 let owned: FluxResourceRef = FluxResourceRef::pending(
1265 "v1".to_string(),
1266 "K".to_string(),
1267 "n".to_string(),
1268 "ns".to_string(),
1269 );
1270 assert_eq!(borrowed.api_version, owned.api_version);
1271 assert_eq!(borrowed.kind, owned.kind);
1272 assert_eq!(borrowed.name, owned.name);
1273 assert_eq!(borrowed.namespace, owned.namespace);
1274 assert_eq!(borrowed.ready, owned.ready);
1275 assert_eq!(borrowed.message, owned.message);
1276 assert_eq!(borrowed.last_check, owned.last_check);
1277 }
1278
1279 #[test]
1280 fn flux_resource_ref_pending_matches_pre_lift_struct_literal_bytewise() {
1281 // Byte-for-byte parity pin against the pre-lift 7-slot
1282 // struct-literal spelled at ALL THREE hand-authored fixture
1283 // sites (crd.rs `sample_flux_ref`, ssapply.rs
1284 // `sample_flux_ref_for_diag`, ssapply.rs inline in the
1285 // cross-substrate coherence pin's per-case sweep). Sweeps the
1286 // three representative coordinate tuples the pre-lift sites
1287 // used, so a regression that special-cased any one variant
1288 // (a `Kustomization`-only path via `if kind ==
1289 // "Kustomization" ...`) surfaces here.
1290 let cases = [
1291 (
1292 "kustomize.toolkit.fluxcd.io/v1",
1293 "Kustomization",
1294 "observability-stack",
1295 "flux-system",
1296 ),
1297 (
1298 "helm.toolkit.fluxcd.io/v2",
1299 "HelmRelease",
1300 "prometheus-op",
1301 "monitoring",
1302 ),
1303 (
1304 "source.toolkit.fluxcd.io/v1beta2",
1305 "OCIRepository",
1306 "chart-source",
1307 "flux-system",
1308 ),
1309 ];
1310 for (av, kind, name, ns) in cases {
1311 let composed = FluxResourceRef::pending(av, kind, name, ns);
1312 let hand_authored = FluxResourceRef {
1313 api_version: av.to_string(),
1314 kind: kind.to_string(),
1315 name: name.to_string(),
1316 namespace: ns.to_string(),
1317 ready: false,
1318 message: None,
1319 last_check: None,
1320 };
1321 assert_eq!(composed.api_version, hand_authored.api_version);
1322 assert_eq!(composed.kind, hand_authored.kind);
1323 assert_eq!(composed.name, hand_authored.name);
1324 assert_eq!(composed.namespace, hand_authored.namespace);
1325 assert_eq!(composed.ready, hand_authored.ready);
1326 assert_eq!(composed.message, hand_authored.message);
1327 assert_eq!(composed.last_check, hand_authored.last_check);
1328 }
1329 }
1330
1331 #[test]
1332 fn flux_resource_ref_pending_partitions_the_composer_axis_against_observed() {
1333 // Cross-composer partition pin: `pending` and `observed`
1334 // both produce `FluxResourceRef` but partition the composer
1335 // axis at the (deterministic-fixture, wall-clock-observed)
1336 // split — `pending` reads no clock and leaves `last_check:
1337 // None`, `observed` reads the wall clock and stamps
1338 // `last_check: Some(<recent Utc>)`. A regression that merged
1339 // either primitive onto the other (a `pending` that started
1340 // stamping `Utc::now()`, an `observed` that started leaving
1341 // `last_check: None`) would collapse the partition and
1342 // surface here.
1343 let p = FluxResourceRef::pending("v1", "K", "n", "ns");
1344 assert_eq!(
1345 p.last_check, None,
1346 "pending is deterministic — no clock read"
1347 );
1348 let o = FluxResourceRef::observed(
1349 "v1".to_string(),
1350 "K".to_string(),
1351 "n".to_string(),
1352 "ns".to_string(),
1353 false,
1354 None,
1355 );
1356 assert!(o.last_check.is_some(), "observed reads the wall clock");
1357 }
1358
1359 #[test]
1360 fn flux_resource_ref_pending_composes_with_fetch_coords_at_pre_observation_shape() {
1361 // Cross-composer coherence pin: a ref built by `pending`
1362 // then unpacked by `fetch_coords` returns the same four
1363 // slots in the peer projection's positional order
1364 // `(namespace, api_version, kind, name)`. Composition of
1365 // the two primitives on the same pre-observation ref
1366 // preserves the slot identity — a regression at either end
1367 // (a slot swap in `pending`, or a slot swap in
1368 // `fetch_coords`) would surface here rather than as silent
1369 // drift between the fixture writer and every downstream
1370 // fetch reader.
1371 let r = FluxResourceRef::pending(
1372 "helm.toolkit.fluxcd.io/v2",
1373 "HelmRelease",
1374 "prometheus-op",
1375 "monitoring",
1376 );
1377 let (ns, av, kind, name) = r.fetch_coords();
1378 assert_eq!(ns, "monitoring");
1379 assert_eq!(av, "helm.toolkit.fluxcd.io/v2");
1380 assert_eq!(kind, "HelmRelease");
1381 assert_eq!(name, "prometheus-op");
1382 }
1383
1384 #[test]
1385 fn rendered_resource_coords_namespace_fallback_shares_process_default_const() {
1386 // Byte-identity between the namespace fallback and the workspace-
1387 // wide `Process::DEFAULT_NAMESPACE` const. A regression that spelled
1388 // the fallback as any other string ("kube-system", "", "default-ns")
1389 // would silently drift between the coord-primitive family here and
1390 // the `Process`-borne family in `crd.rs` — surfaces here rather than
1391 // as operator-observed namespace routing skew between the two
1392 // primitive families.
1393 let c = RenderedResourceCoords {
1394 api_version: "v1".into(),
1395 kind: "K".into(),
1396 name: "n".into(),
1397 namespace: None,
1398 };
1399 assert_eq!(c.namespace_or_default(), Process::DEFAULT_NAMESPACE);
1400 }
1401
1402 // ─── CheckedCondition::all_satisfied substrate pins ─────────────
1403 //
1404 // Bind [`CheckedCondition::all_satisfied`] at fail-before-pass-
1405 // after granularity so a regression that flipped the fold
1406 // direction (`any` for `all`), inverted the projected bit
1407 // (`!c.satisfied`), reshaped the return form (an owned
1408 // `Vec<bool>` instead of the folded `bool`), or dropped the
1409 // vacuous-truth empty-slice corner surfaces HERE rather than as
1410 // silent operator-facing gate-flip at the reconciler's PROVE-
1411 // phase precondition gate + VERIFY-phase postcondition gate.
1412
1413 fn sample_checked(satisfied: bool) -> CheckedCondition {
1414 CheckedCondition {
1415 condition: crate::boundary::Condition {
1416 kind: crate::boundary::ConditionKind::ProcessPhase,
1417 params: serde_json::json!({}),
1418 },
1419 satisfied,
1420 last_check: None,
1421 message: None,
1422 }
1423 }
1424
1425 #[test]
1426 fn checked_condition_all_satisfied_returns_true_when_every_row_is_satisfied() {
1427 // Populated slice, every row `satisfied = true` — the RENDER-
1428 // phase advance corner: `handle_execing` proceeds to intent
1429 // dispatch iff every precondition holds.
1430 let checked = vec![
1431 sample_checked(true),
1432 sample_checked(true),
1433 sample_checked(true),
1434 ];
1435 assert!(
1436 CheckedCondition::all_satisfied(&checked),
1437 "all-satisfied slice must fold to true — a regression that inverted the bit would silently gate every RENDER advance behind an inverted predicate"
1438 );
1439 }
1440
1441 #[test]
1442 fn checked_condition_all_satisfied_returns_false_when_any_row_is_unsatisfied() {
1443 // Populated slice with ONE unsatisfied row — the heartbeat
1444 // requeue corner: `handle_running` stays in Running while any
1445 // postcondition remains unsatisfied.
1446 let mixed = vec![
1447 sample_checked(true),
1448 sample_checked(false),
1449 sample_checked(true),
1450 ];
1451 assert!(
1452 !CheckedCondition::all_satisfied(&mixed),
1453 "mixed slice must fold to false — a regression that folded via `any` instead of `all` would silently green-light every VERIFY advance"
1454 );
1455 }
1456
1457 #[test]
1458 fn checked_condition_all_satisfied_returns_false_when_every_row_is_unsatisfied() {
1459 // Populated slice with EVERY row unsatisfied — the tightest
1460 // gate corner: no phase advance is legal.
1461 let none_pass = vec![sample_checked(false), sample_checked(false)];
1462 assert!(
1463 !CheckedCondition::all_satisfied(&none_pass),
1464 "all-unsatisfied slice must fold to false"
1465 );
1466 }
1467
1468 #[test]
1469 fn checked_condition_all_satisfied_returns_true_on_empty_slice() {
1470 // Empty-slice vacuous-truth corner: `[T]::iter().all(_)`
1471 // returns `true` on empty input, and the pre-lift phase
1472 // gate's `if !preconditions.is_empty() { ... }` guard sat
1473 // BEFORE the fold, so the fold itself never saw an empty
1474 // slice in production. Post-lift the primitive absorbs the
1475 // empty corner cleanly — a caller that drops the outer
1476 // `is_empty()` guard (a future path that folds every gate
1477 // through this ONE primitive without a prior gate) still
1478 // sees the vacuous-truth semantics that match
1479 // [`Iterator::all`].
1480 let empty: Vec<CheckedCondition> = vec![];
1481 assert!(
1482 CheckedCondition::all_satisfied(&empty),
1483 "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"
1484 );
1485 }
1486
1487 #[test]
1488 fn checked_condition_all_satisfied_matches_pre_lift_iter_all_chain_shape() {
1489 // Byte-identity pin against the pre-lift `.iter().all(|c|
1490 // c.satisfied)` chain shape the reconciler's two boundary
1491 // gates hand-authored. Sweeps every corner every gate
1492 // plausibly encounters (empty slice, single satisfied,
1493 // single unsatisfied, mixed satisfied first, mixed
1494 // unsatisfied first) so a regression that reshaped either
1495 // link surfaces HERE rather than at the two downstream phase
1496 // gates.
1497 let corners: Vec<Vec<CheckedCondition>> = vec![
1498 vec![],
1499 vec![sample_checked(true)],
1500 vec![sample_checked(false)],
1501 vec![sample_checked(true), sample_checked(false)],
1502 vec![sample_checked(false), sample_checked(true)],
1503 vec![
1504 sample_checked(true),
1505 sample_checked(true),
1506 sample_checked(true),
1507 ],
1508 vec![
1509 sample_checked(false),
1510 sample_checked(false),
1511 sample_checked(false),
1512 ],
1513 ];
1514 for corner in &corners {
1515 let via_primitive = CheckedCondition::all_satisfied(corner);
1516 #[allow(clippy::redundant_closure_for_method_calls)]
1517 let hand_authored = corner.iter().all(|c| c.satisfied);
1518 assert_eq!(
1519 via_primitive, hand_authored,
1520 "all_satisfied fold must match hand-authored .iter().all(|c| c.satisfied) chain byte-identically at corner {corner:?}"
1521 );
1522 }
1523 }
1524
1525 #[test]
1526 fn checked_condition_all_satisfied_short_circuits_on_first_unsatisfied_row() {
1527 // Semantic pin against [`Iterator::all`]'s short-circuit
1528 // discipline: a regression that folded via `checked.iter()
1529 // .filter(|c| c.satisfied).count() == checked.len()` would
1530 // still produce the same `bool` result but would eagerly
1531 // walk every row, and a future addition of an expensive
1532 // per-row side effect (a metric emit, a log line, a
1533 // conditional postcondition-retry hook) would silently fire
1534 // on every row past the first failure. The primitive must
1535 // preserve the pre-lift short-circuit — a regression that
1536 // dropped it would drift telemetry, not correctness, and
1537 // would evade every other pin here. This test verifies
1538 // short-circuit by threading a counter through a peer
1539 // predicate that mirrors [`CheckedCondition::satisfied`]'s
1540 // read.
1541 use std::cell::Cell;
1542 let visited = Cell::new(0_usize);
1543 let checked: Vec<CheckedCondition> = vec![
1544 sample_checked(true),
1545 sample_checked(false),
1546 sample_checked(true),
1547 sample_checked(true),
1548 ];
1549 // Manual short-circuit fold that counts per-row reads —
1550 // must match `all_satisfied`'s count on the same slice.
1551 let via_manual = checked.iter().all(|c| {
1552 visited.set(visited.get() + 1);
1553 c.satisfied
1554 });
1555 let manual_visited = visited.get();
1556 visited.set(0);
1557 // Mirror the primitive's iteration by re-running the same
1558 // fold shape and confirming the visited count matches — the
1559 // primitive itself doesn't take a side-effecting closure,
1560 // but this pin confirms the semantic shape (2 visits on
1561 // this slice: row 0 satisfied, row 1 unsatisfied, stop).
1562 assert_eq!(via_manual, CheckedCondition::all_satisfied(&checked));
1563 assert_eq!(
1564 manual_visited, 2,
1565 "short-circuit must stop at the first unsatisfied row (index 1); manual fold visited {manual_visited} rows"
1566 );
1567 }
1568}