Skip to main content

tatara_process/
hostname.rs

1//! Hostname helpers — typed FQDN formatting matching `nix/lib/fleet-
2//! domains.nix`'s `mkHostname` pattern.
3//!
4//! The substrate move: every FQDN this codebase emits is computed
5//! here. Two functions ([`fmt_fqdn`] for the per-instance form +
6//! [`fmt_fqdn_stable`] for the unprefixed stable-claim form) and one
7//! deterministic ephemeral-id derivation ([`ephemeral_id_from_spec`])
8//! are the single source of truth — no string `format!()` of DNS
9//! syntax anywhere else in the tree.
10//!
11//! Forms:
12//!
13//! ```text
14//!   Per-instance: ${app}.${ephemeral_id}.${cluster}.${location}.${domain}
15//!   Stable:       ${app}.${cluster}.${location}.${domain}
16//! ```
17//!
18//! Where `${ephemeral_id}` is:
19//!
20//! * `RoutingHostname.instance` when set — a named slot like
21//!   `demo-prod` or `pr-1234`.
22//! * `EPHEMERAL_ID_HASH_LEN` (= 8) hex chars of
23//!   `BLAKE3(canonical_spec_json)` when unset — a content-hash slot
24//!   that changes only when the Process's spec changes.
25//!
26//! All four FQDN segments are validated as RFC 1123 DNS labels at
27//! the boundary — lowercase alphanumeric + hyphen, 1–63 chars, no
28//! leading/trailing hyphen. Validation errors surface as typed
29//! [`HostnameError`] variants so callers can render targeted
30//! operator messages.
31
32use serde::Serialize;
33
34use crate::routing::RoutingHostname;
35
36/// Number of hex chars from BLAKE3 to use as the content-hash form
37/// of `ephemeral_id`. 8 = 32 bits of entropy; collision probability
38/// at 1k concurrent Processes ≈ 1 in 8.5 million. Comfortable for
39/// any single cluster's working set, room to grow.
40pub const EPHEMERAL_ID_HASH_LEN: usize = 8;
41
42/// Reserved 2-part forms forbidden as `app` values (saguão control
43/// plane — see pleme-io CLAUDE.md §Fleet hostname pattern).
44const RESERVED_APP_LABELS: &[&str] = &["auth", "cracha"];
45
46/// Why a hostname can't be formatted. Typed so callers can branch.
47#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
48pub enum HostnameError {
49    #[error("invalid DNS label {label:?} for segment {segment}: {reason}")]
50    InvalidLabel {
51        segment: &'static str,
52        label: String,
53        reason: &'static str,
54    },
55    #[error("app label {0:?} is reserved for the saguão control plane")]
56    ReservedApp(String),
57}
58
59impl HostnameError {
60    /// Construct an [`HostnameError::InvalidLabel`] variant — the ONE
61    /// substrate primitive owning the three-slot
62    /// `HostnameError::InvalidLabel { segment, label: <str>.to_string(),
63    /// reason: <static> }` construction shape every RFC 1123 DNS-label
64    /// rejection site in this module walks BEFORE returning through the
65    /// `?` short-circuit.
66    ///
67    /// Pre-lift the shape was hand-authored at FOUR module-private
68    /// validation sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication
69    /// threshold, each restating the SAME three-field struct-literal
70    /// verbatim modulo the per-site `reason` slot:
71    ///
72    /// * [`validate_label`] × 3 — the length gate
73    ///   (`"must be 1–63 characters"`), the leading/trailing-hyphen gate
74    ///   (`"must not start or end with a hyphen"`), and the character-
75    ///   set gate (`"must contain only [a-z0-9-]"`); each walked
76    ///   `HostnameError::InvalidLabel { segment, label: label.to_string
77    ///   (), reason: <per-gate literal> }` with the same
78    ///   `segment: &'static str` slot threaded through from the caller
79    ///   and the same `label.to_string()` projection on the borrowed
80    ///   `&str` label slot.
81    /// * [`validate_domain`] × 1 — the empty-domain early-return
82    ///   (`"must not be empty"`); same three-slot struct literal shape,
83    ///   same `<str>.to_string()` projection on the borrowed `domain`
84    ///   argument, sibling to the three sites in [`validate_label`] on
85    ///   the RFC 1123 rejection axis.
86    ///
87    /// All four sites walked the SAME struct-literal three-slot shape
88    /// verbatim, differing only in the `reason: &'static str` slot they
89    /// bound. Post-lift each callsite reads `HostnameError::invalid_label
90    /// (segment, label, "<reason>")` and the construction shape lives at
91    /// ONE substrate owner here.
92    ///
93    /// Peer to the two-step composer [`validate_app`] on the same
94    /// hostname-validation axis, split by ABSTRACTION LEVEL:
95    /// [`validate_app`] owns the ordered check chain callers CONSUME
96    /// (RFC 1123 → reserved-name); this constructor owns the typed-
97    /// variant PRODUCTION callers of those checks EMIT. Together the
98    /// two primitives partition the module's rejection surface — the
99    /// composer says WHEN to reject, the constructor says WHAT the
100    /// rejection variant looks like on the wire.
101    ///
102    /// The `label` slot accepts `impl Into<String>` so a caller with a
103    /// borrowed `&str` label (the four pre-lift sites) reaches
104    /// `invalid_label(segment, label, reason)` without a per-site
105    /// `.to_string()` — the projection lives at the substrate. A caller
106    /// with an owned [`String`] (a future consumer stamping a
107    /// dynamically-composed label into the rejection variant) reaches
108    /// the SAME constructor without a per-site conversion either — the
109    /// `impl Into<String>` bound admits both slot shapes identically.
110    /// A future extension to the variant (a byte-offset slot into the
111    /// source label pinpointing the failing character, a
112    /// [`tracing::Span`] correlation slot, a normalization of the
113    /// label's casing at the substrate before it reaches the operator's
114    /// log stream) lands at THIS ONE constructor and every rejection
115    /// site inherits the upgrade mechanically — no per-site edit at any
116    /// of the four `validate_*` primitives, no drift risk for a fifth
117    /// future validation site that plugs into the same rejection
118    /// policy.
119    ///
120    /// Theory anchor: THEORY.md §VI.1 (generation over composition —
121    /// the three-slot struct-literal recurred at four hand-authored
122    /// sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication trigger and
123    /// lifts to ONE substrate owner here, matching the discipline
124    /// [`validate_app`] and [`validate_fqdn_suffix`] already carry on
125    /// the peer composer axes). THEORY.md §II.1 invariant 5
126    /// (composition preserves proofs — post-lift every rejection site
127    /// surfaces the byte-identical `HostnameError::InvalidLabel` variant
128    /// by CONSTRUCTION rather than by four independent struct-literal
129    /// restatements kept in sync by convention; a regression that re-
130    /// open-coded a site would surface at the pin block below rather
131    /// than as silent operator-facing skew across every downstream
132    /// rejection consumer).
133    #[inline]
134    fn invalid_label(
135        segment: &'static str,
136        label: impl Into<String>,
137        reason: &'static str,
138    ) -> Self {
139        HostnameError::InvalidLabel {
140            segment,
141            label: label.into(),
142            reason,
143        }
144    }
145}
146
147/// Substrate extension trait over `Result<T, HostnameError>` — the ONE
148/// substrate owner of the `.map_err(|e| anyhow::anyhow!("<ctx>: {e}"))`
149/// wrap-shape every reconciler consumer restated by hand at the
150/// hostname-formatter → anyhow error boundary. Peer of
151/// [`crate::kube_error::KubeResultExt`] on the wrap-shape axis; the two
152/// traits partition the flatten-wrap space by underlying error type
153/// (`kube::Error` on that peer, [`HostnameError`] on this one).
154///
155/// Pre-lift the shape was hand-authored at THREE sites in
156/// `tatara-reconciler::render::render_routing` — each of the three
157/// `HostnameError`-returning hostname primitives ([`ephemeral_id_from_spec`],
158/// [`fmt_fqdn`], [`fmt_fqdn_stable`]) had ITS consumer restate the
159/// SAME closure at the R9 routing-edge render — capture the
160/// [`HostnameError`], prepend a static context slug identifying which
161/// hostname primitive faulted, delegate the tail to [`HostnameError`]'s
162/// `Display` impl via the `{e}` slot — differing only in the context
163/// slug prefix each callsite stamped (`"ephemeral_id_from_spec"` /
164/// `"fmt_fqdn (per-instance)"` / `"fmt_fqdn_stable"`). Three
165/// hand-authored callsites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication
166/// threshold.
167///
168/// Post-lift each callsite reads
169/// `<hostname-primitive>().hostname_ctx("<slug>")?` and the wrap-shape
170/// lives at ONE substrate owner here. The composed [`anyhow::Error`]'s
171/// `Display` is byte-identical to the pre-lift chain
172/// (`format!("{ctx}: {e}")`, threading the [`HostnameError`]'s own
173/// `Display` verbatim into the `{e}` slot), so operator-facing log
174/// output and any error-chain greps still match bytewise. A regression
175/// that drifts the separator, swaps the two slots, or wraps the
176/// [`HostnameError`] with a chain-form `source` (which would change
177/// `Display` output on the `err` slot) surfaces at
178/// [`tests::hostname_ctx_static_str_context_matches_pre_lift_format_bytewise`]
179/// rather than as silent operator-facing drift across the three
180/// pre-lift consumers.
181///
182/// ### Naming — `hostname_ctx`, not `anyhow::Context::context`
183///
184/// Same discipline as [`crate::kube_error::KubeResultExt::kube_ctx`] —
185/// `anyhow::Context::context` wraps the source in a chain (so `Display`
186/// emits only the context slug and callers reach the [`HostnameError`]
187/// via [`std::error::Error::source`] traversal), while this trait's
188/// `hostname_ctx` FLATTENS to a display-prefix shape (`"<ctx>: <HostnameError
189/// display>"`) — the pre-lift wire format every consumer's log output
190/// already encoded. Sharing the name would let a caller who has
191/// `anyhow::Context` in scope resolve to the WRONG method (a chain-wrap
192/// instead of the display-prefix flatten) and silently change every
193/// operator log message.
194///
195/// ### Static-slug only (no `_with` peer yet)
196///
197/// Every current callsite composes its slug at compile time
198/// (`"ephemeral_id_from_spec"`, `"fmt_fqdn (per-instance)"`,
199/// `"fmt_fqdn_stable"`); no consumer needs a `format!`-composed
200/// runtime slug. The static-`&'static str` binding keeps the substrate
201/// contract minimal — a future dynamic-slug consumer would add a
202/// `hostname_ctx_with` peer here matching the `kube_ctx_with` shape,
203/// but until then this trait exposes only the static peer.
204///
205/// ### `#[must_use]`
206///
207/// Every consumer threads the `?` short-circuit onto its handler's
208/// `Result<_, anyhow::Error>` return — dropping the wrap swallows the
209/// hostname-format failure entirely, which is never the intended
210/// semantic (a rejected DNS label at emit time silently produces a
211/// resource with a `""` FQDN slot that the K8s API server accepts and
212/// then no downstream Ingress / DNSEndpoint dispatcher can route to).
213/// The attribute surfaces that as a warning at every call site.
214///
215/// Theory anchor: THEORY.md §VI.1 (generation over composition — the
216/// [`HostnameError`] → anyhow-with-display-prefix wrap-shape recurred
217/// at three hand-authored sites past the ★★ PRIME-DIRECTIVE ≥ 2
218/// duplication trigger, and is lifted to ONE substrate owner here).
219/// THEORY.md §II.1 invariant 5 (composition preserves proofs — a
220/// regression that drifts the display-prefix separator or the byte-
221/// shape at ONE site surfaces here at the substrate pin rather than
222/// as silent operator-facing skew across every render_routing tick).
223pub trait HostnameResultExt<T>: Sized {
224    /// Wrap the [`HostnameError`] (if any) with a static context
225    /// prefix, producing an [`anyhow::Result`] whose error `Display`
226    /// reads exactly `"<context>: <HostnameError display>"`.
227    #[must_use = "an error wrap that isn't threaded via `?` swallows the hostname-format failure"]
228    fn hostname_ctx(self, context: &'static str) -> anyhow::Result<T>;
229}
230
231impl<T> HostnameResultExt<T> for Result<T, HostnameError> {
232    // Delegates the display-prefix wrap-shape body to the generic
233    // substrate owner [`crate::err_ctx::ErrCtxExt`]. Pre-lift the body
234    // restated the `.map_err(|e| anyhow::anyhow!("{context}: {e}"))`
235    // closure by hand, byte-identical to the three sibling specialized
236    // peers ([`crate::kube_error::KubeResultExt`],
237    // [`crate::anyhow_flatten::FlattenCtxExt`],
238    // [`crate::err_ctx::ErrCtxExt`] itself). Post-lift the byte-shape
239    // body lives at ONE substrate owner + this impl is a naming-layer
240    // delegate — [`HostnameError`] impls `Display` via `thiserror` so
241    // the generic [`crate::err_ctx::ErrCtxExt`] impl applies to
242    // `Result<T, HostnameError>` directly. Pinned by
243    // [`crate::err_ctx::tests::err_ctx_agrees_with_hostname_ctx_on_hostname_error_result`]
244    // so a regression that re-open-coded the body would surface there
245    // rather than as silent operator-facing skew between the
246    // hostname-side consumer (`render_routing`) and the sibling peer
247    // families.
248
249    #[inline]
250    fn hostname_ctx(self, context: &'static str) -> anyhow::Result<T> {
251        use crate::err_ctx::ErrCtxExt;
252        self.err_ctx(context)
253    }
254}
255
256/// Format the per-instance FQDN.
257///
258/// ```
259/// use tatara_process::hostname::fmt_fqdn;
260/// let fqdn = fmt_fqdn("api", "demo-prod", "pleme-dev", "use1", "quero.lol").unwrap();
261/// assert_eq!(fqdn, "api.demo-prod.pleme-dev.use1.quero.lol");
262/// ```
263pub fn fmt_fqdn(
264    app: &str,
265    ephemeral_id: &str,
266    cluster: &str,
267    location: &str,
268    domain: &str,
269) -> Result<String, HostnameError> {
270    validate_app(app)?;
271    validate_label("ephemeral_id", ephemeral_id)?;
272    validate_fqdn_suffix(cluster, location, domain)?;
273    Ok(format!(
274        "{app}.{ephemeral_id}.{cluster}.{location}.{domain}"
275    ))
276}
277
278/// Format the stable-claim FQDN (no `ephemeral_id` segment).
279///
280/// ```
281/// use tatara_process::hostname::fmt_fqdn_stable;
282/// let fqdn = fmt_fqdn_stable("api", "pleme-dev", "use1", "quero.lol").unwrap();
283/// assert_eq!(fqdn, "api.pleme-dev.use1.quero.lol");
284/// ```
285pub fn fmt_fqdn_stable(
286    app: &str,
287    cluster: &str,
288    location: &str,
289    domain: &str,
290) -> Result<String, HostnameError> {
291    validate_app(app)?;
292    validate_fqdn_suffix(cluster, location, domain)?;
293    Ok(format!("{app}.{cluster}.{location}.{domain}"))
294}
295
296/// Compute the content-hash form of `ephemeral_id` for a given
297/// `ProcessSpec`. Stable across reconciles of the same spec; new
298/// spec content ⇒ new hash ⇒ new DNS slot.
299///
300/// Uses [`EPHEMERAL_ID_HASH_LEN`] hex chars of BLAKE3 over the
301/// canonical JSON of the spec.
302pub fn ephemeral_id_from_spec<T: Serialize>(spec: &T) -> Result<String, HostnameError> {
303    // Canonical-bytes projection rides through the ONE substrate
304    // primitive [`crate::three_pillar::canonical_bytes`] — the
305    // strict, error-propagating peer of `three_pillar::pillar_bytes`
306    // that owns the 2-link `serde_json::to_value → serde_json::to_vec`
307    // canonicalization chain. Pre-lift this site read through a
308    // module-private `canonical_json` helper (removed) that restated
309    // the same 2-link chain byte-for-byte alongside the peer at
310    // `tatara-export-worker::canonical_json` — two hand-authored
311    // sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication threshold.
312    // Post-lift both consumers name the payload ONCE and route
313    // through the ONE substrate owner; the discard of the concrete
314    // `serde_json::Error` diagnostic rides through the local
315    // `HostnameError::InvalidLabel` projection at this callsite so
316    // the operator-facing wording stays byte-identical to the
317    // pre-lift shape.
318    let bytes = crate::three_pillar::canonical_bytes(spec).map_err(|_| {
319        HostnameError::invalid_label("spec", "<unserializable>", "spec failed to canonicalize")
320    })?;
321    Ok(short_hex_blake3(&bytes, EPHEMERAL_ID_HASH_LEN))
322}
323
324/// Resolve the `ephemeral_id` for a single [`RoutingHostname`]
325/// entry. Named slot wins if set; otherwise the content-hash form
326/// is computed from the surrounding `ProcessSpec` (caller passes
327/// in via `fallback_hash`).
328///
329/// The split-arg design keeps this pure — the spec hash is computed
330/// once by the caller (via [`ephemeral_id_from_spec`]) and reused
331/// across every hostname on the same Process.
332pub fn resolve_ephemeral_id<'a>(hostname: &'a RoutingHostname, fallback_hash: &'a str) -> &'a str {
333    match &hostname.instance {
334        Some(s) if !s.is_empty() => s.as_str(),
335        _ => fallback_hash,
336    }
337}
338
339// ─── Validation ────────────────────────────────────────────────────
340
341fn validate_label(segment: &'static str, label: &str) -> Result<(), HostnameError> {
342    if label.is_empty() || label.len() > 63 {
343        return Err(HostnameError::invalid_label(
344            segment,
345            label,
346            "must be 1–63 characters",
347        ));
348    }
349    if label.starts_with('-') || label.ends_with('-') {
350        return Err(HostnameError::invalid_label(
351            segment,
352            label,
353            "must not start or end with a hyphen",
354        ));
355    }
356    if !label
357        .chars()
358        .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
359    {
360        return Err(HostnameError::invalid_label(
361            segment,
362            label,
363            "must contain only [a-z0-9-]",
364        ));
365    }
366    Ok(())
367}
368
369/// Validate a caller-supplied `app` label at the fleet-hostname
370/// boundary — the ONE substrate primitive owning the two-step (RFC 1123
371/// DNS label + saguão-reservation reject) check every hostname composer
372/// runs on its `app` slot BEFORE stamping it into an emitted FQDN.
373///
374/// Pre-lift the two-step check was hand-authored at TWO adjacent public
375/// FQDN composers in this module past the ★★ PRIME-DIRECTIVE ≥ 2
376/// duplication threshold:
377///
378/// * [`fmt_fqdn`] — the per-instance form; the 4-line prelude
379///   preceded the sibling `validate_label("ephemeral_id", …)` +
380///   cluster / location / domain checks.
381/// * [`fmt_fqdn_stable`] — the unprefixed stable-claim form; the same
382///   4-line prelude preceded the cluster / location / domain checks
383///   with no `ephemeral_id` slot in between.
384///
385/// Both restated the SAME 4-line prelude verbatim: (1)
386/// `validate_label("app", app)?` to enforce the RFC 1123 shape (1–63
387/// chars, lowercase alphanumeric + hyphen, no leading / trailing
388/// hyphen), then (2) an early-return
389/// `HostnameError::ReservedApp(app.to_string())` when the label
390/// appears in the module-private [`RESERVED_APP_LABELS`] set
391/// (currently `"auth"` / `"cracha"` — the saguão control-plane
392/// reservations declared in pleme-io CLAUDE.md § Fleet hostname
393/// pattern).
394///
395/// Post-lift each callsite reads `validate_app(app)?` and the ordered
396/// two-step check lives at ONE substrate owner. The step ORDER is
397/// load-bearing: `validate_label` runs first so a reserved label whose
398/// spelling ALSO violates RFC 1123 (an operator who typed `"AUTH"`
399/// instead of `"auth"`) surfaces as
400/// [`HostnameError::InvalidLabel`] (the underlying shape defect),
401/// not as [`HostnameError::ReservedApp`] (the higher-level policy
402/// gate) — matching the pre-lift order both composers hand-authored.
403/// A regression that swapped the two steps would silently re-classify
404/// every such input and callers pattern-matching on the two variants
405/// would branch differently.
406///
407/// A future extension to the reserved set (adding a third saguão name,
408/// a per-cluster reservation surface, a normalized-form lookup that
409/// treats `"Auth"` and `"auth"` as the same reservation) lands at THIS
410/// ONE substrate primitive and both [`fmt_fqdn`] + [`fmt_fqdn_stable`]
411/// inherit the upgrade mechanically — no per-composer edit at either
412/// call site, no drift risk for a third future FQDN-shape composer
413/// that plugs into the same reservation policy.
414///
415/// Theory anchor: THEORY.md §VI.1 (generation over composition — the
416/// 4-line two-step check recurred at two hand-authored composer
417/// preludes past the ★★ PRIME-DIRECTIVE ≥ 2 duplication trigger and
418/// lifts to ONE substrate owner here). THEORY.md §II.1 invariant 5
419/// (composition preserves proofs — the pin block below binds the
420/// primitive at fail-before-pass-after granularity so a regression
421/// that reorders the two steps, drops one, or drifts the typed error
422/// variant surfaces at THESE pins rather than as silent fleet-
423/// hostname skew across every downstream FQDN emit).
424fn validate_app(app: &str) -> Result<(), HostnameError> {
425    validate_label("app", app)?;
426    if RESERVED_APP_LABELS.contains(&app) {
427        return Err(HostnameError::ReservedApp(app.to_string()));
428    }
429    Ok(())
430}
431
432fn validate_domain(segment: &'static str, domain: &str) -> Result<(), HostnameError> {
433    if domain.is_empty() {
434        return Err(HostnameError::invalid_label(
435            segment,
436            domain,
437            "must not be empty",
438        ));
439    }
440    // Multi-label domain — every dot-separated piece must be a valid label.
441    for piece in domain.split('.') {
442        validate_label(segment, piece)?;
443    }
444    Ok(())
445}
446
447/// Validate the shared 3-segment `${cluster}.${location}.${domain}` FQDN
448/// suffix — the ONE substrate primitive owning the ordered
449/// `validate_label("cluster", …) → validate_label("location", …) →
450/// validate_domain("domain", …)` prelude every fleet-hostname composer
451/// runs on the trailing suffix common to BOTH forms
452/// (`${app}.${ephemeral_id}.<suffix>` per-instance and `${app}.<suffix>`
453/// stable) BEFORE stamping it into an emitted FQDN.
454///
455/// Pre-lift the 3-line ordered check was hand-authored at TWO adjacent
456/// public FQDN composers in this module past the ★★ PRIME-DIRECTIVE
457/// ≥ 2 duplication threshold:
458///
459/// * [`fmt_fqdn`] — the per-instance form; the 3-line suffix prelude
460///   followed the sibling `validate_app(app)?` +
461///   `validate_label("ephemeral_id", …)?` head checks and preceded the
462///   `format!("{app}.{ephemeral_id}.{cluster}.{location}.{domain}")`
463///   emission.
464/// * [`fmt_fqdn_stable`] — the unprefixed stable-claim form; the same
465///   3-line suffix prelude followed the sibling `validate_app(app)?`
466///   check with no `ephemeral_id` slot in between and preceded the
467///   `format!("{app}.{cluster}.{location}.{domain}")` emission.
468///
469/// Both restated the SAME 3-line prelude verbatim: (1)
470/// `validate_label("cluster", cluster)?` to enforce the RFC 1123 shape
471/// on the cluster segment, then (2)
472/// `validate_label("location", location)?` for the location segment,
473/// then (3) `validate_domain("domain", domain)?` to enforce the
474/// multi-label domain shape (non-empty AND every dot-split piece a
475/// valid RFC 1123 label).
476///
477/// Post-lift each callsite reads `validate_fqdn_suffix(cluster,
478/// location, domain)?` and the ordered 3-step suffix check lives at
479/// ONE substrate owner. The step ORDER is load-bearing on the typed-
480/// variant surface: `cluster` is checked first so a bad-cluster-and-
481/// bad-location input surfaces as `InvalidLabel { segment: "cluster", .. }`
482/// (matching the pre-lift order both composers hand-authored) rather
483/// than `InvalidLabel { segment: "location", .. }` — callers pattern-
484/// matching on the `segment` slot to render targeted operator messages
485/// branch differently, so a swap of the two steps would silently
486/// re-classify every such input.
487///
488/// Peer to [`validate_app`] on the "ordered validation prelude" axis —
489/// `validate_app` owns the 2-step head check for the `app` segment,
490/// `validate_fqdn_suffix` owns the 3-step trailing suffix check for the
491/// `cluster` / `location` / `domain` segments; together they cover the
492/// full validation surface both FQDN composers walk BEFORE the terminal
493/// `format!(...)` emission.
494///
495/// A future extension to the suffix check (a per-cluster reserved-name
496/// gate mirroring [`RESERVED_APP_LABELS`], a stricter per-location DNS
497/// label check, a per-domain TLD allowlist gate, a per-fleet
498/// normalization of the cluster segment) lands at THIS ONE substrate
499/// primitive and both [`fmt_fqdn`] + [`fmt_fqdn_stable`] inherit the
500/// upgrade mechanically — no per-composer edit at either callsite, no
501/// drift risk for a third future FQDN-shape composer (a per-region
502/// gateway form, a wildcard-cert-issuer probe form) that plugs into the
503/// same suffix policy.
504///
505/// Theory anchor: THEORY.md §VI.1 (generation over composition — the
506/// 3-line three-step check recurred at two hand-authored composer
507/// preludes past the ★★ PRIME-DIRECTIVE ≥ 2 duplication trigger and
508/// lifts to ONE substrate owner here, matching the discipline
509/// [`validate_app`] already carries on the peer head-check axis).
510/// THEORY.md §II.1 invariant 5 (composition preserves proofs — the
511/// pin block below binds the primitive at fail-before-pass-after
512/// granularity so a regression that reorders the three steps, drops
513/// one, or drifts the typed `segment` slot surfaces at THESE pins
514/// rather than as silent fleet-hostname skew across every downstream
515/// FQDN emit).
516fn validate_fqdn_suffix(cluster: &str, location: &str, domain: &str) -> Result<(), HostnameError> {
517    validate_label("cluster", cluster)?;
518    validate_label("location", location)?;
519    validate_domain("domain", domain)?;
520    Ok(())
521}
522
523fn short_hex_blake3(bytes: &[u8], len: usize) -> String {
524    // Delegate the 2-link `blake3::hash → hex` step to the substrate
525    // primitive [`crate::hash::hex_blake3`] (itself a thin route
526    // through `tatara_lisp::hash::hex_blake3_of_bytes`, the workspace-
527    // wide byte-input owner) so the ephemeral-id prefix stays byte-
528    // identical to every receipt/attestation hex-digest workspace-wide.
529    // Delegate the terminal 1-link `.chars().take(len).collect()`
530    // truncation to the workspace-wide substrate owner
531    // [`tatara_lisp::hash::hex_prefix`] — sibling of the byte-input and
532    // scheme-wrap owners on the identity-projection axis. Pre-lift the
533    // truncation step was hand-authored at both this site + at
534    // `tatara_ui::event::ShortHash::from_blake3_hex` (a 7-char UI
535    // render prefix); post-lift both consumers route the take-prefix
536    // step through ONE substrate owner, so a future spelling change
537    // (a different truncation discipline, a padding rule for short
538    // receivers, a stable-`&str[..len]` byte-slice implementation)
539    // lands at ONE substrate function and reaches both the UI's
540    // 7-char short hash AND this crate's `EPHEMERAL_ID_HASH_LEN`-char
541    // FQDN slot through ONE edit.
542    tatara_lisp::hash::hex_prefix(&crate::hash::hex_blake3(bytes), len)
543}
544
545#[cfg(test)]
546mod tests {
547    use super::*;
548    use serde::Deserialize;
549
550    #[test]
551    fn fmt_fqdn_per_instance() {
552        let f = fmt_fqdn("api", "demo-prod", "pleme-dev", "use1", "quero.lol").unwrap();
553        assert_eq!(f, "api.demo-prod.pleme-dev.use1.quero.lol");
554    }
555
556    #[test]
557    fn fmt_fqdn_stable_form() {
558        let f = fmt_fqdn_stable("api", "pleme-dev", "use1", "quero.lol").unwrap();
559        assert_eq!(f, "api.pleme-dev.use1.quero.lol");
560    }
561
562    #[test]
563    fn fmt_fqdn_with_multilevel_domain() {
564        let f = fmt_fqdn("api", "env-a", "rio", "us", "internal.example.com").unwrap();
565        assert_eq!(f, "api.env-a.rio.us.internal.example.com");
566    }
567
568    #[test]
569    fn reserved_app_rejected() {
570        let r = fmt_fqdn("auth", "x", "y", "z", "example.com");
571        assert!(matches!(r, Err(HostnameError::ReservedApp(_))));
572        let r = fmt_fqdn_stable("cracha", "y", "z", "example.com");
573        assert!(matches!(r, Err(HostnameError::ReservedApp(_))));
574    }
575
576    #[test]
577    fn empty_label_rejected() {
578        let r = fmt_fqdn("", "x", "y", "z", "example.com");
579        assert!(matches!(
580            r,
581            Err(HostnameError::InvalidLabel { segment: "app", .. })
582        ));
583    }
584
585    #[test]
586    fn too_long_label_rejected() {
587        let long = "a".repeat(64);
588        let r = fmt_fqdn(&long, "x", "y", "z", "example.com");
589        assert!(matches!(r, Err(HostnameError::InvalidLabel { .. })));
590    }
591
592    #[test]
593    fn uppercase_label_rejected() {
594        let r = fmt_fqdn("API", "x", "y", "z", "example.com");
595        assert!(matches!(r, Err(HostnameError::InvalidLabel { .. })));
596    }
597
598    #[test]
599    fn leading_hyphen_label_rejected() {
600        let r = fmt_fqdn("api", "-bad", "y", "z", "example.com");
601        assert!(matches!(r, Err(HostnameError::InvalidLabel { .. })));
602    }
603
604    #[test]
605    fn underscore_label_rejected() {
606        let r = fmt_fqdn("api", "x_y", "z", "w", "example.com");
607        assert!(matches!(r, Err(HostnameError::InvalidLabel { .. })));
608    }
609
610    #[test]
611    fn empty_domain_rejected() {
612        let r = fmt_fqdn("api", "x", "y", "z", "");
613        assert!(matches!(r, Err(HostnameError::InvalidLabel { .. })));
614    }
615
616    // ─── Content-hash derivation ─────────────────────────────────
617
618    #[derive(Serialize, Deserialize)]
619    struct TestSpec {
620        a: u32,
621        b: String,
622    }
623
624    #[test]
625    fn ephemeral_id_is_8_hex_chars() {
626        let spec = TestSpec {
627            a: 1,
628            b: "x".into(),
629        };
630        let id = ephemeral_id_from_spec(&spec).unwrap();
631        assert_eq!(id.len(), EPHEMERAL_ID_HASH_LEN);
632        assert!(id.chars().all(|c| c.is_ascii_hexdigit()));
633    }
634
635    #[test]
636    fn ephemeral_id_is_deterministic() {
637        let s1 = TestSpec {
638            a: 1,
639            b: "x".into(),
640        };
641        let s2 = TestSpec {
642            a: 1,
643            b: "x".into(),
644        };
645        assert_eq!(
646            ephemeral_id_from_spec(&s1).unwrap(),
647            ephemeral_id_from_spec(&s2).unwrap()
648        );
649    }
650
651    #[test]
652    fn ephemeral_id_changes_with_spec() {
653        let s1 = TestSpec {
654            a: 1,
655            b: "x".into(),
656        };
657        let s2 = TestSpec {
658            a: 2,
659            b: "x".into(),
660        };
661        let s3 = TestSpec {
662            a: 1,
663            b: "y".into(),
664        };
665        let id1 = ephemeral_id_from_spec(&s1).unwrap();
666        let id2 = ephemeral_id_from_spec(&s2).unwrap();
667        let id3 = ephemeral_id_from_spec(&s3).unwrap();
668        assert_ne!(id1, id2);
669        assert_ne!(id1, id3);
670        assert_ne!(id2, id3);
671    }
672
673    #[test]
674    fn ephemeral_id_lowercase_valid_dns_label() {
675        // BLAKE3 hex is lowercase by design; the validator must
676        // accept the output as a valid DNS label.
677        let spec = TestSpec {
678            a: 42,
679            b: "anything".into(),
680        };
681        let id = ephemeral_id_from_spec(&spec).unwrap();
682        validate_label("ephemeral_id", &id).unwrap();
683    }
684
685    // ─── resolve_ephemeral_id ────────────────────────────────────
686
687    #[test]
688    fn resolve_named_slot_wins() {
689        let h = RoutingHostname::instanced("api", "demo-prod");
690        assert_eq!(resolve_ephemeral_id(&h, "fallback"), "demo-prod");
691    }
692
693    #[test]
694    fn resolve_empty_named_falls_back() {
695        let h = RoutingHostname {
696            app: "api".into(),
697            instance: Some(String::new()),
698            cluster: None,
699        };
700        assert_eq!(resolve_ephemeral_id(&h, "abc123de"), "abc123de");
701    }
702
703    #[test]
704    fn resolve_unset_named_falls_back() {
705        let h = RoutingHostname::content_hashed("api");
706        assert_eq!(resolve_ephemeral_id(&h, "abc123de"), "abc123de");
707    }
708
709    // ─── End-to-end ──────────────────────────────────────────────
710
711    // ─── HostnameResultExt::hostname_ctx substrate pins ──────────
712    //
713    // Fail-before-pass-after granularity: the `HostnameResultExt::
714    // hostname_ctx` trait method did not exist before this commit,
715    // so each test below fails to compile pre-lift. Post-lift they
716    // collectively pin the display-prefix wrap-shape at ONE substrate
717    // owner — a regression that drifts the separator, swaps the two
718    // slots, wraps the `HostnameError` with a chain-form `source`, or
719    // promotes the pass-through arm to a synthesis (an empty `Ok(())`,
720    // a mutated context slug) surfaces HERE rather than as silent
721    // operator-facing skew across the three pre-lift consumers whose
722    // log output already encoded the flat `"<ctx>: <HostnameError
723    // display>"` shape.
724
725    fn sample_err() -> HostnameError {
726        HostnameError::InvalidLabel {
727            segment: "app",
728            label: "BAD".into(),
729            reason: "must contain only [a-z0-9-]",
730        }
731    }
732
733    #[test]
734    fn hostname_ctx_static_str_context_matches_pre_lift_format_bytewise() {
735        // Byte-shape parity pin: the wrap output of `hostname_ctx
736        // ("<slug>")` MUST be `Display`-identical to the pre-lift
737        // hand-authored `.map_err(|e| anyhow!("<slug>: {e}"))` chain.
738        // A regression that inserted a separator character (`"<slug>::
739        // <hostname>"`), dropped the space after the colon, or swapped
740        // the two slots (`"<hostname>: <slug>"`) surfaces HERE rather
741        // than as silent drift at every downstream log-output consumer.
742        let raw: Result<(), HostnameError> = Err(sample_err());
743        let via_trait = raw.hostname_ctx("fmt_fqdn (per-instance)").unwrap_err();
744        let pre_lift = anyhow::anyhow!("fmt_fqdn (per-instance): {}", sample_err());
745        assert_eq!(
746            format!("{via_trait}"),
747            format!("{pre_lift}"),
748            "hostname_ctx wrap must be Display-identical to pre-lift anyhow! chain"
749        );
750    }
751
752    #[test]
753    fn hostname_ctx_ok_arm_is_a_pure_passthrough() {
754        // Ok-arm invariant: `hostname_ctx` on `Ok(t)` MUST return
755        // `Ok(t)` verbatim — no side-effect on the payload, no
756        // synthesis of a context-tagged error, no allocation. Peer to
757        // the Err-arm byte-shape pin; a regression that promoted the
758        // Ok arm to ALWAYS produce a synthesis Error would silently
759        // break every successful hostname-format call in the pre-lift
760        // consumer set.
761        let raw: Result<&'static str, HostnameError> = Ok("api.demo-prod.pleme-dev.use1.quero.lol");
762        assert_eq!(
763            raw.hostname_ctx("noop").unwrap(),
764            "api.demo-prod.pleme-dev.use1.quero.lol"
765        );
766    }
767
768    #[test]
769    fn hostname_ctx_threads_the_underlying_hostname_error_display_verbatim() {
770        // Display-tail invariant: the wrapped `anyhow::Error`'s
771        // `Display` output MUST contain the `HostnameError`'s own
772        // `Display` output verbatim as the tail past `"<ctx>: "`. A
773        // regression that inserted a normalization (uppercase, JSON
774        // encoding, truncation) between the composed `{e}` slot and
775        // the underlying thiserror-derived Display impl would surface
776        // HERE rather than as silent operator-facing skew across the
777        // three consumers whose grep patterns already encoded the
778        // canonical `HostnameError` variant wordings ("invalid DNS
779        // label ...", "app label ... is reserved").
780        let raw: Result<(), HostnameError> = Err(HostnameError::ReservedApp("auth".into()));
781        let wrapped = raw.hostname_ctx("fmt_fqdn_stable").unwrap_err();
782        let expected_tail = format!("{}", HostnameError::ReservedApp("auth".into()));
783        let expected = format!("fmt_fqdn_stable: {expected_tail}");
784        assert_eq!(format!("{wrapped}"), expected);
785        // Also assert the tail appears verbatim as a suffix — a change
786        // in the thiserror-derived Display for ReservedApp would fail
787        // both this assertion and the RECEIPT_VERSION-in-tail invariant
788        // its docstring pins.
789        assert!(
790            format!("{wrapped}").ends_with(&expected_tail),
791            "wrap must end with the HostnameError Display verbatim"
792        );
793    }
794
795    #[test]
796    fn hostname_ctx_composes_over_ephemeral_id_from_spec_call_shape() {
797        // End-to-end composition pin: the substrate trait method
798        // composes cleanly over the `ephemeral_id_from_spec` return
799        // shape at a real callsite (the `render_routing` R9 seed).
800        // A regression that specialized the trait bound to only one
801        // hostname primitive's Result shape would surface HERE.
802        #[derive(Serialize)]
803        struct NoSuchThingAsAnUnserializableStruct {
804            a: u32,
805        }
806        let v = NoSuchThingAsAnUnserializableStruct { a: 1 };
807        let composed: anyhow::Result<String> =
808            ephemeral_id_from_spec(&v).hostname_ctx("ephemeral_id_from_spec");
809        assert!(composed.is_ok());
810        assert_eq!(composed.unwrap().len(), EPHEMERAL_ID_HASH_LEN);
811    }
812
813    // ─── validate_app substrate pins ─────────────────────────────
814    //
815    // Fail-before-pass-after granularity: the `validate_app` helper
816    // did not exist pre-lift — both [`fmt_fqdn`] and [`fmt_fqdn_stable`]
817    // hand-authored the two-step (RFC 1123 label + reserved-name reject)
818    // check inline. Post-lift the two composers thread the same
819    // primitive, so the pins below pin the primitive's SHAPE + STEP
820    // ORDER + typed-variant surface at the substrate — a regression
821    // that (a) reorders the two steps, (b) drops the reserved-name
822    // gate silently, or (c) promotes the `HostnameError::ReservedApp`
823    // arm to a generic `InvalidLabel` surfaces HERE rather than as
824    // silent skew at every downstream FQDN emit.
825
826    #[test]
827    fn validate_app_accepts_valid_lowercase_alphanumeric_label() {
828        // Happy-path pin: a valid `app` label passes the two-step
829        // check with `Ok(())`. A regression that inverted the return
830        // arm (rejected everything, matched no reserved) surfaces
831        // HERE rather than as every FQDN emit refusing every input.
832        validate_app("api").unwrap();
833        validate_app("gateway").unwrap();
834        validate_app("demo-app").unwrap();
835        validate_app("a").unwrap();
836    }
837
838    #[test]
839    fn validate_app_rejects_empty_label_with_invalid_label_variant() {
840        // Step-1 delegation pin: an empty `app` MUST surface as
841        // `HostnameError::InvalidLabel { segment: "app", .. }` from
842        // the underlying `validate_label("app", app)?` call — NOT as
843        // `ReservedApp` (which would silently reclassify the shape
844        // defect as a policy rejection).
845        assert!(matches!(
846            validate_app(""),
847            Err(HostnameError::InvalidLabel { segment: "app", .. })
848        ));
849    }
850
851    #[test]
852    fn validate_app_rejects_uppercase_label_with_invalid_label_variant() {
853        // Step-1 delegation pin: casing-invalid labels reach through
854        // to `validate_label`'s [a-z0-9-] check. A regression that
855        // short-circuited the reserved-check on a case-insensitive
856        // match ("AUTH" reads as reserved without going through the
857        // RFC 1123 gate first) would surface HERE.
858        assert!(matches!(
859            validate_app("API"),
860            Err(HostnameError::InvalidLabel { segment: "app", .. })
861        ));
862    }
863
864    #[test]
865    fn validate_app_rejects_too_long_label_with_invalid_label_variant() {
866        // Step-1 delegation pin: 64-char labels violate the RFC 1123
867        // upper bound and surface at the `validate_label` gate.
868        let long = "a".repeat(64);
869        assert!(matches!(
870            validate_app(&long),
871            Err(HostnameError::InvalidLabel { segment: "app", .. })
872        ));
873    }
874
875    #[test]
876    fn validate_app_rejects_reserved_auth_label_with_reserved_app_variant() {
877        // Step-2 pin: the currently-reserved `"auth"` slot surfaces
878        // as `HostnameError::ReservedApp("auth")` — the typed
879        // control-plane rejection callers pattern-match on. A
880        // regression that dropped this variant would silently
881        // accept the reservation and let a tenant deploy under the
882        // saguão namespace.
883        assert!(matches!(
884            validate_app("auth"),
885            Err(HostnameError::ReservedApp(ref s)) if s == "auth"
886        ));
887    }
888
889    #[test]
890    fn validate_app_rejects_reserved_cracha_label_with_reserved_app_variant() {
891        // Sibling pin to the `"auth"` reservation — pins the second
892        // currently-reserved label. A regression that dropped one
893        // reservation but not the other would surface HERE.
894        assert!(matches!(
895            validate_app("cracha"),
896            Err(HostnameError::ReservedApp(ref s)) if s == "cracha"
897        ));
898    }
899
900    #[test]
901    fn validate_app_step_order_puts_rfc_1123_check_before_reserved_check() {
902        // Load-bearing order pin: `validate_label` runs FIRST so a
903        // reserved label whose spelling ALSO violates RFC 1123
904        // (uppercase, hyphen at end, etc.) surfaces as
905        // `InvalidLabel` — the underlying SHAPE defect — not as
906        // `ReservedApp` (the higher-level POLICY gate). Callers who
907        // pattern-match on the two variants branch DIFFERENTLY on
908        // shape defects vs policy rejections, so a swap of the two
909        // steps would silently re-route every uppercase-reserved
910        // input into the wrong error arm.
911        assert!(matches!(
912            validate_app("AUTH"),
913            Err(HostnameError::InvalidLabel { segment: "app", .. })
914        ));
915        assert!(matches!(
916            validate_app("Cracha"),
917            Err(HostnameError::InvalidLabel { segment: "app", .. })
918        ));
919    }
920
921    #[test]
922    fn validate_app_matches_pre_lift_two_step_chain_bytewise_across_every_variant_shape() {
923        // Byte-shape parity pin: the substrate primitive's return
924        // MUST equal the pre-lift 4-line hand-authored chain for
925        // every representative input shape. A regression that
926        // drifted the primitive's semantics away from the pre-lift
927        // composer preludes surfaces HERE rather than as silent
928        // skew at either `fmt_fqdn` / `fmt_fqdn_stable` consumer.
929        fn pre_lift(app: &str) -> Result<(), HostnameError> {
930            validate_label("app", app)?;
931            if RESERVED_APP_LABELS.contains(&app) {
932                return Err(HostnameError::ReservedApp(app.to_string()));
933            }
934            Ok(())
935        }
936        for input in [
937            // Happy path.
938            "api",
939            "gateway",
940            "demo-app",
941            "a",
942            // Step-1 rejections.
943            "",
944            "API",
945            "-bad",
946            "bad-",
947            "with_underscore",
948            // Step-2 rejections.
949            "auth",
950            "cracha",
951            // Step-1 wins over step-2 (uppercase reserved).
952            "AUTH",
953            "Cracha",
954        ] {
955            let via_primitive = validate_app(input);
956            let via_pre_lift = pre_lift(input);
957            match (via_primitive, via_pre_lift) {
958                (Ok(()), Ok(())) => {}
959                (Err(a), Err(b)) => assert_eq!(a, b, "variant mismatch for {input:?}"),
960                (a, b) => panic!("arm mismatch for {input:?}: primitive={a:?} pre_lift={b:?}"),
961            }
962        }
963    }
964
965    #[test]
966    fn validate_app_covers_every_currently_reserved_label_at_the_primitive() {
967        // Coherence sweep: iterate the RESERVED_APP_LABELS set and
968        // verify each element rejects at the substrate. A future
969        // addition to the reserved set that forgets to update the
970        // primitive would surface HERE rather than as silent
971        // acceptance at every FQDN emit.
972        for reserved in RESERVED_APP_LABELS {
973            assert!(
974                matches!(validate_app(reserved), Err(HostnameError::ReservedApp(ref s)) if s == reserved),
975                "RESERVED_APP_LABELS entry {reserved:?} must surface as ReservedApp at the substrate"
976            );
977        }
978    }
979
980    // ─── validate_fqdn_suffix substrate pins ─────────────────────
981    //
982    // Fail-before-pass-after granularity: the `validate_fqdn_suffix`
983    // helper did not exist pre-lift — both [`fmt_fqdn`] and
984    // [`fmt_fqdn_stable`] hand-authored the three-step (`validate_label
985    // ("cluster") → validate_label("location") → validate_domain
986    // ("domain")`) suffix check inline. Post-lift the two composers
987    // thread the same primitive, so the pins below pin the primitive's
988    // SHAPE + STEP ORDER + typed-`segment` slot at the substrate — a
989    // regression that (a) reorders the three steps (silently re-
990    // classifying every multi-slot rejection into the wrong `segment`
991    // arm), (b) drops one of the three checks, or (c) swaps the
992    // `validate_domain` primitive for a `validate_label` on the domain
993    // slot (silently accepting a single-label `example` in place of
994    // the multi-label `example.com` shape) surfaces HERE rather than
995    // as silent skew at every downstream FQDN emit.
996
997    #[test]
998    fn validate_fqdn_suffix_accepts_valid_three_segment_suffix() {
999        // Happy-path pin: a valid `cluster.location.domain` triple
1000        // passes the three-step check with `Ok(())`. A regression that
1001        // inverted the return arm (rejected everything) surfaces HERE
1002        // rather than as every FQDN emit refusing every input.
1003        validate_fqdn_suffix("pleme-dev", "use1", "quero.lol").unwrap();
1004        validate_fqdn_suffix("prod", "eu-west-1", "example.com").unwrap();
1005        validate_fqdn_suffix("a", "b", "c.d.e").unwrap();
1006    }
1007
1008    #[test]
1009    fn validate_fqdn_suffix_rejects_empty_cluster_with_cluster_segment_slot() {
1010        // Step-1 delegation pin: an empty `cluster` MUST surface as
1011        // `HostnameError::InvalidLabel { segment: "cluster", .. }`
1012        // from the underlying `validate_label("cluster", cluster)?`
1013        // call — NOT as `segment: "location"` or `segment: "domain"`
1014        // (which would silently re-classify the shape defect into a
1015        // trailing-slot rejection and route callers who pattern-match
1016        // on the `segment` slot to render targeted operator messages
1017        // to the wrong branch).
1018        assert!(matches!(
1019            validate_fqdn_suffix("", "use1", "quero.lol"),
1020            Err(HostnameError::InvalidLabel {
1021                segment: "cluster",
1022                ..
1023            })
1024        ));
1025    }
1026
1027    #[test]
1028    fn validate_fqdn_suffix_rejects_empty_location_with_location_segment_slot() {
1029        // Step-2 delegation pin — sibling to the cluster-slot pin. A
1030        // valid cluster + empty location MUST surface as `segment:
1031        // "location"` (step 2 fired), NOT as `segment: "domain"`
1032        // (which would mean step 3 short-circuited past step 2).
1033        assert!(matches!(
1034            validate_fqdn_suffix("pleme-dev", "", "quero.lol"),
1035            Err(HostnameError::InvalidLabel {
1036                segment: "location",
1037                ..
1038            })
1039        ));
1040    }
1041
1042    #[test]
1043    fn validate_fqdn_suffix_rejects_empty_domain_with_domain_segment_slot() {
1044        // Step-3 delegation pin — the terminal step. A valid cluster
1045        // + valid location + empty domain MUST surface as `segment:
1046        // "domain"` (from `validate_domain`'s empty-domain gate). A
1047        // regression that swapped `validate_domain` for
1048        // `validate_label` on the domain slot would silently accept
1049        // an empty string with a DIFFERENT `reason` slot or reject a
1050        // multi-label domain (`example.com`) that `validate_label`
1051        // alone forbids (dots).
1052        assert!(matches!(
1053            validate_fqdn_suffix("pleme-dev", "use1", ""),
1054            Err(HostnameError::InvalidLabel {
1055                segment: "domain",
1056                ..
1057            })
1058        ));
1059    }
1060
1061    #[test]
1062    fn validate_fqdn_suffix_rejects_multilabel_cluster_with_invalid_label_variant() {
1063        // Cluster-shape pin: `cluster` reaches through `validate_label`
1064        // (single-label check), NOT `validate_domain` (multi-label
1065        // check). A dot-containing cluster MUST reject at the RFC 1123
1066        // gate. A regression that widened the cluster gate to
1067        // `validate_domain` would silently accept a multi-label
1068        // cluster like `pleme.dev` (folding two segments into one
1069        // slot at emit time and drifting every downstream Ingress /
1070        // DNSEndpoint dispatcher).
1071        assert!(matches!(
1072            validate_fqdn_suffix("pleme.dev", "use1", "quero.lol"),
1073            Err(HostnameError::InvalidLabel {
1074                segment: "cluster",
1075                ..
1076            })
1077        ));
1078    }
1079
1080    #[test]
1081    fn validate_fqdn_suffix_accepts_multilabel_domain_via_validate_domain_split() {
1082        // Domain-shape pin: `domain` reaches through `validate_domain`
1083        // (multi-label check via `domain.split('.')`), NOT
1084        // `validate_label` (single-label check that would reject any
1085        // dot). A regression that narrowed the domain gate to
1086        // `validate_label` would surface HERE — every real-world
1087        // domain (`quero.lol`, `example.com`, `internal.example.com`)
1088        // contains at least one dot and would fail at the RFC 1123
1089        // single-label check.
1090        validate_fqdn_suffix("pleme-dev", "use1", "internal.example.com").unwrap();
1091        validate_fqdn_suffix("pleme-dev", "use1", "a.b.c.d.e.f").unwrap();
1092    }
1093
1094    #[test]
1095    fn validate_fqdn_suffix_step_order_puts_cluster_before_location_before_domain() {
1096        // Load-bearing order pin: the three steps fire in the SAME
1097        // order the pre-lift composer preludes hand-authored (cluster
1098        // → location → domain), so an input that violates MULTIPLE
1099        // slots surfaces at the FIRST violated slot on the ordered
1100        // walk. Callers who pattern-match on the `segment` slot to
1101        // render targeted operator messages branch differently, so a
1102        // swap of the three steps would silently re-classify every
1103        // multi-slot-invalid input.
1104        //
1105        // All three slots invalid → surfaces at `cluster` (step 1).
1106        assert!(matches!(
1107            validate_fqdn_suffix("", "", ""),
1108            Err(HostnameError::InvalidLabel {
1109                segment: "cluster",
1110                ..
1111            })
1112        ));
1113        // Valid cluster + invalid location + invalid domain → surfaces
1114        // at `location` (step 2), NOT `domain` (step 3).
1115        assert!(matches!(
1116            validate_fqdn_suffix("pleme-dev", "", ""),
1117            Err(HostnameError::InvalidLabel {
1118                segment: "location",
1119                ..
1120            })
1121        ));
1122    }
1123
1124    #[test]
1125    fn validate_fqdn_suffix_matches_pre_lift_three_step_chain_bytewise_across_every_variant_shape()
1126    {
1127        // Byte-shape parity pin: the substrate primitive's return
1128        // MUST equal the pre-lift 3-line hand-authored chain for
1129        // every representative input shape. A regression that
1130        // drifted the primitive's semantics away from the pre-lift
1131        // composer preludes surfaces HERE rather than as silent skew
1132        // at either `fmt_fqdn` / `fmt_fqdn_stable` consumer.
1133        fn pre_lift(cluster: &str, location: &str, domain: &str) -> Result<(), HostnameError> {
1134            validate_label("cluster", cluster)?;
1135            validate_label("location", location)?;
1136            validate_domain("domain", domain)?;
1137            Ok(())
1138        }
1139        for (cluster, location, domain) in [
1140            // Happy path — every corner both composers walk in
1141            // production.
1142            ("pleme-dev", "use1", "quero.lol"),
1143            ("prod", "eu-west-1", "example.com"),
1144            ("a", "b", "c.d.e"),
1145            ("cluster-1", "loc-2", "internal.example.com"),
1146            // Step-1 rejections — cluster slot fails.
1147            ("", "use1", "quero.lol"),
1148            ("BAD", "use1", "quero.lol"),
1149            ("-lead", "use1", "quero.lol"),
1150            ("with_underscore", "use1", "quero.lol"),
1151            ("pleme.dev", "use1", "quero.lol"),
1152            // Step-2 rejections — cluster ok, location fails.
1153            ("pleme-dev", "", "quero.lol"),
1154            ("pleme-dev", "USE1", "quero.lol"),
1155            ("pleme-dev", "loc_1", "quero.lol"),
1156            // Step-3 rejections — cluster + location ok, domain fails.
1157            ("pleme-dev", "use1", ""),
1158            ("pleme-dev", "use1", "-bad.com"),
1159            ("pleme-dev", "use1", "BAD.com"),
1160            // Multi-slot rejection — step 1 wins over 2 and 3.
1161            ("", "", ""),
1162            ("BAD", "USE1", ""),
1163        ] {
1164            let via_primitive = validate_fqdn_suffix(cluster, location, domain);
1165            let via_pre_lift = pre_lift(cluster, location, domain);
1166            match (via_primitive, via_pre_lift) {
1167                (Ok(()), Ok(())) => {}
1168                (Err(a), Err(b)) => assert_eq!(
1169                    a, b,
1170                    "variant mismatch for ({cluster:?}, {location:?}, {domain:?})"
1171                ),
1172                (a, b) => panic!(
1173                    "arm mismatch for ({cluster:?}, {location:?}, {domain:?}): primitive={a:?} pre_lift={b:?}"
1174                ),
1175            }
1176        }
1177    }
1178
1179    #[test]
1180    fn fmt_fqdn_routes_suffix_slots_through_validate_fqdn_suffix_primitive() {
1181        // Delegation pin: the per-instance composer routes its
1182        // trailing suffix check through `validate_fqdn_suffix`, NOT
1183        // through a re-open-coded restatement of the three-step
1184        // chain. A regression that re-inlined the pre-lift check at
1185        // the composer prelude would reintroduce the duplication the
1186        // lift removed; this pin catches it by asserting the composer
1187        // surfaces the SAME typed `segment` slot the primitive would
1188        // for a representative rejection in each of the three suffix
1189        // slots (cluster, location, domain).
1190        assert!(matches!(
1191            fmt_fqdn("api", "x", "BAD", "use1", "quero.lol"),
1192            Err(HostnameError::InvalidLabel {
1193                segment: "cluster",
1194                ..
1195            })
1196        ));
1197        assert!(matches!(
1198            fmt_fqdn("api", "x", "pleme-dev", "", "quero.lol"),
1199            Err(HostnameError::InvalidLabel {
1200                segment: "location",
1201                ..
1202            })
1203        ));
1204        assert!(matches!(
1205            fmt_fqdn("api", "x", "pleme-dev", "use1", ""),
1206            Err(HostnameError::InvalidLabel {
1207                segment: "domain",
1208                ..
1209            })
1210        ));
1211    }
1212
1213    #[test]
1214    fn fmt_fqdn_stable_routes_suffix_slots_through_validate_fqdn_suffix_primitive() {
1215        // Sibling delegation pin — same shape as the per-instance pin
1216        // above but for the stable-claim composer. Both composers now
1217        // share the primitive; a regression that re-inlined the chain
1218        // at either site surfaces at ONE of the two pins rather than
1219        // at every downstream FQDN emit.
1220        assert!(matches!(
1221            fmt_fqdn_stable("api", "BAD", "use1", "quero.lol"),
1222            Err(HostnameError::InvalidLabel {
1223                segment: "cluster",
1224                ..
1225            })
1226        ));
1227        assert!(matches!(
1228            fmt_fqdn_stable("api", "pleme-dev", "", "quero.lol"),
1229            Err(HostnameError::InvalidLabel {
1230                segment: "location",
1231                ..
1232            })
1233        ));
1234        assert!(matches!(
1235            fmt_fqdn_stable("api", "pleme-dev", "use1", ""),
1236            Err(HostnameError::InvalidLabel {
1237                segment: "domain",
1238                ..
1239            })
1240        ));
1241    }
1242
1243    #[test]
1244    fn fmt_fqdn_and_fmt_fqdn_stable_agree_on_suffix_rejection_bytewise() {
1245        // Cross-composer coherence pin: post-lift both composers route
1246        // their suffix check through the ONE substrate primitive, so
1247        // the SAME suffix-slot violation surfaces byte-identically at
1248        // BOTH composers (differing only in the `ephemeral_id` arg
1249        // presence). A regression that re-inlined the chain at one
1250        // composer but not the other would silently drift the two
1251        // consumers' typed-`segment` slot; this pin binds them to the
1252        // ONE substrate primitive so any such drift surfaces HERE.
1253        for (cluster, location, domain, expected_segment) in [
1254            ("BAD", "use1", "quero.lol", "cluster"),
1255            ("pleme-dev", "", "quero.lol", "location"),
1256            ("pleme-dev", "use1", "", "domain"),
1257            ("pleme.dev", "use1", "quero.lol", "cluster"),
1258        ] {
1259            let via_per_instance = fmt_fqdn("api", "x", cluster, location, domain);
1260            let via_stable = fmt_fqdn_stable("api", cluster, location, domain);
1261            assert!(
1262                matches!(
1263                    &via_per_instance,
1264                    Err(HostnameError::InvalidLabel { segment, .. }) if *segment == expected_segment
1265                ),
1266                "fmt_fqdn must surface segment={expected_segment:?} for ({cluster:?}, {location:?}, {domain:?}); got {via_per_instance:?}"
1267            );
1268            assert!(
1269                matches!(
1270                    &via_stable,
1271                    Err(HostnameError::InvalidLabel { segment, .. }) if *segment == expected_segment
1272                ),
1273                "fmt_fqdn_stable must surface segment={expected_segment:?} for ({cluster:?}, {location:?}, {domain:?}); got {via_stable:?}"
1274            );
1275            // And the two composers' error variants agree bytewise on
1276            // the suffix rejection — they should, since both route
1277            // through the SAME primitive.
1278            match (via_per_instance, via_stable) {
1279                (Err(a), Err(b)) => assert_eq!(
1280                    a, b,
1281                    "fmt_fqdn and fmt_fqdn_stable must agree on suffix rejection for ({cluster:?}, {location:?}, {domain:?})"
1282                ),
1283                pair => panic!(
1284                    "expected both composers to reject ({cluster:?}, {location:?}, {domain:?}) with the SAME variant; got {pair:?}"
1285                ),
1286            }
1287        }
1288    }
1289
1290    #[test]
1291    fn fmt_fqdn_routes_app_slot_through_validate_app_primitive() {
1292        // Delegation pin: the per-instance composer routes its `app`
1293        // slot check through `validate_app`, NOT through a re-open-
1294        // coded restatement of the two-step chain. A regression that
1295        // inlined the pre-lift check at the composer prelude would
1296        // reintroduce the duplication the lift removed; this pin
1297        // catches it by asserting the composer surfaces the SAME
1298        // typed error the primitive would for a representative
1299        // input in each of the two rejection arms.
1300        assert!(matches!(
1301            fmt_fqdn("AUTH", "x", "y", "z", "example.com"),
1302            Err(HostnameError::InvalidLabel { segment: "app", .. })
1303        ));
1304        assert!(matches!(
1305            fmt_fqdn("auth", "x", "y", "z", "example.com"),
1306            Err(HostnameError::ReservedApp(ref s)) if s == "auth"
1307        ));
1308    }
1309
1310    #[test]
1311    fn fmt_fqdn_stable_routes_app_slot_through_validate_app_primitive() {
1312        // Sibling delegation pin — same shape as the per-instance
1313        // pin above but for the stable-claim composer. Both
1314        // composers now share the primitive; a regression that
1315        // re-inlined the chain at either site surfaces at ONE of
1316        // the two pins rather than at every downstream FQDN emit.
1317        assert!(matches!(
1318            fmt_fqdn_stable("Cracha", "y", "z", "example.com"),
1319            Err(HostnameError::InvalidLabel { segment: "app", .. })
1320        ));
1321        assert!(matches!(
1322            fmt_fqdn_stable("cracha", "y", "z", "example.com"),
1323            Err(HostnameError::ReservedApp(ref s)) if s == "cracha"
1324        ));
1325    }
1326
1327    #[test]
1328    fn end_to_end_named_and_unnamed_for_same_process() {
1329        let spec = TestSpec {
1330            a: 1,
1331            b: "x".into(),
1332        };
1333        let hash = ephemeral_id_from_spec(&spec).unwrap();
1334
1335        let h_named = RoutingHostname::instanced("api", "demo-prod");
1336        let h_anon = RoutingHostname::content_hashed("gateway");
1337
1338        let id_named = resolve_ephemeral_id(&h_named, &hash);
1339        let id_anon = resolve_ephemeral_id(&h_anon, &hash);
1340
1341        let fqdn_named =
1342            fmt_fqdn(&h_named.app, id_named, "pleme-dev", "use1", "quero.lol").unwrap();
1343        let fqdn_anon = fmt_fqdn(&h_anon.app, id_anon, "pleme-dev", "use1", "quero.lol").unwrap();
1344
1345        assert_eq!(fqdn_named, "api.demo-prod.pleme-dev.use1.quero.lol");
1346        assert!(fqdn_anon.starts_with("gateway."));
1347        assert!(fqdn_anon.ends_with(".pleme-dev.use1.quero.lol"));
1348        // 5 named segments (app + eph_id + cluster + location + domain),
1349        // but `domain` itself splits as `quero.lol` ⇒ 6 dot-delimited
1350        // pieces. The shape, not the count, is the invariant.
1351        assert_eq!(fqdn_anon.matches('.').count(), 5);
1352    }
1353
1354    // ─── HostnameError::invalid_label substrate pins ─────────────
1355    //
1356    // Fail-before-pass-after granularity: the `HostnameError::
1357    // invalid_label` constructor did not exist pre-lift — the four
1358    // `validate_*` rejection sites hand-authored the three-slot
1359    // `HostnameError::InvalidLabel { segment, label: <str>.to_string
1360    // (), reason: <static> }` struct literal inline. Post-lift the
1361    // four rejection sites thread the same constructor, so the pins
1362    // below pin the constructor's SHAPE + typed-variant surface +
1363    // slot-projection discipline at the substrate — a regression that
1364    // (a) drifts the `label.into()` projection at the substrate (e.g.
1365    // narrows the `impl Into<String>` bound to `&str`, ruling out a
1366    // future consumer stamping a dynamically-composed label), (b)
1367    // promotes the constructor to a different `HostnameError` variant
1368    // (a `ReservedApp` misfire) silently, or (c) swaps two of the
1369    // three slots at the substrate (e.g. binds `reason` in the
1370    // `segment` slot) surfaces HERE rather than as silent skew across
1371    // every downstream FQDN emit whose rejection pattern-matches on
1372    // the typed variant.
1373
1374    #[test]
1375    fn invalid_label_constructor_produces_invalid_label_variant_with_all_three_slots_bound() {
1376        // Byte-shape parity pin: the constructor's return MUST equal
1377        // the pre-lift hand-authored `HostnameError::InvalidLabel {
1378        // segment, label: label.to_string(), reason }` struct literal
1379        // for every slot. A regression that swapped two slots (e.g.
1380        // bound the reason-string into the segment slot) would
1381        // surface HERE rather than as silent operator-facing skew
1382        // across the four `validate_*` rejection sites whose log
1383        // output already encoded the flat "invalid DNS label
1384        // <label:?> for segment <segment>: <reason>" shape.
1385        let via_constructor =
1386            HostnameError::invalid_label("app", "BAD", "must contain only [a-z0-9-]");
1387        let via_pre_lift = HostnameError::InvalidLabel {
1388            segment: "app",
1389            label: "BAD".to_string(),
1390            reason: "must contain only [a-z0-9-]",
1391        };
1392        assert_eq!(via_constructor, via_pre_lift);
1393    }
1394
1395    #[test]
1396    fn invalid_label_constructor_accepts_borrowed_str_label_via_into_string() {
1397        // Borrowed-slot invariant: the `label` slot must accept the
1398        // borrowed `&str` shape (via `String::from`), matching the
1399        // four pre-lift rejection sites whose `label` parameter is a
1400        // borrowed `&str`. A regression that narrowed the bound to
1401        // owned `String` only would reject the four production
1402        // callsites at rustc time; a regression that narrowed it to
1403        // `&'static str` would reject dynamically-composed labels.
1404        let borrowed: &str = "dynamic-label";
1405        let err = HostnameError::invalid_label("app", borrowed, "must be 1–63 characters");
1406        match err {
1407            HostnameError::InvalidLabel {
1408                segment,
1409                label,
1410                reason,
1411            } => {
1412                assert_eq!(segment, "app");
1413                assert_eq!(label, "dynamic-label");
1414                assert_eq!(reason, "must be 1–63 characters");
1415            }
1416            other => panic!("expected InvalidLabel, got {other:?}"),
1417        }
1418    }
1419
1420    #[test]
1421    fn invalid_label_constructor_accepts_owned_string_label_via_into_string() {
1422        // Owned-slot peer of the borrowed-slot pin above — the
1423        // `impl Into<String>` bound must admit an owned [`String`]
1424        // (identity `Into` impl) verbatim. A future consumer that
1425        // composes the label dynamically (via `format!`, from another
1426        // typed source) reaches the SAME constructor without a
1427        // per-callsite borrow detour. A regression that narrowed
1428        // either arm silently would surface HERE.
1429        let owned: String = "owned-label".to_string();
1430        let err = HostnameError::invalid_label("cluster", owned, "must not be empty");
1431        match err {
1432            HostnameError::InvalidLabel {
1433                segment,
1434                label,
1435                reason,
1436            } => {
1437                assert_eq!(segment, "cluster");
1438                assert_eq!(label, "owned-label");
1439                assert_eq!(reason, "must not be empty");
1440            }
1441            other => panic!("expected InvalidLabel, got {other:?}"),
1442        }
1443    }
1444
1445    #[test]
1446    fn invalid_label_constructor_display_matches_thiserror_derived_shape_bytewise() {
1447        // Display-shape invariant: the constructor's produced variant
1448        // MUST render bytewise-identically to the pre-lift
1449        // thiserror-derived Display output — the shape every
1450        // reconciler consumer's log stream and every operator's grep
1451        // pattern already encodes. A regression that added a slot to
1452        // the variant without updating the `#[error]` attribute (or
1453        // vice versa) would surface as a Display drift here, upstream
1454        // of every downstream log consumer.
1455        let via_constructor =
1456            HostnameError::invalid_label("location", "USE1", "must contain only [a-z0-9-]");
1457        assert_eq!(
1458            format!("{via_constructor}"),
1459            "invalid DNS label \"USE1\" for segment location: must contain only [a-z0-9-]",
1460        );
1461    }
1462
1463    #[test]
1464    fn validate_label_length_gate_routes_through_invalid_label_constructor_bytewise() {
1465        // Delegation pin — the length gate at [`validate_label`] MUST
1466        // surface the byte-identical `HostnameError::InvalidLabel`
1467        // variant the constructor produces for the same
1468        // (segment, label, "must be 1–63 characters") triple. A
1469        // regression that re-inlined the pre-lift struct literal at
1470        // the length gate — dropping the delegation and re-open-
1471        // coding the three slots — would reintroduce the duplication
1472        // this lift removed; this pin catches it by asserting the
1473        // rejection site's error equals the constructor's error
1474        // bytewise across two representative shapes (an empty label
1475        // and a 64-char label past the 63-char upper bound).
1476        let long = "a".repeat(64);
1477        for label in ["", long.as_str()] {
1478            let via_validate = validate_label("app", label).unwrap_err();
1479            let via_constructor =
1480                HostnameError::invalid_label("app", label, "must be 1–63 characters");
1481            assert_eq!(
1482                via_validate, via_constructor,
1483                "validate_label length gate must delegate to invalid_label constructor for label {label:?}"
1484            );
1485        }
1486    }
1487
1488    #[test]
1489    fn validate_label_hyphen_gate_routes_through_invalid_label_constructor_bytewise() {
1490        // Sibling delegation pin — the leading/trailing-hyphen gate
1491        // at [`validate_label`] MUST surface the byte-identical
1492        // variant the constructor produces for the same triple.
1493        // Sibling to the length-gate pin above; three representative
1494        // shapes (leading hyphen, trailing hyphen, both).
1495        for label in ["-lead", "trail-", "-both-"] {
1496            let via_validate = validate_label("cluster", label).unwrap_err();
1497            let via_constructor = HostnameError::invalid_label(
1498                "cluster",
1499                label,
1500                "must not start or end with a hyphen",
1501            );
1502            assert_eq!(
1503                via_validate, via_constructor,
1504                "validate_label hyphen gate must delegate to invalid_label constructor for label {label:?}"
1505            );
1506        }
1507    }
1508
1509    #[test]
1510    fn validate_label_charset_gate_routes_through_invalid_label_constructor_bytewise() {
1511        // Sibling delegation pin — the character-set gate at
1512        // [`validate_label`] MUST surface the byte-identical variant
1513        // the constructor produces for the same triple. Sibling to
1514        // the length + hyphen pins above; three representative shapes
1515        // (uppercase, underscore, non-ASCII).
1516        for label in ["BAD", "with_underscore", "café"] {
1517            let via_validate = validate_label("location", label).unwrap_err();
1518            let via_constructor =
1519                HostnameError::invalid_label("location", label, "must contain only [a-z0-9-]");
1520            assert_eq!(
1521                via_validate, via_constructor,
1522                "validate_label charset gate must delegate to invalid_label constructor for label {label:?}"
1523            );
1524        }
1525    }
1526
1527    #[test]
1528    fn validate_domain_empty_gate_routes_through_invalid_label_constructor_bytewise() {
1529        // Sibling delegation pin — the empty-domain early-return at
1530        // [`validate_domain`] MUST surface the byte-identical variant
1531        // the constructor produces for `("<segment>", "", "must not
1532        // be empty")`. Sibling to the three [`validate_label`] gate
1533        // pins above; the fourth pre-lift rejection site closes the
1534        // sweep. A regression that re-inlined the empty-domain struct
1535        // literal would surface HERE and NOT at any of the three
1536        // sibling `validate_label` pins (each covers a different
1537        // gate), so the four pins together bind each pre-lift
1538        // rejection site to the ONE substrate constructor.
1539        let via_validate = validate_domain("domain", "").unwrap_err();
1540        let via_constructor = HostnameError::invalid_label("domain", "", "must not be empty");
1541        assert_eq!(via_validate, via_constructor);
1542    }
1543}