Skip to main content

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
213/// Identifying coordinates of a rendered K8s resource — the
214/// `(apiVersion, kind, metadata.name, metadata.namespace)` 4-tuple
215/// every consumer that walks a rendered `serde_json::Value` resource
216/// unwraps by hand pre-lift.
217///
218/// The three K8s API-path segments (`apiVersion`, `kind`,
219/// `metadata.name`) are REQUIRED — a rendered resource missing any
220/// of them cannot be applied via kube-rs's dynamic API surface, so
221/// the extraction fails fast at the boundary rather than as a
222/// downstream `Api::patch` panic. `metadata.namespace` is
223/// intentionally kept as `Option<String>` because different consumers
224/// resolve the fallback differently: `apply_owned` uses the
225/// caller-supplied `namespace: &str` argument (the reconciler already
226/// resolved the target namespace upstream), while `flux_ref_from_json`
227/// records the K8s canonical `"default"` fallback into the persisted
228/// `FluxResourceRef.namespace` slot. The peer method
229/// [`Self::namespace_or_default`] applies the K8s canonical fallback
230/// (`Process::DEFAULT_NAMESPACE = "default"`) for consumers wanting
231/// the same shape [`FluxResourceRef.namespace`] carries.
232///
233/// Pre-lift the 3+1 slot extraction was hand-authored at TWO sites
234/// past the ★★ PRIME-DIRECTIVE ≥ 2 duplication threshold across
235/// `tatara-reconciler`:
236/// * `tatara-reconciler::phase_machine::flux_ref_from_json` — the
237///   post-SSA `FluxResourceRef` builder that persists into
238///   `ProcessStatus.flux_resources`; namespace half fallback-
239///   defaulted to `"default"`.
240/// * `tatara-reconciler::ssapply::apply_owned` — the SSA entry
241///   point that extracts (apiVersion, kind, name) for the
242///   [`kube::Api::patch`] call; namespace half discarded (the
243///   `namespace: &str` argument comes from the caller upstream).
244///
245/// Both callsites restated the same three
246/// `.get(K).and_then(|v| v.as_str()).ok_or_else(|| anyhow!(...))?
247/// .to_string()` incantations with subtly different error wording
248/// (`"resource missing X"` vs `"rendered resource missing X"`); post-
249/// lift both route through this ONE substrate owner with the
250/// canonical `"rendered resource missing X"` wording. A future
251/// addition (case-fold on the group, a rename of the namespace
252/// fallback, a stricter kind gate, a Unicode-safe collation step,
253/// support for `metadata.generateName` as a name fallback) lands at
254/// the primitive's body on the substrate, not at 2 independent
255/// hand-writes across 2 reconciler files.
256///
257/// Namespace fallback const is shared with
258/// [`Process::DEFAULT_NAMESPACE`] — a rename of the K8s canonical
259/// default namespace lands at that ONE workspace-wide const, not at
260/// per-primitive local literals that would drift silently.
261#[derive(Clone, Debug, PartialEq, Eq)]
262pub struct RenderedResourceCoords {
263    /// `apiVersion` — the group+version pair kube-rs uses to resolve
264    /// the `ApiResource` for the SSA call.
265    pub api_version: String,
266    /// `kind` — the resource kind (Kustomization, HelmRelease, …).
267    pub kind: String,
268    /// `metadata.name` — the API-path leaf segment.
269    pub name: String,
270    /// `metadata.namespace` — raw from the resource, `None` when the
271    /// slot is absent (a cluster-scoped resource, or a namespaced
272    /// resource whose namespace was left for the API server to
273    /// substitute). Consumers apply their own fallback:
274    /// [`Self::namespace_or_default`] applies the K8s canonical
275    /// `"default"` (matching what [`FluxResourceRef.namespace`]
276    /// records); other consumers substitute a caller-supplied string
277    /// (see `tatara-reconciler::ssapply::apply_owned`).
278    pub namespace: Option<String>,
279}
280
281impl RenderedResourceCoords {
282    /// Extract the 4-tuple from a rendered K8s resource JSON `Value`.
283    ///
284    /// Fails with a canonical `"rendered resource missing X"` message
285    /// when any of the three required slots (`apiVersion`, `kind`,
286    /// `metadata.name`) is absent or non-string; `metadata.namespace`
287    /// is optional and captured as `None` when absent.
288    ///
289    /// The error wording is pinned by
290    /// [`tests::rendered_resource_coords_error_wording_is_canonical`]
291    /// so a regression that reshaped the message surfaces at the test
292    /// surface rather than as silent drift between the two pre-lift
293    /// call sites (which used subtly different wording — `"resource
294    /// missing X"` in `apply_owned` vs `"rendered resource missing
295    /// X"` in `flux_ref_from_json`).
296    pub fn from_json(res: &Value) -> anyhow::Result<Self> {
297        let api_version = res
298            .get("apiVersion")
299            .and_then(|v| v.as_str())
300            .ok_or_else(|| anyhow::anyhow!("rendered resource missing apiVersion"))?
301            .to_string();
302        let kind = res
303            .get("kind")
304            .and_then(|v| v.as_str())
305            .ok_or_else(|| anyhow::anyhow!("rendered resource missing kind"))?
306            .to_string();
307        let metadata = res.get("metadata");
308        let name = metadata
309            .and_then(|m| m.get("name"))
310            .and_then(|v| v.as_str())
311            .ok_or_else(|| anyhow::anyhow!("rendered resource missing metadata.name"))?
312            .to_string();
313        let namespace = metadata
314            .and_then(|m| m.get("namespace"))
315            .and_then(|v| v.as_str())
316            .map(str::to_string);
317        Ok(Self {
318            api_version,
319            kind,
320            name,
321            namespace,
322        })
323    }
324
325    /// `metadata.namespace` slice with the K8s canonical `"default"`
326    /// fallback applied — matching what [`Process::DEFAULT_NAMESPACE`]
327    /// spells for the `Process`-borne coordinate primitive family
328    /// and what [`FluxResourceRef.namespace`] records into
329    /// `ProcessStatus.flux_resources`.
330    pub fn namespace_or_default(&self) -> &str {
331        self.namespace
332            .as_deref()
333            .unwrap_or(Process::DEFAULT_NAMESPACE)
334    }
335}
336
337/// A boundary condition paired with its current satisfaction state.
338#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
339#[serde(rename_all = "camelCase")]
340pub struct CheckedCondition {
341    #[serde(flatten)]
342    pub condition: Condition,
343    pub satisfied: bool,
344    #[serde(default, skip_serializing_if = "Option::is_none")]
345    pub last_check: Option<DateTime<Utc>>,
346    #[serde(default, skip_serializing_if = "Option::is_none")]
347    pub message: Option<String>,
348}
349
350/// Summary of boundary verification.
351#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
352#[serde(rename_all = "camelCase")]
353pub struct BoundaryStatus {
354    #[serde(default)]
355    pub preconditions: Vec<CheckedCondition>,
356    #[serde(default)]
357    pub postconditions: Vec<CheckedCondition>,
358    /// Absolute deadline for VERIFY (derived from `spec.boundary.timeout`).
359    #[serde(default, skip_serializing_if = "Option::is_none")]
360    pub deadline: Option<DateTime<Utc>>,
361}
362
363/// Summary of compliance checks at the latest attestation.
364#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
365#[serde(rename_all = "camelCase")]
366pub struct ComplianceStatus {
367    #[serde(default, skip_serializing_if = "Option::is_none")]
368    pub baseline: Option<String>,
369    pub satisfied: u32,
370    pub violated: u32,
371    pub total: u32,
372    #[serde(default)]
373    pub violations: Vec<String>,
374}
375
376#[cfg(test)]
377mod tests {
378    use super::*;
379    use serde_json::json;
380
381    // ─── RenderedResourceCoords substrate pins ──────────────────────
382
383    #[test]
384    fn rendered_resource_coords_from_json_extracts_all_four_slots_when_present() {
385        let res = json!({
386            "apiVersion": "kustomize.toolkit.fluxcd.io/v1",
387            "kind": "Kustomization",
388            "metadata": {
389                "name": "observability-stack",
390                "namespace": "flux-system",
391            },
392        });
393        let c = RenderedResourceCoords::from_json(&res).expect("extract");
394        assert_eq!(c.api_version, "kustomize.toolkit.fluxcd.io/v1");
395        assert_eq!(c.kind, "Kustomization");
396        assert_eq!(c.name, "observability-stack");
397        assert_eq!(c.namespace.as_deref(), Some("flux-system"));
398    }
399
400    #[test]
401    fn rendered_resource_coords_from_json_captures_absent_namespace_as_none() {
402        // Cluster-scoped resource — `metadata.namespace` intentionally absent.
403        let res = json!({
404            "apiVersion": "v1",
405            "kind": "Namespace",
406            "metadata": {"name": "demo-test"},
407        });
408        let c = RenderedResourceCoords::from_json(&res).expect("extract");
409        assert_eq!(c.namespace, None);
410        assert_eq!(c.name, "demo-test");
411    }
412
413    #[test]
414    fn rendered_resource_coords_from_json_errors_on_missing_api_version() {
415        let res = json!({"kind": "K", "metadata": {"name": "n"}});
416        let e = RenderedResourceCoords::from_json(&res).expect_err("must error");
417        assert_eq!(e.to_string(), "rendered resource missing apiVersion");
418    }
419
420    #[test]
421    fn rendered_resource_coords_from_json_errors_on_missing_kind() {
422        let res = json!({"apiVersion": "v1", "metadata": {"name": "n"}});
423        let e = RenderedResourceCoords::from_json(&res).expect_err("must error");
424        assert_eq!(e.to_string(), "rendered resource missing kind");
425    }
426
427    #[test]
428    fn rendered_resource_coords_from_json_errors_on_missing_metadata_name() {
429        let res = json!({"apiVersion": "v1", "kind": "K", "metadata": {}});
430        let e = RenderedResourceCoords::from_json(&res).expect_err("must error");
431        assert_eq!(e.to_string(), "rendered resource missing metadata.name");
432    }
433
434    #[test]
435    fn rendered_resource_coords_from_json_errors_on_missing_metadata_object() {
436        // `metadata` absent entirely — same failure as `metadata.name` missing,
437        // because the API-path leaf segment cannot be resolved.
438        let res = json!({"apiVersion": "v1", "kind": "K"});
439        let e = RenderedResourceCoords::from_json(&res).expect_err("must error");
440        assert_eq!(e.to_string(), "rendered resource missing metadata.name");
441    }
442
443    #[test]
444    fn rendered_resource_coords_from_json_errors_on_non_string_slot() {
445        // A numeric `apiVersion` slot falls through the `.as_str()` gate and
446        // triggers the same missing-slot failure as absence — the API-path
447        // segment is not a string.
448        let res = json!({
449            "apiVersion": 42,
450            "kind": "K",
451            "metadata": {"name": "n"},
452        });
453        let e = RenderedResourceCoords::from_json(&res).expect_err("must error");
454        assert_eq!(e.to_string(), "rendered resource missing apiVersion");
455    }
456
457    #[test]
458    fn rendered_resource_coords_error_wording_is_canonical() {
459        // Pins the exact spelling every downstream consumer sees.
460        // Pre-lift wording differed across the two call sites (`"resource
461        // missing X"` in `apply_owned` vs `"rendered resource missing X"` in
462        // `flux_ref_from_json`); post-lift the canonical wording is
463        // `"rendered resource missing X"` at every site.
464        let cases = [
465            (
466                "apiVersion",
467                json!({"kind": "K", "metadata": {"name": "n"}}),
468            ),
469            (
470                "kind",
471                json!({"apiVersion": "v1", "metadata": {"name": "n"}}),
472            ),
473            (
474                "metadata.name",
475                json!({"apiVersion": "v1", "kind": "K", "metadata": {}}),
476            ),
477        ];
478        for (slot, res) in cases {
479            let e = RenderedResourceCoords::from_json(&res).expect_err("must error");
480            assert_eq!(
481                e.to_string(),
482                format!("rendered resource missing {slot}"),
483                "slot {slot} error must be canonical"
484            );
485        }
486    }
487
488    #[test]
489    fn rendered_resource_coords_namespace_or_default_returns_slice_when_some() {
490        let c = RenderedResourceCoords {
491            api_version: "v1".into(),
492            kind: "K".into(),
493            name: "n".into(),
494            namespace: Some("prod".into()),
495        };
496        assert_eq!(c.namespace_or_default(), "prod");
497    }
498
499    #[test]
500    fn rendered_resource_coords_namespace_or_default_falls_back_when_none() {
501        let c = RenderedResourceCoords {
502            api_version: "v1".into(),
503            kind: "K".into(),
504            name: "n".into(),
505            namespace: None,
506        };
507        assert_eq!(c.namespace_or_default(), Process::DEFAULT_NAMESPACE);
508        assert_eq!(c.namespace_or_default(), "default");
509    }
510
511    // ─── FluxResourceRef::fetch_coords substrate pins ─────────────
512    //
513    // The 4-slot `(&namespace, &api_version, &kind, &name)` borrow
514    // projection lifts the pre-existing 5-slot `ssapply::fetch(client,
515    // &r.namespace, &r.api_version, &r.kind, &r.name)` splat that
516    // recurred at TWO hand-authored sites in
517    // `tatara-reconciler::phase_machine` (`handle_running`,
518    // `handle_attested`) past the ★★ PRIME-DIRECTIVE ≥ 2 duplication
519    // trigger. These pins bind the slot order at fail-before-pass-
520    // after granularity so a regression that swapped `namespace` and
521    // `api_version` (both `String`, mechanically interchangeable to
522    // a bad refactor) surfaces HERE rather than as a silent wire-time
523    // 404 at every downstream Flux fetch consumer.
524
525    fn sample_flux_ref() -> FluxResourceRef {
526        // Slot values are deliberately distinct so a swap between any
527        // two adjacent tuple positions surfaces as an equality
528        // failure at the assertion site — a slot-inversion regression
529        // cannot masquerade as identity by accident.
530        FluxResourceRef {
531            api_version: "kustomize.toolkit.fluxcd.io/v1".to_string(),
532            kind: "Kustomization".to_string(),
533            name: "observability-stack".to_string(),
534            namespace: "flux-system".to_string(),
535            ready: true,
536            message: None,
537            last_check: None,
538        }
539    }
540
541    #[test]
542    fn flux_resource_ref_fetch_coords_binds_slots_by_position() {
543        // Positional pin: the 4-tuple return binds
544        // `(namespace, api_version, kind, name)` in THAT order,
545        // matching the raw `ssapply::fetch(client, ns, av, kind,
546        // name)` positional signature every pre-lift callsite splatted
547        // into. A regression that swapped ANY pair of adjacent slots
548        // (all four axes are `String` and mechanically
549        // indistinguishable at the type level) would surface here
550        // rather than as an operator-visible wire-form 404 at every
551        // downstream fetch consumer.
552        let r = sample_flux_ref();
553        let (ns, av, kind, name) = r.fetch_coords();
554        assert_eq!(ns, "flux-system", "position 0 must be namespace");
555        assert_eq!(
556            av, "kustomize.toolkit.fluxcd.io/v1",
557            "position 1 must be api_version"
558        );
559        assert_eq!(kind, "Kustomization", "position 2 must be kind");
560        assert_eq!(name, "observability-stack", "position 3 must be name");
561    }
562
563    #[test]
564    fn flux_resource_ref_fetch_coords_returns_borrows_of_owned_slots() {
565        // Borrow-discipline pin: the 4-tuple returns `&str` borrows
566        // of the enclosing `FluxResourceRef`'s owned `String` slots —
567        // NOT a fresh allocation or a clone. A regression that
568        // switched the projection to owned strings (via `.clone()` or
569        // `format!`) would defeat the zero-copy contract and would
570        // surface here via pointer-identity comparison.
571        let r = sample_flux_ref();
572        let (ns, av, kind, name) = r.fetch_coords();
573        assert!(std::ptr::eq(ns.as_ptr(), r.namespace.as_ptr()));
574        assert!(std::ptr::eq(av.as_ptr(), r.api_version.as_ptr()));
575        assert!(std::ptr::eq(kind.as_ptr(), r.kind.as_ptr()));
576        assert!(std::ptr::eq(name.as_ptr(), r.name.as_ptr()));
577    }
578
579    #[test]
580    fn flux_resource_ref_fetch_coords_is_a_pure_borrow_projection() {
581        // Purity pin: calling the projection twice on the same ref
582        // returns byte-identical slices (same pointer, same length).
583        // A regression that introduced state — a lazy-cached slot
584        // computed on first call, a normalization step that ran once
585        // and cached — would surface here rather than as silent drift
586        // between the VERIFY-phase and ATTEST-heartbeat consumers on
587        // the SAME ref within one reconcile pass.
588        let r = sample_flux_ref();
589        let a = r.fetch_coords();
590        let b = r.fetch_coords();
591        assert!(std::ptr::eq(a.0.as_ptr(), b.0.as_ptr()));
592        assert!(std::ptr::eq(a.1.as_ptr(), b.1.as_ptr()));
593        assert!(std::ptr::eq(a.2.as_ptr(), b.2.as_ptr()));
594        assert!(std::ptr::eq(a.3.as_ptr(), b.3.as_ptr()));
595    }
596
597    #[test]
598    fn flux_resource_ref_fetch_coords_ignores_status_slots() {
599        // Coverage pin: the projection exposes ONLY the four API-path
600        // slots the fetch call requires; the ref's status slots
601        // (`ready`, `message`, `last_check`) are deliberately absent
602        // from the tuple. The fetch signature admits four `&str`
603        // slots, and the projection carries EXACTLY those four — no
604        // silent widening that would surface as an arity mismatch at
605        // every downstream `fetch(...)` call.
606        let r = sample_flux_ref();
607        let coords = r.fetch_coords();
608        assert_eq!(
609            std::mem::size_of_val(&coords),
610            std::mem::size_of::<(&str, &str, &str, &str)>(),
611            "the 4-tuple width must match the raw fetch signature's four `&str` slots"
612        );
613    }
614
615    // ─── FluxResourceRef::observed substrate pins ─────────────────
616    //
617    // The 6-arg composer stamps `last_check` at ONE substrate site
618    // (the pre-lift 7-slot struct-literal restated `Some(chrono::
619    // Utc::now())` at TWO hand-authored sites in
620    // `tatara-reconciler::phase_machine` — `handle_running`'s per-
621    // ref VERIFY rebuild and `flux_ref_from_json`'s post-SSA
622    // seeder). These pins bind the six input slots by position so a
623    // regression that swapped `api_version` and `kind` (both
624    // `String`, mechanically interchangeable to a bad refactor)
625    // surfaces HERE rather than as a silent wire-time 404 at every
626    // downstream fetch consumer.
627    //
628    // Every test constructs distinct values across the four
629    // `String` coordinate slots so a slot swap fails structurally
630    // rather than by accident of matching literals.
631
632    #[test]
633    fn flux_resource_ref_observed_binds_slots_by_position() {
634        // Positional pin: the 6-arg constructor binds
635        // `(api_version, kind, name, namespace, ready, message)`
636        // in THAT order, matching the pre-lift 7-slot struct-
637        // literal's declaration order. A regression that swapped
638        // ANY pair of adjacent `String` coordinate slots (all four
639        // are mechanically indistinguishable at the type level)
640        // would surface here rather than as a wire-time 404 at
641        // every downstream Flux fetch consumer.
642        let r = FluxResourceRef::observed(
643            "kustomize.toolkit.fluxcd.io/v1".to_string(),
644            "Kustomization".to_string(),
645            "observability-stack".to_string(),
646            "flux-system".to_string(),
647            true,
648            Some("healthy".to_string()),
649        );
650        assert_eq!(r.api_version, "kustomize.toolkit.fluxcd.io/v1");
651        assert_eq!(r.kind, "Kustomization");
652        assert_eq!(r.name, "observability-stack");
653        assert_eq!(r.namespace, "flux-system");
654        assert!(r.ready);
655        assert_eq!(r.message.as_deref(), Some("healthy"));
656    }
657
658    #[test]
659    fn flux_resource_ref_observed_stamps_last_check_at_now() {
660        // Stamp pin: the `last_check` slot is filled with
661        // `Some(<recent Utc>)` at the composer's body. A
662        // regression that dropped the stamp (leaving `None`) or
663        // shifted it to a stale constant would surface here rather
664        // than as silent operator-observed staleness at
665        // `ProcessStatus.flux_resources` panels. Bounds the stamp
666        // to within a generous 5s window of the composer call so
667        // slow CI runners do not false-positive.
668        let before = Utc::now();
669        let r = FluxResourceRef::observed(
670            "v1".to_string(),
671            "K".to_string(),
672            "n".to_string(),
673            "ns".to_string(),
674            false,
675            None,
676        );
677        let after = Utc::now();
678        let stamp = r.last_check.expect("observed must stamp last_check");
679        assert!(stamp >= before, "stamp must be >= before-call `now`");
680        assert!(stamp <= after, "stamp must be <= after-call `now`");
681    }
682
683    #[test]
684    fn flux_resource_ref_observed_round_trips_through_fetch_coords() {
685        // Cross-composer coherence pin: a ref built by `observed`
686        // then unpacked by `fetch_coords` returns the same four
687        // slots in the peer projection's positional order
688        // `(namespace, api_version, kind, name)`. Composition of
689        // the two primitives on the same ref preserves the slot
690        // identity — a regression at either end (a slot swap in
691        // `observed`, or a slot swap in `fetch_coords`) would
692        // surface here rather than as silent drift between the
693        // writer and the reader on the same persisted slice.
694        let r = FluxResourceRef::observed(
695            "helm.toolkit.fluxcd.io/v2".to_string(),
696            "HelmRelease".to_string(),
697            "prometheus-op".to_string(),
698            "monitoring".to_string(),
699            false,
700            Some("applied; awaiting reconciliation".to_string()),
701        );
702        let (ns, av, kind, name) = r.fetch_coords();
703        assert_eq!(ns, "monitoring");
704        assert_eq!(av, "helm.toolkit.fluxcd.io/v2");
705        assert_eq!(kind, "HelmRelease");
706        assert_eq!(name, "prometheus-op");
707    }
708
709    #[test]
710    fn flux_resource_ref_observed_matches_pre_lift_struct_literal_field_for_field() {
711        // Byte-for-byte parity pin against the pre-lift 7-slot
712        // struct-literal spelled at BOTH `phase_machine::
713        // handle_running` and `phase_machine::flux_ref_from_json`.
714        // A regression that reordered any of the six inputs at
715        // the composer's argument list, or that swapped a
716        // `ready`/`message` pair inside the composer's body,
717        // would surface here rather than as silent divergence
718        // between the composer's output and the pre-lift hand-
719        // authored shape every persisted status writer restated.
720        let composed = FluxResourceRef::observed(
721            "source.toolkit.fluxcd.io/v1beta2".to_string(),
722            "OCIRepository".to_string(),
723            "chart-source".to_string(),
724            "flux-system".to_string(),
725            false,
726            Some("applied; awaiting reconciliation".to_string()),
727        );
728        // Hand-authored the same seven slots directly, with a
729        // held-open stamp window across the composer call.
730        let stamped = composed.last_check.expect("stamped");
731        let baseline = FluxResourceRef {
732            api_version: "source.toolkit.fluxcd.io/v1beta2".to_string(),
733            kind: "OCIRepository".to_string(),
734            name: "chart-source".to_string(),
735            namespace: "flux-system".to_string(),
736            ready: false,
737            message: Some("applied; awaiting reconciliation".to_string()),
738            last_check: Some(stamped),
739        };
740        assert_eq!(composed.api_version, baseline.api_version);
741        assert_eq!(composed.kind, baseline.kind);
742        assert_eq!(composed.name, baseline.name);
743        assert_eq!(composed.namespace, baseline.namespace);
744        assert_eq!(composed.ready, baseline.ready);
745        assert_eq!(composed.message, baseline.message);
746        assert_eq!(composed.last_check, baseline.last_check);
747    }
748
749    #[test]
750    fn rendered_resource_coords_namespace_fallback_shares_process_default_const() {
751        // Byte-identity between the namespace fallback and the workspace-
752        // wide `Process::DEFAULT_NAMESPACE` const. A regression that spelled
753        // the fallback as any other string ("kube-system", "", "default-ns")
754        // would silently drift between the coord-primitive family here and
755        // the `Process`-borne family in `crd.rs` — surfaces here rather than
756        // as operator-observed namespace routing skew between the two
757        // primitive families.
758        let c = RenderedResourceCoords {
759            api_version: "v1".into(),
760            kind: "K".into(),
761            name: "n".into(),
762            namespace: None,
763        };
764        assert_eq!(c.namespace_or_default(), Process::DEFAULT_NAMESPACE);
765    }
766}