tatara_process/serde_defaults.rs
1//! Workspace-canonical serde-default owners — the ONE substrate site
2//! every `#[serde(default = "…")]` slot on a plain scalar constant
3//! (`true`, `false`, `0u32`, …) reaches for its serde-default fn.
4//!
5//! Pre-lift each shape lived as a per-file `fn default_<X>() -> <T>
6//! { <constant> }` private shim; because serde's `default = "path"`
7//! contract dispatches on a NAMED function rather than an inline
8//! literal, every consumer file grew its own local shim body — and the
9//! FIVE consumer files that all needed the `-> bool { true }` shape
10//! each spelled the SAME three-line body byte-identically. Post-lift
11//! every consumer routes through the ONE substrate owner here so a
12//! future normalization (a debug-build assertion, a per-fleet override
13//! injected via env var, a rename that folds `default_true` into a
14//! `serde_defaults::TRUE` typed handle) lands at THIS module and every
15//! downstream serde-default consumer inherits the upgrade mechanically.
16//!
17//! Peer to [`crate::lifetime::default_ephemeral_ttl`] +
18//! [`crate::lifetime::default_ephemeral_max_concurrent`] on the
19//! "workspace-canonical serde-default owners" axis. Where those two
20//! primitives own the *ephemeral-authoring-surface-specific* defaults
21//! (`"1h"` / `1u32`), this module owns the *plain-scalar-constant*
22//! defaults that no CRD-specific axis binds to a single crate — the
23//! `-> bool { true }` shape recurs anywhere a boolean field's serde
24//! default is "on" for backward-compat, safe-default, or feature-flag
25//! reasons, and each variant of that shape wants ONE substrate owner
26//! rather than a private per-file shim.
27
28/// Workspace-canonical `-> bool { true }` serde-default owner — the
29/// ONE substrate site every `#[serde(default = "…")]` slot on a `bool`
30/// field whose "on by default" invariant matches the pre-lift shape
31/// routes through.
32///
33/// Pre-lift the SAME 3-line `fn default_true() -> bool { true }` shim
34/// was hand-authored at FIVE workspace-wide sites past the ★★
35/// PRIME-DIRECTIVE ≥ 2 duplication threshold, each serving as the
36/// `#[serde(default = "default_true")]` seed on a different `bool`
37/// field:
38///
39/// * [`crate::matrix::BreatheSpec::dry_run`] — the env-matrix
40/// observability breathe-band spec's "start observe-only" flag; the
41/// safe-by-default posture for a fresh breathe sweep.
42/// * [`crate::spec::SignalPolicy::sigkill_force`] — the process-spec
43/// signal policy's "permit force-reap via SIGKILL" allowance; the
44/// permissive default matching Unix's own SIGKILL semantics.
45/// * [`crate::intent::FluxIntent::decrypt_sops`] — the flux-intent
46/// spec's SOPS-decryption toggle; the pleme-io-convention default of
47/// "SOPS envelopes decrypt on apply".
48/// * [`crate::table::ProcessTableSpec::orphan_reaping_enabled`] — the
49/// process-table spec's PID-1-adopts-orphans switch; the Unix-
50/// process-model default of "PID 1 reaps orphans".
51/// * `tatara-reconciler::ephemeral_defaults::EphemeralDefaults::emit_oci_repository`
52/// — the reconciler's operator-facing "auto-emit OCIRepository peer
53/// for `oci://` chart refs" toggle; the convenience default matching
54/// the sibling render path.
55///
56/// All FIVE sites walked the SAME 3-line body — `fn default_true()
57/// -> bool { true }` — and served the SAME "on-by-default" invariant
58/// through the SAME serde `default = "…"` contract. Post-lift every
59/// consumer's serde slot reads `default = "…serde_defaults::default_true"`
60/// (in-crate via `crate::serde_defaults::default_true`; cross-crate
61/// via `tatara_process::serde_defaults::default_true`) and the local
62/// per-file `fn default_true` shim disappears at each site.
63///
64/// Return-form axis: `bool` — matches the serde `default = "…"` slot
65/// contract exactly (serde invokes the named function and stamps its
66/// returned owned value into the field). The paired
67/// [`DEFAULT_TRUE`] const exposes the underlying `bool` for callers
68/// that want a compile-time handle (a `const fn`-visible pin, a
69/// `matches!(x, DEFAULT_TRUE)` peer check, a const-context comparison).
70///
71/// A future normalization on the workspace-canonical "on-by-default"
72/// invariant (a debug-build assertion that the caller has permission to
73/// stamp `true` at all, a per-fleet override injected via a
74/// `TATARA_DEFAULT_TRUE_<slot>` env var, a shift to a typed
75/// `SerdeDefault<bool>` newtype that carries the semantic label) lands
76/// at THIS ONE substrate primitive and every downstream serde-default
77/// consumer inherits the upgrade mechanically — no per-site edit at
78/// any of the FIVE listed callers or at future consumers (a new
79/// bool-slot serde default, a fleet-wide dashboard reading the
80/// canonical "on" wire-form, a new tatara-eval fixture).
81///
82/// Peer to [`crate::lifetime::default_ephemeral_ttl`] +
83/// [`crate::lifetime::default_ephemeral_max_concurrent`] on the
84/// workspace-canonical serde-default-owner axis — those primitives own
85/// the ephemeral-authoring-surface-specific defaults (`"1h"` / `1u32`),
86/// while this primitive owns the axis-agnostic `-> bool { true }`
87/// scalar-constant default.
88///
89/// Theory anchor: THEORY.md §VI.1 (generation over composition — the
90/// `fn default_true() -> bool { true }` 3-line body recurred at FIVE
91/// hand-authored sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication
92/// trigger, spanning two workspace crates + four `tatara-process`
93/// modules, and is lifted onto ONE workspace-wide substrate owner
94/// here). THEORY.md §II.1 invariant 5 (composition preserves proofs —
95/// the pins bind the wire-form at fail-before-pass-after granularity
96/// so a regression that drifted the returned bool surfaces at
97/// [`tests::default_true_returns_true_bytewise`] rather than as silent
98/// operator-facing skew across the five downstream consumers).
99#[must_use]
100pub fn default_true() -> bool {
101 DEFAULT_TRUE
102}
103
104/// Workspace-canonical `bool` handle over the same `true` value
105/// [`default_true`] returns. Use this const for compile-time
106/// comparisons and const-context readers; use [`default_true`] for
107/// the serde `default = "…"` slot contract.
108pub const DEFAULT_TRUE: bool = true;
109
110/// Workspace-canonical `-> u32 { 480 }` SIGTERM→SIGKILL grace-window
111/// serde-default owner — the ONE substrate site every
112/// `#[serde(default = "…")]` slot on a `u32` field whose semantic is
113/// "seconds to wait after delivering SIGTERM before escalating to
114/// SIGKILL" routes through.
115///
116/// Pre-lift the SAME 3-line `fn default_<slot>() -> u32 { 480 }` shim
117/// was hand-authored at TWO workspace-wide sites past the ★★
118/// PRIME-DIRECTIVE ≥ 2 duplication threshold, each serving as the
119/// `#[serde(default = "default_<slot>")]` seed on a different `u32`
120/// field carrying the SAME semantic wire-form (8-minute SIGTERM→
121/// SIGKILL grace window):
122///
123/// * [`crate::spec::SignalPolicy::sigterm_grace_seconds`] — the
124/// per-Process signal-policy's SIGTERM→SIGKILL grace window; the
125/// escalation timer the Unix-process-model reconciler honors during
126/// Exiting → Zombie.
127/// * [`crate::table::ProcessTableSpec::sigterm_timeout_seconds`] — the
128/// per-ProcessTable default SIGTERM→SIGKILL grace window; the
129/// fallback the reconciler injects when a child Process omits its
130/// own [`SignalPolicy::sigterm_grace_seconds`] slot.
131///
132/// Both sites walked the SAME 3-line body — `-> u32 { 480 }` — and
133/// served the SAME "8-minute SIGTERM→SIGKILL escalation" invariant
134/// through the SAME serde `default = "…"` contract. Post-lift every
135/// consumer's serde slot reads
136/// `default = "…serde_defaults::default_sigterm_grace_seconds"`
137/// (in-crate via `crate::serde_defaults::default_sigterm_grace_seconds`;
138/// cross-crate via
139/// `tatara_process::serde_defaults::default_sigterm_grace_seconds`)
140/// and the local per-file `fn default_sigterm_<slot>` shim disappears
141/// at each site.
142///
143/// Return-form axis: `u32` — matches the serde `default = "…"` slot
144/// contract exactly (serde invokes the named function and stamps its
145/// returned owned value into the field). The paired
146/// [`DEFAULT_SIGTERM_GRACE_SECONDS`] const exposes the underlying
147/// `u32` for callers that want a compile-time handle (a `const fn`-
148/// visible pin, a `matches!(x, DEFAULT_SIGTERM_GRACE_SECONDS)` peer
149/// check, a const-context comparison against a k8s pod-eviction
150/// grace annotation).
151///
152/// A future normalization on the workspace-canonical "SIGTERM→
153/// SIGKILL grace" invariant (a shift to the k8s-recommended 30s
154/// default, a per-fleet override injected via a `TATARA_SIGTERM_
155/// GRACE_SECONDS` env var, a shift to a typed `GracePeriod`
156/// newtype that carries the "seconds" unit) lands at THIS ONE
157/// substrate primitive and every downstream serde-default consumer
158/// inherits the upgrade mechanically — no per-site edit at either
159/// listed caller or at future consumers (a new Process-plane
160/// termination window, a per-container override in `ContainerIntent`,
161/// a `PoolSpec::sigterm_grace_seconds` slot).
162///
163/// Peer to [`default_true`] on the workspace-canonical serde-default-
164/// owner axis. Where [`default_true`] owns the `-> bool { true }`
165/// on-by-default scalar shape, this primitive owns the `-> u32
166/// { 480 }` SIGTERM-grace scalar shape.
167///
168/// Theory anchor: THEORY.md §VI.1 (generation over composition — the
169/// `fn default_<slot>() -> u32 { 480 }` 3-line body recurred at TWO
170/// hand-authored sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication
171/// trigger, and is lifted onto ONE workspace-wide substrate owner
172/// here). THEORY.md §II.1 invariant 5 (composition preserves proofs —
173/// the pins bind the wire-form at fail-before-pass-after granularity
174/// so a regression that drifted the returned u32 surfaces at
175/// [`tests::default_sigterm_grace_seconds_returns_480_bytewise`]
176/// rather than as silent operator-facing skew across the two
177/// downstream consumers).
178#[must_use]
179pub fn default_sigterm_grace_seconds() -> u32 {
180 DEFAULT_SIGTERM_GRACE_SECONDS
181}
182
183/// Workspace-canonical `u32` handle over the same `480` value
184/// [`default_sigterm_grace_seconds`] returns. Use this const for
185/// compile-time comparisons and const-context readers; use
186/// [`default_sigterm_grace_seconds`] for the serde `default = "…"`
187/// slot contract.
188pub const DEFAULT_SIGTERM_GRACE_SECONDS: u32 = 480;
189
190/// Workspace-canonical `-> u32 { 600 }` Zombie-phase force-reap
191/// timeout serde-default owner — the ONE substrate site every
192/// `#[serde(default = "…")]` slot on a `u32` field whose semantic is
193/// "seconds a Process is permitted to sit in `Zombie` before PID 1
194/// force-reaps it" routes through, PLUS the ONE substrate anchor
195/// every hand-authored `ProcessTableSpec` composer that pre-populates
196/// the `zombie_timeout_seconds` slot with the workspace-canonical
197/// wire-form routes through.
198///
199/// Pre-lift the SAME `600` u32 constant lived at TWO workspace-wide
200/// sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication threshold, each
201/// encoding the SAME semantic wire-form (10-minute Zombie force-reap
202/// window):
203///
204/// * [`crate::table::ProcessTableSpec::zombie_timeout_seconds`] — the
205/// per-ProcessTable Zombie force-reap window; the `#[serde(default
206/// = "…")]` seed the wire-parser stamps when a serialized
207/// `ProcessTable` YAML omits its own `zombieTimeoutSeconds:` slot.
208/// * `tatara_reconciler::patch::ensure_process_table` — the
209/// ProcessTable-singleton bootstrap composer; the explicit
210/// `zombie_timeout_seconds: 600` slot in the hand-authored
211/// [`crate::table::ProcessTableSpec`] struct literal used to
212/// materialize a fresh singleton on first observation.
213///
214/// Both sites walked the SAME bare `600u32` constant and served the
215/// SAME "10-minute Zombie force-reap" invariant — the serde path
216/// through the `#[serde(default = "…")]` machinery, the composer path
217/// through the explicit struct-literal slot. Post-lift the serde slot
218/// reads `default = "…serde_defaults::default_zombie_timeout_seconds"`
219/// (in-crate via `crate::serde_defaults::default_zombie_timeout_seconds`;
220/// cross-crate via
221/// `tatara_process::serde_defaults::default_zombie_timeout_seconds`)
222/// and the reconciler composer feeds the substrate fn directly at the
223/// `zombie_timeout_seconds:` slot in place of the bare `600` literal.
224///
225/// Return-form axis: `u32` — matches the serde `default = "…"` slot
226/// contract exactly (serde invokes the named function and stamps its
227/// returned owned value into the field). The paired
228/// [`DEFAULT_ZOMBIE_TIMEOUT_SECONDS`] const exposes the underlying
229/// `u32` for callers that want a compile-time handle (a `const fn`-
230/// visible pin, a `matches!(x, DEFAULT_ZOMBIE_TIMEOUT_SECONDS)` peer
231/// check, a const-context comparison against a k8s pod-termination
232/// grace annotation).
233///
234/// A future normalization on the workspace-canonical "Zombie force-
235/// reap" invariant (a shift to the k8s pod-eviction-recommended 300s
236/// default, a per-fleet override injected via a
237/// `TATARA_ZOMBIE_TIMEOUT_SECONDS` env var, a shift to a typed
238/// `ReapDeadline` newtype that carries the "seconds since Zombie
239/// entry" phase-anchored semantics) lands at THIS ONE substrate
240/// primitive and every downstream serde-default consumer PLUS every
241/// hand-authored `ensure_process_table`-shaped composer inherits the
242/// upgrade mechanically — no per-site edit at either listed caller or
243/// at future consumers (a per-Process `zombieTimeoutSecondsOverride`
244/// slot, a `PoolSpec`-scoped Zombie window, a new debug-build sub-10s
245/// override for local development).
246///
247/// Peer to [`default_sigterm_grace_seconds`] on the workspace-
248/// canonical "termination-phase timing" scalar axis. Where
249/// [`default_sigterm_grace_seconds`] owns the `-> u32 { 480 }`
250/// SIGTERM→SIGKILL escalation window (the Exiting → Zombie phase
251/// transition's grace clock), this primitive owns the `-> u32 { 600 }`
252/// Zombie → Reaped force-reap window (the phase transition that
253/// follows). The two primitives compose sequentially at the
254/// reconciler: a `Process` with the default policy spends up to
255/// [`default_sigterm_grace_seconds`] seconds in Exiting, then up to
256/// [`default_zombie_timeout_seconds`] seconds in Zombie, then is
257/// force-reaped and cascaded via ownerRefs.
258///
259/// Theory anchor: THEORY.md §VI.1 (generation over composition — the
260/// bare `600u32` constant recurred at TWO hand-authored sites past
261/// the ★★ PRIME-DIRECTIVE ≥ 2 duplication trigger, spanning two
262/// workspace crates, and is lifted onto ONE workspace-wide substrate
263/// owner here). THEORY.md §II.1 invariant 5 (composition preserves
264/// proofs — the pins bind the wire-form at fail-before-pass-after
265/// granularity so a regression that drifted the returned u32 surfaces
266/// at [`tests::default_zombie_timeout_seconds_returns_600_bytewise`]
267/// rather than as silent operator-facing skew across the two
268/// downstream consumers).
269#[must_use]
270pub fn default_zombie_timeout_seconds() -> u32 {
271 DEFAULT_ZOMBIE_TIMEOUT_SECONDS
272}
273
274/// Workspace-canonical `u32` handle over the same `600` value
275/// [`default_zombie_timeout_seconds`] returns. Use this const for
276/// compile-time comparisons and const-context readers; use
277/// [`default_zombie_timeout_seconds`] for the serde `default = "…"`
278/// slot contract.
279pub const DEFAULT_ZOMBIE_TIMEOUT_SECONDS: u32 = 600;
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284
285 // ─── default_true substrate pins ─────────────────────────────────
286 //
287 // Bind [`default_true`] + the paired [`DEFAULT_TRUE`] const at
288 // fail-before-pass-after granularity so a regression that drifted
289 // the wire-form default (a shift from `true` to `false` at ONLY
290 // the const, a decoupling of the const from the fn's returned
291 // value, a rename that broke the serde `default = "…"` path
292 // resolution) surfaces HERE rather than as silent operator-visible
293 // skew across the FIVE serde-default consumers that ride through
294 // this ONE substrate owner.
295
296 #[test]
297 fn default_true_returns_true_bytewise() {
298 // Byte-shape parity with the FIVE hand-authored pre-lift shims
299 // that each returned `true`. A regression that flipped the
300 // returned bool (an accidental `false` seed, a `!true` typo
301 // that survives parse, a shift to a typed newtype without a
302 // Deref) fails HERE rather than at the five downstream
303 // consumers whose "on-by-default" invariant would silently
304 // become "off by default".
305 assert!(
306 default_true(),
307 "default_true must return `true` bytewise; a regression \
308 surfaces here rather than as five-way skew across \
309 BreatheSpec::dry_run + SignalPolicy::sigkill_force + \
310 FluxIntent::decrypt_sops + ProcessTableSpec::orphan_reaping_enabled + \
311 EphemeralDefaults::emit_oci_repository",
312 );
313 }
314
315 #[test]
316 #[allow(clippy::assertions_on_constants)]
317 // The `assert!(DEFAULT_TRUE, …)` shape IS the pin: we're binding
318 // the const's wire-form value at test time. Clippy sees a compile-
319 // time-constant assertion and flags it; the flag is exactly the
320 // corner we WANT to trip if a future edit drifts the const to
321 // `false` — the test would then fail with a comprehensible message
322 // rather than the const change slipping past.
323 fn default_true_wire_form_const_matches_fn_return_bytewise() {
324 // Cross-form coherence pin: the `pub const DEFAULT_TRUE: bool`
325 // handle and the `pub fn default_true() -> bool` owner MUST
326 // project onto the SAME wire-form value. A regression that
327 // updated one but not the other (e.g. lifted the const to
328 // `false` for a fleet-wide safe-default shift but forgot the
329 // fn body, or vice versa) would silently produce two divergent
330 // workspace-canonical defaults — the const for compile-time
331 // consumers, the fn for serde-default consumers. Pin the two
332 // projections at equality.
333 assert!(
334 DEFAULT_TRUE,
335 "DEFAULT_TRUE const must byte-match the pre-lift wire-form \
336 default `true`",
337 );
338 assert_eq!(
339 default_true(),
340 DEFAULT_TRUE,
341 "default_true() must byte-match the paired DEFAULT_TRUE \
342 const — a divergence would silently skew serde-default \
343 consumers vs compile-time const readers",
344 );
345 }
346
347 #[test]
348 fn default_true_composes_at_breathe_envelope_serde_default() {
349 // End-to-end: the [`crate::matrix::BreatheEnvelope::dry_run`]
350 // serde default MUST route through the substrate owner. A
351 // YAML fragment that omits the `dryRun:` field parses into a
352 // BreatheEnvelope whose `dry_run` reads bytewise-identical to
353 // [`default_true`]. A regression that reintroduced a local
354 // `fn default_true` shim in `matrix.rs` (bypassing the
355 // substrate) would produce a silent skew between the breathe
356 // envelope's "start observe-only" invariant and the four peer
357 // consumers.
358 let yaml = "\
359dimensions: []
360";
361 let spec: crate::matrix::BreatheEnvelope =
362 serde_yaml::from_str(yaml).expect("BreatheEnvelope YAML parses");
363 assert_eq!(
364 spec.dry_run,
365 default_true(),
366 "BreatheEnvelope serde-default for the omitted `dryRun:` slot \
367 must route through crate::serde_defaults::default_true \
368 — a private-shim reintroduction skews the breathe-envelope \
369 observability posture silently",
370 );
371 }
372
373 #[test]
374 fn default_true_composes_at_signal_policy_serde_default() {
375 // Peer to the BreatheSpec pin above — pin the
376 // [`crate::spec::SignalPolicy::sigkill_force`] serde default
377 // at the substrate owner. A YAML fragment that omits
378 // `sigkillForce:` parses into a SignalPolicy whose
379 // `sigkill_force` reads bytewise-identical to
380 // [`default_true`]. A regression that decoupled the
381 // signal-policy default from the substrate would silently
382 // flip the "permit force-reap" posture without any consumer
383 // tripping.
384 let yaml = "";
385 let sp: crate::spec::SignalPolicy =
386 serde_yaml::from_str(yaml).expect("SignalPolicy YAML parses");
387 assert_eq!(
388 sp.sigkill_force,
389 default_true(),
390 "SignalPolicy serde-default for the omitted `sigkillForce:` \
391 slot must route through crate::serde_defaults::default_true",
392 );
393 }
394
395 #[test]
396 fn default_true_composes_at_process_table_spec_serde_default() {
397 // Peer to the SignalPolicy pin above — pin the
398 // [`crate::table::ProcessTableSpec::orphan_reaping_enabled`]
399 // serde default at the substrate owner. A YAML fragment that
400 // omits `orphanReapingEnabled:` parses into a ProcessTableSpec
401 // whose `orphan_reaping_enabled` reads bytewise-identical to
402 // [`default_true`]. A regression that decoupled the
403 // process-table default from the substrate would silently
404 // flip the "PID 1 reaps orphans" posture without any consumer
405 // tripping.
406 let yaml = "";
407 let ts: crate::table::ProcessTableSpec =
408 serde_yaml::from_str(yaml).expect("ProcessTableSpec YAML parses");
409 assert_eq!(
410 ts.orphan_reaping_enabled,
411 default_true(),
412 "ProcessTableSpec serde-default for the omitted \
413 `orphanReapingEnabled:` slot must route through \
414 crate::serde_defaults::default_true",
415 );
416 }
417
418 // ─── default_sigterm_grace_seconds substrate pins ────────────────
419 //
420 // Bind [`default_sigterm_grace_seconds`] + the paired
421 // [`DEFAULT_SIGTERM_GRACE_SECONDS`] const at fail-before-pass-after
422 // granularity so a regression that drifted the wire-form default
423 // (a shift from `480` to `30` at ONLY the const, a decoupling of
424 // the const from the fn's returned value, a rename that broke the
425 // serde `default = "…"` path resolution) surfaces HERE rather than
426 // as silent operator-visible skew across the TWO serde-default
427 // consumers that ride through this ONE substrate owner.
428
429 #[test]
430 fn default_sigterm_grace_seconds_returns_480_bytewise() {
431 // Byte-shape parity with the TWO hand-authored pre-lift shims
432 // that each returned `480`. A regression that flipped the
433 // returned u32 (an accidental `48` or `4800` seed, a shift to
434 // millis without a unit rename, a shift to a typed newtype
435 // without a Deref) fails HERE rather than at the two
436 // downstream consumers whose "8-minute SIGTERM→SIGKILL grace"
437 // invariant would silently become sub-minute or 80-minute.
438 assert_eq!(
439 default_sigterm_grace_seconds(),
440 480,
441 "default_sigterm_grace_seconds must return `480` bytewise; \
442 a regression surfaces here rather than as two-way skew \
443 across SignalPolicy::sigterm_grace_seconds + \
444 ProcessTableSpec::sigterm_timeout_seconds",
445 );
446 }
447
448 #[test]
449 fn default_sigterm_grace_seconds_wire_form_const_matches_fn_return_bytewise() {
450 // Cross-form coherence pin: the `pub const
451 // DEFAULT_SIGTERM_GRACE_SECONDS: u32` handle and the `pub fn
452 // default_sigterm_grace_seconds() -> u32` owner MUST project
453 // onto the SAME wire-form value. A regression that updated
454 // one but not the other (e.g. lifted the const to `30` for a
455 // fleet-wide k8s-alignment shift but forgot the fn body, or
456 // vice versa) would silently produce two divergent workspace-
457 // canonical defaults — the const for compile-time consumers,
458 // the fn for serde-default consumers. Pin the two projections
459 // at equality.
460 assert_eq!(
461 DEFAULT_SIGTERM_GRACE_SECONDS, 480,
462 "DEFAULT_SIGTERM_GRACE_SECONDS const must byte-match the \
463 pre-lift wire-form default `480`",
464 );
465 assert_eq!(
466 default_sigterm_grace_seconds(),
467 DEFAULT_SIGTERM_GRACE_SECONDS,
468 "default_sigterm_grace_seconds() must byte-match the \
469 paired DEFAULT_SIGTERM_GRACE_SECONDS const — a divergence \
470 would silently skew serde-default consumers vs compile-\
471 time const readers",
472 );
473 }
474
475 #[test]
476 fn default_sigterm_grace_seconds_composes_at_signal_policy_serde_default() {
477 // End-to-end: the
478 // [`crate::spec::SignalPolicy::sigterm_grace_seconds`] serde
479 // default MUST route through the substrate owner. A YAML
480 // fragment that omits `sigtermGraceSeconds:` parses into a
481 // SignalPolicy whose `sigterm_grace_seconds` reads bytewise-
482 // identical to [`default_sigterm_grace_seconds`]. A regression
483 // that reintroduced a local `fn default_sigterm_grace` shim in
484 // `spec.rs` (bypassing the substrate) would produce a silent
485 // skew between the per-Process signal-policy escalation window
486 // and the ProcessTable-plane default that peers with it.
487 let yaml = "";
488 let sp: crate::spec::SignalPolicy =
489 serde_yaml::from_str(yaml).expect("SignalPolicy YAML parses");
490 assert_eq!(
491 sp.sigterm_grace_seconds,
492 default_sigterm_grace_seconds(),
493 "SignalPolicy serde-default for the omitted \
494 `sigtermGraceSeconds:` slot must route through \
495 crate::serde_defaults::default_sigterm_grace_seconds \
496 — a private-shim reintroduction skews the per-Process \
497 SIGTERM→SIGKILL escalation window silently",
498 );
499 }
500
501 #[test]
502 fn default_sigterm_grace_seconds_composes_at_process_table_spec_serde_default() {
503 // Peer to the SignalPolicy pin above — pin the
504 // [`crate::table::ProcessTableSpec::sigterm_timeout_seconds`]
505 // serde default at the substrate owner. A YAML fragment that
506 // omits `sigtermTimeoutSeconds:` parses into a
507 // ProcessTableSpec whose `sigterm_timeout_seconds` reads
508 // bytewise-identical to [`default_sigterm_grace_seconds`]. A
509 // regression that decoupled the process-table default from the
510 // substrate would silently flip the "8-minute grace" invariant
511 // at the fallback layer while the per-Process layer stayed
512 // put, producing table-scoped children with a mismatched
513 // escalation window.
514 let yaml = "";
515 let ts: crate::table::ProcessTableSpec =
516 serde_yaml::from_str(yaml).expect("ProcessTableSpec YAML parses");
517 assert_eq!(
518 ts.sigterm_timeout_seconds,
519 default_sigterm_grace_seconds(),
520 "ProcessTableSpec serde-default for the omitted \
521 `sigtermTimeoutSeconds:` slot must route through \
522 crate::serde_defaults::default_sigterm_grace_seconds",
523 );
524 }
525
526 #[test]
527 fn default_sigterm_grace_seconds_composes_at_signal_policy_impl_default() {
528 // `SignalPolicy::default()` reconstructs the same wire-form
529 // 480 through the substrate — pin it. A regression that
530 // decoupled the Default-impl path from the serde-default path
531 // (e.g. hard-coded `480` at the Default impl, then drifted the
532 // substrate owner to `30`) would produce two different
533 // "default" SignalPolicies depending on construction path.
534 let sp = crate::spec::SignalPolicy::default();
535 assert_eq!(
536 sp.sigterm_grace_seconds,
537 default_sigterm_grace_seconds(),
538 "SignalPolicy::default() must reconstruct \
539 `sigterm_grace_seconds` through the substrate owner — a \
540 hand-authored literal at the Default impl would decouple \
541 the two construction paths",
542 );
543 }
544
545 // ─── default_zombie_timeout_seconds substrate pins ───────────────
546 //
547 // Bind [`default_zombie_timeout_seconds`] + the paired
548 // [`DEFAULT_ZOMBIE_TIMEOUT_SECONDS`] const at fail-before-pass-
549 // after granularity so a regression that drifted the wire-form
550 // default (a shift from `600` to `300` at ONLY the const, a
551 // decoupling of the const from the fn's returned value, a rename
552 // that broke the serde `default = "…"` path resolution) surfaces
553 // HERE rather than as silent operator-visible skew across the TWO
554 // consumers that ride through this ONE substrate owner (the
555 // `ProcessTableSpec::zombie_timeout_seconds` serde default AND
556 // the `tatara_reconciler::patch::ensure_process_table` composer's
557 // explicit `zombie_timeout_seconds:` slot).
558
559 #[test]
560 fn default_zombie_timeout_seconds_returns_600_bytewise() {
561 // Byte-shape parity with the TWO hand-authored pre-lift sites
562 // that each encoded `600` (the serde-default fn body in
563 // `crate::table` + the composer's explicit struct-literal slot
564 // in `tatara_reconciler::patch::ensure_process_table`). A
565 // regression that flipped the returned u32 (an accidental `60`
566 // or `6000` seed, a shift to millis without a unit rename, a
567 // shift to a typed newtype without a Deref) fails HERE rather
568 // than at the two downstream consumers whose "10-minute Zombie
569 // force-reap" invariant would silently become sub-minute or
570 // 100-minute.
571 assert_eq!(
572 default_zombie_timeout_seconds(),
573 600,
574 "default_zombie_timeout_seconds must return `600` bytewise; \
575 a regression surfaces here rather than as two-way skew \
576 across ProcessTableSpec::zombie_timeout_seconds + \
577 tatara_reconciler::patch::ensure_process_table",
578 );
579 }
580
581 #[test]
582 fn default_zombie_timeout_seconds_wire_form_const_matches_fn_return_bytewise() {
583 // Cross-form coherence pin: the `pub const
584 // DEFAULT_ZOMBIE_TIMEOUT_SECONDS: u32` handle and the `pub fn
585 // default_zombie_timeout_seconds() -> u32` owner MUST project
586 // onto the SAME wire-form value. A regression that updated
587 // one but not the other (e.g. lifted the const to `300` for a
588 // fleet-wide k8s-alignment shift but forgot the fn body, or
589 // vice versa) would silently produce two divergent workspace-
590 // canonical defaults — the const for compile-time consumers,
591 // the fn for serde-default + composer consumers. Pin the two
592 // projections at equality.
593 assert_eq!(
594 DEFAULT_ZOMBIE_TIMEOUT_SECONDS, 600,
595 "DEFAULT_ZOMBIE_TIMEOUT_SECONDS const must byte-match the \
596 pre-lift wire-form default `600`",
597 );
598 assert_eq!(
599 default_zombie_timeout_seconds(),
600 DEFAULT_ZOMBIE_TIMEOUT_SECONDS,
601 "default_zombie_timeout_seconds() must byte-match the \
602 paired DEFAULT_ZOMBIE_TIMEOUT_SECONDS const — a divergence \
603 would silently skew serde-default + composer consumers \
604 vs compile-time const readers",
605 );
606 }
607
608 #[test]
609 fn default_zombie_timeout_seconds_composes_at_process_table_spec_serde_default() {
610 // End-to-end: the
611 // [`crate::table::ProcessTableSpec::zombie_timeout_seconds`]
612 // serde default MUST route through the substrate owner. A
613 // YAML fragment that omits `zombieTimeoutSeconds:` parses
614 // into a ProcessTableSpec whose `zombie_timeout_seconds`
615 // reads bytewise-identical to
616 // [`default_zombie_timeout_seconds`]. A regression that
617 // reintroduced a local `fn default_zombie_timeout` shim in
618 // `table.rs` (bypassing the substrate) would produce a silent
619 // skew between the wire-parser Zombie force-reap window and
620 // the reconciler-composer Zombie force-reap window.
621 let yaml = "";
622 let ts: crate::table::ProcessTableSpec =
623 serde_yaml::from_str(yaml).expect("ProcessTableSpec YAML parses");
624 assert_eq!(
625 ts.zombie_timeout_seconds,
626 default_zombie_timeout_seconds(),
627 "ProcessTableSpec serde-default for the omitted \
628 `zombieTimeoutSeconds:` slot must route through \
629 crate::serde_defaults::default_zombie_timeout_seconds",
630 );
631 }
632
633 #[test]
634 fn default_zombie_timeout_seconds_pairs_with_sigterm_grace_on_termination_axis() {
635 // Composition pin: the two termination-phase timing primitives
636 // ([`default_sigterm_grace_seconds`] +
637 // [`default_zombie_timeout_seconds`]) fire sequentially at the
638 // reconciler — Exiting-phase grace first, then Zombie-phase
639 // force-reap. Pin both against their canonical wire-forms in
640 // a single assertion so a regression that lifted one primitive
641 // through a rename that shadowed the peer (or that drifted
642 // both together in a coordinated typo) surfaces HERE. The
643 // Zombie window MUST also be strictly greater than the SIGTERM
644 // grace window (a process only enters Zombie once its
645 // SIGTERM grace has already elapsed, so the reap deadline
646 // must be at least the escalation deadline).
647 assert_eq!(default_sigterm_grace_seconds(), 480);
648 assert_eq!(default_zombie_timeout_seconds(), 600);
649 assert!(
650 default_zombie_timeout_seconds() > default_sigterm_grace_seconds(),
651 "Zombie force-reap window must exceed the SIGTERM grace \
652 window — the Zombie phase only begins after the SIGTERM \
653 grace has elapsed, so a Zombie deadline shorter than the \
654 SIGTERM deadline would mean force-reap fires before the \
655 process ever entered Zombie",
656 );
657 }
658}