Skip to main content

tatara_process/
crd.rs

1//! The `Process` CRD — `tatara.pleme.io/v1alpha1`.
2
3use chrono::{DateTime, Utc};
4use kube::CustomResource;
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7use tatara_lisp::DeriveTataraDomain;
8
9use crate::attestation::ProcessAttestation;
10use crate::boundary::Boundary;
11use crate::classification::Classification;
12use crate::compliance::ComplianceSpec;
13use crate::encapsulates::EncapsulatesSpec;
14use crate::identity::Identity;
15use crate::intent::Intent;
16use crate::lifetime::Lifetime;
17use crate::phase::ProcessPhase;
18use crate::routing::RoutingSpec;
19use crate::signal::ProcessSignal;
20use crate::spec::{DependsOn, IdentitySpec, SignalPolicy};
21use crate::status::{BoundaryStatus, ComplianceStatus, FluxResourceRef, ProcessCondition};
22
23/// Process — one element of the tatara convergence lattice, reconciled as a Unix process.
24///
25/// ```yaml
26/// apiVersion: tatara.pleme.io/v1alpha1
27/// kind: Process
28/// metadata:
29///   name: observability-stack
30///   namespace: seph
31/// spec:
32///   identity:
33///     parent: seph.1
34///   classification:
35///     pointType: Gate
36///     substrate: Observability
37///   intent:
38///     nix:
39///       flakeRef: github:pleme-io/k8s?dir=shared/infrastructure
40///       attribute: observability
41///   compliance:
42///     baseline: fedramp-moderate
43///     bindings:
44///       - framework: nist-800-53
45///         controlId: SC-7
46///         phase: AtBoundary
47///   dependsOn:
48///     - name: secret-injection
49/// ```
50#[derive(CustomResource, DeriveTataraDomain, Clone, Debug, Deserialize, Serialize, JsonSchema)]
51#[kube(
52    group = "tatara.pleme.io",
53    version = "v1alpha1",
54    kind = "Process",
55    plural = "processes",
56    shortname = "proc",
57    namespaced,
58    status = "ProcessStatus",
59    printcolumn = r#"{"name":"PID","type":"string","jsonPath":".status.pid"}"#,
60    printcolumn = r#"{"name":"Phase","type":"string","jsonPath":".status.phase"}"#,
61    printcolumn = r#"{"name":"Type","type":"string","jsonPath":".spec.classification.pointType"}"#,
62    printcolumn = r#"{"name":"Substrate","type":"string","jsonPath":".spec.classification.substrate"}"#,
63    printcolumn = r#"{"name":"Gen","type":"integer","jsonPath":".status.attestation.generation"}"#,
64    printcolumn = r#"{"name":"Age","type":"date","jsonPath":".metadata.creationTimestamp"}"#
65)]
66#[serde(rename_all = "camelCase")]
67#[tatara(keyword = "defpoint")]
68pub struct ProcessSpec {
69    /// Identity (parent, name override).
70    #[serde(default)]
71    pub identity: IdentitySpec,
72
73    /// Lattice position (6 dimensions).
74    pub classification: Classification,
75
76    /// Where rendered artifacts come from. Exactly one variant must be set.
77    pub intent: Intent,
78
79    /// Boundary predicates (preconditions / postconditions).
80    #[serde(default)]
81    pub boundary: Boundary,
82
83    /// Compliance bindings + baseline.
84    #[serde(default)]
85    pub compliance: ComplianceSpec,
86
87    /// Lattice dependencies — must reach phase before we proceed.
88    #[serde(default)]
89    pub depends_on: Vec<DependsOn>,
90
91    /// Signal policy (grace, SIGHUP strategy, start-suspended).
92    #[serde(default)]
93    pub signals: SignalPolicy,
94
95    /// Lifetime — `Permanent` (default, re-converging) or `Ephemeral`
96    /// (auto-SIGTERM per `teardown_policy` + TTL clock).
97    #[serde(default, skip_serializing_if = "Lifetime::is_default")]
98    pub lifetime: Lifetime,
99
100    /// External edges — DNS + Ingress. When `None`, the Process is
101    /// internal-only (matches today's default). See
102    /// [`crate::routing`] for the full shape.
103    #[serde(default, skip_serializing_if = "Option::is_none")]
104    pub routing: Option<RoutingSpec>,
105
106    /// Pre-existing in-cluster state this Process wraps. When `None`,
107    /// the Process is greenfield (Manage mode implicitly applied to
108    /// nothing pre-existing). See [`crate::encapsulates`] for the
109    /// three modes (Manage / Adopt / Observe).
110    #[serde(default, skip_serializing_if = "Option::is_none")]
111    pub encapsulates: Option<EncapsulatesSpec>,
112
113    /// Soft-suspend marker — reconciler treats as SIGSTOP.
114    /// Same effect as delivering SIGSTOP, but persistent across restarts.
115    #[serde(default)]
116    pub suspended: bool,
117}
118
119// Coordinate primitives — the `(namespace, name)` pair every downstream
120// composer (annotation writers, claim arbiter, boundary evaluator,
121// render owner-metadata seed) pulled by hand from `Process.metadata`
122// pre-lift, each restating the same two `Option<String>`-to-`&str`
123// unwrap incantations with the same two workspace-wide fallback
124// strings sprayed inline. Post-lift the pair lives at ONE substrate
125// primitive on `Process` — a future normalization (case-fold,
126// unicode-safe collation, cross-cluster prefix, a rename of either
127// fallback) lands here and every downstream composer inherits the
128// upgrade mechanically. Peer to `qualified_process_ref` in
129// `tatara-reconciler::ssapply`, whose two `&str` arguments are
130// exactly the pair `Process::coordinates_or_defaults` returns.
131impl Process {
132    /// The K8s canonical default namespace — the fallback every
133    /// consumer of a `Process` whose `metadata.namespace` is `None`
134    /// substitutes. Matches the string K8s itself substitutes on
135    /// namespaced resource writes with no explicit namespace.
136    pub const DEFAULT_NAMESPACE: &'static str = "default";
137
138    /// Workspace-wide fallback for a `Process`'s `metadata.name` when
139    /// it is `None` — the sentinel every annotation writer, claim
140    /// arbiter, and owner-metadata seed substitutes so downstream
141    /// grepping / label-selecting sees a stable spelling rather than
142    /// a per-callsite ad-hoc placeholder (`""`, `"<unnamed>"`, or the
143    /// empty `unwrap_or_default()` fallback). A Process authored
144    /// through the reconciler's fork path always has a name; this
145    /// constant covers the surface where an untyped `Process` value
146    /// (test fixture, dynamic API response, adopted resource pre-
147    /// name-resolution) surfaces without one.
148    pub const UNNAMED_PLACEHOLDER: &'static str = "unnamed";
149
150    /// Namespace slice with the [`Self::DEFAULT_NAMESPACE`] fallback
151    /// applied — the ONE-line collapse of the `metadata.namespace
152    /// .as_deref().unwrap_or("default")` incantation every consumer
153    /// spelled by hand pre-lift.
154    ///
155    /// Peer to [`Self::name_or_placeholder`] on the (metadata slot ×
156    /// fallback shape) axis; both compose through
157    /// [`Self::coordinates_or_defaults`] when a consumer needs the
158    /// pair together (annotation writers, claim-arbiter row builders,
159    /// render owner-metadata seed).
160    pub fn namespace_or_default(&self) -> &str {
161        self.metadata
162            .namespace
163            .as_deref()
164            .unwrap_or(Self::DEFAULT_NAMESPACE)
165    }
166
167    /// Name slice with the [`Self::UNNAMED_PLACEHOLDER`] fallback
168    /// applied — the ONE-line collapse of the `metadata.name.as_deref
169    /// ().unwrap_or("unnamed")` incantation every consumer spelled by
170    /// hand pre-lift.
171    ///
172    /// Peer to [`Self::namespace_or_default`] on the (metadata slot ×
173    /// fallback shape) axis; both compose through
174    /// [`Self::coordinates_or_defaults`] when a consumer needs the
175    /// pair together.
176    pub fn name_or_placeholder(&self) -> &str {
177        self.metadata
178            .name
179            .as_deref()
180            .unwrap_or(Self::UNNAMED_PLACEHOLDER)
181    }
182
183    /// `(namespace, name)` coordinates with the workspace-wide default
184    /// fallbacks applied — the ONE-line collapse of the paired
185    /// `metadata.namespace.as_deref().unwrap_or("default")` +
186    /// `metadata.name.as_deref().unwrap_or("unnamed")` extraction
187    /// every downstream composer restated by hand pre-lift.
188    ///
189    /// Return-tuple order matches the axis order of the substrate's
190    /// paired-composer primitive
191    /// `tatara_reconciler::ssapply::qualified_process_ref(ns, name)`:
192    /// the (namespace, name) pair this method returns feeds that
193    /// primitive positionally without an axis-swap step.
194    pub fn coordinates_or_defaults(&self) -> (&str, &str) {
195        (self.namespace_or_default(), self.name_or_placeholder())
196    }
197
198    /// `(namespace, name)` coordinates as owned `String`s, with the
199    /// namespace half fallback-defaulted to [`Self::DEFAULT_NAMESPACE`]
200    /// but the name half REQUIRED — an [`anyhow::Error`] is returned
201    /// when `metadata.name` is absent, because "unnamed" is a display
202    /// placeholder, not a valid K8s API path segment. Fed straight into
203    /// kube-rs API calls (`Api::patch`, `Api::delete`, `Api::get`) that
204    /// take owned `String` arguments; the [`Self::DEFAULT_NAMESPACE`]
205    /// fallback matches what K8s itself substitutes on namespaced
206    /// resource writes with no explicit namespace, so the surface is
207    /// safe against a `Process` whose `metadata.namespace` slot is
208    /// absent (test fixture, dynamic API response pre-defaulting) but
209    /// refuses to guess a name.
210    ///
211    /// Peer to [`Self::coordinates_or_defaults`] on the (return-form ×
212    /// name gate) axis pair:
213    /// * borrow + name-defaulted → `coordinates_or_defaults` (display,
214    ///   annotation writers, ownership-tag composers — every consumer
215    ///   whose downstream drops `"unnamed"` in place of a missing name
216    ///   without an operator-visible failure);
217    /// * owned + name-required → this method (kube-rs API calls —
218    ///   every consumer whose downstream must NOT silently substitute
219    ///   a placeholder for the API call target, because the caller is
220    ///   about to `patch`/`delete`/`get` at `metadata.name`).
221    ///
222    /// The error wording is pinned by
223    /// [`tests::owned_coordinates_or_err_error_message_matches_pre_lift_reconciler_wording`]
224    /// to match the exact spelling every pre-lift `tatara-reconciler`
225    /// helper produced (`"Process has no metadata.name"`) so log-line
226    /// / test greps that anchored on that wording keep matching post-
227    /// lift, and no operator-visible message drift lands as a side
228    /// effect of the substrate move.
229    pub fn owned_coordinates_or_err(&self) -> anyhow::Result<(String, String)> {
230        let ns = self
231            .metadata
232            .namespace
233            .clone()
234            .unwrap_or_else(|| Self::DEFAULT_NAMESPACE.into());
235        let name = self
236            .metadata
237            .name
238            .clone()
239            .ok_or_else(|| anyhow::anyhow!("Process has no metadata.name"))?;
240        Ok((ns, name))
241    }
242
243    /// `(namespace, name)` coordinates in the BORROW + NAME-REQUIRED
244    /// corner of the primitive family — namespace half falls back to
245    /// [`Self::DEFAULT_NAMESPACE`], but the name half is REQUIRED
246    /// (`None` on a `Process` whose `metadata.name` is absent, so the
247    /// caller stops with an `else { continue; }` / `else { return
248    /// …; }` guard rather than proceeding with the empty-string
249    /// sentinel every pre-lift consumer had to spell inline).
250    ///
251    /// Peer to [`Self::coordinates_or_defaults`] +
252    /// [`Self::owned_coordinates_or_err`] on the (return-form ×
253    /// name-gate) axis pair — closes the corner the family previously
254    /// left open:
255    ///
256    /// * borrow + name-defaulted → [`Self::coordinates_or_defaults`]
257    ///   (annotation writers, render owner-metadata seed — consumers
258    ///   whose downstream tolerates the `"unnamed"` display placeholder
259    ///   without operator-visible failure);
260    /// * borrow + name-required → **this method** (claim-arbiter
261    ///   probes, child-Process delete-fan-out — consumers that need a
262    ///   real API-path leaf and cleanly SKIP the row when the name is
263    ///   absent rather than issuing a K8s call with an empty-string
264    ///   name argument);
265    /// * owned + name-required → [`Self::owned_coordinates_or_err`]
266    ///   (kube-rs API-path calls — consumers whose downstream requires
267    ///   owned `String` arguments and rejects the missing-name corner
268    ///   with a load-bearing error message).
269    ///
270    /// The primitive family's `None`-on-missing-name semantics
271    /// intentionally differs from [`Self::owned_coordinates_or_err`]'s
272    /// error-on-missing-name semantics: the caller sites for this form
273    /// (child-Process fan-out, claim-arbiter row probes) are non-fatal
274    /// SKIPS rather than reportable failures — an `Option::None` at
275    /// the primitive lets the caller thread that "skip" through a
276    /// let-else without stringifying / logging an anyhow chain per
277    /// missing-name occurrence.
278    ///
279    /// The namespace fallback matches [`Self::coordinates_or_defaults`]
280    /// (via [`Self::namespace_or_default`]), so a consumer that
281    /// switches between the two borrow-form primitives based on its
282    /// name-gate need never sees a different namespace-fallback string
283    /// as a side effect.
284    pub fn coordinates_or_none(&self) -> Option<(&str, &str)> {
285        let name = self.metadata.name.as_deref()?;
286        Some((self.namespace_or_default(), name))
287    }
288
289    /// Borrowed slice of the FluxCD resources this Process's status
290    /// currently persists at `status.flux_resources`, with the
291    /// missing-`status` corner collapsed to an empty slice — the ONE-
292    /// line collapse of the paired `self.status.as_ref().map(|s|
293    /// s.flux_resources.clone()).unwrap_or_default()` incantation
294    /// every VERIFY-phase / ATTEST-heartbeat consumer restated by hand
295    /// pre-lift.
296    ///
297    /// Pre-lift the 5-line `.status.as_ref().map(|s| s.flux_resources
298    /// .clone()).unwrap_or_default()` chain was hand-authored at TWO
299    /// sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication threshold in
300    /// `tatara-reconciler::phase_machine`:
301    /// * `handle_running` — the VERIFY-phase per-ref readiness probe
302    ///   seed that walks every ref through
303    ///   [`crate::status::FluxResourceRef::fetch_coords`] via
304    ///   `ssapply::fetch_flux_ref` and rebuilds an updated
305    ///   `Vec<FluxResourceRef>` with `ready` + `message` + `last_check`
306    ///   observed at reconcile time.
307    /// * `handle_attested` — the ATTEST-heartbeat drift detector that
308    ///   short-circuits on the first non-Ready ref via
309    ///   `ssapply::fetch_flux_ref` + `ssapply::ready_condition`.
310    ///
311    /// Both sites walked the SAME 5-line chain — clone the vector
312    /// eagerly for the length of the reconcile pass, then iterate it
313    /// by reference — even though neither site ever mutates the vector
314    /// nor keeps it alive past the enclosing async fn. Post-lift both
315    /// callers borrow the slice directly from `self.status`; the two
316    /// pre-lift `.clone()` calls disappear because the slice lives for
317    /// the borrow of `&self`, and both call sites' subsequent
318    /// downstream calls (`ssapply::fetch_flux_ref` / the
319    /// `patch::patch_process_status` write) do not touch the borrowed
320    /// `p: &Process`, so the borrow lifetime holds.
321    ///
322    /// Return-form axis: `&[FluxResourceRef]` mirrors the existing
323    /// borrow-first discipline every pre-lift consumer already
324    /// iterated by reference (`for r in &refs`), and the shape of
325    /// [`crate::status::FluxResourceRef::fetch_coords`]'s per-ref
326    /// borrow projection extends mechanically to the slice-level
327    /// projection here. The missing-`status` corner collapses to the
328    /// empty slice `&[]` so `.is_empty()` / `.len()` / iteration all
329    /// behave identically on a `Process` whose status is `None` and
330    /// on one whose status carries an empty `flux_resources` slot —
331    /// matching what the pre-lift `.unwrap_or_default()` produced
332    /// (an empty `Vec`).
333    ///
334    /// A future normalization step (a per-ref canonicalization pass
335    /// that skips duplicated refs, an owner-filter that returns only
336    /// refs stamped with the CURRENT `metadata.generation`, a
337    /// staleness gate that drops refs whose `last_check` predates a
338    /// reconcile deadline) lands at ONE substrate method here and
339    /// both downstream consumers pick up the upgrade mechanically —
340    /// no per-callsite hand-edit at `handle_running` /
341    /// `handle_attested`.
342    ///
343    /// Sibling to the [`Self::coordinates_or_none`] borrow-first
344    /// primitive on the metadata axis; this method opens the
345    /// analogous borrow-first primitive on the status-projection
346    /// axis. Future status projections (`observed_attestation` on
347    /// the attestation-chain axis, `observed_pid` on the PID axis,
348    /// `observed_children` on the child-fan-out axis) land as peer
349    /// methods on this same axis.
350    ///
351    /// Theory anchor: THEORY.md §VI.1 (generation over composition —
352    /// the 5-line status-projection chain recurred at two hand-
353    /// authored sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication
354    /// trigger, and is lifted to ONE owner here). THEORY.md §II.1
355    /// invariant 5 (composition preserves proofs — the pins bind the
356    /// missing-`status` corner + the slice-lifetime borrow discipline
357    /// + the byte-identical parity with the pre-lift 5-line chain, so
358    /// a regression that drifted any of the three surfaces at
359    /// `tests::observed_flux_resources_*` rather than as silent
360    /// operator-facing skew between the VERIFY-phase and ATTEST-
361    /// heartbeat consumers).
362    pub fn observed_flux_resources(&self) -> &[FluxResourceRef] {
363        self.status
364            .as_ref()
365            .map(|s| s.flux_resources.as_slice())
366            .unwrap_or(&[])
367    }
368
369    /// The borrow-form status-projection primitive on the PID axis:
370    /// returns the hierarchical PID path (e.g. `"seph.1.7"`) the
371    /// reconciler currently persists at `status.pid`, with BOTH the
372    /// missing-`status` corner AND the empty-slot corner collapsed
373    /// to `None` — the ONE-liner collapse of the paired
374    /// `self.status.as_ref().and_then(|s| s.pid.clone())` incantation
375    /// every consumer restated by hand pre-lift.
376    ///
377    /// Pre-lift the 3-line `.status.as_ref().and_then(|s| s.pid
378    /// .clone())` chain was hand-authored at TWO sites past the ★★
379    /// PRIME-DIRECTIVE ≥ 2 duplication threshold in
380    /// `tatara-reconciler::phase_machine`:
381    /// * `handle_forking` — the ALLOCATE-PID gate that short-
382    ///   circuits the PID allocator when the reconciler already
383    ///   assigned a PID on a prior reconcile pass (pre-lift the
384    ///   chain composed with `.is_some()` and threw the clone away
385    ///   without ever reading the string).
386    /// * `handle_exiting` — the SIGTERM cascade that enumerates
387    ///   child Processes and terminates them by matching each
388    ///   child's `spec.identity.parent` against the PID this Process
389    ///   currently owns (pre-lift the chain bound an owned
390    ///   `Option<String>` and threaded `pid.as_str()` into the
391    ///   downstream `.as_deref() == Some(...)` comparator).
392    ///
393    /// Both sites walked the SAME 3-line chain — clone the `String`
394    /// eagerly, then either drop it (the `handle_forking` gate) or
395    /// re-borrow it through `.as_str()` (the `handle_exiting`
396    /// comparator) — even though neither site ever mutates the PID
397    /// nor keeps it alive past the enclosing async fn. Post-lift
398    /// both callers borrow the PID directly from `self.status`; the
399    /// pre-lift `.clone()` at both sites disappears because the
400    /// `&str` lives for the borrow of `&self`, and both call sites'
401    /// subsequent downstream calls (the K8s API list/patch, the
402    /// child-Process comparator) do not touch the borrowed
403    /// `p: &Process`, so the borrow lifetime holds.
404    ///
405    /// Return-form axis: `Option<&str>` mirrors the existing
406    /// borrow-first discipline every pre-lift consumer already
407    /// re-borrowed through `.as_str()` before use, and the shape of
408    /// [`Self::coordinates_or_none`]'s `Option<(&str, &str)>`
409    /// projection extends mechanically to the single-slot
410    /// projection here. The missing-`status` corner AND the
411    /// populated-status-with-`pid=None` corner BOTH collapse to
412    /// `None` so `.is_some()` / `if let Some(_)` / `.map(...)`
413    /// behave identically on a `Process` whose status is `None`
414    /// and on one whose status carries an unpopulated `pid` slot —
415    /// matching what the pre-lift `.and_then(...)` chain produced.
416    ///
417    /// A future normalization step (a per-slot canonicalization
418    /// pass that rejects malformed hierarchical PIDs, a
419    /// generation-filter that returns `None` for a PID stamped
420    /// with a stale `metadata.generation`, a staleness gate that
421    /// drops a PID whose observing `phase_since` predates a
422    /// reconcile deadline) lands at ONE substrate method here and
423    /// both downstream consumers pick up the upgrade mechanically
424    /// — no per-callsite hand-edit at `handle_forking` /
425    /// `handle_exiting`.
426    ///
427    /// Sibling to the peer [`Self::observed_flux_resources`]
428    /// borrow-first primitive on the flux-resources axis; both
429    /// methods compose the same missing-`status` fallback +
430    /// borrow-form return-shape skeleton on distinct
431    /// `ProcessStatus` slots. Future status projections
432    /// (`observed_parent` on the parent-pointer axis,
433    /// `observed_message` on the human-readable-status axis,
434    /// `observed_attestation` on the attestation-chain axis) land
435    /// as peer methods on this same axis.
436    ///
437    /// Theory anchor: THEORY.md §VI.1 (generation over
438    /// composition — the 3-line status-projection chain recurred
439    /// at two hand-authored sites past the ★★ PRIME-DIRECTIVE ≥ 2
440    /// duplication trigger, and is lifted to ONE owner here).
441    /// THEORY.md §II.1 invariant 5 (composition preserves proofs —
442    /// the pins bind the missing-`status` corner + the empty-slot
443    /// corner + the borrow-form `&str` lifetime + the
444    /// byte-identical parity with the pre-lift 3-line chain, so a
445    /// regression that drifted any surface at
446    /// `tests::observed_pid_*` rather than as silent operator-
447    /// facing skew between the ALLOCATE-PID gate and the SIGTERM
448    /// cascade on the SAME `Process`).
449    pub fn observed_pid(&self) -> Option<&str> {
450        self.status.as_ref().and_then(|s| s.pid.as_deref())
451    }
452
453    /// The borrow-form status-projection primitive on the
454    /// attestation-chain axis: returns the last
455    /// [`ProcessAttestation`] the reconciler persisted at
456    /// `status.attestation`, with the missing-`status` corner AND the
457    /// empty-slot corner BOTH collapsed to `None` — the ONE-liner
458    /// collapse of the paired `self.status.as_ref().and_then(|s|
459    /// s.attestation.as_ref())` incantation every consumer restated
460    /// by hand pre-lift.
461    ///
462    /// Pre-lift the 3-line `.status.as_ref().and_then(|s| s
463    /// .attestation.as_ref())` chain was hand-authored at TWO sites
464    /// past the ★★ PRIME-DIRECTIVE ≥ 2 duplication threshold in
465    /// `tatara-reconciler`:
466    /// * `phase_machine::advance_to_attested` — the ATTEST composer
467    ///   that chains `prior.next(pillars)` when a prior attestation
468    ///   is persisted and seeds with `ProcessAttestation::initial`
469    ///   otherwise.
470    /// * `render::render_export_jobs` — the ephemeral-export Job
471    ///   builder that pulls the prior `composed_root` off the last
472    ///   persisted attestation and threads it into every rendered
473    ///   Job's `previousRoot` env var, so the export receipt chains
474    ///   into the Process's BLAKE3 attestation tree at the correct
475    ///   generation boundary.
476    ///
477    /// Both sites walked the SAME 3-line chain — the borrow-form
478    /// `Option<&ProcessAttestation>` shape both consumers wanted
479    /// already — even though neither site ever mutated the
480    /// attestation nor kept it alive past the enclosing async fn.
481    /// Post-lift both callers borrow the attestation directly from
482    /// `self.status`; the pre-lift 3-line chain shrinks to a single
483    /// method call at both sites, and both consumers' subsequent
484    /// downstream calls (`ProcessAttestation::next` for the ATTEST
485    /// composer, `.composed_root.clone()` for the export Job builder)
486    /// do not touch the borrowed `p: &Process`, so the borrow
487    /// lifetime holds.
488    ///
489    /// Return-form axis: `Option<&ProcessAttestation>` mirrors the
490    /// existing borrow-first discipline every pre-lift consumer
491    /// already re-borrowed through `.as_ref()`, and the shape of the
492    /// peer [`Self::observed_pid`] projection extends mechanically
493    /// to the whole-attestation-record projection here. The missing-
494    /// `status` corner AND the populated-status-with-`attestation
495    /// =None` corner BOTH collapse to `None` so `.is_some()` / `if
496    /// let Some(_)` / `.map(...)` behave identically on a `Process`
497    /// whose status is `None` and on one whose status carries an
498    /// unpopulated `attestation` slot — matching what the pre-lift
499    /// `.and_then(...)` chain produced.
500    ///
501    /// A future normalization step (a per-slot canonicalization pass
502    /// that rejects a persisted attestation whose `composed_root`
503    /// fails `verify`, a generation-filter that returns `None` for
504    /// an attestation stamped with a stale `metadata.generation`, a
505    /// staleness gate that drops an attestation whose `attested_at`
506    /// predates a reconcile deadline) lands at ONE substrate method
507    /// here and both downstream consumers pick up the upgrade
508    /// mechanically — no per-callsite hand-edit at
509    /// `advance_to_attested` / `render_export_jobs`.
510    ///
511    /// Sibling to the peer [`Self::observed_pid`] +
512    /// [`Self::observed_flux_resources`] borrow-first primitives on
513    /// the PID + flux-resources axes; all three methods compose the
514    /// same missing-`status` fallback + borrow-form return-shape
515    /// skeleton on distinct `ProcessStatus` slots. Future status
516    /// projections (`observed_parent` on the parent-pointer axis,
517    /// `observed_message` on the human-readable-status axis) land
518    /// as peer methods on this same axis.
519    ///
520    /// Theory anchor: THEORY.md §VI.1 (generation over composition
521    /// — the 3-line status-projection chain recurred at two hand-
522    /// authored sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication
523    /// trigger, and is lifted to ONE owner here). THEORY.md §II.1
524    /// invariant 5 (composition preserves proofs — the pins bind
525    /// the missing-`status` corner + the empty-slot corner + the
526    /// borrow-form `&ProcessAttestation` lifetime + the byte-
527    /// identical parity with the pre-lift 3-line chain, so a
528    /// regression that drifted any surface at
529    /// `tests::observed_attestation_*` rather than as silent
530    /// operator-facing skew between the ATTEST composer and the
531    /// ephemeral-export receipt chain on the SAME `Process`).
532    pub fn observed_attestation(&self) -> Option<&ProcessAttestation> {
533        self.status.as_ref().and_then(|s| s.attestation.as_ref())
534    }
535
536    /// The copy-form status-projection primitive on the phase axis:
537    /// returns the [`ProcessPhase`] the reconciler currently persists
538    /// at `status.phase`, wrapped in an `Option` so the missing-
539    /// `status` corner collapses to `None` — the ONE-liner collapse
540    /// of the paired `self.status.as_ref().map(|s| s.phase)`
541    /// incantation every consumer restated by hand pre-lift.
542    ///
543    /// Peer to the borrow-form projections
544    /// [`Self::observed_pid`] (PID axis, `Option<&str>`),
545    /// [`Self::observed_flux_resources`] (flux-resources axis,
546    /// `&[FluxResourceRef]`), and [`Self::observed_attestation`]
547    /// (attestation-chain axis, `Option<&ProcessAttestation>`); this
548    /// method opens the copy-form peer for `ProcessPhase` — a
549    /// `Copy` scalar with a `Default` impl (`Pending`), so the
550    /// return is `Option<ProcessPhase>` rather than
551    /// `Option<&ProcessPhase>` (borrow would give the caller
552    /// nothing over the copy for a 1-byte enum) and neither the
553    /// missing-`status` corner nor a "empty slot" corner is
554    /// meaningful — the underlying slot is a bare `ProcessPhase`,
555    /// not `Option<ProcessPhase>`, so the primitive returns `None`
556    /// iff `status: None`.
557    ///
558    /// Pre-lift the 3-line `.status.as_ref().map(|s| s.phase)`
559    /// chain was hand-authored at FIVE sites past the ★★
560    /// PRIME-DIRECTIVE ≥ 2 duplication threshold in
561    /// `tatara-reconciler`:
562    /// * `controller::reconcile` — the top-level dispatcher's
563    ///   `current_phase` seed that feeds the deletion-preempt +
564    ///   signal-ingestion gates + the per-phase handler dispatch.
565    ///   Pre-lift `.unwrap_or(ProcessPhase::Pending)`.
566    /// * `boundary::evaluate_process_phase` — the boundary
567    ///   evaluator's `ProcessPhase` condition (a peer-Process
568    ///   `phase`-reached postcondition). Pre-lift
569    ///   `.unwrap_or(ProcessPhase::Pending)`.
570    /// * `boundary::check_depends_on` — the `depends_on`
571    ///   pre-condition audit that stashes the observed phase into
572    ///   the `UnmetDependency::actual: Option<ProcessPhase>` slot
573    ///   (keeps the `Option` form). Pre-lift the raw
574    ///   `.map(|s| s.phase)` shape.
575    /// * `phase_machine::p_current_phase_str` — the released-from
576    ///   annotation composer that emits `"Attested"` for every
577    ///   non-`Failed` phase (SIGSTOP/SIGCONT release gate).
578    ///   Pre-lift `.unwrap_or(ProcessPhase::Attested)` — the ONE
579    ///   site whose default is not `Pending`; the primitive
580    ///   returns the raw `Option` so the caller's `.unwrap_or`
581    ///   default choice stays local rather than baked in.
582    /// * `table_controller::stable_name_group_key` — the routing-
583    ///   groupby seed that pairs the phase with the PID + creation
584    ///   timestamp when partitioning Processes claiming the same
585    ///   stable name. Pre-lift `.unwrap_or(ProcessPhase::Pending)`.
586    ///
587    /// All FIVE sites walked the SAME 3-line `.status.as_ref()
588    /// .map(|s| s.phase)` chain — three closed with `unwrap_or
589    /// (ProcessPhase::Pending)` (the `Default`), one closed with
590    /// `unwrap_or(ProcessPhase::Attested)`, one kept the raw
591    /// `Option<ProcessPhase>` — so the ONE substrate accessor
592    /// returns the raw `Option<ProcessPhase>` and each consumer
593    /// keeps its `.unwrap_or(...)` default choice at its own site.
594    ///
595    /// A future normalization step (a generation-filter that
596    /// returns `None` for a phase stamped with a stale
597    /// `metadata.generation`, a staleness gate that drops a phase
598    /// whose observing `phase_since` predates a reconcile
599    /// deadline, a canonicalization pass that maps a phase that
600    /// no longer belongs to the CRD's closed set to `None`) lands
601    /// at ONE substrate method here and all five consumers pick
602    /// up the upgrade mechanically — no per-callsite hand-edit at
603    /// `reconcile` / `evaluate_process_phase` / `check_depends_on`
604    /// / `p_current_phase_str` / `stable_name_group_key`.
605    ///
606    /// Future status projections (`observed_parent` on the
607    /// parent-pointer axis, `observed_message` on the human-
608    /// readable-status axis, `observed_children` on the child
609    /// fan-out axis, `observed_exit_code` on the terminal-exit
610    /// axis) land as peer methods on this same axis.
611    ///
612    /// Theory anchor: THEORY.md §VI.1 (generation over
613    /// composition — the 3-line status-projection chain recurred
614    /// at FIVE hand-authored sites past the ★★ PRIME-DIRECTIVE
615    /// ≥ 2 duplication trigger, and is lifted to ONE owner here).
616    /// THEORY.md §II.1 invariant 5 (composition preserves proofs
617    /// — the pins bind the missing-`status` corner + the
618    /// per-variant enum round-trip + the byte-identical parity
619    /// with the pre-lift 3-line chain, so a regression that
620    /// drifted any surface at `tests::observed_phase_*` rather
621    /// than as silent operator-facing skew between the
622    /// controller's dispatch seed and the boundary evaluator's
623    /// depends-on audit on the SAME `Process` within one
624    /// reconcile pass).
625    pub fn observed_phase(&self) -> Option<ProcessPhase> {
626        self.status.as_ref().map(|s| s.phase)
627    }
628}
629
630/// Process status — every field optional until the reconciler writes it.
631#[derive(Clone, Debug, Default, Deserialize, Serialize, JsonSchema)]
632#[serde(rename_all = "camelCase")]
633pub struct ProcessStatus {
634    /// Hierarchical PID path — e.g., `"seph.1.7"`.
635    #[serde(default, skip_serializing_if = "Option::is_none")]
636    pub pid: Option<String>,
637
638    /// Parent PID path (mirror of `spec.identity.parent`, resolved at fork).
639    #[serde(default, skip_serializing_if = "Option::is_none")]
640    pub parent: Option<String>,
641
642    /// Direct children's PID paths.
643    #[serde(default)]
644    pub children: Vec<String>,
645
646    /// Resolved identity (name + content hash).
647    #[serde(default, skip_serializing_if = "Option::is_none")]
648    pub identity: Option<Identity>,
649
650    /// Current phase.
651    #[serde(default)]
652    pub phase: ProcessPhase,
653
654    /// When the process entered the current phase.
655    #[serde(default, skip_serializing_if = "Option::is_none")]
656    pub phase_since: Option<DateTime<Utc>>,
657
658    /// Three-pillar attestation (written at end of every successful cycle).
659    #[serde(default, skip_serializing_if = "Option::is_none")]
660    pub attestation: Option<ProcessAttestation>,
661
662    /// FluxCD resources currently owned by this Process.
663    #[serde(default)]
664    pub flux_resources: Vec<FluxResourceRef>,
665
666    /// Boundary verification state.
667    #[serde(default)]
668    pub boundary: BoundaryStatus,
669
670    /// Compliance summary at the latest attestation.
671    #[serde(default)]
672    pub compliance: ComplianceStatus,
673
674    /// Pending signals (delivered, not yet handled).
675    #[serde(default)]
676    pub signal_queue: Vec<ProcessSignal>,
677
678    /// Standard K8s Conditions.
679    #[serde(default)]
680    pub conditions: Vec<ProcessCondition>,
681
682    /// Human-readable last status message.
683    #[serde(default, skip_serializing_if = "Option::is_none")]
684    pub message: Option<String>,
685
686    /// Exit code (only set on Failed / Reaped).
687    #[serde(default, skip_serializing_if = "Option::is_none")]
688    pub exit_code: Option<i32>,
689}
690
691#[cfg(test)]
692mod tests {
693    use super::*;
694    use crate::classification::{ConvergencePointType, SubstrateType};
695    use crate::intent::NixIntent;
696
697    #[test]
698    fn minimal_spec_serializes() {
699        let spec = ProcessSpec {
700            identity: IdentitySpec::default(),
701            classification: Classification {
702                point_type: ConvergencePointType::Gate,
703                substrate: SubstrateType::Observability,
704                horizon: Default::default(),
705                calm: Default::default(),
706                data_classification: Default::default(),
707            },
708            intent: Intent {
709                nix: Some(NixIntent {
710                    flake_ref: "github:pleme-io/k8s".into(),
711                    attribute: "obs".into(),
712                    system: None,
713                    attic_cache: None,
714                    extra_args: vec![],
715                    delegate_to_nix_build: false,
716                }),
717                ..Intent::default()
718            },
719            boundary: Default::default(),
720            compliance: Default::default(),
721            depends_on: vec![],
722            signals: Default::default(),
723            lifetime: Default::default(),
724            routing: None,
725            encapsulates: None,
726            suspended: false,
727        };
728        let yaml = serde_yaml::to_string(&spec).unwrap();
729        assert!(yaml.contains("pointType: Gate"));
730        assert!(yaml.contains("substrate: Observability"));
731        assert!(yaml.contains("flakeRef: github:pleme-io/k8s"));
732    }
733
734    // ─── Process::coordinates_or_defaults substrate pins ────────────────
735    //
736    // Pins the (namespace, name) coordinate-primitive family on the
737    // (metadata slot × fallback shape) axis. Fail-before-pass-after
738    // granularity: a regression that flipped either fallback string,
739    // swapped the return-tuple axis order, or dropped the
740    // `Option::as_deref` unwrap surfaces here rather than as silent
741    // drift at every downstream annotation writer / claim-arbiter row
742    // builder / render owner-metadata seed.
743
744    fn empty_spec() -> ProcessSpec {
745        ProcessSpec {
746            identity: IdentitySpec::default(),
747            classification: Classification {
748                point_type: ConvergencePointType::Gate,
749                substrate: SubstrateType::Compute,
750                horizon: Default::default(),
751                calm: Default::default(),
752                data_classification: Default::default(),
753            },
754            intent: Intent::default(),
755            boundary: Default::default(),
756            compliance: Default::default(),
757            depends_on: vec![],
758            signals: Default::default(),
759            lifetime: Default::default(),
760            routing: None,
761            encapsulates: None,
762            suspended: false,
763        }
764    }
765
766    #[test]
767    fn default_namespace_constant_is_k8s_canonical_default() {
768        // Pins the load-bearing convention that this primitive's
769        // namespace fallback matches K8s's own implicit-namespace
770        // spelling. A regression that renamed this to "kube-system"
771        // or any other K8s-reserved name would silently misroute
772        // every downstream namespaced-Api call on a Process without
773        // a metadata.namespace.
774        assert_eq!(Process::DEFAULT_NAMESPACE, "default");
775    }
776
777    #[test]
778    fn unnamed_placeholder_constant_matches_prior_annotation_writer_fallback() {
779        // Pins the load-bearing convention that this primitive's name
780        // fallback matches the exact spelling every annotation writer
781        // (tatara-reconciler::ssapply::inject_annotations,
782        // tatara-reconciler::render::render, and
783        // tatara-reconciler::table_controller's claim-row builder)
784        // was hand-authoring pre-lift ("unnamed", NOT "<unnamed>" or
785        // ""). A regression that renamed this would break the
786        // annotation-writer / claim-arbiter grep contract silently.
787        assert_eq!(Process::UNNAMED_PLACEHOLDER, "unnamed");
788    }
789
790    #[test]
791    fn namespace_or_default_falls_back_when_metadata_namespace_is_none() {
792        let mut p = Process::new("some-proc", empty_spec());
793        p.metadata.namespace = None;
794        assert_eq!(p.namespace_or_default(), Process::DEFAULT_NAMESPACE);
795    }
796
797    #[test]
798    fn namespace_or_default_returns_metadata_slice_when_some() {
799        let mut p = Process::new("some-proc", empty_spec());
800        p.metadata.namespace = Some("prod-app".into());
801        assert_eq!(p.namespace_or_default(), "prod-app");
802    }
803
804    #[test]
805    fn name_or_placeholder_falls_back_when_metadata_name_is_none() {
806        let mut p = Process::new("real-name", empty_spec());
807        p.metadata.name = None;
808        assert_eq!(p.name_or_placeholder(), Process::UNNAMED_PLACEHOLDER);
809    }
810
811    #[test]
812    fn name_or_placeholder_returns_metadata_slice_when_some() {
813        let p = Process::new("api-gateway", empty_spec());
814        assert_eq!(p.name_or_placeholder(), "api-gateway");
815    }
816
817    #[test]
818    fn coordinates_or_defaults_composes_both_halves() {
819        // Both slots present — returns metadata slices in
820        // (namespace, name) axis order.
821        let mut p = Process::new("api", empty_spec());
822        p.metadata.namespace = Some("staging".into());
823        assert_eq!(p.coordinates_or_defaults(), ("staging", "api"));
824    }
825
826    #[test]
827    fn coordinates_or_defaults_falls_back_on_both_slots() {
828        // Both slots None — returns (DEFAULT_NAMESPACE,
829        // UNNAMED_PLACEHOLDER) in axis order.
830        let mut p = Process::new("scratch", empty_spec());
831        p.metadata.name = None;
832        p.metadata.namespace = None;
833        assert_eq!(
834            p.coordinates_or_defaults(),
835            (Process::DEFAULT_NAMESPACE, Process::UNNAMED_PLACEHOLDER)
836        );
837    }
838
839    #[test]
840    fn coordinates_or_defaults_mixes_slotted_and_fallback_halves() {
841        // Namespace set, name missing — the (namespace, name) tuple
842        // pins each half independently. A regression that returned
843        // BOTH fallbacks when EITHER metadata slot was None would
844        // surface here rather than at every downstream reader.
845        let mut p = Process::new("kept-name", empty_spec());
846        p.metadata.namespace = Some("prod".into());
847        assert_eq!(p.coordinates_or_defaults(), ("prod", "kept-name"));
848
849        // Name set, namespace missing — the peer corner.
850        let mut q = Process::new("api", empty_spec());
851        q.metadata.namespace = None;
852        assert_eq!(
853            q.coordinates_or_defaults(),
854            (Process::DEFAULT_NAMESPACE, "api")
855        );
856    }
857
858    // ─── Process::owned_coordinates_or_err substrate pins ──────────────
859    //
860    // Pins the owned + name-required peer of the coordinate-primitive
861    // family on the (return-form × name gate) axis pair. Fail-before-
862    // pass-after granularity: a regression that flipped the namespace
863    // fallback string, dropped the `Option::clone` unwrap, changed the
864    // return-tuple axis order, or altered the "Process has no
865    // metadata.name" error wording surfaces here rather than as silent
866    // drift at every pre-lift caller (10 sites in
867    // `tatara-reconciler::phase_machine` + 2 sites in
868    // `tatara-reconciler::signals` pre-lift).
869
870    #[test]
871    fn owned_coordinates_or_err_returns_owned_strings_when_both_slots_present() {
872        // Happy path — both slots populated, method returns owned
873        // Strings in (namespace, name) axis order.
874        let mut p = Process::new("api-gateway", empty_spec());
875        p.metadata.namespace = Some("prod-app".into());
876        let (ns, name) = p.owned_coordinates_or_err().unwrap();
877        assert_eq!(ns, "prod-app");
878        assert_eq!(name, "api-gateway");
879        // Ownership pin: type inference above binds ns/name as
880        // owned Strings — a regression that returned &str would
881        // fail to compile at the following .push() call. This
882        // holds the "owned" half of the primitive's contract.
883        let mut owned_ns = ns;
884        owned_ns.push_str("-mutated");
885        assert_eq!(owned_ns, "prod-app-mutated");
886    }
887
888    #[test]
889    fn owned_coordinates_or_err_falls_back_on_namespace_but_returns_owned_name() {
890        // Namespace absent → DEFAULT_NAMESPACE. Name present → owned.
891        let p = Process::new("api", empty_spec());
892        // Process::new leaves metadata.namespace = None by default.
893        let (ns, name) = p.owned_coordinates_or_err().unwrap();
894        assert_eq!(ns, Process::DEFAULT_NAMESPACE);
895        assert_eq!(name, "api");
896    }
897
898    #[test]
899    fn owned_coordinates_or_err_errors_when_metadata_name_absent_regardless_of_namespace() {
900        // Name absent → Err, REGARDLESS of whether the namespace is
901        // populated. The name gate is strictly on `metadata.name` and
902        // does NOT fall back to `Self::UNNAMED_PLACEHOLDER` (that
903        // fallback is on the peer `coordinates_or_defaults`, which
904        // exists precisely for consumers that can tolerate a
905        // display placeholder).
906        for ns_slot in [None, Some("prod".to_string())] {
907            let mut p = Process::new("scratch", empty_spec());
908            p.metadata.name = None;
909            p.metadata.namespace = ns_slot.clone();
910            let err = p.owned_coordinates_or_err().unwrap_err();
911            assert!(
912                err.to_string().contains("metadata.name"),
913                "err on missing name (ns={ns_slot:?}) should mention metadata.name; got {err}"
914            );
915        }
916    }
917
918    #[test]
919    fn owned_coordinates_or_err_error_message_matches_pre_lift_reconciler_wording() {
920        // Load-bearing wording pin — every pre-lift `tatara-reconciler`
921        // helper (`phase_machine::namespace_and_name`,
922        // `signals::ingest`, `signals::consume_effect`) errored with
923        // EXACTLY this wording. Post-lift the substrate owner produces
924        // the same wording so log-line / test greps that anchored on
925        // it keep matching, and no operator-visible message drift
926        // lands as a side effect of the substrate move.
927        let mut p = Process::new("scratch", empty_spec());
928        p.metadata.name = None;
929        let err = p.owned_coordinates_or_err().unwrap_err();
930        assert_eq!(err.to_string(), "Process has no metadata.name");
931    }
932
933    #[test]
934    fn owned_coordinates_or_err_namespace_fallback_matches_default_namespace_const() {
935        // Byte-identity pin between the owned form's namespace
936        // fallback and the workspace-wide `DEFAULT_NAMESPACE` const.
937        // A regression that spelled this fallback as any other
938        // string ("kube-system", "", "default-ns") would silently
939        // misroute every downstream namespaced-Api call on a
940        // Process without a metadata.namespace — surfaces here
941        // rather than at every kube-rs API caller.
942        let mut p = Process::new("api", empty_spec());
943        p.metadata.namespace = None;
944        let (ns, _) = p.owned_coordinates_or_err().unwrap();
945        assert_eq!(ns, Process::DEFAULT_NAMESPACE);
946    }
947
948    #[test]
949    fn owned_coordinates_or_err_matches_pre_lift_reconciler_helper_shape() {
950        // Byte-identical parity pin between the owned + name-required
951        // primitive here and the pre-lift `tatara-reconciler` helper
952        // shape — the exact 2-slot unwrap chain each pre-lift caller
953        // spelled by hand:
954        //
955        //   let ns = p.metadata.namespace.clone().unwrap_or_else(|| "default".into());
956        //   let name = p.metadata.name.clone().ok_or_else(|| anyhow!(...))?;
957        //   Ok((ns, name))
958        //
959        // Sweeps every corner every callsite plausibly encounters
960        // (both slots present, namespace absent, name absent, both
961        // absent). A regression that inserted a normalization step
962        // at the primitive that the pre-lift chain does NOT apply —
963        // or vice versa — surfaces here rather than as silent drift
964        // between the 12 pre-lift consumer callsites and the ONE
965        // substrate owner they now route through.
966        fn pre_lift(p: &Process) -> anyhow::Result<(String, String)> {
967            let ns = p
968                .metadata
969                .namespace
970                .clone()
971                .unwrap_or_else(|| "default".into());
972            let name = p
973                .metadata
974                .name
975                .clone()
976                .ok_or_else(|| anyhow::anyhow!("Process has no metadata.name"))?;
977            Ok((ns, name))
978        }
979        // Both present.
980        let mut p = Process::new("api", empty_spec());
981        p.metadata.namespace = Some("prod".into());
982        assert_eq!(p.owned_coordinates_or_err().unwrap(), pre_lift(&p).unwrap());
983        // Namespace absent.
984        let p = Process::new("api", empty_spec());
985        assert_eq!(p.owned_coordinates_or_err().unwrap(), pre_lift(&p).unwrap());
986        // Name absent → both variants error with the same wording.
987        let mut p = Process::new("api", empty_spec());
988        p.metadata.name = None;
989        p.metadata.namespace = Some("prod".into());
990        assert_eq!(
991            p.owned_coordinates_or_err().unwrap_err().to_string(),
992            pre_lift(&p).unwrap_err().to_string(),
993        );
994        // Both absent → still errors on the name gate.
995        let mut p = Process::new("api", empty_spec());
996        p.metadata.name = None;
997        p.metadata.namespace = None;
998        assert_eq!(
999            p.owned_coordinates_or_err().unwrap_err().to_string(),
1000            pre_lift(&p).unwrap_err().to_string(),
1001        );
1002    }
1003
1004    #[test]
1005    fn owned_coordinates_or_err_axis_order_matches_coordinates_or_defaults() {
1006        // Cross-primitive coherence pin between the owned + name-
1007        // required form and the borrow + name-defaulted peer:
1008        // (namespace, name) axis order is IDENTICAL across both
1009        // return-forms. A regression that swapped the tuple slots on
1010        // only ONE of the two primitives would silently misroute
1011        // every consumer that picked between the two forms based on
1012        // its callsite's ownership needs. The pin re-reads both
1013        // primitives at test time so the equality holds iff both
1014        // live paths are the current implementation.
1015        let mut p = Process::new("app", empty_spec());
1016        p.metadata.namespace = Some("infra".into());
1017        let (borrow_ns, borrow_name) = p.coordinates_or_defaults();
1018        let (owned_ns, owned_name) = p.owned_coordinates_or_err().unwrap();
1019        assert_eq!(owned_ns, borrow_ns);
1020        assert_eq!(owned_name, borrow_name);
1021        // Explicit slot labels — pins the (namespace, name) axis
1022        // order as opposed to (name, namespace).
1023        assert_eq!(owned_ns, "infra"); // NOT "app"
1024        assert_eq!(owned_name, "app"); // NOT "infra"
1025    }
1026
1027    // ─── Process::coordinates_or_none substrate pins ──────────────────
1028    //
1029    // Pins the borrow + name-required peer of the coordinate-primitive
1030    // family on the (return-form × name-gate) axis pair. Closes the
1031    // corner previously left open (borrow + name-required) so the
1032    // three consumer shapes (child-Process delete-fan-out at
1033    // `phase_machine::handle_exiting`, claim-arbiter probe at
1034    // `phase_machine::process_holds_any_claim`, any future non-fatal
1035    // skip site) route through ONE primitive rather than three hand-
1036    // authored empty-string / `unwrap_or_default()` sentinel chains.
1037    // Fail-before-pass-after granularity: a regression that flipped
1038    // the namespace fallback, swapped the return-tuple axis order,
1039    // returned an owned form, or promoted a missing name to an error
1040    // rather than `None` surfaces here rather than as silent drift at
1041    // every borrow + name-required consumer.
1042
1043    #[test]
1044    fn coordinates_or_none_returns_slices_when_both_slots_present() {
1045        // Happy path — both slots populated, method returns borrowed
1046        // (&str, &str) in (namespace, name) axis order wrapped in
1047        // `Some`.
1048        let mut p = Process::new("api-gateway", empty_spec());
1049        p.metadata.namespace = Some("prod-app".into());
1050        let (ns, name) = p.coordinates_or_none().expect("Some when name set");
1051        assert_eq!(ns, "prod-app");
1052        assert_eq!(name, "api-gateway");
1053    }
1054
1055    #[test]
1056    fn coordinates_or_none_falls_back_on_namespace_but_returns_name_slice() {
1057        // Namespace absent → DEFAULT_NAMESPACE (shared with the peer
1058        // `coordinates_or_defaults` + `namespace_or_default`). Name
1059        // present → the metadata slice, wrapped in `Some`.
1060        let mut p = Process::new("api", empty_spec());
1061        p.metadata.namespace = None;
1062        let (ns, name) = p.coordinates_or_none().expect("Some when name set");
1063        assert_eq!(ns, Process::DEFAULT_NAMESPACE);
1064        assert_eq!(name, "api");
1065    }
1066
1067    #[test]
1068    fn coordinates_or_none_returns_none_when_metadata_name_absent_regardless_of_namespace() {
1069        // Name absent → `None`, REGARDLESS of whether the namespace
1070        // slot is populated. The name gate is strictly on
1071        // `metadata.name` and does NOT fall back to
1072        // `Self::UNNAMED_PLACEHOLDER` (that fallback is on the peer
1073        // `coordinates_or_defaults`, which exists precisely for
1074        // consumers that tolerate a display placeholder). Peer to
1075        // `owned_coordinates_or_err_errors_when_metadata_name_absent_regardless_of_namespace`
1076        // on the sibling primitive; a regression that widened THIS
1077        // form to substitute the placeholder while leaving the owned
1078        // form strict would silently drift the two borrow-form
1079        // primitives out of the coherence the family carries.
1080        for ns_slot in [None, Some("prod".to_string())] {
1081            let mut p = Process::new("scratch", empty_spec());
1082            p.metadata.name = None;
1083            p.metadata.namespace = ns_slot.clone();
1084            assert!(
1085                p.coordinates_or_none().is_none(),
1086                "coordinates_or_none must be None on missing name (ns={ns_slot:?})",
1087            );
1088        }
1089    }
1090
1091    #[test]
1092    fn coordinates_or_none_namespace_fallback_matches_default_namespace_const() {
1093        // Byte-identity pin between the borrow + name-required form's
1094        // namespace fallback and the workspace-wide `DEFAULT_NAMESPACE`
1095        // const. Sibling to
1096        // `owned_coordinates_or_err_namespace_fallback_matches_default_namespace_const`
1097        // on the peer primitive — the two forms MUST substitute the
1098        // same fallback string, else a consumer that switches between
1099        // them based on its ownership need silently observes a
1100        // different namespace-fallback shape as a side effect.
1101        let mut p = Process::new("api", empty_spec());
1102        p.metadata.namespace = None;
1103        let (ns, _) = p.coordinates_or_none().unwrap();
1104        assert_eq!(ns, Process::DEFAULT_NAMESPACE);
1105    }
1106
1107    #[test]
1108    fn coordinates_or_none_axis_order_matches_coordinates_or_defaults_when_name_present() {
1109        // Cross-primitive coherence pin between the two borrow-form
1110        // primitives: when the name is present, the (namespace, name)
1111        // return-tuple axis order is IDENTICAL across the two forms,
1112        // and the returned slices are the SAME `&str` view onto the
1113        // same metadata slots. A regression that swapped the tuple
1114        // slots on ONE form would silently misroute every consumer
1115        // that picked between the two forms based on its name-gate
1116        // need. The pin re-reads both primitives at test time so the
1117        // equality holds iff both live paths are the current
1118        // implementation.
1119        let mut p = Process::new("app", empty_spec());
1120        p.metadata.namespace = Some("infra".into());
1121        let (defaulted_ns, defaulted_name) = p.coordinates_or_defaults();
1122        let (required_ns, required_name) = p.coordinates_or_none().unwrap();
1123        assert_eq!(defaulted_ns, required_ns);
1124        assert_eq!(defaulted_name, required_name);
1125        // Explicit slot labels — pins the (namespace, name) axis order
1126        // as opposed to (name, namespace).
1127        assert_eq!(required_ns, "infra"); // NOT "app"
1128        assert_eq!(required_name, "app"); // NOT "infra"
1129    }
1130
1131    #[test]
1132    fn coordinates_or_none_axis_pair_diverges_from_coordinates_or_defaults_on_missing_name() {
1133        // Divergence pin between the two borrow-form primitives when
1134        // the name gate fires: `coordinates_or_defaults` substitutes
1135        // the display placeholder AND still returns a tuple;
1136        // `coordinates_or_none` returns `None`. A regression that
1137        // collapsed the two behaviors (either by dropping the gate
1138        // from the required form or by adding a `None` corner to the
1139        // defaulted form) would blur the axis pair's whole reason to
1140        // exist as two peer primitives.
1141        let mut p = Process::new("scratch", empty_spec());
1142        p.metadata.name = None;
1143        p.metadata.namespace = Some("prod".into());
1144        // Defaulted form: substitutes placeholder, no gate.
1145        assert_eq!(
1146            p.coordinates_or_defaults(),
1147            ("prod", Process::UNNAMED_PLACEHOLDER)
1148        );
1149        // Required form: gate fires, `None`.
1150        assert!(p.coordinates_or_none().is_none());
1151    }
1152
1153    #[test]
1154    fn coordinates_or_none_matches_pre_lift_reconciler_helper_shape() {
1155        // Byte-identical parity pin between the borrow + name-required
1156        // primitive here and the pre-lift `tatara-reconciler` helper
1157        // shapes — the exact 2-slot unwrap + gate chains each pre-lift
1158        // caller spelled by hand (`phase_machine::process_holds_any_claim`
1159        // spelled it as `unwrap_or("")` + `is_empty` early-return;
1160        // `phase_machine::handle_exiting`'s child-fan-out spelled it
1161        // as `unwrap_or_default()` + implicit no-op delete on the
1162        // empty API-path). Sweeps every corner every callsite plausibly
1163        // encounters (both slots present, namespace absent, name
1164        // absent + ns present, both absent). A regression that
1165        // inserted a normalization step at the primitive the pre-lift
1166        // chain does NOT apply — or vice versa — surfaces here rather
1167        // than as silent drift between the pre-lift consumer sites
1168        // and the ONE substrate owner they now route through.
1169        fn pre_lift_holds_any_claim(p: &Process) -> Option<(&str, &str)> {
1170            let ns = p.metadata.namespace.as_deref().unwrap_or("default");
1171            let name = p.metadata.name.as_deref().unwrap_or("");
1172            if name.is_empty() {
1173                return None;
1174            }
1175            Some((ns, name))
1176        }
1177        // Both present.
1178        let mut p = Process::new("api", empty_spec());
1179        p.metadata.namespace = Some("prod".into());
1180        assert_eq!(p.coordinates_or_none(), pre_lift_holds_any_claim(&p));
1181        // Namespace absent.
1182        let p = Process::new("api", empty_spec());
1183        assert_eq!(p.coordinates_or_none(), pre_lift_holds_any_claim(&p));
1184        // Name absent → both variants return `None` regardless of ns.
1185        let mut p = Process::new("api", empty_spec());
1186        p.metadata.name = None;
1187        p.metadata.namespace = Some("prod".into());
1188        assert_eq!(p.coordinates_or_none(), pre_lift_holds_any_claim(&p));
1189        // Both absent → still `None` on the name gate.
1190        let mut p = Process::new("api", empty_spec());
1191        p.metadata.name = None;
1192        p.metadata.namespace = None;
1193        assert_eq!(p.coordinates_or_none(), pre_lift_holds_any_claim(&p));
1194    }
1195
1196    #[test]
1197    fn coordinates_or_none_axis_order_matches_owned_coordinates_or_err_on_happy_path() {
1198        // Cross-primitive coherence pin at the sibling corner: when
1199        // BOTH slots are present, the borrow + name-required form
1200        // (this method) and the owned + name-required peer
1201        // (`owned_coordinates_or_err`) return the SAME `(ns, name)`
1202        // pair — the axis order is IDENTICAL and neither primitive
1203        // silently applies a normalization the other omits. A
1204        // regression that skewed one form's normalization would
1205        // surface here rather than as silent drift between the two
1206        // name-required corners of the primitive family.
1207        let mut p = Process::new("app", empty_spec());
1208        p.metadata.namespace = Some("infra".into());
1209        let (borrow_ns, borrow_name) = p.coordinates_or_none().unwrap();
1210        let (owned_ns, owned_name) = p.owned_coordinates_or_err().unwrap();
1211        assert_eq!(borrow_ns, owned_ns.as_str());
1212        assert_eq!(borrow_name, owned_name.as_str());
1213    }
1214
1215    #[test]
1216    fn coordinates_or_defaults_axis_order_matches_qualified_process_ref() {
1217        // Pins the load-bearing convention that the return-tuple
1218        // axis order is (namespace, name) — the exact positional
1219        // argument order the substrate's paired-composer primitive
1220        // `tatara_reconciler::ssapply::qualified_process_ref(ns,
1221        // name)` consumes. A regression that swapped the tuple
1222        // slots would silently misroute every annotation writer /
1223        // claim-arbiter row / owner-metadata seed built by feeding
1224        // this pair into the composer — every downstream `<ns>/
1225        // <name>` grep would suddenly see `<name>/<ns>`. The test
1226        // verifies the tuple's first slot is what a hand-authored
1227        // `.metadata.namespace.as_deref()...` produced pre-lift, and
1228        // the second slot is what `.metadata.name.as_deref()...`
1229        // produced.
1230        let mut p = Process::new("app", empty_spec());
1231        p.metadata.namespace = Some("infra".into());
1232        let (ns, name) = p.coordinates_or_defaults();
1233        assert_eq!(ns, "infra"); // NOT "app"
1234        assert_eq!(name, "app"); // NOT "infra"
1235    }
1236
1237    // ─── Process::observed_flux_resources substrate pins ───────────────
1238    //
1239    // Pins the borrow-form status-projection primitive that owns the
1240    // 5-line `.status.as_ref().map(|s| s.flux_resources.clone())
1241    // .unwrap_or_default()` chain the two hand-authored
1242    // `tatara-reconciler::phase_machine` sites (`handle_running` +
1243    // `handle_attested`) restated by hand pre-lift. Fail-before-pass-
1244    // after granularity: a regression that widened the missing-`status`
1245    // corner, dropped the slot, or drifted the borrow discipline
1246    // surfaces here rather than as silent operator-facing skew between
1247    // the VERIFY-phase readiness probe and the ATTEST-heartbeat drift
1248    // detector.
1249
1250    fn sample_flux_ref(name: &str) -> FluxResourceRef {
1251        // Distinct slot values so a swap between adjacent tuple
1252        // positions surfaces as an equality failure at the assertion
1253        // site — a slot-inversion regression cannot masquerade as
1254        // identity by accident. Peer to the sibling
1255        // `tatara_process::status::tests::sample_flux_ref` discipline
1256        // on the fetch-coords axis.
1257        FluxResourceRef {
1258            api_version: "kustomize.toolkit.fluxcd.io/v1".to_string(),
1259            kind: "Kustomization".to_string(),
1260            name: name.to_string(),
1261            namespace: "flux-system".to_string(),
1262            ready: false,
1263            message: None,
1264            last_check: None,
1265        }
1266    }
1267
1268    fn process_with_flux_resources(refs: Vec<FluxResourceRef>) -> Process {
1269        let mut p = Process::new("api-gateway", empty_spec());
1270        p.metadata.namespace = Some("prod".into());
1271        let mut status = ProcessStatus::default();
1272        status.flux_resources = refs;
1273        p.status = Some(status);
1274        p
1275    }
1276
1277    #[test]
1278    fn observed_flux_resources_returns_empty_slice_when_status_is_none() {
1279        // Missing-`status` corner pin: the primitive collapses the
1280        // no-status case to `&[]` so downstream `.is_empty()` /
1281        // `.len()` / iteration behave identically on a `Process`
1282        // whose status field is `None` and on one whose status
1283        // carries an empty `flux_resources` slot. Matches the
1284        // pre-lift `.unwrap_or_default()`'s empty-`Vec` corner
1285        // byte-identically at every reconciler consumer's downstream
1286        // shape.
1287        let mut p = Process::new("api", empty_spec());
1288        p.status = None;
1289        assert!(p.observed_flux_resources().is_empty());
1290        assert_eq!(p.observed_flux_resources().len(), 0);
1291    }
1292
1293    #[test]
1294    fn observed_flux_resources_returns_empty_slice_when_flux_resources_is_empty() {
1295        // Zero-refs-under-populated-status corner pin: the primitive
1296        // returns an empty slice, matching the missing-`status`
1297        // corner byte-identically. A regression that treated the two
1298        // corners differently (a `None`-vs-empty signal that
1299        // downstream consumers could grep on) would silently promote
1300        // an internal representation detail (whether the reconciler
1301        // has ever written a status subresource) into observable
1302        // behavior.
1303        let p = process_with_flux_resources(vec![]);
1304        assert!(p.observed_flux_resources().is_empty());
1305        assert_eq!(p.observed_flux_resources().len(), 0);
1306    }
1307
1308    #[test]
1309    fn observed_flux_resources_returns_slice_of_persisted_vec() {
1310        // Happy-path pin: with a populated `status.flux_resources`
1311        // slot, the primitive returns a borrowed slice whose length
1312        // and per-element identity match the persisted vector. A
1313        // regression that filtered / reshaped / deduplicated the
1314        // slice would surface here rather than as silent skew at the
1315        // downstream fetch consumers.
1316        let refs = vec![
1317            sample_flux_ref("observability-stack"),
1318            sample_flux_ref("gateway"),
1319        ];
1320        let p = process_with_flux_resources(refs.clone());
1321        let observed = p.observed_flux_resources();
1322        assert_eq!(observed.len(), 2);
1323        assert_eq!(observed[0].name, "observability-stack");
1324        assert_eq!(observed[1].name, "gateway");
1325    }
1326
1327    #[test]
1328    fn observed_flux_resources_is_a_zero_copy_borrow_projection() {
1329        // Borrow-discipline pin: the returned slice borrows the
1330        // persisted `Vec<FluxResourceRef>` in place — NOT a fresh
1331        // allocation or a clone. A regression that switched the
1332        // projection to owned refs (via `.clone()` or `.to_vec()`)
1333        // would defeat the zero-copy contract the lift's primary
1334        // strict-widening delivers (the pre-lift 5-line chain
1335        // eagerly cloned the whole vector per reconcile pass; the
1336        // post-lift primitive borrows). Peer to the sibling
1337        // `flux_resource_ref_fetch_coords_returns_borrows_of_owned_slots`
1338        // pin on the per-ref borrow-projection axis.
1339        let refs = vec![sample_flux_ref("observability-stack")];
1340        let p = process_with_flux_resources(refs);
1341        let observed = p.observed_flux_resources();
1342        let persisted = &p.status.as_ref().unwrap().flux_resources;
1343        assert!(std::ptr::eq(observed.as_ptr(), persisted.as_ptr()));
1344    }
1345
1346    #[test]
1347    fn observed_flux_resources_is_a_pure_projection() {
1348        // Purity pin: calling the projection twice on the same
1349        // `Process` returns byte-identical slices (same pointer,
1350        // same length). A regression that introduced state — a
1351        // lazy-cached slice materialized on first call, a
1352        // normalization step that ran once and cached — would
1353        // surface here rather than as silent drift between the
1354        // VERIFY-phase and ATTEST-heartbeat consumers on the SAME
1355        // `Process` within one reconcile pass.
1356        let refs = vec![sample_flux_ref("observability-stack")];
1357        let p = process_with_flux_resources(refs);
1358        let a = p.observed_flux_resources();
1359        let b = p.observed_flux_resources();
1360        assert!(std::ptr::eq(a.as_ptr(), b.as_ptr()));
1361        assert_eq!(a.len(), b.len());
1362    }
1363
1364    #[test]
1365    fn observed_flux_resources_matches_pre_lift_reconciler_chain_shape() {
1366        // Byte-identical parity pin between the borrow-form primitive
1367        // here and the pre-lift `tatara-reconciler::phase_machine`
1368        // 5-line chain shape. Sweeps every corner every callsite
1369        // plausibly encounters (missing status, empty flux_resources,
1370        // populated flux_resources with one ref, populated with
1371        // multiple refs). A regression that inserted a normalization
1372        // step at the primitive the pre-lift chain does NOT apply —
1373        // or vice versa — surfaces here rather than as silent drift
1374        // between the pre-lift consumer sites and the ONE substrate
1375        // owner they now route through. Peer to
1376        // `coordinates_or_none_matches_pre_lift_reconciler_helper_shape`
1377        // on the metadata axis's borrow-form primitive.
1378        // `FluxResourceRef` does not derive `PartialEq` — the parity
1379        // check walks the per-ref fetch-coords tuple (the same 4-slot
1380        // borrow projection every downstream fetch consumer routes
1381        // through) so a regression that reshaped ANY slot at ANY
1382        // index surfaces here through the sibling
1383        // `FluxResourceRef::fetch_coords` typed projection.
1384        fn pre_lift(p: &Process) -> Vec<FluxResourceRef> {
1385            p.status
1386                .as_ref()
1387                .map(|s| s.flux_resources.clone())
1388                .unwrap_or_default()
1389        }
1390        fn coord_shape(refs: &[FluxResourceRef]) -> Vec<(String, String, String, String)> {
1391            refs.iter()
1392                .map(|r| {
1393                    let (ns, av, kind, name) = r.fetch_coords();
1394                    (
1395                        ns.to_string(),
1396                        av.to_string(),
1397                        kind.to_string(),
1398                        name.to_string(),
1399                    )
1400                })
1401                .collect()
1402        }
1403        // Missing status.
1404        let mut p = Process::new("api", empty_spec());
1405        p.status = None;
1406        assert_eq!(
1407            coord_shape(p.observed_flux_resources()),
1408            coord_shape(&pre_lift(&p))
1409        );
1410        // Populated status, empty slot.
1411        let p = process_with_flux_resources(vec![]);
1412        assert_eq!(
1413            coord_shape(p.observed_flux_resources()),
1414            coord_shape(&pre_lift(&p))
1415        );
1416        // Populated status, one ref.
1417        let p = process_with_flux_resources(vec![sample_flux_ref("obs")]);
1418        assert_eq!(
1419            coord_shape(p.observed_flux_resources()),
1420            coord_shape(&pre_lift(&p))
1421        );
1422        // Populated status, multiple refs.
1423        let p = process_with_flux_resources(vec![
1424            sample_flux_ref("obs"),
1425            sample_flux_ref("gw"),
1426            sample_flux_ref("api"),
1427        ]);
1428        assert_eq!(
1429            coord_shape(p.observed_flux_resources()),
1430            coord_shape(&pre_lift(&p))
1431        );
1432    }
1433
1434    #[test]
1435    fn observed_flux_resources_missing_status_and_empty_slot_collapse_to_the_same_slice_shape() {
1436        // Cross-corner coherence pin: the missing-`status` corner and
1437        // the populated-empty-slot corner return slices whose
1438        // `.is_empty()` / `.len()` observations are IDENTICAL. A
1439        // regression that promoted the missing-`status` corner to
1440        // returning `None` (via a signature change) — or that widened
1441        // the empty-slot corner to a synthetic single-element slice
1442        // — would surface here rather than as silent operator-facing
1443        // divergence between a never-status-written Process and a
1444        // status-emptied Process.
1445        let mut p_no_status = Process::new("api", empty_spec());
1446        p_no_status.status = None;
1447        let p_empty_status = process_with_flux_resources(vec![]);
1448        assert_eq!(
1449            p_no_status.observed_flux_resources().len(),
1450            p_empty_status.observed_flux_resources().len()
1451        );
1452        assert_eq!(
1453            p_no_status.observed_flux_resources().is_empty(),
1454            p_empty_status.observed_flux_resources().is_empty()
1455        );
1456    }
1457
1458    #[test]
1459    fn observed_flux_resources_slice_preserves_persisted_ordering() {
1460        // Ordering-preservation pin: the borrowed slice preserves
1461        // the exact insertion order of the persisted vector — no
1462        // sort, no dedup, no reshape. A regression that inserted a
1463        // sort or reordering would silently misroute per-ref
1464        // observations at the downstream VERIFY-phase / ATTEST-
1465        // heartbeat consumers, both of which walk the slice
1466        // positionally and correlate the position to the observed
1467        // readiness.
1468        let refs = vec![
1469            sample_flux_ref("z-last"),
1470            sample_flux_ref("a-first"),
1471            sample_flux_ref("m-middle"),
1472        ];
1473        let p = process_with_flux_resources(refs);
1474        let observed = p.observed_flux_resources();
1475        assert_eq!(observed[0].name, "z-last");
1476        assert_eq!(observed[1].name, "a-first");
1477        assert_eq!(observed[2].name, "m-middle");
1478    }
1479
1480    // ─── Process::observed_pid substrate pins ─────────────────────────
1481    //
1482    // Pins the borrow-form status-projection primitive on the PID axis
1483    // that owns the 3-line `.status.as_ref().and_then(|s| s.pid.clone())`
1484    // chain the two hand-authored `tatara-reconciler::phase_machine`
1485    // sites (`handle_forking` ALLOCATE-PID gate + `handle_exiting`
1486    // SIGTERM cascade) restated by hand pre-lift. Peer to the sibling
1487    // `observed_flux_resources_*` pin family on the flux-resources
1488    // axis; both compose the missing-`status` fallback + borrow-form
1489    // return-shape skeleton on distinct `ProcessStatus` slots. Fail-
1490    // before-pass-after granularity: `observed_pid` did not exist
1491    // pre-lift, so any test invoking it fails to compile pre-lift and
1492    // passes post-lift.
1493
1494    fn process_with_pid(pid: Option<&str>) -> Process {
1495        let mut p = Process::new("api-gateway", empty_spec());
1496        p.metadata.namespace = Some("prod".into());
1497        let mut status = ProcessStatus::default();
1498        status.pid = pid.map(str::to_string);
1499        p.status = Some(status);
1500        p
1501    }
1502
1503    #[test]
1504    fn observed_pid_returns_none_when_status_is_none() {
1505        // Missing-`status` corner pin: the primitive collapses the
1506        // no-status case to `None` so downstream `.is_some()` /
1507        // `if let Some(_)` / `.map(...)` behave identically on a
1508        // `Process` whose status field is `None` and on one whose
1509        // status carries an unpopulated `pid` slot. Matches the
1510        // pre-lift `.and_then(...)` chain's `None` byte-identically
1511        // at every reconciler consumer's downstream shape.
1512        let mut p = Process::new("api", empty_spec());
1513        p.status = None;
1514        assert!(p.observed_pid().is_none());
1515    }
1516
1517    #[test]
1518    fn observed_pid_returns_none_when_pid_slot_is_none() {
1519        // Empty-slot-under-populated-status corner pin: the
1520        // primitive returns `None`, matching the missing-`status`
1521        // corner byte-identically. A regression that treated the
1522        // two corners differently (a `None`-vs-`Some("")` signal
1523        // that downstream consumers could grep on) would silently
1524        // promote an internal representation detail (whether the
1525        // reconciler has ever written a status subresource) into
1526        // observable behavior at the ALLOCATE-PID gate.
1527        let p = process_with_pid(None);
1528        assert!(p.observed_pid().is_none());
1529    }
1530
1531    #[test]
1532    fn observed_pid_returns_borrowed_str_when_pid_slot_is_populated() {
1533        // Happy-path pin: with a populated `status.pid` slot, the
1534        // primitive returns a borrowed `&str` whose contents match
1535        // the persisted `String`. A regression that filtered /
1536        // reshaped / canonicalized the string would surface here
1537        // rather than as silent skew at the downstream cascade
1538        // comparator's `.as_deref() == Some(...)` equality check.
1539        let p = process_with_pid(Some("seph.1.7"));
1540        assert_eq!(p.observed_pid(), Some("seph.1.7"));
1541    }
1542
1543    #[test]
1544    fn observed_pid_is_a_zero_copy_borrow_projection() {
1545        // Borrow-discipline pin: the returned `&str` borrows the
1546        // persisted `String`'s underlying byte buffer in place —
1547        // NOT a fresh allocation or a clone. A regression that
1548        // switched the projection to an owned `String` (via
1549        // `.clone()` or `.to_owned()`) would defeat the zero-copy
1550        // contract the lift's primary strict-widening delivers
1551        // (the pre-lift 3-line chain eagerly cloned the `String`
1552        // per reconcile pass at BOTH call sites even though the
1553        // ALLOCATE-PID gate immediately dropped the clone and the
1554        // SIGTERM cascade only re-borrowed it via `.as_str()`; the
1555        // post-lift primitive borrows). Peer to the sibling
1556        // `observed_flux_resources_is_a_zero_copy_borrow_projection`
1557        // pin on the flux-resources borrow-projection axis.
1558        let p = process_with_pid(Some("seph.1.7"));
1559        let observed = p.observed_pid().expect("populated slot");
1560        let persisted = p.status.as_ref().unwrap().pid.as_ref().unwrap();
1561        assert!(std::ptr::eq(observed.as_ptr(), persisted.as_ptr()));
1562    }
1563
1564    #[test]
1565    fn observed_pid_is_a_pure_projection() {
1566        // Purity pin: calling the projection twice on the same
1567        // `Process` returns byte-identical `&str`s (same pointer,
1568        // same length). A regression that introduced state — a
1569        // lazy-cached slice materialized on first call, a
1570        // normalization step that ran once and cached — would
1571        // surface here rather than as silent drift between the
1572        // ALLOCATE-PID gate and the SIGTERM cascade on the SAME
1573        // `Process` within one reconcile pass.
1574        let p = process_with_pid(Some("seph.1.7"));
1575        let a = p.observed_pid().expect("populated slot");
1576        let b = p.observed_pid().expect("populated slot");
1577        assert!(std::ptr::eq(a.as_ptr(), b.as_ptr()));
1578        assert_eq!(a.len(), b.len());
1579    }
1580
1581    #[test]
1582    fn observed_pid_matches_pre_lift_reconciler_chain_shape() {
1583        // Byte-identical parity pin between the borrow-form
1584        // primitive here and the pre-lift `tatara-reconciler
1585        // ::phase_machine` 3-line chain shape. Sweeps every corner
1586        // every callsite plausibly encounters (missing status,
1587        // empty pid slot, populated pid slot). A regression that
1588        // inserted a normalization step at the primitive the pre-
1589        // lift chain does NOT apply — or vice versa — surfaces
1590        // here rather than as silent drift between the pre-lift
1591        // consumer sites and the ONE substrate owner they now
1592        // route through. Peer to
1593        // `observed_flux_resources_matches_pre_lift_reconciler_chain_shape`
1594        // on the flux-resources axis's borrow-form primitive.
1595        fn pre_lift(p: &Process) -> Option<String> {
1596            p.status.as_ref().and_then(|s| s.pid.clone())
1597        }
1598        // Missing status.
1599        let mut p = Process::new("api", empty_spec());
1600        p.status = None;
1601        assert_eq!(p.observed_pid().map(str::to_string), pre_lift(&p));
1602        // Populated status, empty pid slot.
1603        let p = process_with_pid(None);
1604        assert_eq!(p.observed_pid().map(str::to_string), pre_lift(&p));
1605        // Populated status, populated pid slot.
1606        let p = process_with_pid(Some("seph.1.7"));
1607        assert_eq!(p.observed_pid().map(str::to_string), pre_lift(&p));
1608    }
1609
1610    #[test]
1611    fn observed_pid_missing_status_and_empty_slot_collapse_to_the_same_option_shape() {
1612        // Cross-corner coherence pin: the missing-`status` corner
1613        // and the populated-empty-slot corner return `Option`s whose
1614        // `.is_none()` observations are IDENTICAL. A regression
1615        // that promoted the missing-`status` corner to returning a
1616        // typed error (via a signature change to `Result<_, _>`) —
1617        // or that widened the empty-slot corner to a synthetic
1618        // `Some("")` — would surface here rather than as silent
1619        // operator-facing divergence between a never-status-
1620        // written Process and a status-emptied Process on the
1621        // ALLOCATE-PID gate.
1622        let mut p_no_status = Process::new("api", empty_spec());
1623        p_no_status.status = None;
1624        let p_empty_slot = process_with_pid(None);
1625        assert_eq!(
1626            p_no_status.observed_pid().is_none(),
1627            p_empty_slot.observed_pid().is_none()
1628        );
1629        assert_eq!(
1630            p_no_status.observed_pid().is_some(),
1631            p_empty_slot.observed_pid().is_some()
1632        );
1633    }
1634
1635    #[test]
1636    fn observed_pid_preserves_hierarchical_pid_format() {
1637        // Format-preservation pin: the hierarchical PID path
1638        // (dotted-segment form `seph.1.7`, matching the ported
1639        // `convergence-controller/src/identity.rs` scheme) reaches
1640        // the caller with segments and separators byte-identical
1641        // to the persisted `String`. A regression that inserted a
1642        // canonicalization pass (a segment-count validator, a
1643        // separator swap `.` → `/`, a leading/trailing whitespace
1644        // trim) would silently misroute the SIGTERM cascade's
1645        // `spec.identity.parent == Some(pid)` comparator against
1646        // children whose `parent` field was authored in the ported
1647        // scheme's exact form.
1648        for pid in ["seph", "seph.1", "seph.1.7", "seph.1.7.42"] {
1649            let p = process_with_pid(Some(pid));
1650            assert_eq!(p.observed_pid(), Some(pid));
1651        }
1652    }
1653
1654    // ─── Process::observed_attestation substrate pins ─────────────────
1655    //
1656    // Pins the borrow-form status-projection primitive on the
1657    // attestation-chain axis that owns the 3-line
1658    // `.status.as_ref().and_then(|s| s.attestation.as_ref())` chain
1659    // the two hand-authored `tatara-reconciler` sites
1660    // (`phase_machine::advance_to_attested` ATTEST composer +
1661    // `render::render_export_jobs` export-Job builder) restated by
1662    // hand pre-lift. Peer to the sibling `observed_pid_*` +
1663    // `observed_flux_resources_*` pin families; all three compose
1664    // the missing-`status` fallback + borrow-form return-shape
1665    // skeleton on distinct `ProcessStatus` slots. Fail-before-pass-
1666    // after granularity: `observed_attestation` did not exist
1667    // pre-lift, so any test invoking it fails to compile pre-lift
1668    // and passes post-lift.
1669
1670    fn sample_attestation(artifact: &str, intent: &str) -> ProcessAttestation {
1671        // Distinct pillar strings so a regression that swapped the
1672        // artifact / intent pillars silently surfaces as an
1673        // equality failure at the composed-root parity pin.
1674        ProcessAttestation::initial(artifact.to_string(), None, intent.to_string())
1675    }
1676
1677    fn process_with_attestation(attestation: Option<ProcessAttestation>) -> Process {
1678        let mut p = Process::new("api-gateway", empty_spec());
1679        p.metadata.namespace = Some("prod".into());
1680        let mut status = ProcessStatus::default();
1681        status.attestation = attestation;
1682        p.status = Some(status);
1683        p
1684    }
1685
1686    #[test]
1687    fn observed_attestation_returns_none_when_status_is_none() {
1688        // Missing-`status` corner pin: the primitive collapses the
1689        // no-status case to `None` so downstream `.is_some()` /
1690        // `if let Some(_)` / `.map(...)` behave identically on a
1691        // `Process` whose status field is `None` and on one whose
1692        // status carries an unpopulated `attestation` slot.
1693        // Matches the pre-lift `.and_then(...)` chain's `None`
1694        // byte-identically at every reconciler consumer's
1695        // downstream shape.
1696        let mut p = Process::new("api", empty_spec());
1697        p.status = None;
1698        assert!(p.observed_attestation().is_none());
1699    }
1700
1701    #[test]
1702    fn observed_attestation_returns_none_when_attestation_slot_is_none() {
1703        // Empty-slot-under-populated-status corner pin: the
1704        // primitive returns `None`, matching the missing-`status`
1705        // corner byte-identically. A regression that treated the
1706        // two corners differently (a `None`-vs-`Some(_)` signal
1707        // that downstream consumers could grep on) would silently
1708        // promote an internal representation detail (whether the
1709        // reconciler has ever written a status subresource) into
1710        // observable behavior at the ATTEST composer's
1711        // seed-vs-chain branch.
1712        let p = process_with_attestation(None);
1713        assert!(p.observed_attestation().is_none());
1714    }
1715
1716    #[test]
1717    fn observed_attestation_returns_borrow_when_slot_is_populated() {
1718        // Happy-path pin: with a populated `status.attestation`
1719        // slot, the primitive returns a borrowed
1720        // `&ProcessAttestation` whose fields match the persisted
1721        // record. A regression that filtered / reshaped /
1722        // canonicalized the record would surface here rather than
1723        // as silent skew at the downstream `prior.next(pillars)`
1724        // chain composer + the ephemeral-export receipt's
1725        // `previous_root` linker.
1726        let att = sample_attestation("art-1", "int-1");
1727        let composed_root = att.composed_root.clone();
1728        let p = process_with_attestation(Some(att));
1729        let observed = p.observed_attestation().expect("populated slot");
1730        assert_eq!(observed.artifact_hash, "art-1");
1731        assert_eq!(observed.intent_hash, "int-1");
1732        assert_eq!(observed.composed_root, composed_root);
1733        assert_eq!(observed.generation, 0);
1734        assert!(observed.previous_root.is_none());
1735    }
1736
1737    #[test]
1738    fn observed_attestation_is_a_zero_copy_borrow_projection() {
1739        // Borrow-discipline pin: the returned reference points at
1740        // the persisted `ProcessAttestation` in place — NOT a fresh
1741        // allocation or a clone. A regression that switched the
1742        // projection to an owned `ProcessAttestation` (via
1743        // `.clone()`) would defeat the zero-copy contract the
1744        // lift's primary strict-widening delivers (the pre-lift
1745        // 3-line chain returned a borrow, but the export-Job
1746        // builder then cloned `composed_root` off it; the post-
1747        // lift primitive preserves the borrow all the way to the
1748        // consumer's own cloning choice). Peer to the sibling
1749        // `observed_pid_is_a_zero_copy_borrow_projection` +
1750        // `observed_flux_resources_is_a_zero_copy_borrow_projection`
1751        // pins on the PID + flux-resources borrow-projection axes.
1752        let att = sample_attestation("art-1", "int-1");
1753        let p = process_with_attestation(Some(att));
1754        let observed = p.observed_attestation().expect("populated slot") as *const _;
1755        let persisted = p.status.as_ref().unwrap().attestation.as_ref().unwrap() as *const _;
1756        assert!(std::ptr::eq(observed, persisted));
1757    }
1758
1759    #[test]
1760    fn observed_attestation_is_a_pure_projection() {
1761        // Purity pin: calling the projection twice on the same
1762        // `Process` returns byte-identical borrows (same pointer).
1763        // A regression that introduced state — a lazy-cached
1764        // reference materialized on first call, a normalization
1765        // step that ran once and cached — would surface here
1766        // rather than as silent drift between the ATTEST composer
1767        // and the ephemeral-export receipt chain on the SAME
1768        // `Process` within one reconcile pass.
1769        let att = sample_attestation("art-1", "int-1");
1770        let p = process_with_attestation(Some(att));
1771        let a = p.observed_attestation().expect("populated slot") as *const _;
1772        let b = p.observed_attestation().expect("populated slot") as *const _;
1773        assert!(std::ptr::eq(a, b));
1774    }
1775
1776    #[test]
1777    fn observed_attestation_matches_pre_lift_reconciler_chain_shape() {
1778        // Byte-identical parity pin between the borrow-form
1779        // primitive here and the pre-lift `tatara-reconciler`
1780        // 3-line chain shape. Sweeps every corner every callsite
1781        // plausibly encounters (missing status, empty attestation
1782        // slot, populated attestation slot). A regression that
1783        // inserted a normalization step at the primitive the pre-
1784        // lift chain does NOT apply — or vice versa — surfaces
1785        // here rather than as silent drift between the pre-lift
1786        // consumer sites and the ONE substrate owner they now
1787        // route through. Peer to
1788        // `observed_pid_matches_pre_lift_reconciler_chain_shape` +
1789        // `observed_flux_resources_matches_pre_lift_reconciler_chain_shape`
1790        // on the PID + flux-resources axes.
1791        // `ProcessAttestation` does not derive `PartialEq` — the
1792        // parity check walks the `composed_root` field (the
1793        // byte-string every downstream consumer keys off) so a
1794        // regression that reshaped the record without touching
1795        // the composed-root observation surfaces here through
1796        // the receipt-chain projection.
1797        fn pre_lift(p: &Process) -> Option<String> {
1798            p.status
1799                .as_ref()
1800                .and_then(|s| s.attestation.as_ref())
1801                .map(|a| a.composed_root.clone())
1802        }
1803        // Missing status.
1804        let mut p = Process::new("api", empty_spec());
1805        p.status = None;
1806        assert_eq!(
1807            p.observed_attestation().map(|a| a.composed_root.clone()),
1808            pre_lift(&p)
1809        );
1810        // Populated status, empty attestation slot.
1811        let p = process_with_attestation(None);
1812        assert_eq!(
1813            p.observed_attestation().map(|a| a.composed_root.clone()),
1814            pre_lift(&p)
1815        );
1816        // Populated status, populated attestation slot.
1817        let p = process_with_attestation(Some(sample_attestation("art-1", "int-1")));
1818        assert_eq!(
1819            p.observed_attestation().map(|a| a.composed_root.clone()),
1820            pre_lift(&p)
1821        );
1822    }
1823
1824    #[test]
1825    fn observed_attestation_missing_status_and_empty_slot_collapse_to_the_same_option_shape() {
1826        // Cross-corner coherence pin: the missing-`status` corner
1827        // and the populated-empty-slot corner return `Option`s
1828        // whose `.is_none()` observations are IDENTICAL. A
1829        // regression that promoted the missing-`status` corner to
1830        // returning a typed error (via a signature change to
1831        // `Result<_, _>`) — or that widened the empty-slot corner
1832        // to a synthetic `Some(default_attestation)` — would
1833        // surface here rather than as silent operator-facing
1834        // divergence between a never-status-written Process and
1835        // an attestation-emptied Process on the ATTEST composer's
1836        // seed-vs-chain branch.
1837        let mut p_no_status = Process::new("api", empty_spec());
1838        p_no_status.status = None;
1839        let p_empty_slot = process_with_attestation(None);
1840        assert_eq!(
1841            p_no_status.observed_attestation().is_none(),
1842            p_empty_slot.observed_attestation().is_none()
1843        );
1844        assert_eq!(
1845            p_no_status.observed_attestation().is_some(),
1846            p_empty_slot.observed_attestation().is_some()
1847        );
1848    }
1849
1850    #[test]
1851    fn observed_attestation_preserves_chain_generation_field() {
1852        // Generation-preservation pin: a chained attestation
1853        // (`prior.next(...)` at generation N ≥ 1 with a
1854        // `previous_root` linked to `prior.composed_root`) reaches
1855        // the caller with its `generation` counter + `previous_root`
1856        // link byte-identical to the persisted record. The pre-lift
1857        // ATTEST composer discriminated exactly on this borrow's
1858        // `Some(prior)` vs `None` arm; a regression that dropped
1859        // the chain's `generation` counter (say, by folding
1860        // `next(...)` into a fresh `initial(...)` on every
1861        // reconcile pass) would silently reset every chain and
1862        // orphan every downstream `previous_root` link, but that
1863        // drift is invisible to a Process CRD reader who only
1864        // observes the LATEST composed_root.
1865        let prior = sample_attestation("art-0", "int-0");
1866        let chained = prior.next("art-1".to_string(), None, "int-1".to_string());
1867        let expected_generation = chained.generation;
1868        let expected_previous = chained.previous_root.clone();
1869        let p = process_with_attestation(Some(chained));
1870        let observed = p.observed_attestation().expect("populated slot");
1871        assert_eq!(observed.generation, expected_generation);
1872        assert_eq!(observed.generation, 1);
1873        assert_eq!(observed.previous_root, expected_previous);
1874        assert_eq!(
1875            observed.previous_root.as_deref(),
1876            Some(prior.composed_root.as_str())
1877        );
1878    }
1879
1880    // ─── Process::observed_phase substrate pins ───────────────────────
1881    //
1882    // The copy-form status-projection primitive on the phase axis.
1883    // Collapses the paired 3-line `.status.as_ref().map(|s| s.phase)`
1884    // chain every consumer in `tatara-reconciler` restated by hand
1885    // pre-lift at FIVE sites. Peer to the borrow-form
1886    // `observed_pid_*` + `observed_flux_resources_*` +
1887    // `observed_attestation_*` pin families; all four compose the
1888    // same missing-`status` fallback skeleton on distinct
1889    // `ProcessStatus` slots, with the phase-axis form returning
1890    // `Option<ProcessPhase>` (copy of a `Copy` scalar) rather than
1891    // `Option<&T>` (borrow) because the underlying slot is a bare
1892    // `ProcessPhase` — no allocation to borrow past, and the enum
1893    // is one byte on the wire. Each pin fails-before-pass-after
1894    // granularity: `observed_phase` did not exist pre-lift, so any
1895    // test invoking it fails to compile pre-lift and passes
1896    // post-lift.
1897
1898    fn process_with_phase(phase: Option<ProcessPhase>) -> Process {
1899        let mut p = Process::new("api-gateway", empty_spec());
1900        p.metadata.namespace = Some("prod".into());
1901        if let Some(ph) = phase {
1902            let mut status = ProcessStatus::default();
1903            status.phase = ph;
1904            p.status = Some(status);
1905        }
1906        p
1907    }
1908
1909    #[test]
1910    fn observed_phase_returns_none_when_status_is_none() {
1911        // Missing-`status` corner pin: the primitive collapses the
1912        // no-status case to `None` so downstream `.unwrap_or(...)`
1913        // at every reconciler consumer chooses the default
1914        // deliberately (`Pending` for the top-level dispatch seed
1915        // + boundary evaluator + routing groupby; `Attested` for
1916        // the released-from annotation composer). Matches the
1917        // pre-lift `.map(|s| s.phase)` chain's `None`
1918        // byte-identically at every consumer's downstream shape.
1919        let mut p = Process::new("api", empty_spec());
1920        p.status = None;
1921        assert!(p.observed_phase().is_none());
1922    }
1923
1924    #[test]
1925    fn observed_phase_returns_some_default_when_status_is_populated_with_default_phase() {
1926        // Populated-status corner pin: the primitive returns
1927        // `Some(ProcessPhase::default())` — a `ProcessStatus`
1928        // constructed via `default()` carries `phase: Pending`
1929        // because the phase field is a bare `ProcessPhase` (not
1930        // `Option<ProcessPhase>`), so there is NO "empty slot"
1931        // corner peer to the borrow-form projections' empty-slot
1932        // pins. A regression that reshaped the return type to
1933        // filter out `Pending` (treating it as "unset") would
1934        // surface here and silently break the top-level
1935        // dispatcher's Pending → Forking transition on a Process
1936        // freshly written by the reconciler.
1937        let p = process_with_phase(Some(ProcessPhase::default()));
1938        assert_eq!(p.observed_phase(), Some(ProcessPhase::Pending));
1939        assert_eq!(p.observed_phase(), Some(ProcessPhase::default()));
1940    }
1941
1942    #[test]
1943    fn observed_phase_returns_persisted_phase_when_status_is_populated() {
1944        // Happy-path pin: with a populated `status.phase` slot,
1945        // the primitive returns the persisted `ProcessPhase`.
1946        // A regression that filtered / reshaped / canonicalized
1947        // the phase would surface here rather than as silent
1948        // skew at the top-level dispatcher's phase handler
1949        // dispatch on the SAME Process.
1950        let p = process_with_phase(Some(ProcessPhase::Running));
1951        assert_eq!(p.observed_phase(), Some(ProcessPhase::Running));
1952    }
1953
1954    #[test]
1955    fn observed_phase_is_a_pure_projection() {
1956        // Purity pin: two consecutive calls return byte-identical
1957        // `Option<ProcessPhase>` values (no lazy materialization,
1958        // no interior mutation of `self`). Peer to the sibling
1959        // `observed_pid_is_a_pure_projection` +
1960        // `observed_flux_resources_is_a_pure_projection` +
1961        // `observed_attestation_is_a_pure_projection` pins; all
1962        // four bind the pure-projection discipline on the ONE
1963        // substrate accessor per status slot.
1964        let p = process_with_phase(Some(ProcessPhase::Attested));
1965        let a = p.observed_phase();
1966        let b = p.observed_phase();
1967        assert_eq!(a, b);
1968        assert_eq!(a, Some(ProcessPhase::Attested));
1969    }
1970
1971    #[test]
1972    fn observed_phase_matches_pre_lift_reconciler_chain_shape() {
1973        // Parity pin: sweeps the two corners every pre-lift
1974        // consumer plausibly encountered (missing status,
1975        // populated status with a particular phase) and compares
1976        // the substrate call against a hand-authored pre-lift
1977        // chain byte-identically. A regression that reshaped ANY
1978        // of the two corners would surface here rather than as
1979        // silent operator-facing skew between the top-level
1980        // dispatcher and any of the four other reconciler
1981        // consumers on the SAME `Process`.
1982        fn pre_lift(p: &Process) -> Option<ProcessPhase> {
1983            p.status.as_ref().map(|s| s.phase)
1984        }
1985        let mut p = Process::new("api", empty_spec());
1986        p.status = None;
1987        assert_eq!(p.observed_phase(), pre_lift(&p));
1988        let p = process_with_phase(Some(ProcessPhase::Running));
1989        assert_eq!(p.observed_phase(), pre_lift(&p));
1990        let p = process_with_phase(Some(ProcessPhase::Attested));
1991        assert_eq!(p.observed_phase(), pre_lift(&p));
1992        let p = process_with_phase(Some(ProcessPhase::Failed));
1993        assert_eq!(p.observed_phase(), pre_lift(&p));
1994    }
1995
1996    #[test]
1997    fn observed_phase_default_unwrap_matches_pre_lift_pending_default() {
1998        // Callsite-shape pin: three of the FIVE pre-lift consumers
1999        // (`controller::reconcile`, `boundary::evaluate_process_phase`,
2000        // `table_controller::stable_name_group_key`) closed the
2001        // 3-line chain with `.unwrap_or(ProcessPhase::Pending)`
2002        // (identical to `.unwrap_or_default()`). This pin binds
2003        // that call-site shape: `observed_phase().unwrap_or
2004        // (Pending)` returns `Pending` on missing status and the
2005        // persisted phase otherwise. A regression that swapped
2006        // the `None` sentinel's downstream default would surface
2007        // here rather than as silent skew at three of the five
2008        // consumer sites.
2009        let mut p = Process::new("api", empty_spec());
2010        p.status = None;
2011        assert_eq!(
2012            p.observed_phase().unwrap_or(ProcessPhase::Pending),
2013            ProcessPhase::Pending
2014        );
2015        let p = process_with_phase(Some(ProcessPhase::Running));
2016        assert_eq!(
2017            p.observed_phase().unwrap_or(ProcessPhase::Pending),
2018            ProcessPhase::Running
2019        );
2020    }
2021
2022    #[test]
2023    fn observed_phase_attested_unwrap_matches_pre_lift_released_from_default() {
2024        // Callsite-shape pin: the ONE pre-lift consumer
2025        // (`phase_machine::p_current_phase_str` — the
2026        // released-from annotation composer) closed the 3-line
2027        // chain with `.unwrap_or(ProcessPhase::Attested)` rather
2028        // than the `Default` (`Pending`). This pin binds that
2029        // call-site shape: `observed_phase().unwrap_or(Attested)`
2030        // returns `Attested` on missing status and the persisted
2031        // phase otherwise. A regression that folded the
2032        // `Attested`-default consumer into the `Pending`-default
2033        // majority would break the SIGSTOP/SIGCONT release gate's
2034        // "which annotation label to emit" branch — the pin binds
2035        // the primitive at the raw `Option<ProcessPhase>` form so
2036        // this default choice stays local at the callsite.
2037        let mut p = Process::new("api", empty_spec());
2038        p.status = None;
2039        assert_eq!(
2040            p.observed_phase().unwrap_or(ProcessPhase::Attested),
2041            ProcessPhase::Attested
2042        );
2043        let p = process_with_phase(Some(ProcessPhase::Failed));
2044        assert_eq!(
2045            p.observed_phase().unwrap_or(ProcessPhase::Attested),
2046            ProcessPhase::Failed
2047        );
2048    }
2049
2050    #[test]
2051    fn observed_phase_preserves_every_process_phase_variant() {
2052        // Round-trip pin: every `ProcessPhase` variant round-
2053        // trips through the primitive unchanged. Peer to the
2054        // sibling `observed_pid_preserves_hierarchical_pid_format`
2055        // pin's dotted-segment sweep; this pin sweeps the closed
2056        // set of `ProcessPhase` variants directly so a
2057        // canonicalization pass that dropped or reshaped one
2058        // (e.g. folded `Reconverging` back into `Execing`, or
2059        // remapped `Zombie` to `Reaped`) surfaces here rather
2060        // than as silent skew at the SIGSTOP/SIGCONT release
2061        // gate's phase-name annotation branch. Covers every
2062        // variant the `ProcessPhase::DeriveClosedSet` enumerates
2063        // so a future variant addition surfaces via the closed-
2064        // set macro rather than at a silent partial sweep.
2065        for phase in [
2066            ProcessPhase::Pending,
2067            ProcessPhase::Forking,
2068            ProcessPhase::Execing,
2069            ProcessPhase::Running,
2070            ProcessPhase::Attested,
2071            ProcessPhase::Reconverging,
2072            ProcessPhase::Releasing,
2073            ProcessPhase::Exiting,
2074            ProcessPhase::Failed,
2075            ProcessPhase::Zombie,
2076            ProcessPhase::Reaped,
2077        ] {
2078            let p = process_with_phase(Some(phase));
2079            assert_eq!(
2080                p.observed_phase(),
2081                Some(phase),
2082                "phase variant {phase:?} did not round-trip"
2083            );
2084        }
2085    }
2086}