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;
10
11/// Standard K8s Condition (shape of `metav1.Condition`).
12#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
13#[serde(rename_all = "camelCase")]
14pub struct ProcessCondition {
15 #[serde(rename = "type")]
16 pub type_: String,
17 pub status: String,
18 pub last_transition_time: DateTime<Utc>,
19 #[serde(default, skip_serializing_if = "Option::is_none")]
20 pub reason: Option<String>,
21 #[serde(default, skip_serializing_if = "Option::is_none")]
22 pub message: Option<String>,
23}
24
25impl ProcessCondition {
26 pub fn ready(reason: impl Into<String>, message: Option<String>) -> Self {
27 Self {
28 type_: "Ready".into(),
29 status: "True".into(),
30 last_transition_time: Utc::now(),
31 reason: Some(reason.into()),
32 message,
33 }
34 }
35
36 pub fn not_ready(reason: impl Into<String>, message: impl Into<String>) -> Self {
37 Self {
38 type_: "Ready".into(),
39 status: "False".into(),
40 last_transition_time: Utc::now(),
41 reason: Some(reason.into()),
42 message: Some(message.into()),
43 }
44 }
45
46 pub fn attested(root: &str) -> Self {
47 Self {
48 type_: "Attested".into(),
49 status: "True".into(),
50 last_transition_time: Utc::now(),
51 reason: Some("AttestationWritten".into()),
52 message: Some(format!("composed_root={root}")),
53 }
54 }
55}
56
57/// Reference to a FluxCD resource emitted as part of this Process.
58#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
59#[serde(rename_all = "camelCase")]
60pub struct FluxResourceRef {
61 pub api_version: String,
62 pub kind: String,
63 pub name: String,
64 pub namespace: String,
65 #[serde(default)]
66 pub ready: bool,
67 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub message: Option<String>,
69 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub last_check: Option<DateTime<Utc>>,
71}
72
73impl FluxResourceRef {
74 /// Pure typed projection of the four fetch coordinates
75 /// `(namespace, api_version, kind, name)` every consumer that
76 /// dispatches this persisted reference through kube-rs's dynamic-
77 /// object surface splats by hand pre-lift. The 4-tuple binds the
78 /// slot order at ONE typed accessor so a copy-paste at any downstream
79 /// consumer cannot swap two adjacent `&str` slots in the fetch call.
80 ///
81 /// Peer projection to
82 /// [`crate::k8s_wire_identity::K8sWireIdentity`] on the static-
83 /// identity axis: [`K8sWireIdentity`] carries a
84 /// `(&'static str, &'static str)` closed-set variant's pair for
85 /// emit-time (RENDER phase) composition; this method carries the
86 /// full `(ns, apiVersion, kind, name)` 4-slot borrow for fetch-time
87 /// (VERIFY / ATTEST-heartbeat) composition where the ref's payload
88 /// comes back off the persisted `ProcessStatus.flux_resources`
89 /// slice with owned `String`s rather than static literals. The two
90 /// primitives partition the fetch axis by whether the caller starts
91 /// from a closed-set variant (emit-time) or a persisted status
92 /// slice (fetch-time).
93 ///
94 /// Pre-lift the 5-slot `ssapply::fetch(client, &r.namespace,
95 /// &r.api_version, &r.kind, &r.name)` splat was hand-authored at
96 /// TWO sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication threshold
97 /// in `tatara-reconciler::phase_machine`:
98 /// * `handle_running` — the VERIFY-phase per-ref readiness probe
99 /// that populates the updated `FluxResourceRef` slice with
100 /// `ready` + `message` + `last_check`.
101 /// * `handle_attested` — the ATTEST-heartbeat drift detector that
102 /// short-circuits on the first non-Ready ref.
103 ///
104 /// Both sites splatted the SAME four `&r.X` field borrows in the
105 /// SAME order into raw `ssapply::fetch`. A copy-paste that swapped
106 /// two adjacent `&str` slots (`&r.api_version` and `&r.kind` are
107 /// both strings that look interchangeable to a mechanical
108 /// substitution) would silently 404 at wire time and diagnose as a
109 /// broken CRD rather than as slot skew at the callsite. Post-lift
110 /// each site names the ref ONCE and unpacks it through this ONE
111 /// projection; the slot order binds structurally at the tuple
112 /// return so a caller cannot desync one axis.
113 ///
114 /// A future addition (a case-fold normalization on the group, a
115 /// virtual-cluster prefix rewrite for multi-tenancy, a
116 /// `generateName` fallback on the name slot, a cluster-cache
117 /// short-circuit inserted between the projection and the fetch
118 /// call) lands at this ONE method and every downstream fetch
119 /// consumer inherits the upgrade mechanically — no per-site edit
120 /// at `handle_running` / `handle_attested` / any future kenshi-
121 /// runner / mirror-audit / drift-probe consumer that grows a third
122 /// consumer.
123 ///
124 /// Return-order pin lives at
125 /// [`tests::flux_resource_ref_fetch_coords_binds_slots_by_position`]
126 /// so a regression that swapped `namespace` and `api_version`
127 /// (both `String`, same type) inside the tuple constructor fails-
128 /// loudly here rather than as a silent wire-time 404 at every
129 /// downstream fetch consumer.
130 ///
131 /// Theory grounding: THEORY.md §II.1 invariant 5 (composition
132 /// preserves proofs — the 4-tuple slot order binds at ONE typed
133 /// projection so a regression across the two fields of the same
134 /// `String` type fails at the projection's positional pin rather
135 /// than at every downstream fetch consumer). THEORY.md §VI.1
136 /// (generation over composition — the 5-slot splat recurred at
137 /// two hand-authored sites past the ≥ 2 duplication trigger, and
138 /// is lifted to ONE typed borrow-projection here).
139 pub fn fetch_coords(&self) -> (&str, &str, &str, &str) {
140 (&self.namespace, &self.api_version, &self.kind, &self.name)
141 }
142
143 /// Compose a `FluxResourceRef` stamped at "observed now" — the
144 /// `last_check` slot is set to `Some(Utc::now())` at ONE substrate
145 /// owner, and the four coordinate slots + `ready` + `message`
146 /// are bound positionally so a slot-swap regression surfaces at
147 /// the constructor's positional pin rather than as silent drift
148 /// at every downstream `ProcessStatus.flux_resources` writer.
149 ///
150 /// Pre-lift the 7-slot `FluxResourceRef { …, last_check:
151 /// Some(chrono::Utc::now()) }` struct-literal was hand-authored
152 /// at TWO sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication
153 /// threshold in `tatara-reconciler::phase_machine`:
154 /// * `handle_running` — the VERIFY-phase per-ref rebuild that
155 /// restamps each polled ref with fresh `ready` + `message` +
156 /// `last_check`.
157 /// * `flux_ref_from_json` — the post-SSA initial-state seeder
158 /// that stamps a freshly-applied ref as `ready = false`,
159 /// `message = Some("applied; awaiting reconciliation")`,
160 /// `last_check = Some(Utc::now())`.
161 ///
162 /// Both sites restated the SAME seven field bindings in the
163 /// SAME order, and both restated the SAME `Some(chrono::Utc::
164 /// now())` stamp. A copy-paste that swapped two adjacent
165 /// `String` slots (`api_version` and `kind`, `kind` and `name`,
166 /// or `name` and `namespace` are all mechanically
167 /// indistinguishable at the type level) would silently persist
168 /// a slot-inverted ref that the downstream Flux fetch consumer
169 /// (via [`Self::fetch_coords`]) would then 404 on. Post-lift
170 /// both sites name the six inputs ONCE and route through this
171 /// ONE composer; the seventh slot (`last_check`) is stamped at
172 /// the composer's body so a future injection point (a fake
173 /// clock for testing, a monotonic-clock cross-check, a per-
174 /// fleet skew tolerance) lands at ONE substrate site rather
175 /// than at every hand-authored `Some(chrono::Utc::now())` stamp.
176 ///
177 /// Return-order pin lives at
178 /// [`tests::flux_resource_ref_observed_binds_slots_by_position`]
179 /// so a regression that swapped `api_version` and `kind` (both
180 /// `String`, same type) inside the constructor's argument list
181 /// fails-loudly here rather than as a silent wire-time 404 at
182 /// every downstream fetch consumer.
183 ///
184 /// Theory grounding: THEORY.md §II.1 invariant 5 (composition
185 /// preserves proofs — the 6-slot positional binding + the
186 /// `last_check` stamp compose at ONE typed owner, so a
187 /// regression across the four `String` coordinate slots fails
188 /// at the composer's positional pin rather than at every
189 /// downstream Flux status writer). THEORY.md §VI.1 (generation
190 /// over composition — the 7-slot struct-literal recurred at two
191 /// hand-authored sites past the ≥ 2 duplication trigger, and is
192 /// lifted to ONE typed composer here).
193 pub fn observed(
194 api_version: String,
195 kind: String,
196 name: String,
197 namespace: String,
198 ready: bool,
199 message: Option<String>,
200 ) -> Self {
201 Self {
202 api_version,
203 kind,
204 name,
205 namespace,
206 ready,
207 message,
208 last_check: Some(Utc::now()),
209 }
210 }
211
212 /// Compose a `FluxResourceRef` in the pre-observation shape — the
213 /// 4-slot coordinate binding with the three status slots defaulted
214 /// (`ready: false`, `message: None`, `last_check: None`). The
215 /// deterministic-fixture peer of [`Self::observed`] on the same
216 /// `→ FluxResourceRef` composer axis: `observed` reads the wall
217 /// clock and takes 6 args (a live post-fetch stamp), `pending`
218 /// reads no clock and takes 4 args (a pre-observation fixture
219 /// seed, and the natural base for `..base.clone()` spread updates
220 /// that vary a single slot for a per-corner test sweep).
221 ///
222 /// Pre-lift the SAME 7-slot `FluxResourceRef { api_version, kind,
223 /// name, namespace, ready: false, message: None, last_check: None
224 /// }` struct-literal was hand-authored at THREE workspace-wide
225 /// fixture sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication
226 /// threshold:
227 ///
228 /// * [`crate::crd`]
229 /// `crd::observed_flux_resources_tests::sample_flux_ref(name)`
230 /// — the shared `Kustomization`/`flux-system` fixture the
231 /// `Process::observed_flux_resources` pin family destructures
232 /// for its `flux_resources`-populated corners.
233 /// * `tatara-reconciler::ssapply::tests::sample_flux_ref_for_diag`
234 /// — the `HelmRelease`/`flux-system` fixture the
235 /// `flux_ref_fetch_error_context` diagnostic-wording pin
236 /// family destructures for its (kind, name) slot-coverage
237 /// sweep.
238 /// * `tatara-reconciler::ssapply::tests::
239 /// flux_ref_fetch_error_context_matches_pre_lift_hand_authored_wording`
240 /// — the inline 7-slot literal inside the cross-substrate
241 /// coherence pin's per-case sweep over three distinct
242 /// `(api_version, kind, name, namespace)` tuples.
243 ///
244 /// All THREE sites restated the SAME seven field bindings in the
245 /// SAME order and the SAME three defaulted status slots (`ready:
246 /// false, message: None, last_check: None`), differing only in
247 /// the four coordinate `String` values. Post-lift each callsite
248 /// reads `FluxResourceRef::pending(<api_version>, <kind>, <name>,
249 /// <namespace>)` and the four-slot bind + three-slot default
250 /// sinks live at ONE substrate owner.
251 ///
252 /// The `impl Into<String>` signature accepts BOTH `&'static str`
253 /// (the fixture-helper sites that spell coordinate literals
254 /// inline) AND owned `String` (a future callsite handing off a
255 /// dynamically-derived coordinate) without widening. Matches the
256 /// discipline of the sibling substrate composers
257 /// [`crate::pool::PoolMember::unallocated`] +
258 /// [`crate::allocation::AllocationRef::new`] on the identity-slot
259 /// axis.
260 ///
261 /// A future normalization (a case-fold on the group, a
262 /// virtual-cluster prefix rewrite for multi-tenancy, a stricter
263 /// kind gate, a `generateName` fallback on the name slot, a
264 /// canonical rename of one of the three defaulted status slots
265 /// to a typed `PreObservation` marker) lands at THIS ONE
266 /// substrate primitive and every downstream fixture / helper
267 /// inherits the upgrade mechanically — no per-site edit at any
268 /// of the THREE listed callers or at future consumers (a
269 /// stable-name claim-arbiter's pending-ref seed, a kenshi-runner
270 /// pre-observation fixture, a mirror-audit drift-probe test
271 /// helper).
272 ///
273 /// Theory grounding: THEORY.md §II.1 invariant 5 (composition
274 /// preserves proofs — the 4-slot positional binding + the three
275 /// defaulted status slots compose at ONE typed owner, so a
276 /// regression across the four `String` coordinate slots fails at
277 /// the composer's positional pin rather than at every downstream
278 /// fixture consumer). THEORY.md §VI.1 (generation over
279 /// composition — the 7-slot struct-literal recurred at three
280 /// hand-authored fixture sites past the ≥ 2 duplication trigger,
281 /// and is lifted to ONE typed composer here).
282 #[must_use]
283 pub fn pending(
284 api_version: impl Into<String>,
285 kind: impl Into<String>,
286 name: impl Into<String>,
287 namespace: impl Into<String>,
288 ) -> Self {
289 Self {
290 api_version: api_version.into(),
291 kind: kind.into(),
292 name: name.into(),
293 namespace: namespace.into(),
294 ready: false,
295 message: None,
296 last_check: None,
297 }
298 }
299}
300
301/// Identifying coordinates of a rendered K8s resource — the
302/// `(apiVersion, kind, metadata.name, metadata.namespace)` 4-tuple
303/// every consumer that walks a rendered `serde_json::Value` resource
304/// unwraps by hand pre-lift.
305///
306/// The three K8s API-path segments (`apiVersion`, `kind`,
307/// `metadata.name`) are REQUIRED — a rendered resource missing any
308/// of them cannot be applied via kube-rs's dynamic API surface, so
309/// the extraction fails fast at the boundary rather than as a
310/// downstream `Api::patch` panic. `metadata.namespace` is
311/// intentionally kept as `Option<String>` because different consumers
312/// resolve the fallback differently: `apply_owned` uses the
313/// caller-supplied `namespace: &str` argument (the reconciler already
314/// resolved the target namespace upstream), while `flux_ref_from_json`
315/// records the K8s canonical `"default"` fallback into the persisted
316/// `FluxResourceRef.namespace` slot. The peer method
317/// [`Self::namespace_or_default`] applies the K8s canonical fallback
318/// (`Process::DEFAULT_NAMESPACE = "default"`) for consumers wanting
319/// the same shape [`FluxResourceRef.namespace`] carries.
320///
321/// Pre-lift the 3+1 slot extraction was hand-authored at TWO sites
322/// past the ★★ PRIME-DIRECTIVE ≥ 2 duplication threshold across
323/// `tatara-reconciler`:
324/// * `tatara-reconciler::phase_machine::flux_ref_from_json` — the
325/// post-SSA `FluxResourceRef` builder that persists into
326/// `ProcessStatus.flux_resources`; namespace half fallback-
327/// defaulted to `"default"`.
328/// * `tatara-reconciler::ssapply::apply_owned` — the SSA entry
329/// point that extracts (apiVersion, kind, name) for the
330/// [`kube::Api::patch`] call; namespace half discarded (the
331/// `namespace: &str` argument comes from the caller upstream).
332///
333/// Both callsites restated the same three
334/// `.get(K).and_then(|v| v.as_str()).ok_or_else(|| anyhow!(...))?
335/// .to_string()` incantations with subtly different error wording
336/// (`"resource missing X"` vs `"rendered resource missing X"`); post-
337/// lift both route through this ONE substrate owner with the
338/// canonical `"rendered resource missing X"` wording. A future
339/// addition (case-fold on the group, a rename of the namespace
340/// fallback, a stricter kind gate, a Unicode-safe collation step,
341/// support for `metadata.generateName` as a name fallback) lands at
342/// the primitive's body on the substrate, not at 2 independent
343/// hand-writes across 2 reconciler files.
344///
345/// Namespace fallback const is shared with
346/// [`Process::DEFAULT_NAMESPACE`] — a rename of the K8s canonical
347/// default namespace lands at that ONE workspace-wide const, not at
348/// per-primitive local literals that would drift silently.
349#[derive(Clone, Debug, PartialEq, Eq)]
350pub struct RenderedResourceCoords {
351 /// `apiVersion` — the group+version pair kube-rs uses to resolve
352 /// the `ApiResource` for the SSA call.
353 pub api_version: String,
354 /// `kind` — the resource kind (Kustomization, HelmRelease, …).
355 pub kind: String,
356 /// `metadata.name` — the API-path leaf segment.
357 pub name: String,
358 /// `metadata.namespace` — raw from the resource, `None` when the
359 /// slot is absent (a cluster-scoped resource, or a namespaced
360 /// resource whose namespace was left for the API server to
361 /// substitute). Consumers apply their own fallback:
362 /// [`Self::namespace_or_default`] applies the K8s canonical
363 /// `"default"` (matching what [`FluxResourceRef.namespace`]
364 /// records); other consumers substitute a caller-supplied string
365 /// (see `tatara-reconciler::ssapply::apply_owned`).
366 pub namespace: Option<String>,
367}
368
369impl RenderedResourceCoords {
370 /// Extract the 4-tuple from a rendered K8s resource JSON `Value`.
371 ///
372 /// Fails with a canonical `"rendered resource missing X"` message
373 /// when any of the three required slots (`apiVersion`, `kind`,
374 /// `metadata.name`) is absent or non-string; `metadata.namespace`
375 /// is optional and captured as `None` when absent.
376 ///
377 /// The error wording is pinned by
378 /// [`tests::rendered_resource_coords_error_wording_is_canonical`]
379 /// so a regression that reshaped the message surfaces at the test
380 /// surface rather than as silent drift between the two pre-lift
381 /// call sites (which used subtly different wording — `"resource
382 /// missing X"` in `apply_owned` vs `"rendered resource missing
383 /// X"` in `flux_ref_from_json`).
384 pub fn from_json(res: &Value) -> anyhow::Result<Self> {
385 let api_version = res
386 .get("apiVersion")
387 .and_then(|v| v.as_str())
388 .ok_or_else(|| anyhow::anyhow!("rendered resource missing apiVersion"))?
389 .to_string();
390 let kind = res
391 .get("kind")
392 .and_then(|v| v.as_str())
393 .ok_or_else(|| anyhow::anyhow!("rendered resource missing kind"))?
394 .to_string();
395 let metadata = res.get("metadata");
396 let name = metadata
397 .and_then(|m| m.get("name"))
398 .and_then(|v| v.as_str())
399 .ok_or_else(|| anyhow::anyhow!("rendered resource missing metadata.name"))?
400 .to_string();
401 let namespace = metadata
402 .and_then(|m| m.get("namespace"))
403 .and_then(|v| v.as_str())
404 .map(str::to_string);
405 Ok(Self {
406 api_version,
407 kind,
408 name,
409 namespace,
410 })
411 }
412
413 /// `metadata.namespace` slice with the K8s canonical `"default"`
414 /// fallback applied — matching what [`Process::DEFAULT_NAMESPACE`]
415 /// spells for the `Process`-borne coordinate primitive family
416 /// and what [`FluxResourceRef.namespace`] records into
417 /// `ProcessStatus.flux_resources`.
418 pub fn namespace_or_default(&self) -> &str {
419 self.namespace
420 .as_deref()
421 .unwrap_or(Process::DEFAULT_NAMESPACE)
422 }
423}
424
425/// A boundary condition paired with its current satisfaction state.
426#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
427#[serde(rename_all = "camelCase")]
428pub struct CheckedCondition {
429 #[serde(flatten)]
430 pub condition: Condition,
431 pub satisfied: bool,
432 #[serde(default, skip_serializing_if = "Option::is_none")]
433 pub last_check: Option<DateTime<Utc>>,
434 #[serde(default, skip_serializing_if = "Option::is_none")]
435 pub message: Option<String>,
436}
437
438/// Summary of boundary verification.
439#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
440#[serde(rename_all = "camelCase")]
441pub struct BoundaryStatus {
442 #[serde(default)]
443 pub preconditions: Vec<CheckedCondition>,
444 #[serde(default)]
445 pub postconditions: Vec<CheckedCondition>,
446 /// Absolute deadline for VERIFY (derived from `spec.boundary.timeout`).
447 #[serde(default, skip_serializing_if = "Option::is_none")]
448 pub deadline: Option<DateTime<Utc>>,
449}
450
451/// Summary of compliance checks at the latest attestation.
452#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
453#[serde(rename_all = "camelCase")]
454pub struct ComplianceStatus {
455 #[serde(default, skip_serializing_if = "Option::is_none")]
456 pub baseline: Option<String>,
457 pub satisfied: u32,
458 pub violated: u32,
459 pub total: u32,
460 #[serde(default)]
461 pub violations: Vec<String>,
462}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467 use serde_json::json;
468
469 // ─── RenderedResourceCoords substrate pins ──────────────────────
470
471 #[test]
472 fn rendered_resource_coords_from_json_extracts_all_four_slots_when_present() {
473 let res = json!({
474 "apiVersion": "kustomize.toolkit.fluxcd.io/v1",
475 "kind": "Kustomization",
476 "metadata": {
477 "name": "observability-stack",
478 "namespace": "flux-system",
479 },
480 });
481 let c = RenderedResourceCoords::from_json(&res).expect("extract");
482 assert_eq!(c.api_version, "kustomize.toolkit.fluxcd.io/v1");
483 assert_eq!(c.kind, "Kustomization");
484 assert_eq!(c.name, "observability-stack");
485 assert_eq!(c.namespace.as_deref(), Some("flux-system"));
486 }
487
488 #[test]
489 fn rendered_resource_coords_from_json_captures_absent_namespace_as_none() {
490 // Cluster-scoped resource — `metadata.namespace` intentionally absent.
491 let res = json!({
492 "apiVersion": "v1",
493 "kind": "Namespace",
494 "metadata": {"name": "demo-test"},
495 });
496 let c = RenderedResourceCoords::from_json(&res).expect("extract");
497 assert_eq!(c.namespace, None);
498 assert_eq!(c.name, "demo-test");
499 }
500
501 #[test]
502 fn rendered_resource_coords_from_json_errors_on_missing_api_version() {
503 let res = json!({"kind": "K", "metadata": {"name": "n"}});
504 let e = RenderedResourceCoords::from_json(&res).expect_err("must error");
505 assert_eq!(e.to_string(), "rendered resource missing apiVersion");
506 }
507
508 #[test]
509 fn rendered_resource_coords_from_json_errors_on_missing_kind() {
510 let res = json!({"apiVersion": "v1", "metadata": {"name": "n"}});
511 let e = RenderedResourceCoords::from_json(&res).expect_err("must error");
512 assert_eq!(e.to_string(), "rendered resource missing kind");
513 }
514
515 #[test]
516 fn rendered_resource_coords_from_json_errors_on_missing_metadata_name() {
517 let res = json!({"apiVersion": "v1", "kind": "K", "metadata": {}});
518 let e = RenderedResourceCoords::from_json(&res).expect_err("must error");
519 assert_eq!(e.to_string(), "rendered resource missing metadata.name");
520 }
521
522 #[test]
523 fn rendered_resource_coords_from_json_errors_on_missing_metadata_object() {
524 // `metadata` absent entirely — same failure as `metadata.name` missing,
525 // because the API-path leaf segment cannot be resolved.
526 let res = json!({"apiVersion": "v1", "kind": "K"});
527 let e = RenderedResourceCoords::from_json(&res).expect_err("must error");
528 assert_eq!(e.to_string(), "rendered resource missing metadata.name");
529 }
530
531 #[test]
532 fn rendered_resource_coords_from_json_errors_on_non_string_slot() {
533 // A numeric `apiVersion` slot falls through the `.as_str()` gate and
534 // triggers the same missing-slot failure as absence — the API-path
535 // segment is not a string.
536 let res = json!({
537 "apiVersion": 42,
538 "kind": "K",
539 "metadata": {"name": "n"},
540 });
541 let e = RenderedResourceCoords::from_json(&res).expect_err("must error");
542 assert_eq!(e.to_string(), "rendered resource missing apiVersion");
543 }
544
545 #[test]
546 fn rendered_resource_coords_error_wording_is_canonical() {
547 // Pins the exact spelling every downstream consumer sees.
548 // Pre-lift wording differed across the two call sites (`"resource
549 // missing X"` in `apply_owned` vs `"rendered resource missing X"` in
550 // `flux_ref_from_json`); post-lift the canonical wording is
551 // `"rendered resource missing X"` at every site.
552 let cases = [
553 (
554 "apiVersion",
555 json!({"kind": "K", "metadata": {"name": "n"}}),
556 ),
557 (
558 "kind",
559 json!({"apiVersion": "v1", "metadata": {"name": "n"}}),
560 ),
561 (
562 "metadata.name",
563 json!({"apiVersion": "v1", "kind": "K", "metadata": {}}),
564 ),
565 ];
566 for (slot, res) in cases {
567 let e = RenderedResourceCoords::from_json(&res).expect_err("must error");
568 assert_eq!(
569 e.to_string(),
570 format!("rendered resource missing {slot}"),
571 "slot {slot} error must be canonical"
572 );
573 }
574 }
575
576 #[test]
577 fn rendered_resource_coords_namespace_or_default_returns_slice_when_some() {
578 let c = RenderedResourceCoords {
579 api_version: "v1".into(),
580 kind: "K".into(),
581 name: "n".into(),
582 namespace: Some("prod".into()),
583 };
584 assert_eq!(c.namespace_or_default(), "prod");
585 }
586
587 #[test]
588 fn rendered_resource_coords_namespace_or_default_falls_back_when_none() {
589 let c = RenderedResourceCoords {
590 api_version: "v1".into(),
591 kind: "K".into(),
592 name: "n".into(),
593 namespace: None,
594 };
595 assert_eq!(c.namespace_or_default(), Process::DEFAULT_NAMESPACE);
596 assert_eq!(c.namespace_or_default(), "default");
597 }
598
599 // ─── FluxResourceRef::fetch_coords substrate pins ─────────────
600 //
601 // The 4-slot `(&namespace, &api_version, &kind, &name)` borrow
602 // projection lifts the pre-existing 5-slot `ssapply::fetch(client,
603 // &r.namespace, &r.api_version, &r.kind, &r.name)` splat that
604 // recurred at TWO hand-authored sites in
605 // `tatara-reconciler::phase_machine` (`handle_running`,
606 // `handle_attested`) past the ★★ PRIME-DIRECTIVE ≥ 2 duplication
607 // trigger. These pins bind the slot order at fail-before-pass-
608 // after granularity so a regression that swapped `namespace` and
609 // `api_version` (both `String`, mechanically interchangeable to
610 // a bad refactor) surfaces HERE rather than as a silent wire-time
611 // 404 at every downstream Flux fetch consumer.
612
613 fn sample_flux_ref() -> FluxResourceRef {
614 // Slot values are deliberately distinct so a swap between any
615 // two adjacent tuple positions surfaces as an equality
616 // failure at the assertion site — a slot-inversion regression
617 // cannot masquerade as identity by accident.
618 FluxResourceRef {
619 api_version: "kustomize.toolkit.fluxcd.io/v1".to_string(),
620 kind: "Kustomization".to_string(),
621 name: "observability-stack".to_string(),
622 namespace: "flux-system".to_string(),
623 ready: true,
624 message: None,
625 last_check: None,
626 }
627 }
628
629 #[test]
630 fn flux_resource_ref_fetch_coords_binds_slots_by_position() {
631 // Positional pin: the 4-tuple return binds
632 // `(namespace, api_version, kind, name)` in THAT order,
633 // matching the raw `ssapply::fetch(client, ns, av, kind,
634 // name)` positional signature every pre-lift callsite splatted
635 // into. A regression that swapped ANY pair of adjacent slots
636 // (all four axes are `String` and mechanically
637 // indistinguishable at the type level) would surface here
638 // rather than as an operator-visible wire-form 404 at every
639 // downstream fetch consumer.
640 let r = sample_flux_ref();
641 let (ns, av, kind, name) = r.fetch_coords();
642 assert_eq!(ns, "flux-system", "position 0 must be namespace");
643 assert_eq!(
644 av, "kustomize.toolkit.fluxcd.io/v1",
645 "position 1 must be api_version"
646 );
647 assert_eq!(kind, "Kustomization", "position 2 must be kind");
648 assert_eq!(name, "observability-stack", "position 3 must be name");
649 }
650
651 #[test]
652 fn flux_resource_ref_fetch_coords_returns_borrows_of_owned_slots() {
653 // Borrow-discipline pin: the 4-tuple returns `&str` borrows
654 // of the enclosing `FluxResourceRef`'s owned `String` slots —
655 // NOT a fresh allocation or a clone. A regression that
656 // switched the projection to owned strings (via `.clone()` or
657 // `format!`) would defeat the zero-copy contract and would
658 // surface here via pointer-identity comparison.
659 let r = sample_flux_ref();
660 let (ns, av, kind, name) = r.fetch_coords();
661 assert!(std::ptr::eq(ns.as_ptr(), r.namespace.as_ptr()));
662 assert!(std::ptr::eq(av.as_ptr(), r.api_version.as_ptr()));
663 assert!(std::ptr::eq(kind.as_ptr(), r.kind.as_ptr()));
664 assert!(std::ptr::eq(name.as_ptr(), r.name.as_ptr()));
665 }
666
667 #[test]
668 fn flux_resource_ref_fetch_coords_is_a_pure_borrow_projection() {
669 // Purity pin: calling the projection twice on the same ref
670 // returns byte-identical slices (same pointer, same length).
671 // A regression that introduced state — a lazy-cached slot
672 // computed on first call, a normalization step that ran once
673 // and cached — would surface here rather than as silent drift
674 // between the VERIFY-phase and ATTEST-heartbeat consumers on
675 // the SAME ref within one reconcile pass.
676 let r = sample_flux_ref();
677 let a = r.fetch_coords();
678 let b = r.fetch_coords();
679 assert!(std::ptr::eq(a.0.as_ptr(), b.0.as_ptr()));
680 assert!(std::ptr::eq(a.1.as_ptr(), b.1.as_ptr()));
681 assert!(std::ptr::eq(a.2.as_ptr(), b.2.as_ptr()));
682 assert!(std::ptr::eq(a.3.as_ptr(), b.3.as_ptr()));
683 }
684
685 #[test]
686 fn flux_resource_ref_fetch_coords_ignores_status_slots() {
687 // Coverage pin: the projection exposes ONLY the four API-path
688 // slots the fetch call requires; the ref's status slots
689 // (`ready`, `message`, `last_check`) are deliberately absent
690 // from the tuple. The fetch signature admits four `&str`
691 // slots, and the projection carries EXACTLY those four — no
692 // silent widening that would surface as an arity mismatch at
693 // every downstream `fetch(...)` call.
694 let r = sample_flux_ref();
695 let coords = r.fetch_coords();
696 assert_eq!(
697 std::mem::size_of_val(&coords),
698 std::mem::size_of::<(&str, &str, &str, &str)>(),
699 "the 4-tuple width must match the raw fetch signature's four `&str` slots"
700 );
701 }
702
703 // ─── FluxResourceRef::observed substrate pins ─────────────────
704 //
705 // The 6-arg composer stamps `last_check` at ONE substrate site
706 // (the pre-lift 7-slot struct-literal restated `Some(chrono::
707 // Utc::now())` at TWO hand-authored sites in
708 // `tatara-reconciler::phase_machine` — `handle_running`'s per-
709 // ref VERIFY rebuild and `flux_ref_from_json`'s post-SSA
710 // seeder). These pins bind the six input slots by position so a
711 // regression that swapped `api_version` and `kind` (both
712 // `String`, mechanically interchangeable to a bad refactor)
713 // surfaces HERE rather than as a silent wire-time 404 at every
714 // downstream fetch consumer.
715 //
716 // Every test constructs distinct values across the four
717 // `String` coordinate slots so a slot swap fails structurally
718 // rather than by accident of matching literals.
719
720 #[test]
721 fn flux_resource_ref_observed_binds_slots_by_position() {
722 // Positional pin: the 6-arg constructor binds
723 // `(api_version, kind, name, namespace, ready, message)`
724 // in THAT order, matching the pre-lift 7-slot struct-
725 // literal's declaration order. A regression that swapped
726 // ANY pair of adjacent `String` coordinate slots (all four
727 // are mechanically indistinguishable at the type level)
728 // would surface here rather than as a wire-time 404 at
729 // every downstream Flux fetch consumer.
730 let r = FluxResourceRef::observed(
731 "kustomize.toolkit.fluxcd.io/v1".to_string(),
732 "Kustomization".to_string(),
733 "observability-stack".to_string(),
734 "flux-system".to_string(),
735 true,
736 Some("healthy".to_string()),
737 );
738 assert_eq!(r.api_version, "kustomize.toolkit.fluxcd.io/v1");
739 assert_eq!(r.kind, "Kustomization");
740 assert_eq!(r.name, "observability-stack");
741 assert_eq!(r.namespace, "flux-system");
742 assert!(r.ready);
743 assert_eq!(r.message.as_deref(), Some("healthy"));
744 }
745
746 #[test]
747 fn flux_resource_ref_observed_stamps_last_check_at_now() {
748 // Stamp pin: the `last_check` slot is filled with
749 // `Some(<recent Utc>)` at the composer's body. A
750 // regression that dropped the stamp (leaving `None`) or
751 // shifted it to a stale constant would surface here rather
752 // than as silent operator-observed staleness at
753 // `ProcessStatus.flux_resources` panels. Bounds the stamp
754 // to within a generous 5s window of the composer call so
755 // slow CI runners do not false-positive.
756 let before = Utc::now();
757 let r = FluxResourceRef::observed(
758 "v1".to_string(),
759 "K".to_string(),
760 "n".to_string(),
761 "ns".to_string(),
762 false,
763 None,
764 );
765 let after = Utc::now();
766 let stamp = r.last_check.expect("observed must stamp last_check");
767 assert!(stamp >= before, "stamp must be >= before-call `now`");
768 assert!(stamp <= after, "stamp must be <= after-call `now`");
769 }
770
771 #[test]
772 fn flux_resource_ref_observed_round_trips_through_fetch_coords() {
773 // Cross-composer coherence pin: a ref built by `observed`
774 // then unpacked by `fetch_coords` returns the same four
775 // slots in the peer projection's positional order
776 // `(namespace, api_version, kind, name)`. Composition of
777 // the two primitives on the same ref preserves the slot
778 // identity — a regression at either end (a slot swap in
779 // `observed`, or a slot swap in `fetch_coords`) would
780 // surface here rather than as silent drift between the
781 // writer and the reader on the same persisted slice.
782 let r = FluxResourceRef::observed(
783 "helm.toolkit.fluxcd.io/v2".to_string(),
784 "HelmRelease".to_string(),
785 "prometheus-op".to_string(),
786 "monitoring".to_string(),
787 false,
788 Some("applied; awaiting reconciliation".to_string()),
789 );
790 let (ns, av, kind, name) = r.fetch_coords();
791 assert_eq!(ns, "monitoring");
792 assert_eq!(av, "helm.toolkit.fluxcd.io/v2");
793 assert_eq!(kind, "HelmRelease");
794 assert_eq!(name, "prometheus-op");
795 }
796
797 #[test]
798 fn flux_resource_ref_observed_matches_pre_lift_struct_literal_field_for_field() {
799 // Byte-for-byte parity pin against the pre-lift 7-slot
800 // struct-literal spelled at BOTH `phase_machine::
801 // handle_running` and `phase_machine::flux_ref_from_json`.
802 // A regression that reordered any of the six inputs at
803 // the composer's argument list, or that swapped a
804 // `ready`/`message` pair inside the composer's body,
805 // would surface here rather than as silent divergence
806 // between the composer's output and the pre-lift hand-
807 // authored shape every persisted status writer restated.
808 let composed = FluxResourceRef::observed(
809 "source.toolkit.fluxcd.io/v1beta2".to_string(),
810 "OCIRepository".to_string(),
811 "chart-source".to_string(),
812 "flux-system".to_string(),
813 false,
814 Some("applied; awaiting reconciliation".to_string()),
815 );
816 // Hand-authored the same seven slots directly, with a
817 // held-open stamp window across the composer call.
818 let stamped = composed.last_check.expect("stamped");
819 let baseline = FluxResourceRef {
820 api_version: "source.toolkit.fluxcd.io/v1beta2".to_string(),
821 kind: "OCIRepository".to_string(),
822 name: "chart-source".to_string(),
823 namespace: "flux-system".to_string(),
824 ready: false,
825 message: Some("applied; awaiting reconciliation".to_string()),
826 last_check: Some(stamped),
827 };
828 assert_eq!(composed.api_version, baseline.api_version);
829 assert_eq!(composed.kind, baseline.kind);
830 assert_eq!(composed.name, baseline.name);
831 assert_eq!(composed.namespace, baseline.namespace);
832 assert_eq!(composed.ready, baseline.ready);
833 assert_eq!(composed.message, baseline.message);
834 assert_eq!(composed.last_check, baseline.last_check);
835 }
836
837 // ─── FluxResourceRef::pending substrate pins ─────────────────────
838 //
839 // Bind [`FluxResourceRef::pending`] at fail-before-pass-after
840 // granularity so a regression that leaked a non-default status
841 // slot (`ready: true`, `message: Some("something")`, `last_check:
842 // Some(Utc::now())`), swapped two adjacent coordinate slots (all
843 // four are `String` and mechanically interchangeable at the type
844 // level), or diverged from the pre-lift 7-slot struct-literal on
845 // any of the seven fields surfaces HERE rather than as silent
846 // operator-invisible drift at the 3 downstream fixture consumers
847 // (crd.rs `sample_flux_ref`, ssapply.rs `sample_flux_ref_for_diag`,
848 // ssapply.rs `flux_ref_fetch_error_context_matches_pre_lift_...`).
849 //
850 // Each pin is fail-before-pass-after: the primitive did not exist
851 // pre-lift, so any test that invokes it fails to compile pre-lift
852 // and passes post-lift; the byte-identity pins below then bind
853 // the specific shape choice.
854
855 #[test]
856 fn flux_resource_ref_pending_binds_coordinate_slots_by_position() {
857 // Positional pin: the 4-arg constructor binds `(api_version,
858 // kind, name, namespace)` in THAT order, matching the pre-
859 // lift 7-slot struct-literal's declaration order. A regression
860 // that swapped ANY pair of adjacent `String` coordinate slots
861 // (all four are mechanically indistinguishable at the type
862 // level) would surface here rather than as a wire-time 404 at
863 // every downstream Flux fetch consumer that walks
864 // `FluxResourceRef.fetch_coords`.
865 let r = FluxResourceRef::pending(
866 "kustomize.toolkit.fluxcd.io/v1",
867 "Kustomization",
868 "observability-stack",
869 "flux-system",
870 );
871 assert_eq!(r.api_version, "kustomize.toolkit.fluxcd.io/v1");
872 assert_eq!(r.kind, "Kustomization");
873 assert_eq!(r.name, "observability-stack");
874 assert_eq!(r.namespace, "flux-system");
875 }
876
877 #[test]
878 fn flux_resource_ref_pending_defaults_every_status_slot() {
879 // Default-slot pin: the three status slots (`ready`, `message`,
880 // `last_check`) are ALL defaulted at the composer's body — no
881 // wall-clock read, no non-`None` `message` leak, no `ready:
882 // true` regression that would silently un-pend the fixture.
883 // A regression that stamped `Some(Utc::now())` into
884 // `last_check` (matching the sibling `observed` composer's
885 // wall-clock read) would silently defeat the deterministic-
886 // fixture contract the peer partition holds.
887 let r = FluxResourceRef::pending("v1", "K", "n", "ns");
888 assert!(
889 !r.ready,
890 "pending composer must default `ready` to false — non-`false` breaks the pre-observation contract"
891 );
892 assert_eq!(
893 r.message, None,
894 "pending composer must default `message` to None — non-`None` leaks a stale message into the pre-observation seed",
895 );
896 assert_eq!(
897 r.last_check, None,
898 "pending composer must default `last_check` to None — a `Some(_)` leak defeats the deterministic-peer partition against `observed`",
899 );
900 }
901
902 #[test]
903 fn flux_resource_ref_pending_accepts_both_owned_and_borrowed_coordinates() {
904 // The `impl Into<String>` ergonomic contract: both `&'static
905 // str` literals (the fixture-helper sites that spell
906 // coordinates inline) and owned `String` (a future callsite
907 // handing off a dynamically-derived coordinate) round-trip
908 // through the SAME composer signature without widening. A
909 // regression that specialised the signature to one form or
910 // the other would break either the inline-literal helpers or
911 // the owned-`String` downstream consumers.
912 let borrowed: FluxResourceRef = FluxResourceRef::pending("v1", "K", "n", "ns");
913 let owned: FluxResourceRef = FluxResourceRef::pending(
914 "v1".to_string(),
915 "K".to_string(),
916 "n".to_string(),
917 "ns".to_string(),
918 );
919 assert_eq!(borrowed.api_version, owned.api_version);
920 assert_eq!(borrowed.kind, owned.kind);
921 assert_eq!(borrowed.name, owned.name);
922 assert_eq!(borrowed.namespace, owned.namespace);
923 assert_eq!(borrowed.ready, owned.ready);
924 assert_eq!(borrowed.message, owned.message);
925 assert_eq!(borrowed.last_check, owned.last_check);
926 }
927
928 #[test]
929 fn flux_resource_ref_pending_matches_pre_lift_struct_literal_bytewise() {
930 // Byte-for-byte parity pin against the pre-lift 7-slot
931 // struct-literal spelled at ALL THREE hand-authored fixture
932 // sites (crd.rs `sample_flux_ref`, ssapply.rs
933 // `sample_flux_ref_for_diag`, ssapply.rs inline in the
934 // cross-substrate coherence pin's per-case sweep). Sweeps the
935 // three representative coordinate tuples the pre-lift sites
936 // used, so a regression that special-cased any one variant
937 // (a `Kustomization`-only path via `if kind ==
938 // "Kustomization" ...`) surfaces here.
939 let cases = [
940 (
941 "kustomize.toolkit.fluxcd.io/v1",
942 "Kustomization",
943 "observability-stack",
944 "flux-system",
945 ),
946 (
947 "helm.toolkit.fluxcd.io/v2",
948 "HelmRelease",
949 "prometheus-op",
950 "monitoring",
951 ),
952 (
953 "source.toolkit.fluxcd.io/v1beta2",
954 "OCIRepository",
955 "chart-source",
956 "flux-system",
957 ),
958 ];
959 for (av, kind, name, ns) in cases {
960 let composed = FluxResourceRef::pending(av, kind, name, ns);
961 let hand_authored = FluxResourceRef {
962 api_version: av.to_string(),
963 kind: kind.to_string(),
964 name: name.to_string(),
965 namespace: ns.to_string(),
966 ready: false,
967 message: None,
968 last_check: None,
969 };
970 assert_eq!(composed.api_version, hand_authored.api_version);
971 assert_eq!(composed.kind, hand_authored.kind);
972 assert_eq!(composed.name, hand_authored.name);
973 assert_eq!(composed.namespace, hand_authored.namespace);
974 assert_eq!(composed.ready, hand_authored.ready);
975 assert_eq!(composed.message, hand_authored.message);
976 assert_eq!(composed.last_check, hand_authored.last_check);
977 }
978 }
979
980 #[test]
981 fn flux_resource_ref_pending_partitions_the_composer_axis_against_observed() {
982 // Cross-composer partition pin: `pending` and `observed`
983 // both produce `FluxResourceRef` but partition the composer
984 // axis at the (deterministic-fixture, wall-clock-observed)
985 // split — `pending` reads no clock and leaves `last_check:
986 // None`, `observed` reads the wall clock and stamps
987 // `last_check: Some(<recent Utc>)`. A regression that merged
988 // either primitive onto the other (a `pending` that started
989 // stamping `Utc::now()`, an `observed` that started leaving
990 // `last_check: None`) would collapse the partition and
991 // surface here.
992 let p = FluxResourceRef::pending("v1", "K", "n", "ns");
993 assert_eq!(
994 p.last_check, None,
995 "pending is deterministic — no clock read"
996 );
997 let o = FluxResourceRef::observed(
998 "v1".to_string(),
999 "K".to_string(),
1000 "n".to_string(),
1001 "ns".to_string(),
1002 false,
1003 None,
1004 );
1005 assert!(o.last_check.is_some(), "observed reads the wall clock");
1006 }
1007
1008 #[test]
1009 fn flux_resource_ref_pending_composes_with_fetch_coords_at_pre_observation_shape() {
1010 // Cross-composer coherence pin: a ref built by `pending`
1011 // then unpacked by `fetch_coords` returns the same four
1012 // slots in the peer projection's positional order
1013 // `(namespace, api_version, kind, name)`. Composition of
1014 // the two primitives on the same pre-observation ref
1015 // preserves the slot identity — a regression at either end
1016 // (a slot swap in `pending`, or a slot swap in
1017 // `fetch_coords`) would surface here rather than as silent
1018 // drift between the fixture writer and every downstream
1019 // fetch reader.
1020 let r = FluxResourceRef::pending(
1021 "helm.toolkit.fluxcd.io/v2",
1022 "HelmRelease",
1023 "prometheus-op",
1024 "monitoring",
1025 );
1026 let (ns, av, kind, name) = r.fetch_coords();
1027 assert_eq!(ns, "monitoring");
1028 assert_eq!(av, "helm.toolkit.fluxcd.io/v2");
1029 assert_eq!(kind, "HelmRelease");
1030 assert_eq!(name, "prometheus-op");
1031 }
1032
1033 #[test]
1034 fn rendered_resource_coords_namespace_fallback_shares_process_default_const() {
1035 // Byte-identity between the namespace fallback and the workspace-
1036 // wide `Process::DEFAULT_NAMESPACE` const. A regression that spelled
1037 // the fallback as any other string ("kube-system", "", "default-ns")
1038 // would silently drift between the coord-primitive family here and
1039 // the `Process`-borne family in `crd.rs` — surfaces here rather than
1040 // as operator-observed namespace routing skew between the two
1041 // primitive families.
1042 let c = RenderedResourceCoords {
1043 api_version: "v1".into(),
1044 kind: "K".into(),
1045 name: "n".into(),
1046 namespace: None,
1047 };
1048 assert_eq!(c.namespace_or_default(), Process::DEFAULT_NAMESPACE);
1049 }
1050}