Skip to main content

rlmesh_proto/
lib.rs

1//! Generated RLMesh protobuf bindings and protocol-level constants.
2#![deny(rustdoc::broken_intra_doc_links)]
3
4use std::collections::HashMap;
5use std::sync::RwLock;
6
7/// Identity of the frozen wire substrate: the `core` handshake plus the `spaces`
8/// value transport the runtime relays between env and model. NOT the package
9/// version, NOT the product semver, and NOT a per-service version — it names the
10/// one shared byte contract every component is built against, decoupled from all
11/// of them.
12///
13/// Compatibility is plain equality: a peer is compatible iff its token equals
14/// this. There is no support window and no range negotiation. The wire grows
15/// additively forever under this single token; workflow *behavior* rides on
16/// editions and optional *features* on capabilities, so neither forces a bump.
17///
18/// This is a failsafe, not a version counter: in the normal course it is never
19/// bumped. Bumping it to `rlmesh-wire-v2` is a deliberate, public hard pivot — a
20/// build that intentionally will not interoperate with `rlmesh-wire-v1` — reserved
21/// for a wire break that additive growth, editions, and capabilities genuinely
22/// cannot absorb.
23pub const PROTOCOL_GENERATION: &str = "rlmesh-wire-v1";
24
25/// Current workflow semantics edition.
26///
27/// Stable releases use a bare sealed `YYYY.MM` label. Prerelease and local
28/// source builds use a suffixed cohort (`YYYY.MM-<semver-prerelease>` or
29/// `YYYY.MM-dev.<git-token>`) so moving builds only interoperate with the same
30/// cohort unless both sides explicitly advertise a sealed fallback edition.
31pub const CURRENT_WORKFLOW_EDITION: &str = env!("RLMESH_CURRENT_WORKFLOW_EDITION");
32
33/// Bare `YYYY.MM` workflow edition base for this build.
34pub const WORKFLOW_EDITION_BASE: &str = env!("RLMESH_WORKFLOW_EDITION_BASE");
35
36/// Build cohort used to spell [`CURRENT_WORKFLOW_EDITION`].
37pub const BUILD_COHORT: &str = env!("RLMESH_BUILD_COHORT");
38
39/// Source of the build cohort: `release`, `package`, or `git`.
40pub const BUILD_SOURCE: &str = env!("RLMESH_BUILD_SOURCE");
41
42/// Workflow editions this crate can operate under, generated by `build.rs` from
43/// `rlmesh.toml`'s `[workflow] supported_editions` with
44/// [`CURRENT_WORKFLOW_EDITION`] first. Each edition names an immutable
45/// behavioral contract documented in `docs/editions/<base>.md` (the spec file is
46/// keyed by the bare `YYYY.MM` base, never the suffixed cohort), so a sealed
47/// edition is retained here under its bare name for as long as the crate can
48/// still drive it. A published crate ships no `rlmesh.toml`; it reads the same
49/// list from its shipped `supported_editions.txt`.
50pub const SUPPORTED_WORKFLOW_EDITIONS: &[&str] = &SUPPORTED_WORKFLOW_EDITION_ARRAY;
51
52const SUPPORTED_WORKFLOW_EDITION_LIST: &str = env!("RLMESH_SUPPORTED_WORKFLOW_EDITIONS");
53
54const SUPPORTED_WORKFLOW_EDITION_ARRAY: [&str; edition_count(SUPPORTED_WORKFLOW_EDITION_LIST)] =
55    split_editions(SUPPORTED_WORKFLOW_EDITION_LIST);
56
57/// Number of comma-separated editions in the build-time list (never zero: the
58/// list always carries at least [`CURRENT_WORKFLOW_EDITION`]).
59const fn edition_count(list: &str) -> usize {
60    let bytes = list.as_bytes();
61    let mut count = 1;
62    let mut index = 0;
63    while index < bytes.len() {
64        if bytes[index] == b',' {
65            count += 1;
66        }
67        index += 1;
68    }
69    count
70}
71
72/// Split the build-time list on commas. Edition names are `YYYY.MM` bases with
73/// SemVer/git cohort suffixes, so no name can contain the separator.
74const fn split_editions<const N: usize>(list: &'static str) -> [&'static str; N] {
75    let bytes = list.as_bytes();
76    let mut editions = [""; N];
77    let mut start = 0;
78    let mut index = 0;
79    let mut slot = 0;
80    while index < bytes.len() {
81        if bytes[index] == b',' {
82            editions[slot] = edition_at(bytes, start, index);
83            slot += 1;
84            start = index + 1;
85        }
86        index += 1;
87    }
88    editions[slot] = edition_at(bytes, start, bytes.len());
89    editions
90}
91
92const fn edition_at(bytes: &'static [u8], start: usize, end: usize) -> &'static str {
93    let (_, tail) = bytes.split_at(start);
94    let (edition, _) = tail.split_at(end - start);
95    match std::str::from_utf8(edition) {
96        Ok(edition) => edition,
97        Err(_) => panic!("RLMESH_SUPPORTED_WORKFLOW_EDITIONS is not UTF-8"),
98    }
99}
100
101/// A workflow semantics edition this build implements.
102///
103/// An edition is a sticky behavioral declaration (the NixOS `stateVersion`
104/// model), so this enum is deliberately exhaustive and its arms are append-only:
105/// a future default change adds an arm here plus a row in [`defaults`] and
106/// leaves every sealed arm — and every code path reading it — untouched. A name
107/// no arm implements is refused at the boundary by [`Edition::parse`], never
108/// folded into a neighbouring arm.
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
110pub enum Edition {
111    /// `2026.06`, sealed at v0.1.0. Contract: `docs/editions/2026.06.md`.
112    E2026_06,
113}
114
115impl Edition {
116    /// Every arm, in declaration order; append-only, like the enum itself.
117    ///
118    /// Rust cannot enumerate variants, so this list is written by hand and
119    /// guarded by `every_arm_is_listed_and_has_a_row`: its exhaustive match means
120    /// a new arm does not compile until it is named there, and the test fails
121    /// unless every name this build offers ([`SUPPORTED_WORKFLOW_EDITIONS`]) and
122    /// every arm's own base resolve through this list.
123    const ALL: &'static [Edition] = &[Edition::E2026_06];
124
125    /// The bare `YYYY.MM` base this edition's contract document is keyed by.
126    pub const fn base(&self) -> &'static str {
127        match self {
128            Edition::E2026_06 => "2026.06",
129        }
130    }
131
132    /// The arm this build's own [`CURRENT_WORKFLOW_EDITION`] spells: what a
133    /// participant runs at when nothing pins it elsewhere.
134    pub fn current() -> Edition {
135        Edition::parse(CURRENT_WORKFLOW_EDITION)
136            .expect("CURRENT_WORKFLOW_EDITION names an arm (every_arm_is_listed_and_has_a_row)")
137    }
138
139    /// Resolve a wire edition name to the arm it names.
140    ///
141    /// The name is split at its first `-` exactly as [`edition_sort_key`] splits
142    /// it: the `YYYY.MM` base names the contract, and a cohort suffix only
143    /// identifies a moving build of that SAME base (`YYYY.MM-<semver-prerelease>`
144    /// or `YYYY.MM-dev.<git-token>`). So every cohort spelling of a known base
145    /// resolves to that base's arm — this build's own
146    /// ([`CURRENT_WORKFLOW_EDITION`], e.g. `2026.06-0.1.0-rc.12`) and equally a
147    /// retained older base's cohort, which this build never spells itself.
148    /// Whitespace is trimmed; a base no arm implements is an [`UnknownEdition`].
149    ///
150    /// This is the ONLY place an edition name is compared as a string. Every
151    /// guard and default downstream branches on the typed value.
152    pub fn parse(edition: &str) -> Result<Edition, UnknownEdition> {
153        let name = edition.trim();
154        let (base, _, _) = edition_sort_key(name);
155        Edition::ALL
156            .iter()
157            .copied()
158            .find(|edition| edition.base() == base)
159            .ok_or_else(|| UnknownEdition(name.to_string()))
160    }
161}
162
163impl std::fmt::Display for Edition {
164    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165        f.write_str(self.base())
166    }
167}
168
169/// A wire edition name no [`Edition`] arm implements.
170#[derive(Debug, Clone, PartialEq, Eq)]
171pub struct UnknownEdition(String);
172
173impl UnknownEdition {
174    /// The rejected name, trimmed, exactly as it arrived on the wire.
175    pub fn name(&self) -> &str {
176        &self.0
177    }
178}
179
180impl std::fmt::Display for UnknownEdition {
181    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182        write!(f, "unknown workflow edition {:?}", self.0)
183    }
184}
185
186impl std::error::Error for UnknownEdition {}
187
188/// The edition-governed defaults one [`Edition`] promises: the values a
189/// participant would otherwise hardcode, lifted here so a future default change
190/// is a new row rather than an edit to a code path a sealed edition already
191/// depends on. One row per arm, returned by [`defaults`]; **a sealed row is
192/// never edited.**
193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194pub struct EditionDefaults {
195    /// Per-episode step bound applied when the session sets no explicit
196    /// `max_episode_steps` and the runtime owns resets, so a broken termination
197    /// condition surfaces as a truncation instead of hanging the run.
198    ///
199    /// Not named in the prose of `docs/editions/2026.06.md`, but observable in
200    /// the episode ledger the edition's "Episode Accounting" section governs
201    /// ("When a sub-environment reports terminated or truncated, its episode
202    /// completes"), and pinned by the `05-default-truncation-bound` fingerprint.
203    ///
204    /// Mirrored by `_MAX_STEPS_PER_EPISODE` in
205    /// `python/rlmesh/src/rlmesh/_models/_eval.py`, which bounds the Python
206    /// `Session` loop identically. The two loops must agree, so an edition that
207    /// changes this row changes that constant in the same commit.
208    pub default_max_episode_steps: i64,
209
210    /// The single reserved `ResetRequest.options` key this edition defines.
211    ///
212    /// `docs/editions/2026.06.md`, Reset: "This edition reserves exactly one,
213    /// `trial_index`: the 0-based ordinal of the episode being started, an
214    /// integer for a single lane or a per-lane list aligned to the lanes being
215    /// reset."
216    pub trial_index_option_key: &'static str,
217
218    /// The autoreset modes under which the RUNTIME owns lane restarts (it issues
219    /// the `Reset`s), as opposed to the env rolling its own lanes.
220    ///
221    /// `docs/editions/2026.06.md`, Episode Accounting: "`AUTORESET_MODE_DISABLED`:
222    /// only an explicit `Reset` restarts a lane." and "`AUTORESET_MODE_UNSPECIFIED`:
223    /// treated as `DISABLED`."
224    pub driver_owned_reset_modes: &'static [core::v1::AutoresetMode],
225
226    /// The `final_info` keys this edition reads an episode's task outcome from,
227    /// in priority order: the first key present decides, and a numeric value
228    /// coerces by truthiness (`1`/`1.0` → true).
229    ///
230    /// Which keys count is an edition-governed promise — an env that reports its
231    /// outcome under one of these names has it surface on the episode summary
232    /// and the `EpisodeCompleted` event; one that uses any other name does not.
233    /// Gymnasium's `is_success` leads; `success` and `task_success` follow as the
234    /// two spellings the ecosystem also ships.
235    ///
236    /// The Python `Session` loop reads the outcome from the same `info` with
237    /// `bool(info[key])` (`_success_from_info` in
238    /// `python/rlmesh/src/rlmesh/_models/_eval.py`); an edition that changes this
239    /// row revisits that sweep in the same commit.
240    pub success_info_keys: &'static [&'static str],
241
242    /// The reserved info-map key a served env reports value-conformance
243    /// warnings under.
244    ///
245    /// `docs/editions/2026.06.md`, Value Conformance: "A conformance warning is
246    /// reported in-band under the reserved `rlmesh.conformance.warning` key in
247    /// the info map returned by `reset` and `step`, at most once per (deviation
248    /// kind, value path) per session."
249    pub conformance_warning_info_key: &'static str,
250}
251
252/// Edition `2026.06`, sealed at v0.1.0. Never edited: a default change mints a
253/// new edition with its own row.
254static DEFAULTS_2026_06: EditionDefaults = EditionDefaults {
255    default_max_episode_steps: 100_000,
256    trial_index_option_key: "trial_index",
257    driver_owned_reset_modes: &[
258        core::v1::AutoresetMode::Disabled,
259        core::v1::AutoresetMode::Unspecified,
260    ],
261    success_info_keys: &["is_success", "success", "task_success"],
262    conformance_warning_info_key: "rlmesh.conformance.warning",
263};
264
265/// The defaults row governing a session running at `edition`.
266///
267/// Exhaustive by construction: a new [`Edition`] arm does not compile until it
268/// has a row here.
269pub const fn defaults(edition: Edition) -> &'static EditionDefaults {
270    match edition {
271        Edition::E2026_06 => &DEFAULTS_2026_06,
272    }
273}
274
275/// Whether this build retains `edition`: it has an arm for it AND
276/// [`SUPPORTED_WORKFLOW_EDITIONS`] (generated from `rlmesh.toml`) still names a
277/// spelling of it.
278///
279/// This is the authority for enforcing a runtime-pinned edition. Membership, not
280/// equality with [`CURRENT_WORKFLOW_EDITION`]: a retained older edition is a
281/// legitimate session floor, and rejecting it would refuse a route this build
282/// can actually drive.
283pub fn is_retained_edition(edition: Edition) -> bool {
284    SUPPORTED_WORKFLOW_EDITIONS
285        .iter()
286        .any(|retained| Edition::parse(retained) == Ok(edition))
287}
288
289/// Resolve a wire edition name to the arm this build both implements AND
290/// retains, or say why it cannot.
291///
292/// The boundary every string-carrying caller goes through — the env client's
293/// handshake, the runtime spec, a language binding — so a name this build was not
294/// built to drive is refused where it arrives, in one wording, rather than at a
295/// later guard that only ever sees the typed value. The message names the string
296/// exactly as it arrived (trimmed) and the retained list, so an operator can see
297/// both halves of the mismatch.
298pub fn parse_retained_edition(edition: &str) -> Result<Edition, String> {
299    Edition::parse(edition)
300        .ok()
301        .filter(|edition| is_retained_edition(*edition))
302        .ok_or_else(|| {
303            format!(
304                "runtime cannot drive workflow edition {:?}; this build implements {:?}",
305                edition.trim(),
306                SUPPORTED_WORKFLOW_EDITIONS
307            )
308        })
309}
310
311/// Resolve a **declared** edition — a participant's WANT — to the arm it names,
312/// or say why this build cannot declare it.
313///
314/// Stricter than [`parse_retained_edition`], which answers a different question:
315/// whether this build can be *pinned* to a value the runtime already selected out
316/// of an intersection of CAN sets. A declaration is a ceiling instead
317/// ([`want_admits`]), so it is only usable if it admits something this build
318/// offers. The bare `YYYY.MM` base of a retained edition always does — it admits
319/// every cohort spelling of that base, which is what makes it the value to write
320/// down. A cohort spelling that sorts below everything offered (an older
321/// prerelease of this build's own base, say) would refuse every session the
322/// build takes part in — including a purely local run — so it is refused here,
323/// where it was typed.
324pub fn parse_declared_edition(edition: &str) -> Result<Edition, String> {
325    let parsed = parse_retained_edition(edition)?;
326    let want = edition.trim();
327    if SUPPORTED_WORKFLOW_EDITIONS
328        .iter()
329        .any(|can| want_admits(want, can))
330    {
331        return Ok(parsed);
332    }
333    Err(format!(
334        "workflow edition {want:?} admits none of the editions this build offers \
335         ({SUPPORTED_WORKFLOW_EDITIONS:?}), so declaring it would refuse every session; \
336         declare one of those, or the bare {:?} base, instead",
337        parsed.base()
338    ))
339}
340
341/// Stable capability names exchanged during handshake.
342///
343/// Capabilities are advisory. A present key means the named optional feature is
344/// available; an absent key means it is not. They cover optional features that
345/// preserve interaction semantics. A feature that changes meaning belongs in an
346/// edition or generation. House rule: when an older peer would mishandle an
347/// absent field, the emitter checks a capability before sending it; if absence
348/// changes semantics, use an edition.
349pub mod capabilities {
350    /// A served model endpoint processes Join-stream requests concurrently
351    /// (pipelined predict): responses arrive in completion order rather than
352    /// strict arrival order, while per-route lifecycle ordering is preserved.
353    ///
354    /// Advisory only; this is not an edition change. The wire messages are
355    /// identical: every response still mirrors its `request_id`. A client uses
356    /// it to decide whether overlapping multiple predicts on one connection will
357    /// actually pipeline (capability present) or serialize behind the handler
358    /// (capability absent). See `docs/editions/2026.06.md`.
359    pub const MODEL_CONCURRENT_PREDICT_V1: &str = "rlmesh.model.concurrent_predict.v1";
360
361    /// A served model endpoint understands observation history: it answers
362    /// `ResolveAdapterResponse.history` when the runtime offers
363    /// `delivers_history`, ingests `PredictRequest.history` rows, and holds the
364    /// producer to consecutive `step`s. Advisory: a runtime that never offers
365    /// history sees no wire difference.
366    pub const MODEL_OBSERVATION_HISTORY_V1: &str = "rlmesh.model.observation_history.v1";
367
368    /// A served env endpoint steps and resets lanes individually: a `Reset` or
369    /// `Step` naming `env_indices` is honored (and answered partial-width)
370    /// instead of rejected with `UNSUPPORTED`, and lane-scoped requests on one
371    /// Join stream may be processed concurrently, answered in completion order.
372    ///
373    /// The wire spelling is the bare `subset_step` shipped peers send, so it
374    /// carries no `rlmesh.*` prefix. See `docs/editions/2026.06.md`.
375    pub const ENV_SUBSET_STEP: &str = "subset_step";
376}
377
378/// Whether the given protocol generation is the one this build speaks. Plain
379/// equality — there is no support window. Whitespace is trimmed so a padded wire
380/// value still matches.
381pub fn is_protocol_generation_supported(generation: &str) -> bool {
382    generation.trim() == PROTOCOL_GENERATION
383}
384
385/// Ordering key for a workflow edition name: `(base, cohort?, suffix)`.
386///
387/// The name is split at its **first** `-` into a `YYYY.MM` base and an optional
388/// cohort suffix:
389/// - `base` compares lexicographically — the zero-padded fixed-width `YYYY.MM`
390///   makes that chronological, so a newer date always outranks an older one.
391/// - `cohort?` is `true` for a suffixed prerelease/dev cohort and `false` for a
392///   bare sealed fallback, so an exact matching moving cohort wins over its
393///   sealed fallback when both peers support it.
394/// - `suffix` is the full cohort as a deterministic third tiebreak; two
395///   same-date cohorts are ordered by suffix rather than by iteration order.
396///
397/// Applies to workflow editions only. Protocol generations are compared by plain
398/// equality ([`is_protocol_generation_supported`]), never ordered — there is no
399/// generation window to pick a highest from.
400pub fn edition_sort_key(edition: &str) -> (&str, bool, &str) {
401    match edition.split_once('-') {
402        Some((base, suffix)) => (base, true, suffix),
403        None => (edition, false, ""),
404    }
405}
406
407/// Whether a declared WANT admits `edition` as the session's edition.
408///
409/// A declaration names the contract a participant was authored against, so a
410/// bare `YYYY.MM` WANT is a **base-level** ceiling: it admits every spelling of
411/// that base or an older one — on a prerelease or dev build, whose CAN set is
412/// only its cohort, `2026.06` selects that build's `2026.06-<cohort>`. A WANT
413/// that carries a cohort pins to that exact moving build and keeps the full
414/// [`edition_sort_key`] order, so a differing cohort of the same base is still
415/// excluded.
416pub fn want_admits(want: &str, edition: &str) -> bool {
417    match edition_sort_key(want) {
418        (base, false, _) => edition_sort_key(edition).0 <= base,
419        want => edition_sort_key(edition) <= want,
420    }
421}
422
423/// One participant's bind-time offer: the two sets it brings to negotiation.
424///
425/// Built for the env, the model, AND this runtime; [`negotiate_session_floor`]
426/// reconciles all three (the runtime is a participant because it re-frames
427/// env<->model traffic, so the session runs at an edition all three speak). When
428/// the model and runtime are the same build (in-process / `run_local`), the floor
429/// degenerates to `env ∩ self`. Protocol generation is NOT part of the offer — it
430/// is gated by plain equality at each pairwise handshake, so a session that reaches
431/// edition negotiation already shares one generation. Capabilities are advisory and
432/// pairwise (each peer reads the other's advertised map directly), not negotiated here.
433#[derive(Debug, Clone, PartialEq, Eq)]
434pub struct SessionOffer {
435    /// CAN: every workflow edition this participant can operate under — its
436    /// retained sealed list, carried on `supported_workflow_editions`.
437    pub editions: Vec<String>,
438    /// WANT: the one edition this participant declares, carried on
439    /// `preferred_workflow_edition`. `None` (an empty wire value) means
440    /// undeclared, and negotiation then reads it as `max(editions)` — which is
441    /// what every peer built before the field existed means.
442    pub preferred: Option<String>,
443}
444
445impl SessionOffer {
446    /// Build an undeclared offer (no WANT) from edition string slices.
447    /// Whitespace is trimmed.
448    pub fn new(editions: &[&str]) -> Self {
449        Self {
450            editions: editions.iter().map(|e| e.trim().to_string()).collect(),
451            preferred: None,
452        }
453    }
454
455    /// This build's own offer: the retained list it CAN drive, plus the edition
456    /// it declares — `declared`, or [`CURRENT_WORKFLOW_EDITION`] when the
457    /// participant pins nothing.
458    pub fn this_build(declared: Option<&str>) -> Self {
459        Self {
460            editions: supported_workflow_editions(),
461            preferred: Some(declared_workflow_edition(declared).to_string()),
462        }
463    }
464
465    /// The CAN set, trimmed, with empty entries dropped (they never match).
466    fn can(&self) -> impl Iterator<Item = &str> {
467        self.editions
468            .iter()
469            .map(|edition| edition.trim())
470            .filter(|edition| !edition.is_empty())
471    }
472
473    /// The declared WANT, trimmed; `None` when undeclared.
474    fn want(&self) -> Option<&str> {
475        self.preferred
476            .as_deref()
477            .map(str::trim)
478            .filter(|want| !want.is_empty())
479    }
480}
481
482/// The edition a participant declares (its WANT): the explicit declaration, or
483/// [`CURRENT_WORKFLOW_EDITION`] when it declares none. Whitespace is trimmed and
484/// an empty declaration reads as absent.
485pub fn declared_workflow_edition(declared: Option<&str>) -> &str {
486    declared
487        .map(str::trim)
488        .filter(|declared| !declared.is_empty())
489        .unwrap_or(CURRENT_WORKFLOW_EDITION)
490}
491
492/// One tier's position in a refused negotiation, for the diagnostic.
493#[derive(Debug, Clone, PartialEq, Eq)]
494pub struct TierOffer {
495    /// Which participant this is: `env`, `model`, or `runtime`.
496    pub tier: String,
497    /// The tier's CAN set as it arrived.
498    pub can: Vec<String>,
499    /// The tier's declared WANT, or `None` when it declared none.
500    pub want: Option<String>,
501}
502
503/// No edition satisfies every participant — the negotiation's clean refusal.
504///
505/// Carries each tier's WANT and CAN so an operator can see which side is the
506/// blocker without reading logs from three processes. Displays as one line per
507/// the [`Display`](std::fmt::Display) impl; callers wrap it in their own context.
508#[derive(Debug, Clone, PartialEq, Eq)]
509pub struct EditionRefusal {
510    /// Every participant, in negotiation order.
511    pub tiers: Vec<TierOffer>,
512}
513
514impl std::fmt::Display for EditionRefusal {
515    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
516        for (index, tier) in self.tiers.iter().enumerate() {
517            if index > 0 {
518                f.write_str("; ")?;
519            }
520            match &tier.want {
521                Some(want) => write!(f, "{} wants {want:?} and can {:?}", tier.tier, tier.can)?,
522                None => write!(
523                    f,
524                    "{} declares no edition and can {:?}",
525                    tier.tier, tier.can
526                )?,
527            }
528        }
529        Ok(())
530    }
531}
532
533impl std::error::Error for EditionRefusal {}
534
535/// Select the one edition a whole session runs at, from every participant's
536/// WANT and CAN — the single rule both [`negotiate_workflow_edition`] (two
537/// parties) and [`negotiate_session_floor`] (three) apply.
538///
539/// ```text
540/// E = max { e ∈ ⋂ can(P) : ∀P want(P) admits e }      ordered by edition_sort_key
541/// ```
542///
543/// where a WANT admits an edition per [`want_admits`]: a bare base admits every
544/// spelling of that base or older, a cohort spelling admits what sorts at or
545/// below it. A participant that declares no WANT is read as wanting `max(can)`
546/// — which is what every peer built before the field existed means — and every
547/// member of the intersection is already ≤ its own max, so with nothing
548/// declared anywhere the ceiling is vacuous and this reduces exactly to the
549/// highest mutual edition.
550///
551/// A WANT is a **ceiling, not an exact demand**: a pin the intersection does not
552/// contain still selects the highest mutual edition it admits, and a pin above
553/// what another participant can drive never lifts the session above the floor,
554/// because the ceiling is applied *after* intersecting the CAN sets.
555///
556/// `Err` means no edition satisfies everyone; the caller must fail the session
557/// before any Join stream opens.
558fn select_workflow_edition(tiers: &[(&str, &SessionOffer)]) -> Result<String, EditionRefusal> {
559    let refuse = || EditionRefusal {
560        tiers: tiers
561            .iter()
562            .map(|(tier, offer)| TierOffer {
563                tier: (*tier).to_string(),
564                can: offer.editions.clone(),
565                want: offer.want().map(str::to_string),
566            })
567            .collect(),
568    };
569    let (_, first) = tiers.first().ok_or_else(&refuse)?;
570
571    first
572        .can()
573        .filter(|edition| {
574            tiers
575                .iter()
576                .all(|(_, offer)| offer.can().any(|other| other == *edition))
577        })
578        .filter(|edition| {
579            tiers
580                .iter()
581                .all(|(_, offer)| offer.want().is_none_or(|want| want_admits(want, edition)))
582        })
583        .max_by_key(|edition| edition_sort_key(edition))
584        .map(str::to_string)
585        .ok_or_else(&refuse)
586}
587
588/// Select the workflow edition governing a session between this runtime and one
589/// peer — the co-located floor (`env ∩ self`) the in-process / `run_local` path
590/// runs at, with the model and runtime being the same build.
591///
592/// Applies `select_workflow_edition`'s rule to the two offers. Editions the
593/// peer offers that this build does not retain are ignored, never accepted on
594/// the assumption they are compatible.
595///
596/// This is the **env-leg** negotiation only: `peer` is named `env` in the
597/// [`EditionRefusal`] diagnostic, which is correct for the one caller (the env
598/// client's handshake). A model-leg two-party negotiation would need its own
599/// tier label, and a three-party route uses [`negotiate_session_floor`].
600pub fn negotiate_workflow_edition(
601    peer: &SessionOffer,
602    runtime: &SessionOffer,
603) -> Result<String, EditionRefusal> {
604    select_workflow_edition(&[("env", peer), ("runtime", runtime)])
605}
606
607/// Why the runtime capped a session below what its peers would have run at.
608#[derive(Debug, Clone, Copy, PartialEq, Eq)]
609pub enum RuntimeCap {
610    /// The runtime's CAN set, not its declaration, is what holds the session
611    /// down: undeclared it would still not reach the peers' edition. Upgrading
612    /// the runtime unlocks it.
613    Capability,
614    /// The runtime's declared WANT holds the session below what its own CAN set
615    /// would otherwise have reached. This is the sticky model working as
616    /// intended, not a defect.
617    Declaration,
618}
619
620/// The reconciled workflow edition produced by [`negotiate_session_floor`].
621#[derive(Debug, Clone, PartialEq, Eq)]
622pub struct SessionFloor {
623    /// The edition the session runs at: `select_workflow_edition` across env,
624    /// model, AND this runtime.
625    pub selected_workflow_edition: String,
626    /// The edition env + model alone would have used (always >= `selected`). When
627    /// it differs from `selected`, the runtime is the limiting tier — see
628    /// [`runtime_cap`](Self::runtime_cap).
629    pub desired_workflow_edition: String,
630    /// Why the runtime capped the session, or `None` when it did not.
631    pub runtime_cap: Option<RuntimeCap>,
632}
633
634impl SessionFloor {
635    /// Whether the runtime capped the session below what env + model would have
636    /// used on their own. The run is still safe at `selected_workflow_edition`
637    /// (all three speak it); see [`runtime_cap`](Self::runtime_cap) for whether
638    /// that is a limitation or a declaration.
639    pub fn runtime_limited(&self) -> bool {
640        self.runtime_cap.is_some()
641    }
642}
643
644/// Reconcile the route's workflow edition across env, model, and this runtime.
645///
646/// The runtime decode-rebuilds env<->model envelopes (prost drops fields it does
647/// not know), so a session runs at an edition all three support — never the
648/// env<->model pairwise max — or an edition-gated field would be silently stripped
649/// crossing an older runtime. The result also carries the env+model `desired`
650/// edition, and why the runtime capped it, so the caller can tell a runtime that
651/// *cannot* keep up from one that deliberately declares an older edition.
652/// Protocol generation is not reconciled here — it is gated by equality at each
653/// pairwise handshake, so all three already share it.
654///
655/// `Err` when the three share no edition; the caller must then fail the session
656/// before any Join stream opens.
657pub fn negotiate_session_floor(
658    env: &SessionOffer,
659    model: &SessionOffer,
660    runtime: &SessionOffer,
661) -> Result<SessionFloor, EditionRefusal> {
662    let selected_workflow_edition =
663        select_workflow_edition(&[("env", env), ("model", model), ("runtime", runtime)])?;
664    // env+model alone always admit at least `selected` (it clears both their CAN
665    // sets and both their ceilings), so this is Ok; fall back defensively.
666    let desired_workflow_edition = select_workflow_edition(&[("env", env), ("model", model)])
667        .unwrap_or_else(|_| selected_workflow_edition.clone());
668    let runtime_cap = (desired_workflow_edition != selected_workflow_edition).then(|| {
669        // Attribute the cap to the runtime's declaration only when dropping that
670        // declaration would actually lift the session: what its CAN set alone
671        // admits. Asking whether its CAN set contains `desired` would blame the
672        // CAN set for a runtime that is both older AND declared below its own max,
673        // telling the operator to upgrade when the declaration is the real pin.
674        let undeclared = SessionOffer {
675            editions: runtime.editions.clone(),
676            preferred: None,
677        };
678        // Relaxing one ceiling never refuses what `selected` already cleared.
679        let capable =
680            select_workflow_edition(&[("env", env), ("model", model), ("runtime", &undeclared)])
681                .unwrap_or_else(|_| selected_workflow_edition.clone());
682        if edition_sort_key(&selected_workflow_edition) < edition_sort_key(&capable) {
683            RuntimeCap::Declaration
684        } else {
685            RuntimeCap::Capability
686        }
687    });
688    Ok(SessionFloor {
689        selected_workflow_edition,
690        desired_workflow_edition,
691        runtime_cap,
692    })
693}
694
695/// Whether a client's handshake offer is compatible with this server: the only
696/// handshake-level decision is protocol generation (a hard, full-restart break).
697///
698/// The workflow **edition** is NOT decided here — only the runtime sees every
699/// participant, so it is the sole edition authority (via [`negotiate_session_floor`],
700/// which degenerates to `env ∩ self` when the model and runtime are the same build).
701/// A generation-compatible peer that shares no edition handshakes fine and then
702/// fails at the runtime's floor, with a clearer all-tiers diagnostic. Env and model
703/// servers use this same function, so the verdict cannot drift between services.
704pub fn evaluate_handshake(client_protocol_generation: &str) -> bool {
705    is_protocol_generation_supported(client_protocol_generation)
706}
707
708/// The core handshake request this build sends as a gRPC client: its protocol
709/// generation, the editions it CAN drive, the edition it declares (WANT), its
710/// advertised capabilities, and PeerInfo. Shared by the env and model clients
711/// (each wraps it in its service-specific request) so the request shape cannot
712/// drift between services.
713///
714/// `declared` is the caller's pinned edition; `None` declares
715/// [`CURRENT_WORKFLOW_EDITION`], which is `max(can)` for this build and therefore
716/// leaves negotiation exactly where it was before the field existed.
717pub fn core_handshake_request(
718    component: &str,
719    capabilities: &[&str],
720    declared: Option<&str>,
721) -> core::v1::HandshakeRequest {
722    core::v1::HandshakeRequest {
723        protocol_generation: PROTOCOL_GENERATION.to_string(),
724        peer_info: Some(peer_info(component)),
725        capabilities: capability_map(capabilities),
726        supported_workflow_editions: supported_workflow_editions(),
727        preferred_workflow_edition: declared_workflow_edition(declared).to_string(),
728    }
729}
730
731/// The human-facing rejection message when a client's protocol generation does not
732/// match this server. Generation is the ONLY handshake-level rejection; an edition
733/// mismatch is decided — and diagnosed — by the runtime's floor, not here. Shared by
734/// the env and model servers so the rejection prose cannot drift.
735pub fn generation_mismatch_message(client_protocol_generation: &str) -> String {
736    format!(
737        "protocol generation {client_protocol_generation} not compatible with server \
738         {PROTOCOL_GENERATION}"
739    )
740}
741
742/// Return supported workflow editions as owned strings for protobuf messages.
743pub fn supported_workflow_editions() -> Vec<String> {
744    SUPPORTED_WORKFLOW_EDITIONS
745        .iter()
746        .map(|edition| (*edition).to_string())
747        .collect()
748}
749
750/// Return a handshake capability map for the given capability names.
751///
752/// # Value grammar
753///
754/// A handshake capability map is keyed by capability name; the value is the
755/// literal string `"true"` and nothing else is defined by this protocol
756/// generation. Every RLMesh emitter goes through this function, so every value
757/// RLMesh puts on the wire is `"true"`, and [`has_capability`] is the matching
758/// reader: a key whose value is anything else — `"1"`, `"yes"`, `"TRUE"`, or
759/// the empty string — reads as NOT advertised, exactly like an absent key. A
760/// third-party peer that wants a capability honored must therefore send
761/// `"true"` verbatim.
762///
763/// Capabilities are advisory, so this strictness is safe in both directions:
764/// the worst case of an unrecognized value is the feature staying off.
765pub fn capability_map(names: &[&str]) -> HashMap<String, String> {
766    names
767        .iter()
768        .map(|name| ((*name).to_string(), "true".to_string()))
769        .collect()
770}
771
772/// Whether a peer's handshake capability map advertises the named capability.
773///
774/// Present means the key is mapped to the literal `"true"` (see
775/// [`capability_map`] for the value grammar). An absent key, an empty value, or
776/// any other value is read as absent — never as present — so an advisory
777/// feature a peer spelled differently stays off rather than being negotiated on
778/// a guess.
779pub fn has_capability(map: &HashMap<String, String>, name: &str) -> bool {
780    map.get(name).is_some_and(|value| value == "true")
781}
782
783pub mod core {
784    pub mod v1 {
785        tonic::include_proto!("rlmesh.core.v1");
786    }
787}
788
789/// Advisory runtime identity supplied by a non-Rust host (e.g. the Python SDK)
790/// to enrich the handshake [`PeerInfo`](core::v1::PeerInfo).
791///
792/// Every field is optional and best-effort. When set process-wide via
793/// [`set_peer_info_override`], [`peer_info`] merges these values over the
794/// Rust-detected defaults: a non-empty override field wins, an empty/absent one
795/// falls back to the Rust-detected value (`os`/`arch`/`package_version`). The
796/// `component` passed to [`peer_info`] is always honored; an override
797/// `component` is ignored so each call site keeps naming itself.
798///
799/// This is purely additive diagnostics: PeerInfo never gates compatibility.
800#[derive(Debug, Clone, Default, PartialEq, Eq)]
801pub struct PeerInfoOverride {
802    /// Implementation language, e.g. `"python"`. Empty leaves the Rust default.
803    pub language: String,
804    /// Language runtime version, e.g. `"3.11.4"`.
805    pub language_version: String,
806    /// Package/build version of the host SDK. Empty falls back to this crate's.
807    pub package_version: String,
808    /// Operating system, e.g. `"linux"`, `"macos"`. Empty falls back to the
809    /// Rust-detected [`std::env::consts::OS`].
810    pub os: String,
811    /// OS version/release.
812    pub os_version: String,
813    /// CPU architecture, e.g. `"x86_64"`. Empty falls back to the Rust-detected
814    /// [`std::env::consts::ARCH`].
815    pub arch: String,
816    /// High-value framework versions for debugging (e.g. `{"numpy":"1.26.4"}`).
817    pub framework_versions: HashMap<String, String>,
818    /// Additional advisory key/value diagnostics.
819    pub extra: HashMap<String, String>,
820}
821
822/// Process-wide host identity override consulted by [`peer_info`].
823///
824/// `None` (the default) means no override: pure-Rust peers handshake exactly as
825/// before. A Python-hosted process sets this once at import; the value applies
826/// to every handshake the process performs (it is the only host).
827static PEER_INFO_OVERRIDE: RwLock<Option<PeerInfoOverride>> = RwLock::new(None);
828
829/// Install (or replace) the process-wide [`PeerInfoOverride`] consulted by
830/// [`peer_info`]. Intended for non-Rust hosts (the Python SDK) to report their
831/// real runtime. Idempotent and thread-safe; passing the value again overwrites.
832pub fn set_peer_info_override(info: PeerInfoOverride) {
833    if let Ok(mut guard) = PEER_INFO_OVERRIDE.write() {
834        *guard = Some(info);
835    }
836}
837
838/// Build advisory [`PeerInfo`](core::v1::PeerInfo) diagnostics for a handshake.
839///
840/// `component` names the emitting participant (e.g. `"rlmesh-runtime"`,
841/// `"rlmesh-env"`, `"rlmesh-model"`). The build version, language, OS and arch
842/// default to this crate's compile environment (`language="rust"`, empty
843/// `language_version`/`os_version`/`framework_versions`).
844///
845/// When a process-wide [`PeerInfoOverride`] has been installed via
846/// [`set_peer_info_override`] (e.g. by the Python SDK), its non-empty fields win
847/// over the Rust defaults, falling back to the Rust-detected
848/// `os`/`arch`/`package_version` for any empty override field. The `component`
849/// argument is always preserved so each call site keeps naming itself. PeerInfo
850/// is advisory only and never gates compatibility.
851pub fn peer_info(component: &str) -> core::v1::PeerInfo {
852    let mut extra = HashMap::new();
853    extra.insert(
854        "rlmesh.workflow.base".to_string(),
855        WORKFLOW_EDITION_BASE.to_string(),
856    );
857    extra.insert(
858        "rlmesh.workflow.edition".to_string(),
859        CURRENT_WORKFLOW_EDITION.to_string(),
860    );
861    extra.insert("rlmesh.build.cohort".to_string(), BUILD_COHORT.to_string());
862    extra.insert("rlmesh.build.source".to_string(), BUILD_SOURCE.to_string());
863
864    let mut info = core::v1::PeerInfo {
865        component: component.to_string(),
866        package_version: env!("CARGO_PKG_VERSION").to_string(),
867        language: "rust".to_string(),
868        language_version: String::new(),
869        os: std::env::consts::OS.to_string(),
870        os_version: String::new(),
871        arch: std::env::consts::ARCH.to_string(),
872        framework_versions: HashMap::new(),
873        extra,
874    };
875
876    if let Ok(guard) = PEER_INFO_OVERRIDE.read()
877        && let Some(over) = guard.as_ref()
878    {
879        // Python (or other host) values win when present; empty fields keep the
880        // Rust-detected fallback. `component` is never overridden.
881        if !over.language.is_empty() {
882            info.language = over.language.clone();
883        }
884        if !over.language_version.is_empty() {
885            info.language_version = over.language_version.clone();
886        }
887        if !over.package_version.is_empty() {
888            info.package_version = over.package_version.clone();
889        }
890        if !over.os.is_empty() {
891            info.os = over.os.clone();
892        }
893        if !over.os_version.is_empty() {
894            info.os_version = over.os_version.clone();
895        }
896        if !over.arch.is_empty() {
897            info.arch = over.arch.clone();
898        }
899        if !over.framework_versions.is_empty() {
900            info.framework_versions = over.framework_versions.clone();
901        }
902        if !over.extra.is_empty() {
903            info.extra.extend(over.extra.clone());
904        }
905    }
906
907    info
908}
909
910pub mod env {
911    pub mod v1 {
912        tonic::include_proto!("rlmesh.env.v1");
913    }
914}
915
916pub mod spaces {
917    pub mod v1 {
918        tonic::include_proto!("rlmesh.spaces.v1");
919    }
920}
921
922pub mod model {
923    pub mod v1 {
924        tonic::include_proto!("rlmesh.model.v1");
925    }
926}
927
928/// The endpoint-local split of `JoinResponse.endpoint_total_ns` (ns), plus the
929/// pre-handler wait and slot depth a pipelining model endpoint reports.
930///
931/// A zero duration is "not measured": a peer built before these fields existed
932/// sends none of them, and a reader sees this default. The gauges are `Option`
933/// because for them a measured zero is a real sample (an even vector has zero
934/// skew; an engine that just evicted holds zero episodes) that must stay
935/// distinguishable from a peer that never measures. `in_flight` is model-only;
936/// `lane_skew_ns` is env-only.
937#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
938pub struct EndpointPhases {
939    /// Decoding the request off the wire.
940    pub decode_ns: u64,
941    /// The env or model implementation's own work, user code included.
942    pub user_ns: u64,
943    /// Encoding the response onto the wire.
944    pub encode_ns: u64,
945    /// Wait before the handler ran. Model: concurrency permit, route gate,
946    /// handler lock. Env: the env lock, which serializes ops on one env.
947    pub queue_ns: u64,
948    /// Adapter work inside `user_ns` (observation assembly + action apply) —
949    /// a sub-span, not additive: the model's own forward is `user_ns -
950    /// adapter_ns`. Model-only, and zero for a spec-less route.
951    pub adapter_ns: u64,
952    /// Episodes whose frame-stack windows the endpoint's adapter engine held
953    /// when the response was stamped, across all routes. Model-only; `None`
954    /// from a handler that keeps no such accounting.
955    pub held_episodes: Option<u32>,
956    /// Bytes those held frame-stack windows occupy. Model-only; `None` from a
957    /// handler that keeps no such accounting.
958    pub held_state_bytes: Option<u64>,
959    /// Requests holding a concurrency slot when this request was dispatched to
960    /// its handler (>= 1): slot occupancy, not parallelism.
961    pub in_flight: u32,
962    /// Straggler skew across a vector env's lanes for this op — see
963    /// [`lane_skew_ns`]. Env-only; `None` from an env that does not time its
964    /// lanes, `Some(0)` from one whose lanes were even.
965    pub lane_skew_ns: Option<u64>,
966}
967
968impl EndpointPhases {
969    /// Wire form of a measurement: an unmeasured (zero) phase is left off the
970    /// message rather than sent as a `0` a reader cannot tell from silence.
971    pub fn reported(ns: u64) -> Option<u64> {
972        (ns != 0).then_some(ns)
973    }
974
975    /// Build a split from measured phase durations, saturating at `u64::MAX` ns.
976    pub fn from_durations(
977        decode: std::time::Duration,
978        user: std::time::Duration,
979        encode: std::time::Duration,
980    ) -> Self {
981        fn ns(duration: std::time::Duration) -> u64 {
982            duration.as_nanos().min(u128::from(u64::MAX)) as u64
983        }
984        Self {
985            decode_ns: ns(decode),
986            user_ns: ns(user),
987            encode_ns: ns(encode),
988            ..Self::default()
989        }
990    }
991
992    /// Fold an inner layer's own split into this layer's decode/call/encode
993    /// measurement. The inner layer's decode and encode accumulate into the
994    /// outer ones, and `user_ns` keeps the rest of the call — the inner user
995    /// work plus whatever it cost to reach it (GIL wait, adapter overhead) —
996    /// so the phases still sum to this layer's whole measurement. An inner
997    /// layer that measures nothing, or whose wire share does not even fit
998    /// inside the measured call, leaves the whole call as user time.
999    pub fn nest(decode_ns: u64, call_ns: u64, encode_ns: u64, inner: Self) -> Self {
1000        // Lane skew is the inner env's alone; an outer layer never measures it.
1001        let inner_wire = inner.decode_ns.saturating_add(inner.encode_ns);
1002        if inner_wire > call_ns {
1003            return Self {
1004                decode_ns,
1005                user_ns: call_ns,
1006                encode_ns,
1007                lane_skew_ns: inner.lane_skew_ns,
1008                ..Self::default()
1009            };
1010        }
1011        Self {
1012            decode_ns: decode_ns.saturating_add(inner.decode_ns),
1013            user_ns: call_ns - inner_wire,
1014            encode_ns: encode_ns.saturating_add(inner.encode_ns),
1015            lane_skew_ns: inner.lane_skew_ns,
1016            ..Self::default()
1017        }
1018    }
1019
1020    /// Read the split an env peer stamped; all-zero for a peer that sends none.
1021    pub fn from_env_response(response: &env::v1::JoinResponse) -> Self {
1022        Self {
1023            decode_ns: response.decode_ns.unwrap_or(0),
1024            user_ns: response.user_ns.unwrap_or(0),
1025            encode_ns: response.encode_ns.unwrap_or(0),
1026            queue_ns: response.queue_ns.unwrap_or(0),
1027            lane_skew_ns: response.lane_skew_ns,
1028            ..Self::default()
1029        }
1030    }
1031
1032    /// Read the split a model peer stamped; all-zero for a peer that sends none.
1033    pub fn from_model_response(response: &model::v1::JoinResponse) -> Self {
1034        Self {
1035            decode_ns: response.decode_ns.unwrap_or(0),
1036            user_ns: response.user_ns.unwrap_or(0),
1037            encode_ns: response.encode_ns.unwrap_or(0),
1038            queue_ns: response.queue_ns.unwrap_or(0),
1039            in_flight: response.in_flight.unwrap_or(0),
1040            adapter_ns: response.adapter_ns.unwrap_or(0),
1041            held_episodes: response.held_episodes,
1042            held_state_bytes: response.held_state_bytes,
1043            ..Self::default()
1044        }
1045    }
1046}
1047
1048/// Nanoseconds elapsed since `started_at`, saturating instead of wrapping.
1049pub fn elapsed_ns(started_at: std::time::Instant) -> u64 {
1050    started_at.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64
1051}
1052
1053/// Straggler skew across a vector env's lanes for one op: how much longer the
1054/// slowest lane took than the median lane, in nanoseconds.
1055///
1056/// One scalar per op, so a 32-lane env costs one metric rather than 32 series,
1057/// and the aggregator's p50/p95/p99 over it answer how often and how badly a
1058/// single lane gates the batch the other lanes ride in. A shifted-but-even
1059/// vector reads zero; one bad lane among healthy ones reads its full excess.
1060///
1061/// `lane_ns` is reordered in place and never grown — pass a scratch buffer the
1062/// env keeps across steps. Fewer than two lanes have nothing to compare and
1063/// read zero. The median is the lower one for an even lane count, so a
1064/// two-lane vector still reports its straggler.
1065pub fn lane_skew_ns(lane_ns: &mut [u64]) -> u64 {
1066    if lane_ns.len() < 2 {
1067        return 0;
1068    }
1069    let (_, median, slower) = lane_ns.select_nth_unstable((lane_ns.len() - 1) / 2);
1070    let median = *median;
1071    slower
1072        .iter()
1073        .copied()
1074        .max()
1075        .unwrap_or(median)
1076        .saturating_sub(median)
1077}
1078
1079/// A completing lane's final info from a step's `infos`, `lane` being its
1080/// position among the `width` lanes the step covered. An explicit
1081/// `final_info` entry wins (masked by `_final_info`). Otherwise a single-lane
1082/// step's info IS the final info, and a vector step's lane is sliced out of the
1083/// Gymnasium vector-info layout. `None` when the lane has nothing.
1084pub fn lane_final_info(
1085    info: Option<&spaces::v1::MetaMap>,
1086    lane: usize,
1087    width: usize,
1088) -> Option<spaces::v1::MetaMap> {
1089    use spaces::v1::meta_value::Kind;
1090    let info = info?;
1091    let Some(final_info) = info.entries.get("final_info") else {
1092        if width == 1 {
1093            return Some(info.clone());
1094        }
1095        let sliced = vector_info_lane(info, lane, width);
1096        return (!sliced.entries.is_empty()).then_some(sliced);
1097    };
1098    let is_present = match info.entries.get("_final_info") {
1099        Some(mask) => meta_bool_at(mask, lane).unwrap_or(false),
1100        None => width == 1,
1101    };
1102    if !is_present {
1103        return None;
1104    }
1105    match &final_info.kind {
1106        Some(Kind::Map(map)) => Some(map.clone()),
1107        Some(Kind::List(list)) => match &list.items.get(lane)?.kind {
1108            Some(Kind::Map(map)) => Some(map.clone()),
1109            _ => None,
1110        },
1111        _ => None,
1112    }
1113}
1114
1115/// One lane of a Gymnasium vector info: each `key` holds a per-lane array or a
1116/// nested info map, and its `_key` mask marks the lanes that actually set it.
1117fn vector_info_lane(info: &spaces::v1::MetaMap, lane: usize, width: usize) -> spaces::v1::MetaMap {
1118    use spaces::v1::meta_value::Kind;
1119    let entries = info
1120        .entries
1121        .iter()
1122        .filter(|(key, _)| {
1123            !key.strip_prefix('_')
1124                .is_some_and(|masked| info.entries.contains_key(masked))
1125        })
1126        .filter(|(key, _)| {
1127            info.entries
1128                .get(&format!("_{key}"))
1129                .is_none_or(|mask| meta_bool_at(mask, lane) == Some(true))
1130        })
1131        .filter_map(|(key, value)| {
1132            let lane_value = match &value.kind {
1133                Some(Kind::Map(map)) => spaces::v1::MetaValue {
1134                    kind: Some(Kind::Map(vector_info_lane(map, lane, width))),
1135                },
1136                Some(Kind::List(list)) if list.items.len() == width => list.items[lane].clone(),
1137                _ => return None,
1138            };
1139            Some((key.clone(), lane_value))
1140        })
1141        .collect();
1142    spaces::v1::MetaMap { entries }
1143}
1144
1145fn meta_bool_at(value: &spaces::v1::MetaValue, lane: usize) -> Option<bool> {
1146    use spaces::v1::meta_value::Kind;
1147    match &value.kind {
1148        Some(Kind::List(list)) => match list.items.get(lane)?.kind {
1149            Some(Kind::Bool(flag)) => Some(flag),
1150            _ => None,
1151        },
1152        _ => None,
1153    }
1154}
1155
1156/// The `rlmesh.toml` array scan `build.rs` reads the retained edition list
1157/// with, compiled into the test build so `cargo test` covers the real parser —
1158/// a `#[test]` inside a build script never runs.
1159#[cfg(test)]
1160#[path = "../build_manifest.rs"]
1161mod build_manifest;
1162
1163#[cfg(test)]
1164mod tests {
1165    use super::{
1166        CURRENT_WORKFLOW_EDITION, PROTOCOL_GENERATION, SUPPORTED_WORKFLOW_EDITIONS, SessionOffer,
1167        evaluate_handshake, is_protocol_generation_supported, negotiate_session_floor,
1168        negotiate_workflow_edition, supported_workflow_editions,
1169    };
1170
1171    /// One tier of a negotiation case: what it CAN drive, and what it WANTs
1172    /// (`None` = undeclared, which negotiation reads as `max(can)`).
1173    fn tier(can: &[&str], want: Option<&str>) -> SessionOffer {
1174        SessionOffer {
1175            editions: offer(can),
1176            preferred: want.map(str::to_string),
1177        }
1178    }
1179
1180    #[test]
1181    fn lane_final_info_reads_each_gymnasium_info_layout() {
1182        use super::lane_final_info;
1183        use super::spaces::v1::meta_value::Kind;
1184        use super::spaces::v1::{MetaList, MetaMap, MetaValue};
1185
1186        let value = |kind: Kind| MetaValue { kind: Some(kind) };
1187        let list = |items: Vec<MetaValue>| value(Kind::List(MetaList { items }));
1188        let map = |entries: Vec<(&str, MetaValue)>| MetaMap {
1189            entries: entries
1190                .into_iter()
1191                .map(|(key, value)| (key.to_string(), value))
1192                .collect(),
1193        };
1194        let success = |flag: bool| map(vec![("is_success", value(Kind::Bool(flag)))]);
1195
1196        let scalar = success(false);
1197        assert_eq!(lane_final_info(Some(&scalar), 0, 1), Some(success(false)));
1198        assert_eq!(lane_final_info(None, 0, 2), None);
1199
1200        let explicit = map(vec![
1201            (
1202                "final_info",
1203                list(vec![
1204                    value(Kind::Map(success(true))),
1205                    value(Kind::Map(MetaMap::default())),
1206                ]),
1207            ),
1208            (
1209                "_final_info",
1210                list(vec![value(Kind::Bool(true)), value(Kind::Bool(false))]),
1211            ),
1212        ]);
1213        assert_eq!(lane_final_info(Some(&explicit), 0, 2), Some(success(true)));
1214        assert_eq!(lane_final_info(Some(&explicit), 1, 2), None);
1215
1216        // No masks: a lane-wide array is still per-lane.
1217        let unmasked = map(vec![(
1218            "is_success",
1219            list(vec![value(Kind::Bool(false)), value(Kind::Bool(true))]),
1220        )]);
1221        assert_eq!(lane_final_info(Some(&unmasked), 1, 2), Some(success(true)));
1222
1223        let masked_out = map(vec![
1224            (
1225                "is_success",
1226                list(vec![value(Kind::Bool(true)), value(Kind::Bool(false))]),
1227            ),
1228            (
1229                "_is_success",
1230                list(vec![value(Kind::Bool(true)), value(Kind::Bool(false))]),
1231            ),
1232        ]);
1233        assert_eq!(lane_final_info(Some(&masked_out), 1, 2), None);
1234    }
1235
1236    #[test]
1237    fn phases_from_a_peer_that_stamps_nothing_read_back_as_zero() {
1238        use super::{EndpointPhases, env, model};
1239
1240        // An older peer sends neither the split nor the queue scalars.
1241        let env_response = env::v1::JoinResponse {
1242            endpoint_total_ns: Some(1_000),
1243            ..Default::default()
1244        };
1245        assert_eq!(
1246            EndpointPhases::from_env_response(&env_response),
1247            EndpointPhases::default()
1248        );
1249
1250        let model_response = model::v1::JoinResponse {
1251            endpoint_total_ns: Some(1_000),
1252            ..Default::default()
1253        };
1254        assert_eq!(
1255            EndpointPhases::from_model_response(&model_response),
1256            EndpointPhases::default()
1257        );
1258
1259        // A peer that does stamp them reads back verbatim.
1260        let stamped = model::v1::JoinResponse {
1261            endpoint_total_ns: Some(1_000),
1262            decode_ns: Some(10),
1263            user_ns: Some(20),
1264            encode_ns: Some(30),
1265            queue_ns: Some(40),
1266            in_flight: Some(3),
1267            adapter_ns: Some(15),
1268            held_episodes: Some(5),
1269            held_state_bytes: Some(6_000),
1270            ..Default::default()
1271        };
1272        assert_eq!(
1273            EndpointPhases::from_model_response(&stamped),
1274            EndpointPhases {
1275                decode_ns: 10,
1276                user_ns: 20,
1277                encode_ns: 30,
1278                queue_ns: 40,
1279                in_flight: 3,
1280                adapter_ns: 15,
1281                held_episodes: Some(5),
1282                held_state_bytes: Some(6_000),
1283                lane_skew_ns: None,
1284            }
1285        );
1286
1287        // Lane skew rides the env response only.
1288        let skewed = env::v1::JoinResponse {
1289            endpoint_total_ns: Some(1_000),
1290            lane_skew_ns: Some(880),
1291            ..Default::default()
1292        };
1293        assert_eq!(
1294            EndpointPhases::from_env_response(&skewed).lane_skew_ns,
1295            Some(880)
1296        );
1297        // A measured zero (an even vector) is a sample, not an absence.
1298        let even = env::v1::JoinResponse {
1299            lane_skew_ns: Some(0),
1300            ..Default::default()
1301        };
1302        assert_eq!(
1303            EndpointPhases::from_env_response(&even).lane_skew_ns,
1304            Some(0)
1305        );
1306
1307        // An unmeasured phase is left off the wire rather than sent as a zero.
1308        assert_eq!(EndpointPhases::reported(0), None);
1309        assert_eq!(EndpointPhases::reported(7), Some(7));
1310    }
1311
1312    #[test]
1313    fn nesting_folds_an_inner_split_into_the_outer_one() {
1314        use super::EndpointPhases;
1315
1316        // An inner layer that measures nothing leaves the whole call as user time.
1317        assert_eq!(
1318            EndpointPhases::nest(1, 10, 2, EndpointPhases::default()),
1319            EndpointPhases {
1320                decode_ns: 1,
1321                user_ns: 10,
1322                encode_ns: 2,
1323                ..EndpointPhases::default()
1324            }
1325        );
1326
1327        // One that does splits the call: its own decode/encode join this layer's,
1328        // and user keeps the remainder of the call.
1329        let inner = EndpointPhases {
1330            decode_ns: 3,
1331            user_ns: 5,
1332            encode_ns: 2,
1333            ..EndpointPhases::default()
1334        };
1335        assert_eq!(
1336            EndpointPhases::nest(1, 10, 2, inner),
1337            EndpointPhases {
1338                decode_ns: 4,
1339                user_ns: 5,
1340                encode_ns: 4,
1341                ..EndpointPhases::default()
1342            }
1343        );
1344
1345        // A call longer than the inner layer's own accounting (GIL wait, adapter
1346        // overhead) keeps the residual in user, so the phases still sum to this
1347        // layer's whole measurement: 1 + 14 + 2 == 4 + 9 + 4.
1348        assert_eq!(
1349            EndpointPhases::nest(1, 14, 2, inner),
1350            EndpointPhases {
1351                decode_ns: 4,
1352                user_ns: 9,
1353                encode_ns: 4,
1354                ..EndpointPhases::default()
1355            }
1356        );
1357
1358        // An inner split whose wire share does not fit inside the measured call
1359        // is inconsistent; folding it would break the sum, so none of it folds.
1360        assert_eq!(
1361            EndpointPhases::nest(1, 4, 2, inner),
1362            EndpointPhases {
1363                decode_ns: 1,
1364                user_ns: 4,
1365                encode_ns: 2,
1366                ..EndpointPhases::default()
1367            }
1368        );
1369    }
1370
1371    #[test]
1372    fn lane_skew_is_the_slowest_lane_over_the_median_one() {
1373        use super::lane_skew_ns;
1374
1375        // One straggler among healthy lanes: its full excess over a typical lane.
1376        assert_eq!(lane_skew_ns(&mut [10, 10, 10, 10, 10, 10, 10, 900]), 890);
1377        // An evenly slow vector is not a straggler — the whole distribution moved,
1378        // which `endpoint.user` already shows.
1379        assert_eq!(lane_skew_ns(&mut [900; 8]), 0);
1380        // Two lanes compare against the faster one, so the straggler still reads.
1381        assert_eq!(lane_skew_ns(&mut [10, 100]), 90);
1382        // A lane faster than its peers is not skew.
1383        assert_eq!(lane_skew_ns(&mut [1, 100, 100, 100]), 0);
1384        // Nothing to compare.
1385        assert_eq!(lane_skew_ns(&mut [42]), 0);
1386        assert_eq!(lane_skew_ns(&mut []), 0);
1387    }
1388
1389    #[test]
1390    fn nesting_keeps_the_inner_envs_lane_skew() {
1391        use super::EndpointPhases;
1392
1393        let measured = EndpointPhases {
1394            user_ns: 5,
1395            lane_skew_ns: Some(77),
1396            ..EndpointPhases::default()
1397        };
1398        assert_eq!(
1399            EndpointPhases::nest(1, 10, 2, measured).lane_skew_ns,
1400            Some(77)
1401        );
1402
1403        // An env that times its lanes but reports no split of its own still gets
1404        // the skew through.
1405        let skew_only = EndpointPhases {
1406            lane_skew_ns: Some(77),
1407            ..EndpointPhases::default()
1408        };
1409        assert_eq!(
1410            EndpointPhases::nest(1, 10, 2, skew_only).lane_skew_ns,
1411            Some(77)
1412        );
1413    }
1414
1415    fn offer(editions: &[&str]) -> Vec<String> {
1416        editions.iter().map(|edition| edition.to_string()).collect()
1417    }
1418
1419    #[test]
1420    fn peer_info_default_then_override_merges_python_with_rust_fallback() {
1421        use super::{PeerInfoOverride, peer_info, set_peer_info_override};
1422        use std::collections::HashMap;
1423
1424        // No override installed yet: a pure-Rust peer reports the Rust defaults.
1425        let rust_info = peer_info("rlmesh-env");
1426        assert_eq!(rust_info.component, "rlmesh-env");
1427        assert_eq!(rust_info.language, "rust");
1428        assert!(rust_info.language_version.is_empty());
1429        assert!(rust_info.framework_versions.is_empty());
1430        let detected_os = rust_info.os.clone();
1431        let detected_arch = rust_info.arch.clone();
1432        let detected_pkg = rust_info.package_version.clone();
1433
1434        // Install a Python-style override with `os`/`package_version` left empty
1435        // so the Rust-detected fallbacks fill them.
1436        let mut frameworks = HashMap::new();
1437        frameworks.insert("numpy".to_string(), "1.26.4".to_string());
1438        set_peer_info_override(PeerInfoOverride {
1439            language: "python".to_string(),
1440            language_version: "3.11.4".to_string(),
1441            package_version: String::new(),
1442            os: String::new(),
1443            os_version: "ubuntu-22.04".to_string(),
1444            arch: "aarch64".to_string(),
1445            framework_versions: frameworks,
1446            extra: HashMap::from([("rlmesh.startup.listen_ms".to_string(), "4200".to_string())]),
1447        });
1448
1449        let py_info = peer_info("rlmesh-env");
1450        // Host-supplied extras ride beside the build keys, never replace them.
1451        assert_eq!(
1452            py_info
1453                .extra
1454                .get("rlmesh.startup.listen_ms")
1455                .map(String::as_str),
1456            Some("4200")
1457        );
1458        assert!(py_info.extra.contains_key("rlmesh.build.cohort"));
1459        // component still names this call site; not taken from the override.
1460        assert_eq!(py_info.component, "rlmesh-env");
1461        // Python values win.
1462        assert_eq!(py_info.language, "python");
1463        assert_eq!(py_info.language_version, "3.11.4");
1464        assert_eq!(py_info.os_version, "ubuntu-22.04");
1465        assert_eq!(py_info.arch, "aarch64");
1466        assert_eq!(
1467            py_info.framework_versions.get("numpy").map(String::as_str),
1468            Some("1.26.4")
1469        );
1470        // Empty override fields fall back to the Rust-detected values.
1471        assert_eq!(py_info.os, detected_os);
1472        assert_eq!(py_info.package_version, detected_pkg);
1473        assert_eq!(
1474            py_info
1475                .extra
1476                .get("rlmesh.workflow.edition")
1477                .map(String::as_str),
1478            Some(CURRENT_WORKFLOW_EDITION)
1479        );
1480        // `arch` was overridden, so it differs from the detected value here.
1481        let _ = detected_arch;
1482    }
1483
1484    #[test]
1485    fn has_capability_reads_advertised_features() {
1486        use super::{capabilities, capability_map, has_capability};
1487        let map = capability_map(&[capabilities::MODEL_CONCURRENT_PREDICT_V1]);
1488        assert!(has_capability(
1489            &map,
1490            capabilities::MODEL_CONCURRENT_PREDICT_V1
1491        ));
1492        assert!(!has_capability(&map, "rlmesh.not.advertised.v1"));
1493        // The documented value grammar: emitters write the literal "true", and
1494        // any other spelling reads as absent rather than as a guessed "on".
1495        assert_eq!(map[capabilities::MODEL_CONCURRENT_PREDICT_V1], "true");
1496        for value in ["1", "yes", "TRUE", ""] {
1497            let odd =
1498                std::collections::HashMap::from([("rlmesh.odd.v1".to_string(), value.to_string())]);
1499            assert!(!has_capability(&odd, "rlmesh.odd.v1"), "{value:?}");
1500        }
1501    }
1502
1503    #[test]
1504    fn env_subset_step_keeps_its_shipped_wire_spelling() {
1505        // Peers built against 0.1.0 advertise the bare `subset_step` key; the
1506        // constant renames the site, never the string on the wire.
1507        assert_eq!(super::capabilities::ENV_SUBSET_STEP, "subset_step");
1508    }
1509
1510    #[test]
1511    fn protocol_generation_is_plain_equality() {
1512        // The only generation check is equality with this build's generation —
1513        // there is no support window. Whitespace is trimmed; anything else is a
1514        // hard mismatch (a deliberate major break).
1515        assert!(is_protocol_generation_supported(PROTOCOL_GENERATION));
1516        assert!(is_protocol_generation_supported(&format!(
1517            " {PROTOCOL_GENERATION} "
1518        )));
1519        assert!(!is_protocol_generation_supported("rlmesh-wire-v2"));
1520        assert!(!is_protocol_generation_supported(""));
1521        assert!(!is_protocol_generation_supported("0.1.0"));
1522    }
1523
1524    #[test]
1525    fn split_editions_recovers_every_supported_edition() {
1526        // The build-time list is comma-separated; today's tree generates one
1527        // entry, so cover the retained-fallback shape here rather than waiting
1528        // for a second edition to be sealed.
1529        const LIST: &str = "2026.09-0.2.0-rc.1,2026.06";
1530        const EDITIONS: [&str; super::edition_count(LIST)] = super::split_editions(LIST);
1531        assert_eq!(EDITIONS, ["2026.09-0.2.0-rc.1", "2026.06"]);
1532        assert_eq!(super::edition_count("2026.06"), 1);
1533    }
1534
1535    #[test]
1536    fn manifest_string_list_reads_both_array_spellings() {
1537        use super::build_manifest::manifest_string_list;
1538
1539        // `bump_version.py` writes the single-line spelling, but a hand-edited
1540        // manifest may spread the array over lines: both must offer the same
1541        // editions, or a retained edition would silently vanish from the build.
1542        let single = "[workflow]\nsupported_editions = [\"2026.09-0.2.0-rc.1\", \"2026.06\"]\n";
1543        let multi = "[workflow]\nsupported_editions = [\n  \"2026.09-0.2.0-rc.1\", # cohort\n  \
1544                     \"2026.06\",\n]\n";
1545        let want = ["2026.09-0.2.0-rc.1".to_string(), "2026.06".to_string()];
1546        assert_eq!(manifest_string_list(single, "supported_editions"), want);
1547        assert_eq!(manifest_string_list(multi, "supported_editions"), want);
1548        assert!(manifest_string_list(single, "current_edition").is_empty());
1549    }
1550
1551    #[test]
1552    fn supported_workflow_editions_lead_with_current() {
1553        // The list is generated from `rlmesh.toml`; whatever it retains, this
1554        // build's own cohort is first, nothing repeats, and the owned-string
1555        // form handed to protobuf mirrors it exactly.
1556        assert!(!SUPPORTED_WORKFLOW_EDITIONS.is_empty());
1557        assert_eq!(SUPPORTED_WORKFLOW_EDITIONS[0], CURRENT_WORKFLOW_EDITION);
1558        let unique: std::collections::BTreeSet<&&str> =
1559            SUPPORTED_WORKFLOW_EDITIONS.iter().collect();
1560        assert_eq!(unique.len(), SUPPORTED_WORKFLOW_EDITIONS.len());
1561        assert!(
1562            SUPPORTED_WORKFLOW_EDITIONS
1563                .iter()
1564                .all(|edition| !edition.trim().is_empty())
1565        );
1566        assert_eq!(
1567            supported_workflow_editions(),
1568            SUPPORTED_WORKFLOW_EDITIONS
1569                .iter()
1570                .map(|edition| (*edition).to_string())
1571                .collect::<Vec<_>>()
1572        );
1573    }
1574
1575    #[test]
1576    fn negotiation_selects_mutual_edition() {
1577        let runtime = SessionOffer::this_build(None);
1578        assert_eq!(
1579            negotiate_workflow_edition(&SessionOffer::new(&[CURRENT_WORKFLOW_EDITION]), &runtime),
1580            Ok(CURRENT_WORKFLOW_EDITION.to_string())
1581        );
1582        assert_eq!(
1583            negotiate_workflow_edition(
1584                &SessionOffer::new(&["2025.01", CURRENT_WORKFLOW_EDITION, "2031.12"]),
1585                &runtime
1586            ),
1587            Ok(CURRENT_WORKFLOW_EDITION.to_string())
1588        );
1589    }
1590
1591    #[test]
1592    fn negotiation_trims_offered_editions() {
1593        let padded = SessionOffer {
1594            editions: vec![format!(" {CURRENT_WORKFLOW_EDITION} ")],
1595            preferred: None,
1596        };
1597        assert_eq!(
1598            negotiate_workflow_edition(&padded, &SessionOffer::this_build(None)),
1599            Ok(CURRENT_WORKFLOW_EDITION.to_string())
1600        );
1601    }
1602
1603    #[test]
1604    fn negotiation_rejects_unknown_or_empty_offers() {
1605        let runtime = SessionOffer::this_build(None);
1606        for offered in [
1607            &[][..],
1608            &[""][..],
1609            &["2026"][..],
1610            &["next"][..],
1611            &["2026.11", "2027.01"][..],
1612        ] {
1613            let refusal = negotiate_workflow_edition(&SessionOffer::new(offered), &runtime)
1614                .expect_err("no mutual edition");
1615            // The refusal names both tiers and both of their sets.
1616            let message = refusal.to_string();
1617            assert!(message.contains("env"), "{message}");
1618            assert!(message.contains("runtime"), "{message}");
1619            assert!(message.contains(CURRENT_WORKFLOW_EDITION), "{message}");
1620        }
1621    }
1622
1623    #[test]
1624    fn evaluate_handshake_gates_generation_only() {
1625        // The handshake decides ONE thing: protocol generation. Editions are the
1626        // runtime's call (the floor), so a generation-ok peer is compatible
1627        // regardless of editions — even with no mutual edition (it fails later at
1628        // the floor, with a clearer all-tiers message).
1629        assert!(evaluate_handshake(PROTOCOL_GENERATION));
1630
1631        // A protocol mismatch is never compatible.
1632        assert!(!evaluate_handshake("rlmesh-wire-v2"));
1633    }
1634
1635    #[test]
1636    fn is_retained_edition_matches_the_window() {
1637        use super::{Edition, is_retained_edition};
1638        let current = Edition::parse(CURRENT_WORKFLOW_EDITION).expect("the current edition parses");
1639        assert!(is_retained_edition(current));
1640        // Trimmed, and this build's cohort spelling names the same arm as its base.
1641        assert_eq!(
1642            Edition::parse(&format!("  {CURRENT_WORKFLOW_EDITION}  ")),
1643            Ok(current)
1644        );
1645        assert_eq!(Edition::parse(super::WORKFLOW_EDITION_BASE), Ok(current));
1646        // An edition no arm implements never resolves, so it is never retained.
1647        assert!(Edition::parse("2099.01").is_err());
1648        assert!(Edition::parse("").is_err());
1649    }
1650
1651    /// The base names the contract; a cohort suffix only identifies a moving
1652    /// build of it. So EVERY cohort spelling of a known base resolves to that
1653    /// base's arm -- including one this build never spells itself, which is what a
1654    /// policy-legal manifest retaining an older base's cohort would carry.
1655    #[test]
1656    fn parse_resolves_any_cohort_spelling_of_a_known_base() {
1657        use super::{Edition, WORKFLOW_EDITION_BASE, parse_retained_edition};
1658
1659        let current = Edition::parse(CURRENT_WORKFLOW_EDITION).expect("the current edition parses");
1660        assert_eq!(Edition::parse(WORKFLOW_EDITION_BASE), Ok(current));
1661        assert_eq!(Edition::parse(CURRENT_WORKFLOW_EDITION), Ok(current));
1662        for foreign in [
1663            format!("{WORKFLOW_EDITION_BASE}-0.0.1-rc.1"),
1664            format!("{WORKFLOW_EDITION_BASE}-dev.deadbeef"),
1665        ] {
1666            assert_eq!(
1667                Edition::parse(&foreign),
1668                Ok(current),
1669                "{foreign} is a cohort of a known base"
1670            );
1671        }
1672        // A base no arm implements is refused, bare or suffixed.
1673        assert!(Edition::parse("2099.01").is_err());
1674        assert!(Edition::parse("2099.01-0.1.0-rc.12").is_err());
1675
1676        // The shared boundary refuses the same names, naming the arrived string
1677        // and the retained list.
1678        assert_eq!(
1679            parse_retained_edition(CURRENT_WORKFLOW_EDITION),
1680            Ok(current)
1681        );
1682        let error = parse_retained_edition(" 2099.01 ").expect_err("no arm implements it");
1683        assert!(
1684            error.contains("\"2099.01\"") && error.contains(CURRENT_WORKFLOW_EDITION),
1685            "expected the arrived name and the retained list, got: {error}"
1686        );
1687    }
1688
1689    /// A declaration is a ceiling, so it is only usable when it admits something
1690    /// this build offers. Every name this build offers is declarable by
1691    /// construction, and so is the bare base on EVERY build — it admits every
1692    /// cohort of itself, which is what makes it the value to write down. A
1693    /// cohort that would leave the ceiling below the whole CAN set is refused
1694    /// here rather than deadlocking negotiation.
1695    #[test]
1696    fn a_declaration_must_leave_this_build_something_to_run() {
1697        use super::{Edition, WORKFLOW_EDITION_BASE, parse_declared_edition};
1698
1699        let current = Edition::parse(CURRENT_WORKFLOW_EDITION).expect("the current edition parses");
1700        for offered in SUPPORTED_WORKFLOW_EDITIONS {
1701            assert_eq!(
1702                parse_declared_edition(offered),
1703                Ok(current),
1704                "{offered} is offered, so it is declarable"
1705            );
1706        }
1707        assert_eq!(parse_declared_edition(WORKFLOW_EDITION_BASE), Ok(current));
1708        assert_eq!(
1709            parse_declared_edition(&format!(" {WORKFLOW_EDITION_BASE} ")),
1710            Ok(current)
1711        );
1712        // A cohort of this base that sorts below everything offered: on a
1713        // sealed build the bare base is offered and admitted, on a prerelease
1714        // or dev build only a higher cohort is, so it is refused naming both
1715        // halves and the base to declare instead.
1716        let stale_cohort = format!("{WORKFLOW_EDITION_BASE}-0.0.0");
1717        match parse_declared_edition(&stale_cohort) {
1718            Ok(edition) => {
1719                assert_eq!(edition, current);
1720                assert!(SUPPORTED_WORKFLOW_EDITIONS.contains(&WORKFLOW_EDITION_BASE));
1721            }
1722            Err(error) => {
1723                assert!(
1724                    error.contains(&stale_cohort)
1725                        && error.contains(CURRENT_WORKFLOW_EDITION)
1726                        && error.contains(&format!("{WORKFLOW_EDITION_BASE:?} base")),
1727                    "expected both halves of the mismatch, got: {error}"
1728                );
1729            }
1730        }
1731        // An unimplemented base still fails on the retained check, unchanged.
1732        assert!(parse_declared_edition("2099.01").is_err());
1733    }
1734
1735    /// The two shapes of a WANT: a bare base is a base-level ceiling, a cohort
1736    /// spelling keeps the exact order.
1737    #[test]
1738    fn want_admits_by_base_when_bare_and_by_key_when_suffixed() {
1739        use super::want_admits;
1740
1741        assert!(want_admits("2026.06", "2026.06"));
1742        assert!(want_admits("2026.06", "2026.06-dev.aaa"));
1743        assert!(want_admits("2026.06", "2026.06-0.1.0-rc.12"));
1744        assert!(want_admits("2026.06", "2026.01"));
1745        assert!(!want_admits("2026.06", "2026.08"));
1746        assert!(!want_admits("2026.06", "2026.08-dev.aaa"));
1747
1748        assert!(want_admits("2026.06-dev.bbb", "2026.06-dev.bbb"));
1749        assert!(want_admits("2026.06-dev.bbb", "2026.06-dev.aaa"));
1750        assert!(want_admits("2026.06-dev.bbb", "2026.06"));
1751        assert!(!want_admits("2026.06-dev.bbb", "2026.06-dev.ccc"));
1752        assert!(!want_admits("2026.06-dev.bbb", "2026.08"));
1753    }
1754
1755    /// Every arm has a defaults row, and every name this build offers or runs
1756    /// under resolves back to an arm. `defaults` is an exhaustive match, so the
1757    /// "has a row" half is enforced by the compiler; this pins the rest.
1758    #[test]
1759    fn every_arm_is_listed_and_has_a_row() {
1760        use super::{Edition, defaults, is_retained_edition};
1761        for edition in Edition::ALL {
1762            // Exhaustive: a new arm does not compile until it is named here, so
1763            // one cannot be added without visiting this test. `Edition::ALL`
1764            // itself is held complete by the retained-list loop below, which
1765            // parses every name this build offers back through it.
1766            match edition {
1767                Edition::E2026_06 => {
1768                    assert!(Edition::ALL.contains(&Edition::E2026_06));
1769                }
1770            }
1771            let row = defaults(*edition);
1772            assert!(!row.trial_index_option_key.is_empty());
1773            assert!(!row.conformance_warning_info_key.is_empty());
1774            assert_eq!(Edition::parse(edition.base()), Ok(*edition));
1775            assert_eq!(edition.to_string(), edition.base());
1776        }
1777        assert_eq!(
1778            Edition::parse(CURRENT_WORKFLOW_EDITION).map(is_retained_edition),
1779            Ok(true)
1780        );
1781        assert_eq!(
1782            Edition::parse(CURRENT_WORKFLOW_EDITION),
1783            Ok(Edition::current())
1784        );
1785        for retained in SUPPORTED_WORKFLOW_EDITIONS {
1786            let edition = Edition::parse(retained)
1787                .unwrap_or_else(|err| panic!("retained edition {retained:?} has no arm: {err}"));
1788            assert!(is_retained_edition(edition));
1789        }
1790    }
1791
1792    #[test]
1793    fn session_floor_picks_highest_all_three_support() {
1794        // Highest edition all three share wins (ranked by edition_sort_key);
1795        // whitespace is trimmed so a padded edition still matches. When all three
1796        // reach the same top edition, the runtime is not limiting.
1797        let env = SessionOffer::new(&["2026.01", " 2026.06 "]);
1798        let model = SessionOffer::new(&["2026.06", "2026.01"]);
1799        let runtime = SessionOffer::new(&["2026.06"]);
1800        let floor = negotiate_session_floor(&env, &model, &runtime).expect("a floor");
1801        assert_eq!(floor.selected_workflow_edition, "2026.06");
1802        assert_eq!(floor.desired_workflow_edition, "2026.06");
1803        assert!(!floor.runtime_limited());
1804    }
1805
1806    #[test]
1807    fn session_floor_flags_runtime_as_limiting_tier() {
1808        // env+model both reach 2026.08, but the runtime only speaks 2026.06, so the
1809        // floor drops to 2026.06 (safe — all three speak it) and the runtime is
1810        // flagged as the tier holding the session back.
1811        let env = SessionOffer::new(&["2026.06", "2026.08"]);
1812        let model = SessionOffer::new(&["2026.06", "2026.08"]);
1813        let runtime = SessionOffer::new(&["2026.06"]);
1814        let floor = negotiate_session_floor(&env, &model, &runtime).expect("a floor");
1815        assert_eq!(floor.selected_workflow_edition, "2026.06");
1816        assert_eq!(floor.desired_workflow_edition, "2026.08");
1817        assert!(floor.runtime_limited());
1818    }
1819
1820    #[test]
1821    fn session_floor_is_none_when_no_common_edition() {
1822        // env+model agree on 2026.08 but the runtime can't speak it, and they can't
1823        // speak the runtime's 2026.06 → no edition all three share → None.
1824        let env = SessionOffer::new(&["2026.08"]);
1825        let model = SessionOffer::new(&["2026.08"]);
1826        let runtime = SessionOffer::new(&["2026.06"]);
1827        assert!(negotiate_session_floor(&env, &model, &runtime).is_err());
1828
1829        // Empty strings never match, so an offer of only "" has no mutual value.
1830        let empty = SessionOffer::new(&[""]);
1831        let ok = SessionOffer::new(&["2026.06"]);
1832        assert!(negotiate_session_floor(&empty, &ok, &ok).is_err());
1833    }
1834
1835    #[test]
1836    fn edition_ordering_prefers_exact_cohort_then_newer_date() {
1837        use super::edition_sort_key;
1838
1839        // Exact moving cohorts beat their own sealed fallback. This lets two
1840        // matching prerelease/dev peers use the newest cohort while still allowing
1841        // fallback to the sealed edition when the moving cohorts differ.
1842        assert!(edition_sort_key("2026.06-0.1.0-rc.1") > edition_sort_key("2026.06"));
1843
1844        // newer-date-wins: a newer date outranks an older one regardless of
1845        // cohort status.
1846        assert!(edition_sort_key("2026.09-0.2.0-beta.1") > edition_sort_key("2026.06"));
1847        assert!(edition_sort_key("2026.09") > edition_sort_key("2026.06-0.1.0-rc.1"));
1848
1849        // deterministic suffix tiebreak: two same-date cohorts order by
1850        // their full suffix, never by iteration order, so two honest builds
1851        // never disagree on the winner.
1852        assert!(edition_sort_key("2026.06-0.1.0-rc.2") > edition_sort_key("2026.06-0.1.0-rc.1"));
1853
1854        // negotiate_workflow_edition applies the same key: offered against a
1855        // hypothetical multi-edition supported set, the highest by key wins. With
1856        // the single supported edition this build ships, the current edition is
1857        // selected when offered alongside older/newer noise.
1858        assert_eq!(
1859            negotiate_workflow_edition(
1860                &SessionOffer::new(&["2025.01", CURRENT_WORKFLOW_EDITION, "2099.12"]),
1861                &SessionOffer::this_build(None)
1862            ),
1863            Ok(CURRENT_WORKFLOW_EDITION.to_string())
1864        );
1865    }
1866
1867    #[test]
1868    fn session_floor_prefers_exact_edition_cohort_over_sealed_fallback() {
1869        // The floor uses edition_sort_key: when all three offer the exact moving
1870        // cohort and its sealed fallback, the exact cohort is selected.
1871        let editions = &["2026.06", "2026.06-0.1.0-rc.1"];
1872        let offer = SessionOffer::new(editions);
1873        let floor = negotiate_session_floor(&offer, &offer, &offer).expect("a floor");
1874        assert_eq!(floor.selected_workflow_edition, "2026.06-0.1.0-rc.1");
1875    }
1876
1877    /// The WANT/CAN rule, end to end: two- and three-party, declared and not.
1878    ///
1879    /// Each case is `(name, [(tier, CAN, WANT)], expected selection)`; `None`
1880    /// expects a refusal that names every tier. Plain strings throughout —
1881    /// negotiation is string-level, and `Edition` typing happens at the boundary
1882    /// the selected name is then parsed through.
1883    #[test]
1884    fn want_can_selection_table() {
1885        type Tier<'a> = (&'a str, &'a [&'a str], Option<&'a str>);
1886        let cases: &[(&str, &[Tier<'_>], Option<&str>)] = &[
1887            // --- nothing declared: exactly the pre-WANT highest-mutual rule ---
1888            (
1889                "two-party, undeclared: highest mutual",
1890                &[
1891                    ("env", &["2026.01", "2026.06"], None),
1892                    ("runtime", &["2026.06"], None),
1893                ],
1894                Some("2026.06"),
1895            ),
1896            (
1897                "three-party, undeclared: highest all three share",
1898                &[
1899                    ("env", &["2026.01", " 2026.06 "], None),
1900                    ("model", &["2026.06", "2026.01"], None),
1901                    ("runtime", &["2026.06"], None),
1902                ],
1903                Some("2026.06"),
1904            ),
1905            (
1906                "three-party, undeclared: the runtime's CAN set caps the floor",
1907                &[
1908                    ("env", &["2026.06", "2026.08"], None),
1909                    ("model", &["2026.06", "2026.08"], None),
1910                    ("runtime", &["2026.06"], None),
1911                ],
1912                Some("2026.06"),
1913            ),
1914            (
1915                "three-party, undeclared: empty intersection refuses",
1916                &[
1917                    ("env", &["2026.08"], None),
1918                    ("model", &["2026.08"], None),
1919                    ("runtime", &["2026.06"], None),
1920                ],
1921                None,
1922            ),
1923            // --- a declared WANT is a ceiling ---
1924            (
1925                "one peer declares below the mutual max: the declared one wins",
1926                &[
1927                    ("env", &["2026.06", "2026.08"], Some("2026.06")),
1928                    ("model", &["2026.06", "2026.08"], None),
1929                    ("runtime", &["2026.06", "2026.08"], None),
1930                ],
1931                Some("2026.06"),
1932            ),
1933            (
1934                "a WANT above what another peer CAN never lifts the floor",
1935                &[
1936                    ("env", &["2026.06", "2026.08", "2026.10"], Some("2026.10")),
1937                    ("model", &["2026.06", "2026.08"], None),
1938                    ("runtime", &["2026.06", "2026.08", "2026.10"], None),
1939                ],
1940                Some("2026.08"),
1941            ),
1942            (
1943                "a WANT the intersection does not contain still selects the \
1944                 highest mutual below it: a pin is a ceiling, not an exact demand",
1945                &[
1946                    ("env", &["2026.06", "2026.08"], Some("2026.07")),
1947                    ("model", &["2026.06", "2026.08"], None),
1948                    ("runtime", &["2026.06", "2026.08"], None),
1949                ],
1950                Some("2026.06"),
1951            ),
1952            (
1953                "a WANT below everything mutual refuses rather than running higher",
1954                &[
1955                    ("env", &["2026.06"], Some("2025.01")),
1956                    ("model", &["2026.06"], None),
1957                    ("runtime", &["2026.06"], None),
1958                ],
1959                None,
1960            ),
1961            (
1962                "the lowest WANT wins when several are declared",
1963                &[
1964                    ("env", &["2026.06", "2026.08", "2026.10"], Some("2026.10")),
1965                    ("model", &["2026.06", "2026.08", "2026.10"], Some("2026.08")),
1966                    (
1967                        "runtime",
1968                        &["2026.06", "2026.08", "2026.10"],
1969                        Some("2026.10"),
1970                    ),
1971                ],
1972                Some("2026.08"),
1973            ),
1974            // --- dev cohorts vs their sealed base ---
1975            (
1976                "undeclared, matching dev cohorts: the exact cohort beats its base",
1977                &[
1978                    ("env", &["2026.06", "2026.06-dev.aaa"], None),
1979                    ("model", &["2026.06", "2026.06-dev.aaa"], None),
1980                    ("runtime", &["2026.06", "2026.06-dev.aaa"], None),
1981                ],
1982                Some("2026.06-dev.aaa"),
1983            ),
1984            (
1985                "differing dev cohorts do not match: both fall back to the sealed base",
1986                &[
1987                    ("env", &["2026.06", "2026.06-dev.aaa"], None),
1988                    ("model", &["2026.06", "2026.06-dev.bbb"], None),
1989                    ("runtime", &["2026.06", "2026.06-dev.aaa"], None),
1990                ],
1991                Some("2026.06"),
1992            ),
1993            (
1994                "a WANT of the bare base admits every cohort of it: the exact cohort still wins",
1995                &[
1996                    ("env", &["2026.06", "2026.06-dev.aaa"], Some("2026.06")),
1997                    ("model", &["2026.06", "2026.06-dev.aaa"], None),
1998                    ("runtime", &["2026.06", "2026.06-dev.aaa"], None),
1999                ],
2000                Some("2026.06-dev.aaa"),
2001            ),
2002            // --- a bare-base WANT against cohort-only builds (the stateVersion value) ---
2003            (
2004                "bare base WANT on both sides, cohort-only CANs: selects the cohort",
2005                &[
2006                    ("env", &["2026.06-dev.aaa"], Some("2026.06")),
2007                    ("runtime", &["2026.06-dev.aaa"], Some("2026.06")),
2008                ],
2009                Some("2026.06-dev.aaa"),
2010            ),
2011            (
2012                "bare base WANT on one side, three cohort-only tiers: selects the cohort",
2013                &[
2014                    ("env", &["2026.06-dev.aaa"], Some("2026.06")),
2015                    ("model", &["2026.06-dev.aaa"], None),
2016                    ("runtime", &["2026.06-dev.aaa"], None),
2017                ],
2018                Some("2026.06-dev.aaa"),
2019            ),
2020            (
2021                "bare base WANT admits its own cohorts but not a newer base's",
2022                &[
2023                    (
2024                        "env",
2025                        &["2026.06-dev.aaa", "2026.08-dev.aaa"],
2026                        Some("2026.06"),
2027                    ),
2028                    ("runtime", &["2026.06-dev.aaa", "2026.08-dev.aaa"], None),
2029                ],
2030                Some("2026.06-dev.aaa"),
2031            ),
2032            (
2033                "bare base WANT against a newer base only refuses",
2034                &[
2035                    ("env", &["2026.08"], Some("2026.06")),
2036                    ("model", &["2026.08"], None),
2037                    ("runtime", &["2026.08", "2026.08-dev.aaa"], None),
2038                ],
2039                None,
2040            ),
2041            (
2042                "cohort WANT against a differing cohort of the same base refuses",
2043                &[
2044                    ("env", &["2026.06-dev.aaa"], Some("2026.06-dev.aaa")),
2045                    ("runtime", &["2026.06-dev.bbb"], None),
2046                ],
2047                None,
2048            ),
2049            (
2050                "cohort WANT keeps the exact order: a higher cohort in the intersection is excluded",
2051                &[
2052                    ("env", &["2026.06-dev.bbb"], Some("2026.06-dev.aaa")),
2053                    ("runtime", &["2026.06-dev.bbb"], None),
2054                ],
2055                None,
2056            ),
2057            // --- a 0.1.0-shaped peer against a build that retains more ---
2058            (
2059                "a 0.1.0-shaped peer (one CAN, no WANT) pins a newer build to 2026.06",
2060                &[
2061                    ("env", &["2026.06"], None),
2062                    ("runtime", &["2026.06", "2099.01"], None),
2063                ],
2064                Some("2026.06"),
2065            ),
2066            (
2067                "the same peer against a runtime that declares the newer edition",
2068                &[
2069                    ("env", &["2026.06"], None),
2070                    ("model", &["2026.06"], None),
2071                    ("runtime", &["2026.06", "2099.01"], Some("2099.01")),
2072                ],
2073                Some("2026.06"),
2074            ),
2075            // --- degenerate sets ---
2076            (
2077                "a tier that CAN nothing refuses",
2078                &[("env", &[], None), ("runtime", &["2026.06"], None)],
2079                None,
2080            ),
2081        ];
2082
2083        for (name, tiers, expected) in cases {
2084            let offers: Vec<(&str, SessionOffer)> = tiers
2085                .iter()
2086                .map(|(tier_name, can, want)| (*tier_name, tier(can, *want)))
2087                .collect();
2088            let borrowed: Vec<(&str, &SessionOffer)> = offers
2089                .iter()
2090                .map(|(tier_name, o)| (*tier_name, o))
2091                .collect();
2092            let selected = super::select_workflow_edition(&borrowed);
2093            match expected {
2094                Some(edition) => assert_eq!(selected.as_deref(), Ok(*edition), "{name}"),
2095                None => {
2096                    let refusal = selected.expect_err(name).to_string();
2097                    for (tier_name, can, _) in tiers.iter() {
2098                        assert!(refusal.contains(tier_name), "{name}: {refusal}");
2099                        for edition in can.iter() {
2100                            assert!(refusal.contains(edition), "{name}: {refusal}");
2101                        }
2102                    }
2103                }
2104            }
2105        }
2106    }
2107
2108    /// With nothing declared anywhere, selection is bit-for-bit the pre-WANT
2109    /// rule: the WANT ceiling is `min(max(can))`, and every member of the
2110    /// intersection is already <= each tier's own max, so the filter is vacuous.
2111    ///
2112    /// `highest_mutual` below is the function this unit replaced, copied
2113    /// verbatim from `git show HEAD:crates/rlmesh-proto/src/lib.rs` (minus its
2114    /// generic `key`, which every caller instantiated with `edition_sort_key`).
2115    #[test]
2116    fn undeclared_everywhere_is_the_old_highest_mutual() {
2117        fn highest_mutual(sets: &[&[String]]) -> Option<String> {
2118            let (first, rest) = sets.split_first()?;
2119            first
2120                .iter()
2121                .map(|value| value.trim())
2122                .filter(|value| !value.is_empty())
2123                .filter(|value| {
2124                    rest.iter()
2125                        .all(|set| set.iter().any(|other| other.trim() == *value))
2126                })
2127                .max_by_key(|value| super::edition_sort_key(value))
2128                .map(|value| value.to_string())
2129        }
2130
2131        let shapes: &[&[&[&str]]] = &[
2132            &[&["2026.06"], &["2026.06"]],
2133            &[&["2026.01", " 2026.06 "], &["2026.06", "2026.01"]],
2134            &[&["2025.01", "2026.06", "2031.12"], &["2026.06"]],
2135            &[&["2026.11", "2027.01"], &["2026.06"]],
2136            &[&[""], &["2026.06"]],
2137            &[&[], &["2026.06"]],
2138            &[
2139                &["2026.06", "2026.08"],
2140                &["2026.06", "2026.08"],
2141                &["2026.06"],
2142            ],
2143            &[&["2026.08"], &["2026.08"], &["2026.06"]],
2144            &[
2145                &["2026.06", "2026.06-0.1.0-rc.1"],
2146                &["2026.06", "2026.06-0.1.0-rc.1"],
2147                &["2026.06", "2026.06-0.1.0-rc.1"],
2148            ],
2149            &[
2150                &["2026.06", "2026.06-dev.aaa"],
2151                &["2026.06", "2026.06-dev.bbb"],
2152                &["2026.06", "2026.06-dev.aaa"],
2153            ],
2154            &[&["2026.06"], &["2026.06", "2099.01"]],
2155        ];
2156
2157        for shape in shapes {
2158            let offers: Vec<SessionOffer> = shape.iter().map(|can| tier(can, None)).collect();
2159            let owned: Vec<Vec<String>> = shape.iter().map(|can| offer(can)).collect();
2160            let sets: Vec<&[String]> = owned.iter().map(Vec::as_slice).collect();
2161            let borrowed: Vec<(&str, &SessionOffer)> = offers.iter().map(|o| ("tier", o)).collect();
2162            assert_eq!(
2163                super::select_workflow_edition(&borrowed).ok(),
2164                highest_mutual(&sets),
2165                "{shape:?}"
2166            );
2167        }
2168    }
2169
2170    /// The runtime holding the floor down is diagnosed by cause (plan E.3): a
2171    /// CAN-set limit is a defect the operator can fix by upgrading; a declared
2172    /// WANT is the sticky model working as intended.
2173    #[test]
2174    fn session_floor_reports_why_the_runtime_capped_the_session() {
2175        use super::RuntimeCap;
2176
2177        // The runtime cannot drive 2026.08 at all.
2178        let peers = tier(&["2026.06", "2026.08"], None);
2179        let floor =
2180            negotiate_session_floor(&peers, &peers, &tier(&["2026.06"], None)).expect("a floor");
2181        assert_eq!(floor.runtime_cap, Some(RuntimeCap::Capability));
2182
2183        // The runtime can drive 2026.08 and declares 2026.06 instead.
2184        let runtime = tier(&["2026.06", "2026.08"], Some("2026.06"));
2185        let floor = negotiate_session_floor(&peers, &peers, &runtime).expect("a floor");
2186        assert_eq!(floor.selected_workflow_edition, "2026.06");
2187        assert_eq!(floor.desired_workflow_edition, "2026.08");
2188        assert_eq!(floor.runtime_cap, Some(RuntimeCap::Declaration));
2189
2190        // Both older AND declared below its own max: the declaration is what
2191        // pins the session (undeclared it would have run at 2026.08), so this is
2192        // not an upgrade-the-runtime diagnostic even though 2026.10 is out of
2193        // the runtime's reach entirely.
2194        let wide = tier(&["2026.06", "2026.08", "2026.10"], None);
2195        let runtime = tier(&["2026.06", "2026.08"], Some("2026.06"));
2196        let floor = negotiate_session_floor(&wide, &wide, &runtime).expect("a floor");
2197        assert_eq!(floor.selected_workflow_edition, "2026.06");
2198        assert_eq!(floor.desired_workflow_edition, "2026.10");
2199        assert_eq!(floor.runtime_cap, Some(RuntimeCap::Declaration));
2200
2201        // A peer, not the runtime, holding the floor down is not a runtime cap.
2202        let pinned = tier(&["2026.06", "2026.08"], Some("2026.06"));
2203        let floor = negotiate_session_floor(&pinned, &peers, &peers).expect("a floor");
2204        assert_eq!(floor.selected_workflow_edition, "2026.06");
2205        assert_eq!(floor.desired_workflow_edition, "2026.06");
2206        assert_eq!(floor.runtime_cap, None);
2207        assert!(!floor.runtime_limited());
2208    }
2209
2210    /// An undeclared participant declares this build's current edition, which is
2211    /// `max(can)` for this build — so this build's own offer never caps a peer.
2212    #[test]
2213    fn this_build_declares_its_current_edition_by_default() {
2214        use super::{core_handshake_request, declared_workflow_edition, edition_sort_key};
2215
2216        // The no-op argument rests on this: the default WANT is `max(can)`, so a
2217        // retained edition sorting above the current one would make this build's
2218        // own offer cap its peers.
2219        assert_eq!(
2220            SUPPORTED_WORKFLOW_EDITIONS
2221                .iter()
2222                .max_by_key(|edition| edition_sort_key(edition))
2223                .copied(),
2224            Some(CURRENT_WORKFLOW_EDITION)
2225        );
2226
2227        assert_eq!(declared_workflow_edition(None), CURRENT_WORKFLOW_EDITION);
2228        assert_eq!(
2229            declared_workflow_edition(Some("  ")),
2230            CURRENT_WORKFLOW_EDITION
2231        );
2232        assert_eq!(declared_workflow_edition(Some(" 2026.06 ")), "2026.06");
2233
2234        let offer = SessionOffer::this_build(None);
2235        assert_eq!(offer.editions, supported_workflow_editions());
2236        assert_eq!(
2237            offer.preferred.as_deref(),
2238            Some(CURRENT_WORKFLOW_EDITION),
2239            "an undeclared build wants its current edition"
2240        );
2241
2242        let request = core_handshake_request("rlmesh-env", &[], None);
2243        assert_eq!(request.preferred_workflow_edition, CURRENT_WORKFLOW_EDITION);
2244        assert_eq!(
2245            core_handshake_request("rlmesh-env", &[], Some("2026.06")).preferred_workflow_edition,
2246            "2026.06"
2247        );
2248    }
2249}