hyperactor_config/
global.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! Global layered configuration for Hyperactor.
10//!
11//! This module provides the process-wide configuration store and APIs
12//! to access it. Configuration values are resolved via a **layered
13//! model**: `TestOverride → Env → Runtime → File → ClientOverride →
14//! Default`.
15//!
16//! - Reads (`get`, `get_cloned`) consult layers in that order, falling
17//!   back to defaults if no explicit value is set.
18//! - `attrs()` returns a complete snapshot of all CONFIG-marked keys at
19//!   call time: it materializes defaults for keys not set in any layer.
20//!   Keys without @meta(CONFIG = …) are excluded.
21//! - In tests, `lock()` and `override_key` allow temporary overrides
22//!   that are removed automatically when the guard drops.
23//! - In normal operation, a parent process can capture its effective
24//!   config via `attrs()` and pass that snapshot to a child during
25//!   bootstrap. The child installs it as a `ClientOverride` layer.
26//!   Note that Env and Runtime layers will take precedence over this
27//!   inherited configuration.
28//!
29//! This design provides flexibility (easy test overrides, runtime
30//! updates, YAML/Env baselines) while ensuring type safety and
31//! predictable resolution order.
32//!
33//! # Testing
34//!
35//! Tests can override global configuration using [`lock`]. This
36//! ensures such tests are serialized (and cannot clobber each other's
37//! overrides).
38//!
39//! ```ignore
40//! #[test]
41//! fn test_my_feature() {
42//!     let config = hyperactor::config::global::lock();
43//!     let _guard = config.override_key(SOME_CONFIG_KEY, test_value);
44//!     // ... test logic here ...
45//! }
46//! ```
47use std::collections::HashMap;
48use std::marker::PhantomData;
49use std::path::Path;
50use std::sync::Arc;
51use std::sync::LazyLock;
52use std::sync::RwLock;
53use std::sync::atomic::AtomicU64;
54use std::sync::atomic::Ordering;
55
56use arc_swap::ArcSwap;
57use serde::Deserialize;
58use serde::Serialize;
59
60use crate::CONFIG;
61use crate::attrs::AttrKeyInfo;
62use crate::attrs::AttrValue;
63use crate::attrs::Attrs;
64use crate::attrs::Key;
65use crate::from_env;
66use crate::from_yaml;
67
68/// Configuration source layers in priority order.
69///
70/// Resolution order is always: **TestOverride -> Env -> Runtime
71/// -> File -> ClientOverride -> Default**.
72///
73/// Smaller `priority()` number = higher precedence.
74#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
75pub enum Source {
76    /// Values set by the config snapshot sent from the client
77    /// during proc bootstrap.
78    ClientOverride,
79    /// Values loaded from configuration files (e.g., YAML).
80    File,
81    /// Values read from environment variables at process startup.
82    /// Higher priority than Runtime, File, and ClientOverride, but
83    /// lower than TestOverride.
84    Env,
85    /// Values set programmatically at runtime. High-priority layer
86    /// but overridden by Env and TestOverride.
87    Runtime,
88    /// Ephemeral values inserted by tests via
89    /// `ConfigLock::override_key`. Always wins over all other
90    /// sources; removed when the guard drops.
91    TestOverride,
92    /// The key's declared default value. Not stored as a layer —
93    /// used only in [`ConfigEntry`] to report that no explicit layer
94    /// provided a value.
95    Default,
96}
97
98/// Return the numeric priority for a source.
99///
100/// Smaller number = higher precedence. Matches the documented order:
101/// TestOverride (0) -> Env (1) -> Runtime (2) -> File (3) ->
102/// ClientOverride (4).
103fn priority(s: Source) -> u8 {
104    match s {
105        Source::TestOverride => 0,
106        Source::Env => 1,
107        Source::Runtime => 2,
108        Source::File => 3,
109        Source::ClientOverride => 4,
110        Source::Default => 5,
111    }
112}
113
114/// The full set of configuration layers in priority order.
115///
116/// `Layers` wraps a vector of [`Layer`]s, always kept sorted by
117/// [`priority`] (lowest number = highest precedence).
118///
119/// Resolution (`get`, `get_cloned`, `attrs`) consults `ordered` from
120/// front to back, returning the first value found for each key and
121/// falling back to defaults if none are set in any layer.
122struct Layers {
123    /// Kept sorted by `priority` (lowest number first = highest
124    /// priority).
125    ordered: Vec<Layer>,
126}
127
128/// A single configuration layer in the global configuration model.
129///
130/// Layers are consulted in priority order (`TestOverride → Env →
131/// Runtime → File → ClientOverride → Default`) when resolving
132/// configuration values. Each variant holds an [`Attrs`] map of
133/// key/value pairs.
134///
135/// The `TestOverride` variant additionally maintains per-key override
136/// stacks to support nested and out-of-order test overrides.
137///
138/// Variants:
139/// - [`Layer::ClientOverride`] - Values set by the config snapshot
140///   sent from the client during proc bootstrap.
141/// - [`Layer::File`] — Values loaded from configuration files.
142/// - [`Layer::Env`] — Values sourced from process environment
143///   variables.
144/// - [`Layer::Runtime`] — Programmatically set runtime overrides.
145/// - [`Layer::TestOverride`] — Temporary in-test overrides applied
146///   under [`ConfigLock`].
147///
148/// Layers are stored in [`Layers::ordered`], kept sorted by their
149/// effective [`Source`] priority (`TestOverride` first, `Default`
150/// last).
151enum Layer {
152    /// Values set by the config snapshot sent from the client
153    /// during proc bootstrap.
154    ClientOverride(Attrs),
155
156    /// Values loaded from configuration files.
157    File(Attrs),
158
159    /// Values read from process environment variables. Typically
160    /// installed at startup via [`init_from_env`].
161    Env(Attrs),
162
163    /// Values set programmatically at runtime. High-priority layer
164    /// used for dynamic updates (e.g., Python `configure()` API), but
165    /// overridden by Env and TestOverride layers.
166    Runtime(Attrs),
167
168    /// Ephemeral values inserted during tests via
169    /// [`ConfigLock::override_key`]. Always takes precedence over all
170    /// other layers. Holds both the active `attrs` map and a per-key
171    /// `stacks` table to support nested and out-of-order test
172    /// overrides.
173    TestOverride {
174        attrs: Attrs,
175        stacks: HashMap<&'static str, OverrideStack>,
176    },
177}
178
179/// A per-key stack of test overrides used by the
180/// [`Layer::TestOverride`] layer.
181///
182/// Each stack tracks the sequence of active overrides applied to a
183/// single configuration key. The topmost frame represents the
184/// currently effective override; earlier frames represent older
185/// (still live) guards that may drop out of order.
186///
187/// Fields:
188/// - `env_var`: The associated process environment variable name, if
189///   any.
190/// - `saved_env`: The original environment variable value before the
191///   first override was applied (or `None` if it did not exist).
192/// - `frames`: The stack of active override frames, with the top
193///   being the last element in the vector.
194struct OverrideStack {
195    /// The name of the process environment variable associated with
196    /// this configuration key, if any.
197    ///
198    /// Used to mirror changes to the environment when overrides are
199    /// applied or removed. `None` if the key has no
200    /// `CONFIG.env_name`.
201    env_var: Option<String>,
202
203    /// The original value of the environment variable before the
204    /// first override was applied.
205    ///
206    /// Stored so it can be restored once the last frame is dropped.
207    /// `None` means the variable did not exist prior to overriding.
208    saved_env: Option<String>,
209
210    /// The sequence of active override frames for this key.
211    ///
212    /// Each frame represents one active test override; the last
213    /// element (`frames.last()`) is the current top-of-stack and
214    /// defines the effective value seen in the configuration and
215    /// environment.
216    frames: Vec<OverrideFrame>,
217}
218
219/// A single entry in a per-key override stack.
220///
221/// Each `OverrideFrame` represents one active test override applied
222/// via [`ConfigLock::override_key`]. Frames are uniquely identified
223/// by a monotonically increasing token and record both the value
224/// being overridden and its string form for environment mirroring.
225///
226/// When a guard drops, its frame is removed from the stack; if it was
227/// the top, the next frame (if any) becomes active, or the original
228/// environment is restored when the stack becomes empty.
229struct OverrideFrame {
230    /// A unique, monotonically increasing identifier for this
231    /// override frame.
232    ///
233    /// Used to associate a dropping [`ConfigValueGuard`] with its
234    /// corresponding entry in the stack, even if drops occur out of
235    /// order.
236    token: u64,
237
238    /// The serialized configuration value active while this frame is
239    /// on top of its stack.
240    ///
241    /// Stored as a boxed [`SerializableValue`] to match how values
242    /// are kept within [`Attrs`].
243    value: Box<dyn crate::attrs::SerializableValue>,
244
245    /// Pre-rendered string form of the value, used for environment
246    /// variable updates when this frame becomes active.
247    ///
248    /// Avoids recomputing `value.display()` on every push or pop.
249    env_str: String,
250}
251
252/// Return the [`Source`] corresponding to a given [`Layer`].
253///
254/// This provides a uniform way to retrieve a layer's logical source
255/// (File, Env, Runtime, TestOverride, or ClientOverride) regardless
256/// of its internal representation. Used for sorting layers by
257/// priority and for source-based lookups or removals.
258fn layer_source(l: &Layer) -> Source {
259    match l {
260        Layer::File(_) => Source::File,
261        Layer::Env(_) => Source::Env,
262        Layer::Runtime(_) => Source::Runtime,
263        Layer::TestOverride { .. } => Source::TestOverride,
264        Layer::ClientOverride(_) => Source::ClientOverride,
265    }
266}
267
268/// Return an immutable reference to the [`Attrs`] contained in a
269/// [`Layer`].
270///
271/// This abstracts over the specific layer variant so callers can read
272/// configuration values uniformly without needing to pattern-match on
273/// the layer type. For `TestOverride`, this returns the current
274/// top-level attributes reflecting the active overrides.
275fn layer_attrs(l: &Layer) -> &Attrs {
276    match l {
277        Layer::File(a) | Layer::Env(a) | Layer::Runtime(a) | Layer::ClientOverride(a) => a,
278        Layer::TestOverride { attrs, .. } => attrs,
279    }
280}
281
282/// Return a mutable reference to the [`Attrs`] contained in a
283/// [`Layer`].
284///
285/// This allows callers to modify configuration values within any
286/// layer without needing to pattern-match on its variant. For
287/// `TestOverride`, the returned [`Attrs`] always reflect the current
288/// top-of-stack overrides for each key.
289fn layer_attrs_mut(l: &mut Layer) -> &mut Attrs {
290    match l {
291        Layer::File(a) | Layer::Env(a) | Layer::Runtime(a) | Layer::ClientOverride(a) => a,
292        Layer::TestOverride { attrs, .. } => attrs,
293    }
294}
295
296impl Layers {
297    // Mutation methods:
298
299    /// Insert or replace a configuration layer for the given source.
300    ///
301    /// If a layer with the same [`Source`] already exists, its contents
302    /// are replaced with the provided `attrs`. Otherwise a new layer is
303    /// added. After insertion, layers are re-sorted so that
304    /// higher-priority sources (e.g. [`Source::TestOverride`],
305    /// [`Source::Env`]) appear before lower-priority ones
306    /// ([`Source::Runtime`], [`Source::File`]).
307    fn set(&mut self, source: Source, attrs: Attrs) {
308        if let Some(l) = self.ordered.iter_mut().find(|l| layer_source(l) == source) {
309            *layer_attrs_mut(l) = attrs;
310        } else {
311            self.ordered.push(make_layer(source, attrs));
312        }
313        self.ordered.sort_by_key(|l| priority(layer_source(l)));
314    }
315
316    /// Insert or update a configuration layer for the given [`Source`].
317    ///
318    /// If a layer with the same [`Source`] already exists, its attributes
319    /// are **updated in place**: all keys present in `attrs` are absorbed
320    /// into the existing layer, overwriting any previous values for those
321    /// keys while leaving all other keys in that layer unchanged.
322    ///
323    /// If no layer for `source` exists yet, this behaves like [`set`]: a
324    /// new layer is created with the provided `attrs`.
325    fn merge(&mut self, source: Source, attrs: Attrs) {
326        if let Some(layer) = self.ordered.iter_mut().find(|l| layer_source(l) == source) {
327            layer_attrs_mut(layer).merge(attrs);
328        } else {
329            self.ordered.push(make_layer(source, attrs));
330        }
331        self.ordered.sort_by_key(|l| priority(layer_source(l)));
332    }
333
334    /// Remove the configuration layer for the given [`Source`], if
335    /// present.
336    ///
337    /// After this call, values from that source will no longer contribute
338    /// to resolution in [`get`], [`get_cloned`], or [`attrs`]. Defaults
339    /// and any remaining layers continue to apply in their normal
340    /// priority order.
341    fn clear(&mut self, source: Source) {
342        self.ordered.retain(|l| layer_source(l) != source);
343    }
344
345    /// Reset the global configuration to only Defaults (for testing).
346    ///
347    /// This clears all explicit layers (`File`, `Env`, `Runtime`,
348    /// `ClientOverride`, and `TestOverride`). Subsequent lookups will
349    /// resolve keys entirely from their declared defaults.
350    fn reset(&mut self) {
351        self.ordered.clear();
352    }
353
354    // Read methods:
355
356    /// Return a complete, merged snapshot of the effective configuration
357    /// **(only keys marked with `@meta(CONFIG = ...)`)**.
358    ///
359    /// Resolution per key:
360    /// 1) First explicit value found in layers (TestOverride → Env →
361    ///    Runtime → File → ClientOverride).
362    /// 2) Otherwise, the key's default (if any).
363    fn materialize(&self) -> Attrs {
364        let mut merged = Attrs::new();
365        for info in inventory::iter::<AttrKeyInfo>() {
366            if info.meta.get(CONFIG).is_none() {
367                continue;
368            }
369            let name = info.name;
370            let mut chosen: Option<Box<dyn crate::attrs::SerializableValue>> = None;
371            for layer in &self.ordered {
372                if let Some(v) = layer_attrs(layer).get_value_by_name(name) {
373                    chosen = Some(v.cloned());
374                    break;
375                }
376            }
377            let boxed = match chosen {
378                Some(b) => b,
379                None => {
380                    if let Some(default) = info.default {
381                        default.cloned()
382                    } else {
383                        continue;
384                    }
385                }
386            };
387            merged.insert_value_by_name_unchecked(name, boxed);
388        }
389        merged
390    }
391
392    /// Return a complete, merged snapshot of the effective configuration
393    /// **(only keys marked with `@meta(CONFIG = ...)` and `propagate: true`)**.
394    ///
395    /// This is similar to [`materialize`] but excludes keys with
396    /// `propagate: false`. Use this when sending config to child processes
397    /// via `BootstrapProcManager`.
398    fn materialize_propagatable(&self) -> Attrs {
399        let mut merged = Attrs::new();
400        for info in inventory::iter::<AttrKeyInfo>() {
401            let Some(cfg_meta) = info.meta.get(CONFIG) else {
402                continue;
403            };
404            if !cfg_meta.propagate {
405                continue;
406            }
407            let name = info.name;
408            let mut chosen: Option<Box<dyn crate::attrs::SerializableValue>> = None;
409            for layer in &self.ordered {
410                if let Some(v) = layer_attrs(layer).get_value_by_name(name) {
411                    chosen = Some(v.cloned());
412                    break;
413                }
414            }
415            let boxed = match chosen {
416                Some(b) => b,
417                None => {
418                    if let Some(default) = info.default {
419                        default.cloned()
420                    } else {
421                        continue;
422                    }
423                }
424            };
425            merged.insert_value_by_name_unchecked(name, boxed);
426        }
427        merged
428    }
429
430    /// Return a snapshot of the attributes for a specific configuration
431    /// source.
432    ///
433    /// If a layer with the given [`Source`] exists, this clones and
434    /// returns its [`Attrs`]. Otherwise an empty [`Attrs`] is returned.
435    fn layer_attrs_for(&self, source: Source) -> Attrs {
436        if let Some(layer) = self.ordered.iter().find(|l| layer_source(l) == source) {
437            layer_attrs(layer).clone()
438        } else {
439            Attrs::new()
440        }
441    }
442
443    // Test override support:
444
445    /// Ensure TestOverride layer exists, return mutable access to its
446    /// attrs and stacks.
447    fn ensure_test_override(&mut self) -> (&mut Attrs, &mut HashMap<&'static str, OverrideStack>) {
448        let idx = if let Some(i) = self
449            .ordered
450            .iter()
451            .position(|l| matches!(l, Layer::TestOverride { .. }))
452        {
453            i
454        } else {
455            self.ordered.push(Layer::TestOverride {
456                attrs: Attrs::new(),
457                stacks: HashMap::new(),
458            });
459            self.ordered.sort_by_key(|l| priority(layer_source(l)));
460            self.ordered
461                .iter()
462                .position(|l| matches!(l, Layer::TestOverride { .. }))
463                .expect("just inserted TestOverride layer")
464        };
465        match &mut self.ordered[idx] {
466            Layer::TestOverride { attrs, stacks } => (attrs, stacks),
467            _ => unreachable!(),
468        }
469    }
470
471    /// Get mutable access to TestOverride layer if it exists.
472    fn test_override_mut(
473        &mut self,
474    ) -> Option<(&mut Attrs, &mut HashMap<&'static str, OverrideStack>)> {
475        let idx = self
476            .ordered
477            .iter()
478            .position(|l| matches!(l, Layer::TestOverride { .. }))?;
479        match &mut self.ordered[idx] {
480            Layer::TestOverride { attrs, stacks } => Some((attrs, stacks)),
481            _ => None,
482        }
483    }
484
485    /// Remove the TestOverride layer entirely.
486    fn remove_test_override(&mut self) {
487        self.ordered
488            .retain(|l| !matches!(l, Layer::TestOverride { .. }));
489    }
490}
491
492/// Global configuration state combining layers and materialized
493/// snapshot.
494///
495/// This struct holds both the mutable layer stack (protected by
496/// RwLock) and the pre-materialized snapshot (in ArcSwap) together to
497/// avoid initialization order dependencies.
498struct GlobalConfig {
499    /// The layered configuration store.
500    layers: RwLock<Layers>,
501    /// Pre-materialized snapshot for lock-free reads.
502    materialized: ArcSwap<Attrs>,
503}
504
505/// Global layered configuration store.
506///
507/// This is the single authoritative store for configuration in the
508/// process. It is always present, protected by an `RwLock`, and holds
509/// a [`Layers`] struct containing all active sources.
510///
511/// On startup it is seeded with a single [`Source::Env`] layer
512/// (values loaded from process environment variables). Additional
513/// layers can be installed later via [`set`] or cleared with
514/// [`clear`]. Reads (`get`, `get_cloned`, `attrs`) consult the layers
515/// in priority order.
516///
517/// In tests, a [`Source::TestOverride`] layer is pushed on demand by
518/// [`ConfigLock::override_key`]. This layer always takes precedence
519/// and is automatically removed when the guard drops.
520///
521/// In normal operation, a parent process may capture its config with
522/// [`attrs`] and pass it to a child during bootstrap. The child
523/// installs this snapshot as its [`Source::ClientOverride`] layer,
524/// which has the lowest precedence among explicit layers.
525static GLOBAL: LazyLock<GlobalConfig> = LazyLock::new(|| {
526    let env = from_env();
527    let layers = Layers {
528        ordered: vec![Layer::Env(env)],
529    };
530    let materialized = ArcSwap::new(Arc::new(layers.materialize()));
531    GlobalConfig {
532        layers: RwLock::new(layers),
533        materialized,
534    }
535});
536
537/// Update the materialized snapshot from the current layers.
538///
539/// Must be called while holding `GLOBAL.layers.write()` to ensure the
540/// snapshot is consistent with the layers.
541fn rematerialize(layers: &Layers) {
542    GLOBAL.materialized.store(Arc::new(layers.materialize()));
543}
544
545/// Monotonically increasing sequence used to assign unique tokens to
546/// each test override frame.
547///
548/// Tokens identify individual [`ConfigValueGuard`] instances within a
549/// key's override stack, allowing frames to be removed safely even
550/// when guards are dropped out of order. The counter starts at 1 and
551/// uses relaxed atomic ordering since exact sequencing across threads
552/// is not required—only uniqueness.
553static OVERRIDE_TOKEN_SEQ: AtomicU64 = AtomicU64::new(1);
554
555/// Acquire the global configuration lock.
556///
557/// This lock serializes all mutations of the global configuration,
558/// ensuring they cannot clobber each other. It returns a
559/// [`ConfigLock`] guard, which must be held for the duration of any
560/// mutation (e.g. inserting or overriding values).
561///
562/// Most commonly used in tests, where it provides exclusive access to
563/// push a [`Source::TestOverride`] layer via
564/// [`ConfigLock::override_key`]. The override layer is automatically
565/// removed when the guard drops, restoring the original state.
566///
567/// # Example
568/// ```rust,ignore
569/// let lock = hyperactor::config::global::lock();
570/// let _guard = lock.override_key(CONFIG_KEY, "test_value");
571/// // Code under test sees the overridden config.
572/// // On drop, the key is restored.
573/// ```
574pub fn lock() -> ConfigLock {
575    static MUTEX: LazyLock<std::sync::Mutex<()>> = LazyLock::new(|| std::sync::Mutex::new(()));
576    ConfigLock {
577        _guard: MUTEX.lock().unwrap_or_else(|e| e.into_inner()),
578    }
579}
580
581/// Initialize the global configuration from environment variables.
582///
583/// Reads values from process environment variables, using each key's
584/// `CONFIG.env_name` (from `@meta(CONFIG = ConfigAttr { … })`) to
585/// determine its mapping. The resulting values are installed as the
586/// [`Source::Env`] layer. Keys without a corresponding environment
587/// variable fall back to lower-priority sources or defaults.
588///
589/// Typically invoked once at process startup to overlay config values
590/// from the environment. Repeated calls replace the existing Env
591/// layer.
592pub fn init_from_env() {
593    set(Source::Env, from_env());
594}
595
596/// Initialize the global configuration from a YAML file.
597///
598/// Loads values from the specified YAML file and installs them as the
599/// [`Source::File`] layer. During resolution, File is consulted after
600/// TestOverride, Env, and Runtime layers, and before ClientOverride
601/// and defaults.
602///
603/// Typically invoked once at process startup to provide a baseline
604/// configuration. Repeated calls replace the existing File layer.
605pub fn init_from_yaml<P: AsRef<Path>>(path: P) -> Result<(), anyhow::Error> {
606    let file = from_yaml(path)?;
607    set(Source::File, file);
608    Ok(())
609}
610
611/// Get a key from the global configuration (Copy types).
612///
613/// Resolution order: TestOverride -> Env -> Runtime -> File ->
614/// ClientOverride -> Default. Panics if the key has no default and is
615/// not set in any layer.
616///
617/// This function reads from a pre-materialized snapshot for lock-free
618/// performance. The snapshot is updated atomically whenever layers
619/// change.
620pub fn get<T: AttrValue + Copy>(key: Key<T>) -> T {
621    let snapshot = GLOBAL.materialized.load();
622    *snapshot.get(key).expect("key must have a default")
623}
624
625/// Return the override value for `key` if it is explicitly present in
626/// `overrides`, otherwise fall back to the global value for that key.
627pub fn override_or_global<T: AttrValue + Copy>(overrides: &Attrs, key: Key<T>) -> T {
628    if overrides.contains_key(key) {
629        *overrides.get(key).unwrap()
630    } else {
631        get(key)
632    }
633}
634
635/// Get a key by cloning the value.
636///
637/// Resolution order: TestOverride -> Env -> Runtime -> File ->
638/// ClientOverride -> Default. Panics if the key has no default and
639/// is not set in any layer.
640pub fn get_cloned<T: AttrValue>(key: Key<T>) -> T {
641    try_get_cloned(key)
642        .expect("key must have a default")
643        .clone()
644}
645
646/// Try to get a key by cloning the value.
647///
648/// Resolution order: TestOverride -> Env -> Runtime -> File ->
649/// ClientOverride -> Default. Returns None if the key has no default
650/// and is not set in any layer.
651///
652/// This function reads from a pre-materialized snapshot for lock-free
653/// performance.
654pub fn try_get_cloned<T: AttrValue>(key: Key<T>) -> Option<T> {
655    let snapshot = GLOBAL.materialized.load();
656    snapshot.get(key).cloned()
657}
658
659/// Construct a [`Layer`] for the given [`Source`] using the provided
660/// `attrs`.
661///
662/// Used by [`set`] and [`create_or_merge`] when installing a new
663/// configuration layer.
664fn make_layer(source: Source, attrs: Attrs) -> Layer {
665    match source {
666        Source::File => Layer::File(attrs),
667        Source::Env => Layer::Env(attrs),
668        Source::Runtime => Layer::Runtime(attrs),
669        Source::TestOverride => Layer::TestOverride {
670            attrs,
671            stacks: HashMap::new(),
672        },
673        Source::ClientOverride => Layer::ClientOverride(attrs),
674        Source::Default => {
675            // Default is a reporting-only source used in ConfigEntry;
676            // it does not correspond to a real layer.
677            unreachable!("Source::Default is not a real layer — it cannot be used in make_layer")
678        }
679    }
680}
681
682/// Insert or replace a configuration layer for the given source.
683///
684/// If a layer with the same [`Source`] already exists, its contents
685/// are replaced with the provided `attrs`. Otherwise a new layer is
686/// added. After insertion, layers are re-sorted so that
687/// higher-priority sources (e.g. [`Source::TestOverride`],
688/// [`Source::Env`]) appear before lower-priority ones
689/// ([`Source::Runtime`], [`Source::File`]).
690///
691/// This function is used by initialization routines (e.g.
692/// `init_from_env`, `init_from_yaml`) and by tests when overriding
693/// configuration values.
694pub fn set(source: Source, attrs: Attrs) {
695    let mut g = GLOBAL.layers.write().unwrap();
696    g.set(source, attrs);
697    rematerialize(&g);
698}
699
700/// Insert or update a configuration layer for the given [`Source`].
701///
702/// If a layer with the same [`Source`] already exists, its attributes
703/// are **updated in place**: all keys present in `attrs` are absorbed
704/// into the existing layer, overwriting any previous values for those
705/// keys while leaving all other keys in that layer unchanged.
706///
707/// If no layer for `source` exists yet, this behaves like [`set`]: a
708/// new layer is created with the provided `attrs`.
709///
710/// This is useful for incremental / additive updates (for example,
711/// runtime configuration driven by a Python API), where callers want
712/// to change a subset of keys without discarding previously installed
713/// values in the same layer.
714///
715/// By contrast, [`set`] replaces the entire layer for `source` with
716/// `attrs`, discarding any existing values in that layer.
717pub fn create_or_merge(source: Source, attrs: Attrs) {
718    let mut g = GLOBAL.layers.write().unwrap();
719    g.merge(source, attrs);
720    rematerialize(&g);
721}
722
723/// Remove the configuration layer for the given [`Source`], if
724/// present.
725///
726/// After this call, values from that source will no longer contribute
727/// to resolution in [`get`], [`get_cloned`], or [`attrs`]. Defaults
728/// and any remaining layers continue to apply in their normal
729/// priority order.
730pub fn clear(source: Source) {
731    let mut g = GLOBAL.layers.write().unwrap();
732    g.clear(source);
733    rematerialize(&g);
734}
735
736/// Return a complete, merged snapshot of the effective configuration
737/// **(only keys marked with `@meta(CONFIG = ...)`)**.
738///
739/// Resolution per key:
740/// 1) First explicit value found in layers (TestOverride → Env →
741///    Runtime → File → ClientOverride).
742/// 2) Otherwise, the key's default (if any).
743///
744/// Notes:
745/// - This materializes defaults into the returned Attrs for all
746///   CONFIG-marked keys, so it's self-contained.
747/// - Keys without `CONFIG` meta are excluded.
748pub fn attrs() -> Attrs {
749    GLOBAL.layers.read().unwrap().materialize()
750}
751
752/// Return a complete, merged snapshot of the effective configuration
753/// **(only keys marked with `@meta(CONFIG = ...)` and `propagate: true`)**.
754///
755/// Resolution per key:
756/// 1) First explicit value found in layers (TestOverride → Env →
757///    Runtime → File → ClientOverride).
758/// 2) Otherwise, the key's default (if any).
759///
760/// Notes:
761/// - This materializes defaults into the returned Attrs for all
762///   CONFIG-marked keys with `propagate: true`.
763/// - Keys without `CONFIG` meta are excluded.
764/// - Keys with `propagate: false` are excluded.
765///
766/// Use this when sending config to child processes via
767/// `BootstrapProcManager`. Process-local configs (like TLS cert paths)
768/// should have `propagate: false` and will not be included.
769pub fn propagatable_attrs() -> Attrs {
770    GLOBAL.layers.read().unwrap().materialize_propagatable()
771}
772
773/// A single config entry with its resolved value and provenance.
774///
775/// Used by the admin config-inspection endpoint to report per-proc
776/// configuration state including which layer the effective value
777/// came from.
778#[derive(Debug, Clone, Serialize, Deserialize)]
779pub struct ConfigEntry {
780    /// Fully qualified key name (e.g. `hyperactor::config::codec_max_frame_length`).
781    pub name: String,
782    /// Display representation of the resolved value.
783    pub value: String,
784    /// Display representation of the declared default, if any.
785    pub default_value: Option<String>,
786    /// Which layer the resolved value came from.
787    pub source: Source,
788    /// True when the resolved display value differs from the declared default.
789    pub changed_from_default: bool,
790    /// Environment variable name, if the key declares one.
791    pub env_var: Option<String>,
792}
793
794/// Snapshot all CONFIG-marked keys with their resolved values and
795/// source layers.
796///
797/// Iterates the global key registry, filters to `@meta(CONFIG = ...)`
798/// keys, and for each key walks layers top-to-bottom to find which
799/// layer provides the effective value. Results are sorted by name.
800pub fn config_entries() -> Vec<ConfigEntry> {
801    let g = GLOBAL.layers.read().unwrap();
802    let mut entries = Vec::new();
803
804    for info in inventory::iter::<AttrKeyInfo>() {
805        let Some(cfg_meta) = info.meta.get(CONFIG) else {
806            continue;
807        };
808
809        let name = info.name;
810
811        // Walk layers to find the effective value and its source.
812        let mut chosen_value: Option<Box<dyn crate::attrs::SerializableValue>> = None;
813        let mut chosen_source: Option<Source> = None;
814        for layer in &g.ordered {
815            if let Some(v) = layer_attrs(layer).get_value_by_name(name) {
816                chosen_value = Some(v.cloned());
817                chosen_source = Some(layer_source(layer));
818                break;
819            }
820        }
821
822        // Fall back to declared default if no layer provides a value.
823        let (value_str, source) = match chosen_value {
824            Some(v) => ((info.display)(&*v), chosen_source.unwrap()),
825            None => {
826                if let Some(default) = info.default {
827                    ((info.display)(default), Source::Default)
828                } else {
829                    continue;
830                }
831            }
832        };
833
834        // Compute default display string for changed_from_default comparison.
835        let default_str = info.default.map(|d| (info.display)(d));
836        let changed_from_default = match &default_str {
837            Some(ds) => value_str != *ds,
838            None => true, // no declared default → always "changed"
839        };
840
841        entries.push(ConfigEntry {
842            name: name.to_string(),
843            value: value_str,
844            default_value: default_str,
845            source,
846            changed_from_default,
847            env_var: cfg_meta.env_name.clone(),
848        });
849    }
850
851    entries.sort_by(|a, b| a.name.cmp(&b.name));
852    entries
853}
854
855/// Return a snapshot of the attributes for a specific configuration
856/// source.
857///
858/// If a layer with the given [`Source`] exists, this clones and
859/// returns its [`Attrs`]. Otherwise an empty [`Attrs`] is returned.
860/// The returned map is detached from the global store – mutating it
861/// does **not** affect the underlying layer; use [`set`] or
862/// [`create_or_merge`] to modify layers.
863fn layer_attrs_for(source: Source) -> Attrs {
864    GLOBAL.layers.read().unwrap().layer_attrs_for(source)
865}
866
867/// Snapshot the current attributes in the **Runtime** configuration
868/// layer.
869///
870/// This returns a cloned [`Attrs`] containing only values explicitly
871/// set in the [`Source::Runtime`] layer (no merging with
872/// Env/File/Defaults). If no Runtime layer is present, an empty
873/// [`Attrs`] is returned.
874pub fn runtime_attrs() -> Attrs {
875    layer_attrs_for(Source::Runtime)
876}
877
878/// Reset the global configuration to only Defaults (for testing).
879///
880/// This clears all explicit layers (`File`, `Env`, `Runtime`,
881/// `ClientOverride`, and `TestOverride`). Subsequent lookups will
882/// resolve keys entirely from their declared defaults.
883///
884/// Note: Should be called while holding [`global::lock`] in tests, to
885/// ensure no concurrent modifications happen.
886pub fn reset_to_defaults() {
887    let mut g = GLOBAL.layers.write().unwrap();
888    g.reset();
889    rematerialize(&g);
890}
891
892/// A guard that holds the global configuration lock and provides
893/// override functionality.
894///
895/// This struct acts as both a lock guard (preventing other tests from
896/// modifying global config) and as the only way to create
897/// configuration overrides. Override guards cannot outlive this
898/// ConfigLock, ensuring proper synchronization.
899pub struct ConfigLock {
900    _guard: std::sync::MutexGuard<'static, ()>,
901}
902
903impl ConfigLock {
904    /// Create a configuration override that is active until the
905    /// returned guard is dropped.
906    ///
907    /// Each call pushes a new frame onto a per-key override stack
908    /// within the [`Source::TestOverride`] layer. The topmost frame
909    /// defines the effective value seen by `get()` and in the
910    /// mirrored environment variable (if any). When a guard is
911    /// dropped, its frame is removed: if it was the top, the previous
912    /// frame (if any) becomes active or the key and env var are
913    /// restored to their prior state.
914    ///
915    /// The returned guard must not outlive this [`ConfigLock`].
916    pub fn override_key<'a, T: AttrValue>(
917        &'a self,
918        key: crate::attrs::Key<T>,
919        value: T,
920    ) -> ConfigValueGuard<'a, T> {
921        let token = OVERRIDE_TOKEN_SEQ.fetch_add(1, Ordering::Relaxed);
922
923        let mut g = GLOBAL.layers.write().unwrap();
924
925        {
926            // Ensure TestOverride layer exists and get mutable access.
927            let (attrs, stacks) = g.ensure_test_override();
928
929            // Compute env var (if any) for this key once.
930            let (env_var, env_str) = if let Some(cfg) = key.attrs().get(crate::CONFIG) {
931                if let Some(name) = &cfg.env_name {
932                    (Some(name.clone()), value.display())
933                } else {
934                    (None, String::new())
935                }
936            } else {
937                (None, String::new())
938            };
939
940            // Get per-key stack (by declared name).
941            let key_name = key.name();
942            let stack = stacks.entry(key_name).or_insert_with(|| OverrideStack {
943                env_var: env_var.clone(),
944                saved_env: env_var.as_ref().and_then(|n| std::env::var(n).ok()),
945                frames: Vec::new(),
946            });
947
948            // Push the new frame.
949            let boxed: Box<dyn crate::attrs::SerializableValue> = Box::new(value.clone());
950            stack.frames.push(OverrideFrame {
951                token,
952                value: boxed,
953                env_str,
954            });
955
956            // Make this frame the active value in TestOverride attrs.
957            attrs.set(key, value.clone());
958
959            // Update process env to reflect new top-of-stack.
960            if let (Some(var), Some(top)) = (stack.env_var.as_ref(), stack.frames.last()) {
961                // SAFETY: Under global ConfigLock during tests.
962                unsafe { std::env::set_var(var, &top.env_str) }
963            }
964        }
965
966        rematerialize(&g);
967
968        ConfigValueGuard {
969            key,
970            token,
971            _phantom: PhantomData,
972        }
973    }
974}
975
976/// When a [`ConfigLock`] is dropped, the special
977/// [`Source::TestOverride`] layer (if present) is removed entirely.
978/// This discards all temporary overrides created under the lock,
979/// ensuring they cannot leak into subsequent tests or callers. Other
980/// layers (`Runtime`, `Env`, `File`, `ClientOverride`, and defaults)
981/// are left untouched.
982///
983/// Note: individual values within the TestOverride layer may already
984/// have been restored by [`ConfigValueGuard`]s as they drop. This
985/// final removal guarantees no residual layer remains once the lock
986/// itself is released.
987impl Drop for ConfigLock {
988    fn drop(&mut self) {
989        let mut g = GLOBAL.layers.write().unwrap();
990        g.remove_test_override();
991        rematerialize(&g);
992    }
993}
994
995/// A guard that restores a single configuration value when dropped.
996pub struct ConfigValueGuard<'a, T: 'static> {
997    key: crate::attrs::Key<T>,
998    token: u64,
999    // This is here so we can hold onto a 'a lifetime.
1000    _phantom: PhantomData<&'a ()>,
1001}
1002
1003/// When a [`ConfigValueGuard`] is dropped, it restores configuration
1004/// state for the key it was guarding.
1005///
1006/// Behavior:
1007/// - Each key maintains a stack of override frames. The most recent
1008///   frame (top of stack) defines the effective value in
1009///   [`Source::TestOverride`].
1010/// - Dropping a guard removes its frame. If it was the top frame, the
1011///   next frame (if any) becomes active and both the config and
1012///   mirrored env var are updated accordingly.
1013/// - If the dropped frame was not on top, no changes occur until the
1014///   active frame is dropped.
1015/// - When the last frame for a key is removed, the key is deleted
1016///   from the TestOverride layer and its associated environment
1017///   variable (if any) is restored to its original value or removed
1018///   if it did not exist.
1019///
1020/// This guarantees that nested or out-of-order test overrides are
1021/// restored deterministically and without leaking state into
1022/// subsequent tests.
1023impl<T: 'static> Drop for ConfigValueGuard<'_, T> {
1024    fn drop(&mut self) {
1025        let mut g = GLOBAL.layers.write().unwrap();
1026
1027        // Track whether the config actually changed (for rematerialization).
1028        let mut config_changed = false;
1029
1030        // Env var restoration info (captured inside the block, applied
1031        // outside).
1032        let mut restore_env_var: Option<String> = None;
1033        let mut restore_env_to: Option<String> = None;
1034
1035        if let Some((attrs, stacks)) = g.test_override_mut() {
1036            let key_name = self.key.name();
1037
1038            // We need a tiny scope for the &mut borrow of the stack so
1039            // we can call `stacks.remove(key_name)` afterward if it
1040            // becomes empty.
1041            let mut remove_empty_stack = false;
1042
1043            if let Some(stack) = stacks.get_mut(key_name) {
1044                // Find this guard's frame by token.
1045                if let Some(pos) = stack.frames.iter().position(|f| f.token == self.token) {
1046                    let is_top = pos + 1 == stack.frames.len();
1047
1048                    if is_top {
1049                        // Pop the active frame
1050                        stack.frames.pop();
1051                        config_changed = true;
1052
1053                        if let Some(new_top) = stack.frames.last() {
1054                            // New top becomes active: update attrs and env.
1055                            attrs.insert_value(self.key, (*new_top.value).cloned());
1056                            if let Some(var) = stack.env_var.as_ref() {
1057                                // SAFETY: Under global ConfigLock during tests.
1058                                unsafe { std::env::set_var(var, &new_top.env_str) }
1059                            }
1060                        } else {
1061                            // Stack empty: remove the key now, then after
1062                            // releasing the &mut borrow of the stack,
1063                            // restore the env var and remove the stack
1064                            // entry.
1065                            let _ = attrs.remove_value(self.key);
1066
1067                            // Capture restoration details while we still
1068                            // have access to the stack.
1069                            if let Some(var) = stack.env_var.as_ref() {
1070                                restore_env_var = Some(var.clone());
1071                                restore_env_to = stack.saved_env.clone(); // None => unset
1072                            }
1073                            remove_empty_stack = true
1074                        }
1075                    } else {
1076                        // Out-of-order drop: remove only that frame:
1077                        // active top stays
1078                        stack.frames.remove(pos);
1079                        // No changes to attrs or env here (and no
1080                        // rematerialization needed).
1081                    }
1082                } // else: token already handled; nothing to do
1083            } // &mut stack borrow ends here
1084
1085            // If we emptied the stack for this key, remove the stack
1086            // entry.
1087            if remove_empty_stack {
1088                let _ = stacks.remove(key_name);
1089            }
1090        }
1091
1092        // Restore env var outside the borrow scope.
1093        if let Some(var) = restore_env_var.as_ref() {
1094            // SAFETY: Under global ConfigLock during tests.
1095            unsafe {
1096                if let Some(val) = restore_env_to.as_ref() {
1097                    std::env::set_var(var, val);
1098                } else {
1099                    std::env::remove_var(var);
1100                }
1101            }
1102        }
1103
1104        // Rematerialize if the config actually changed.
1105        if config_changed {
1106            rematerialize(&g);
1107        }
1108    }
1109}
1110
1111#[cfg(test)]
1112mod tests {
1113    use std::time::Duration;
1114
1115    use super::*;
1116    use crate::ConfigAttr;
1117    use crate::attrs::declare_attrs;
1118
1119    // Test configuration keys used to exercise the layered config
1120    // infrastructure. These mirror hyperactor's config keys but are
1121    // declared locally to keep hyperactor_config independent.
1122
1123    declare_attrs! {
1124        /// Maximum frame length for codec
1125        @meta(CONFIG = ConfigAttr::new(
1126            Some("HYPERACTOR_CODEC_MAX_FRAME_LENGTH".to_string()),
1127            None,
1128        ))
1129        pub attr CODEC_MAX_FRAME_LENGTH: usize = 10 * 1024 * 1024 * 1024; // 10 GiB
1130
1131        /// Message delivery timeout
1132        @meta(CONFIG = ConfigAttr::new(
1133            Some("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT".to_string()),
1134            None,
1135        ))
1136        pub attr MESSAGE_DELIVERY_TIMEOUT: Duration = Duration::from_secs(30);
1137
1138        /// Number of messages after which to send an acknowledgment
1139        @meta(CONFIG = ConfigAttr::new(
1140            Some("HYPERACTOR_MESSAGE_ACK_EVERY_N_MESSAGES".to_string()),
1141            None,
1142        ))
1143        pub attr MESSAGE_ACK_EVERY_N_MESSAGES: u64 = 1000;
1144
1145        /// Maximum buffer size for split port messages
1146        @meta(CONFIG = ConfigAttr::new(
1147            Some("HYPERACTOR_SPLIT_MAX_BUFFER_SIZE".to_string()),
1148            None,
1149        ))
1150        pub attr SPLIT_MAX_BUFFER_SIZE: usize = 5;
1151
1152        /// Whether to use multipart encoding for network channel communications
1153        @meta(CONFIG = ConfigAttr::new(
1154            Some("HYPERACTOR_CHANNEL_MULTIPART".to_string()),
1155            None,
1156        ))
1157        pub attr CHANNEL_MULTIPART: bool = true;
1158
1159        /// Default hop Time-To-Live for message envelopes
1160        @meta(CONFIG = ConfigAttr::new(
1161            Some("HYPERACTOR_MESSAGE_TTL_DEFAULT".to_string()),
1162            None,
1163        ))
1164        pub attr MESSAGE_TTL_DEFAULT: u8 = 64;
1165
1166        /// A test key with no environment variable mapping
1167        @meta(CONFIG = ConfigAttr::new(
1168            None,
1169            None,
1170        ))
1171        pub attr CONFIG_KEY_NO_ENV: u32 = 100;
1172    }
1173
1174    #[test]
1175    fn test_global_config() {
1176        let config = lock();
1177
1178        // Reset global config to defaults to avoid interference from
1179        // other tests
1180        reset_to_defaults();
1181
1182        assert_eq!(get(CODEC_MAX_FRAME_LENGTH), CODEC_MAX_FRAME_LENGTH_DEFAULT);
1183        {
1184            let _guard = config.override_key(CODEC_MAX_FRAME_LENGTH, 1024);
1185            assert_eq!(get(CODEC_MAX_FRAME_LENGTH), 1024);
1186            // The configuration will be automatically restored when
1187            // _guard goes out of scope
1188        }
1189
1190        assert_eq!(get(CODEC_MAX_FRAME_LENGTH), CODEC_MAX_FRAME_LENGTH_DEFAULT);
1191    }
1192
1193    #[test]
1194    fn test_overrides() {
1195        let config = lock();
1196
1197        // Reset global config to defaults to avoid interference from
1198        // other tests
1199        reset_to_defaults();
1200
1201        // Test the new lock/override API for individual config values
1202        assert_eq!(get(CODEC_MAX_FRAME_LENGTH), CODEC_MAX_FRAME_LENGTH_DEFAULT);
1203        assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(30));
1204
1205        // Test single value override
1206        {
1207            let _guard = config.override_key(CODEC_MAX_FRAME_LENGTH, 2048);
1208            assert_eq!(get(CODEC_MAX_FRAME_LENGTH), 2048);
1209            assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(30)); // Unchanged
1210        }
1211
1212        // Values should be restored after guard is dropped
1213        assert_eq!(get(CODEC_MAX_FRAME_LENGTH), CODEC_MAX_FRAME_LENGTH_DEFAULT);
1214
1215        // Test multiple overrides
1216        let orig_value = std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").ok();
1217        {
1218            let _guard1 = config.override_key(CODEC_MAX_FRAME_LENGTH, 4096);
1219            let _guard2 = config.override_key(MESSAGE_DELIVERY_TIMEOUT, Duration::from_mins(1));
1220
1221            assert_eq!(get(CODEC_MAX_FRAME_LENGTH), 4096);
1222            assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_mins(1));
1223            // This was overridden:
1224            assert_eq!(
1225                std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").unwrap(),
1226                "1m"
1227            );
1228        }
1229        assert_eq!(
1230            std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").ok(),
1231            orig_value
1232        );
1233
1234        // All values should be restored
1235        assert_eq!(get(CODEC_MAX_FRAME_LENGTH), CODEC_MAX_FRAME_LENGTH_DEFAULT);
1236        assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(30));
1237    }
1238
1239    #[test]
1240    fn test_layer_precedence_env_over_file_and_replacement() {
1241        let _lock = lock();
1242        reset_to_defaults();
1243
1244        // File sets a value.
1245        let mut file = Attrs::new();
1246        file[CODEC_MAX_FRAME_LENGTH] = 1111;
1247        set(Source::File, file);
1248
1249        // Env sets a different value.
1250        let mut env = Attrs::new();
1251        env[CODEC_MAX_FRAME_LENGTH] = 2222;
1252        set(Source::Env, env);
1253
1254        // Env should win over File.
1255        assert_eq!(get(CODEC_MAX_FRAME_LENGTH), 2222);
1256
1257        // Replace Env layer with a new value.
1258        let mut env2 = Attrs::new();
1259        env2[CODEC_MAX_FRAME_LENGTH] = 3333;
1260        set(Source::Env, env2);
1261
1262        assert_eq!(get(CODEC_MAX_FRAME_LENGTH), 3333);
1263    }
1264
1265    #[test]
1266    fn test_layer_precedence_read_file_if_not_found_in_env() {
1267        let _lock = lock();
1268        reset_to_defaults();
1269
1270        // Read the default value because no layers have been set.
1271        assert_eq!(get(CODEC_MAX_FRAME_LENGTH), 10737418240);
1272
1273        // File sets a value.
1274        let mut file = Attrs::new();
1275        file[CODEC_MAX_FRAME_LENGTH] = 1111;
1276        set(Source::File, file);
1277
1278        // Env does not have any attribute.
1279        let env = Attrs::new();
1280        set(Source::Env, env);
1281
1282        // Should read from File.
1283        assert_eq!(get(CODEC_MAX_FRAME_LENGTH), 1111);
1284
1285        // Replace Env layer with a new value.
1286        let mut env2 = Attrs::new();
1287        env2[CODEC_MAX_FRAME_LENGTH] = 2222;
1288        set(Source::Env, env2);
1289
1290        // Env should win over File.
1291        assert_eq!(get(CODEC_MAX_FRAME_LENGTH), 2222);
1292    }
1293
1294    #[test]
1295    fn test_runtime_overrides_and_clear_restores_lower_layers() {
1296        let _lock = lock();
1297        reset_to_defaults();
1298
1299        // File baseline.
1300        let mut file = Attrs::new();
1301        file[MESSAGE_DELIVERY_TIMEOUT] = Duration::from_secs(30);
1302        set(Source::File, file);
1303
1304        // Env override.
1305        let mut env = Attrs::new();
1306        env[MESSAGE_DELIVERY_TIMEOUT] = Duration::from_secs(40);
1307        set(Source::Env, env);
1308
1309        // Runtime layer (but Env beats it).
1310        let mut rt = Attrs::new();
1311        rt[MESSAGE_DELIVERY_TIMEOUT] = Duration::from_secs(50);
1312        set(Source::Runtime, rt);
1313
1314        assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(40));
1315
1316        // Clearing Env should reveal Runtime.
1317        clear(Source::Env);
1318
1319        // With the Env layer gone, Runtime wins over File.
1320        assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(50));
1321    }
1322
1323    #[test]
1324    fn test_attrs_snapshot_materializes_defaults_and_omits_meta() {
1325        let _lock = lock();
1326        reset_to_defaults();
1327
1328        // No explicit layers: values should come from Defaults.
1329        let snap = attrs();
1330
1331        // A few representative defaults are materialized:
1332        assert_eq!(snap[CODEC_MAX_FRAME_LENGTH], 10 * 1024 * 1024 * 1024);
1333        assert_eq!(snap[MESSAGE_DELIVERY_TIMEOUT], Duration::from_secs(30));
1334
1335        // CONFIG has no default and wasn't explicitly set: should be
1336        // omitted.
1337        let json = serde_json::to_string(&snap).unwrap();
1338        assert!(
1339            !json.contains("hyperactor::config::config"),
1340            "CONFIG must not appear in snapshot unless explicitly set"
1341        );
1342    }
1343
1344    #[test]
1345    fn test_parent_child_snapshot_as_clientoverride_layer() {
1346        let _lock = lock();
1347        reset_to_defaults();
1348
1349        // Parent effective config (pretend it's a parent process).
1350        let mut parent_env = Attrs::new();
1351        parent_env[MESSAGE_ACK_EVERY_N_MESSAGES] = 12345;
1352        set(Source::Env, parent_env);
1353
1354        let parent_snap = attrs();
1355
1356        // "Child" process: start clean, install parent snapshot as
1357        // ClientOverride.
1358        reset_to_defaults();
1359        set(Source::ClientOverride, parent_snap);
1360
1361        // Child should observe parent's effective value from the
1362        // ClientOverride layer (since child has no Env/Runtime/File
1363        // layers set).
1364        assert_eq!(get(MESSAGE_ACK_EVERY_N_MESSAGES), 12345);
1365    }
1366
1367    #[test]
1368    fn test_testoverride_layer_override_and_env_restore() {
1369        let lock = lock();
1370        reset_to_defaults();
1371
1372        assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(30));
1373
1374        // SAFETY: single-threaded test.
1375        unsafe {
1376            std::env::remove_var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT");
1377        }
1378
1379        {
1380            let _guard = lock.override_key(MESSAGE_DELIVERY_TIMEOUT, Duration::from_secs(99));
1381            // Override wins:
1382            assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(99));
1383
1384            // Env should be mirrored to the same duration (string may
1385            // be "1m 39s")
1386            let s = std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").unwrap();
1387            let parsed = humantime::parse_duration(&s).unwrap();
1388            assert_eq!(parsed, Duration::from_secs(99));
1389        }
1390
1391        // After drop, value and env restored:
1392        assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(30));
1393        assert!(std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").is_err());
1394    }
1395
1396    #[test]
1397    fn test_reset_to_defaults_clears_all_layers() {
1398        let _lock = lock();
1399        reset_to_defaults();
1400
1401        // Seed multiple layers.
1402        let mut file = Attrs::new();
1403        file[SPLIT_MAX_BUFFER_SIZE] = 7;
1404        set(Source::File, file);
1405
1406        let mut env = Attrs::new();
1407        env[SPLIT_MAX_BUFFER_SIZE] = 8;
1408        set(Source::Env, env);
1409
1410        let mut rt = Attrs::new();
1411        rt[SPLIT_MAX_BUFFER_SIZE] = 9;
1412        set(Source::Runtime, rt);
1413
1414        // Sanity: Env wins over Runtime and File.
1415        assert_eq!(get(SPLIT_MAX_BUFFER_SIZE), 8);
1416
1417        // Reset clears all explicit layers; defaults apply.
1418        reset_to_defaults();
1419        assert_eq!(get(SPLIT_MAX_BUFFER_SIZE), 5); // default
1420    }
1421
1422    #[test]
1423    fn test_get_cloned_resolution_matches_get() {
1424        let _lock = lock();
1425        reset_to_defaults();
1426
1427        let mut env = Attrs::new();
1428        env[MESSAGE_DELIVERY_TIMEOUT] = Duration::from_mins(2);
1429        set(Source::Env, env);
1430
1431        assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_mins(2));
1432        let v = get_cloned(MESSAGE_DELIVERY_TIMEOUT);
1433        assert_eq!(v, Duration::from_mins(2));
1434    }
1435
1436    #[test]
1437    fn test_attrs_snapshot_respects_layer_precedence_per_key() {
1438        let _lock = lock();
1439        reset_to_defaults();
1440
1441        let mut file = Attrs::new();
1442        file[MESSAGE_TTL_DEFAULT] = 10;
1443        set(Source::File, file);
1444
1445        let mut env = Attrs::new();
1446        env[MESSAGE_TTL_DEFAULT] = 20;
1447        set(Source::Env, env);
1448
1449        let snap = attrs();
1450        assert_eq!(snap[MESSAGE_TTL_DEFAULT], 20); // Env beats File
1451    }
1452
1453    declare_attrs! {
1454      @meta(CONFIG = ConfigAttr::new(
1455          None,
1456          None,
1457      ))
1458      pub attr CONFIG_KEY: bool = true;
1459
1460      pub attr NON_CONFIG_KEY: bool = true;
1461
1462      @meta(CONFIG = ConfigAttr::new(
1463          None,
1464          None,
1465      ).process_local())
1466      pub attr NON_PROPAGATE_KEY: bool = true;
1467    }
1468
1469    #[test]
1470    fn test_attrs_excludes_non_config_keys() {
1471        let _lock = lock();
1472        reset_to_defaults();
1473
1474        let snap = attrs();
1475        let json = serde_json::to_string(&snap).unwrap();
1476
1477        // Expect our CONFIG_KEY to be present.
1478        assert!(
1479            json.contains("hyperactor_config::global::tests::config_key"),
1480            "attrs() should include keys with @meta(CONFIG = ...)"
1481        );
1482        // Expect our NON_CONFIG_KEY to be omitted.
1483        assert!(
1484            !json.contains("hyperactor_config::global::tests::non_config_key"),
1485            "attrs() should exclude keys without @meta(CONFIG = ...)"
1486        );
1487    }
1488
1489    #[test]
1490    fn test_propagatable_attrs_excludes_non_propagate_keys() {
1491        let _lock = lock();
1492        reset_to_defaults();
1493
1494        // attrs() should include NON_PROPAGATE_KEY (it has CONFIG meta)
1495        let snap = attrs();
1496        let json = serde_json::to_string(&snap).unwrap();
1497        assert!(
1498            json.contains("hyperactor_config::global::tests::non_propagate_key"),
1499            "attrs() should include keys with propagate: false"
1500        );
1501
1502        // propagatable_attrs() should exclude NON_PROPAGATE_KEY
1503        let propagatable = propagatable_attrs();
1504        let json_propagatable = serde_json::to_string(&propagatable).unwrap();
1505        assert!(
1506            !json_propagatable.contains("hyperactor_config::global::tests::non_propagate_key"),
1507            "propagatable_attrs() should exclude keys with propagate: false"
1508        );
1509
1510        // propagatable_attrs() should still include CONFIG_KEY (propagate: true)
1511        assert!(
1512            json_propagatable.contains("hyperactor_config::global::tests::config_key"),
1513            "propagatable_attrs() should include keys with propagate: true"
1514        );
1515    }
1516
1517    #[test]
1518    fn test_testoverride_multiple_stacked_overrides_lifo() {
1519        let lock = lock();
1520        reset_to_defaults();
1521
1522        // Baseline sanity.
1523        assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(30));
1524
1525        // Start from a clean env so we can assert restoration to "unset".
1526        // SAFETY: single-threaded tests.
1527        unsafe {
1528            std::env::remove_var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT");
1529        }
1530        assert!(std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").is_err());
1531
1532        // Stack A: 40s (becomes top)
1533        let guard_a = lock.override_key(MESSAGE_DELIVERY_TIMEOUT, Duration::from_secs(40));
1534        assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(40));
1535        {
1536            let s = std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").unwrap();
1537            assert_eq!(
1538                humantime::parse_duration(&s).unwrap(),
1539                Duration::from_secs(40)
1540            );
1541        }
1542
1543        // Stack B: 50s (new top)
1544        let guard_b = lock.override_key(MESSAGE_DELIVERY_TIMEOUT, Duration::from_secs(50));
1545        assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(50));
1546        {
1547            let s = std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").unwrap();
1548            assert_eq!(
1549                humantime::parse_duration(&s).unwrap(),
1550                Duration::from_secs(50)
1551            );
1552        }
1553
1554        // Drop B first → should reveal A (LIFO)
1555        std::mem::drop(guard_b);
1556        assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(40));
1557        {
1558            let s = std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").unwrap();
1559            assert_eq!(
1560                humantime::parse_duration(&s).unwrap(),
1561                Duration::from_secs(40)
1562            );
1563        }
1564
1565        // Drop A → should restore default and unset env.
1566        std::mem::drop(guard_a);
1567        assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(30));
1568        assert!(std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").is_err());
1569    }
1570
1571    #[test]
1572    fn test_testoverride_out_of_order_drop_keeps_top_stable() {
1573        let lock = lock();
1574        reset_to_defaults();
1575
1576        // Clean env baseline.
1577        // SAFETY: single-threaded tests.
1578        unsafe {
1579            std::env::remove_var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT");
1580        }
1581        assert!(std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").is_err());
1582
1583        // Push three frames in order: A=40s, B=50s, C=70s (C is top).
1584        let guard_a = lock.override_key(MESSAGE_DELIVERY_TIMEOUT, Duration::from_secs(40));
1585        let guard_b = lock.override_key(MESSAGE_DELIVERY_TIMEOUT, Duration::from_secs(50));
1586        let guard_c = lock.override_key(MESSAGE_DELIVERY_TIMEOUT, Duration::from_secs(70));
1587
1588        // Top is C.
1589        assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(70));
1590        {
1591            let s = std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").unwrap();
1592            assert_eq!(
1593                humantime::parse_duration(&s).unwrap(),
1594                Duration::from_secs(70)
1595            );
1596        }
1597
1598        // Drop the *middle* frame (B) first → top must remain C, env unchanged.
1599        std::mem::drop(guard_b);
1600        assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(70));
1601        {
1602            let s = std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").unwrap();
1603            assert_eq!(
1604                humantime::parse_duration(&s).unwrap(),
1605                Duration::from_secs(70)
1606            );
1607        }
1608
1609        // Now drop C → A becomes top, env follows A.
1610        std::mem::drop(guard_c);
1611        assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(40));
1612        {
1613            let s = std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").unwrap();
1614            assert_eq!(
1615                humantime::parse_duration(&s).unwrap(),
1616                Duration::from_secs(40)
1617            );
1618        }
1619
1620        // Drop A → restore default and clear env.
1621        std::mem::drop(guard_a);
1622        assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(30));
1623        assert!(std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").is_err());
1624    }
1625
1626    #[test]
1627    fn test_priority_order() {
1628        use Source::*;
1629        assert!(priority(TestOverride) < priority(Env));
1630        assert!(priority(Env) < priority(Runtime));
1631        assert!(priority(Runtime) < priority(File));
1632        assert!(priority(File) < priority(ClientOverride));
1633    }
1634
1635    #[test]
1636    fn test_create_or_merge_runtime_merges_keys() {
1637        let _lock = lock();
1638        reset_to_defaults();
1639
1640        // Seed Runtime with one key.
1641        let mut rt = Attrs::new();
1642        rt[MESSAGE_TTL_DEFAULT] = 10;
1643        set(Source::Runtime, rt);
1644
1645        // Now update Runtime with a different key via
1646        // `create_or_merge`.
1647        let mut update = Attrs::new();
1648        update[MESSAGE_ACK_EVERY_N_MESSAGES] = 123;
1649        create_or_merge(Source::Runtime, update);
1650
1651        // Both keys should now be visible from Runtime.
1652        assert_eq!(get(MESSAGE_TTL_DEFAULT), 10);
1653        assert_eq!(get(MESSAGE_ACK_EVERY_N_MESSAGES), 123);
1654    }
1655
1656    #[test]
1657    fn test_create_or_merge_runtime_creates_layer_if_missing() {
1658        let _lock = lock();
1659        reset_to_defaults();
1660
1661        let mut rt = Attrs::new();
1662        rt[MESSAGE_TTL_DEFAULT] = 42;
1663        create_or_merge(Source::Runtime, rt);
1664
1665        assert_eq!(get(MESSAGE_TTL_DEFAULT), 42);
1666    }
1667
1668    #[test]
1669    fn test_clientoverride_precedence_loses_to_all_other_layers() {
1670        let _lock = lock();
1671        reset_to_defaults();
1672
1673        // ClientOverride sets a baseline value.
1674        let mut client = Attrs::new();
1675        client[MESSAGE_TTL_DEFAULT] = 10;
1676        set(Source::ClientOverride, client);
1677        assert_eq!(get(MESSAGE_TTL_DEFAULT), 10);
1678
1679        // File should beat ClientOverride.
1680        let mut file = Attrs::new();
1681        file[MESSAGE_TTL_DEFAULT] = 20;
1682        set(Source::File, file);
1683        assert_eq!(get(MESSAGE_TTL_DEFAULT), 20);
1684
1685        // Runtime should beat both File and ClientOverride.
1686        let mut runtime = Attrs::new();
1687        runtime[MESSAGE_TTL_DEFAULT] = 30;
1688        set(Source::Runtime, runtime);
1689        assert_eq!(get(MESSAGE_TTL_DEFAULT), 30);
1690
1691        // Env should beat Runtime, File, and ClientOverride.
1692        let mut env = Attrs::new();
1693        env[MESSAGE_TTL_DEFAULT] = 40;
1694        set(Source::Env, env);
1695        assert_eq!(get(MESSAGE_TTL_DEFAULT), 40);
1696
1697        // Clear higher layers one by one to verify fallback.
1698        clear(Source::Env);
1699        assert_eq!(get(MESSAGE_TTL_DEFAULT), 30); // Runtime
1700
1701        clear(Source::Runtime);
1702        assert_eq!(get(MESSAGE_TTL_DEFAULT), 20); // File
1703
1704        clear(Source::File);
1705        assert_eq!(get(MESSAGE_TTL_DEFAULT), 10); // ClientOverride
1706    }
1707
1708    #[test]
1709    fn test_create_or_merge_clientoverride() {
1710        let _lock = lock();
1711        reset_to_defaults();
1712
1713        // Seed ClientOverride with one key.
1714        let mut client = Attrs::new();
1715        client[MESSAGE_TTL_DEFAULT] = 10;
1716        set(Source::ClientOverride, client);
1717
1718        // Merge in a different key.
1719        let mut update = Attrs::new();
1720        update[MESSAGE_ACK_EVERY_N_MESSAGES] = 123;
1721        create_or_merge(Source::ClientOverride, update);
1722
1723        // Both keys should now be visible.
1724        assert_eq!(get(MESSAGE_TTL_DEFAULT), 10);
1725        assert_eq!(get(MESSAGE_ACK_EVERY_N_MESSAGES), 123);
1726    }
1727
1728    #[test]
1729    fn test_override_or_global_returns_override_when_present() {
1730        let _lock = lock();
1731        reset_to_defaults();
1732
1733        // Set a global value via Env.
1734        let mut env = Attrs::new();
1735        env[MESSAGE_TTL_DEFAULT] = 99;
1736        set(Source::Env, env);
1737
1738        // Create an override Attrs with a different value.
1739        let mut overrides = Attrs::new();
1740        overrides[MESSAGE_TTL_DEFAULT] = 42;
1741
1742        // Should return the override value, not global.
1743        assert_eq!(override_or_global(&overrides, MESSAGE_TTL_DEFAULT), 42);
1744    }
1745
1746    #[test]
1747    fn test_override_or_global_returns_global_when_not_present() {
1748        let _lock = lock();
1749        reset_to_defaults();
1750
1751        // Set a global value via Env.
1752        let mut env = Attrs::new();
1753        env[MESSAGE_TTL_DEFAULT] = 99;
1754        set(Source::Env, env);
1755
1756        // Empty overrides.
1757        let overrides = Attrs::new();
1758
1759        // Should return the global value.
1760        assert_eq!(override_or_global(&overrides, MESSAGE_TTL_DEFAULT), 99);
1761    }
1762
1763    #[test]
1764    fn test_runtime_attrs_returns_only_runtime_layer() {
1765        let _lock = lock();
1766        reset_to_defaults();
1767
1768        // Set values in multiple layers.
1769        let mut file = Attrs::new();
1770        file[MESSAGE_TTL_DEFAULT] = 10;
1771        set(Source::File, file);
1772
1773        let mut env = Attrs::new();
1774        env[SPLIT_MAX_BUFFER_SIZE] = 20;
1775        set(Source::Env, env);
1776
1777        let mut runtime = Attrs::new();
1778        runtime[MESSAGE_ACK_EVERY_N_MESSAGES] = 123;
1779        set(Source::Runtime, runtime);
1780
1781        // runtime_attrs() should return only Runtime layer contents.
1782        let rt = runtime_attrs();
1783
1784        // Should have the Runtime key.
1785        assert_eq!(rt[MESSAGE_ACK_EVERY_N_MESSAGES], 123);
1786
1787        // Should NOT have File or Env keys.
1788        assert!(!rt.contains_key(MESSAGE_TTL_DEFAULT));
1789        assert!(!rt.contains_key(SPLIT_MAX_BUFFER_SIZE));
1790    }
1791
1792    #[test]
1793    fn test_override_key_without_env_name_does_not_mirror_to_env() {
1794        let lock = lock();
1795        reset_to_defaults();
1796
1797        // Verify default value.
1798        assert_eq!(get(CONFIG_KEY_NO_ENV), 100);
1799
1800        // Override the key (which has no env_name).
1801        let _guard = lock.override_key(CONFIG_KEY_NO_ENV, 999);
1802
1803        // Should see the override value.
1804        assert_eq!(get(CONFIG_KEY_NO_ENV), 999);
1805
1806        // No env var should have been set (test doesn't crash,
1807        // behavior is clean). This test mainly ensures no panic
1808        // occurs during override/restore.
1809
1810        drop(_guard);
1811
1812        // Should restore to default.
1813        assert_eq!(get(CONFIG_KEY_NO_ENV), 100);
1814    }
1815
1816    #[test]
1817    fn test_multiple_different_keys_overridden_simultaneously() {
1818        let lock = lock();
1819        reset_to_defaults();
1820
1821        // SAFETY: single-threaded test.
1822        unsafe {
1823            std::env::remove_var("HYPERACTOR_CODEC_MAX_FRAME_LENGTH");
1824            std::env::remove_var("HYPERACTOR_MESSAGE_TTL_DEFAULT");
1825        }
1826
1827        // Override multiple different keys at once.
1828        let guard1 = lock.override_key(CODEC_MAX_FRAME_LENGTH, 1111);
1829        let guard2 = lock.override_key(MESSAGE_TTL_DEFAULT, 42);
1830        let guard3 = lock.override_key(CHANNEL_MULTIPART, false);
1831
1832        // All should reflect their override values.
1833        assert_eq!(get(CODEC_MAX_FRAME_LENGTH), 1111);
1834        assert_eq!(get(MESSAGE_TTL_DEFAULT), 42);
1835        assert!(!get(CHANNEL_MULTIPART));
1836
1837        // Env vars should be mirrored.
1838        assert_eq!(
1839            std::env::var("HYPERACTOR_CODEC_MAX_FRAME_LENGTH").unwrap(),
1840            "1111"
1841        );
1842        assert_eq!(
1843            std::env::var("HYPERACTOR_MESSAGE_TTL_DEFAULT").unwrap(),
1844            "42"
1845        );
1846
1847        // Drop guards in arbitrary order.
1848        drop(guard2); // Drop MESSAGE_TTL_DEFAULT first
1849
1850        // MESSAGE_TTL_DEFAULT should restore, others should remain.
1851        assert_eq!(get(MESSAGE_TTL_DEFAULT), MESSAGE_TTL_DEFAULT_DEFAULT);
1852        assert_eq!(get(CODEC_MAX_FRAME_LENGTH), 1111);
1853        assert!(!get(CHANNEL_MULTIPART));
1854
1855        // Env for MESSAGE_TTL_DEFAULT should be cleared.
1856        assert!(std::env::var("HYPERACTOR_MESSAGE_TTL_DEFAULT").is_err());
1857
1858        drop(guard1);
1859        drop(guard3);
1860
1861        // All should be restored.
1862        assert_eq!(get(CODEC_MAX_FRAME_LENGTH), CODEC_MAX_FRAME_LENGTH_DEFAULT);
1863        assert_eq!(get(CHANNEL_MULTIPART), CHANNEL_MULTIPART_DEFAULT);
1864    }
1865
1866    #[test]
1867    fn test_lock_recovers_after_panic() {
1868        let handle = std::thread::spawn(|| {
1869            let _lock = lock();
1870            panic!("intentional panic while holding ConfigLock");
1871        });
1872
1873        let result = handle.join();
1874        assert!(result.is_err(), "thread should have panicked");
1875
1876        let lock = lock();
1877        reset_to_defaults();
1878
1879        let _guard = lock.override_key(CODEC_MAX_FRAME_LENGTH, 9999);
1880        assert_eq!(get(CODEC_MAX_FRAME_LENGTH), 9999);
1881    }
1882
1883    // ── config_entries() tests ─────────────────────────────────────────────
1884
1885    #[test]
1886    fn test_config_entries_excludes_non_config_keys() {
1887        let _lock = lock();
1888        reset_to_defaults();
1889
1890        let entries = config_entries();
1891        assert!(
1892            entries
1893                .iter()
1894                .any(|e| e.name.contains("config_key") && !e.name.contains("non_config")),
1895            "CONFIG_KEY should appear in config_entries()"
1896        );
1897        assert!(
1898            !entries.iter().any(|e| e.name.contains("non_config_key")),
1899            "NON_CONFIG_KEY should not appear in config_entries()"
1900        );
1901    }
1902
1903    #[test]
1904    fn test_config_entries_layer_precedence() {
1905        let lock = lock();
1906        reset_to_defaults();
1907
1908        // Defaults-only: source should be Default.
1909        let entries = config_entries();
1910        let codec = entries
1911            .iter()
1912            .find(|e| e.name.contains("codec_max_frame_length"))
1913            .expect("CODEC_MAX_FRAME_LENGTH should appear");
1914        assert_eq!(codec.source, Source::Default);
1915
1916        // Override at TestOverride → source should be TestOverride.
1917        let _guard = lock.override_key(CODEC_MAX_FRAME_LENGTH, 1024);
1918        let entries = config_entries();
1919        let codec = entries
1920            .iter()
1921            .find(|e| e.name.contains("codec_max_frame_length"))
1922            .expect("CODEC_MAX_FRAME_LENGTH should appear");
1923        assert_eq!(codec.source, Source::TestOverride);
1924        assert_eq!(codec.value, "1024");
1925    }
1926
1927    #[test]
1928    fn test_config_entries_changed_from_default_different_value() {
1929        let lock = lock();
1930        reset_to_defaults();
1931
1932        let _guard = lock.override_key(CODEC_MAX_FRAME_LENGTH, 1024);
1933        let entries = config_entries();
1934        let codec = entries
1935            .iter()
1936            .find(|e| e.name.contains("codec_max_frame_length"))
1937            .expect("CODEC_MAX_FRAME_LENGTH should appear");
1938        assert!(
1939            codec.changed_from_default,
1940            "value differs from default → changed_from_default should be true"
1941        );
1942    }
1943
1944    #[test]
1945    fn test_config_entries_changed_from_default_same_value() {
1946        let lock = lock();
1947        reset_to_defaults();
1948
1949        // Override to the same value as the declared default.
1950        let _guard = lock.override_key(CODEC_MAX_FRAME_LENGTH, CODEC_MAX_FRAME_LENGTH_DEFAULT);
1951        let entries = config_entries();
1952        let codec = entries
1953            .iter()
1954            .find(|e| e.name.contains("codec_max_frame_length"))
1955            .expect("CODEC_MAX_FRAME_LENGTH should appear");
1956        assert!(
1957            !codec.changed_from_default,
1958            "value matches default → changed_from_default should be false"
1959        );
1960    }
1961
1962    #[test]
1963    fn test_config_entries_sorted_by_name() {
1964        let _lock = lock();
1965        reset_to_defaults();
1966
1967        let entries = config_entries();
1968        for w in entries.windows(2) {
1969            assert!(
1970                w[0].name <= w[1].name,
1971                "entries not sorted: {:?} > {:?}",
1972                w[0].name,
1973                w[1].name,
1974            );
1975        }
1976    }
1977
1978    #[test]
1979    fn test_config_entries_env_var_extraction() {
1980        let _lock = lock();
1981        reset_to_defaults();
1982
1983        let entries = config_entries();
1984        let codec = entries
1985            .iter()
1986            .find(|e| e.name.contains("codec_max_frame_length"))
1987            .expect("CODEC_MAX_FRAME_LENGTH should appear");
1988        assert_eq!(
1989            codec.env_var.as_deref(),
1990            Some("HYPERACTOR_CODEC_MAX_FRAME_LENGTH")
1991        );
1992
1993        // CONFIG_KEY_NO_ENV has no env_name.
1994        let no_env = entries
1995            .iter()
1996            .find(|e| e.name.contains("config_key_no_env"))
1997            .expect("CONFIG_KEY_NO_ENV should appear");
1998        assert_eq!(no_env.env_var, None);
1999    }
2000}