caixa_core/limits.rs
1//! Lunatic-style per-process resource limits — the typed slot of
2//! `caixa.lisp` that wasm-engine consumes at component instantiation.
3//!
4//! See `theory/INSPIRATIONS.md` §III.1 for the prior-art frame: every
5//! caixa Servico runs sandboxed by default; no "trust the author".
6//!
7//! ```lisp
8//! (defcaixa
9//! :nome "my-service"
10//! :versao "0.1.0"
11//! :kind Servico
12//! :limits ((:memory "64MiB") ;; max linear memory per instance
13//! (:fuel 1000000) ;; max wasm-instructions per request
14//! (:wall-clock "30s") ;; max wall-clock per request
15//! (:cpu "500m")) ;; soft cgroup CPU share (millicores)
16//! :servicos ("servicos/my-service.computeunit.yaml"))
17//! ```
18//!
19//! Authors omit the slot for "no limits" (today's behavior). When set,
20//! wasm-engine M2 wires:
21//!
22//! - [`LimitsSpec::memory`] → `wasmtime::StoreLimits::memory_size`
23//! - [`LimitsSpec::fuel`] → `Store::set_fuel` + per-tick refill
24//! - [`LimitsSpec::wall_clock`] → epoch deadline cancellation
25//! - [`LimitsSpec::cpu`] → cgroup-v2 hint propagated via the pod spec
26
27use std::time::Duration;
28
29use serde::{Deserialize, Deserializer, Serialize, Serializer};
30use thiserror::Error;
31
32/// Hard upper bound for `:limits :memory`, in bytes — the
33/// `wasm32-wasip2` linear-memory ceiling. The canonical caixa Servico
34/// compilation target ([`theory/CAIXA-SDLC.md` §V — *Substrate /
35/// Nix*][sdlc-v]) is `wasm32-wasip2`, whose linear memory is 32-bit-
36/// addressed at a 64 KiB page size; the in-spec maximum is
37/// `2^16 pages × 2^16 bytes/page = 2^32` bytes = 4 GiB exactly.
38/// A `:limits :memory` value above this bound is structurally
39/// unreachable under wasm32: wasmtime's `Store::limiter` cannot grow
40/// past the 32-bit address space, so an authored `"8GiB"` either
41/// silently saturates at the engine's effective cap or surfaces as a
42/// `memory.grow` trap at runtime, far from the source caixa.lisp.
43///
44/// Pairs with [`LimitsError::MemoryZero`] (the zero-floor gate added
45/// by the prior typed-shape lift on this axis) to bracket the valid
46/// `:memory` set top-to-bottom: every validated value lies in
47/// `1..=LIMITS_MEMORY_WASM32_MAX_BYTES` (inclusive on both ends).
48/// Renderers ([`crate::render::servico_m2_overlay`] and the M2.5
49/// `wasm-engine` instantiator the ABSORPTION-ROADMAP names as the
50/// downstream wiring) consume the typed value with no re-validation
51/// — the value-shape gate is the structural contract.
52///
53/// Lifted as a typed `pub const` (rather than an inline literal at
54/// the [`LimitsSpec::validate`] call site) so the bound has exactly
55/// one source of truth — a future axis reaching for the same value
56/// (a future `memory64`-target opt-in raising the cap to 2^64, a
57/// wasm-engine smoke test asserting the engine's effective limit
58/// matches the typed bound, the M4 `mesh.pleme.io/v1alpha1/Caixa`
59/// CR materializer's per-`:limits :memory` admission webhook)
60/// reads from one place. Same shape every other typed bound in this
61/// crate carries ([`crate::render::DNS_1123_LABEL_MAX_LEN`],
62/// [`crate::render::GATEWAY_API_HTTP_PATH_MAX_LEN`],
63/// [`crate::render::NATS_SUBJECT_MAX_LEN`]).
64///
65/// [sdlc-v]: https://github.com/pleme-io/theory/blob/main/CAIXA-SDLC.md
66pub const LIMITS_MEMORY_WASM32_MAX_BYTES: u64 = 4 * 1024 * 1024 * 1024;
67
68/// Structural floor for `:limits :memory`, in bytes — the
69/// `wasm32-wasip2` linear-memory page size. The wasm spec defines
70/// linear memory in fixed 64 KiB pages (`2^16` bytes); every typed
71/// memory cap is consumed by `wasmtime::StoreLimits::memory_size` as a
72/// per-component byte ceiling against which the engine checks every
73/// `memory.grow` request. A cap below one page (`< 65536` bytes) is
74/// structurally a "no wasm linear memory allowed" cap — instantiation
75/// of any wasm component that declares `(memory 1)` (i.e. min=1 page,
76/// the canonical default for every cdylib-shaped wasm component cargo
77/// emits) fails immediately with `memory minimum size of 1 pages
78/// exceeds memory limits`; a min=0 component traps the first
79/// `memory.grow(1)` because the next-page allocation would cross the
80/// sub-page cap. Either way the typed value the wasm-engine consumes
81/// is operationally indistinguishable from [`LimitsError::MemoryZero`]
82/// (no memory at all), but the diagnostic surfaces at engine-load
83/// time rather than at caixa-build time, far from the source
84/// caixa.lisp.
85///
86/// Pairs with [`LIMITS_MEMORY_WASM32_MAX_BYTES`] (the 4 GiB upper
87/// cap added by the prior typed-shape lift on this axis) to bracket
88/// the valid `:memory` set top-to-bottom in *operational* units, not
89/// just byte units: every validated value lies in
90/// `LIMITS_MEMORY_WASM32_PAGE_BYTES..=LIMITS_MEMORY_WASM32_MAX_BYTES`
91/// inclusive on both ends — i.e. at least one wasm32 linear memory
92/// page can be allocated, and at most the wasm32 address-space
93/// ceiling fits.
94///
95/// Lifted as a typed `pub const` (rather than an inline literal at
96/// the [`LimitsSpec::validate`] call site) so the bound has exactly
97/// one source of truth — a future axis reaching for the same value
98/// (a future `memory64`-target opt-in raising the page size, the M4
99/// `mesh.pleme.io/v1alpha1/Caixa` CR materializer's per-`:limits
100/// :memory` admission webhook, a wasm-engine smoke test asserting
101/// every instantiated component can fit one page within its
102/// configured cap) reads from one place. Same single-source-of-truth
103/// shape every typed bound in this crate carries
104/// ([`LIMITS_MEMORY_WASM32_MAX_BYTES`],
105/// [`crate::render::DNS_1123_LABEL_MAX_LEN`]).
106pub const LIMITS_MEMORY_WASM32_PAGE_BYTES: u64 = 64 * 1024;
107
108/// Upper-bound ceiling on the `:limits :wall-clock` axis — every
109/// validated [`LimitsSpec::wall_clock`] past [`LimitsSpec::validate`]
110/// lies in `1ms..=LIMITS_WALL_CLOCK_MAX` (inclusive on both ends,
111/// integer-millisecond magnitudes by the canonical-form gate
112/// immediately preceding).
113///
114/// The typed field is `Option<Duration>` (the zero-floor arm
115/// [`LimitsError::WallClockZero`] already rejects `Duration::ZERO`, and
116/// the canonical-form arm [`LimitsError::WallClockNotCanonical`]
117/// already rejects sub-millisecond residue), so a programmatic struct
118/// literal (`LimitsSpec { wall_clock: Some(Duration::from_secs(86_400)),
119/// .. }` — 24h) and the equivalent author-surface form
120/// (`(:limits (:wall-clock "24h"))` — the codec emits `"<n>h"` for any
121/// integer-hour magnitude) both round-trip cleanly through serde — a
122/// structurally unbounded `Duration` ceiling. A `:wall-clock` value far
123/// above the per-process production band (Lunatic / Wasmtime documented
124/// per-call deadlines sit in the seconds-to-minutes range; Kubernetes
125/// activeDeadlineSeconds typical `≤ 3600s`; the longest per-request
126/// timeout any upstream HTTP runtime documents — Kubernetes
127/// ingress-nginx `proxy_read_timeout` — caps at the same 3600s) turns
128/// the typed per-process deadline into a nominal-only contract: the
129/// wasm-engine's epoch-deadline cancellation reaches for a `Duration`
130/// so long no realistic synchronous wasm call can hit it, the runaway-
131/// process invariant the MESH-COMPOSITION §V "no infinite blocking" CSE
132/// invariant pins at the per-Servico layer degenerates to a runtime,
133/// not build-time, contract. Pairs with the
134/// [`crate::POLICY_TIMEOUT_MAX`] cap on the sibling `:politicas :timeout`
135/// mesh-edge axis and the [`crate::POLICY_BREAKER_WINDOW_MAX`] cap on
136/// the sibling `:politicas :circuit-breaker :window` rolling-window
137/// axis — all three close the "structurally unbounded `Duration`
138/// ceiling on a typed slot" footgun the prior zero-floor-and-canonical-
139/// form-only checks left open.
140///
141/// The 1h (3600s = `3_600_000` ms) ceiling matches the largest unit
142/// the shared duration codec emits (`"<n>h"` for any integer-hour
143/// magnitude) — every value in the canonical authoring form's
144/// `<integer><unit>` grammar at or below this cap renders to a clean
145/// canonical string — and matches the two sibling typed-`Duration`
146/// caps already lifted to this surface
147/// ([`crate::POLICY_TIMEOUT_MAX`], [`crate::POLICY_BREAKER_WINDOW_MAX`]).
148/// The three typed-`Duration` axes — per-process `:limits :wall-clock`,
149/// per-edge `:politicas :timeout`, per-breaker `:politicas
150/// :circuit-breaker :window` — now share a single uniform top edge so
151/// the next typed-slot wiring (the wasm-engine M2.5 epoch-deadline
152/// cancellation hook, the future caixa-helm `pleme-computeunit` chart's
153/// `:limits` value mapping, the M4 `mesh.pleme.io/v1alpha1/Caixa` CR
154/// materializer's per-`:limits :wall-clock` admission webhook) reaches
155/// for any of the three knowing the value is in `1ms..=1h` without
156/// re-validating at the renderer layer. The cap sits above the
157/// documented per-request playbook band (Envoy / Istio / Linkerd
158/// production `≤ 60s`, AWS App Mesh / ingress-nginx typical `≤ 300s`,
159/// Kubernetes activeDeadlineSeconds typical `≤ 3600s`) and below the
160/// clearly-pathological "effectively no deadline" floor (`24h`, `7d`,
161/// `Duration::MAX`): a value the author can plausibly want for a
162/// long-running synchronous workflow, but a hard wall above which the
163/// per-process deadline is structurally a non-deadline.
164///
165/// Lifted as a typed `pub const` so the bound has exactly one source
166/// of truth — the wasm-engine M2.5 epoch-deadline wiring, a wasm-engine
167/// smoke test asserting the engine's epoch interrupt fires within the
168/// typed bound, the M4 `mesh.pleme.io/v1alpha1/Caixa` CR materializer's
169/// per-`:limits :wall-clock` admission webhook all read from one place.
170/// Same shape every other typed upper bound in this crate carries
171/// ([`LIMITS_MEMORY_WASM32_MAX_BYTES`], [`crate::POLICY_TIMEOUT_MAX`],
172/// [`crate::POLICY_BREAKER_WINDOW_MAX`],
173/// [`crate::render::DNS_1123_LABEL_MAX_LEN`],
174/// [`crate::render::NATS_SUBJECT_MAX_LEN`]).
175pub const LIMITS_WALL_CLOCK_MAX: Duration = Duration::from_secs(3600);
176
177/// Upper-bound ceiling on the `:limits :cpu` axis, in Kubernetes
178/// millicores — every validated [`LimitsSpec::cpu`] past
179/// [`LimitsSpec::validate`] lies in `1..=LIMITS_CPU_MILLICORES_MAX`
180/// (inclusive on both ends).
181///
182/// The typed field is `Option<u32>` (the zero-floor arm
183/// [`LimitsError::CpuZero`] already rejects `Some(0)` — a zero cgroup
184/// share starves the process), so a programmatic struct literal
185/// (`LimitsSpec { cpu: Some(u32::MAX), .. }` — ≈ 4.3 million cores)
186/// and the equivalent author-surface form (`(:limits (:cpu
187/// "1000000m"))` — the millicore codec parses any `u32`-shaped
188/// magnitude) both round-trip cleanly through serde — a structurally
189/// unbounded `u32` ceiling. The runtime substrate consuming the value
190/// ([`crate::render::servico_m2_overlay`]'s `pleme-computeunit.limits.cpu`
191/// projection, the M2.5 `wasm-engine` instantiator the
192/// `ABSORPTION-ROADMAP` names as the downstream wiring, the future
193/// M4 `mesh.pleme.io/v1alpha1/Caixa` CR materializer's admission
194/// webhook) lands the value verbatim as the K8s pod's
195/// `resources.requests.cpu`. A value far above the largest commodity
196/// node's vCPU count turns the typed slot into an unschedulable hint:
197/// the Kubernetes scheduler refuses to bind the pod to any node
198/// (insufficient `cpu` available), the Servico sits `Pending`
199/// indefinitely, and the per-process CSE invariant (every typed
200/// `:cpu` reaches a node) is a runtime, not build-time, contract —
201/// the canonical declared-but-unschedulable footgun the sibling
202/// `:limits :memory` wasm32-cap arm closes on its peer "cannot be
203/// honored" shape.
204///
205/// The `128_000` (128 cores) ceiling matches the largest commercially
206/// common non-metal cloud Kubernetes node vCPU count (AWS m7i.32xlarge
207/// / c7i.32xlarge = 128 vCPU; Azure HBv3-128rs = 128 vCPU; GCP
208/// c3-standard-128 = 128 vCPU — every major managed-Kubernetes provider
209/// tops out at 128 vCPU on its general-purpose non-metal SKUs) and sits
210/// two orders of magnitude above every realistic per-Servico
211/// production-playbook band (the canonical caixa Servico runs in the
212/// 100m–2000m band; the in-tree
213/// `limits_slot_propagates_into_values_block` smoke test pins
214/// `cpu: Some(500)` = 500m as the load-bearing example, peer to the
215/// `caixa-flux` projector's identical 500m default). A value above this
216/// cap is structurally unschedulable on any commercial managed
217/// Kubernetes node pool: GKE Standard / EKS managed / AKS default
218/// node-group SKU ladders cap at 128 vCPU per node for general-purpose
219/// instance families, so a `:cpu` request above `128_000m` cannot bind to
220/// any node the operator can provision through the standard
221/// cloud-provider control plane. The wasm32-wasip2 single-threaded
222/// execution model the canonical caixa Servico targets
223/// ([`theory/CAIXA-SDLC.md` §V][sdlc-v]) reinforces the structural
224/// argument: a single wasm component cannot saturate more than one
225/// core, so even the Lunatic-style supervised-multi-process host
226/// (`theory/INSPIRATIONS.md` §III.1) — which fans wasm processes across
227/// the host runtime's Tokio thread pool — bounds its useful CPU request
228/// to the host node's vCPU count, never higher.
229///
230/// Lifted as a typed `pub const` (rather than an inline literal at the
231/// [`LimitsSpec::validate`] call site) so the bound has exactly one
232/// source of truth — the future M4
233/// `mesh.pleme.io/v1alpha1/Caixa` CR materializer's per-`:limits :cpu`
234/// admission webhook, the caixa-helm `pleme-computeunit` chart's
235/// resource-request mapping, the M2.5 `wasm-engine` host-runtime
236/// thread-pool sizing hint all read from one place. Same shape every
237/// other typed upper bound in this crate carries
238/// ([`LIMITS_MEMORY_WASM32_MAX_BYTES`], [`LIMITS_WALL_CLOCK_MAX`],
239/// [`crate::POLICY_TIMEOUT_MAX`], [`crate::POLICY_BREAKER_WINDOW_MAX`],
240/// [`crate::POLICY_RATE_LIMIT_MAX`],
241/// [`crate::render::DNS_1123_LABEL_MAX_LEN`]).
242///
243/// [sdlc-v]: https://github.com/pleme-io/theory/blob/main/CAIXA-SDLC.md
244pub const LIMITS_CPU_MILLICORES_MAX: u32 = 128_000;
245
246/// Upper-bound ceiling on the `:limits :fuel` axis, in wasm
247/// instructions per outermost call — every validated
248/// [`LimitsSpec::fuel`] past [`LimitsSpec::validate`] lies in
249/// `1..=LIMITS_FUEL_MAX` (inclusive on both ends).
250///
251/// The typed field is `Option<u64>` (the zero-floor arm
252/// [`LimitsError::FuelZero`] already rejects `Some(0)` — wasmtime
253/// traps the first instruction at `fuel=0`), so a programmatic
254/// struct literal (`LimitsSpec { fuel: Some(u64::MAX), .. }` —
255/// ≈ 1.8 × 10¹⁹ instructions) and the equivalent author-surface
256/// form (`(:limits (:fuel 18446744073709551615))`) both
257/// round-trip cleanly through serde — a structurally unbounded
258/// `u64` ceiling. The runtime substrate consuming the value
259/// ([`crate::render::servico_m2_overlay`]'s
260/// `pleme-computeunit.limits.fuel` projection, the M2.5
261/// `wasm-engine` `Store::set_fuel` call the
262/// `ABSORPTION-ROADMAP` names as the downstream wiring, the
263/// future M4 `mesh.pleme.io/v1alpha1/Caixa` CR materializer's
264/// admission webhook) lands the value verbatim as the
265/// wasmtime store's per-call fuel budget. A value far above any
266/// reachable wasm execution count turns the typed slot into a
267/// no-op budget: the sibling [`LIMITS_WALL_CLOCK_MAX`] (1h)
268/// cap fires before the fuel counter ever drains, the per-call
269/// fuel-tracking contract degenerates to "rely on `:wall-clock`
270/// instead" enforcement, and the per-process CSE invariant
271/// (every typed `:fuel` is a meaningful budget the wasm-engine
272/// can actually consume) is a runtime, not build-time, contract
273/// on every above-cap input — the canonical declared-but-no-op
274/// footgun the sibling `:wall-clock` / `:cpu` / `:memory` cap
275/// arms close on the peer "cannot be honored" /
276/// "unschedulable hint" / "no-op budget" shapes, and the peer
277/// `:politicas :rate-limit` / `:politicas :timeout` /
278/// `:politicas :circuit-breaker :window` /
279/// `:supervisor :max-restarts` cap arms close on every other
280/// `Option<numeric>` axis on the typed Caixa surface.
281///
282/// The `1_000_000_000_000` (10¹² = 1 trillion wasm instructions)
283/// ceiling matches the operational envelope the sibling
284/// [`LIMITS_WALL_CLOCK_MAX`] cap pins: at wasmtime's documented
285/// fuel-tracked execution rate (~10⁸–10⁹ fuel-units per second
286/// on modern x86_64 / aarch64 hosts running wasmtime through
287/// Cranelift — the substrate's wasm32-wasip2 default backend per
288/// the `caixa-feira` runner), the largest realistic per-call
289/// fuel budget reachable within `LIMITS_WALL_CLOCK_MAX` (1h)
290/// sits at ~3.6 × 10¹¹–3.6 × 10¹² fuel-units. The 10¹² cap is
291/// the round-number ceiling above this operational envelope,
292/// sits six orders of magnitude above the canonical fixture
293/// (the in-tree `Caixa::template` documentation and
294/// `caixa-feira` examples carry `:fuel 1_000_000` = 10⁶,
295/// peer to wasmtime's official `Store::set_fuel(1_000_000)`
296/// example in the `wasmtime` book), and surfaces every
297/// paste-from-binary / overflow / u64-magnitude-typo footgun
298/// (`u64::MAX`, `0xFFFF_FFFF_FFFF_FFFF`, large hex literals
299/// confused for instruction-count budgets) at validate time.
300/// A value above this cap is operationally a no-op fuel
301/// counter: the wall-clock deadline ([`LIMITS_WALL_CLOCK_MAX`]
302/// = 3600s × ~10⁹ fuel/sec ≈ 3.6 × 10¹² instructions reachable)
303/// fires before the fuel counter could ever be drained,
304/// so the typed `:fuel` slot becomes a no-op budget far from
305/// the source caixa.lisp. The wasm32-wasip2 single-threaded
306/// execution model the canonical caixa Servico targets
307/// ([`theory/CAIXA-SDLC.md` §V][sdlc-v]) reinforces the
308/// structural argument: a single wasm component cannot
309/// out-execute its host's CPU clock, so even the Lunatic-style
310/// supervised-multi-process host (`theory/INSPIRATIONS.md`
311/// §III.1) bounds its useful fuel-per-call budget to a
312/// per-clock-tick magnitude, never higher.
313///
314/// Lifted as a typed `pub const` (rather than an inline literal
315/// at the [`LimitsSpec::validate`] call site) so the bound has
316/// exactly one source of truth — the future M4
317/// `mesh.pleme.io/v1alpha1/Caixa` CR materializer's per-`:limits
318/// :fuel` admission webhook, the caixa-helm `pleme-computeunit`
319/// chart's fuel-budget mapping, the M2.5 `wasm-engine` host-
320/// runtime `Store::set_fuel` propagation all read from one
321/// place. Same shape every other typed upper bound in this
322/// crate carries ([`LIMITS_MEMORY_WASM32_MAX_BYTES`],
323/// [`LIMITS_WALL_CLOCK_MAX`], [`LIMITS_CPU_MILLICORES_MAX`],
324/// [`crate::POLICY_TIMEOUT_MAX`],
325/// [`crate::POLICY_BREAKER_WINDOW_MAX`],
326/// [`crate::POLICY_RATE_LIMIT_MAX`],
327/// [`crate::SUPERVISOR_MAX_RESTARTS_MAX`],
328/// [`crate::render::DNS_1123_LABEL_MAX_LEN`]).
329///
330/// [sdlc-v]: https://github.com/pleme-io/theory/blob/main/CAIXA-SDLC.md
331pub const LIMITS_FUEL_MAX: u64 = 1_000_000_000_000;
332
333/// Per-process limits. All fields optional — `None` = unbounded for that axis.
334#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default, PartialEq, Eq)]
335#[serde(rename_all = "camelCase")]
336pub struct LimitsSpec {
337 /// Max linear memory in bytes. Authored as a byte-size string
338 /// (`"64MiB"`, `"1GiB"`, `"512KB"`). Round-trips back to the same
339 /// canonical string on serialize.
340 #[serde(
341 default,
342 skip_serializing_if = "Option::is_none",
343 serialize_with = "ser_byte_size",
344 deserialize_with = "de_byte_size"
345 )]
346 pub memory: Option<u64>,
347
348 /// Max wasm instructions per outermost call (`wasmtime` fuel).
349 /// Plain integer; `None` = unbounded.
350 #[serde(default, skip_serializing_if = "Option::is_none")]
351 pub fuel: Option<u64>,
352
353 /// Wall-clock cap per outermost call. Authored as a duration
354 /// string (`"30s"`, `"500ms"`, `"2m"`).
355 #[serde(
356 default,
357 skip_serializing_if = "Option::is_none",
358 serialize_with = "ser_duration",
359 deserialize_with = "de_duration"
360 )]
361 pub wall_clock: Option<Duration>,
362
363 /// Soft CPU share. Authored as a Kubernetes-style millicore string
364 /// (`"500m"` for half a core, `"2"` or `"2000m"` for two cores).
365 /// Stored as millicores (u32).
366 #[serde(
367 default,
368 skip_serializing_if = "Option::is_none",
369 serialize_with = "ser_millicores",
370 deserialize_with = "de_millicores"
371 )]
372 pub cpu: Option<u32>,
373}
374
375impl LimitsSpec {
376 /// True when no axis is bounded.
377 #[must_use]
378 pub const fn is_empty(&self) -> bool {
379 self.memory().is_none()
380 && self.fuel().is_none()
381 && self.wall_clock().is_none()
382 && self.cpu().is_none()
383 }
384
385 /// Substrate-canonical per-`:limits` `:memory` Lunatic-per-process
386 /// wasm32-linear-memory byte-cap scalar accessor every consumer of
387 /// the Servico's `wasmtime::StoreLimits::memory_size` propagation
388 /// keys off — returns the author-declared `:limits :memory` typed
389 /// byte-cap verbatim as an `Option<u64>`, copied out of the typed
390 /// slot's own `Option<u64>` storage (`Option<u64>` is `Copy`, so
391 /// the accessor returns by value; no borrow of `&self` past the
392 /// call). `None` when the slot is absent (the "no memory cap
393 /// declared — engine-default applies, today the pre-M2 unbounded-
394 /// linear-memory shape" arm the module-level docstring names on
395 /// [`LimitsSpec::memory`] itself — [`LimitsSpec::is_empty`]'s
396 /// `memory().is_none()` arm reads this predicate too, so an
397 /// authored-but-unset `:limits (:memory ())` round-trips to a
398 /// `servico_m2_overlay` emission structurally identical to one
399 /// that omits the slot entirely).
400 ///
401 /// The `:limits :memory` slot carries the "per-process wasm32
402 /// linear-memory byte-cap" Lunatic-shaped sandboxing contract
403 /// (`theory/INSPIRATIONS.md` §III.1) — the typed slot's
404 /// `Option<u64>` accept-set (zero-floor rejected through
405 /// [`LimitsError::MemoryZero`], wasm32-page-floor rejected through
406 /// [`LimitsError::MemoryBelowWasm32Page`], upper-bounded by
407 /// [`LIMITS_MEMORY_WASM32_MAX_BYTES`], authored as a byte-size
408 /// string that round-trips back to the canonical form through
409 /// [`ser_byte_size`] / [`de_byte_size`]) maps onto the wasmtime
410 /// `Store::limiter`-side `memory_size` projection the wasm-engine
411 /// M2 wires and, via [`crate::render::servico_m2_overlay`], onto
412 /// the `pleme-computeunit` Helm-library-chart values sub-block's
413 /// `limits.memory` key that lands as the ComputeUnit CR's
414 /// `spec.limits.memory` field.
415 ///
416 /// Prior to this lift the `.memory` field was accessed inline at
417 /// four sites inside `impl LimitsSpec` — [`LimitsSpec::is_empty`]'s
418 /// `self.memory.is_none()` arm and three [`LimitsSpec::validate`]
419 /// arms (the numeric zero-floor arm at line 397, the wasm32-page
420 /// structural floor arm at line 427, and the wasm32 upper-cap
421 /// arm at line 449) — four open-coded field-accesses that
422 /// expressed no compile-time link back to the typed slot. A
423 /// future extension of the `:limits :memory` axis to a richer
424 /// author surface — a per-instance memory-declaration override
425 /// the operator pins through a future ComputeUnit CR-side
426 /// `spec.limits.memory` overlay, a split of the single `u64`
427 /// byte-cap into a `{min, max}` pair once wasm32's `(memory M N)`
428 /// two-arg form promotes past its current single-`max` typed
429 /// bound, a wasm64 promotion once the wasm-engine grows past the
430 /// wasm32 4 GiB structural ceiling — would have had to be
431 /// threaded through every open-coded copy in lockstep or the
432 /// emptiness predicate and the validate call would silently
433 /// disagree on which cap a given [`LimitsSpec`] resolves to.
434 /// Lifting the resolution to a typed method on the substrate
435 /// primitive means every downstream consumer of the Servico's
436 /// per-`:limits` byte-cap surface reaches for exactly one typed
437 /// dispatch — the resolver's accept-set migrates as a unit on any
438 /// future axis addition.
439 ///
440 /// First `Option<Copy-T>`-return accessor on the M2 slot family
441 /// (peer of the sibling per-`:politicas` [`crate::MeshPolicy::mtls_required`]
442 /// c0110f1 `Option<bool>` accessor, per-`:politicas`
443 /// [`crate::MeshPolicy::retries`] bdfb399 `Option<u32>` accessor,
444 /// and per-`:politicas` [`crate::MeshPolicy::timeout`] 7073d0f
445 /// `Option<Duration>` accessor on the M3 mesh-slot family — same
446 /// "one typed dispatch on the substrate primitive, thin
447 /// projections at each consumer" discipline extended onto the
448 /// peer per-`:limits` typed-`u64` optional-scalar axis; opens the
449 /// "optional per-slot Copy-T scalar" projection pattern the
450 /// sibling per-`:limits` `:fuel` (Option<u64>) / `:wall-clock`
451 /// (Option<Duration>) / `:cpu` (Option<u32>) future lifts fold
452 /// on). Named `memory()` to match the storage field's name; the
453 /// accessor's identity maps onto the canonical Lunatic-shaped
454 /// `theory/INSPIRATIONS.md` §III.1 vocabulary the slot's docstring
455 /// already carries.
456 #[must_use]
457 pub const fn memory(&self) -> Option<u64> {
458 self.memory
459 }
460
461 /// Substrate-canonical per-`:limits` `:fuel` wasmtime-per-call
462 /// wasm-instruction budget scalar accessor every consumer of the
463 /// Servico's `wasmtime::Store::set_fuel` propagation keys off —
464 /// returns the author-declared `:limits :fuel` typed
465 /// wasm-instruction budget verbatim as an `Option<u64>`, copied
466 /// out of the typed slot's own `Option<u64>` storage
467 /// (`Option<u64>` is `Copy`, so the accessor returns by value; no
468 /// borrow of `&self` past the call). `None` when the slot is
469 /// absent (the "no fuel budget declared — engine-default applies,
470 /// today the pre-M2 unbounded-fuel-counter shape" arm the
471 /// module-level docstring names on [`LimitsSpec::fuel`] itself —
472 /// [`LimitsSpec::is_empty`]'s `fuel().is_none()` arm reads this
473 /// predicate too, so an authored-but-unset `:limits (:fuel ())`
474 /// round-trips to a `servico_m2_overlay` emission structurally
475 /// identical to one that omits the slot entirely).
476 ///
477 /// The `:limits :fuel` slot carries the "per-call wasm-instruction
478 /// budget" wasmtime-shaped sandboxing contract
479 /// (`theory/INSPIRATIONS.md` §III.1 — Lunatic's supervised
480 /// wasm-`Store`-per-process fuel accounting, translated onto
481 /// pleme-io's typed `:limits` slot) — the typed slot's
482 /// `Option<u64>` accept-set (zero-floor rejected through
483 /// [`LimitsError::FuelZero`] because wasmtime traps the first
484 /// instruction at `fuel=0`, upper-bounded by [`LIMITS_FUEL_MAX`]
485 /// (10¹² wasm instructions — the operationally-reachable
486 /// per-call budget within the sibling [`LIMITS_WALL_CLOCK_MAX`]
487 /// 1h ceiling)) maps onto the wasmtime `Store::set_fuel` call
488 /// the M2.5 wasm-engine wires per outermost call and, via
489 /// [`crate::render::servico_m2_overlay`], onto the
490 /// `pleme-computeunit` Helm-library-chart values sub-block's
491 /// `limits.fuel` key that lands as the `ComputeUnit` CR's
492 /// `spec.limits.fuel` field.
493 ///
494 /// Prior to this lift the `.fuel` field was accessed inline at
495 /// two sites inside `impl LimitsSpec` — [`LimitsSpec::is_empty`]'s
496 /// `self.fuel.is_none()` arm and [`LimitsSpec::validate`]'s
497 /// `if let Some(f) = self.fuel { … }` zero-floor + upper-cap
498 /// bracket arm — two open-coded field-accesses that expressed no
499 /// compile-time link back to the typed slot. A future extension
500 /// of the `:limits :fuel` axis to a richer author surface — a
501 /// per-instance `ComputeUnit` CR-side `spec.limits.fuel` overlay
502 /// the operator pins per-cluster, a wasm-instruction-count →
503 /// wasmtime-fuel-unit rescale once the fuel-tracking backend
504 /// switches from Cranelift's implicit 1:1 count to a
505 /// per-opcode-weighted budget, a split of the single
506 /// per-outermost-call `u64` budget into a `{per_call, per_second}`
507 /// pair once the wasm-engine grows a sustained-throughput cap —
508 /// would have had to be threaded through every open-coded copy in
509 /// lockstep or the emptiness predicate and the validate call
510 /// would silently disagree on which fuel budget a given
511 /// [`LimitsSpec`] resolves to. Lifting the resolution to a typed
512 /// method on the substrate primitive means every downstream
513 /// consumer of the Servico's per-`:limits` fuel-budget surface
514 /// reaches for exactly one typed dispatch — the resolver's
515 /// accept-set migrates as a unit on any future axis addition.
516 ///
517 /// Second `Option<Copy-T>`-return accessor on the M2 slot family
518 /// (peer of the sibling per-`:limits` [`LimitsSpec::memory`]
519 /// (620c067) `Option<u64>` accessor — same typed-`u64`
520 /// optional-scalar shape, extended to the peer per-`:limits`
521 /// wasm-instruction-budget axis; sibling to
522 /// [`crate::MeshPolicy::mtls_required`] (c0110f1) / [`crate::MeshPolicy::retries`]
523 /// (bdfb399) / [`crate::MeshPolicy::timeout`] (7073d0f) on the
524 /// closed M3 mesh-slot `Option<Copy-T>` accessor family). The
525 /// pair `(memory(), fuel())` jointly projects the two `Option<u64>`
526 /// axes every M2 `:limits` consumer that fans on
527 /// wasm-linear-memory-cap + wasm-fuel-budget keys off. Two of the
528 /// four `:limits` axes now route through a typed dispatch on the
529 /// substrate primitive; the two remaining (`wall_clock:
530 /// Option<Duration>`, `cpu: Option<u32>`) fold on the same
531 /// one-line accessor + is_empty-arm-route + validate-arm-route +
532 /// three-test pattern. Named `fuel()` to match the storage field's
533 /// name; the accessor's identity maps onto the canonical
534 /// wasmtime-`Store::set_fuel`-shaped vocabulary the slot's
535 /// docstring already carries.
536 #[must_use]
537 pub const fn fuel(&self) -> Option<u64> {
538 self.fuel
539 }
540
541 /// Substrate-canonical per-`:limits` `:wall-clock` wasmtime-per-call
542 /// wall-clock deadline scalar accessor every consumer of the
543 /// Servico's `wasmtime::Store::epoch_deadline_*` / `wasi:clocks`
544 /// propagation keys off — returns the author-declared `:limits
545 /// :wall-clock` typed `Duration` verbatim as an `Option<Duration>`,
546 /// copied out of the typed slot's own `Option<Duration>` storage
547 /// (`Duration` is `Copy`, so `Option<Duration>` is `Copy` and the
548 /// accessor returns by value; no borrow of `&self` past the call).
549 /// `None` when the slot is absent (the "no wall-clock deadline
550 /// declared — engine-default applies, today the pre-M2
551 /// unbounded-wall-clock shape" arm the module-level docstring names
552 /// on [`LimitsSpec::wall_clock`] itself — [`LimitsSpec::is_empty`]'s
553 /// `wall_clock().is_none()` arm reads this predicate too, so an
554 /// authored-but-unset `:limits (:wall-clock ())` round-trips to a
555 /// `servico_m2_overlay` emission structurally identical to one that
556 /// omits the slot entirely).
557 ///
558 /// The `:limits :wall-clock` slot carries the "per-outermost-call
559 /// wall-clock deadline" wasmtime-shaped sandboxing contract
560 /// (`theory/INSPIRATIONS.md` §III.1 — Lunatic's supervised
561 /// wasm-`Store`-per-process epoch-deadline accounting, translated
562 /// onto pleme-io's typed `:limits` slot) — the typed slot's
563 /// `Option<Duration>` accept-set (zero-floor rejected through
564 /// [`LimitsError::WallClockZero`] because a zero deadline traps the
565 /// first instruction; integer-millisecond granularity enforced
566 /// through [`LimitsError::WallClockNotCanonical`] because the
567 /// duration codec's canonical form emits `"1500ms"` not `"1.5s"`
568 /// and the operator's wall-clock scheduler quantizes at
569 /// milliseconds; upper-bounded by [`LIMITS_WALL_CLOCK_MAX`] (1h —
570 /// the coarsest per-call deadline any operationally-reachable
571 /// Servico can honor without spanning multiple scheduler epochs))
572 /// maps onto the wasmtime `Store::epoch_deadline_*` call the M2.5
573 /// wasm-engine wires per outermost call and, via
574 /// [`crate::render::servico_m2_overlay`], onto the
575 /// `pleme-computeunit` Helm-library-chart values sub-block's
576 /// `limits.wallClock` key that lands as the `ComputeUnit` CR's
577 /// `spec.limits.wallClock` field.
578 ///
579 /// Prior to this lift the `.wall_clock` field was accessed inline at
580 /// two sites inside `impl LimitsSpec` — [`LimitsSpec::is_empty`]'s
581 /// `self.wall_clock.is_none()` arm and [`LimitsSpec::validate`]'s
582 /// `if let Some(w) = self.wall_clock { … }` zero-floor +
583 /// canonical-form + upper-cap bracket arm — two open-coded
584 /// field-accesses that expressed no compile-time link back to the
585 /// typed slot. A future extension of the `:limits :wall-clock` axis
586 /// to a richer author surface — a per-instance `ComputeUnit`
587 /// CR-side `spec.limits.wallClock` overlay the operator pins
588 /// per-cluster, a wall-clock-vs-monotonic-clock discriminator once
589 /// the wasm-engine grows a `:limits (:wall-clock (:kind monotonic
590 /// …))` axis, a split of the single per-outermost-call `Duration`
591 /// budget into a `{deadline, warn_at}` pair once the wasm-engine
592 /// grows a soft-deadline warning surface — would have had to be
593 /// threaded through every open-coded copy in lockstep or the
594 /// emptiness predicate and the validate call would silently
595 /// disagree on which deadline a given [`LimitsSpec`] resolves to.
596 /// Lifting the resolution to a typed method on the substrate
597 /// primitive means every downstream consumer of the Servico's
598 /// per-`:limits` wall-clock-deadline surface reaches for exactly
599 /// one typed dispatch — the resolver's accept-set migrates as a
600 /// unit on any future axis addition.
601 ///
602 /// Third `Option<Copy-T>`-return accessor on the M2 slot family
603 /// (peer of the sibling per-`:limits` [`LimitsSpec::memory`]
604 /// (620c067) `Option<u64>` accessor and per-`:limits`
605 /// [`LimitsSpec::fuel`] (795dee7) `Option<u64>` accessor — same
606 /// typed-optional-scalar shape extended to the peer per-`:limits`
607 /// wall-clock-deadline axis; sibling to [`crate::MeshPolicy::timeout`]
608 /// (7073d0f) on the closed M3 mesh-slot `Option<Duration>` accessor
609 /// axis — same typed-`Duration` shape extended from the M3
610 /// per-call-timeout to the M2 per-outermost-call deadline). The
611 /// triple `(memory(), fuel(), wall_clock())` jointly projects three
612 /// of the four `Option<Copy-T>` axes every M2 `:limits` consumer
613 /// that fans on wasm-linear-memory-cap + wasm-fuel-budget +
614 /// wall-clock-deadline keys off. Three of the four `:limits` axes
615 /// now route through a typed dispatch on the substrate primitive;
616 /// the one remaining (`cpu: Option<u32>`) folds on the same
617 /// one-line accessor + is_empty-arm-route + validate-arm-route +
618 /// three-test pattern in the next run, closing the M2 `:limits`
619 /// slot family's `Option<Copy-T>` accessor axis. Named `wall_clock()`
620 /// to match the storage field's name; the accessor's identity maps
621 /// onto the canonical wasmtime-`Store::epoch_deadline_*`-shaped
622 /// vocabulary the slot's docstring already carries.
623 #[must_use]
624 pub const fn wall_clock(&self) -> Option<Duration> {
625 self.wall_clock
626 }
627
628 /// Substrate-canonical per-`:limits` `:cpu` Kubernetes-millicore
629 /// soft cgroup-share scalar accessor every consumer of the Servico's
630 /// pod-spec `resources.requests.cpu` propagation keys off — returns
631 /// the author-declared `:limits :cpu` typed millicore magnitude
632 /// verbatim as an `Option<u32>`, copied out of the typed slot's own
633 /// `Option<u32>` storage (`Option<u32>` is `Copy`, so the accessor
634 /// returns by value; no borrow of `&self` past the call). `None`
635 /// when the slot is absent (the "no cpu share declared —
636 /// scheduler-default applies, today the pre-M2 unbounded-cpu-share
637 /// shape" arm the module-level docstring names on
638 /// [`LimitsSpec::cpu`] itself — [`LimitsSpec::is_empty`]'s
639 /// `cpu().is_none()` arm reads this predicate too, so an
640 /// authored-but-unset `:limits (:cpu ())` round-trips to a
641 /// `servico_m2_overlay` emission structurally identical to one that
642 /// omits the slot entirely).
643 ///
644 /// The `:limits :cpu` slot carries the "per-process soft cgroup-v2
645 /// CPU share" Kubernetes-scheduler-shaped sandboxing hint
646 /// (`theory/INSPIRATIONS.md` §III.1 — Lunatic's supervised
647 /// wasm-`Store`-per-process host-runtime CPU accounting, translated
648 /// onto pleme-io's typed `:limits` slot as a scheduler-facing
649 /// millicore request the pod's kubelet propagates to the container's
650 /// cgroup) — the typed slot's `Option<u32>` accept-set (zero-floor
651 /// rejected through [`LimitsError::CpuZero`] because a zero cgroup
652 /// share starves the process; upper-bounded by
653 /// [`LIMITS_CPU_MILLICORES_MAX`] (128 cores — the largest commercially-
654 /// common non-metal cloud Kubernetes node vCPU count on managed GKE
655 /// / EKS / AKS general-purpose SKUs)) maps onto the K8s pod spec's
656 /// `spec.containers[].resources.requests.cpu` field the
657 /// M2.5 `wasm-engine` host-runtime lands on the `ComputeUnit` CR-side
658 /// pod template and, via [`crate::render::servico_m2_overlay`], onto
659 /// the `pleme-computeunit` Helm-library-chart values sub-block's
660 /// `limits.cpu` key that lands as the `ComputeUnit` CR's
661 /// `spec.limits.cpu` field.
662 ///
663 /// Prior to this lift the `.cpu` field was accessed inline at two
664 /// sites inside `impl LimitsSpec` — [`LimitsSpec::is_empty`]'s
665 /// `self.cpu.is_none()` arm and [`LimitsSpec::validate`]'s
666 /// `if let Some(m) = self.cpu { … }` zero-floor + upper-cap bracket
667 /// arm — two open-coded field-accesses that expressed no
668 /// compile-time link back to the typed slot. A future extension of
669 /// the `:limits :cpu` axis to a richer author surface — a
670 /// per-instance `ComputeUnit` CR-side `spec.limits.cpu` overlay the
671 /// operator pins per-cluster, a split of the single `u32` millicore
672 /// request into a `{request, limit}` pair once the pod spec's
673 /// `resources.requests.cpu` / `resources.limits.cpu` distinction
674 /// promotes past its current single-request author surface, a
675 /// millicore → cgroup-v2 `cpu.weight` rescale once the operator's
676 /// scheduler-facing translation lands past its current kubelet
677 /// passthrough — would have had to be threaded through every
678 /// open-coded copy in lockstep or the emptiness predicate and the
679 /// validate call would silently disagree on which cgroup share a
680 /// given [`LimitsSpec`] resolves to. Lifting the resolution to a
681 /// typed method on the substrate primitive means every downstream
682 /// consumer of the Servico's per-`:limits` cpu-share surface reaches
683 /// for exactly one typed dispatch — the resolver's accept-set
684 /// migrates as a unit on any future axis addition.
685 ///
686 /// Fourth and final `Option<Copy-T>`-return accessor on the M2 slot
687 /// family (peer of the sibling per-`:limits` [`LimitsSpec::memory`]
688 /// (620c067) `Option<u64>` accessor, per-`:limits`
689 /// [`LimitsSpec::fuel`] (795dee7) `Option<u64>` accessor, and
690 /// per-`:limits` [`LimitsSpec::wall_clock`] (8cb717b)
691 /// `Option<Duration>` accessor — same typed-optional-scalar shape
692 /// extended to the peer per-`:limits` cgroup-cpu-share axis; sibling
693 /// to [`crate::MeshPolicy::mtls_required`] (c0110f1) /
694 /// [`crate::MeshPolicy::retries`] (bdfb399) /
695 /// [`crate::MeshPolicy::timeout`] (7073d0f) on the closed M3
696 /// mesh-slot `Option<Copy-T>` accessor family). The four-tuple
697 /// `(memory(), fuel(), wall_clock(), cpu())` jointly projects every
698 /// `Option<Copy-T>` axis on the M2 `:limits` slot every consumer
699 /// that fans on wasm-linear-memory-cap + wasm-fuel-budget +
700 /// wall-clock-deadline + cgroup-cpu-share keys off — closes the M2
701 /// `:limits` slot family's `Option<Copy-T>` accessor axis (the
702 /// last unlifted `:limits` field-access site on the M2 slot family;
703 /// every axis now routes through a typed dispatch on the substrate
704 /// primitive, with no open-coded field access anywhere on the impl).
705 /// Named `cpu()` to match the storage field's name; the accessor's
706 /// identity maps onto the canonical Kubernetes-`resources.requests.cpu`-
707 /// shaped vocabulary the slot's docstring already carries.
708 #[must_use]
709 pub const fn cpu(&self) -> Option<u32> {
710 self.cpu
711 }
712
713 /// Reject operationally-meaningless zero values on every declared
714 /// axis. Each axis remains optional — omitting a field expresses
715 /// "no bound on this axis"; the bug being closed is *carrying* a
716 /// zero value, which the wasm-engine consumes as "trap the first
717 /// instruction" / "instantiation refused" / "immediate timeout"
718 /// rather than the author's intended "an unspecified bound".
719 ///
720 /// Mirrors the discipline applied to `:politicas` axes in
721 /// `AplicacaoSpec::validate` and to `SupervisorSpec::max_restarts`
722 /// — every typed value carried by a slot is either absent or
723 /// meaningfully non-zero.
724 pub fn validate(&self) -> Result<(), LimitsError> {
725 // Route the `:memory` axis's four value-shape gates
726 // (zero-floor → wasm32-page-floor → wasm32-address-cap →
727 // page-multiple) through the substrate helper
728 // [`crate::render::require_positive_quantum_multiple_bounded_u64`]
729 // rather than four sequential inline
730 // `if let Some(m) = self.memory()` guards each restating one
731 // arm. Brings the `:memory` axis onto the same "one substrate
732 // helper per typed axis" discipline the peer `:fuel` (routed
733 // through [`crate::render::require_positive_bounded_u64`]),
734 // `:wall-clock` (through
735 // [`crate::render::require_positive_canonical_bounded_duration`]),
736 // and `:cpu` (through
737 // [`crate::render::require_positive_bounded_u32`]) axes
738 // already carry — every `LimitsSpec::validate` axis is now
739 // exactly one typed-helper dispatch, with the four-arm
740 // ordering (zero → below-quantum → cap → not-multiple)
741 // promoted from a per-site convention four inline blocks
742 // re-derived by hand to a structural contract on the
743 // substrate primitive. Byte-equal today: the helper fires the
744 // same four arms in the same canonical order at the same
745 // boundary values, threading the offending byte count into
746 // the same `MemoryBelowWasm32Page` / `MemoryExceedsWasm32Cap`
747 // / `MemoryNotPageMultiple` discriminator fields the four
748 // pre-lift inline arms already carried, so every existing
749 // per-arm test in this module continues to pin the same
750 // shape unchanged. Pinned end-to-end by
751 // `validate_memory_axis_routes_through_quantum_multiple_bounded_helper`.
752 if let Some(m) = self.memory() {
753 crate::render::require_positive_quantum_multiple_bounded_u64(
754 m,
755 LIMITS_MEMORY_WASM32_PAGE_BYTES,
756 LIMITS_MEMORY_WASM32_MAX_BYTES,
757 || LimitsError::MemoryZero,
758 LimitsError::memory_below_wasm32_page,
759 LimitsError::memory_exceeds_wasm32_cap,
760 LimitsError::memory_not_page_multiple,
761 )?;
762 }
763 // Zero-floor + upper-cap bracket on the typed `:fuel` axis. See
764 // [`crate::render::require_positive_bounded_u64`] for the
765 // ordering discipline (zero-floor arm strictly precedes cap arm
766 // so `Some(0)` surfaces the self-locating `FuelZero` diagnostic
767 // with its omit-axis remediation directly named, not the
768 // misleading `0 > LIMITS_FUEL_MAX == false` cap-arm miss).
769 // Until this bracket landed the `Option<u64>` slot accepted any
770 // value past zero (the parser's only upper bound was `u64::MAX`),
771 // so `(:fuel 18446744073709551615)` round-tripped cleanly
772 // through serde and the per-process CSE invariant (no value the
773 // wasm-engine's fuel counter can't honor as a meaningful budget
774 // before the sibling `:wall-clock` deadline fires) was a
775 // runtime, not build-time, contract on every above-cap input
776 // — the canonical declared-but-no-op footgun the sibling
777 // [`LimitsError::MemoryExceedsWasm32Cap`] /
778 // [`LimitsError::WallClockExceedsCap`] /
779 // [`LimitsError::CpuExceedsCap`] arms close on the peer
780 // "cannot be honored" / "unschedulable hint" /
781 // "nominal-only deadline" shapes, the peer
782 // [`crate::AplicacaoError::PolicyTimeoutExceedsCap`] /
783 // [`crate::AplicacaoError::PolicyBreakerWindowExceedsCap`] /
784 // [`crate::AplicacaoError::PolicyRateLimitExceedsCap`] arms
785 // close on the no-op-deadline / lifetime-counter / no-op-limiter
786 // shapes, and the
787 // [`crate::SupervisorError::MaxRestartsExceedsCap`] arm closes
788 // on the no-op-supervisor shape. The four `:limits` axes are
789 // now uniformly bracketed top and bottom (`:memory` in
790 // `LIMITS_MEMORY_WASM32_PAGE_BYTES..=LIMITS_MEMORY_WASM32_MAX_BYTES`,
791 // `:fuel` in `1..=LIMITS_FUEL_MAX`, `:wall-clock` in
792 // `1ms..=LIMITS_WALL_CLOCK_MAX`, `:cpu` in
793 // `1..=LIMITS_CPU_MILLICORES_MAX`).
794 if let Some(f) = self.fuel() {
795 crate::render::require_positive_bounded_u64(
796 f,
797 LIMITS_FUEL_MAX,
798 || LimitsError::FuelZero,
799 LimitsError::fuel_exceeds_cap,
800 )?;
801 }
802 if let Some(w) = self.wall_clock() {
803 // Zero-floor + integer-millisecond canonical-form +
804 // upper-cap bracket on the typed `:wall-clock` axis. See
805 // [`crate::render::require_positive_canonical_bounded_duration`]
806 // for the full three-arm ordering discipline (zero-floor
807 // strictly precedes canonical-form so `Duration::ZERO`
808 // surfaces the self-locating `WallClockZero` diagnostic;
809 // canonical-form strictly precedes the cap arm so a
810 // sub-millisecond above-cap value surfaces the more
811 // fundamental round-trip-shape diagnostic first) and the
812 // three peer typed-`Duration` sites that share this
813 // canonical bracket ([`crate::MeshPolicy::timeout`],
814 // [`crate::CircuitBreaker::window`],
815 // [`crate::SupervisorSpec::restart_window`]). Every
816 // validated value lies in `1ms..=LIMITS_WALL_CLOCK_MAX`
817 // (1ms..=1h), integer-millisecond granularity.
818 crate::render::require_positive_canonical_bounded_duration(
819 w,
820 LIMITS_WALL_CLOCK_MAX,
821 || LimitsError::WallClockZero,
822 LimitsError::wall_clock_not_canonical,
823 LimitsError::wall_clock_exceeds_cap,
824 )?;
825 }
826 // Zero-floor + upper-cap bracket on the typed `:cpu` axis. See
827 // [`crate::render::require_positive_bounded_u32`] for the
828 // ordering discipline (zero-floor arm strictly precedes cap arm
829 // so `Some(0)` surfaces the self-locating `CpuZero` diagnostic
830 // with its omit-axis remediation directly named, not the
831 // misleading `0 > LIMITS_CPU_MILLICORES_MAX == false` cap-arm
832 // miss). The bracket set is `1..=LIMITS_CPU_MILLICORES_MAX`
833 // (128 cores = 128_000 millicores — the largest commercially-
834 // common non-metal cloud Kubernetes node vCPU count). Until
835 // this bracket landed the millicore codec accepted any
836 // `Option<u32>` past zero (the prior numeric-zero arm's only
837 // floor), so `(:cpu "1000000m")` (1000 cores) round-tripped
838 // cleanly through serde and the per-axis CSE invariant (no
839 // value the Kubernetes scheduler can't honor) was a runtime,
840 // not build-time, contract on every above-cap input: the
841 // `pleme-computeunit` chart's `resources.requests.cpu` landed
842 // verbatim, the pod sat `Pending` indefinitely with a `0/N
843 // nodes are available: N Insufficient cpu` event, and the
844 // typed `:cpu` slot became an unschedulable hint far from the
845 // source caixa.lisp. Closes the same gap the wasm32-wasip2
846 // upper ceiling closes on the `:memory` axis — the typed `:cpu`
847 // axis is now operationally bracketed. Peer with every sibling
848 // cap arm on this surface ([`LimitsError::MemoryExceedsWasm32Cap`],
849 // [`LimitsError::WallClockExceedsCap`],
850 // [`crate::AplicacaoError::PolicyTimeoutExceedsCap`],
851 // [`crate::AplicacaoError::PolicyRetriesExceedsCap`],
852 // [`crate::AplicacaoError::PolicyBreakerMaxFailuresExceedsCap`],
853 // [`crate::AplicacaoError::PolicyBreakerWindowExceedsCap`],
854 // [`crate::AplicacaoError::PolicyRateLimitExceedsCap`],
855 // [`crate::SupervisorError::MaxRestartsExceedsCap`]).
856 if let Some(m) = self.cpu() {
857 crate::render::require_positive_bounded_u32(
858 m,
859 LIMITS_CPU_MILLICORES_MAX,
860 || LimitsError::CpuZero,
861 LimitsError::cpu_exceeds_cap,
862 )?;
863 }
864 Ok(())
865 }
866}
867
868#[derive(Debug, Error, PartialEq, Eq)]
869pub enum LimitsError {
870 #[error("byte-size: missing magnitude in {0:?}")]
871 EmptyByteSize(String),
872 #[error("byte-size: unknown unit {unit:?} (expected one of B, KB, MB, GB, KiB, MiB, GiB)")]
873 UnknownByteUnit { unit: String },
874 #[error("byte-size: failed to parse magnitude {0:?}")]
875 BadByteMagnitude(String),
876 #[error(
877 "byte-size: magnitude {value:?} is not a non-negative integer — the canonical \
878 authoring form for `:limits :memory` is `<integer><unit>` (e.g. `\"1024\"`, \
879 `\"64MiB\"`, `\"1GiB\"`) with no decimal point and no leading `+` sign. A \
880 fractional / decimal-shaped magnitude (`\"1.5KiB\"`, `\"1.0MiB\"`, `\"0.5GiB\"`, \
881 `\"+1024\"`) round-trips through `render_byte_size` to a *different* canonical \
882 form (`\"1536\"`, `\"1MiB\"`, `\"512MiB\"`, `\"1KiB\"`) on first serialize — \
883 breaking the THEORY.md §V.2.7 render-determinism contract every typed slot \
884 carries. Pick an integer magnitude in the unit that divides cleanly (write \
885 `\"1536\"` instead of `\"1.5KiB\"`; `\"512MiB\"` instead of `\"0.5GiB\"`)"
886 )]
887 NonIntegerByteMagnitude { value: String },
888 #[error(
889 "byte-size: magnitude {value:?} has a non-canonical leading zero — the canonical \
890 authoring form for `:limits :memory` is `<integer><unit>` (e.g. `\"64MiB\"`, \
891 `\"1GiB\"`, `\"512KiB\"`, `\"1024\"`) with no leading-zero padding on the magnitude. \
892 A leading-zero magnitude (`\"064MiB\"`, `\"01024\"`, `\"00KiB\"`, `\"0500MB\"`) round-trips \
893 through `render_byte_size` to a *different* canonical form (`\"64MiB\"`, `\"1KiB\"`, \
894 `\"0\"`, `\"500MB\"`) on first serialize — breaking the THEORY.md Part V \
895 render-determinism contract every typed slot carries. Strip the leading zeros \
896 (write `\"64MiB\"` instead of `\"064MiB\"`)"
897 )]
898 LeadingZeroByteMagnitude { value: String },
899 #[error(
900 "byte-size: value {value:?} contains whitespace byte 0x{byte:02x} — the canonical \
901 authoring form for `:limits :memory` is `<integer><unit>` (e.g. `\"64MiB\"`, \
902 `\"1GiB\"`, `\"512KiB\"`, `\"1024\"`) with no whitespace bytes anywhere. A \
903 whitespace-carrying shape (`\" 64MiB\"`, `\"64MiB \"`, `\"64 MiB\"`, `\"\\t64MiB\"`, \
904 `\"64MiB\\n\"`) round-trips through `render_byte_size` to a *different* canonical \
905 form (`\"64MiB\"`) on first serialize — breaking the THEORY.md Part V \
906 render-determinism contract every typed slot carries. Strip every whitespace byte \
907 (write `\"64MiB\"` verbatim)"
908 )]
909 WhitespaceInByteSize { value: String, byte: u8 },
910 #[error(
911 "byte-size: value {value:?} contains a non-ASCII Unicode whitespace character \
912 {ch:?} (U+{codepoint:04X}) — the canonical authoring form for `:limits :memory` \
913 is `<integer><unit>` (e.g. `\"64MiB\"`, `\"1GiB\"`, `\"512KiB\"`, `\"1024\"`) \
914 with no whitespace characters anywhere (ASCII or Unicode). A non-ASCII-whitespace-\
915 carrying shape (`\"\\u{{00A0}}64MiB\"` — paste-from-typography NBSP prefix; \
916 `\"64MiB\\u{{2028}}\"` — paste-from-web-doc line-separator suffix; \
917 `\"64\\u{{2003}}MiB\"` — paste-from-typography EM-SPACE between magnitude and \
918 unit) survives the pre-existing `u8::is_ascii_whitespace` byte-scan (none of \
919 its bytes match the ASCII whitespace set) but `str::trim` (which uses \
920 `char::is_whitespace` — the Unicode `White_Space` property, strictly wider than \
921 the ASCII byte set) silently strips it at parse entry, and the value round-trips \
922 through `render_byte_size` to a *different* canonical form (`\"64MiB\"`) on \
923 first serialize — breaking the THEORY.md Part V render-determinism contract \
924 every typed slot carries. Strip every non-ASCII whitespace character (write \
925 `\"64MiB\"` verbatim with only ASCII bytes)"
926 )]
927 NonAsciiWhitespaceInByteSize {
928 value: String,
929 ch: char,
930 codepoint: u32,
931 },
932 #[error("duration: missing magnitude in {0:?}")]
933 EmptyDuration(String),
934 #[error("duration: unknown unit {unit:?} (expected one of ms, s, m, h)")]
935 UnknownDurationUnit { unit: String },
936 #[error("duration: failed to parse magnitude {0:?}")]
937 BadDurationMagnitude(String),
938 #[error(
939 "duration: magnitude {value:?} is not a non-negative integer — the canonical \
940 authoring form for `:limits :wall-clock` is `<integer><unit>` (e.g. `\"30s\"`, \
941 `\"500ms\"`, `\"2m\"`, `\"1h\"`) with no decimal point and no leading `+` sign. A \
942 fractional / decimal-shaped magnitude (`\"1.5s\"`, `\"1.0s\"`, `\"0.5m\"`, \
943 `\"+30s\"`, `\"-30s\"`) round-trips through `render_duration` to a *different* \
944 canonical form (`\"1500ms\"`, `\"1s\"`, `\"30s\"`, `\"30s\"`) on first serialize \
945 — breaking the THEORY.md Part V render-determinism contract every typed slot \
946 carries. Pick an integer magnitude in the unit that divides cleanly (write \
947 `\"1500ms\"` instead of `\"1.5s\"`; `\"30s\"` instead of `\"0.5m\"`)"
948 )]
949 NonIntegerDurationMagnitude { value: String },
950 #[error(
951 "duration: magnitude {value:?} has a non-canonical leading zero — the canonical \
952 authoring form for `:limits :wall-clock` is `<integer><unit>` (e.g. `\"30s\"`, \
953 `\"500ms\"`, `\"2m\"`, `\"1h\"`) with no leading-zero padding on the magnitude. \
954 A leading-zero magnitude (`\"030s\"`, `\"00s\"`, `\"01h\"`, `\"0500ms\"`) round-trips \
955 through `render_duration` to a *different* canonical form (`\"30s\"`, `\"0s\"`, \
956 `\"1h\"`, `\"500ms\"`) on first serialize — breaking the THEORY.md Part V \
957 render-determinism contract every typed slot carries. Strip the leading zeros \
958 (write `\"30s\"` instead of `\"030s\"`)"
959 )]
960 LeadingZeroDurationMagnitude { value: String },
961 #[error(
962 "duration: value {value:?} contains whitespace byte 0x{byte:02x} — the canonical \
963 authoring form for `:limits :wall-clock` is `<integer><unit>` (e.g. `\"30s\"`, \
964 `\"500ms\"`, `\"2m\"`, `\"1h\"`) with no whitespace bytes anywhere. A \
965 whitespace-carrying shape (`\" 30s\"`, `\"30s \"`, `\"30 s\"`, `\"\\t30s\"`, \
966 `\"30s\\n\"`) round-trips through `render_duration` to a *different* canonical form \
967 (`\"30s\"`) on first serialize — breaking the THEORY.md Part V render-determinism \
968 contract every typed slot carries. Strip every whitespace byte (write `\"30s\"` \
969 verbatim)"
970 )]
971 WhitespaceInDuration { value: String, byte: u8 },
972 #[error(
973 "duration: value {value:?} contains a non-ASCII Unicode whitespace character \
974 {ch:?} (U+{codepoint:04X}) — the canonical authoring form for `:limits :wall-clock` \
975 is `<integer><unit>` (e.g. `\"30s\"`, `\"500ms\"`, `\"2m\"`, `\"1h\"`) with no \
976 whitespace characters anywhere (ASCII or Unicode). A non-ASCII-whitespace-\
977 carrying shape (`\"\\u{{00A0}}30s\"` — paste-from-typography NBSP prefix; \
978 `\"30s\\u{{2028}}\"` — paste-from-web-doc line-separator suffix; \
979 `\"30\\u{{2003}}s\"` — paste-from-typography EM-SPACE between magnitude and \
980 unit) survives the pre-existing `u8::is_ascii_whitespace` byte-scan (none of \
981 its bytes match the ASCII whitespace set) but `str::trim` (which uses \
982 `char::is_whitespace` — the Unicode `White_Space` property, strictly wider than \
983 the ASCII byte set) silently strips it at parse entry, and the value round-trips \
984 through `render_duration` to a *different* canonical form (`\"30s\"`) on first \
985 serialize — breaking the THEORY.md Part V render-determinism contract every \
986 typed slot carries. Strip every non-ASCII whitespace character (write `\"30s\"` \
987 verbatim with only ASCII bytes)"
988 )]
989 NonAsciiWhitespaceInDuration {
990 value: String,
991 ch: char,
992 codepoint: u32,
993 },
994 #[error("millicores: bad value {0:?} (expected `<int>m` or `<int>`)")]
995 BadMillicores(String),
996 #[error(
997 "millicores: magnitude {value:?} is not a non-negative integer — the canonical \
998 authoring form for `:limits :cpu` is `<integer>m` (Kubernetes millicores, e.g. \
999 `\"500m\"` for half a core, `\"2000m\"` for two cores) or the bare-core \
1000 shorthand `<integer>` (e.g. `\"2\"` = `\"2000m\"`), with no decimal point and \
1001 no leading `+` sign. A fractional / decimal-shaped magnitude (`\"1.5\"`, \
1002 `\"500.0m\"`, `\"+500m\"`, `\"-100m\"`) round-trips through `render_millicores` \
1003 to a *different* canonical form (`\"1500m\"`, `\"500m\"`, `\"500m\"`, \
1004 parse-rejection) on first serialize — breaking the THEORY.md Part V \
1005 render-determinism contract every typed slot carries. Pick an integer magnitude \
1006 in millicores (write `\"1500m\"` instead of `\"1.5\"`; `\"500m\"` instead of \
1007 `\"500.0m\"`)"
1008 )]
1009 NonIntegerMillicoreMagnitude { value: String },
1010 #[error(
1011 "millicores: magnitude {value:?} has a non-canonical leading zero — the canonical \
1012 authoring form for `:limits :cpu` is `<integer>m` (Kubernetes millicores, e.g. \
1013 `\"500m\"` for half a core, `\"2000m\"` for two cores) or the bare-core shorthand \
1014 `<integer>` (e.g. `\"2\"` = `\"2000m\"`) with no leading-zero padding on the \
1015 magnitude. A leading-zero magnitude (`\"0500m\"`, `\"00m\"`, `\"02\"`, `\"01500m\"`) \
1016 round-trips through `render_millicores` to a *different* canonical form (`\"500m\"`, \
1017 `\"0m\"`, `\"2000m\"`, `\"1500m\"`) on first serialize — breaking the THEORY.md Part \
1018 V render-determinism contract every typed slot carries. Strip the leading zeros \
1019 (write `\"500m\"` instead of `\"0500m\"`; `\"2\"` instead of `\"02\"`)"
1020 )]
1021 LeadingZeroMillicoreMagnitude { value: String },
1022 #[error(
1023 "millicores: value {value:?} contains whitespace byte 0x{byte:02x} — the canonical \
1024 authoring form for `:limits :cpu` is `<integer>m` (Kubernetes millicores, e.g. \
1025 `\"500m\"`, `\"2000m\"`) or the bare-core shorthand `<integer>` (e.g. `\"2\"`) \
1026 with no whitespace bytes anywhere. A whitespace-carrying shape (`\" 500m\"`, \
1027 `\"500m \"`, `\"500 m\"`, `\"\\t500m\"`, `\"500m\\n\"`) round-trips through \
1028 `render_millicores` to a *different* canonical form (`\"500m\"`) on first \
1029 serialize — breaking the THEORY.md Part V render-determinism contract every \
1030 typed slot carries. Strip every whitespace byte (write `\"500m\"` verbatim)"
1031 )]
1032 WhitespaceInMillicores { value: String, byte: u8 },
1033 #[error(
1034 "millicores: value {value:?} contains a non-ASCII Unicode whitespace character \
1035 {ch:?} (U+{codepoint:04X}) — the canonical authoring form for `:limits :cpu` is \
1036 `<integer>m` (Kubernetes millicores, e.g. `\"500m\"`, `\"2000m\"`) or the \
1037 bare-core shorthand `<integer>` (e.g. `\"2\"`) with no whitespace characters \
1038 anywhere (ASCII or Unicode). A non-ASCII-whitespace-carrying shape \
1039 (`\"\\u{{00A0}}500m\"` — paste-from-typography NBSP prefix; \
1040 `\"500m\\u{{2028}}\"` — paste-from-web-doc line-separator suffix; \
1041 `\"500\\u{{2003}}m\"` — paste-from-typography EM-SPACE between magnitude and \
1042 unit) survives the pre-existing `u8::is_ascii_whitespace` byte-scan (none of \
1043 its bytes match the ASCII whitespace set) but `str::trim` (which uses \
1044 `char::is_whitespace` — the Unicode `White_Space` property, strictly wider than \
1045 the ASCII byte set) silently strips it at parse entry, and the value round-trips \
1046 through `render_millicores` to a *different* canonical form (`\"500m\"`) on \
1047 first serialize — breaking the THEORY.md Part V render-determinism contract \
1048 every typed slot carries. Strip every non-ASCII whitespace character (write \
1049 `\"500m\"` verbatim with only ASCII bytes)"
1050 )]
1051 NonAsciiWhitespaceInMillicores {
1052 value: String,
1053 ch: char,
1054 codepoint: u32,
1055 },
1056 #[error(
1057 ":limits :memory must be > 0 — wasmtime StoreLimits refuses a zero memory cap; omit the field for unbounded"
1058 )]
1059 MemoryZero,
1060 #[error(
1061 ":limits :memory ({bytes} bytes) is below the wasm32-wasip2 linear-memory page size (64 KiB = 65536 bytes) — a sub-page cap cannot hold a single wasm linear memory page, so instantiation of any component declaring `(memory 1)` traps with `memory minimum size of 1 pages exceeds memory limits` and a `(memory 0)` component traps the first `memory.grow(1)`. Pin a value ≥ 64 KiB (e.g. `\"64KiB\"`, `\"1MiB\"`, `\"64MiB\"`) or omit the field for unbounded"
1062 )]
1063 MemoryBelowWasm32Page { bytes: u64 },
1064 #[error(
1065 ":limits :memory ({bytes} bytes) exceeds the wasm32-wasip2 linear-memory ceiling (4 GiB = 4294967296 bytes); pin a value ≤ 4 GiB or omit the field for unbounded"
1066 )]
1067 MemoryExceedsWasm32Cap { bytes: u64 },
1068 #[error(
1069 ":limits :memory ({bytes} bytes) carries a sub-page residue the wasm32-wasip2 \
1070 linear-memory model cannot honor — the wasm spec defines linear memory in \
1071 fixed 64 KiB pages (LIMITS_MEMORY_WASM32_PAGE_BYTES = 65536 bytes) and \
1072 wasmtime's StoreLimits::memory_size is consumed as a page-quantized ceiling: \
1073 the engine can grow at most floor({bytes} / 65536) pages, and the bytes in \
1074 [floor({bytes} / 65536) * 65536, {bytes}] are structural dead space the \
1075 runtime cannot honor. Pin a page-aligned value in 64KiB..=4GiB \
1076 (the canonical authoring magnitudes — `\"64KiB\"`, `\"128KiB\"`, `\"1MiB\"`, \
1077 `\"64MiB\"`, `\"1GiB\"`, `\"4GiB\"` — every power-of-1024 unit the byte-size \
1078 codec emits divides cleanly by the page size) or omit the field for unbounded"
1079 )]
1080 MemoryNotPageMultiple { bytes: u64 },
1081 #[error(
1082 ":limits :fuel must be > 0 — wasmtime traps the first instruction at fuel=0; omit the field for unbounded"
1083 )]
1084 FuelZero,
1085 #[error(
1086 ":limits :fuel ({fuel} instructions) exceeds the per-process ceiling \
1087 (LIMITS_FUEL_MAX = 1_000_000_000_000 = 10^12 wasm instructions) — a value \
1088 above this cap turns the typed per-call fuel counter into a no-op budget: \
1089 the sibling `:wall-clock` cap (LIMITS_WALL_CLOCK_MAX = 1h = 3600s) fires \
1090 before the fuel counter could ever be drained (wasmtime's documented \
1091 fuel-tracked execution rate sits at ~10^8–10^9 fuel-units per second on \
1092 modern x86_64 / aarch64 hosts running wasmtime through Cranelift, so the \
1093 largest realistic per-call fuel budget reachable within 1h sits at ~3.6 × \
1094 10^11–3.6 × 10^12 fuel-units, and a value above 10^12 is structurally \
1095 unreachable as a per-call counter), so the typed `:fuel` slot becomes a \
1096 declared-but-no-op contract far from the source caixa.lisp. Pin a value \
1097 in 1..=1_000_000_000_000 (the canonical caixa Servico runs in the \
1098 10^6..=10^9 fuel band — the in-tree `Caixa::template` documentation and \
1099 `caixa-feira` examples carry `:fuel 1_000_000` = 10^6, peer to \
1100 wasmtime's official `Store::set_fuel(1_000_000)` example in the wasmtime \
1101 book; production-shape per-request fuel budgets sit in the 10^7..=10^9 \
1102 band for compute-bound workloads) or omit :fuel to express `no per-call \
1103 fuel budget on this axis` (the wasm-engine then relies entirely on the \
1104 sibling `:wall-clock` cgroup / Kubernetes activeDeadlineSeconds deadline)"
1105 )]
1106 FuelExceedsCap { fuel: u64 },
1107 #[error(
1108 ":limits :wall-clock must be > 0 — a zero deadline expires before the call starts; omit the field for unbounded"
1109 )]
1110 WallClockZero,
1111 #[error(
1112 ":limits :wall-clock ({wall_clock:?}) carries a sub-millisecond residue the typed `:wall-clock` duration codec cannot round-trip — \
1113 the codec truncates to `as_millis()` before picking the canonical unit, so a value with `subsec_nanos() % 1_000_000 != 0` either \
1114 truncates on first serialize (e.g. `Duration::from_micros(1500)` → \"1ms\" → `Duration::from_millis(1)` ≠ original) or renders \
1115 as \"0s\" the `WallClockZero` arm then rejects on re-validate. Pin an integer-millisecond magnitude in the canonical authoring form \
1116 (`<integer><unit>` for unit ∈ {{ms, s, m, h}}, e.g. `\"500ms\"`, `\"30s\"`, `\"2m\"`, `\"1h\"`) or omit the field for unbounded"
1117 )]
1118 WallClockNotCanonical { wall_clock: Duration },
1119 #[error(
1120 ":limits :wall-clock ({wall_clock:?}) exceeds the per-process ceiling \
1121 (LIMITS_WALL_CLOCK_MAX = 1h = 3600s) — a value above this cap turns the typed \
1122 per-call deadline into a nominal-only contract (the wasm-engine's epoch-deadline \
1123 cancellation reaches for a `Duration` so long no realistic synchronous wasm call \
1124 can hit it), and the MESH-COMPOSITION §V \"no infinite blocking\" CSE invariant \
1125 degenerates to enforcement only at the per-Servico cgroup / Kubernetes \
1126 activeDeadlineSeconds layer — far above the per-call granularity the typed \
1127 `:limits :wall-clock` slot is meant to express. Pin a value in 1ms..=1h \
1128 (Envoy / Istio / Linkerd production per-request playbooks all recommend ≤ 60s; \
1129 AWS App Mesh / ingress-nginx typical ≤ 300s; the longest per-request \
1130 `proxy_read_timeout` ingress-nginx documents maxes out at the same 3600s ceiling) \
1131 or omit :wall-clock to express `no per-process deadline on this axis` (the \
1132 deadline then relies entirely on the cluster-level cgroup / pod \
1133 activeDeadlineSeconds bound)"
1134 )]
1135 WallClockExceedsCap { wall_clock: Duration },
1136 #[error(
1137 ":limits :cpu must be > 0m — a zero cgroup share starves the process; omit the field for unbounded"
1138 )]
1139 CpuZero,
1140 #[error(
1141 ":limits :cpu ({millicores}m) exceeds the per-process ceiling \
1142 (LIMITS_CPU_MILLICORES_MAX = 128_000m = 128 cores) — a value above this cap is \
1143 structurally unschedulable on every commercially-common managed-Kubernetes node \
1144 pool (GKE Standard / EKS managed / AKS default general-purpose SKU ladders top out \
1145 at 128 vCPU per node; AWS m7i.32xlarge / c7i.32xlarge, Azure HBv3-128rs, GCP \
1146 c3-standard-128 all sit at the same 128-vCPU ceiling), so the resulting \
1147 `pleme-computeunit` chart's `resources.requests.cpu` lands as a hint the \
1148 Kubernetes scheduler cannot bind to any node — the pod sits `Pending` indefinitely \
1149 with a `0/N nodes are available: N Insufficient cpu` event, and the typed `:cpu` \
1150 slot becomes an unschedulable contract far from the source caixa.lisp. The \
1151 wasm32-wasip2 single-threaded execution model the canonical caixa Servico targets \
1152 reinforces the structural argument: a single wasm component cannot saturate more \
1153 than one core, so even the Lunatic-style supervised-multi-process host bounds its \
1154 useful CPU request to the host node's vCPU count. Pin a value in 1m..=128000m \
1155 (the canonical caixa Servico runs in the 100m..=2000m band — every in-tree \
1156 example uses 500m; AWS App Mesh / Envoy / Istio per-pod CPU production playbooks \
1157 all sit ≤ 8000m / 8 cores; the longest documented per-Servico CPU request any \
1158 pleme-io substrate playbook recommends maxes at ~16 cores) or omit :cpu to \
1159 express `no per-process CPU hint on this axis` (the cgroup share then defaults to \
1160 the cluster-level `LimitRange` / `ResourceQuota` policy the operator pins on the \
1161 host namespace)"
1162 )]
1163 CpuExceedsCap { millicores: u32 },
1164}
1165
1166// ── byte-size codec ────────────────────────────────────────────────────
1167
1168fn parse_byte_size(s: &str) -> Result<u64, LimitsError> {
1169 // Paired whitespace-rejection arm — the ASCII byte-scan
1170 // (paste-from-aligned-doc leading space, shell-history trailing
1171 // space, typography space between magnitude and unit, block-scalar
1172 // tab, multi-line trailing newline) closes the WhatWG-conformant
1173 // ASCII whitespace bytes (`0x20`, `0x09`, `0x0A`, `0x0C`, `0x0D`);
1174 // the non-ASCII `char::is_whitespace` scan closes the strictly-
1175 // complementary Unicode `White_Space` class (NBSP `\u{00A0}`, LINE
1176 // SEPARATOR `\u{2028}`, EM-SPACE `\u{2003}`, and the peer
1177 // typography codepoints) that `str::trim` at parse entry silently
1178 // strips. Either drift class would round-trip through
1179 // `render_byte_size` to a *different* canonical form on next emit
1180 // — breaking the THEORY.md Part V render-determinism contract every
1181 // typed slot carries. Diagnostics stay typed at
1182 // `WhitespaceInByteSize` / `NonAsciiWhitespaceInByteSize` so the
1183 // failing byte / char + U+XXXX codepoint reaches the author verbatim
1184 // rather than being value-laundered through a downstream
1185 // `BadByteMagnitude` arm.
1186 //
1187 // Routed through the lifted [`crate::render::reject_whitespace`]
1188 // primitive — the substrate-side single-owner gate every typed-
1189 // magnitude codec in caixa-core (`parse_byte_size` /
1190 // `parse_duration` / `parse_millicores` /
1191 // `supervisor::duration_codec` / `rate_limit_codec`) shares. Drift
1192 // between any two codec sites' paired-arm rejection set becomes a
1193 // single-edit fix at the composed predicate rather than five
1194 // independent paired-arm re-inlines diverging over time.
1195 crate::render::reject_whitespace(
1196 s,
1197 |byte| LimitsError::whitespace_in_byte_size(s, byte),
1198 |ch| LimitsError::non_ascii_whitespace_in_byte_size(s, ch),
1199 )?;
1200 let s = s.trim();
1201 if s.is_empty() {
1202 return Err(LimitsError::empty_byte_size(s));
1203 }
1204 // Route the `<integer><ASCII-alphabetic-unit>` split through the
1205 // lifted [`crate::render::split_magnitude_and_alpha_unit`] primitive
1206 // — the substrate-side single-owner split every ASCII-alphabetic-unit
1207 // typed-magnitude codec in caixa-core (`parse_byte_size` /
1208 // `parse_duration` / `supervisor::duration_codec::parse`) shares.
1209 // Drift between any two codec sites' magnitude/unit split rule
1210 // becomes a single-edit fix at the composed helper rather than three
1211 // independent `s.find(|c: char| c.is_ascii_alphabetic())` re-inlines
1212 // diverging over time.
1213 let (num_part, unit) = crate::render::split_magnitude_and_alpha_unit(s);
1214 let num_trim = num_part.trim();
1215 // The canonical authoring form for `:limits :memory` is
1216 // `<integer><unit>` — every magnitude `render_byte_size` emits is a
1217 // non-negative integer with no decimal point and no leading sign,
1218 // so the parser's accepted set must match for serialize/deserialize
1219 // to round-trip without canonical-form drift. Until this gate
1220 // landed the parser accepted any `f64`-shaped magnitude
1221 // (`"1.5KiB"` → 1536 bytes, `"1.0MiB"` → 1MiB, `"0.5GiB"` → 512MiB,
1222 // `"+1024"` → 1024) and serde silently round-tripped the value to
1223 // a *different* canonical string on the next emit (`"1.5KiB"` →
1224 // 1536 → `"1536"`, `"1.0MiB"` → 1048576 → `"1MiB"`, `"0.5GiB"` →
1225 // 536870912 → `"512MiB"`, `"+1024"` → 1024 → `"1KiB"`) — breaking
1226 // the THEORY.md §V.2.7 render-determinism contract every typed slot
1227 // carries.
1228 //
1229 // Strict canonical form: every byte of the magnitude is an ASCII
1230 // digit (no `.`, no `+`, no `-`). On current Rust `u64::from_str`
1231 // permissively accepts a leading `+` (`"+1024"` → 1024) — that's a
1232 // canonical-drift shape `render_byte_size` never emits, so the
1233 // digit-only check is what closes the leading-sign class; relying
1234 // on `u64::from_str`'s strictness alone would silently admit it.
1235 // On non-digit-only inputs the gate distinguishes "non-canonical-
1236 // but-numeric" (parses as f64 or i64, so it's an authoring-shape
1237 // footgun) from "garbage" (parses as neither, so it's not a
1238 // numeric input at all) — the diagnostic names the offending
1239 // magnitude shape verbatim rather than collapsing both authoring
1240 // footguns into a single opaque `BadByteMagnitude`.
1241 //
1242 // Same canonical-form discipline
1243 // [`crate::AplicacaoSpec::validate_politicas`]'s
1244 // [`is_canonical_rate_limit_window`] gate (808017c) applies to the
1245 // rate-limit `:window` axis — the codec's accepted set matches its
1246 // emitted set, structurally.
1247 //
1248 // (Scientific-notation magnitudes like `"1e3KiB"` are also rejected,
1249 // but on a different arm: the parser splits on the first ASCII-
1250 // alphabetic byte, so the `e` is read as a unit prefix and the
1251 // input falls into the `UnknownByteUnit { unit: "e3KiB" }` branch
1252 // before this gate is consulted — that's the existing diagnostic
1253 // for the scientific-shape footgun, and this gate is additive to
1254 // it.)
1255 //
1256 // Routed through the lifted
1257 // [`crate::render::is_digit_only_magnitude`] predicate — the
1258 // single source of truth every typed-magnitude codec in
1259 // caixa-core (`parse_byte_size` / `parse_duration` /
1260 // `parse_millicores` / `supervisor::duration_codec` /
1261 // `rate_limit_codec`) shares. Drift between any two codec sites'
1262 // digit-only rejection set becomes a single-edit fix at the
1263 // shared predicate rather than five independent
1264 // `!<var>.is_empty() && <var>.bytes().all(|b| b.is_ascii_digit())`
1265 // scans diverging over time — same "single lifted source of truth"
1266 // discipline the peer canonical-form predicates
1267 // ([`crate::render::find_ascii_whitespace_byte`] /
1268 // [`crate::render::find_non_ascii_whitespace_char`] /
1269 // [`crate::render::is_leading_zero_padded_magnitude`]) carry on
1270 // the whitespace and leading-zero-padding drift-class axes.
1271 let digit_only = crate::render::is_digit_only_magnitude(num_trim);
1272 if !digit_only {
1273 // Distinguish "non-canonical-but-numeric" (`"1.5"`, `"1.0"`,
1274 // `"+1024"`, `"-1"`) from "garbage" (`"abc"`, `"--1"`) so the
1275 // diagnostic names the offending magnitude shape verbatim.
1276 // Use f64 + i64 fallbacks for the "numeric" detection so every
1277 // non-digit-only-but-parseable input lands on
1278 // `NonIntegerByteMagnitude` regardless of sign or fractionality.
1279 let numeric = num_trim.parse::<f64>().is_ok() || num_trim.parse::<i64>().is_ok();
1280 if numeric {
1281 return Err(LimitsError::non_integer_byte_magnitude(num_trim));
1282 }
1283 return Err(LimitsError::bad_byte_magnitude(num_part));
1284 }
1285 // Leading-zero arm — peer with the `parse_duration` leading-zero
1286 // arm (39762d7), the `supervisor::duration_codec` leading-zero arm
1287 // (9178904) and the `rate_limit_codec` leading-zero arm (4f46830)
1288 // on the same canonical-form render-determinism axis. The
1289 // digit-only gate accepts `"0064MiB"`, `"01024"`, `"00KiB"`,
1290 // `"0500MB"` as `u64::from_str` parses them losslessly (= 64, 1024,
1291 // 0, 500), but `render_byte_size` emits the leading-zero-stripped
1292 // form (`"64MiB"`, `"1KiB"`, `"0"`, `"500MB"`) — a *different*
1293 // canonical string on the next emit, breaking the THEORY.md Part V
1294 // render-determinism contract the same way `"+1024"` did before the
1295 // leading-`+` arm landed. The single-byte magnitude `"0"` (or
1296 // `"0B"` / `"0KiB"`) round-trips losslessly through
1297 // `render_byte_size` (`render_byte_size(0)` emits `"0"`) — the
1298 // downstream semantic-zero gate [`LimitsError::MemoryZero`] refuses
1299 // zero-magnitude authoring at the typed-validate layer above, so
1300 // the single-byte `"0"` stays in the accepted set at this codec
1301 // layer and the diagnostic partitioning between canonical-form
1302 // drift (this arm) and semantic-zero (the downstream gate) remains
1303 // stable. Same codec-layer / typed-validate-layer partition the
1304 // peer codecs preserve.
1305 //
1306 // Routed through the lifted
1307 // [`crate::render::is_leading_zero_padded_magnitude`] predicate —
1308 // the single source of truth every typed-magnitude codec in
1309 // caixa-core (`parse_byte_size` / `parse_duration` /
1310 // `parse_millicores` / `supervisor::duration_codec` /
1311 // `rate_limit_codec`) shares. Drift between any two codec sites'
1312 // leading-zero rejection set becomes a single-edit fix at the
1313 // shared predicate rather than five independent
1314 // `s.len() > 1 && s.as_bytes()[0] == b'0'` scans diverging over
1315 // time — same "single lifted source of truth" discipline the
1316 // peer whitespace predicates
1317 // ([`crate::render::find_ascii_whitespace_byte`] /
1318 // [`crate::render::find_non_ascii_whitespace_char`]) carry on
1319 // their strictly-complementary axes.
1320 if crate::render::is_leading_zero_padded_magnitude(num_trim) {
1321 return Err(LimitsError::leading_zero_byte_magnitude(num_trim));
1322 }
1323 // `digit_only` guarantees every byte is `[0-9]`, so the only way
1324 // u64::from_str can fail here is overflow (the magnitude exceeds
1325 // u64::MAX). Surface that as `BadByteMagnitude` with an overflow-
1326 // shaped wording so the diagnostic names the offending magnitude
1327 // verbatim rather than collapsing onto the non-canonical arm.
1328 let num: u64 = num_trim.parse::<u64>().map_err(|_| {
1329 LimitsError::bad_byte_magnitude(format!("{num_trim} (digit-only magnitude overflows u64)"))
1330 })?;
1331 let multiplier: u64 = match unit.trim() {
1332 "" | "B" => 1,
1333 "KB" => 1_000,
1334 "MB" => 1_000_000,
1335 "GB" => 1_000_000_000,
1336 "KiB" => 1024,
1337 "MiB" => 1024 * 1024,
1338 "GiB" => 1024 * 1024 * 1024,
1339 other => {
1340 return Err(LimitsError::unknown_byte_unit(other));
1341 }
1342 };
1343 // Overflow surfaces as `BadByteMagnitude` (a u64-saturating
1344 // multiply would silently truncate to `u64::MAX` and then the
1345 // wasm32-cap gate at validate time would catch it — but a u64
1346 // overflow is a parse-shaped failure on the author's input, not a
1347 // domain-cap rejection on a well-formed value, so it surfaces here
1348 // as a parser diagnostic naming the offending magnitude × unit
1349 // pair rather than as `MemoryExceedsWasm32Cap { bytes: u64::MAX }`
1350 // far from the author's intent).
1351 num.checked_mul(multiplier).ok_or_else(|| {
1352 LimitsError::bad_byte_magnitude(format!(
1353 "{num_trim}{unit_trim} overflows u64 (magnitude × unit > 2^64-1)",
1354 unit_trim = unit.trim()
1355 ))
1356 })
1357}
1358
1359fn render_byte_size(n: u64) -> String {
1360 // Prefer the largest power-of-1024 unit that divides cleanly; fall
1361 // back to bytes if nothing matches.
1362 const UNITS: &[(u64, &str)] = &[
1363 (1024 * 1024 * 1024, "GiB"),
1364 (1024 * 1024, "MiB"),
1365 (1024, "KiB"),
1366 ];
1367 for (mult, label) in UNITS {
1368 if n >= *mult && n.is_multiple_of(*mult) {
1369 return format!("{}{label}", n / mult);
1370 }
1371 }
1372 format!("{n}")
1373}
1374
1375fn ser_byte_size<S: Serializer>(v: &Option<u64>, s: S) -> Result<S::Ok, S::Error> {
1376 // Route through the canonical [`crate::render::serialize_option_via_str`]
1377 // — the substrate-side single-owner primitive for the forward arm
1378 // of the typed-magnitude codec family. See its docstring for the
1379 // full sibling roster and the compounding rationale that pins this
1380 // lift; load-bearing pinned by
1381 // `tests::ser_byte_size_routes_through_render_serialize_option_via_str_canonical`.
1382 crate::render::serialize_option_via_str(v, s, render_byte_size)
1383}
1384
1385fn de_byte_size<'de, D: Deserializer<'de>>(d: D) -> Result<Option<u64>, D::Error> {
1386 // Route through the canonical [`crate::render::deserialize_option_via_str`]
1387 // — the substrate-side single-owner primitive for the reverse arm
1388 // of the typed-magnitude codec family. See its docstring for the
1389 // full sibling roster and the compounding rationale that pins this
1390 // lift; load-bearing pinned by
1391 // `tests::de_byte_size_routes_through_render_deserialize_option_via_str_canonical`.
1392 crate::render::deserialize_option_via_str(d, parse_byte_size)
1393}
1394
1395// ── duration codec ─────────────────────────────────────────────────────
1396
1397fn parse_duration(s: &str) -> Result<Duration, LimitsError> {
1398 // Paired whitespace-rejection arm — same canonical-form
1399 // render-determinism discipline as the peer `parse_byte_size` /
1400 // `parse_millicores` / `supervisor::duration_codec::parse` /
1401 // `rate_limit_codec::parse` sites: the ASCII byte-scan closes the
1402 // WhatWG-conformant whitespace bytes every downstream YAML / JSON /
1403 // TOML parser can feed through a quoted-scalar value verbatim
1404 // (`0x20`, `0x09`, `0x0A`, `0x0C`, `0x0D`), the non-ASCII
1405 // `char::is_whitespace` scan closes the strictly-complementary
1406 // Unicode `White_Space` class (NBSP `\u{00A0}`, LINE SEPARATOR
1407 // `\u{2028}`, EM-SPACE `\u{2003}`, and the peer typography
1408 // codepoints) that `str::trim` at parse entry silently strips.
1409 // Either drift class would round-trip through `render_duration` to
1410 // a *different* canonical form on next emit — breaking the
1411 // THEORY.md Part V render-determinism contract. Diagnostics stay
1412 // typed at `WhitespaceInDuration` / `NonAsciiWhitespaceInDuration`.
1413 //
1414 // Routed through the lifted [`crate::render::reject_whitespace`]
1415 // primitive — the substrate-side single-owner paired-arm gate every
1416 // typed-magnitude codec in caixa-core shares.
1417 crate::render::reject_whitespace(
1418 s,
1419 |byte| LimitsError::whitespace_in_duration(s, byte),
1420 |ch| LimitsError::non_ascii_whitespace_in_duration(s, ch),
1421 )?;
1422 let s = s.trim();
1423 if s.is_empty() {
1424 return Err(LimitsError::EmptyDuration(s.into()));
1425 }
1426 // Routed through the lifted
1427 // [`crate::render::split_magnitude_and_alpha_unit`] primitive — the
1428 // single-owner split every ASCII-alphabetic-unit typed-magnitude
1429 // codec in caixa-core shares. See its docstring for the full
1430 // sibling roster on the same primitive altitude.
1431 let (num_part, unit) = crate::render::split_magnitude_and_alpha_unit(s);
1432 let num_trim = num_part.trim();
1433 // The canonical authoring form for `:limits :wall-clock` is
1434 // `<integer><unit>` — every magnitude `render_duration` emits is a
1435 // non-negative integer with no decimal point and no leading sign,
1436 // so the parser's accepted set must match for serialize/deserialize
1437 // to round-trip without canonical-form drift. Until this gate
1438 // landed the parser accepted any `f64`-shaped magnitude
1439 // (`"1.5s"` → 1500ms, `"1.0s"` → 1s, `"0.5m"` → 30s, `"+30s"` →
1440 // 30s) and serde silently round-tripped the value to a *different*
1441 // canonical string on the next emit (`"1.5s"` → 1500ms →
1442 // `"1500ms"`, `"1.0s"` → 1s → `"1s"`, `"0.5m"` → 30s → `"30s"`,
1443 // `"+30s"` → 30s → `"30s"`) — breaking the THEORY.md Part V
1444 // render-determinism contract every typed slot carries. The same
1445 // canonical-form discipline `parse_byte_size`'s integer-magnitude
1446 // gate (the immediate predecessor on the peer `:limits :memory`
1447 // codec) applies; this gate is the direct successor on the
1448 // `:limits :wall-clock` codec.
1449 //
1450 // Strict canonical form: every byte of the magnitude is an ASCII
1451 // digit (no `.`, no `+`, no `-`). On current Rust `u64::from_str`
1452 // permissively accepts a leading `+` (`"+30"` → 30) — that's a
1453 // canonical-drift shape `render_duration` never emits, so the
1454 // digit-only check is what closes the leading-sign class; relying
1455 // on `u64::from_str`'s strictness alone would silently admit it.
1456 // On non-digit-only inputs the gate distinguishes "non-canonical-
1457 // but-numeric" (parses as f64 or i64 — surfaced as the new
1458 // `NonIntegerDurationMagnitude` variant with a self-locating
1459 // diagnostic) from "garbage" (parses as neither — surfaced as the
1460 // existing `BadDurationMagnitude` so its narrower diagnostic
1461 // remains load-bearing).
1462 //
1463 // Routed through the lifted
1464 // [`crate::render::is_digit_only_magnitude`] predicate — the same
1465 // source of truth the four peer typed-magnitude codec sites share.
1466 let digit_only = crate::render::is_digit_only_magnitude(num_trim);
1467 if !digit_only {
1468 let numeric = num_trim.parse::<f64>().is_ok() || num_trim.parse::<i64>().is_ok();
1469 if numeric {
1470 return Err(LimitsError::non_integer_duration_magnitude(num_trim));
1471 }
1472 return Err(LimitsError::bad_duration_magnitude(num_part));
1473 }
1474 // Leading-zero arm — peer with the `supervisor::duration_codec`
1475 // leading-zero arm (9178904) and the `rate_limit_codec`
1476 // leading-zero arm (4f46830) on the same canonical-form
1477 // render-determinism axis. The digit-only gate accepts `"030s"`,
1478 // `"00s"`, `"01h"`, `"0500ms"` as `u64::from_str` parses them
1479 // losslessly (= 30, 0, 1, 500), but `render_duration` emits the
1480 // leading-zero-stripped form (`"30s"`, `"0s"`, `"1h"`, `"500ms"`)
1481 // — a *different* canonical string on the next emit, breaking the
1482 // THEORY.md Part V render-determinism contract the same way
1483 // `"+30s"` did before the leading-`+` arm landed. The single-byte
1484 // magnitude `"0"` (or `"0s"` / `"0ms"`) round-trips losslessly
1485 // through `render_duration` (`render_duration(Duration::ZERO)`
1486 // emits `"0s"`) — the downstream semantic-zero gate
1487 // [`LimitsError::WallClockZero`] refuses zero-magnitude authoring
1488 // at the typed-validate layer above, so the single-byte `"0"`
1489 // stays in the accepted set at this codec layer and the
1490 // diagnostic partitioning between canonical-form drift (this arm)
1491 // and semantic-zero (the downstream gate) remains stable. Same
1492 // codec-layer / typed-validate-layer partition the peer codecs
1493 // preserve.
1494 //
1495 // Routed through the lifted
1496 // [`crate::render::is_leading_zero_padded_magnitude`] predicate —
1497 // the same source of truth the four peer typed-magnitude codec
1498 // sites share.
1499 if crate::render::is_leading_zero_padded_magnitude(num_trim) {
1500 return Err(LimitsError::leading_zero_duration_magnitude(num_trim));
1501 }
1502 // The digit-only gate guarantees every byte is `[0-9]`, and the
1503 // leading-zero arm above guarantees the magnitude is either the
1504 // single byte `"0"` or starts with `[1-9]`, so the only way
1505 // `u64::from_str` can fail here is overflow.
1506 let num: u64 = num_trim.parse::<u64>().map_err(|_| {
1507 LimitsError::bad_duration_magnitude(format!(
1508 "{num_trim} (digit-only magnitude overflows u64)"
1509 ))
1510 })?;
1511 // Route the `{"ms" | "s" | "" | "m" | "h"} → Duration` unit-arm
1512 // dispatch through the canonical
1513 // [`crate::render::duration_from_integer_magnitude_and_unit`]
1514 // primitive — the substrate-side single-owner unit-dispatch table
1515 // every typed-duration codec in caixa-core routes through
1516 // (peer: `supervisor::duration_codec::parse` backing the shared
1517 // `:supervisor :restart-window` / `:politicas :timeout` /
1518 // `:politicas :circuit-breaker :window` slots). Every unit
1519 // conversion is integer-exact for an integer magnitude; overflow
1520 // surfaces via the typed `DurationUnitError::Overflow { multiplier }`
1521 // discriminant so this arm reconstructs the pre-lift
1522 // `"…overflows u64 (magnitude × 60 > 2^64-1)"` /
1523 // `"…overflows u64 (magnitude × 3600 > 2^64-1)"` wording verbatim
1524 // from `num_trim` / `unit_trim` / the returned `multiplier`, and
1525 // the unknown-unit arm reconstructs the pre-lift
1526 // `LimitsError::UnknownDurationUnit { unit }` variant from the
1527 // caller-scoped `unit_trim`. Load-bearing pinned by
1528 // `crate::render::tests::duration_from_integer_magnitude_and_unit_matches_pre_lift_unit_dispatch_table`.
1529 let unit_trim = unit.trim();
1530 let dur = crate::render::duration_from_integer_magnitude_and_unit(num, unit_trim).map_err(
1531 |e| match e {
1532 crate::render::DurationUnitError::Overflow { multiplier } => {
1533 LimitsError::bad_duration_magnitude(format!(
1534 "{num_trim}{unit_trim} overflows u64 (magnitude × {multiplier} > 2^64-1)"
1535 ))
1536 }
1537 crate::render::DurationUnitError::UnknownUnit => {
1538 LimitsError::unknown_duration_unit(unit_trim)
1539 }
1540 },
1541 )?;
1542 Ok(dur)
1543}
1544
1545fn ser_duration<S: Serializer>(v: &Option<Duration>, s: S) -> Result<S::Ok, S::Error> {
1546 // Route through the canonical [`crate::render::serialize_option_via_str`]
1547 // — the substrate-side single-owner primitive for the forward arm
1548 // of the typed-magnitude codec family — around the canonical
1549 // [`crate::supervisor::duration_codec::render`] duration-byte
1550 // dispatch. The `render` dispatch is itself the load-bearing
1551 // single-owner primitive for duration bytes across every caixa
1552 // typed-duration surface (`:limits :wall-clock`,
1553 // `:politicas :timeout`, `:circuit-breaker :window`, future OTP
1554 // `gen_server` per-call timeouts); the outer
1555 // `serialize_option_via_str` closes the `Some(_) => serialize_str`
1556 // / `None => serialize_none` `Option`-arm dispatch every peer
1557 // typed-magnitude serializer shares. Load-bearing pinned by
1558 // `tests::ser_duration_routes_through_supervisor_duration_codec_render_canonical`.
1559 crate::render::serialize_option_via_str(v, s, crate::supervisor::duration_codec::render)
1560}
1561
1562fn de_duration<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Duration>, D::Error> {
1563 // Route through the canonical [`crate::render::deserialize_option_via_str`]
1564 // — the substrate-side single-owner primitive for the reverse arm
1565 // of the typed-magnitude codec family. See its docstring for the
1566 // full sibling roster and the compounding rationale that pins this
1567 // lift.
1568 crate::render::deserialize_option_via_str(d, parse_duration)
1569}
1570
1571// ── millicores codec ───────────────────────────────────────────────────
1572
1573fn parse_millicores(s: &str) -> Result<u32, LimitsError> {
1574 // Paired whitespace-rejection arm — same canonical-form
1575 // render-determinism discipline as the peer `parse_byte_size` /
1576 // `parse_duration` / `supervisor::duration_codec::parse` /
1577 // `rate_limit_codec::parse` sites: the ASCII byte-scan closes the
1578 // WhatWG-conformant whitespace bytes (`0x20`, `0x09`, `0x0A`,
1579 // `0x0C`, `0x0D`), the non-ASCII `char::is_whitespace` scan closes
1580 // the strictly-complementary Unicode `White_Space` class (NBSP
1581 // `\u{00A0}`, LINE SEPARATOR `\u{2028}`, EM-SPACE `\u{2003}`, and
1582 // the peer typography codepoints) that `str::trim` at parse entry
1583 // silently strips. Either drift class would round-trip through
1584 // `render_millicores` to a *different* canonical form on next emit
1585 // — breaking the THEORY.md Part V render-determinism contract.
1586 // Diagnostics stay typed at `WhitespaceInMillicores` /
1587 // `NonAsciiWhitespaceInMillicores` — peer with every prior
1588 // canonical-form-drift arm on this codec
1589 // (`NonIntegerMillicoreMagnitude`, `LeadingZeroMillicoreMagnitude`).
1590 //
1591 // Routed through the lifted [`crate::render::reject_whitespace`]
1592 // primitive — the substrate-side single-owner paired-arm gate every
1593 // typed-magnitude codec in caixa-core shares.
1594 crate::render::reject_whitespace(
1595 s,
1596 |byte| LimitsError::whitespace_in_millicores(s, byte),
1597 |ch| LimitsError::non_ascii_whitespace_in_millicores(s, ch),
1598 )?;
1599 let s_trim = s.trim();
1600 if s_trim.is_empty() {
1601 return Err(LimitsError::bad_millicores(s));
1602 }
1603 let (magnitude, has_m_suffix) = match s_trim.strip_suffix('m') {
1604 Some(stripped) => (stripped.trim(), true),
1605 None => (s_trim, false),
1606 };
1607 if magnitude.is_empty() {
1608 // Bare `"m"` (or `" m "`) — no magnitude was authored. The
1609 // canonical millicores authoring form requires a magnitude in
1610 // front of the unit (`"500m"`, not `"m"`). Surface as
1611 // `BadMillicores` so the existing narrower-arm wording stays
1612 // load-bearing for "no recognizable magnitude" inputs.
1613 return Err(LimitsError::bad_millicores(s));
1614 }
1615 // The canonical authoring form for `:limits :cpu` is `<integer>m`
1616 // (Kubernetes millicores) or the bare-core shorthand `<integer>`
1617 // (`"2"` = 2000 millicores). Every magnitude `render_millicores`
1618 // emits is a non-negative integer (`format!("{m}m")`) — no decimal
1619 // point, no leading sign — so the parser's accepted set must match
1620 // for serialize/deserialize to round-trip without canonical-form
1621 // drift. Until this gate landed the parser accepted any
1622 // `u32::from_str`-shaped magnitude (`"+500m"` → 500, `"+2"` →
1623 // 2000) and serde silently round-tripped the value to a *different*
1624 // canonical string on the next emit (`"+500m"` → `"500m"`, `"+2"`
1625 // → `"2000m"`) — breaking the THEORY.md Part V render-determinism
1626 // contract every typed slot carries. Closes the sixth (and last)
1627 // typed-codec surface in caixa-core on the integer-magnitude
1628 // canonical-form axis, peer with the five duration / byte-size /
1629 // rate-limit codecs the prior trajectory (1c55a2a / 818dd38 /
1630 // d1fd67b / f479c41 / d53c922) covered.
1631 //
1632 // Strict canonical form: every byte of the magnitude is an ASCII
1633 // digit (no `.`, no `+`, no `-`). On current Rust `u32::from_str`
1634 // permissively accepts a leading `+` (`"+500"` → 500) — that's a
1635 // canonical-drift shape `render_millicores` never emits, so the
1636 // digit-only check is what closes the leading-sign class; relying
1637 // on `u32::from_str`'s strictness alone would silently admit it.
1638 // On non-digit-only inputs the gate distinguishes "non-canonical-
1639 // but-numeric" (parses as f64 or i64 — surfaced as the new
1640 // `NonIntegerMillicoreMagnitude` variant naming the offending
1641 // magnitude verbatim with the canonical-form remediation) from
1642 // "garbage" (parses as neither — surfaced as the existing
1643 // `BadMillicores` so its narrower diagnostic shape remains
1644 // load-bearing for the not-a-numeric-input class).
1645 //
1646 // Routed through the lifted
1647 // [`crate::render::is_digit_only_magnitude`] predicate — the same
1648 // source of truth the four peer typed-magnitude codec sites share.
1649 // The predicate carries a `!<var>.is_empty()` gate that is
1650 // strictly no-op here (the `magnitude.is_empty()` arm above
1651 // already surfaces an empty magnitude as
1652 // [`LimitsError::BadMillicores`] before this line is reached), so
1653 // the semantics are preserved verbatim: on every reachable input
1654 // the predicate returns `magnitude.bytes().all(|b|
1655 // b.is_ascii_digit())`, byte-for-byte what the removed inline
1656 // expression computed.
1657 let digit_only = crate::render::is_digit_only_magnitude(magnitude);
1658 if !digit_only {
1659 let numeric = magnitude.parse::<f64>().is_ok() || magnitude.parse::<i64>().is_ok();
1660 if numeric {
1661 return Err(LimitsError::non_integer_millicore_magnitude(magnitude));
1662 }
1663 return Err(LimitsError::bad_millicores(s));
1664 }
1665 // Leading-zero arm — peer with the `parse_byte_size` leading-zero
1666 // arm (cea9a78), the `parse_duration` leading-zero arm (39762d7),
1667 // the `supervisor::duration_codec` leading-zero arm (9178904) and
1668 // the `rate_limit_codec` leading-zero arm (4f46830) on the same
1669 // canonical-form render-determinism axis. The digit-only gate
1670 // accepts `"0500m"`, `"00m"`, `"02"`, `"01500m"` as `u32::from_str`
1671 // parses them losslessly (= 500, 0, 2, 1500), but `render_millicores`
1672 // emits the leading-zero-stripped form (`"500m"`, `"0m"`, `"2000m"`,
1673 // `"1500m"`) — a *different* canonical string on the next emit,
1674 // breaking the THEORY.md Part V render-determinism contract the
1675 // same way `"+500m"` did before the leading-`+` arm landed. The
1676 // single-byte magnitude `"0"` (or `"0m"`) round-trips losslessly
1677 // through `render_millicores` (`render_millicores(0)` emits `"0m"`)
1678 // — the downstream semantic-zero gate [`LimitsError::CpuZero`]
1679 // refuses zero-magnitude authoring at the typed-validate layer
1680 // above, so the single-byte `"0"` stays in the accepted set at this
1681 // codec layer and the diagnostic partitioning between canonical-
1682 // form drift (this arm) and semantic-zero (the downstream gate)
1683 // remains stable. Same codec-layer / typed-validate-layer partition
1684 // the peer codecs preserve. Closes the sixth (and last) typed
1685 // numeric-codec surface in caixa-core on the integer-magnitude
1686 // leading-zero axis — the trajectory the prior `parse_byte_size`
1687 // arm (cea9a78) explicitly named.
1688 //
1689 // Routed through the lifted
1690 // [`crate::render::is_leading_zero_padded_magnitude`] predicate —
1691 // the same source of truth the four peer typed-magnitude codec
1692 // sites share.
1693 if crate::render::is_leading_zero_padded_magnitude(magnitude) {
1694 return Err(LimitsError::leading_zero_millicore_magnitude(magnitude));
1695 }
1696 // The digit-only gate guarantees every byte is `[0-9]`, and the
1697 // leading-zero arm above guarantees the magnitude is either the
1698 // single byte `"0"` or starts with `[1-9]`, so the only way
1699 // `u32::from_str` can fail here is overflow (the magnitude exceeds
1700 // `u32::MAX`). Surface that as `BadMillicores` with an overflow-
1701 // shaped wording so the diagnostic names the offending magnitude
1702 // verbatim rather than collapsing onto the non-canonical arm —
1703 // matches `parse_byte_size` / `parse_duration` / `rate_limit_codec`
1704 // overflow-arm shape on the peer typed codecs.
1705 let num: u32 = magnitude.parse::<u32>().map_err(|_| {
1706 LimitsError::bad_millicores(format!("{magnitude} (digit-only magnitude overflows u32)"))
1707 })?;
1708 if has_m_suffix {
1709 Ok(num)
1710 } else {
1711 // Bare-core shorthand: `"2"` = 2000 millicores. Use
1712 // `checked_mul` (not the prior `saturating_mul`) so a
1713 // magnitude that overflows u32 on the × 1000 conversion
1714 // surfaces a parser-shaped diagnostic at parse time rather
1715 // than silently saturating to `u32::MAX` (which would land
1716 // as the cap value far from the author's intent and bypass
1717 // any future validate-time upper-bound gate the `:cpu` axis
1718 // grows). Matches `parse_byte_size`'s overflow-arm shape on
1719 // the magnitude × unit multiply.
1720 num.checked_mul(1000).ok_or_else(|| {
1721 LimitsError::bad_millicores(format!(
1722 "{magnitude} cores × 1000 overflows u32 (write the value in millicores: max \"{}m\")",
1723 u32::MAX
1724 ))
1725 })
1726 }
1727}
1728
1729fn render_millicores(m: u32) -> String {
1730 format!("{m}m")
1731}
1732
1733fn ser_millicores<S: Serializer>(v: &Option<u32>, s: S) -> Result<S::Ok, S::Error> {
1734 // Route through the canonical [`crate::render::serialize_option_via_str`]
1735 // — see peer `ser_byte_size` / `ser_duration` routing notes above.
1736 crate::render::serialize_option_via_str(v, s, render_millicores)
1737}
1738
1739fn de_millicores<'de, D: Deserializer<'de>>(d: D) -> Result<Option<u32>, D::Error> {
1740 // Route through the canonical [`crate::render::deserialize_option_via_str`]
1741 // — see peer `de_byte_size` / `de_duration` routing notes above.
1742 crate::render::deserialize_option_via_str(d, parse_millicores)
1743}
1744
1745// Fold the six `LimitsError::{NonInteger,LeadingZero}<Kind>Magnitude
1746// { value: <val>.into() }` wire-up sites on the three typed-magnitude
1747// codec surfaces (`parse_byte_size` / `parse_duration` /
1748// `parse_millicores`) onto one substrate-primitive family per typed
1749// variant — the paired `{ value: String }` single-slot family on
1750// [`LimitsError`]. First fold family on [`LimitsError`], peer of the
1751// four `LayoutError` ctor macro families (`layout_violation_ctors!`
1752// 131ca0d — 16 `{ caixa, issue }` variants; `layout_slot_kind_ctors!`
1753// 0419438 — 4 `{ caixa, kind, slots }` variants;
1754// `LayoutError::missing_entry` 1b09f9d — 1 `{ kind, path }` variant;
1755// `layout_nome_only_ctors!` 3fe3dd7 — 6 `<Variant>(String)` variants)
1756// on the sibling layout-side envelopes, and of the four `AplicacaoError`
1757// ctor macro families (`aplicacao_field_reason_ctors!` 981060b — 7
1758// `{ <field>, reason }` variants; `contrato_target_ctors!` 14b81d5 — 2
1759// `{ de, para, wit, expected }` variants; `contrato_empty_pair_ctors!`
1760// 8580068 — 4 `{ de, para }` variants; `contrato_pair_value_reason_ctors!`
1761// 14e13f1 — 3 `{ de, para, <field>, reason }` variants) on the sibling
1762// mesh-side envelopes.
1763//
1764// Every one of the six wire-up sites — the `NonInteger` / `LeadingZero`
1765// arms inside [`parse_byte_size`], [`parse_duration`], and
1766// [`parse_millicores`] — opened the identical three-line
1767// `return Err(LimitsError::<Variant> { value: <val>.into() });` block
1768// against the per-codec local magnitude binding (`num_trim` on the two
1769// alpha-unit codecs, `magnitude` on the millicores codec) — the exact
1770// "same block re-inlined at every consumer" shape the PRIME DIRECTIVE
1771// names as a bug, on the same altitude the peer four `LayoutError` and
1772// four `AplicacaoError` constructor families each closed on their
1773// sibling envelopes.
1774//
1775// The macro below generates one `#[must_use]` inherent constructor per
1776// variant of shape `fn <ctor>(value: &str) -> LimitsError`, collapsing
1777// the six sites onto one dispatch per arm:
1778// `return Err(LimitsError::<ctor>(<val>));`, byte-equal to the pre-lift
1779// struct-literal on the same `value` argument. The uniform single-field
1780// construction (`value: value.to_string()`) is spelled once — inside the
1781// macro — rather than at every wire-up site. `#[must_use]` fires a
1782// compile warning at any wire-up that mistakenly discards the
1783// constructed error.
1784//
1785// Every future consumer that wants to construct one of these six
1786// variants outside the three current codec surfaces (a deferred
1787// `feira lint --canonical-magnitudes` per-caixa admission verb probing
1788// each authored `:memory` / `:wall-clock` / `:cpu` value against the
1789// same canonical-form gate, an M4 typed `mesh.pleme.io/v1alpha1/Servico`
1790// CR materializer's per-`:limits` admission validators, a per-
1791// `computeunit.yaml` value-shape pre-emitter probing each declared
1792// magnitude ahead of the operator's admit-cycle) reaches the variant
1793// through one call rather than re-inlining the three-line struct-literal
1794// in lockstep with the pre-existing six sites.
1795macro_rules! limits_codec_value_only_ctors {
1796 ($($ctor:ident => $variant:ident),* $(,)?) => {
1797 impl LimitsError {
1798 $(
1799 #[doc = concat!(
1800 "Construct a [`LimitsError::",
1801 stringify!($variant),
1802 "`] naming the offending magnitude `value`. Folds the ",
1803 "uniform `{ value: value.to_string() }` single-slot ",
1804 "construction onto one substrate primitive so every ",
1805 "wire-up on this variant reads through one dispatch ",
1806 "rather than the pre-lift three-line struct-literal ",
1807 "block."
1808 )]
1809 #[must_use]
1810 pub fn $ctor(value: &str) -> Self {
1811 Self::$variant { value: value.to_string() }
1812 }
1813 )*
1814 }
1815 };
1816}
1817
1818limits_codec_value_only_ctors! {
1819 non_integer_byte_magnitude => NonIntegerByteMagnitude,
1820 leading_zero_byte_magnitude => LeadingZeroByteMagnitude,
1821 non_integer_duration_magnitude => NonIntegerDurationMagnitude,
1822 leading_zero_duration_magnitude => LeadingZeroDurationMagnitude,
1823 non_integer_millicore_magnitude => NonIntegerMillicoreMagnitude,
1824 leading_zero_millicore_magnitude => LeadingZeroMillicoreMagnitude,
1825}
1826
1827// Fold the two `LimitsError::Unknown<Kind>Unit { unit: <val>.into() }`
1828// wire-up sites on the two alpha-unit typed-magnitude codec surfaces
1829// (`parse_byte_size` at the `KB | MB | GB | KiB | MiB | GiB | "" | B`
1830// unit-dispatch table's fallthrough arm; `parse_duration` at the
1831// `crate::render::DurationUnitError::UnknownUnit` reverse-map arm of the
1832// `ms | s | "" | m | h` unit-dispatch table) onto one substrate-primitive
1833// family per typed variant — the paired `{ unit: String }` single-slot
1834// family on [`LimitsError`]. Direct peer of the sibling
1835// [`limits_codec_value_only_ctors!`] single-slot family on the same
1836// [`LimitsError`] envelope (6 variants on the `{ value: String }` axis
1837// of the codec surface) and of the peer [`limits_codec_value_byte_ctors!`]
1838// / [`limits_codec_value_char_ctors!`] families on the wider two-slot /
1839// three-slot whitespace-class axes of the same three codec surfaces.
1840//
1841// Every one of the two wire-up sites — the fallthrough of
1842// [`parse_byte_size`]'s unit-dispatch `match` on the caller-scoped
1843// `other: &str` binding; the [`crate::render::DurationUnitError::UnknownUnit`]
1844// reverse-map arm of [`parse_duration`]'s codec-scoped `unit_trim: &str`
1845// binding — opened the identical two-line
1846// `LimitsError::Unknown<Kind>Unit { unit: <val>.into() }` block against
1847// the codec-scoped unit binding — the exact "same block re-inlined at
1848// every consumer" shape the PRIME DIRECTIVE names as a bug, on the same
1849// altitude the peer [`limits_codec_value_only_ctors!`] family closed on
1850// the sibling `{ value: String }` axis of the same codec surface.
1851//
1852// The macro below generates one `#[must_use]` inherent constructor per
1853// variant of shape `fn <ctor>(unit: &str) -> LimitsError`, collapsing
1854// the two sites onto one dispatch per arm: `LimitsError::<ctor>(<val>)`,
1855// byte-equal to the pre-lift struct-literal on the same `unit` argument.
1856// The uniform single-field construction (`unit: unit.to_string()`) is
1857// spelled once — inside the macro — rather than at every wire-up site.
1858// `#[must_use]` fires a compile warning at any wire-up that mistakenly
1859// discards the constructed error.
1860//
1861// Every future consumer that wants to construct one of these two
1862// variants outside the two current codec surfaces (a deferred
1863// `feira lint --canonical-units` per-caixa admission verb probing each
1864// authored `:memory` / `:wall-clock` value against the same
1865// unit-dispatch table, an M4 typed `mesh.pleme.io/v1alpha1/Servico` CR
1866// materializer's per-`:limits` admission validators pre-checking a
1867// per-slot unit alphabet against a cluster-local snapshot, a future
1868// unit-alphabet widening on either codec that shares the same
1869// unknown-unit fallthrough shape) now reaches each variant through one
1870// call rather than re-inlining the two-line struct-literal in lockstep
1871// with the pre-existing two sites.
1872macro_rules! limits_codec_unit_only_ctors {
1873 ($($ctor:ident => $variant:ident),* $(,)?) => {
1874 impl LimitsError {
1875 $(
1876 #[doc = concat!(
1877 "Construct a [`LimitsError::",
1878 stringify!($variant),
1879 "`] naming the offending magnitude `unit`. Folds the ",
1880 "uniform `{ unit: unit.to_string() }` single-slot ",
1881 "construction onto one substrate primitive so every ",
1882 "wire-up on this variant reads through one dispatch ",
1883 "rather than the pre-lift two-line struct-literal ",
1884 "block."
1885 )]
1886 #[must_use]
1887 pub fn $ctor(unit: &str) -> Self {
1888 Self::$variant { unit: unit.to_string() }
1889 }
1890 )*
1891 }
1892 };
1893}
1894
1895limits_codec_unit_only_ctors! {
1896 unknown_byte_unit => UnknownByteUnit,
1897 unknown_duration_unit => UnknownDurationUnit,
1898}
1899
1900// Fold the three `LimitsError::WhitespaceIn<Kind> { value: <val>.into(),
1901// byte }` wire-up sites on the three typed-magnitude codec surfaces
1902// (`parse_byte_size` / `parse_duration` / `parse_millicores`) onto one
1903// substrate-primitive family per typed variant — the paired
1904// `{ value: String, byte: u8 }` two-slot family on [`LimitsError`].
1905// Sibling of the peer [`limits_codec_value_only_ctors!`] single-slot
1906// family on the same three codec surfaces, and of the peer
1907// [`limits_codec_value_char_ctors!`] three-slot family on the
1908// strictly-complementary non-ASCII whitespace class.
1909//
1910// Every one of the three wire-up sites — the ASCII-whitespace-rejection
1911// arm of the paired [`crate::render::reject_whitespace`] closure at
1912// each codec — opened the identical four-line
1913// `|byte| LimitsError::WhitespaceIn<Kind> { value: <s>.into(), byte }`
1914// block against the codec-scoped `<s>: &str` binding.
1915//
1916// The macro below generates one `#[must_use]` inherent constructor per
1917// variant of shape `fn <ctor>(value: &str, byte: u8) -> LimitsError`,
1918// collapsing the three sites onto one dispatch per arm:
1919// `|byte| LimitsError::<ctor>(s, byte)`, byte-equal to the pre-lift
1920// struct-literal on the same `(value, byte)` pair. The uniform two-field
1921// construction (`value: value.to_string()`, `byte`) is spelled once —
1922// inside the macro — rather than at every wire-up site.
1923macro_rules! limits_codec_value_byte_ctors {
1924 ($($ctor:ident => $variant:ident),* $(,)?) => {
1925 impl LimitsError {
1926 $(
1927 #[doc = concat!(
1928 "Construct a [`LimitsError::",
1929 stringify!($variant),
1930 "`] naming the offending magnitude `value` and the ",
1931 "raw ASCII-whitespace `byte` that fell inside it. ",
1932 "Folds the uniform `{ value: value.to_string(), byte }` ",
1933 "two-slot construction onto one substrate primitive so ",
1934 "every wire-up on this variant reads through one dispatch ",
1935 "rather than the pre-lift four-line struct-literal block."
1936 )]
1937 #[must_use]
1938 pub fn $ctor(value: &str, byte: u8) -> Self {
1939 Self::$variant { value: value.to_string(), byte }
1940 }
1941 )*
1942 }
1943 };
1944}
1945
1946limits_codec_value_byte_ctors! {
1947 whitespace_in_byte_size => WhitespaceInByteSize,
1948 whitespace_in_duration => WhitespaceInDuration,
1949 whitespace_in_millicores => WhitespaceInMillicores,
1950}
1951
1952// Fold the three `LimitsError::NonAsciiWhitespaceIn<Kind>
1953// { value: <val>.into(), ch, codepoint: ch as u32 }` wire-up sites on
1954// the three typed-magnitude codec surfaces (`parse_byte_size` /
1955// `parse_duration` / `parse_millicores`) onto one substrate-primitive
1956// family per typed variant — the paired `{ value: String, ch: char,
1957// codepoint: u32 }` three-slot family on [`LimitsError`]. Sibling of
1958// the peer [`limits_codec_value_only_ctors!`] single-slot family on the
1959// same three codec surfaces, and of the peer
1960// [`limits_codec_value_byte_ctors!`] two-slot family on the strictly-
1961// complementary ASCII whitespace class.
1962//
1963// Every one of the three wire-up sites — the Unicode-`White_Space`-
1964// rejection arm of the paired [`crate::render::reject_whitespace`]
1965// closure at each codec — opened the identical five-line
1966// `|ch| LimitsError::NonAsciiWhitespaceIn<Kind> { value: <s>.into(),
1967// ch, codepoint: ch as u32 }` block against the codec-scoped
1968// `<s>: &str` binding, with the load-bearing `codepoint: ch as u32`
1969// derivation open-coded at every wire-up. The macro pulls the
1970// derivation inside the ctor body so every wire-up now reads
1971// `|ch| LimitsError::<ctor>(s, ch)` and every future consumer of the
1972// variant is guaranteed to carry the derivation through one canonical
1973// path rather than re-open-coding it in lockstep with the pre-existing
1974// three sites.
1975//
1976// The macro below generates one `#[must_use]` inherent constructor per
1977// variant of shape `fn <ctor>(value: &str, ch: char) -> LimitsError`,
1978// collapsing the three sites onto one dispatch per arm:
1979// `|ch| LimitsError::<ctor>(s, ch)`, byte-equal to the pre-lift
1980// struct-literal on the same `(value, ch, ch as u32)` triple.
1981macro_rules! limits_codec_value_char_ctors {
1982 ($($ctor:ident => $variant:ident),* $(,)?) => {
1983 impl LimitsError {
1984 $(
1985 #[doc = concat!(
1986 "Construct a [`LimitsError::",
1987 stringify!($variant),
1988 "`] naming the offending magnitude `value` and the ",
1989 "non-ASCII Unicode whitespace `ch` that fell inside it. ",
1990 "Folds the uniform `{ value: value.to_string(), ch, ",
1991 "codepoint: ch as u32 }` three-slot construction onto ",
1992 "one substrate primitive so every wire-up on this ",
1993 "variant reads through one dispatch rather than the ",
1994 "pre-lift five-line struct-literal block. The load-",
1995 "bearing `codepoint = ch as u32` derivation is pulled ",
1996 "inside the ctor body so every future consumer of the ",
1997 "variant carries it through one canonical path."
1998 )]
1999 #[must_use]
2000 pub fn $ctor(value: &str, ch: char) -> Self {
2001 Self::$variant {
2002 value: value.to_string(),
2003 ch,
2004 codepoint: ch as u32,
2005 }
2006 }
2007 )*
2008 }
2009 };
2010}
2011
2012limits_codec_value_char_ctors! {
2013 non_ascii_whitespace_in_byte_size => NonAsciiWhitespaceInByteSize,
2014 non_ascii_whitespace_in_duration => NonAsciiWhitespaceInDuration,
2015 non_ascii_whitespace_in_millicores => NonAsciiWhitespaceInMillicores,
2016}
2017
2018// Fold the seven `LimitsError::<Variant> { <field>: <Copy> }` one-field
2019// `Copy`-scalar struct-variant wire-up sites at [`LimitsSpec::validate`]'s
2020// four typed-axis bracket cascades — three closure-slots at the
2021// [`crate::render::require_positive_quantum_multiple_bounded_u64`] `:memory`
2022// axis (`MemoryBelowWasm32Page { bytes }`, `MemoryExceedsWasm32Cap { bytes }`,
2023// `MemoryNotPageMultiple { bytes }`), one at the
2024// [`crate::render::require_positive_bounded_u64`] `:fuel` axis
2025// (`FuelExceedsCap { fuel }`), two at the
2026// [`crate::render::require_positive_canonical_bounded_duration`]
2027// `:wall-clock` axis (`WallClockNotCanonical { wall_clock }`,
2028// `WallClockExceedsCap { wall_clock }`), and one at the
2029// [`crate::render::require_positive_bounded_u32`] `:cpu` axis
2030// (`CpuExceedsCap { millicores }`) — onto one substrate primitive per typed
2031// variant, matching the sibling
2032// [`crate::supervisor::supervisor_scalar_ctors!`] macro (f0f77a2, 4 variants
2033// on the same `{ <field>: RestartStrategy | u32 | Duration }` shape) and the
2034// peer [`crate::aplicacao::aplicacao_policy_scalar_ctors!`] macro (7ef425e,
2035// 8 variants on the same `{ <field>: Duration | u32 }` shape) at that
2036// discipline on the sibling `SupervisorError` per-`:supervisor` scalar axis
2037// and the peer `AplicacaoError` per-`:politicas` scalar axis. Every variant
2038// is a one-field `Copy`-pass-through struct-literal — `u64 | u32 |
2039// Duration` — so the fold routes each wire-up site through one dispatch per
2040// typed variant without a runtime-work delta. Last unlifted per-`:limits`
2041// scalar `LimitsError` variant family folded onto a substrate primitive;
2042// every M2 `LimitsSpec::validate` per-axis bracket-closure slot now reaches
2043// for a bare-function-pointer `LimitsError::<ctor>` in place of the pre-lift
2044// open-coded `|<field>| LimitsError::<Variant> { <field> }` one-line
2045// closure over the same one-field struct-literal.
2046//
2047// Each of the seven wire-up sites opened the identical
2048// `|<field>| LimitsError::<Variant> { <field> }` bracket-closure — the exact
2049// "same block re-inlined at every consumer" shape the PRIME DIRECTIVE names
2050// as a bug, on the same altitude the peer `supervisor_scalar_ctors!` /
2051// `aplicacao_policy_scalar_ctors!` folds each closed on the sibling
2052// `SupervisorError` / `AplicacaoError` envelopes' per-axis cap /
2053// canonical-form / below-quantum arms. The seven variants share one
2054// `{ <field>: <Copy> }` shape, so the fold routes each wire-up site through
2055// one dispatch per typed variant.
2056//
2057// The macro below generates one static constructor per variant of shape
2058// `const fn <ctor>(<field>: <ty>) -> LimitsError`, so every wire-up site
2059// collapses onto one dispatch: `LimitsError::<ctor>(<val>)`, byte-equal to
2060// the pre-lift struct-literal on the same `Copy`-`<ty>` fixture — as a bare
2061// function pointer in the `impl FnOnce(<ty>) -> LimitsError` bracket-
2062// closure slot every [`crate::render::require_positive_bounded_u32`] /
2063// [`crate::render::require_positive_bounded_u64`] /
2064// [`crate::render::require_positive_canonical_bounded_duration`] /
2065// [`crate::render::require_positive_quantum_multiple_bounded_u64`] gate
2066// carries — rather than the pre-lift open-coded one-line closure over the
2067// same one-field struct-literal. `const fn` preserves the `Copy`-pass-
2068// through's zero-runtime-work property verbatim. Every constructor is
2069// `#[must_use]` so a caller who mistakenly discards the constructed error
2070// trips a compile warning at the wire-up site.
2071//
2072// Every future consumer that wants to construct one of these seven variants
2073// outside `LimitsSpec::validate` — a deferred
2074// `mesh.pleme.io/v1alpha1/Servico` CR materializer's admission webhook
2075// re-checking one edited `:memory` / `:fuel` / `:wall-clock` / `:cpu` slot
2076// against the below-quantum + cap + canonical-form cascade, a future
2077// `feira validate --limits` per-caixa admission verb re-running the shape
2078// gates on demand, a per-Servico overlay resolver rejecting an author-
2079// supplied slot against a cluster-local snapshot — now reaches each variant
2080// through one call rather than re-inlining the per-shape struct-literal
2081// block in lockstep with the seven in-crate wire-up sites.
2082macro_rules! limits_scalar_ctors {
2083 ($($ctor:ident => $variant:ident { $field:ident: $ty:ty }),* $(,)?) => {
2084 impl LimitsError {
2085 $(
2086 #[doc = concat!(
2087 "Construct a [`LimitsError::",
2088 stringify!($variant),
2089 "`] naming the offending per-`:limits` `",
2090 stringify!($field),
2091 "` scalar. Folds the uniform `Self::",
2092 stringify!($variant),
2093 " { ",
2094 stringify!($field),
2095 " }` one-field `Copy`-pass-through struct-literal onto ",
2096 "one substrate primitive so every per-axis wire-up on ",
2097 "this variant reads through one dispatch — as a bare ",
2098 "function pointer in the `impl FnOnce(",
2099 stringify!($ty),
2100 ") -> LimitsError` bracket-closure slot every ",
2101 "`crate::render::require_positive_bounded_*` / ",
2102 "`crate::render::require_positive_canonical_bounded_*` / ",
2103 "`crate::render::require_positive_quantum_multiple_bounded_*` ",
2104 "gate carries — rather than the pre-lift open-coded ",
2105 "one-line closure over the same one-field struct-literal. ",
2106 "`const fn` preserves the `Copy`-pass-through's ",
2107 "zero-runtime-work property verbatim."
2108 )]
2109 #[must_use]
2110 pub const fn $ctor($field: $ty) -> Self {
2111 Self::$variant { $field }
2112 }
2113 )*
2114 }
2115 };
2116}
2117
2118limits_scalar_ctors! {
2119 memory_below_wasm32_page => MemoryBelowWasm32Page { bytes: u64 },
2120 memory_exceeds_wasm32_cap => MemoryExceedsWasm32Cap { bytes: u64 },
2121 memory_not_page_multiple => MemoryNotPageMultiple { bytes: u64 },
2122 fuel_exceeds_cap => FuelExceedsCap { fuel: u64 },
2123 wall_clock_not_canonical => WallClockNotCanonical { wall_clock: Duration },
2124 wall_clock_exceeds_cap => WallClockExceedsCap { wall_clock: Duration },
2125 cpu_exceeds_cap => CpuExceedsCap { millicores: u32 },
2126}
2127
2128// Fold the five `LimitsError::BadMillicores(<into-String-expr>)` wire-up
2129// sites on the [`parse_millicores`] codec surface onto one substrate
2130// primitive per typed variant — the paired `(String)` single-slot
2131// tuple-newtype [`LimitsError::BadMillicores`] on the millicores codec
2132// surface. Peer of the sibling [`limits_codec_value_only_ctors!`] /
2133// [`limits_codec_unit_only_ctors!`] / [`limits_codec_value_byte_ctors!`]
2134// / [`limits_codec_value_char_ctors!`] families on the same
2135// [`LimitsError`] envelope (the paired `{ value: String }` /
2136// `{ unit: String }` / `{ value: String, byte: u8 }` /
2137// `{ value: String, ch: char, codepoint: u32 }` struct-shaped families
2138// on the same codec surface) and of the peer [`limits_scalar_ctors!`]
2139// family on the wider `Copy`-`{ <field>: <ty> }` typed-scalar axis of
2140// the same [`LimitsError`] envelope. Closes the widest un-lifted variant
2141// on [`LimitsError`] — every one of the five wire-up sites opened the
2142// identical `LimitsError::BadMillicores(<into-String-expr>)` block
2143// against the codec-scoped `&str` (`s`) or `String` (`format!(...)`)
2144// binding, so the fold routes each site through one dispatch on a
2145// uniform `impl Into<String>` param, byte-equal to the pre-lift tuple-
2146// newtype construction on the same argument. The `impl Into<String>`
2147// bound covers both wire-up shapes — the three `s.into()` `&str` sites
2148// (empty-`:cpu`, bare-`m`-magnitude fallthrough, non-digit-only garbage
2149// fallthrough) and the two `format!(...)` `String` sites (digit-only
2150// magnitude overflows u32, bare-core-shorthand × 1000 overflow) —
2151// without forcing either caller to spell the conversion at the wire-up
2152// site. `#[must_use]` fires a compile warning at any wire-up that
2153// mistakenly discards the constructed error.
2154//
2155// Every future consumer that wants to construct this variant outside
2156// [`parse_millicores`] (a deferred `feira lint --canonical-magnitudes`
2157// per-caixa admission verb probing each authored `:cpu` value against
2158// the same canonical-form gate, an M4 typed
2159// `mesh.pleme.io/v1alpha1/Servico` CR materializer's per-`:limits`
2160// admission validator re-checking one edited `:cpu` slot against the
2161// codec's parser floor, a per-`computeunit.yaml` value-shape pre-emitter
2162// probing each declared millicores magnitude ahead of the operator's
2163// admit-cycle) now reaches the variant through one call rather than
2164// re-inlining the tuple-newtype block in lockstep with the pre-existing
2165// five sites — same discipline the peer per-variant lifts on
2166// [`AplicacaoError`] / [`SupervisorError`] / [`UpgradeError`] /
2167// [`LayoutError`] / [`DepError`] / [`ManifestError`] have converged
2168// through the "one substrate primitive per emit-site variant" ratchet.
2169impl LimitsError {
2170 /// Construct a [`LimitsError::BadMillicores`] carrying the offending
2171 /// millicores authoring string `value` verbatim in the variant's
2172 /// tuple-newtype payload. Folds the uniform
2173 /// `Self::BadMillicores(value.into())` tuple-newtype construction
2174 /// onto one substrate primitive so every wire-up on the variant
2175 /// reads through one dispatch rather than the pre-lift open-coded
2176 /// `LimitsError::BadMillicores(<into-String-expr>)` block. The
2177 /// `impl Into<String>` bound covers both wire-up shapes on
2178 /// [`parse_millicores`] — a `&str` binding (`s.into()`) and a
2179 /// `String` binding (`format!(...)`) — without forcing the caller
2180 /// to spell the conversion at the wire-up site.
2181 #[must_use]
2182 pub fn bad_millicores(value: impl Into<String>) -> Self {
2183 Self::BadMillicores(value.into())
2184 }
2185}
2186
2187// Fold the three `LimitsError::BadByteMagnitude(<into-String-expr>)`
2188// wire-up sites on the [`parse_byte_size`] codec surface onto one
2189// substrate primitive — the paired `(String)` single-slot tuple-newtype
2190// [`LimitsError::BadByteMagnitude`] on the byte-size codec surface, the
2191// direct sibling to the [`LimitsError::bad_millicores`] fold above on
2192// the peer [`parse_millicores`] codec surface (da7602f). Same
2193// discipline the peer per-variant lifts on [`AplicacaoError`] /
2194// [`SupervisorError`] / [`UpgradeError`] / [`LayoutError`] /
2195// [`DepError`] / [`ManifestError`] have converged through the
2196// "one substrate primitive per emit-site variant" ratchet: the three
2197// wire-up sites open the identical
2198// `LimitsError::BadByteMagnitude(<into-String-expr>)` block against
2199// the codec-scoped `&str` (`num_part.into()` — non-digit-only garbage
2200// fallthrough after the numeric-shape gate) or `String`
2201// (`format!(...)` — digit-only magnitude overflows u64, magnitude ×
2202// unit overflows u64) binding, so the fold routes each site through
2203// one dispatch on a uniform `impl Into<String>` param, byte-equal to
2204// the pre-lift tuple-newtype construction on the same argument.
2205//
2206// Every future consumer that wants to construct this variant outside
2207// [`parse_byte_size`] (a deferred `feira lint --canonical-magnitudes`
2208// per-caixa admission verb probing each authored `:memory` value
2209// against the same canonical-form gate, an M4 typed
2210// `mesh.pleme.io/v1alpha1/Servico` CR materializer's per-`:limits`
2211// admission validator re-checking one edited `:memory` slot against
2212// the codec's parser floor, a per-`computeunit.yaml` value-shape pre-
2213// emitter probing each declared byte-size magnitude ahead of the
2214// operator's admit-cycle) now reaches the variant through one call
2215// rather than re-inlining the tuple-newtype block in lockstep with
2216// the pre-existing three sites.
2217impl LimitsError {
2218 /// Construct a [`LimitsError::BadByteMagnitude`] carrying the
2219 /// offending byte-size authoring string `value` verbatim in the
2220 /// variant's tuple-newtype payload. Folds the uniform
2221 /// `Self::BadByteMagnitude(value.into())` tuple-newtype
2222 /// construction onto one substrate primitive so every wire-up on
2223 /// the variant reads through one dispatch rather than the pre-lift
2224 /// open-coded `LimitsError::BadByteMagnitude(<into-String-expr>)`
2225 /// block. The `impl Into<String>` bound covers both wire-up shapes
2226 /// on [`parse_byte_size`] — a `&str` binding (`num_part.into()`)
2227 /// and a `String` binding (`format!(...)`) — without forcing the
2228 /// caller to spell the conversion at the wire-up site. Direct
2229 /// sibling to [`LimitsError::bad_millicores`] on the peer
2230 /// [`parse_millicores`] codec surface.
2231 #[must_use]
2232 pub fn bad_byte_magnitude(value: impl Into<String>) -> Self {
2233 Self::BadByteMagnitude(value.into())
2234 }
2235}
2236
2237// Fold the sole `LimitsError::EmptyByteSize(<into-String-expr>)` wire-up
2238// site on the [`parse_byte_size`] codec surface onto one substrate
2239// primitive — the paired `(String)` single-slot tuple-newtype
2240// [`LimitsError::EmptyByteSize`] on the byte-size codec surface, the
2241// peer to the sibling [`LimitsError::bad_byte_magnitude`] fold above on
2242// the same [`parse_byte_size`] codec surface (837babc) but on the
2243// empty-shape axis rather than the bad-magnitude axis of the same
2244// `(String)` tuple-newtype codec-magnitude family. Same discipline the
2245// peer per-variant lifts on [`AplicacaoError`] / [`SupervisorError`] /
2246// [`UpgradeError`] / [`LayoutError`] / [`DepError`] / [`ManifestError`]
2247// have converged through the "one substrate primitive per emit-site
2248// variant" ratchet: the sole wire-up site opens the identical
2249// `LimitsError::EmptyByteSize(<into-String-expr>)` block against the
2250// codec-scoped `&str` (`s.into()`) binding after the outer `s.trim()` /
2251// `is_empty()` gate on the codec entry surface, so the fold routes the
2252// site through one dispatch on a uniform `impl Into<String>` param,
2253// byte-equal to the pre-lift tuple-newtype construction on the same
2254// argument. The `impl Into<String>` bound covers the pre-lift `&str`
2255// binding without forcing the caller to spell the `.into()` conversion
2256// at the wire-up site — same shape the peer [`LimitsError::bad_millicores`]
2257// / [`LimitsError::bad_byte_magnitude`] / [`LimitsError::bad_duration_magnitude`]
2258// folds carry on the peer bad-magnitude axis of the same paired codec-
2259// magnitude family. `#[must_use]` fires a compile warning at any
2260// wire-up that mistakenly discards the constructed error.
2261//
2262// Every future consumer that wants to construct this variant outside
2263// [`parse_byte_size`] (a deferred `feira lint --canonical-magnitudes`
2264// per-caixa admission verb probing each authored `:memory` value
2265// against the same empty-shape gate, an M4 typed
2266// `mesh.pleme.io/v1alpha1/Servico` CR materializer's per-`:limits`
2267// admission validator re-checking one edited `:memory` slot against
2268// the codec's parser floor, a per-`computeunit.yaml` value-shape
2269// pre-emitter probing each declared byte-size magnitude ahead of the
2270// operator's admit-cycle) now reaches the variant through one call
2271// rather than re-inlining the tuple-newtype block in lockstep with
2272// the pre-existing wire-up.
2273impl LimitsError {
2274 /// Construct a [`LimitsError::EmptyByteSize`] carrying the offending
2275 /// empty-magnitude authoring string `value` verbatim in the variant's
2276 /// tuple-newtype payload. Folds the uniform
2277 /// `Self::EmptyByteSize(value.into())` tuple-newtype construction
2278 /// onto one substrate primitive so every wire-up on the variant
2279 /// reads through one dispatch rather than the pre-lift open-coded
2280 /// `LimitsError::EmptyByteSize(<into-String-expr>)` block. The
2281 /// `impl Into<String>` bound covers the pre-lift `&str` wire-up
2282 /// shape on [`parse_byte_size`] (`s.into()` on the codec-scoped
2283 /// `s: &str` binding after the outer `s.trim()` / `is_empty()` gate)
2284 /// without forcing the caller to spell the conversion at the wire-up
2285 /// site. Peer to the sibling [`LimitsError::bad_byte_magnitude`] on
2286 /// the same [`parse_byte_size`] codec surface but on the empty-shape
2287 /// axis rather than the bad-magnitude axis of the same `(String)`
2288 /// tuple-newtype codec-magnitude family.
2289 #[must_use]
2290 pub fn empty_byte_size(value: impl Into<String>) -> Self {
2291 Self::EmptyByteSize(value.into())
2292 }
2293}
2294
2295// Fold the three `LimitsError::BadDurationMagnitude(<into-String-expr>)`
2296// wire-up sites on the [`parse_duration`] codec surface onto one
2297// substrate primitive — the paired `(String)` single-slot tuple-newtype
2298// [`LimitsError::BadDurationMagnitude`] on the duration codec surface,
2299// the direct sibling to the [`LimitsError::bad_millicores`] (da7602f)
2300// and [`LimitsError::bad_byte_magnitude`] (837babc) folds above on the
2301// peer [`parse_millicores`] / [`parse_byte_size`] codec surfaces. Same
2302// discipline the peer per-variant lifts on [`AplicacaoError`] /
2303// [`SupervisorError`] / [`UpgradeError`] / [`LayoutError`] /
2304// [`DepError`] / [`ManifestError`] have converged through the
2305// "one substrate primitive per emit-site variant" ratchet: the three
2306// wire-up sites open the identical
2307// `LimitsError::BadDurationMagnitude(<into-String-expr>)` block against
2308// the codec-scoped `&str` (`num_part.into()` — non-digit-only garbage
2309// fallthrough after the numeric-shape gate) or `String`
2310// (`format!(...)` — digit-only magnitude overflows u64, magnitude ×
2311// unit overflows u64) binding, so the fold routes each site through
2312// one dispatch on a uniform `impl Into<String>` param, byte-equal to
2313// the pre-lift tuple-newtype construction on the same argument. Closes
2314// the last un-lifted variant of the paired `(String)` tuple-newtype
2315// codec-magnitude family across the three typed-magnitude codec
2316// surfaces the peer folds already own.
2317//
2318// Every future consumer that wants to construct this variant outside
2319// [`parse_duration`] (a deferred `feira lint --canonical-magnitudes`
2320// per-caixa admission verb probing each authored `:wall-clock` /
2321// `:restart-window` / `:politicas :timeout` /
2322// `:politicas :circuit-breaker :window` value against the same
2323// canonical-form gate, an M4 typed `mesh.pleme.io/v1alpha1/Servico` CR
2324// materializer's per-`:limits` admission validator re-checking one
2325// edited `:wall-clock` slot against the codec's parser floor, a
2326// per-`computeunit.yaml` value-shape pre-emitter probing each declared
2327// duration magnitude ahead of the operator's admit-cycle) now reaches
2328// the variant through one call rather than re-inlining the tuple-
2329// newtype block in lockstep with the pre-existing three sites.
2330impl LimitsError {
2331 /// Construct a [`LimitsError::BadDurationMagnitude`] carrying the
2332 /// offending duration authoring string `value` verbatim in the
2333 /// variant's tuple-newtype payload. Folds the uniform
2334 /// `Self::BadDurationMagnitude(value.into())` tuple-newtype
2335 /// construction onto one substrate primitive so every wire-up on
2336 /// the variant reads through one dispatch rather than the pre-lift
2337 /// open-coded `LimitsError::BadDurationMagnitude(<into-String-expr>)`
2338 /// block. The `impl Into<String>` bound covers both wire-up shapes
2339 /// on [`parse_duration`] — a `&str` binding (`num_part.into()`)
2340 /// and a `String` binding (`format!(...)`) — without forcing the
2341 /// caller to spell the conversion at the wire-up site. Direct
2342 /// sibling to [`LimitsError::bad_millicores`] on the peer
2343 /// [`parse_millicores`] codec surface and to
2344 /// [`LimitsError::bad_byte_magnitude`] on the peer [`parse_byte_size`]
2345 /// codec surface — closes the last un-lifted `(String)` tuple-
2346 /// newtype variant on the paired codec-magnitude family.
2347 #[must_use]
2348 pub fn bad_duration_magnitude(value: impl Into<String>) -> Self {
2349 Self::BadDurationMagnitude(value.into())
2350 }
2351}
2352
2353#[cfg(test)]
2354mod tests {
2355 use super::*;
2356
2357 #[test]
2358 fn parse_byte_size_known_units() {
2359 assert_eq!(parse_byte_size("64MiB").unwrap(), 64 * 1024 * 1024);
2360 assert_eq!(parse_byte_size("1GiB").unwrap(), 1024 * 1024 * 1024);
2361 assert_eq!(parse_byte_size("512KiB").unwrap(), 512 * 1024);
2362 assert_eq!(parse_byte_size("1KB").unwrap(), 1_000);
2363 assert_eq!(parse_byte_size("1024").unwrap(), 1024);
2364 }
2365
2366 #[test]
2367 fn parse_byte_size_rejects_unknown() {
2368 assert!(matches!(
2369 parse_byte_size("1YiB"),
2370 Err(LimitsError::UnknownByteUnit { .. })
2371 ));
2372 assert!(matches!(
2373 parse_byte_size("not-a-number"),
2374 Err(LimitsError::BadByteMagnitude(_))
2375 ));
2376 }
2377
2378 #[test]
2379 fn parse_duration_known_units() {
2380 assert_eq!(parse_duration("30s").unwrap(), Duration::from_secs(30));
2381 assert_eq!(parse_duration("500ms").unwrap(), Duration::from_millis(500));
2382 assert_eq!(parse_duration("2m").unwrap(), Duration::from_secs(120));
2383 assert_eq!(parse_duration("1h").unwrap(), Duration::from_secs(3600));
2384 }
2385
2386 #[test]
2387 fn parse_millicores_both_forms() {
2388 assert_eq!(parse_millicores("500m").unwrap(), 500);
2389 assert_eq!(parse_millicores("2").unwrap(), 2000);
2390 }
2391
2392 #[test]
2393 fn render_byte_size_canonical() {
2394 assert_eq!(render_byte_size(64 * 1024 * 1024), "64MiB");
2395 assert_eq!(render_byte_size(1024 * 1024 * 1024), "1GiB");
2396 assert_eq!(render_byte_size(1024), "1KiB");
2397 assert_eq!(render_byte_size(123), "123");
2398 }
2399
2400 #[test]
2401 fn ser_byte_size_routes_through_render_serialize_option_via_str_canonical() {
2402 // Routing pin: `ser_byte_size` (the `#[serde(serialize_with = …)]`
2403 // hook on `LimitsSpec::memory`) MUST emit exactly the bytes the
2404 // canonical `crate::render::serialize_option_via_str` primitive
2405 // produces when threaded through the peer `render_byte_size`
2406 // dispatch. Any future accidental re-inline of a bespoke
2407 // `match v { Some(_) => s.serialize_str(_), None =>
2408 // s.serialize_none() }` block inside this module — the shape
2409 // this lift removed — surfaces here as a byte-value drift on
2410 // the very first canonical form the two implementations
2411 // disagree on. Peer of
2412 // `ser_duration_routes_through_supervisor_duration_codec_render_canonical`
2413 // on the sibling `LimitsSpec::wall_clock` axis; same "one
2414 // canonical dispatch per axis, thin projections at each
2415 // consumer" discipline the sibling caixa-core substrate
2416 // primitives already carry.
2417 for n in [
2418 0u64,
2419 1,
2420 1023,
2421 1024,
2422 64 * 1024 * 1024,
2423 4 * 1024 * 1024 * 1024,
2424 ] {
2425 let limits = LimitsSpec {
2426 memory: Some(n),
2427 fuel: None,
2428 wall_clock: None,
2429 cpu: None,
2430 };
2431 let json: serde_json::Value =
2432 serde_json::from_str(&serde_json::to_string(&limits).unwrap()).unwrap();
2433 let emitted = json[crate::render::M2_LIMITS_KEY_MEMORY]
2434 .as_str()
2435 .expect("memory must serialize to a string");
2436 let canonical = render_byte_size(n);
2437 assert_eq!(
2438 emitted, canonical,
2439 "ser_byte_size drifted from render_byte_size via \
2440 serialize_option_via_str on {n} bytes",
2441 );
2442 }
2443 }
2444
2445 #[test]
2446 fn de_byte_size_routes_through_render_deserialize_option_via_str_canonical() {
2447 // Routing pin: `de_byte_size` (the
2448 // `#[serde(deserialize_with = …)]` hook on
2449 // `LimitsSpec::memory`) MUST accept exactly the canonical
2450 // string set the peer `parse_byte_size` function accepts, and
2451 // reject everything else with the parser's typed `LimitsError`
2452 // surfaced through `serde::de::Error::custom` — the shape the
2453 // lifted `crate::render::deserialize_option_via_str` primitive
2454 // enforces. A future accidental re-inline of a bespoke `let
2455 // opt: Option<String> = Option::deserialize(d)?; match opt {
2456 // … }` block inside this module — the shape this lift removed
2457 // — that drifted on either arm (silently accepting a value the
2458 // parser rejects, or swallowing a parser error as `Ok(None)`)
2459 // surfaces here.
2460 for raw in ["64MiB", "1024", "0", "4GiB"] {
2461 let field = crate::render::M2_LIMITS_KEY_MEMORY;
2462 let payload = format!("{{\"{field}\":\"{raw}\"}}");
2463 let limits: LimitsSpec =
2464 serde_json::from_str(&payload).expect("canonical memory string must round-trip");
2465 let canonical = parse_byte_size(raw).expect("parse_byte_size accepts canonical form");
2466 assert_eq!(
2467 limits.memory,
2468 Some(canonical),
2469 "de_byte_size drifted from parse_byte_size via \
2470 deserialize_option_via_str on {raw:?}",
2471 );
2472 }
2473 // Null-arm pin: `null` folds to `None` without invoking the
2474 // parser — the exact contract the lifted primitive's null-arm
2475 // test pins.
2476 let field = crate::render::M2_LIMITS_KEY_MEMORY;
2477 let null_payload = format!("{{\"{field}\":null}}");
2478 let empty: LimitsSpec = serde_json::from_str(&null_payload)
2479 .expect("null memory field must fold to LimitsSpec::memory = None");
2480 assert_eq!(
2481 empty.memory, None,
2482 "de_byte_size must fold null → None via \
2483 deserialize_option_via_str's null-arm",
2484 );
2485 // Reject-arm pin: a bogus string surfaces the parser's error
2486 // through `serde::de::Error::custom` — not `Ok(None)`.
2487 let bad_payload = format!("{{\"{field}\":\"64XiB\"}}");
2488 let err = serde_json::from_str::<LimitsSpec>(&bad_payload)
2489 .expect_err("bogus memory string must surface the parser's error");
2490 let err_text = err.to_string();
2491 assert!(
2492 err_text.contains("64XiB") || err_text.contains("XiB"),
2493 "de_byte_size must surface parse_byte_size's typed \
2494 LimitsError through serde::de::Error::custom — got \
2495 {err_text:?}",
2496 );
2497 }
2498
2499 #[test]
2500 fn ser_duration_routes_through_supervisor_duration_codec_render_canonical() {
2501 // Routing pin: `ser_duration` (the `#[serde(serialize_with = …)]`
2502 // hook on `LimitsSpec::wall_clock`) MUST emit exactly the bytes
2503 // the canonical `crate::supervisor::duration_codec::render`
2504 // primitive produces. Any future accidental re-introduction of a
2505 // sibling free-function `render_duration` shadow inside this
2506 // module — or a per-slot `serialize_with` closure that inlines
2507 // its own magnitude/unit decision tree — surfaces here as a
2508 // byte-value drift on the very first canonical form the two
2509 // implementations disagree on, well before the drift reaches any
2510 // downstream renderer's `wall_clock:` overlay. Same "one
2511 // canonical dispatch per axis, thin projections at each consumer"
2512 // discipline the sibling caixa-core substrate primitives already
2513 // carry on the peer WIT-shape / M2 supervisor-strategy / M3
2514 // mesh-slot free-function classifier families.
2515 for d in [
2516 Duration::from_secs(30),
2517 Duration::from_millis(500),
2518 Duration::from_secs(120),
2519 Duration::from_secs(3600),
2520 Duration::from_millis(0),
2521 Duration::from_millis(1500),
2522 ] {
2523 let limits = LimitsSpec {
2524 memory: None,
2525 fuel: None,
2526 wall_clock: Some(d),
2527 cpu: None,
2528 };
2529 let json: serde_json::Value =
2530 serde_json::from_str(&serde_json::to_string(&limits).unwrap()).unwrap();
2531 let emitted = json[crate::render::M2_LIMITS_KEY_WALL_CLOCK]
2532 .as_str()
2533 .expect("wall_clock must serialize to a string");
2534 let canonical = crate::supervisor::duration_codec::render(d);
2535 assert_eq!(
2536 emitted, canonical,
2537 "ser_duration drifted from supervisor::duration_codec::render on {d:?}",
2538 );
2539 }
2540 }
2541
2542 #[test]
2543 fn parse_byte_size_routes_whitespace_through_render_reject_whitespace_canonical() {
2544 // Routing pin: the paired whitespace-rejection block at the
2545 // top of `parse_byte_size` MUST route through the substrate-
2546 // side [`crate::render::reject_whitespace`] primitive — the
2547 // single-owner paired-arm gate every typed-magnitude codec
2548 // in caixa-core shares. Any future accidental re-inline of a
2549 // bespoke
2550 //
2551 // ```ignore
2552 // if let Some(byte) = find_ascii_whitespace_byte(s) { … }
2553 // if let Some(ch) = find_non_ascii_whitespace_char(s) { … }
2554 // ```
2555 //
2556 // block inside this module — the shape this lift removed —
2557 // that drifted on either arm surfaces here as a variant-shape
2558 // drift on the very first canonical form the two
2559 // implementations disagree on. Byte-shape pins cover the
2560 // ASCII WhatWG-conformant set (space / tab / LF / FF / CR)
2561 // and the strictly-complementary non-ASCII Unicode
2562 // `White_Space` class (NBSP / LINE SEPARATOR / EM-SPACE /
2563 // IDEOGRAPHIC SPACE) on the exemplar `:limits :memory` axis
2564 // — peer of the pre-existing `ser_byte_size_routes_through_
2565 // render_serialize_option_via_str_canonical` /
2566 // `de_byte_size_routes_through_render_deserialize_option_
2567 // via_str_canonical` pins on the sibling codec-hook axis.
2568 for (raw, byte) in [
2569 (" 64MiB", 0x20u8),
2570 ("64MiB ", 0x20u8),
2571 ("64 MiB", 0x20u8),
2572 ("\t64MiB", 0x09u8),
2573 ("64MiB\n", 0x0Au8),
2574 ] {
2575 let err = parse_byte_size(raw)
2576 .expect_err("ASCII-whitespace-carrying byte-size input must be rejected");
2577 let via_primitive = crate::render::reject_whitespace::<LimitsError, _, _>(
2578 raw,
2579 |b| LimitsError::WhitespaceInByteSize {
2580 value: raw.into(),
2581 byte: b,
2582 },
2583 |ch| LimitsError::NonAsciiWhitespaceInByteSize {
2584 value: raw.into(),
2585 ch,
2586 codepoint: ch as u32,
2587 },
2588 )
2589 .expect_err("primitive must reject the same ASCII-whitespace shape");
2590 assert_eq!(
2591 err, via_primitive,
2592 "parse_byte_size drifted from crate::render::reject_whitespace \
2593 on ASCII-whitespace input {raw:?}"
2594 );
2595 assert!(
2596 matches!(
2597 err,
2598 LimitsError::WhitespaceInByteSize { value: ref v, byte: b } if v == raw && b == byte
2599 ),
2600 "parse_byte_size must surface WhitespaceInByteSize {{ value: {raw:?}, byte: 0x{byte:02x} }}"
2601 );
2602 }
2603 for (raw, expected_ch) in [
2604 ("\u{00A0}64MiB", '\u{00A0}'),
2605 ("64\u{2003}MiB", '\u{2003}'),
2606 ("64MiB\u{2028}", '\u{2028}'),
2607 ("\u{3000}64MiB", '\u{3000}'),
2608 ] {
2609 let err = parse_byte_size(raw)
2610 .expect_err("non-ASCII-whitespace-carrying byte-size input must be rejected");
2611 let via_primitive = crate::render::reject_whitespace::<LimitsError, _, _>(
2612 raw,
2613 |b| LimitsError::WhitespaceInByteSize {
2614 value: raw.into(),
2615 byte: b,
2616 },
2617 |ch| LimitsError::NonAsciiWhitespaceInByteSize {
2618 value: raw.into(),
2619 ch,
2620 codepoint: ch as u32,
2621 },
2622 )
2623 .expect_err("primitive must reject the same non-ASCII-whitespace shape");
2624 assert_eq!(
2625 err, via_primitive,
2626 "parse_byte_size drifted from crate::render::reject_whitespace \
2627 on non-ASCII-whitespace input {raw:?}"
2628 );
2629 assert!(
2630 matches!(
2631 err,
2632 LimitsError::NonAsciiWhitespaceInByteSize { value: ref v, ch, codepoint }
2633 if v == raw && ch == expected_ch && codepoint == expected_ch as u32
2634 ),
2635 "parse_byte_size must surface NonAsciiWhitespaceInByteSize \
2636 {{ value: {raw:?}, ch: {expected_ch:?}, codepoint: {cp:#06X} }}",
2637 cp = expected_ch as u32
2638 );
2639 }
2640 }
2641
2642 #[test]
2643 fn limits_round_trip_through_json() {
2644 let limits = LimitsSpec {
2645 memory: Some(64 * 1024 * 1024),
2646 fuel: Some(1_000_000),
2647 wall_clock: Some(Duration::from_secs(30)),
2648 cpu: Some(500),
2649 };
2650 let json = serde_json::to_string(&limits).unwrap();
2651 let back: LimitsSpec = serde_json::from_str(&json).unwrap();
2652 assert_eq!(limits, back);
2653 }
2654
2655 #[test]
2656 fn empty_limits_serialises_to_empty_object() {
2657 let limits = LimitsSpec::default();
2658 assert!(limits.is_empty());
2659 let json = serde_json::to_string(&limits).unwrap();
2660 assert_eq!(json, "{}");
2661 }
2662
2663 // ── drift-detection: serde-derive-to-M2_LIMITS_KEY_* identity ────────
2664
2665 #[test]
2666 fn limits_spec_serde_keys_match_lifted_m2_limits_key_consts() {
2667 // Load-bearing invariant: the four `M2_LIMITS_KEY_*` consts
2668 // (`M2_LIMITS_KEY_MEMORY` / `M2_LIMITS_KEY_FUEL` /
2669 // `M2_LIMITS_KEY_WALL_CLOCK` / `M2_LIMITS_KEY_CPU`) name the
2670 // exact camelCase JSON keys the `#[serde(rename_all = "camelCase")]`
2671 // attribute on `LimitsSpec` emits, and every test-side probe
2672 // across the caixa-core / caixa-flux / caixa-helm renderer test
2673 // fixtures navigates into the rendered `:limits` overlay
2674 // sub-block by consulting one of these four `&'static str`s.
2675 // Serialize a fully-populated LimitsSpec and pin that each
2676 // canonical byte-sequence appears verbatim in the JSON — a
2677 // future accidental `rename_all = "snake_case"` /
2678 // `"kebab-case"` / verbatim-field-name flip at the derive
2679 // attribute (any of which would silently break every test-side
2680 // probe that reaches for one of the four consts) surfaces here
2681 // as a build-time test failure at `limits.rs`, not as an
2682 // apply-time `.get(<stale-canonical-const>)` returning `None`
2683 // far from the derive-attr drift's commit. Same discipline the
2684 // sibling M3 `PlacementStrategy::as_str` lift (0a2f653)
2685 // established on the peer per-`:placement :estrategia` axis:
2686 // one canonical byte-string per typed sub-key axis, pinned to
2687 // the load-bearing serde derivation at the type itself.
2688 let limits = LimitsSpec {
2689 memory: Some(64 * 1024 * 1024),
2690 fuel: Some(1_000_000),
2691 wall_clock: Some(Duration::from_secs(30)),
2692 cpu: Some(500),
2693 };
2694 let json = serde_json::to_string(&limits).unwrap();
2695 for key in [
2696 crate::render::M2_LIMITS_KEY_MEMORY,
2697 crate::render::M2_LIMITS_KEY_FUEL,
2698 crate::render::M2_LIMITS_KEY_WALL_CLOCK,
2699 crate::render::M2_LIMITS_KEY_CPU,
2700 ] {
2701 let quoted = format!("\"{key}\"");
2702 assert!(
2703 json.contains("ed),
2704 "serialized LimitsSpec must carry the lifted \
2705 M2_LIMITS_KEY_* byte-sequence {quoted} verbatim in \
2706 the JSON emission (got: {json})",
2707 );
2708 }
2709 }
2710
2711 #[test]
2712 fn m2_limits_key_consts_are_pairwise_distinct() {
2713 // Cross-axis drift-detection pin: a future collapse of two
2714 // canonical sub-key byte-strings onto the same value (e.g. an
2715 // accidental copy-paste flip of `M2_LIMITS_KEY_CPU` to also
2716 // read `"memory"`) would silently reroute every test-side
2717 // probe on one axis onto the sibling axis's overlay entry and
2718 // pass every propagation-probe test that expected only the
2719 // stale axis's value. Peer of the sibling three-way distinct
2720 // pin on the `FLUX_GITREPOSITORY_REF_KEY_*` trio (7d40380).
2721 let all = [
2722 crate::render::M2_LIMITS_KEY_MEMORY,
2723 crate::render::M2_LIMITS_KEY_FUEL,
2724 crate::render::M2_LIMITS_KEY_WALL_CLOCK,
2725 crate::render::M2_LIMITS_KEY_CPU,
2726 ];
2727 for (i, a) in all.iter().enumerate() {
2728 for b in all.iter().skip(i + 1) {
2729 assert_ne!(
2730 a, b,
2731 "M2_LIMITS_KEY_* consts must be pairwise-distinct \
2732 canonical byte-sequences — got `{a}` == `{b}`",
2733 );
2734 }
2735 }
2736 }
2737
2738 #[test]
2739 fn m2_limits_key_consts_are_lower_camel_case_shape() {
2740 // Shape-pin: every `M2_LIMITS_KEY_*` const must be a
2741 // lowerCamelCase byte-sequence (no `snake_case` underscores,
2742 // no `kebab-case` hyphens, no `PascalCase` leading capital, no
2743 // whitespace / colons / dots) — the canonical shape the
2744 // `#[serde(rename_all = "camelCase")]` derive produces on
2745 // `LimitsSpec`. A future flip to a non-camelCase attribute at
2746 // the derive surfaces both here (this test fails on the
2747 // stale-constant shape) and at
2748 // `limits_spec_serde_keys_match_lifted_m2_limits_key_consts`
2749 // (that test fails on the mismatch between const and derive).
2750 for key in [
2751 crate::render::M2_LIMITS_KEY_MEMORY,
2752 crate::render::M2_LIMITS_KEY_FUEL,
2753 crate::render::M2_LIMITS_KEY_WALL_CLOCK,
2754 crate::render::M2_LIMITS_KEY_CPU,
2755 ] {
2756 assert!(
2757 !key.is_empty(),
2758 "M2_LIMITS_KEY_* must be non-empty (got {key:?})"
2759 );
2760 let first = key.chars().next().unwrap();
2761 assert!(
2762 first.is_ascii_lowercase(),
2763 "M2_LIMITS_KEY_* must lead with an ASCII-lowercase byte \
2764 (got {key:?}, leads with {first:?})",
2765 );
2766 assert!(
2767 key.chars().all(|c| c.is_ascii_alphanumeric()),
2768 "M2_LIMITS_KEY_* must be ASCII-alphanumeric only \
2769 — no `_` / `-` / `:` / `.` / whitespace (got {key:?})",
2770 );
2771 }
2772 }
2773
2774 // ── value-shape: zero on any declared axis is rejected ────────────────
2775
2776 #[test]
2777 fn validate_accepts_default_unbounded_limits() {
2778 // Every axis None → "no bound declared" is the omit-the-slot
2779 // shape and stays valid. This is the pre-M2 default behaviour.
2780 LimitsSpec::default().validate().unwrap();
2781 }
2782
2783 #[test]
2784 fn validate_accepts_full_nonzero_limits() {
2785 let l = LimitsSpec {
2786 memory: Some(64 * 1024 * 1024),
2787 fuel: Some(1_000_000),
2788 wall_clock: Some(Duration::from_secs(30)),
2789 cpu: Some(500),
2790 };
2791 l.validate().unwrap();
2792 }
2793
2794 #[test]
2795 fn validate_rejects_zero_memory() {
2796 let l = LimitsSpec {
2797 memory: Some(0),
2798 ..Default::default()
2799 };
2800 assert_eq!(l.validate().unwrap_err(), LimitsError::MemoryZero);
2801 }
2802
2803 #[test]
2804 fn validate_rejects_zero_fuel() {
2805 let l = LimitsSpec {
2806 fuel: Some(0),
2807 ..Default::default()
2808 };
2809 assert_eq!(l.validate().unwrap_err(), LimitsError::FuelZero);
2810 }
2811
2812 #[test]
2813 fn validate_rejects_zero_wall_clock() {
2814 let l = LimitsSpec {
2815 wall_clock: Some(Duration::ZERO),
2816 ..Default::default()
2817 };
2818 assert_eq!(l.validate().unwrap_err(), LimitsError::WallClockZero);
2819 }
2820
2821 #[test]
2822 fn validate_rejects_zero_cpu() {
2823 let l = LimitsSpec {
2824 cpu: Some(0),
2825 ..Default::default()
2826 };
2827 assert_eq!(l.validate().unwrap_err(), LimitsError::CpuZero);
2828 }
2829
2830 #[test]
2831 fn validate_rejects_first_zero_axis_deterministically() {
2832 // Memory is checked first; with multiple zero axes, the
2833 // diagnostic names :memory rather than reporting some other
2834 // axis non-deterministically.
2835 let l = LimitsSpec {
2836 memory: Some(0),
2837 fuel: Some(0),
2838 wall_clock: Some(Duration::ZERO),
2839 cpu: Some(0),
2840 };
2841 assert_eq!(l.validate().unwrap_err(), LimitsError::MemoryZero);
2842 }
2843
2844 // ── value-shape: :memory upper bound — wasm32-wasip2 4 GiB ceiling ────
2845
2846 #[test]
2847 fn wasm32_memory_cap_matches_parsed_4_gib() {
2848 // The cap constant tracks the canonical "4 GiB" byte-size
2849 // codec output structurally — drift between the codec's
2850 // accepted magnitude for `"4GiB"` and the validate gate's
2851 // accepted upper bound would surface here, not as a silent
2852 // round-trip break at the renderer layer. Same single-source-
2853 // of-truth shape the is_canonical_rate_limit_window predicate
2854 // gives the rate-limit window set.
2855 assert_eq!(
2856 parse_byte_size("4GiB").unwrap(),
2857 LIMITS_MEMORY_WASM32_MAX_BYTES
2858 );
2859 assert_eq!(LIMITS_MEMORY_WASM32_MAX_BYTES, 4 * 1024 * 1024 * 1024);
2860 assert_eq!(LIMITS_MEMORY_WASM32_MAX_BYTES, 1u64 << 32);
2861 }
2862
2863 #[test]
2864 fn validate_accepts_memory_at_wasm32_cap() {
2865 // 4 GiB exactly is the wasm32 in-spec maximum — `2^16 pages ×
2866 // 2^16 bytes/page`. The validate gate is inclusive on the
2867 // upper end (mirrors the inclusive lower-end rejection: zero
2868 // is *out*, one is *in*; 4 GiB+1 is *out*, 4 GiB is *in*).
2869 let l = LimitsSpec {
2870 memory: Some(LIMITS_MEMORY_WASM32_MAX_BYTES),
2871 ..Default::default()
2872 };
2873 l.validate().unwrap();
2874 }
2875
2876 #[test]
2877 fn validate_rejects_memory_one_byte_above_wasm32_cap() {
2878 // Boundary case: exactly 1 byte past the cap. Catches a
2879 // future "strictly less than" half-measure and pins the
2880 // diagnostic to name the offending byte count verbatim.
2881 let bytes = LIMITS_MEMORY_WASM32_MAX_BYTES + 1;
2882 let l = LimitsSpec {
2883 memory: Some(bytes),
2884 ..Default::default()
2885 };
2886 assert_eq!(
2887 l.validate().unwrap_err(),
2888 LimitsError::MemoryExceedsWasm32Cap { bytes }
2889 );
2890 }
2891
2892 #[test]
2893 fn validate_rejects_memory_8_gib() {
2894 // The "obvious authoring footgun" case: a value the byte-size
2895 // codec accepts cleanly (`"8GiB"` → 8 * 1024^3 bytes) and
2896 // serde round-trips silently, but no wasm32 component can
2897 // honor. Until this gate landed `validate` accepted it.
2898 let bytes = parse_byte_size("8GiB").unwrap();
2899 let l = LimitsSpec {
2900 memory: Some(bytes),
2901 ..Default::default()
2902 };
2903 assert_eq!(
2904 l.validate().unwrap_err(),
2905 LimitsError::MemoryExceedsWasm32Cap { bytes }
2906 );
2907 }
2908
2909 #[test]
2910 fn validate_memory_zero_takes_precedence_over_cap_check() {
2911 // Memory zero is structurally meaningless under *any* wasm
2912 // engine (zero-cap traps the first allocation); above-cap is
2913 // wasm32-specific. The zero arm fires first so the canonical
2914 // "omit the slot for unbounded" remediation in the existing
2915 // MemoryZero diagnostic still leads — pinning this precedence
2916 // guards against a future re-ordering that would surface the
2917 // wasm32-specific message in the case where the simpler
2918 // zero-floor message is more actionable.
2919 let l = LimitsSpec {
2920 memory: Some(0),
2921 ..Default::default()
2922 };
2923 assert_eq!(l.validate().unwrap_err(), LimitsError::MemoryZero);
2924 }
2925
2926 #[test]
2927 fn validate_rejects_memory_cap_before_other_axes() {
2928 // With both an above-cap :memory and a zero :fuel, the
2929 // diagnostic names :memory rather than :fuel — peer of the
2930 // existing `validate_rejects_first_zero_axis_deterministically`
2931 // ordering pin.
2932 let bytes = LIMITS_MEMORY_WASM32_MAX_BYTES + 1024;
2933 let l = LimitsSpec {
2934 memory: Some(bytes),
2935 fuel: Some(0),
2936 wall_clock: Some(Duration::ZERO),
2937 cpu: Some(0),
2938 };
2939 assert_eq!(
2940 l.validate().unwrap_err(),
2941 LimitsError::MemoryExceedsWasm32Cap { bytes }
2942 );
2943 }
2944
2945 #[test]
2946 fn above_cap_value_still_round_trips_through_serde() {
2947 // The byte-size codec accepts the above-cap value (the cap
2948 // lives in the validate gate, not the codec). This pins that
2949 // the structural property is "above-cap is rejected by
2950 // validate" — not "above-cap is unparseable by the codec";
2951 // the latter would prevent the diagnostic from naming the
2952 // offending byte count at all, since deserialize would fail
2953 // first.
2954 let l = LimitsSpec {
2955 memory: Some(LIMITS_MEMORY_WASM32_MAX_BYTES + 1),
2956 ..Default::default()
2957 };
2958 let json = serde_json::to_string(&l).unwrap();
2959 let back: LimitsSpec = serde_json::from_str(&json).unwrap();
2960 assert_eq!(l, back);
2961 assert!(back.validate().is_err());
2962 }
2963
2964 // ── value-shape: :memory lower bound — wasm32-wasip2 64 KiB page floor ─
2965
2966 #[test]
2967 fn wasm32_memory_page_matches_parsed_64_kib() {
2968 // The page-floor constant tracks the canonical "64 KiB"
2969 // byte-size codec output structurally — drift between the
2970 // codec's accepted magnitude for `"64KiB"` and the validate
2971 // gate's accepted lower bound would surface here, not as a
2972 // silent round-trip break at the renderer layer. Same single-
2973 // source-of-truth shape `wasm32_memory_cap_matches_parsed_4_gib`
2974 // pins on the peer upper-cap bound and
2975 // `is_canonical_rate_limit_window` gives the rate-limit window
2976 // set. The page-size identities (2^16, integer-divides the
2977 // upper cap exactly 2^16 times) are pinned alongside so a
2978 // future memory64-target opt-in raising one bound surfaces
2979 // here if the other bound's relationship to it drifts.
2980 assert_eq!(
2981 parse_byte_size("64KiB").unwrap(),
2982 LIMITS_MEMORY_WASM32_PAGE_BYTES
2983 );
2984 assert_eq!(LIMITS_MEMORY_WASM32_PAGE_BYTES, 64 * 1024);
2985 assert_eq!(LIMITS_MEMORY_WASM32_PAGE_BYTES, 1u64 << 16);
2986 assert_eq!(
2987 LIMITS_MEMORY_WASM32_MAX_BYTES / LIMITS_MEMORY_WASM32_PAGE_BYTES,
2988 1u64 << 16,
2989 "the wasm32 page count cap is 2^16 pages exactly",
2990 );
2991 assert_eq!(
2992 LIMITS_MEMORY_WASM32_MAX_BYTES % LIMITS_MEMORY_WASM32_PAGE_BYTES,
2993 0
2994 );
2995 }
2996
2997 #[test]
2998 fn validate_rejects_memory_below_wasm32_page() {
2999 // The fail-before-pass-after pin: until this gate landed a
3000 // `(:memory "32KiB")` (or any programmatic struct literal with
3001 // a sub-page byte count — `LimitsSpec { memory: Some(50000),
3002 // .. }`) silently passed validate, the byte-size codec
3003 // round-tripped cleanly through serde, and the wasm-engine
3004 // either refused instantiation (`memory minimum size of 1
3005 // pages exceeds memory limits` on any cdylib-shaped component
3006 // declaring `(memory 1)`) or trapped the first `memory.grow(1)`
3007 // far from the source caixa.lisp.
3008 let bytes = parse_byte_size("32KiB").unwrap();
3009 let l = LimitsSpec {
3010 memory: Some(bytes),
3011 ..Default::default()
3012 };
3013 assert_eq!(
3014 l.validate().unwrap_err(),
3015 LimitsError::MemoryBelowWasm32Page { bytes }
3016 );
3017 }
3018
3019 #[test]
3020 fn validate_rejects_memory_one_byte_below_page() {
3021 // Boundary case: exactly 1 byte below the page-size floor
3022 // (`LIMITS_MEMORY_WASM32_PAGE_BYTES - 1` = 65535 bytes). Pins
3023 // the inclusive-upper-end / strict-lower-end relationship on
3024 // the page-floor arm: 65535 is *out*, 65536 is *in*. Catches a
3025 // future "strictly greater than" half-measure and matches the
3026 // peer `validate_rejects_memory_one_byte_above_wasm32_cap`
3027 // shape on the top edge.
3028 let bytes = LIMITS_MEMORY_WASM32_PAGE_BYTES - 1;
3029 let l = LimitsSpec {
3030 memory: Some(bytes),
3031 ..Default::default()
3032 };
3033 assert_eq!(
3034 l.validate().unwrap_err(),
3035 LimitsError::MemoryBelowWasm32Page { bytes }
3036 );
3037 }
3038
3039 #[test]
3040 fn validate_rejects_memory_one_byte() {
3041 // The far-floor case: a `(:memory "1")` cap is non-zero (so
3042 // `MemoryZero` doesn't fire) but structurally cannot hold any
3043 // wasm linear memory page. The page-floor gate at this layer
3044 // surfaces a self-locating diagnostic naming the offending
3045 // byte count verbatim rather than a downstream wasm-engine
3046 // instantiation failure whose error message points at the
3047 // engine's internals, not the caixa.lisp `:memory` slot.
3048 let l = LimitsSpec {
3049 memory: Some(1),
3050 ..Default::default()
3051 };
3052 assert_eq!(
3053 l.validate().unwrap_err(),
3054 LimitsError::MemoryBelowWasm32Page { bytes: 1 }
3055 );
3056 }
3057
3058 #[test]
3059 fn validate_accepts_memory_at_wasm32_page() {
3060 // 64 KiB exactly is the wasm32 linear-memory page size — the
3061 // smallest cap that admits one wasm `(memory 1)` page. The
3062 // page-floor gate is inclusive on the lower end (mirrors the
3063 // inclusive upper-end acceptance: 4 GiB is *in*, 4 GiB+1 is
3064 // *out*; 64 KiB is *in*, 64 KiB-1 is *out*).
3065 let l = LimitsSpec {
3066 memory: Some(LIMITS_MEMORY_WASM32_PAGE_BYTES),
3067 ..Default::default()
3068 };
3069 l.validate().unwrap();
3070 }
3071
3072 #[test]
3073 fn validate_accepts_multi_page_memory() {
3074 // The positive-control sweep: every typed `:memory` cap that
3075 // admits at least one wasm linear memory page (i.e. ≥
3076 // `LIMITS_MEMORY_WASM32_PAGE_BYTES`) passes `validate`. Sweeps
3077 // single-page, two-page, the canonical 64 MiB / 1 GiB / 4 GiB
3078 // upper-bound boundary so a future tightening of either edge
3079 // surfaces here. Peer of
3080 // `validate_accepts_integer_millisecond_wall_clock_values` on
3081 // the sibling `:wall-clock` axis.
3082 for bytes in [
3083 LIMITS_MEMORY_WASM32_PAGE_BYTES,
3084 2 * LIMITS_MEMORY_WASM32_PAGE_BYTES,
3085 64 * 1024 * 1024,
3086 1024 * 1024 * 1024,
3087 LIMITS_MEMORY_WASM32_MAX_BYTES,
3088 ] {
3089 let l = LimitsSpec {
3090 memory: Some(bytes),
3091 ..Default::default()
3092 };
3093 l.validate()
3094 .unwrap_or_else(|e| panic!("multi-page {bytes} must validate, got {e:?}"));
3095 }
3096 }
3097
3098 #[test]
3099 fn validate_memory_zero_takes_precedence_over_page_floor() {
3100 // Cross-arm ordering pin: `Some(0)` would otherwise pass the
3101 // page-floor arm's `m < PAGE_BYTES` check (0 < 65536), but the
3102 // zero-floor arm strictly precedes the page-floor arm so the
3103 // more self-locating `MemoryZero` diagnostic (with its omit-
3104 // axis remediation directly named, applicable under *any* wasm
3105 // engine not just wasm32) leads. Same posture every peer
3106 // zero-then-shape gate uses on this surface
3107 // (`PolicyTimeoutZero` → `PolicyTimeoutNotCanonical`,
3108 // `PolicyBreakerZeroWindow` → `PolicyBreakerWindowNotCanonical`,
3109 // `WallClockZero` → `WallClockNotCanonical`).
3110 let l = LimitsSpec {
3111 memory: Some(0),
3112 ..Default::default()
3113 };
3114 assert_eq!(l.validate().unwrap_err(), LimitsError::MemoryZero);
3115 }
3116
3117 #[test]
3118 fn validate_memory_page_floor_takes_precedence_over_other_axes() {
3119 // With a sub-page `:memory` and zero values on every other
3120 // axis, the diagnostic names `:memory` rather than `:fuel` /
3121 // `:wall-clock` / `:cpu` — peer of the existing
3122 // `validate_rejects_first_zero_axis_deterministically` and
3123 // `validate_rejects_memory_cap_before_other_axes` ordering
3124 // pins. Memory is the first axis the validate cascade checks,
3125 // so a sub-page value surfaces before any other-axis
3126 // diagnostic regardless of how many other axes are
3127 // simultaneously invalid.
3128 let bytes = LIMITS_MEMORY_WASM32_PAGE_BYTES / 2;
3129 let l = LimitsSpec {
3130 memory: Some(bytes),
3131 fuel: Some(0),
3132 wall_clock: Some(Duration::ZERO),
3133 cpu: Some(0),
3134 };
3135 assert_eq!(
3136 l.validate().unwrap_err(),
3137 LimitsError::MemoryBelowWasm32Page { bytes }
3138 );
3139 }
3140
3141 #[test]
3142 fn memory_page_floor_diagnostic_carries_offending_bytes() {
3143 // Diagnostic-shape pin: the page-floor arm names the
3144 // offending byte count verbatim so the author's grep lands on
3145 // the field's value, not a generic "memory too small" message.
3146 // Same shape every other typed-cap arm on this surface
3147 // carries (`MemoryExceedsWasm32Cap` carries the offending byte
3148 // count verbatim, `WallClockNotCanonical` carries the
3149 // offending `Duration` verbatim, `PolicyRetriesExceedsCap`
3150 // carries the offending retry count verbatim).
3151 let l = LimitsSpec {
3152 memory: Some(50_000),
3153 ..Default::default()
3154 };
3155 let err = l.validate().unwrap_err();
3156 let msg = err.to_string();
3157 assert!(
3158 msg.contains("50000"),
3159 "diagnostic must carry the offending byte count verbatim (got {msg:?})"
3160 );
3161 assert!(
3162 msg.contains("64 KiB") || msg.contains("65536"),
3163 "diagnostic must name the page-size floor (got {msg:?})"
3164 );
3165 }
3166
3167 #[test]
3168 fn below_page_value_still_round_trips_through_serde() {
3169 // The byte-size codec accepts the sub-page value (the floor
3170 // lives in the validate gate, not the codec) — peer of
3171 // `above_cap_value_still_round_trips_through_serde` on the top
3172 // edge. Pins that the structural property is "sub-page is
3173 // rejected by validate" — not "sub-page is unparseable by the
3174 // codec"; the latter would prevent the diagnostic from naming
3175 // the offending byte count at all, since deserialize would
3176 // fail first.
3177 let l = LimitsSpec {
3178 memory: Some(LIMITS_MEMORY_WASM32_PAGE_BYTES - 1),
3179 ..Default::default()
3180 };
3181 let json = serde_json::to_string(&l).unwrap();
3182 let back: LimitsSpec = serde_json::from_str(&json).unwrap();
3183 assert_eq!(l, back);
3184 assert!(back.validate().is_err());
3185 }
3186
3187 // ── value-shape: :memory page-multiple granularity gate ───────────────
3188
3189 #[test]
3190 fn validate_rejects_memory_one_byte_above_page() {
3191 // The fail-before-pass-after pin: until this gate landed a
3192 // `LimitsSpec { memory: Some(LIMITS_MEMORY_WASM32_PAGE_BYTES +
3193 // 1), .. }` (65537 bytes — one wasm32 page plus a 1-byte
3194 // unreachable residue) silently passed validate, the byte-size
3195 // codec round-tripped cleanly through serde (`render_byte_size`
3196 // falls through to `"65537"` on any non-power-of-1024 magnitude),
3197 // and wasmtime's `StoreLimits::memory_size` consumed the value
3198 // verbatim as a page-quantized ceiling — the engine grew at
3199 // most floor(65537 / 65536) = 1 page, and the byte at offset
3200 // 65536 became structural dead space the runtime cannot honor.
3201 let bytes = LIMITS_MEMORY_WASM32_PAGE_BYTES + 1;
3202 let l = LimitsSpec {
3203 memory: Some(bytes),
3204 ..Default::default()
3205 };
3206 assert_eq!(
3207 l.validate().unwrap_err(),
3208 LimitsError::MemoryNotPageMultiple { bytes }
3209 );
3210 }
3211
3212 #[test]
3213 fn validate_rejects_memory_just_below_two_pages() {
3214 // Boundary case: exactly 1 byte below two pages (`2 *
3215 // LIMITS_MEMORY_WASM32_PAGE_BYTES - 1` = 131071 bytes). Pins
3216 // the inclusive-page-boundary / strict-sub-page-residue
3217 // relationship on the page-multiple arm: 131071 is *out*
3218 // (sub-page residue), 131072 is *in* (exactly two pages).
3219 // Matches the peer `validate_rejects_memory_one_byte_below_page`
3220 // / `validate_rejects_memory_one_byte_above_wasm32_cap` shape
3221 // on the surrounding edges.
3222 let bytes = 2 * LIMITS_MEMORY_WASM32_PAGE_BYTES - 1;
3223 let l = LimitsSpec {
3224 memory: Some(bytes),
3225 ..Default::default()
3226 };
3227 assert_eq!(
3228 l.validate().unwrap_err(),
3229 LimitsError::MemoryNotPageMultiple { bytes }
3230 );
3231 }
3232
3233 #[test]
3234 fn validate_rejects_memory_100000_bytes() {
3235 // The "obvious authoring footgun" case: a magnitude the
3236 // byte-size codec accepts cleanly (`"100000"` → 100000 bytes
3237 // ≈ 97.65 KiB) and serde round-trips silently, but no wasm32
3238 // engine can honor as a meaningful ceiling — the engine grows
3239 // at most floor(100000 / 65536) = 1 page, and the 34464 bytes
3240 // between offsets 65536 and 100000 are structural dead space.
3241 // Until this gate landed `validate` accepted it. Peer of
3242 // `validate_rejects_memory_8_gib` on the cap arm.
3243 let bytes = parse_byte_size("100000").unwrap();
3244 let l = LimitsSpec {
3245 memory: Some(bytes),
3246 ..Default::default()
3247 };
3248 assert_eq!(
3249 l.validate().unwrap_err(),
3250 LimitsError::MemoryNotPageMultiple { bytes }
3251 );
3252 }
3253
3254 #[test]
3255 fn validate_accepts_every_page_aligned_value_through_serde() {
3256 // Positive-control sweep through the byte-size codec: every
3257 // canonical magnitude `render_byte_size` emits at or above
3258 // the page floor divides cleanly by the page size, so the
3259 // page-multiple gate accepts the entire canonical-output
3260 // domain at and above the page floor. The sweep walks
3261 // single-page (`"64KiB"`), two-page (`"128KiB"`), every
3262 // power-of-1024 unit (`"1MiB"`, `"64MiB"`, `"1GiB"`, `"4GiB"`),
3263 // and the cap (`"4GiB"`) — pinning that the codec's
3264 // emitted-canonical-form set is a structural subset of the
3265 // validate gate's accepted set. Drift between the codec's
3266 // emit alphabet and the validate gate would surface here
3267 // rather than at a future serializer round trip.
3268 for s in ["64KiB", "128KiB", "1MiB", "64MiB", "1GiB", "4GiB"] {
3269 let bytes = parse_byte_size(s).unwrap();
3270 assert_eq!(
3271 bytes % LIMITS_MEMORY_WASM32_PAGE_BYTES,
3272 0,
3273 "canonical byte-size codec output {s:?} ({bytes}) must be page-aligned",
3274 );
3275 let l = LimitsSpec {
3276 memory: Some(bytes),
3277 ..Default::default()
3278 };
3279 l.validate()
3280 .unwrap_or_else(|e| panic!("canonical {s:?} = {bytes} must validate, got {e:?}"));
3281 }
3282 }
3283
3284 #[test]
3285 fn validate_memory_below_page_takes_precedence_over_page_multiple() {
3286 // Cross-arm ordering pin: `Some(1)` would otherwise pass the
3287 // page-multiple arm's `m % PAGE_BYTES != 0` check (1 % 65536
3288 // == 1 ≠ 0), but the page-floor arm strictly precedes the
3289 // page-multiple arm so the more self-locating
3290 // `MemoryBelowWasm32Page` diagnostic (with its "single page
3291 // cannot fit" remediation, applicable to every sub-page
3292 // value uniformly) leads. Peer of `MemoryZero` →
3293 // `MemoryBelowWasm32Page` precedence on the zero edge:
3294 // every value `m` in the range `1..=PAGE_BYTES-1` satisfies
3295 // both `m < PAGE_BYTES` and `m % PAGE_BYTES != 0`, but the
3296 // structurally-narrower diagnostic (page-floor) leads.
3297 let l = LimitsSpec {
3298 memory: Some(1),
3299 ..Default::default()
3300 };
3301 assert_eq!(
3302 l.validate().unwrap_err(),
3303 LimitsError::MemoryBelowWasm32Page { bytes: 1 }
3304 );
3305 }
3306
3307 #[test]
3308 fn validate_memory_cap_takes_precedence_over_page_multiple() {
3309 // Cross-arm ordering pin: `LIMITS_MEMORY_WASM32_MAX_BYTES + 1`
3310 // (4 GiB + 1 byte) is *both* above-cap and not page-aligned.
3311 // The cap arm strictly precedes the page-multiple arm so the
3312 // more aggressive cap-shape diagnostic leads (the page-multiple
3313 // remediation would be misleading when the offending value
3314 // exceeds the wasm32 address-space ceiling anyway — the
3315 // canonical fix collapses both into "pin a page-aligned value
3316 // ≤ 4 GiB"). Peer of `WallClockNotCanonical` →
3317 // `WallClockExceedsCap` ordering on the sibling `:wall-clock`
3318 // axis (with the inverse polarity — there the granularity
3319 // gate leads because sub-millisecond residue breaks serde
3320 // round-trip; here the cap leads because both gates' offending
3321 // values round-trip cleanly through serde and the broader
3322 // magnitude constraint is the more aggressive one).
3323 let bytes = LIMITS_MEMORY_WASM32_MAX_BYTES + 1;
3324 let l = LimitsSpec {
3325 memory: Some(bytes),
3326 ..Default::default()
3327 };
3328 assert_eq!(
3329 l.validate().unwrap_err(),
3330 LimitsError::MemoryExceedsWasm32Cap { bytes }
3331 );
3332 }
3333
3334 #[test]
3335 fn validate_rejects_memory_page_multiple_before_other_axes() {
3336 // With a sub-page-residue `:memory` and zero values on every
3337 // other axis, the diagnostic names `:memory` rather than
3338 // `:fuel` / `:wall-clock` / `:cpu` — peer of the existing
3339 // `validate_memory_page_floor_takes_precedence_over_other_axes`
3340 // and `validate_rejects_memory_cap_before_other_axes` ordering
3341 // pins. Memory is the first axis the validate cascade checks,
3342 // so a sub-page-residue value surfaces before any other-axis
3343 // diagnostic regardless of how many other axes are
3344 // simultaneously invalid.
3345 let bytes = LIMITS_MEMORY_WASM32_PAGE_BYTES + 1;
3346 let l = LimitsSpec {
3347 memory: Some(bytes),
3348 fuel: Some(0),
3349 wall_clock: Some(Duration::ZERO),
3350 cpu: Some(0),
3351 };
3352 assert_eq!(
3353 l.validate().unwrap_err(),
3354 LimitsError::MemoryNotPageMultiple { bytes }
3355 );
3356 }
3357
3358 #[test]
3359 fn memory_page_multiple_diagnostic_carries_offending_bytes() {
3360 // Diagnostic-shape pin: the page-multiple arm names the
3361 // offending byte count verbatim so the author's grep lands on
3362 // the field's value, not a generic "memory not aligned"
3363 // message. Same shape every other typed-cap arm on this
3364 // surface carries (`MemoryExceedsWasm32Cap` carries the
3365 // offending byte count verbatim, `WallClockNotCanonical`
3366 // carries the offending `Duration` verbatim).
3367 let bytes = LIMITS_MEMORY_WASM32_PAGE_BYTES + 12345;
3368 let l = LimitsSpec {
3369 memory: Some(bytes),
3370 ..Default::default()
3371 };
3372 let err = l.validate().unwrap_err();
3373 let msg = err.to_string();
3374 assert!(
3375 msg.contains(&bytes.to_string()),
3376 "diagnostic must carry the offending byte count verbatim (got {msg:?})"
3377 );
3378 assert!(
3379 msg.contains("64 KiB") || msg.contains("65536") || msg.contains("page"),
3380 "diagnostic must name the page-size granularity (got {msg:?})"
3381 );
3382 }
3383
3384 #[test]
3385 fn sub_page_residue_value_still_round_trips_through_serde() {
3386 // The byte-size codec accepts the sub-page-residue value (the
3387 // page-multiple gate lives in validate, not in the codec) —
3388 // peer of `above_cap_value_still_round_trips_through_serde`
3389 // and `below_page_value_still_round_trips_through_serde`.
3390 // Pins that the structural property is "sub-page-residue is
3391 // rejected by validate" — not "sub-page-residue is
3392 // unparseable by the codec"; the latter would prevent the
3393 // diagnostic from naming the offending byte count at all,
3394 // since deserialize would fail first. The render-then-parse
3395 // round trip also pins the codec's flow-through-to-bytes
3396 // shape on non-power-of-1024 magnitudes: `render_byte_size`
3397 // falls through every `(mult, label)` arm whose `n % mult !=
3398 // 0` and emits the bare byte count.
3399 let bytes = LIMITS_MEMORY_WASM32_PAGE_BYTES + 1;
3400 let l = LimitsSpec {
3401 memory: Some(bytes),
3402 ..Default::default()
3403 };
3404 let json = serde_json::to_string(&l).unwrap();
3405 let back: LimitsSpec = serde_json::from_str(&json).unwrap();
3406 assert_eq!(l, back);
3407 assert!(back.validate().is_err());
3408 }
3409
3410 #[test]
3411 fn validate_memory_axis_routes_through_quantum_multiple_bounded_helper() {
3412 // Byte-parity pin on the pre-lift `if self.memory() == Some(0)
3413 // { … } if let Some(m) = self.memory() { if m <
3414 // LIMITS_MEMORY_WASM32_PAGE_BYTES { … } } if let Some(m) =
3415 // self.memory() { if m > LIMITS_MEMORY_WASM32_MAX_BYTES { … } }
3416 // if let Some(m) = self.memory() && m %
3417 // LIMITS_MEMORY_WASM32_PAGE_BYTES != 0 { … }` four-sequential-
3418 // `if let` shape the `LimitsSpec::validate` `:memory` axis
3419 // routed through today via
3420 // `crate::render::require_positive_quantum_multiple_bounded_u64`.
3421 // Refuses a future accidental split between the helper's
3422 // four-arm ordering (zero → below-quantum → cap → not-multiple)
3423 // and the four typed `LimitsError::Memory*` variants each arm
3424 // threads its offending byte count into — a swap of any two
3425 // arms in the helper, or a partial widening (e.g. removing the
3426 // page-multiple arm), or a widening of the `on_below_quantum`
3427 // arm's closure to the `MemoryExceedsWasm32Cap` variant instead
3428 // of `MemoryBelowWasm32Page` — would break exactly one row of
3429 // this pin, matching the pre-lift shape the four consumer sites
3430 // route through today. Same shape as
3431 // `as_seq_body_partitions_the_same_arm_set_as_seq_delims` in
3432 // caixa-ast and the peer `require_positive_bounded_u64` tests
3433 // in the sibling render.rs test module.
3434 //
3435 // (Some(bytes) → expected LimitsError)
3436 let quantum = LIMITS_MEMORY_WASM32_PAGE_BYTES;
3437 let cap = LIMITS_MEMORY_WASM32_MAX_BYTES;
3438 let cases: &[(u64, LimitsError)] = &[
3439 (0, LimitsError::MemoryZero),
3440 (1, LimitsError::MemoryBelowWasm32Page { bytes: 1 }),
3441 (
3442 quantum - 1,
3443 LimitsError::MemoryBelowWasm32Page { bytes: quantum - 1 },
3444 ),
3445 (
3446 cap + 1,
3447 LimitsError::MemoryExceedsWasm32Cap { bytes: cap + 1 },
3448 ),
3449 (
3450 cap + quantum,
3451 LimitsError::MemoryExceedsWasm32Cap {
3452 bytes: cap + quantum,
3453 },
3454 ),
3455 (
3456 quantum + 1,
3457 LimitsError::MemoryNotPageMultiple { bytes: quantum + 1 },
3458 ),
3459 (
3460 quantum + 12_345,
3461 LimitsError::MemoryNotPageMultiple {
3462 bytes: quantum + 12_345,
3463 },
3464 ),
3465 ];
3466 for (bytes, expected) in cases {
3467 let l = LimitsSpec {
3468 memory: Some(*bytes),
3469 ..Default::default()
3470 };
3471 assert_eq!(
3472 l.validate().unwrap_err(),
3473 *expected,
3474 "memory={bytes} must surface the {expected:?} arm via the substrate helper",
3475 );
3476 }
3477 // Positive-control: every quantum-multiple in `quantum..=cap`
3478 // passes, closing the four-arm cascade with an `Ok(())` shape.
3479 for bytes in [quantum, quantum * 2, quantum * 100, cap] {
3480 let l = LimitsSpec {
3481 memory: Some(bytes),
3482 ..Default::default()
3483 };
3484 l.validate().unwrap();
3485 }
3486 }
3487
3488 // ── canonical-form: integer-magnitude byte-size codec gate ────────────
3489 //
3490 // Every magnitude `render_byte_size` emits is a non-negative integer
3491 // (no decimal point, no leading sign, no scientific notation). The
3492 // parser's accepted set must match for parse → render → parse to
3493 // round-trip without canonical-form drift. The tests below pin every
3494 // canonical-drift shape — fractional (`"1.5KiB"`), decimal-shaped-
3495 // integer (`"1.0MiB"`), half-unit (`"0.5GiB"`), leading-`+`
3496 // (`"+1024"`) — plus the scientific-notation dispatch path (caught
3497 // by `UnknownByteUnit` on a different arm), the two complement-side
3498 // pins (the integer happy paths the gate must continue to accept),
3499 // the round-trip convergence property (parse → render → parse must
3500 // converge on a single canonical form for every accepted input),
3501 // the BadByteMagnitude-precedence pin (genuinely unparseable inputs
3502 // keep their narrower diagnostic), the overflow-surface pin
3503 // (u64-overflow on magnitude × unit surfaces at parse time), and
3504 // the serde-path pin (the gate fires at deserialize, before any
3505 // validate gate runs).
3506
3507 #[test]
3508 fn parse_byte_size_rejects_fractional_kib() {
3509 // The fail-before-pass-after pin: `"1.5KiB"` parsed cleanly on
3510 // every pre-gate codebase (f64::parse accepts the decimal), the
3511 // codec produced 1536 bytes, and `render_byte_size(1536)`
3512 // emitted `"1536"` on the next serialize — silently drifting
3513 // the canonical form away from the author's intent. The new
3514 // gate surfaces the round-trip break at the parser layer with
3515 // a self-locating diagnostic (the offending magnitude verbatim,
3516 // the canonical-form remediation in the wording).
3517 let err = parse_byte_size("1.5KiB").unwrap_err();
3518 assert!(
3519 matches!(err, LimitsError::NonIntegerByteMagnitude { ref value } if value == "1.5"),
3520 "got {err:?}"
3521 );
3522 }
3523
3524 #[test]
3525 fn parse_byte_size_rejects_decimal_shaped_integer() {
3526 // The canonical-drift case where the *value* is integer but
3527 // the *form* carries a redundant decimal point — `"1.0MiB"`
3528 // parses to 1 MiB (integer), but the renderer emits `"1MiB"`
3529 // on the next serialize (no decimal point). The parse-shape
3530 // gate fires here too so the codec's accepted set is exactly
3531 // the renderer's emitted set — no `"1.0MiB"` ↔ `"1MiB"` drift
3532 // surviving a round-trip silently.
3533 let err = parse_byte_size("1.0MiB").unwrap_err();
3534 assert!(
3535 matches!(err, LimitsError::NonIntegerByteMagnitude { ref value } if value == "1.0"),
3536 "got {err:?}"
3537 );
3538 }
3539
3540 #[test]
3541 fn parse_byte_size_rejects_half_gib() {
3542 // `"0.5GiB"` parses to 536870912 bytes = 512MiB; the renderer
3543 // emits `"512MiB"` on the next serialize. Pin the round-trip
3544 // drift on the explicitly-fractional case sized to land on a
3545 // unit boundary, so the gate's coverage includes both the
3546 // "doesn't land on a boundary" (1.5KiB → 1536) and "lands on
3547 // a smaller-unit boundary" (0.5GiB → 512MiB) drift shapes.
3548 let err = parse_byte_size("0.5GiB").unwrap_err();
3549 assert!(
3550 matches!(err, LimitsError::NonIntegerByteMagnitude { ref value } if value == "0.5"),
3551 "got {err:?}"
3552 );
3553 }
3554
3555 #[test]
3556 fn parse_byte_size_rejects_scientific_notation_via_unit_arm() {
3557 // Scientific-notation magnitudes are canonical-form drift too
3558 // — the renderer never emits `"1e3KiB"` for any value. But
3559 // they're caught on a *different* arm than the fractional /
3560 // leading-`+` shapes: the parser's split-on-first-alphabetic-
3561 // byte heuristic reads the `e` as a unit prefix, so the input
3562 // falls into the existing `UnknownByteUnit { unit: "e3KiB" }`
3563 // diagnostic before the `NonIntegerByteMagnitude` gate is
3564 // consulted. Pin this dispatch path so a future relaxation of
3565 // the split heuristic (e.g. recognizing `e` as part of a
3566 // scientific-notation magnitude) surfaces here as a test
3567 // failure — at which point the `NonIntegerByteMagnitude` gate
3568 // would correctly take over, and this test would flip to that
3569 // arm with no other change required.
3570 let err = parse_byte_size("1e3KiB").unwrap_err();
3571 assert!(
3572 matches!(err, LimitsError::UnknownByteUnit { ref unit } if unit == "e3KiB"),
3573 "got {err:?}"
3574 );
3575 }
3576
3577 #[test]
3578 fn parse_byte_size_rejects_leading_plus() {
3579 // `"+1024"` parses through f64 as 1024 bytes; the renderer
3580 // emits `"1KiB"` on the next serialize. The leading `+` is
3581 // not a renderer-emitted shape, so it falls in the same
3582 // canonical-drift class as the fractional / scientific forms
3583 // — surfacing under the same diagnostic keeps the gate's
3584 // coverage uniform across every non-canonical-but-numeric
3585 // input shape the parser would otherwise accept.
3586 let err = parse_byte_size("+1024").unwrap_err();
3587 assert!(
3588 matches!(err, LimitsError::NonIntegerByteMagnitude { ref value } if value == "+1024"),
3589 "got {err:?}"
3590 );
3591 }
3592
3593 #[test]
3594 fn parse_byte_size_continues_to_accept_integer_magnitudes() {
3595 // The complement-side pin: every canonical integer-magnitude
3596 // form the renderer emits must continue to parse to the same
3597 // value the renderer produced. Sweep the five canonical
3598 // authoring shapes (unitless integer, KiB, MiB, GiB, KB) so a
3599 // future tightening of the parser surfaces here as a test
3600 // failure rather than a silent regression in the canonical
3601 // authoring set.
3602 assert_eq!(parse_byte_size("1024").unwrap(), 1024);
3603 assert_eq!(parse_byte_size("1KiB").unwrap(), 1024);
3604 assert_eq!(parse_byte_size("64MiB").unwrap(), 64 * 1024 * 1024);
3605 assert_eq!(parse_byte_size("1GiB").unwrap(), 1024 * 1024 * 1024);
3606 assert_eq!(parse_byte_size("1000KB").unwrap(), 1_000_000);
3607 }
3608
3609 #[test]
3610 fn parse_byte_size_round_trips_through_render_for_every_canonical_form() {
3611 // The structural property the gate makes load-bearing: every
3612 // value the parser accepts round-trips through `render_byte_size`
3613 // to a string the parser also accepts — and to the *same* value.
3614 // Sweep the values the renderer emits canonically (1024 / 1MiB
3615 // / 1GiB / 1536 / 64MiB) so a future codec change that breaks
3616 // round-trip convergence surfaces here, not at a downstream
3617 // renderer that double-emits a typed slot.
3618 for n in [1u64, 1023, 1024, 1536, 64 * 1024 * 1024, 1024 * 1024 * 1024] {
3619 let rendered = render_byte_size(n);
3620 let reparsed = parse_byte_size(&rendered)
3621 .unwrap_or_else(|e| panic!("render({n}) = {rendered:?} must reparse, got {e:?}"));
3622 assert_eq!(
3623 reparsed, n,
3624 "round-trip drift on {n}: rendered={rendered:?}, reparsed={reparsed}",
3625 );
3626 }
3627 }
3628
3629 #[test]
3630 fn parse_byte_size_keeps_bad_magnitude_for_unparseable_input() {
3631 // The precedence pin: the new `NonIntegerByteMagnitude` arm
3632 // distinguishes *non-canonical-but-numeric* (`"1.5"`, `"1.0"`,
3633 // `"+1024"`, `"-1"`) from *genuinely-unparseable* (`"abc"`,
3634 // `"--1"`) so the existing `BadByteMagnitude` diagnostic's
3635 // wording remains load-bearing for the latter class — the gate
3636 // is additive, not replacing. Pin both arms so a future
3637 // relaxation that collapses them surfaces here.
3638 let err = parse_byte_size("abc").unwrap_err();
3639 assert!(
3640 matches!(err, LimitsError::BadByteMagnitude(_)),
3641 "got {err:?}"
3642 );
3643 let err = parse_byte_size("--1").unwrap_err();
3644 assert!(
3645 matches!(err, LimitsError::BadByteMagnitude(_)),
3646 "got {err:?}"
3647 );
3648 }
3649
3650 #[test]
3651 fn parse_byte_size_overflow_surfaces_as_bad_magnitude() {
3652 // `u64::MAX KiB` overflows the u64 result; the parser surfaces
3653 // the overflow as a `BadByteMagnitude` (not as a saturated
3654 // `u64::MAX` value that the wasm32-cap validate gate then
3655 // catches), so the diagnostic names the offending magnitude ×
3656 // unit pair at parse time rather than as
3657 // `MemoryExceedsWasm32Cap { bytes: u64::MAX }` far from the
3658 // author's intent. (`u64::MAX` itself parses cleanly with no
3659 // unit since `u64::MAX × 1 = u64::MAX` fits.)
3660 let err = parse_byte_size("18446744073709551615KiB").unwrap_err();
3661 let LimitsError::BadByteMagnitude(reason) = err else {
3662 panic!("expected BadByteMagnitude(overflow), got other variant");
3663 };
3664 assert!(
3665 reason.contains("overflow"),
3666 "overflow diagnostic must mention overflow (got {reason:?})"
3667 );
3668 }
3669
3670 // ── canonical-form: leading-zero byte-size codec gate ─────────────────
3671 //
3672 // Direct successor to the `parse_duration` leading-zero arm (39762d7),
3673 // the `supervisor::duration_codec` leading-zero arm (9178904), and the
3674 // `rate_limit_codec` leading-zero arm (4f46830) — the same canonical-
3675 // form render-determinism axis applied to the last typed-numeric codec
3676 // that still admitted leading-zero magnitudes. The digit-only gate
3677 // immediately above accepts every `u64::from_str`-parseable magnitude
3678 // including leading-zero padding, but `render_byte_size` always emits
3679 // the stripped form (`64MiB`, never `064MiB`) — silently drifting the
3680 // canonical string across a parse/render round-trip. Pins each
3681 // canonical leading-zero shape across the unit-set the codec admits
3682 // (KB / MB / GB / KiB / MiB / GiB / bare-integer), the all-zero
3683 // degenerate case, the codec-vs-validate-layer partition (single-byte
3684 // `"0"` stays accepted at the codec because the typed-validate gate
3685 // `MemoryZero` refuses semantic-zero authoring), the complement-side
3686 // pin (`1`..=`9`-led magnitudes stay accepted), and the serde-path pin
3687 // (the gate fires at deserialize, before any validate gate runs).
3688
3689 #[test]
3690 fn parse_byte_size_rejects_leading_zero_magnitude() {
3691 // The fail-before-pass-after pin: `"064MiB"` parsed cleanly on
3692 // every pre-gate codebase (`u64::from_str` accepts the leading
3693 // zero), the codec produced 64 MiB, and
3694 // `render_byte_size(64*1024*1024)` emitted `"64MiB"` on the next
3695 // serialize — silently dropping the leading zero and drifting
3696 // the canonical form away from the author's intent. The new
3697 // gate surfaces the round-trip break at the parser layer with a
3698 // self-locating diagnostic, peer with
3699 // `parse_duration_rejects_leading_zero_magnitude` on the sibling
3700 // codec.
3701 let err = parse_byte_size("064MiB").unwrap_err();
3702 assert!(
3703 matches!(err, LimitsError::LeadingZeroByteMagnitude { ref value } if value == "064"),
3704 "got {err:?}"
3705 );
3706 }
3707
3708 #[test]
3709 fn parse_byte_size_rejects_multi_digit_zero_magnitude() {
3710 // `"00MiB"` is the degenerate leading-zero case — every byte is
3711 // `0`. `u64::from_str("00")` = 0, and the codec produces 0;
3712 // `render_byte_size(0)` emits `"0"` on the next serialize —
3713 // drift from `"00MiB"` to `"0"`. The leading-zero arm refuses
3714 // the drift class at the codec layer while leaving the
3715 // canonical single-byte `"0"` accepted. Peer with
3716 // `parse_duration_rejects_multi_digit_zero_magnitude` on the
3717 // sibling codec.
3718 let err = parse_byte_size("00MiB").unwrap_err();
3719 assert!(
3720 matches!(err, LimitsError::LeadingZeroByteMagnitude { ref value } if value == "00"),
3721 "got {err:?}"
3722 );
3723 }
3724
3725 #[test]
3726 fn parse_byte_size_rejects_leading_zero_in_gib_unit() {
3727 // `"01GiB"` parses to 1 GiB; the renderer emits `"1GiB"` on the
3728 // next serialize. The leading-zero class is a property of the
3729 // magnitude, not the unit — pin a per-GiB magnitude alongside
3730 // the per-MiB / per-KiB / bare-integer pins so the gate's
3731 // coverage is structural across every canonical unit suffix
3732 // the codec accepts. Mirrors the per-hour pin
3733 // `parse_duration_rejects_leading_zero_in_hour_window` carries
3734 // on the sibling codec.
3735 let err = parse_byte_size("01GiB").unwrap_err();
3736 assert!(
3737 matches!(err, LimitsError::LeadingZeroByteMagnitude { ref value } if value == "01"),
3738 "got {err:?}"
3739 );
3740 }
3741
3742 #[test]
3743 fn parse_byte_size_rejects_leading_zero_in_kib_unit() {
3744 // `"0512KiB"` parses to 512 KiB; the renderer emits `"512KiB"`
3745 // on the next serialize. Pin the per-KiB magnitude alongside
3746 // the per-MiB / per-GiB pins so the gate's coverage extends to
3747 // the smallest-unit power-of-1024 suffix the codec admits.
3748 let err = parse_byte_size("0512KiB").unwrap_err();
3749 assert!(
3750 matches!(err, LimitsError::LeadingZeroByteMagnitude { ref value } if value == "0512"),
3751 "got {err:?}"
3752 );
3753 }
3754
3755 #[test]
3756 fn parse_byte_size_rejects_leading_zero_in_decimal_units() {
3757 // `"0500MB"` parses to 500 MB (decimal-unit family — `KB` /
3758 // `MB` / `GB` powers of 1000, distinct from the `KiB` / `MiB` /
3759 // `GiB` powers-of-1024 family); the renderer emits the
3760 // appropriate canonical form on the next serialize. Pin the
3761 // decimal-unit family alongside the power-of-1024 family so the
3762 // gate's coverage is structural across both unit families the
3763 // codec admits.
3764 for (s, expected) in [("0500MB", "0500"), ("01KB", "01"), ("00GB", "00")] {
3765 let err = parse_byte_size(s).unwrap_err();
3766 assert!(
3767 matches!(err, LimitsError::LeadingZeroByteMagnitude { value: ref v } if v == expected),
3768 "got {err:?} for {s:?}"
3769 );
3770 }
3771 }
3772
3773 #[test]
3774 fn parse_byte_size_rejects_leading_zero_bare_integer() {
3775 // The bare-integer (no unit) shorthand inherits the leading-
3776 // zero arm: `"01024"` parses losslessly to 1024 bytes but
3777 // `render_byte_size(1024)` emits `"1KiB"` on the next serialize.
3778 // Pin the bare-integer path so a future relaxation that
3779 // special-cases the unitless shorthand surfaces here as a test
3780 // failure. Mirrors the bare-integer pin
3781 // `parse_duration_rejects_leading_zero_bare_integer_as_seconds`
3782 // carries on the sibling codec.
3783 let err = parse_byte_size("01024").unwrap_err();
3784 assert!(
3785 matches!(err, LimitsError::LeadingZeroByteMagnitude { ref value } if value == "01024"),
3786 "got {err:?}"
3787 );
3788 }
3789
3790 #[test]
3791 fn parse_byte_size_accepts_single_zero_magnitude_at_codec_layer() {
3792 // The codec-layer / typed-validate-layer boundary pin: the
3793 // single-byte `"0"` magnitude round-trips losslessly through
3794 // `render_byte_size` (`render_byte_size(0)` emits `"0"`), so it
3795 // stays accepted at this codec layer across every canonical
3796 // unit suffix. The downstream `LimitsError::MemoryZero` gate is
3797 // what refuses zero-magnitude authoring at the typed-validate
3798 // layer above — the partition keeps the canonical-form-drift
3799 // diagnostic (this arm) and the semantic-zero diagnostic (the
3800 // validate gate) disjoint. Mirrors the
3801 // `parse_duration_accepts_single_zero_magnitude_at_codec_layer`
3802 // partition pin on the sibling codec.
3803 assert_eq!(parse_byte_size("0").unwrap(), 0);
3804 assert_eq!(parse_byte_size("0B").unwrap(), 0);
3805 assert_eq!(parse_byte_size("0KiB").unwrap(), 0);
3806 assert_eq!(parse_byte_size("0MiB").unwrap(), 0);
3807 assert_eq!(parse_byte_size("0GiB").unwrap(), 0);
3808 assert_eq!(parse_byte_size("0KB").unwrap(), 0);
3809 }
3810
3811 #[test]
3812 fn parse_byte_size_accepts_canonical_magnitude_with_leading_one() {
3813 // The complement-side pin on the leading-zero arm: magnitudes
3814 // beginning with `1`..=`9` stay accepted across every canonical
3815 // unit suffix the codec accepts. Pin this so a future
3816 // tightening cannot drift into rejecting valid canonical
3817 // magnitudes — peer with the
3818 // `parse_duration_accepts_canonical_magnitude_with_leading_one`
3819 // pin on the sibling codec.
3820 assert_eq!(parse_byte_size("1").unwrap(), 1);
3821 assert_eq!(parse_byte_size("1KiB").unwrap(), 1024);
3822 assert_eq!(parse_byte_size("1MiB").unwrap(), 1024 * 1024);
3823 assert_eq!(parse_byte_size("1GiB").unwrap(), 1024 * 1024 * 1024);
3824 assert_eq!(parse_byte_size("64MiB").unwrap(), 64 * 1024 * 1024);
3825 assert_eq!(parse_byte_size("9").unwrap(), 9);
3826 }
3827
3828 #[test]
3829 fn de_byte_size_rejects_leading_zero_through_serde() {
3830 // The serde-path pin: a `:limits :memory` carrying a
3831 // leading-zero magnitude (`"064MiB"`) must fail at deserialize
3832 // time, not silently round-trip the value through the parser.
3833 // The gate fires at deserialize, before any validate gate runs
3834 // — peer with `de_duration_rejects_leading_zero_through_serde`
3835 // on the sibling codec.
3836 let json = r#"{"memory":"064MiB"}"#;
3837 let err = serde_json::from_str::<LimitsSpec>(json).unwrap_err();
3838 let msg = err.to_string();
3839 assert!(
3840 msg.contains("leading zero"),
3841 "serde diagnostic must surface the leading-zero reason verbatim (got {msg:?})"
3842 );
3843 }
3844
3845 // ── canonical-form: whitespace-rejection byte-size codec gate ─────────
3846 //
3847 // Direct successor to the `parse_duration` whitespace-rejection arm
3848 // (ebc3a75), the `supervisor::duration_codec` whitespace-rejection
3849 // arm (a7ae622), and the `rate_limit_codec` whitespace-rejection arm
3850 // (1ad7755) on the same canonical-form render-determinism axis. The
3851 // pre-gate top-level `s.trim()` at parse entry and the per-part
3852 // `num_part.trim()` / `unit.trim()` calls silently ate leading /
3853 // trailing / internal whitespace, so every whitespace-carrying
3854 // shape parsed to the same byte magnitude and round-tripped through
3855 // `render_byte_size` to a *different* canonical string on next
3856 // serialize — the same canonical-form-drift class the leading-`+` /
3857 // fractional / leading-zero arms already close on this codec.
3858 // `u8::is_ascii_whitespace` covers the five WhatWG-conformant ASCII
3859 // whitespace bytes (space `0x20`, tab `0x09`, LF `0x0A`, FF `0x0C`,
3860 // CR `0x0D`). Closes the whitespace-rejection axis across every
3861 // typed-magnitude codec in caixa-core.
3862
3863 #[test]
3864 fn parse_byte_size_rejects_leading_whitespace() {
3865 // The fail-before-pass-after pin: `" 64MiB"` — the canonical
3866 // paste-from-aligned-doc / paste-from-YAML-quoted-plain-scalar
3867 // footgun. Before this gate the top-level `s.trim()` at parse
3868 // entry silently ate the leading space and parsed the value to
3869 // 64 * 1024 * 1024 bytes, which then round-tripped through
3870 // `render_byte_size` to `"64MiB"` (a *different* canonical
3871 // string on the next emit) — the exact canonical-form-drift
3872 // class the leading-`+` / leading-zero arms already close,
3873 // extended to the whitespace-byte class. Peer with the sibling
3874 // `parse_duration_rejects_leading_whitespace` arm (ebc3a75) on
3875 // the shared canonical-form-drift trajectory.
3876 let err = parse_byte_size(" 64MiB").unwrap_err();
3877 assert!(
3878 matches!(err, LimitsError::WhitespaceInByteSize { ref value, byte } if value == " 64MiB" && byte == 0x20),
3879 "got {err:?}"
3880 );
3881 let msg = err.to_string();
3882 assert!(
3883 msg.contains("whitespace byte 0x20"),
3884 "diagnostic must surface the offending byte verbatim (got {msg:?})"
3885 );
3886 assert!(
3887 msg.contains("THEORY.md"),
3888 "diagnostic must cite the render-determinism contract (got {msg:?})"
3889 );
3890 }
3891
3892 #[test]
3893 fn parse_byte_size_rejects_trailing_whitespace() {
3894 // `"64MiB "` — the canonical shell-history / trailing-space
3895 // paste footgun. Before this gate the top-level `s.trim()`
3896 // silently ate the trailing space and parsed to 64 * 1024 *
3897 // 1024 bytes, round-tripping to `"64MiB"` on the next emit —
3898 // same canonical-form drift as the leading-space sibling,
3899 // closed on the same whitespace-byte arm.
3900 let err = parse_byte_size("64MiB ").unwrap_err();
3901 assert!(
3902 matches!(err, LimitsError::WhitespaceInByteSize { ref value, byte } if value == "64MiB " && byte == 0x20),
3903 "got {err:?}"
3904 );
3905 }
3906
3907 #[test]
3908 fn parse_byte_size_rejects_internal_whitespace_between_magnitude_and_unit() {
3909 // `"64 MiB"` — the canonical typographically-spaced author
3910 // shape (the same idiom every prose reference to a byte-size
3911 // renders as, mistakenly retained when the value is pasted
3912 // into a codec-shaped slot). Before this gate the per-part
3913 // `num_part.trim()` / `unit.trim()` calls silently ate the
3914 // whitespace between the magnitude and the unit and parsed the
3915 // value to 64 * 1024 * 1024 bytes, round-tripping to `"64MiB"`
3916 // — the codec's *internal* whitespace-tolerance vector,
3917 // orthogonal to the leading / trailing surface but the same
3918 // canonical-form-drift class. Pins the arm as strictly
3919 // stronger than the pre-existing top-level `s.trim()`
3920 // behavior: it fires on whitespace anywhere in the value, not
3921 // just at the string boundary.
3922 let err = parse_byte_size("64 MiB").unwrap_err();
3923 assert!(
3924 matches!(err, LimitsError::WhitespaceInByteSize { ref value, byte } if value == "64 MiB" && byte == 0x20),
3925 "got {err:?}"
3926 );
3927 }
3928
3929 #[test]
3930 fn parse_byte_size_rejects_tab_byte() {
3931 // `"\t64MiB"` — the canonical paste-from-indented-doc /
3932 // paste-from-YAML-block-scalar footgun where a tab byte leads
3933 // the magnitude. Pins that the gate covers tab (`0x09`) as
3934 // well as space (`0x20`) — both are `u8::is_ascii_whitespace`
3935 // members and both would be silently swallowed by `s.trim()`
3936 // pre-gate. The `is_ascii_whitespace` coverage extends beyond
3937 // space alone to the full ASCII-whitespace set (space `0x20`,
3938 // tab `0x09`, LF `0x0A`, FF `0x0C`, CR `0x0D`); this test pins
3939 // the tab arm as a representative of the non-space members.
3940 let err = parse_byte_size("\t64MiB").unwrap_err();
3941 assert!(
3942 matches!(err, LimitsError::WhitespaceInByteSize { ref value, byte } if value == "\t64MiB" && byte == 0x09),
3943 "got {err:?}"
3944 );
3945 }
3946
3947 #[test]
3948 fn parse_byte_size_rejects_trailing_newline() {
3949 // `"64MiB\n"` — the canonical multi-line-paste footgun where
3950 // a trailing LF byte survives the paste. Pins the LF member
3951 // (`0x0A`) of the `is_ascii_whitespace` set as a peer to the
3952 // space and tab pins above — every non-space non-tab
3953 // whitespace byte the WhatWG ASCII-whitespace set covers is
3954 // refused by the same arm.
3955 let err = parse_byte_size("64MiB\n").unwrap_err();
3956 assert!(
3957 matches!(err, LimitsError::WhitespaceInByteSize { ref value, byte } if value == "64MiB\n" && byte == 0x0a),
3958 "got {err:?}"
3959 );
3960 }
3961
3962 #[test]
3963 fn parse_byte_size_accepts_whitespace_free_canonical_forms() {
3964 // The complement-side pin: every canonical whitespace-free
3965 // authoring form the renderer emits stays accepted post-gate.
3966 // Sweep the canonical unit suffixes plus the bare-integer
3967 // shorthand so a future tightening of the whitespace arm that
3968 // over-fires on the accepted set surfaces here as a test
3969 // failure. Peer with the
3970 // `parse_duration_accepts_whitespace_free_canonical_forms` pin
3971 // on the sibling codec.
3972 assert_eq!(parse_byte_size("64MiB").unwrap(), 64 * 1024 * 1024);
3973 assert_eq!(parse_byte_size("1GiB").unwrap(), 1024 * 1024 * 1024);
3974 assert_eq!(parse_byte_size("512KiB").unwrap(), 512 * 1024);
3975 assert_eq!(parse_byte_size("1KB").unwrap(), 1_000);
3976 assert_eq!(parse_byte_size("1024").unwrap(), 1024);
3977 assert_eq!(parse_byte_size("0").unwrap(), 0);
3978 }
3979
3980 #[test]
3981 fn de_byte_size_rejects_whitespace_through_serde() {
3982 // The serde-path pin: a `:limits :memory` carrying a
3983 // whitespace-byte-carrying value (`" 64MiB"`) must fail at
3984 // deserialize time, not silently round-trip the value through
3985 // the pre-existing top-level `s.trim()`. The gate fires at
3986 // deserialize, before any validate gate runs — peer with the
3987 // existing `de_byte_size_rejects_leading_zero_through_serde` /
3988 // `de_duration_rejects_whitespace_through_serde` pins on the
3989 // same canonical-form-drift axis.
3990 let json = r#"{"memory":" 64MiB"}"#;
3991 let err = serde_json::from_str::<LimitsSpec>(json).unwrap_err();
3992 let msg = err.to_string();
3993 assert!(
3994 msg.contains("whitespace byte"),
3995 "serde diagnostic must surface the whitespace reason verbatim (got {msg:?})"
3996 );
3997 assert!(
3998 msg.contains("0x20"),
3999 "serde diagnostic must name the offending byte (got {msg:?})"
4000 );
4001
4002 // The whitespace-free complement — same author-side intent,
4003 // written in the canonical form the renderer would emit,
4004 // deserializes cleanly.
4005 let json = r#"{"memory":"64MiB"}"#;
4006 let l: LimitsSpec = serde_json::from_str(json).unwrap();
4007 assert_eq!(l.memory, Some(64 * 1024 * 1024));
4008 }
4009
4010 // ── canonical-form: non-ASCII Unicode `White_Space` byte-size gate ────
4011 //
4012 // Direct successor to the `parse_byte_size` ASCII-whitespace arm
4013 // (24a8ad4) — closes the strictly-complementary class the byte-scan
4014 // above cannot see. `str::trim` uses `char::is_whitespace` (Unicode
4015 // `White_Space`, strictly wider than the ASCII byte set); a leading /
4016 // trailing / internal NBSP (`\u{00A0}`) / LINE SEPARATOR (`\u{2028}`)
4017 // / EM-SPACE (`\u{2003}`) survives the byte-scan but is silently
4018 // stripped by the top-level trim, drifting to canonical `"64MiB"` on
4019 // round-trip. Pins the arm through the lifted
4020 // [`crate::render::find_non_ascii_whitespace_char`] predicate.
4021
4022 #[test]
4023 fn parse_byte_size_rejects_leading_nbsp() {
4024 // NBSP (`\u{00A0}` = UTF-8 `0xC2 0xA0`) — the canonical
4025 // paste-from-typography / paste-from-word-processor footgun.
4026 // Before this arm landed the byte-scan missed it (neither `0xC2`
4027 // nor `0xA0` is `is_ascii_whitespace`) and `str::trim` at parse
4028 // entry silently stripped it, yielding the same `64 * 1024 *
4029 // 1024` bytes as the whitespace-free canonical form and drifting
4030 // to `"64MiB"` on next serialize.
4031 let s = "\u{00A0}64MiB";
4032 let err = parse_byte_size(s).unwrap_err();
4033 assert!(
4034 matches!(err, LimitsError::NonAsciiWhitespaceInByteSize { ref value, ch, codepoint } if value == s && ch == '\u{00A0}' && codepoint == 0x00A0),
4035 "got {err:?}"
4036 );
4037 let msg = err.to_string();
4038 assert!(
4039 msg.contains("U+00A0"),
4040 "diagnostic must surface the codepoint verbatim (got {msg:?})"
4041 );
4042 assert!(
4043 msg.contains("THEORY.md"),
4044 "diagnostic must cite the render-determinism contract (got {msg:?})"
4045 );
4046 }
4047
4048 #[test]
4049 fn parse_byte_size_rejects_internal_line_separator() {
4050 // LINE SEPARATOR (`\u{2028}`) between magnitude and unit — the
4051 // canonical paste-from-web-doc footgun (many rendering engines
4052 // insert `\u{2028}` at soft-wrap boundaries in RTF/HTML → plain
4053 // text conversion). Pins the arm on a non-space non-NBSP Unicode
4054 // `White_Space` member.
4055 let s = "64\u{2028}MiB";
4056 let err = parse_byte_size(s).unwrap_err();
4057 assert!(
4058 matches!(err, LimitsError::NonAsciiWhitespaceInByteSize { ref value, ch, codepoint } if value == s && ch == '\u{2028}' && codepoint == 0x2028),
4059 "got {err:?}"
4060 );
4061 }
4062
4063 #[test]
4064 fn parse_byte_size_rejects_trailing_ideographic_space() {
4065 // IDEOGRAPHIC SPACE (`\u{3000}`) — the CJK-typography paste
4066 // footgun (canonical U+3000 is the full-width space that
4067 // Japanese / Chinese IMEs emit when input is auto-widened). Pins
4068 // the arm at the top edge of the `char::is_whitespace` set.
4069 let s = "64MiB\u{3000}";
4070 let err = parse_byte_size(s).unwrap_err();
4071 assert!(
4072 matches!(err, LimitsError::NonAsciiWhitespaceInByteSize { ref value, ch, codepoint } if value == s && ch == '\u{3000}' && codepoint == 0x3000),
4073 "got {err:?}"
4074 );
4075 }
4076
4077 #[test]
4078 fn parse_byte_size_accepts_ascii_only_canonical_forms_after_unicode_arm() {
4079 // Positive-control pin: every ASCII-only canonical form the
4080 // renderer emits stays accepted through the new arm — the
4081 // lifted predicate is a strict no-op on ASCII input.
4082 assert_eq!(parse_byte_size("64MiB").unwrap(), 64 * 1024 * 1024);
4083 assert_eq!(parse_byte_size("1GiB").unwrap(), 1024 * 1024 * 1024);
4084 assert_eq!(parse_byte_size("512KiB").unwrap(), 512 * 1024);
4085 assert_eq!(parse_byte_size("1024").unwrap(), 1024);
4086 }
4087
4088 // ── canonical-form: integer-magnitude duration codec gate ─────────────
4089 //
4090 // Direct successor to the `parse_byte_size` integer-magnitude gate on
4091 // the peer `:limits :memory` codec — every magnitude `render_duration`
4092 // emits is a non-negative integer (no decimal point, no leading sign,
4093 // no scientific notation). The parser's accepted set must match for
4094 // parse → render → parse to round-trip without canonical-form drift.
4095 // Pins every canonical-drift shape — fractional (`"1.5s"`),
4096 // decimal-shaped-integer (`"1.0s"`), half-unit (`"0.5m"`),
4097 // leading-`+` (`"+30s"`), leading-`-` (`"-30s"`) — plus the
4098 // complement-side pin (integer happy paths), the round-trip
4099 // convergence property, the BadDurationMagnitude-precedence pin
4100 // (genuinely unparseable inputs keep their narrower diagnostic), the
4101 // overflow-surface pin (u64-overflow on magnitude × unit surfaces at
4102 // parse time), and the serde-path pin (the gate fires at deserialize,
4103 // before any validate gate runs).
4104
4105 #[test]
4106 fn parse_duration_rejects_fractional_seconds() {
4107 // The fail-before-pass-after pin: `"1.5s"` parsed cleanly on
4108 // every pre-gate codebase (f64::parse accepts the decimal), the
4109 // codec produced 1500ms, and `render_duration(1500ms)` emitted
4110 // `"1500ms"` on the next serialize — silently drifting the
4111 // canonical form away from the author's intent. The new gate
4112 // surfaces the round-trip break at the parser layer with a
4113 // self-locating diagnostic.
4114 let err = parse_duration("1.5s").unwrap_err();
4115 assert!(
4116 matches!(err, LimitsError::NonIntegerDurationMagnitude { ref value } if value == "1.5"),
4117 "got {err:?}"
4118 );
4119 }
4120
4121 #[test]
4122 fn parse_duration_rejects_decimal_shaped_integer() {
4123 // The canonical-drift case where the *value* is integer but the
4124 // *form* carries a redundant decimal point — `"1.0s"` parses to
4125 // 1s (integer), but the renderer emits `"1s"` on the next
4126 // serialize (no decimal point). The parse-shape gate fires here
4127 // too so the codec's accepted set is exactly the renderer's
4128 // emitted set.
4129 let err = parse_duration("1.0s").unwrap_err();
4130 assert!(
4131 matches!(err, LimitsError::NonIntegerDurationMagnitude { ref value } if value == "1.0"),
4132 "got {err:?}"
4133 );
4134 }
4135
4136 #[test]
4137 fn parse_duration_rejects_half_minute() {
4138 // `"0.5m"` parses to 30s; the renderer emits `"30s"` on the
4139 // next serialize. Pin the round-trip drift on the explicitly-
4140 // fractional case sized to land on a smaller-unit boundary, so
4141 // the gate's coverage includes both the "doesn't land on a
4142 // boundary" (1.5s → 1500ms) and "lands on a smaller-unit
4143 // boundary" (0.5m → 30s) drift shapes — the same two-shape
4144 // pattern the byte-size gate covers (1.5KiB → 1536, 0.5GiB →
4145 // 512MiB).
4146 let err = parse_duration("0.5m").unwrap_err();
4147 assert!(
4148 matches!(err, LimitsError::NonIntegerDurationMagnitude { ref value } if value == "0.5"),
4149 "got {err:?}"
4150 );
4151 }
4152
4153 #[test]
4154 fn parse_duration_rejects_leading_plus() {
4155 // `"+30s"` parses through f64 as 30s; the renderer emits `"30s"`
4156 // on the next serialize. The leading `+` is not a renderer-
4157 // emitted shape, so it falls in the same canonical-drift class
4158 // as the fractional forms — surfacing under the same diagnostic
4159 // keeps the gate's coverage uniform across every non-canonical-
4160 // but-numeric input shape the parser would otherwise accept.
4161 let err = parse_duration("+30s").unwrap_err();
4162 assert!(
4163 matches!(err, LimitsError::NonIntegerDurationMagnitude { ref value } if value == "+30"),
4164 "got {err:?}"
4165 );
4166 }
4167
4168 #[test]
4169 fn parse_duration_rejects_negative_seconds_via_integer_gate() {
4170 // The negative-magnitude class — pre-gate the parser routed
4171 // negatives through the `num < 0.0` check to `BadDurationMagnitude`;
4172 // the new digit-only gate fires earlier and routes the same
4173 // input to `NonIntegerDurationMagnitude` (negatives are not
4174 // digit-only). Pin the new diagnostic so a future relaxation
4175 // that re-routes negatives back to the old arm surfaces here.
4176 let err = parse_duration("-30s").unwrap_err();
4177 assert!(
4178 matches!(err, LimitsError::NonIntegerDurationMagnitude { ref value } if value == "-30"),
4179 "got {err:?}"
4180 );
4181 }
4182
4183 #[test]
4184 fn parse_duration_continues_to_accept_integer_magnitudes() {
4185 // The complement-side pin: every canonical integer-magnitude
4186 // form the renderer emits must continue to parse to the same
4187 // value the renderer produced. Sweep the canonical authoring
4188 // shapes (ms, bare-s, s, m, h, and the bare-integer "0" zero-
4189 // shape) so a future tightening of the parser surfaces here as
4190 // a test failure rather than a silent regression.
4191 assert_eq!(parse_duration("0s").unwrap(), Duration::ZERO);
4192 assert_eq!(parse_duration("500ms").unwrap(), Duration::from_millis(500));
4193 assert_eq!(parse_duration("30s").unwrap(), Duration::from_secs(30));
4194 assert_eq!(parse_duration("2m").unwrap(), Duration::from_secs(120));
4195 assert_eq!(parse_duration("1h").unwrap(), Duration::from_secs(3600));
4196 assert_eq!(parse_duration("3600").unwrap(), Duration::from_secs(3600));
4197 }
4198
4199 #[test]
4200 fn parse_duration_round_trips_through_render_for_every_canonical_form() {
4201 // The structural property the gate makes load-bearing: every
4202 // value the parser accepts round-trips through the canonical
4203 // [`crate::supervisor::duration_codec::render`] primitive to a
4204 // string the parser also accepts — and to the *same* value.
4205 // Sweep the values the renderer emits canonically (ms / s / m /
4206 // h boundaries plus a non-aligned millisecond) so a future
4207 // codec change that breaks round-trip convergence surfaces here.
4208 for d in [
4209 Duration::from_millis(1),
4210 Duration::from_millis(500),
4211 Duration::from_millis(1500),
4212 Duration::from_secs(1),
4213 Duration::from_secs(30),
4214 Duration::from_secs(60),
4215 Duration::from_secs(120),
4216 Duration::from_secs(3600),
4217 ] {
4218 let rendered = crate::supervisor::duration_codec::render(d);
4219 let reparsed = parse_duration(&rendered)
4220 .unwrap_or_else(|e| panic!("render({d:?}) = {rendered:?} must reparse, got {e:?}"));
4221 assert_eq!(
4222 reparsed, d,
4223 "round-trip drift on {d:?}: rendered={rendered:?}, reparsed={reparsed:?}",
4224 );
4225 }
4226 }
4227
4228 #[test]
4229 fn parse_duration_keeps_bad_magnitude_for_unparseable_input() {
4230 // The precedence pin: the new `NonIntegerDurationMagnitude` arm
4231 // distinguishes *non-canonical-but-numeric* (`"1.5"`, `"+30"`,
4232 // `"-30"`) from *genuinely-unparseable* (`"abc"`, `"--1"`) so
4233 // the existing `BadDurationMagnitude` diagnostic's wording
4234 // remains load-bearing for the latter class — the gate is
4235 // additive, not replacing.
4236 let err = parse_duration("abcs").unwrap_err();
4237 assert!(
4238 matches!(err, LimitsError::BadDurationMagnitude(_)),
4239 "got {err:?}"
4240 );
4241 let err = parse_duration("--1s").unwrap_err();
4242 assert!(
4243 matches!(err, LimitsError::BadDurationMagnitude(_)),
4244 "got {err:?}"
4245 );
4246 }
4247
4248 #[test]
4249 fn parse_duration_overflow_surfaces_as_bad_magnitude() {
4250 // `u64::MAX h` overflows the seconds computation (magnitude ×
4251 // 3600); the parser surfaces the overflow as a
4252 // `BadDurationMagnitude` with an overflow-shaped wording so the
4253 // diagnostic names the offending magnitude × unit pair at parse
4254 // time. Matches `parse_byte_size`'s overflow-surface arm
4255 // structurally.
4256 let err = parse_duration("18446744073709551615h").unwrap_err();
4257 let LimitsError::BadDurationMagnitude(reason) = err else {
4258 panic!("expected BadDurationMagnitude(overflow), got other variant");
4259 };
4260 assert!(
4261 reason.contains("overflow"),
4262 "overflow diagnostic must mention overflow (got {reason:?})"
4263 );
4264 }
4265
4266 // ── canonical-form: leading-zero duration codec gate ─────────────────
4267 //
4268 // Direct successor to the `supervisor::duration_codec` leading-zero
4269 // arm (9178904) and the `rate_limit_codec` leading-zero arm (4f46830)
4270 // — closes the leading-zero canonical-form-drift class on the
4271 // `:limits :wall-clock` codec. Every magnitude `render_duration`
4272 // emits is a non-negative integer with no leading-zero padding; the
4273 // parser's accepted set must match for parse → render → parse to
4274 // round-trip without canonical-form drift. The single-byte `"0"`
4275 // round-trips losslessly (`render_duration(Duration::ZERO)` emits
4276 // `"0s"`) and the downstream [`LimitsError::WallClockZero`] gate
4277 // refuses zero-magnitude authoring at the typed-validate layer above
4278 // — the codec-layer / typed-validate-layer partition is what keeps
4279 // the diagnostic partitioning stable.
4280
4281 #[test]
4282 fn parse_duration_rejects_leading_zero_magnitude() {
4283 // The fail-before-pass-after pin: `"030s"` parsed cleanly on
4284 // every pre-gate codebase (`u64::from_str` accepts the leading
4285 // zero), the codec produced 30s, and `render_duration(30s)`
4286 // emitted `"30s"` on the next serialize — silently dropping
4287 // the leading zero and drifting the canonical form away from
4288 // the author's intent. The new gate surfaces the round-trip
4289 // break at the parser layer with a self-locating diagnostic.
4290 let err = parse_duration("030s").unwrap_err();
4291 assert!(
4292 matches!(err, LimitsError::LeadingZeroDurationMagnitude { ref value } if value == "030"),
4293 "got {err:?}"
4294 );
4295 }
4296
4297 #[test]
4298 fn parse_duration_rejects_multi_digit_zero_magnitude() {
4299 // `"00s"` is the degenerate leading-zero case — every byte is
4300 // `0`. `u64::from_str("00")` = 0, and the codec produces
4301 // `Duration::ZERO`; `render_duration(Duration::ZERO)` emits
4302 // `"0s"` on the next serialize — drift from `"00s"` to `"0s"`.
4303 // The leading-zero arm refuses the drift class at the codec
4304 // layer while leaving the canonical single-byte `"0s"` accepted.
4305 let err = parse_duration("00s").unwrap_err();
4306 assert!(
4307 matches!(err, LimitsError::LeadingZeroDurationMagnitude { ref value } if value == "00"),
4308 "got {err:?}"
4309 );
4310 }
4311
4312 #[test]
4313 fn parse_duration_rejects_leading_zero_in_hour_window() {
4314 // `"01h"` parses to 1h; the renderer emits `"1h"` on the next
4315 // serialize. The leading-zero class is a property of the
4316 // magnitude, not the unit — pin a per-hour magnitude alongside
4317 // the per-second / per-ms pins so the gate's coverage is
4318 // structural across every canonical unit suffix the codec
4319 // accepts. Mirrors the `_per_hour_window` pin the
4320 // `supervisor::duration_codec` and `rate_limit_codec` leading-
4321 // zero arms carry on the peer codecs.
4322 let err = parse_duration("01h").unwrap_err();
4323 assert!(
4324 matches!(err, LimitsError::LeadingZeroDurationMagnitude { ref value } if value == "01"),
4325 "got {err:?}"
4326 );
4327 }
4328
4329 #[test]
4330 fn parse_duration_rejects_leading_zero_bare_integer_as_seconds() {
4331 // The bare-integer-as-seconds shorthand (`"30"` → 30s, no unit
4332 // suffix because the parser routes the empty `unit` slot to
4333 // `Duration::from_secs`) inherits the leading-zero arm: `"030"`
4334 // parses losslessly to 30s but `render_duration(30s)` emits
4335 // `"30s"` on the next serialize. Pin the bare-integer path so a
4336 // future relaxation that special-cases the unitless shorthand
4337 // surfaces here as a test failure.
4338 let err = parse_duration("030").unwrap_err();
4339 assert!(
4340 matches!(err, LimitsError::LeadingZeroDurationMagnitude { ref value } if value == "030"),
4341 "got {err:?}"
4342 );
4343 }
4344
4345 #[test]
4346 fn parse_duration_accepts_single_zero_magnitude_at_codec_layer() {
4347 // The codec-layer / typed-validate-layer boundary pin: the
4348 // single-byte `"0"` magnitude round-trips losslessly through
4349 // `render_duration` (`render_duration(Duration::ZERO)` emits
4350 // `"0s"`), so it stays accepted at this codec layer across
4351 // every canonical unit suffix. The downstream
4352 // `LimitsError::WallClockZero` gate is what refuses
4353 // zero-magnitude authoring at the typed-validate layer above
4354 // — the partition keeps the canonical-form-drift diagnostic
4355 // (this arm) and the semantic-zero diagnostic (the validate
4356 // gate) disjoint.
4357 assert_eq!(parse_duration("0s").unwrap(), Duration::ZERO);
4358 assert_eq!(parse_duration("0ms").unwrap(), Duration::ZERO);
4359 assert_eq!(parse_duration("0m").unwrap(), Duration::ZERO);
4360 assert_eq!(parse_duration("0h").unwrap(), Duration::ZERO);
4361 assert_eq!(parse_duration("0").unwrap(), Duration::ZERO);
4362 }
4363
4364 #[test]
4365 fn parse_duration_accepts_canonical_magnitude_with_leading_one() {
4366 // The complement-side pin on the leading-zero arm: magnitudes
4367 // beginning with `1`..=`9` stay accepted across every canonical
4368 // unit suffix the codec accepts. Pin this so a future
4369 // tightening cannot drift into rejecting valid canonical
4370 // magnitudes — peer with the `_accepts_canonical_magnitude_with_leading_one`
4371 // pin the `supervisor::duration_codec` and `rate_limit_codec`
4372 // leading-zero arms carry.
4373 assert_eq!(parse_duration("1ms").unwrap(), Duration::from_millis(1));
4374 assert_eq!(parse_duration("1s").unwrap(), Duration::from_secs(1));
4375 assert_eq!(parse_duration("1m").unwrap(), Duration::from_secs(60));
4376 assert_eq!(parse_duration("1h").unwrap(), Duration::from_secs(3600));
4377 assert_eq!(parse_duration("100ms").unwrap(), Duration::from_millis(100));
4378 assert_eq!(parse_duration("500ms").unwrap(), Duration::from_millis(500));
4379 }
4380
4381 // ── canonical-form: whitespace-rejection duration codec gate ─────────
4382 //
4383 // Direct successor to the `supervisor::duration_codec` whitespace-
4384 // rejection arm (a7ae622) and the `rate_limit_codec` whitespace-
4385 // rejection arm (1ad7755) on the same canonical-form
4386 // render-determinism axis. The pre-gate top-level `s.trim()` at
4387 // parse entry and the per-part `num_part.trim()` / `unit.trim()`
4388 // calls silently ate leading / trailing / internal whitespace, so
4389 // every whitespace-carrying shape parsed to the same integer
4390 // magnitude and round-tripped through `render_duration` to a
4391 // *different* canonical string on next serialize — the same
4392 // canonical-form-drift class the leading-`+` / fractional /
4393 // leading-zero arms already close on this codec. `u8::is_ascii_whitespace`
4394 // covers the five WhatWG-conformant ASCII whitespace bytes
4395 // (space `0x20`, tab `0x09`, LF `0x0A`, FF `0x0C`, CR `0x0D`).
4396
4397 #[test]
4398 fn parse_duration_rejects_leading_whitespace() {
4399 // The fail-before-pass-after pin: `" 30s"` — the canonical
4400 // paste-from-aligned-doc / paste-from-YAML-quoted-plain-scalar
4401 // footgun. Before this gate the top-level `s.trim()` at parse
4402 // entry silently ate the leading space and parsed the value to
4403 // `Duration::from_secs(30)`, which then round-tripped through
4404 // `render_duration` to `"30s"` (a *different* canonical string
4405 // on the next emit) — the exact canonical-form-drift class the
4406 // leading-`+` / leading-zero arms already close, extended to
4407 // the whitespace-byte class. Peer with the sibling
4408 // `supervisor::duration_codec` `parse_rejects_leading_whitespace`
4409 // arm (a7ae622) on the shared duration-codec trajectory.
4410 let err = parse_duration(" 30s").unwrap_err();
4411 assert!(
4412 matches!(err, LimitsError::WhitespaceInDuration { ref value, byte } if value == " 30s" && byte == 0x20),
4413 "got {err:?}"
4414 );
4415 let msg = err.to_string();
4416 assert!(
4417 msg.contains("whitespace byte 0x20"),
4418 "diagnostic must surface the offending byte verbatim (got {msg:?})"
4419 );
4420 assert!(
4421 msg.contains("THEORY.md"),
4422 "diagnostic must cite the render-determinism contract (got {msg:?})"
4423 );
4424 }
4425
4426 #[test]
4427 fn parse_duration_rejects_trailing_whitespace() {
4428 // `"30s "` — the canonical shell-history / trailing-space paste
4429 // footgun. Before this gate the top-level `s.trim()` silently
4430 // ate the trailing space and parsed to `Duration::from_secs(30)`,
4431 // round-tripping to `"30s"` on the next emit — same canonical-
4432 // form drift as the leading-space sibling, closed on the same
4433 // whitespace-byte arm.
4434 let err = parse_duration("30s ").unwrap_err();
4435 assert!(
4436 matches!(err, LimitsError::WhitespaceInDuration { ref value, byte } if value == "30s " && byte == 0x20),
4437 "got {err:?}"
4438 );
4439 }
4440
4441 #[test]
4442 fn parse_duration_rejects_internal_whitespace_between_magnitude_and_unit() {
4443 // `"30 s"` — the canonical typographically-spaced author shape
4444 // (the same idiom every prose reference to a duration renders as,
4445 // mistakenly retained when the value is pasted into a codec-
4446 // shaped slot). Before this gate the per-part `num_part.trim()`
4447 // / `unit.trim()` calls silently ate the whitespace between the
4448 // magnitude and the unit and parsed the value to
4449 // `Duration::from_secs(30)`, round-tripping to `"30s"` — the
4450 // codec's *internal* whitespace-tolerance vector, orthogonal
4451 // to the leading / trailing surface but the same canonical-
4452 // form-drift class. Pins the arm as strictly stronger than the
4453 // pre-existing top-level `s.trim()` behavior: it fires on
4454 // whitespace anywhere in the value, not just at the string
4455 // boundary.
4456 let err = parse_duration("30 s").unwrap_err();
4457 assert!(
4458 matches!(err, LimitsError::WhitespaceInDuration { ref value, byte } if value == "30 s" && byte == 0x20),
4459 "got {err:?}"
4460 );
4461 }
4462
4463 #[test]
4464 fn parse_duration_rejects_tab_byte() {
4465 // `"\t30s"` — the canonical paste-from-indented-doc /
4466 // paste-from-YAML-block-scalar footgun where a tab byte leads
4467 // the magnitude. Pins that the gate covers tab (`0x09`) as well
4468 // as space (`0x20`) — both are `u8::is_ascii_whitespace` members
4469 // and both would be silently swallowed by `s.trim()` pre-gate.
4470 // The `is_ascii_whitespace` coverage extends beyond space alone
4471 // to the full ASCII-whitespace set (space `0x20`, tab `0x09`,
4472 // LF `0x0A`, FF `0x0C`, CR `0x0D`); this test pins the tab arm
4473 // as a representative of the non-space members.
4474 let err = parse_duration("\t30s").unwrap_err();
4475 assert!(
4476 matches!(err, LimitsError::WhitespaceInDuration { ref value, byte } if value == "\t30s" && byte == 0x09),
4477 "got {err:?}"
4478 );
4479 }
4480
4481 #[test]
4482 fn parse_duration_rejects_trailing_newline() {
4483 // `"30s\n"` — the canonical multi-line-paste footgun where a
4484 // trailing LF byte survives the paste. Pins the LF member
4485 // (`0x0A`) of the `is_ascii_whitespace` set as a peer to the
4486 // space and tab pins above — every non-space non-tab whitespace
4487 // byte the WhatWG ASCII-whitespace set covers is refused by
4488 // the same arm.
4489 let err = parse_duration("30s\n").unwrap_err();
4490 assert!(
4491 matches!(err, LimitsError::WhitespaceInDuration { ref value, byte } if value == "30s\n" && byte == 0x0a),
4492 "got {err:?}"
4493 );
4494 }
4495
4496 #[test]
4497 fn parse_duration_accepts_whitespace_free_canonical_forms() {
4498 // The complement-side pin: every canonical whitespace-free
4499 // authoring form the renderer emits stays accepted post-gate.
4500 // Sweep the canonical unit suffixes plus the bare-integer
4501 // shorthand so a future tightening of the whitespace arm that
4502 // over-fires on the accepted set surfaces here as a test
4503 // failure. Peer with the `parse_duration_continues_to_accept_integer_magnitudes`
4504 // pin the fractional / leading-`+` gate carries.
4505 assert_eq!(parse_duration("30s").unwrap(), Duration::from_secs(30));
4506 assert_eq!(parse_duration("500ms").unwrap(), Duration::from_millis(500));
4507 assert_eq!(parse_duration("2m").unwrap(), Duration::from_secs(120));
4508 assert_eq!(parse_duration("1h").unwrap(), Duration::from_secs(3600));
4509 assert_eq!(parse_duration("0s").unwrap(), Duration::ZERO);
4510 assert_eq!(parse_duration("3600").unwrap(), Duration::from_secs(3600));
4511 }
4512
4513 #[test]
4514 fn de_duration_rejects_whitespace_through_serde() {
4515 // The serde-path pin: a `:limits :wall-clock` carrying a
4516 // whitespace-byte-carrying value (`" 30s"`) must fail at
4517 // deserialize time, not silently round-trip the value through
4518 // the pre-existing top-level `s.trim()`. The gate fires at
4519 // deserialize, before any validate gate runs — peer with the
4520 // existing `de_duration_rejects_leading_zero_through_serde` /
4521 // `de_duration_rejects_fractional_value_through_serde` pins on
4522 // the same canonical-form-drift axis.
4523 let json = r#"{"wallClock":" 30s"}"#;
4524 let err = serde_json::from_str::<LimitsSpec>(json).unwrap_err();
4525 let msg = err.to_string();
4526 assert!(
4527 msg.contains("whitespace byte"),
4528 "serde diagnostic must surface the whitespace reason verbatim (got {msg:?})"
4529 );
4530 assert!(
4531 msg.contains("0x20"),
4532 "serde diagnostic must name the offending byte (got {msg:?})"
4533 );
4534
4535 // The whitespace-free complement — same author-side intent,
4536 // written in the canonical form the renderer would emit,
4537 // deserializes cleanly.
4538 let json = r#"{"wallClock":"30s"}"#;
4539 let l: LimitsSpec = serde_json::from_str(json).unwrap();
4540 assert_eq!(l.wall_clock, Some(Duration::from_secs(30)));
4541 }
4542
4543 // ── canonical-form: non-ASCII Unicode `White_Space` duration gate ─────
4544 //
4545 // Successor to the `parse_duration` ASCII-whitespace arm (ebc3a75)
4546 // — closes the strictly-complementary class the byte-scan cannot
4547 // see, through the lifted
4548 // [`crate::render::find_non_ascii_whitespace_char`] predicate.
4549
4550 #[test]
4551 fn parse_duration_rejects_leading_nbsp() {
4552 // NBSP prefix — paste-from-typography footgun. Byte-scan misses,
4553 // `str::trim` strips silently, drifting to `"30s"` on next
4554 // emit.
4555 let s = "\u{00A0}30s";
4556 let err = parse_duration(s).unwrap_err();
4557 assert!(
4558 matches!(err, LimitsError::NonAsciiWhitespaceInDuration { ref value, ch, codepoint } if value == s && ch == '\u{00A0}' && codepoint == 0x00A0),
4559 "got {err:?}"
4560 );
4561 let msg = err.to_string();
4562 assert!(
4563 msg.contains("U+00A0"),
4564 "diagnostic must name codepoint (got {msg:?})"
4565 );
4566 }
4567
4568 #[test]
4569 fn parse_duration_rejects_internal_em_space() {
4570 // EM-SPACE (`\u{2003}`) between magnitude and unit — canonical
4571 // paste-from-typography footgun on the `<integer><unit>` shape.
4572 let s = "30\u{2003}s";
4573 let err = parse_duration(s).unwrap_err();
4574 assert!(
4575 matches!(err, LimitsError::NonAsciiWhitespaceInDuration { ref value, ch, codepoint } if value == s && ch == '\u{2003}' && codepoint == 0x2003),
4576 "got {err:?}"
4577 );
4578 }
4579
4580 #[test]
4581 fn parse_duration_accepts_ascii_only_canonical_forms_after_unicode_arm() {
4582 // Positive-control pin: every ASCII-only canonical form the
4583 // renderer emits stays accepted through the new arm.
4584 assert_eq!(parse_duration("30s").unwrap(), Duration::from_secs(30));
4585 assert_eq!(parse_duration("500ms").unwrap(), Duration::from_millis(500));
4586 assert_eq!(parse_duration("1h").unwrap(), Duration::from_secs(3600));
4587 }
4588
4589 #[test]
4590 fn de_duration_rejects_leading_zero_through_serde() {
4591 // The serde-path pin: a `:limits :wall-clock` carrying a
4592 // leading-zero magnitude (`"030s"`) must fail at deserialize
4593 // time, not silently round-trip the value through the parser.
4594 // The gate fires at deserialize, before any validate gate runs
4595 // — peer with the existing `de_duration_rejects_fractional_value_through_serde`
4596 // pin on the same canonical-form-drift axis.
4597 let json = r#"{"wallClock":"030s"}"#;
4598 let err = serde_json::from_str::<LimitsSpec>(json).unwrap_err();
4599 let msg = err.to_string();
4600 assert!(
4601 msg.contains("leading zero"),
4602 "serde diagnostic must surface the leading-zero reason verbatim (got {msg:?})"
4603 );
4604
4605 let json = r#"{"wallClock":"30s"}"#;
4606 let l: LimitsSpec = serde_json::from_str(json).unwrap();
4607 assert_eq!(l.wall_clock, Some(Duration::from_secs(30)));
4608 }
4609
4610 #[test]
4611 fn de_duration_rejects_fractional_value_through_serde() {
4612 // The serde-path pin: a `:limits :wall-clock` carrying a
4613 // fractional magnitude (`"1.5s"`) must fail at deserialize time,
4614 // not silently round-trip the value through the f64 parser. Pin
4615 // both the success-on-canonical path (the integer form
4616 // deserializes cleanly) and the failure-on-non-canonical path
4617 // (the fractional form is rejected by the codec before any
4618 // validate gate runs).
4619 let json = r#"{"wallClock":"1.5s"}"#;
4620 let err = serde_json::from_str::<LimitsSpec>(json).unwrap_err();
4621 let msg = err.to_string();
4622 assert!(
4623 msg.contains("non-negative integer"),
4624 "serde diagnostic must surface the integer-magnitude reason verbatim \
4625 (got {msg:?})"
4626 );
4627
4628 // The integer-form complement — same author-side intent
4629 // (1.5s = 1500ms), written in the canonical form the renderer
4630 // would emit, deserializes cleanly.
4631 let json = r#"{"wallClock":"1500ms"}"#;
4632 let l: LimitsSpec = serde_json::from_str(json).unwrap();
4633 assert_eq!(l.wall_clock, Some(Duration::from_millis(1500)));
4634 }
4635
4636 #[test]
4637 fn de_byte_size_rejects_fractional_value_through_serde() {
4638 // The serde-path pin: a `:limits :memory` carrying a fractional
4639 // magnitude (`"1.5KiB"`) must fail at deserialize time, not
4640 // silently round-trip the value through the f64 parser. Pin
4641 // both the success-on-canonical path (the integer form
4642 // deserializes cleanly) and the failure-on-non-canonical path
4643 // (the fractional form is rejected by the codec before any
4644 // validate gate runs).
4645 let json = r#"{"memory":"1.5KiB"}"#;
4646 let err = serde_json::from_str::<LimitsSpec>(json).unwrap_err();
4647 let msg = err.to_string();
4648 assert!(
4649 msg.contains("non-negative integer"),
4650 "serde diagnostic must surface the integer-magnitude reason verbatim (got {msg:?})"
4651 );
4652
4653 // The integer-form complement — same author-side intent
4654 // (1.5KiB = 1536 bytes), written in the canonical form the
4655 // renderer would emit, deserializes cleanly.
4656 let json = r#"{"memory":"1536"}"#;
4657 let l: LimitsSpec = serde_json::from_str(json).unwrap();
4658 assert_eq!(l.memory, Some(1536));
4659 }
4660
4661 // ── canonical-form: integer-magnitude millicores codec gate ───────────
4662 //
4663 // Direct successor to the `parse_byte_size` / `parse_duration` /
4664 // shared `supervisor::duration_codec` / `rate_limit_codec`
4665 // integer-magnitude gates on the four peer typed codecs in
4666 // caixa-core — closes the sixth (and last) typed-codec surface in
4667 // the crate. Every magnitude `render_millicores` emits is a
4668 // non-negative integer (`format!("{m}m")`) — no decimal point, no
4669 // leading sign, no scientific notation. The parser's accepted set
4670 // must match for parse → render → parse to round-trip without
4671 // canonical-form drift. Pins every canonical-drift shape —
4672 // leading-`+` (`"+500m"` / `"+2"`, the load-bearing class the
4673 // digit-only gate closes beyond `u32::from_str` strictness),
4674 // leading-`-` (`"-100m"`), fractional (`"1.5"`), decimal-shaped-
4675 // integer on both authoring paths (`"500.0m"` / `"2.0"`), the
4676 // bare-`m`-with-no-magnitude pin, the empty-string pin, the
4677 // garbage-precedence pin (genuinely unparseable inputs keep the
4678 // narrower `BadMillicores` diagnostic), the u32-overflow surface
4679 // pin on both the `m`-suffix and bare-core multiply paths, the
4680 // complement-side pin (every integer happy path the gate must
4681 // continue to accept), the round-trip convergence property, and
4682 // the serde-path pin (the gate fires at deserialize, before any
4683 // validate gate runs).
4684
4685 #[test]
4686 fn parse_millicores_rejects_fractional_magnitude() {
4687 // The fail-before-pass-after pin on the bare-core path:
4688 // `"1.5"` parsed cleanly on no pre-gate codebase (`u32::from_str`
4689 // rejects the decimal), but the diagnostic was value-laundered
4690 // (the bare `BadMillicores("1.5")` wording didn't name the
4691 // canonical-form remediation or the round-trip drift the next
4692 // emit would produce — `1.5 cores × 1000 = 1500 millicores` →
4693 // `"1500m"` on the renderer). The gate routes the same input to
4694 // `NonIntegerMillicoreMagnitude` with the canonical-form wording.
4695 let err = parse_millicores("1.5").unwrap_err();
4696 assert!(
4697 matches!(err, LimitsError::NonIntegerMillicoreMagnitude { ref value } if value == "1.5"),
4698 "got {err:?}"
4699 );
4700 }
4701
4702 #[test]
4703 fn parse_millicores_rejects_decimal_shaped_integer_with_suffix() {
4704 // The canonical-drift case on the `m`-suffix path where the
4705 // *value* is integer but the *form* carries a redundant decimal
4706 // point — `"500.0m"` parses to 500 millicores (integer), but
4707 // the renderer emits `"500m"` on the next serialize (no decimal
4708 // point). The parse-shape gate fires here too so the codec's
4709 // accepted set is exactly the renderer's emitted set — same
4710 // shape as `parse_byte_size`'s `"1.0MiB"` case.
4711 let err = parse_millicores("500.0m").unwrap_err();
4712 assert!(
4713 matches!(err, LimitsError::NonIntegerMillicoreMagnitude { ref value } if value == "500.0"),
4714 "got {err:?}"
4715 );
4716 }
4717
4718 #[test]
4719 fn parse_millicores_rejects_decimal_shaped_integer_bare_core() {
4720 // The decimal-shaped-integer pin on the bare-core path —
4721 // `"2.0"` would be 2000 millicores (the canonical `"2000m"`),
4722 // but the redundant decimal point is not a renderer-emitted
4723 // shape. Surfaces under the same diagnostic as the `m`-suffix
4724 // path so the gate's coverage is uniform across both authoring
4725 // paths.
4726 let err = parse_millicores("2.0").unwrap_err();
4727 assert!(
4728 matches!(err, LimitsError::NonIntegerMillicoreMagnitude { ref value } if value == "2.0"),
4729 "got {err:?}"
4730 );
4731 }
4732
4733 #[test]
4734 fn parse_millicores_rejects_leading_plus_sign_with_suffix() {
4735 // The load-bearing class the digit-only gate closes beyond
4736 // `u32::from_str`'s strictness: current Rust `u32::from_str`
4737 // permissively accepts `"+500"` → 500, so `"+500m"` parsed
4738 // cleanly through the pre-gate codec to `RateLimit`-shaped
4739 // 500 millicores and serde silently round-tripped to `"500m"`
4740 // on the next emit — a *different* canonical string. Same
4741 // shape as `parse_byte_size`'s `"+1024"` (875 commit) and
4742 // `parse_duration`'s `"+30s"` (1027 commit) cases on the peer
4743 // codecs.
4744 let err = parse_millicores("+500m").unwrap_err();
4745 assert!(
4746 matches!(err, LimitsError::NonIntegerMillicoreMagnitude { ref value } if value == "+500"),
4747 "got {err:?}"
4748 );
4749 }
4750
4751 #[test]
4752 fn parse_millicores_rejects_leading_plus_sign_bare_core() {
4753 // The leading-`+` pin on the bare-core path — `"+2"` parsed
4754 // through `u32::from_str` as 2 → 2000 millicores → `"2000m"`
4755 // on the renderer; canonical-drift. The digit-only gate routes
4756 // the same input to `NonIntegerMillicoreMagnitude`, peer with
4757 // the `m`-suffix path.
4758 let err = parse_millicores("+2").unwrap_err();
4759 assert!(
4760 matches!(err, LimitsError::NonIntegerMillicoreMagnitude { ref value } if value == "+2"),
4761 "got {err:?}"
4762 );
4763 }
4764
4765 #[test]
4766 fn parse_millicores_rejects_leading_minus_sign() {
4767 // The negative-magnitude class — pre-gate `u32::from_str`
4768 // rejected negatives but the diagnostic collapsed onto the
4769 // opaque `BadMillicores("-100m")` wording. The digit-only gate
4770 // fires earlier and routes the same input to
4771 // `NonIntegerMillicoreMagnitude` (negatives are not digit-only,
4772 // and `i64::from_str` accepts the leading sign so the numeric
4773 // arm matches). Pin the new diagnostic so a future relaxation
4774 // that re-routes negatives back to the old arm surfaces here.
4775 let err = parse_millicores("-100m").unwrap_err();
4776 assert!(
4777 matches!(err, LimitsError::NonIntegerMillicoreMagnitude { ref value } if value == "-100"),
4778 "got {err:?}"
4779 );
4780 }
4781
4782 #[test]
4783 fn parse_millicores_rejects_empty_string() {
4784 // The empty-input pin — `""` is not a magnitude at all. Pre-
4785 // gate this fell through to `s.parse::<u32>()` and surfaced as
4786 // a generic parse failure with the same `BadMillicores("")`
4787 // wording; the explicit empty-check at the top of the codec
4788 // surfaces the same diagnostic earlier and makes the empty-
4789 // input class structurally distinct from the digit-only /
4790 // numeric / garbage arms below.
4791 let err = parse_millicores("").unwrap_err();
4792 assert!(matches!(err, LimitsError::BadMillicores(_)), "got {err:?}");
4793 }
4794
4795 #[test]
4796 fn parse_millicores_rejects_bare_unit_with_no_magnitude() {
4797 // The bare-`m`-with-no-magnitude pin — `"m"` strips to `""`,
4798 // which is not a magnitude at all. The canonical millicores
4799 // authoring form requires a magnitude in front of the unit
4800 // (`"500m"`, not `"m"`). Surface as `BadMillicores` so the
4801 // narrower-arm wording stays load-bearing for this class.
4802 let err = parse_millicores("m").unwrap_err();
4803 assert!(matches!(err, LimitsError::BadMillicores(_)), "got {err:?}");
4804 }
4805
4806 #[test]
4807 fn parse_millicores_garbage_still_falls_through_to_bad_millicores() {
4808 // The precedence pin: the new `NonIntegerMillicoreMagnitude`
4809 // arm distinguishes *non-canonical-but-numeric* (`"1.5"`,
4810 // `"+500m"`, `"-100m"`, `"500.0m"`) from *genuinely-
4811 // unparseable* (`"abc"`, `"--1m"`, `"foo"`) so the existing
4812 // `BadMillicores` diagnostic's wording remains load-bearing
4813 // for the latter class — the gate is additive, not replacing.
4814 // Pin both arms so a future relaxation that collapses them
4815 // surfaces here.
4816 let err = parse_millicores("abc").unwrap_err();
4817 assert!(matches!(err, LimitsError::BadMillicores(_)), "got {err:?}");
4818 let err = parse_millicores("--1m").unwrap_err();
4819 assert!(matches!(err, LimitsError::BadMillicores(_)), "got {err:?}");
4820 let err = parse_millicores("foo").unwrap_err();
4821 assert!(matches!(err, LimitsError::BadMillicores(_)), "got {err:?}");
4822 }
4823
4824 #[test]
4825 fn parse_millicores_u32_overflow_with_suffix_surfaces_as_overflow() {
4826 // The u32-overflow surface pin on the `m`-suffix path: a
4827 // magnitude exceeding `u32::MAX` (4294967296 = u32::MAX + 1)
4828 // surfaces as `BadMillicores` with an overflow-shaped wording
4829 // naming the offending magnitude verbatim. The digit-only
4830 // guard guarantees every byte is `[0-9]`, so overflow is the
4831 // only remaining `u32::from_str` failure mode — the overflow
4832 // arm is no longer in unreachable-by-prior-gate territory.
4833 // Matches the overflow-arm shape on `parse_byte_size` /
4834 // `parse_duration` / `rate_limit_codec`.
4835 let err = parse_millicores("4294967296m").unwrap_err();
4836 let LimitsError::BadMillicores(reason) = err else {
4837 panic!("expected BadMillicores(overflow), got other variant");
4838 };
4839 assert!(
4840 reason.contains("overflow"),
4841 "overflow diagnostic must mention overflow (got {reason:?})"
4842 );
4843 }
4844
4845 #[test]
4846 fn parse_millicores_bare_core_overflow_surfaces_as_overflow() {
4847 // The u32-overflow surface pin on the bare-core path: a
4848 // magnitude that fits u32 on its own but overflows on the
4849 // `× 1000` conversion to millicores surfaces as
4850 // `BadMillicores` with an overflow-shaped wording. Pre-gate
4851 // the codec used `saturating_mul(1000)` which silently
4852 // saturated the result at `u32::MAX` — landing as the cap
4853 // value far from the author's intent and bypassing any
4854 // future validate-time upper-bound gate the `:cpu` axis
4855 // grows. The `checked_mul` rewrite surfaces the overflow at
4856 // parse time. (4294968 cores × 1000 = 4294968000 > u32::MAX
4857 // = 4294967295 — the smallest digit-string that overflows
4858 // u32 on the × 1000 multiply while fitting u32 on its own.)
4859 let err = parse_millicores("4294968").unwrap_err();
4860 let LimitsError::BadMillicores(reason) = err else {
4861 panic!("expected BadMillicores(× 1000 overflow), got other variant");
4862 };
4863 assert!(
4864 reason.contains("overflow"),
4865 "× 1000 overflow diagnostic must mention overflow (got {reason:?})"
4866 );
4867 }
4868
4869 #[test]
4870 fn parse_millicores_continues_to_accept_canonical_forms() {
4871 // The complement-side pin: every canonical integer-magnitude
4872 // form the renderer emits must continue to parse to the same
4873 // value the renderer produced. Sweep the canonical authoring
4874 // shapes on both paths (the `m`-suffix path: `"0m"`, `"500m"`,
4875 // `"2000m"`; the bare-core shorthand: `"0"`, `"2"`, `"4"`) so
4876 // a future tightening of the parser surfaces here as a test
4877 // failure rather than a silent regression. The `0` case is at
4878 // the codec layer only; `validate_rejects_zero_cpu` rejects
4879 // `Some(0)` one level up.
4880 assert_eq!(parse_millicores("0m").unwrap(), 0);
4881 assert_eq!(parse_millicores("500m").unwrap(), 500);
4882 assert_eq!(parse_millicores("1500m").unwrap(), 1500);
4883 assert_eq!(parse_millicores("2000m").unwrap(), 2000);
4884 assert_eq!(parse_millicores("0").unwrap(), 0);
4885 assert_eq!(parse_millicores("2").unwrap(), 2000);
4886 assert_eq!(parse_millicores("4").unwrap(), 4000);
4887 }
4888
4889 #[test]
4890 fn parse_millicores_round_trips_through_render_for_every_canonical_form() {
4891 // The structural property the gate makes load-bearing: every
4892 // value the parser accepts round-trips through
4893 // `render_millicores` to a string the parser also accepts —
4894 // and to the *same* value. Sweep the values the renderer emits
4895 // canonically (zero, sub-core, single-core boundary, multi-
4896 // core, and a non-1000-multiple millicore value) so a future
4897 // codec change that breaks round-trip convergence surfaces
4898 // here, not at a downstream renderer that double-emits a
4899 // typed slot.
4900 for m in [0u32, 1, 100, 500, 1000, 1500, 2000, 12345] {
4901 let rendered = render_millicores(m);
4902 let reparsed = parse_millicores(&rendered)
4903 .unwrap_or_else(|e| panic!("render({m}) = {rendered:?} must reparse, got {e:?}"));
4904 assert_eq!(
4905 reparsed, m,
4906 "round-trip drift on {m}: rendered={rendered:?}, reparsed={reparsed}",
4907 );
4908 }
4909 }
4910
4911 #[test]
4912 fn de_millicores_rejects_leading_plus_through_serde() {
4913 // The serde-path pin: a `:limits :cpu` carrying a leading-`+`
4914 // magnitude (`"+500m"`) must fail at deserialize time, not
4915 // silently round-trip the value through `u32::from_str`'s
4916 // permissive sign-acceptance. Pin both the success-on-canonical
4917 // path (the integer form deserializes cleanly) and the
4918 // failure-on-non-canonical path (the leading-`+` form is
4919 // rejected by the codec before any validate gate runs).
4920 let json = r#"{"cpu":"+500m"}"#;
4921 let err = serde_json::from_str::<LimitsSpec>(json).unwrap_err();
4922 let msg = err.to_string();
4923 assert!(
4924 msg.contains("non-negative integer"),
4925 "serde diagnostic must surface the integer-magnitude reason verbatim \
4926 (got {msg:?})"
4927 );
4928
4929 // The integer-form complement — same author-side intent
4930 // (500 millicores), written in the canonical form the renderer
4931 // would emit, deserializes cleanly.
4932 let json = r#"{"cpu":"500m"}"#;
4933 let l: LimitsSpec = serde_json::from_str(json).unwrap();
4934 assert_eq!(l.cpu, Some(500));
4935 }
4936
4937 // ── canonical-form: leading-zero millicores codec gate ────────────────
4938 //
4939 // Direct successor to the `parse_byte_size` / `parse_duration` /
4940 // `supervisor::duration_codec` / `rate_limit_codec` leading-zero
4941 // arms (cea9a78 / 39762d7 / 9178904 / 4f46830) — closes the sixth
4942 // (and last) typed numeric-codec surface in caixa-core on the
4943 // integer-magnitude leading-zero axis. Every magnitude
4944 // `render_millicores` emits is the leading-zero-stripped form
4945 // (`format!("{m}m")` — no leading-zero padding), so a digit-only-
4946 // but-leading-zero magnitude parses losslessly through `u32::from_str`
4947 // and serde silently round-trips the value to a *different*
4948 // canonical string on the next emit. Pins every canonical-drift
4949 // shape on the `m`-suffix and bare-core paths, the codec-vs-
4950 // typed-validate-layer boundary (the single-byte `"0"` stays in the
4951 // codec's accepted set; `CpuZero` refuses it at validate), the
4952 // complement-side pin (every canonical leading-`[1-9]` magnitude
4953 // continues to parse cleanly), and the serde-path pin.
4954
4955 #[test]
4956 fn parse_millicores_rejects_leading_zero_magnitude_with_suffix() {
4957 // The fail-before-pass-after pin on the `m`-suffix path:
4958 // `"0500m"` parsed cleanly on no pre-gate codebase
4959 // (`u32::from_str` accepts `"0500"` → 500), then `render_millicores`
4960 // emitted `"500m"` on the next serialize — canonical-form drift.
4961 // The leading-zero arm routes the same input to
4962 // `LeadingZeroMillicoreMagnitude` with the canonical-form
4963 // remediation wording. Peer with the `parse_byte_size` `"064MiB"`
4964 // case and the `parse_duration` `"030s"` case.
4965 let err = parse_millicores("0500m").unwrap_err();
4966 assert!(
4967 matches!(err, LimitsError::LeadingZeroMillicoreMagnitude { ref value } if value == "0500"),
4968 "got {err:?}"
4969 );
4970 }
4971
4972 #[test]
4973 fn parse_millicores_rejects_multi_digit_zero_magnitude_with_suffix() {
4974 // The multi-zero pin on the `m`-suffix path: `"00m"` parses to 0
4975 // millicores at the codec, but the renderer emits `"0m"` on the
4976 // next serialize — the single canonical zero form on this axis.
4977 // The leading-zero arm rejects multi-byte leading-zero shapes
4978 // even when the value is zero; the single-byte `"0m"` /
4979 // bare-`"0"` stays in the codec's accepted set per the boundary
4980 // pin below. Peer with the `parse_byte_size` `"00MiB"` case and
4981 // the `parse_duration` `"00s"` case.
4982 let err = parse_millicores("00m").unwrap_err();
4983 assert!(
4984 matches!(err, LimitsError::LeadingZeroMillicoreMagnitude { ref value } if value == "00"),
4985 "got {err:?}"
4986 );
4987 }
4988
4989 #[test]
4990 fn parse_millicores_rejects_leading_zero_bare_core() {
4991 // The leading-zero pin on the bare-core path: `"02"` parsed to
4992 // 2 cores → 2000 millicores at the codec, but `render_millicores`
4993 // emits `"2000m"` on the next serialize — canonical-form drift.
4994 // The bare-core shorthand carries the same leading-zero discipline
4995 // as the `m`-suffix path; both authoring paths converge to the
4996 // same gate. Peer with the `parse_byte_size` bare-integer
4997 // `"01024"` case.
4998 let err = parse_millicores("02").unwrap_err();
4999 assert!(
5000 matches!(err, LimitsError::LeadingZeroMillicoreMagnitude { ref value } if value == "02"),
5001 "got {err:?}"
5002 );
5003 }
5004
5005 #[test]
5006 fn parse_millicores_rejects_leading_zero_multi_digit_with_suffix() {
5007 // The multi-digit leading-zero pin on the `m`-suffix path:
5008 // `"01500m"` parses to 1500 millicores at the codec, but the
5009 // renderer emits `"1500m"` on the next serialize — canonical-form
5010 // drift on a non-zero magnitude. Sweeps a different magnitude
5011 // shape than the `"0500m"` case so a future tightening that
5012 // misses the multi-digit-leading-zero class surfaces here.
5013 let err = parse_millicores("01500m").unwrap_err();
5014 assert!(
5015 matches!(err, LimitsError::LeadingZeroMillicoreMagnitude { ref value } if value == "01500"),
5016 "got {err:?}"
5017 );
5018 }
5019
5020 #[test]
5021 fn parse_millicores_accepts_single_zero_magnitude_at_codec_layer() {
5022 // The codec-layer / typed-validate-layer boundary pin: the
5023 // single-byte magnitude `"0"` (bare) and `"0m"` (with suffix)
5024 // round-trip losslessly through `render_millicores` (which
5025 // emits `"0m"` for 0 millicores), so they stay in the codec's
5026 // accepted set. The downstream `CpuZero` gate refuses
5027 // semantic-zero authoring at the typed-validate layer above —
5028 // the diagnostic partitioning between canonical-form drift
5029 // (the leading-zero arm) and semantic-zero (the `CpuZero` gate)
5030 // remains stable. Same codec-layer / typed-validate-layer
5031 // partition the peer codecs preserve.
5032 assert_eq!(parse_millicores("0").unwrap(), 0);
5033 assert_eq!(parse_millicores("0m").unwrap(), 0);
5034 }
5035
5036 #[test]
5037 fn parse_millicores_accepts_canonical_magnitude_with_leading_one() {
5038 // The complement-side pin: every canonical leading-`[1-9]`
5039 // magnitude continues to parse cleanly through the leading-zero
5040 // arm, on both the `m`-suffix and bare-core paths. Sweep the
5041 // canonical values the renderer emits across the unit-multiplier
5042 // boundary (sub-core, single-core, multi-core) so a future
5043 // tightening cannot drift into rejecting valid canonical
5044 // magnitudes. Same complement-side discipline the peer
5045 // `parse_byte_size_accepts_canonical_magnitude_with_leading_one`
5046 // and `parse_duration_accepts_canonical_magnitude_with_leading_one`
5047 // pins enforce on the sibling codecs.
5048 assert_eq!(parse_millicores("1m").unwrap(), 1);
5049 assert_eq!(parse_millicores("500m").unwrap(), 500);
5050 assert_eq!(parse_millicores("1500m").unwrap(), 1500);
5051 assert_eq!(parse_millicores("9000m").unwrap(), 9000);
5052 assert_eq!(parse_millicores("1").unwrap(), 1000);
5053 assert_eq!(parse_millicores("2").unwrap(), 2000);
5054 assert_eq!(parse_millicores("9").unwrap(), 9000);
5055 }
5056
5057 #[test]
5058 fn de_millicores_rejects_leading_zero_through_serde() {
5059 // The serde-path pin: a `:limits :cpu` carrying a leading-zero
5060 // magnitude (`"0500m"`) must fail at deserialize time, not
5061 // silently round-trip the value through `u32::from_str`'s
5062 // leading-zero-permissive accepting. Pin both the success-on-
5063 // canonical path (the leading-zero-stripped form deserializes
5064 // cleanly) and the failure-on-non-canonical path (the leading-
5065 // zero form is rejected by the codec before any validate gate
5066 // runs). Peer with the
5067 // `de_byte_size_rejects_leading_zero_through_serde` and
5068 // `de_duration_rejects_leading_zero_through_serde` pins on the
5069 // sibling codecs.
5070 let json = r#"{"cpu":"0500m"}"#;
5071 let err = serde_json::from_str::<LimitsSpec>(json).unwrap_err();
5072 let msg = err.to_string();
5073 assert!(
5074 msg.contains("leading zero"),
5075 "serde diagnostic must surface the leading-zero reason verbatim \
5076 (got {msg:?})"
5077 );
5078
5079 // The integer-form complement — same author-side intent
5080 // (500 millicores), written in the canonical form the renderer
5081 // would emit, deserializes cleanly.
5082 let json = r#"{"cpu":"500m"}"#;
5083 let l: LimitsSpec = serde_json::from_str(json).unwrap();
5084 assert_eq!(l.cpu, Some(500));
5085 }
5086
5087 // ── canonical-form: whitespace-rejection millicores codec gate ────────
5088 //
5089 // Direct successor to the `parse_byte_size` (24a8ad4), `parse_duration`
5090 // (ebc3a75), `supervisor::duration_codec` (a7ae622), and
5091 // `rate_limit_codec` (1ad7755) whitespace-rejection arms — closes the
5092 // fifth (and last) typed-magnitude codec surface in caixa-core on the
5093 // ASCII-whitespace axis. The pre-gate top-level `s.trim()` at parse
5094 // entry and the per-part `magnitude.trim()` calls silently ate leading
5095 // / trailing / internal whitespace, so every whitespace-carrying shape
5096 // parsed to the same millicore value and round-tripped through
5097 // `render_millicores` to a *different* canonical string on next
5098 // serialize — the same canonical-form-drift class the leading-`+` /
5099 // fractional / leading-zero arms already close on this codec.
5100
5101 #[test]
5102 fn parse_millicores_rejects_leading_whitespace() {
5103 // `" 500m"` — the canonical paste-from-aligned-doc / YAML-quoted-
5104 // plain-scalar footgun. Before this gate the top-level `s.trim()`
5105 // at parse entry silently ate the leading space and parsed the
5106 // value to 500 millicores, round-tripping to `"500m"` on next
5107 // serialize.
5108 let err = parse_millicores(" 500m").unwrap_err();
5109 assert!(
5110 matches!(err, LimitsError::WhitespaceInMillicores { ref value, byte } if value == " 500m" && byte == 0x20),
5111 "got {err:?}"
5112 );
5113 let msg = err.to_string();
5114 assert!(
5115 msg.contains("whitespace byte 0x20"),
5116 "diagnostic must surface the offending byte verbatim (got {msg:?})"
5117 );
5118 assert!(
5119 msg.contains("THEORY.md"),
5120 "diagnostic must cite the render-determinism contract (got {msg:?})"
5121 );
5122 }
5123
5124 #[test]
5125 fn parse_millicores_rejects_trailing_whitespace() {
5126 // `"500m "` — the canonical shell-history trailing-space footgun.
5127 let err = parse_millicores("500m ").unwrap_err();
5128 assert!(
5129 matches!(err, LimitsError::WhitespaceInMillicores { ref value, byte } if value == "500m " && byte == 0x20),
5130 "got {err:?}"
5131 );
5132 }
5133
5134 #[test]
5135 fn parse_millicores_rejects_internal_whitespace_between_magnitude_and_unit() {
5136 // `"500 m"` — the typographically-spaced author shape (the same
5137 // idiom every prose reference to millicores renders as). Before
5138 // this gate the per-part `magnitude.trim()` silently ate the
5139 // internal space and parsed the value to 500 millicores.
5140 let err = parse_millicores("500 m").unwrap_err();
5141 assert!(
5142 matches!(err, LimitsError::WhitespaceInMillicores { ref value, byte } if value == "500 m" && byte == 0x20),
5143 "got {err:?}"
5144 );
5145 }
5146
5147 #[test]
5148 fn parse_millicores_rejects_tab_byte() {
5149 // `"\t500m"` — the paste-from-indented-doc / YAML-block-scalar tab
5150 // footgun. Pins the tab (`0x09`) arm alongside the space arm above.
5151 let err = parse_millicores("\t500m").unwrap_err();
5152 assert!(
5153 matches!(err, LimitsError::WhitespaceInMillicores { ref value, byte } if value == "\t500m" && byte == 0x09),
5154 "got {err:?}"
5155 );
5156 }
5157
5158 #[test]
5159 fn parse_millicores_rejects_trailing_newline() {
5160 // `"500m\n"` — the multi-line-paste footgun where a trailing LF
5161 // byte survives the paste. Pins the LF member (`0x0A`) of the
5162 // `is_ascii_whitespace` set.
5163 let err = parse_millicores("500m\n").unwrap_err();
5164 assert!(
5165 matches!(err, LimitsError::WhitespaceInMillicores { ref value, byte } if value == "500m\n" && byte == 0x0a),
5166 "got {err:?}"
5167 );
5168 }
5169
5170 #[test]
5171 fn parse_millicores_accepts_whitespace_free_canonical_forms() {
5172 // The complement-side pin: every canonical whitespace-free
5173 // authoring form the renderer emits stays accepted post-gate.
5174 // Sweep the canonical `m`-suffix path plus the bare-core shorthand
5175 // so a future tightening of the whitespace arm that over-fires on
5176 // the accepted set surfaces here as a test failure.
5177 assert_eq!(parse_millicores("500m").unwrap(), 500);
5178 assert_eq!(parse_millicores("2000m").unwrap(), 2000);
5179 assert_eq!(parse_millicores("1m").unwrap(), 1);
5180 assert_eq!(parse_millicores("0m").unwrap(), 0);
5181 assert_eq!(parse_millicores("2").unwrap(), 2000);
5182 assert_eq!(parse_millicores("0").unwrap(), 0);
5183 }
5184
5185 #[test]
5186 fn de_millicores_rejects_whitespace_through_serde() {
5187 // The serde-path pin: a `:limits :cpu` carrying a whitespace-byte-
5188 // carrying value (`" 500m"`) must fail at deserialize time, not
5189 // silently round-trip the value through the pre-existing top-level
5190 // `s.trim()`. Peer with the
5191 // `de_byte_size_rejects_whitespace_through_serde` and
5192 // `de_duration_rejects_whitespace_through_serde` pins on the
5193 // sibling codecs.
5194 let json = r#"{"cpu":" 500m"}"#;
5195 let err = serde_json::from_str::<LimitsSpec>(json).unwrap_err();
5196 let msg = err.to_string();
5197 assert!(
5198 msg.contains("whitespace byte"),
5199 "serde diagnostic must surface the whitespace reason verbatim (got {msg:?})"
5200 );
5201 assert!(
5202 msg.contains("0x20"),
5203 "serde diagnostic must name the offending byte (got {msg:?})"
5204 );
5205
5206 // The whitespace-free complement — same author-side intent,
5207 // written in the canonical form the renderer would emit,
5208 // deserializes cleanly.
5209 let json = r#"{"cpu":"500m"}"#;
5210 let l: LimitsSpec = serde_json::from_str(json).unwrap();
5211 assert_eq!(l.cpu, Some(500));
5212 }
5213
5214 // ── canonical-form: non-ASCII Unicode `White_Space` millicores gate ───
5215 //
5216 // Direct successor to the ASCII-whitespace arm above — closes the
5217 // strictly-complementary class the byte-scan cannot see. `str::trim`
5218 // uses `char::is_whitespace` (Unicode `White_Space`, strictly wider
5219 // than the ASCII byte set); a leading / trailing / internal NBSP
5220 // (`\u{00A0}`) / LINE SEPARATOR (`\u{2028}`) / EM-SPACE (`\u{2003}`)
5221 // survives the byte-scan but is silently stripped by the top-level
5222 // trim, drifting to canonical `"500m"` on round-trip. Pins the arm
5223 // through the lifted [`crate::render::find_non_ascii_whitespace_char`]
5224 // predicate — the same shared predicate 1b75b38 landed on the four
5225 // peer typed-magnitude codecs, extended here to the fifth.
5226
5227 #[test]
5228 fn parse_millicores_rejects_leading_nbsp() {
5229 // NBSP (`\u{00A0}` = UTF-8 `0xC2 0xA0`) — the paste-from-typography
5230 // / paste-from-word-processor footgun. Before this arm landed the
5231 // byte-scan missed it (neither `0xC2` nor `0xA0` is
5232 // `is_ascii_whitespace`) and `str::trim` at parse entry silently
5233 // stripped it, yielding the same 500 millicores as the whitespace-
5234 // free canonical form and drifting to `"500m"` on next serialize.
5235 let s = "\u{00A0}500m";
5236 let err = parse_millicores(s).unwrap_err();
5237 assert!(
5238 matches!(err, LimitsError::NonAsciiWhitespaceInMillicores { ref value, ch, codepoint } if value == s && ch == '\u{00A0}' && codepoint == 0x00A0),
5239 "got {err:?}"
5240 );
5241 let msg = err.to_string();
5242 assert!(
5243 msg.contains("U+00A0"),
5244 "diagnostic must surface the codepoint verbatim (got {msg:?})"
5245 );
5246 assert!(
5247 msg.contains("THEORY.md"),
5248 "diagnostic must cite the render-determinism contract (got {msg:?})"
5249 );
5250 }
5251
5252 #[test]
5253 fn parse_millicores_rejects_internal_em_space() {
5254 // EM-SPACE (`\u{2003}`) between magnitude and unit — pins the arm
5255 // on an internal-position non-NBSP Unicode `White_Space` member.
5256 let s = "500\u{2003}m";
5257 let err = parse_millicores(s).unwrap_err();
5258 assert!(
5259 matches!(err, LimitsError::NonAsciiWhitespaceInMillicores { ref value, ch, codepoint } if value == s && ch == '\u{2003}' && codepoint == 0x2003),
5260 "got {err:?}"
5261 );
5262 }
5263
5264 #[test]
5265 fn parse_millicores_rejects_trailing_line_separator() {
5266 // LINE SEPARATOR (`\u{2028}`) — the canonical paste-from-web-doc
5267 // footgun (many rendering engines insert `\u{2028}` at soft-wrap
5268 // boundaries in RTF/HTML → plain text conversion). Pins the arm on
5269 // a trailing-position Unicode `White_Space` member.
5270 let s = "500m\u{2028}";
5271 let err = parse_millicores(s).unwrap_err();
5272 assert!(
5273 matches!(err, LimitsError::NonAsciiWhitespaceInMillicores { ref value, ch, codepoint } if value == s && ch == '\u{2028}' && codepoint == 0x2028),
5274 "got {err:?}"
5275 );
5276 }
5277
5278 #[test]
5279 fn parse_millicores_accepts_ascii_only_canonical_forms_after_unicode_arm() {
5280 // Positive-control pin: every ASCII-only canonical form the
5281 // renderer emits stays accepted through the new arm — the lifted
5282 // predicate is a strict no-op on ASCII input.
5283 assert_eq!(parse_millicores("500m").unwrap(), 500);
5284 assert_eq!(parse_millicores("2000m").unwrap(), 2000);
5285 assert_eq!(parse_millicores("1m").unwrap(), 1);
5286 assert_eq!(parse_millicores("2").unwrap(), 2000);
5287 }
5288
5289 // ── canonical-form: integer-millisecond :wall-clock gate ──────────────
5290 //
5291 // The peer typed-`Duration` axes routed through
5292 // `supervisor::duration_codec` (`:politicas :timeout` a4ae535,
5293 // `:circuit-breaker :window` a4ae535) already gate on
5294 // `is_integer_millisecond_duration` because the codec's `render`
5295 // truncates to `as_millis()` and parses with integer-ms granularity;
5296 // this crate's in-module `render_duration` / `parse_duration` pair
5297 // carries the same `as_millis()`-truncation shape, so the same sub-
5298 // millisecond-residue footgun lived on this axis until this gate
5299 // landed. The tests below pin the fail-before-pass-after boundary,
5300 // the diagnostic shape, the cross-arm zero-then-canonical ordering
5301 // matching the `:politicas` peer, the integer-ms happy-path sweep,
5302 // and the codec round-trip property (every validated `wall_clock`
5303 // survives serialize → deserialize equality).
5304
5305 #[test]
5306 fn validate_rejects_sub_millisecond_wall_clock() {
5307 // The fail-before-pass-after pin: a programmatic
5308 // `Duration::from_micros(1500)` (= 1_500_000 ns) silently passed
5309 // validate on every pre-gate codebase, then truncated to
5310 // `as_millis() == 1` on first serialize — `render_duration`
5311 // emits `"1ms"`, the codec parses it back to
5312 // `Duration::from_millis(1)` = 1_000_000 ns, the typed
5313 // `wall_clock` no longer matches its rendered form.
5314 let l = LimitsSpec {
5315 wall_clock: Some(Duration::from_micros(1500)),
5316 ..Default::default()
5317 };
5318 match l.validate().unwrap_err() {
5319 LimitsError::WallClockNotCanonical { wall_clock } => {
5320 assert_eq!(wall_clock, Duration::from_micros(1500));
5321 }
5322 other => panic!("expected WallClockNotCanonical, got {other:?}"),
5323 }
5324 }
5325
5326 #[test]
5327 fn validate_rejects_one_nanosecond_wall_clock() {
5328 // The far-sub-ms case: `Duration::from_nanos(1)` is non-zero
5329 // (so `WallClockZero` doesn't fire) but `as_millis() == 0`, so
5330 // `render_duration` emits the literal `"0s"` — the next serde
5331 // round-trip would parse back to `Duration::ZERO`, which the
5332 // `WallClockZero` arm then rejects on re-validate. The
5333 // canonical-form gate at this layer surfaces a self-locating
5334 // diagnostic naming the offending Duration verbatim rather
5335 // than a downstream `WallClockZero` whose remediation points
5336 // at omitting the slot.
5337 let l = LimitsSpec {
5338 wall_clock: Some(Duration::from_nanos(1)),
5339 ..Default::default()
5340 };
5341 match l.validate().unwrap_err() {
5342 LimitsError::WallClockNotCanonical { wall_clock } => {
5343 assert_eq!(wall_clock, Duration::from_nanos(1));
5344 }
5345 other => panic!("expected WallClockNotCanonical, got {other:?}"),
5346 }
5347 }
5348
5349 #[test]
5350 fn validate_rejects_nanosecond_past_canonical_boundary() {
5351 // The 1-ns-past-1ms boundary case: a `Duration` carrying
5352 // 1_000_001 ns is structurally past the integer-ms granularity
5353 // floor — `subsec_nanos() % 1_000_000 == 1`. The codec
5354 // round-trip would truncate to `1ms` and the consumer would
5355 // observe a 1-ns drift on every emit. Same boundary the peer
5356 // `is_integer_millisecond_duration_predicate_tracks_codec` test
5357 // in aplicacao.rs pins for the `:politicas` axes.
5358 let w = Duration::from_nanos(1_000_001);
5359 let l = LimitsSpec {
5360 wall_clock: Some(w),
5361 ..Default::default()
5362 };
5363 assert_eq!(
5364 l.validate().unwrap_err(),
5365 LimitsError::WallClockNotCanonical { wall_clock: w }
5366 );
5367 }
5368
5369 #[test]
5370 fn validate_accepts_integer_millisecond_wall_clock_values() {
5371 // The positive-control sweep: every `Duration` the codec can
5372 // round-trip losslessly — the canonical `<integer>{ms,s,m,h}`
5373 // set the `render_duration` / `parse_duration` pair emits and
5374 // accepts — passes `validate` without surfacing the new
5375 // canonical-form arm. Mirrors
5376 // `accepts_policy_retries_typical_values` /
5377 // `accepts_circuit_breaker_max_failures_typical_values` on
5378 // sibling axes.
5379 for w in [
5380 Duration::from_millis(1),
5381 Duration::from_millis(500),
5382 Duration::from_millis(1500),
5383 Duration::from_secs(1),
5384 Duration::from_secs(30),
5385 Duration::from_secs(60),
5386 Duration::from_secs(120),
5387 Duration::from_secs(3600),
5388 ] {
5389 let l = LimitsSpec {
5390 wall_clock: Some(w),
5391 ..Default::default()
5392 };
5393 l.validate()
5394 .unwrap_or_else(|e| panic!("integer-ms {w:?} must validate, got {e:?}"));
5395 }
5396 }
5397
5398 #[test]
5399 fn validate_wall_clock_zero_takes_precedence_over_canonical_gate() {
5400 // Cross-arm ordering pin: `Duration::ZERO` has
5401 // `subsec_nanos() == 0` and would otherwise pass the
5402 // canonical-form arm — the zero-floor arm must fire first so
5403 // the more self-locating `WallClockZero` diagnostic (with its
5404 // omit-axis remediation directly named) leads. Same posture
5405 // every peer zero-then-shape gate uses
5406 // (`PolicyTimeoutZero` → `PolicyTimeoutNotCanonical`,
5407 // `PolicyBreakerZeroWindow` → `PolicyBreakerWindowNotCanonical`).
5408 let l = LimitsSpec {
5409 wall_clock: Some(Duration::ZERO),
5410 ..Default::default()
5411 };
5412 assert_eq!(l.validate().unwrap_err(), LimitsError::WallClockZero);
5413 }
5414
5415 #[test]
5416 fn wall_clock_canonical_diagnostic_carries_offending_duration() {
5417 // Diagnostic-shape pin: the canonical-form arm names the
5418 // offending `Duration` verbatim so the author's grep lands on
5419 // the field's value, not a generic "duration not canonical"
5420 // message. Same shape every other typed-cap arm on this
5421 // surface carries (`MemoryExceedsWasm32Cap` carries the
5422 // offending byte count verbatim, `PolicyRetriesExceedsCap`
5423 // carries the offending retry count verbatim,
5424 // `PolicyBreakerMaxFailuresExceedsCap` carries the offending
5425 // u32 verbatim).
5426 let w = Duration::from_micros(500);
5427 let l = LimitsSpec {
5428 wall_clock: Some(w),
5429 ..Default::default()
5430 };
5431 let err = l.validate().unwrap_err();
5432 let msg = err.to_string();
5433 assert!(
5434 msg.contains("500"),
5435 "diagnostic must carry the offending magnitude verbatim (got {msg:?})"
5436 );
5437 }
5438
5439 #[test]
5440 fn wall_clock_validated_value_round_trips_through_codec() {
5441 // The structural property the canonical-ms gate enforces:
5442 // every `LimitsSpec::wall_clock` past `LimitsSpec::validate`
5443 // round-trips losslessly through the in-module duration codec
5444 // (serialize → string → deserialize → equal value). Pin this
5445 // end-to-end so a future change to either side (the validate
5446 // gate's accepted granularity, the codec's parse/render unit
5447 // set) that breaks the alignment surfaces here. Peer of
5448 // `policy_timeout_validated_value_round_trips_through_codec` /
5449 // `circuit_breaker_window_validated_value_round_trips_through_codec`
5450 // on the sibling `:politicas` axes.
5451 for w in [
5452 Duration::from_millis(1),
5453 Duration::from_millis(1500),
5454 Duration::from_secs(30),
5455 Duration::from_secs(3600),
5456 ] {
5457 let l = LimitsSpec {
5458 wall_clock: Some(w),
5459 ..Default::default()
5460 };
5461 l.validate().unwrap();
5462 let json = serde_json::to_string(&l).unwrap();
5463 let back: LimitsSpec = serde_json::from_str(&json).unwrap();
5464 assert_eq!(
5465 back.wall_clock, l.wall_clock,
5466 "every validated :wall-clock must round-trip losslessly through the codec"
5467 );
5468 }
5469 }
5470
5471 // ── value-shape: :wall-clock upper bound — 1h ceiling ──────────────────
5472 //
5473 // The third typed-`Duration` axis brought to the uniform top edge
5474 // `LIMITS_WALL_CLOCK_MAX` = 1h established by the prior cap lifts
5475 // on `:politicas :timeout` (POLICY_TIMEOUT_MAX) and
5476 // `:politicas :circuit-breaker :window` (POLICY_BREAKER_WINDOW_MAX).
5477 // Mirrors the test discipline those peers carry: the
5478 // fail-before-pass-after pin, the 1ms-boundary pin, the
5479 // far-above-cap sweep (24h / 7d / ~11.5d — the values a
5480 // `(:wall-clock "24h")` typo or copy-paste typically lands), the
5481 // inclusive-at-cap positive control, the production-band positive-
5482 // control sweep, the cross-arm zero-then-cap and
5483 // canonical-then-cap ordering pins, the diagnostic-shape pin
5484 // carrying the offending `Duration` verbatim, and the cap-value
5485 // literal-identity + codec-round-trip pins anchoring the constant
5486 // to the codec's largest emitted unit and to its peer constants.
5487
5488 #[test]
5489 fn validate_rejects_wall_clock_above_cap() {
5490 // The fail-before-pass-after pin: 3601s = 1h + 1s is
5491 // structurally one canonical-tick past the
5492 // [`LIMITS_WALL_CLOCK_MAX`] ceiling (1h = 3600s) — an
5493 // integer-millisecond magnitude the canonical-form arm above
5494 // accepts cleanly, that the in-module duration codec
5495 // round-trips losslessly as `"3601s"`, and that silently
5496 // passed validate on every pre-gate codebase because the typed
5497 // slot's only checks were the zero-floor and canonical-form
5498 // arms. The wasm-engine consuming the value (the M2.5
5499 // `wasm-engine`'s epoch-deadline cancellation hook, the future
5500 // caixa-helm `pleme-computeunit` chart's `:limits` value
5501 // mapping) reaches for a `Duration` so long no realistic
5502 // synchronous wasm call hits it, far from the source
5503 // caixa.lisp.
5504 let w = LIMITS_WALL_CLOCK_MAX + Duration::from_secs(1);
5505 let l = LimitsSpec {
5506 wall_clock: Some(w),
5507 ..Default::default()
5508 };
5509 assert_eq!(
5510 l.validate().unwrap_err(),
5511 LimitsError::WallClockExceedsCap { wall_clock: w }
5512 );
5513 }
5514
5515 #[test]
5516 fn validate_rejects_wall_clock_one_millisecond_above_cap() {
5517 // Boundary case: exactly 1ms past the cap (the granularity the
5518 // canonical-form gate enforces). Catches a future "strictly
5519 // less than" half-measure and pins the diagnostic to name the
5520 // offending `Duration` verbatim. Peer of
5521 // `rejects_policy_timeout_one_millisecond_above_cap` /
5522 // `rejects_circuit_breaker_window_one_millisecond_above_cap`
5523 // on the sibling typed-`Duration` axes' top edges.
5524 let w = LIMITS_WALL_CLOCK_MAX + Duration::from_millis(1);
5525 let l = LimitsSpec {
5526 wall_clock: Some(w),
5527 ..Default::default()
5528 };
5529 assert_eq!(
5530 l.validate().unwrap_err(),
5531 LimitsError::WallClockExceedsCap { wall_clock: w }
5532 );
5533 }
5534
5535 #[test]
5536 fn validate_rejects_wall_clock_far_above_cap() {
5537 // The "obvious authoring footgun" case: a `(:wall-clock "24h")`
5538 // or `(:wall-clock "7d")` — values the canonical-form arm
5539 // accepts as integer-millisecond magnitudes, the codec
5540 // round-trips losslessly through serde, but the wasm-engine
5541 // cannot honor as a meaningful per-call deadline. Until this
5542 // gate landed validate accepted them. Pin the common
5543 // above-cap values (24h, 7d, ~11.5d) so a future relaxation
5544 // that drops the upper bound surfaces here.
5545 for w in [
5546 Duration::from_secs(86_400), // 24h
5547 Duration::from_secs(604_800), // 7d
5548 Duration::from_secs(1_000_000), // ~11.5 days
5549 ] {
5550 let l = LimitsSpec {
5551 wall_clock: Some(w),
5552 ..Default::default()
5553 };
5554 assert_eq!(
5555 l.validate().unwrap_err(),
5556 LimitsError::WallClockExceedsCap { wall_clock: w }
5557 );
5558 }
5559 }
5560
5561 #[test]
5562 fn validate_accepts_wall_clock_at_cap() {
5563 // The boundary value — exactly [`LIMITS_WALL_CLOCK_MAX`] (1h)
5564 // — must validate. The cap is inclusive on the top edge,
5565 // matching the [`crate::POLICY_TIMEOUT_MAX`] /
5566 // [`crate::POLICY_BREAKER_WINDOW_MAX`] /
5567 // [`LIMITS_MEMORY_WASM32_MAX_BYTES`] discipline on the sibling
5568 // capped axes. Pin the boundary explicitly so a future
5569 // off-by-one tightening (`>= LIMITS_WALL_CLOCK_MAX` instead of
5570 // `>`) surfaces here as a test failure rather than a silent
5571 // contract narrowing.
5572 let l = LimitsSpec {
5573 wall_clock: Some(LIMITS_WALL_CLOCK_MAX),
5574 ..Default::default()
5575 };
5576 l.validate()
5577 .expect("wall_clock == LIMITS_WALL_CLOCK_MAX must validate");
5578 }
5579
5580 #[test]
5581 fn validate_accepts_wall_clock_typical_values() {
5582 // The documented per-request production-playbook band positive-
5583 // control sweep — every value Envoy / Istio / Linkerd / AWS
5584 // App Mesh / Kubernetes ingress-nginx recommend
5585 // (1ms..=3600s) must pass, plus a sweep through the
5586 // long-running-workflow band (5m, 15m, 30m, 1h) the cap
5587 // accepts. Mirrors `accepts_policy_timeout_typical_values` on
5588 // the sibling `:politicas :timeout` axis.
5589 for w in [
5590 Duration::from_millis(1),
5591 Duration::from_millis(500),
5592 Duration::from_secs(1),
5593 Duration::from_secs(10),
5594 Duration::from_secs(15), // Envoy default
5595 Duration::from_secs(30),
5596 Duration::from_secs(60), // AWS App Mesh typical
5597 Duration::from_secs(300), // 5m
5598 Duration::from_secs(900), // 15m
5599 Duration::from_secs(1800),
5600 Duration::from_secs(3600), // exactly 1h, the cap
5601 ] {
5602 let l = LimitsSpec {
5603 wall_clock: Some(w),
5604 ..Default::default()
5605 };
5606 l.validate()
5607 .unwrap_or_else(|e| panic!("wall_clock={w:?} must validate; got {e:?}"));
5608 }
5609 }
5610
5611 #[test]
5612 fn wall_clock_zero_takes_precedence_over_cap() {
5613 // The cross-arm ordering pin: `Duration::ZERO` is structurally
5614 // outside both `>= 1ms` (zero-floor) and `<= LIMITS_WALL_CLOCK_MAX`
5615 // (cap), but the zero-floor diagnostic is the more
5616 // self-locating one (it directly names the omit-axis
5617 // remediation), so the validate gate must fire on zero first.
5618 // Same shape every other zero-then-shape ordering on this
5619 // surface uses (`MemoryZero` then `MemoryExceedsWasm32Cap`,
5620 // `PolicyTimeoutZero` then `PolicyTimeoutExceedsCap`).
5621 let l = LimitsSpec {
5622 wall_clock: Some(Duration::ZERO),
5623 ..Default::default()
5624 };
5625 assert_eq!(
5626 l.validate().unwrap_err(),
5627 LimitsError::WallClockZero,
5628 "Duration::ZERO must surface the zero-floor diagnostic, not the cap diagnostic"
5629 );
5630 }
5631
5632 #[test]
5633 fn wall_clock_canonical_takes_precedence_over_cap() {
5634 // The cross-arm ordering pin: a `Duration` that is *both*
5635 // sub-millisecond (non-canonical-form) and structurally above
5636 // the cap surfaces the canonical-form diagnostic first,
5637 // because the round-trip-shape break is the more fundamental
5638 // issue (the value can't even round-trip through the codec, so
5639 // the cap diagnostic naming `1ms..=1h` would be misleading —
5640 // there's no integer-ms form of the offending value). Pin the
5641 // order so a future refactor that reorders the arms surfaces
5642 // here as a test failure rather than a silent diagnostic
5643 // regression. Peer of
5644 // `policy_timeout_canonical_takes_precedence_over_cap`.
5645 let w = LIMITS_WALL_CLOCK_MAX + Duration::from_nanos(1);
5646 let l = LimitsSpec {
5647 wall_clock: Some(w),
5648 ..Default::default()
5649 };
5650 assert_eq!(
5651 l.validate().unwrap_err(),
5652 LimitsError::WallClockNotCanonical { wall_clock: w },
5653 "sub-ms above-cap value must surface the canonical-form diagnostic, not the cap diagnostic"
5654 );
5655 }
5656
5657 #[test]
5658 fn wall_clock_cap_diagnostic_carries_offending_value() {
5659 // The diagnostic-shape pin: the offending `Duration` is
5660 // carried verbatim into the
5661 // [`LimitsError::WallClockExceedsCap`] variant so the surfaced
5662 // error message names the value the author wrote, not just
5663 // the cap. Same self-locating diagnostic shape every other
5664 // typed-cap arm on this surface carries
5665 // (`MemoryExceedsWasm32Cap` carries the offending byte count
5666 // verbatim, `PolicyTimeoutExceedsCap` carries the offending
5667 // `Duration` verbatim).
5668 let w = Duration::from_secs(7200); // 2h
5669 let l = LimitsSpec {
5670 wall_clock: Some(w),
5671 ..Default::default()
5672 };
5673 let err = l.validate().unwrap_err();
5674 assert!(
5675 matches!(err, LimitsError::WallClockExceedsCap { wall_clock } if wall_clock == w),
5676 "got {err:?}"
5677 );
5678 let msg = err.to_string();
5679 assert!(
5680 msg.contains("7200"),
5681 ":limits :wall-clock cap diagnostic must carry the offending value verbatim (got: {msg})"
5682 );
5683 }
5684
5685 #[test]
5686 fn wall_clock_cap_pins_canonical_value() {
5687 // The [`LIMITS_WALL_CLOCK_MAX`] constant pins the value at
5688 // exactly 1 hour (3600s = 3_600_000ms) — the largest unit the
5689 // shared duration codec emits as a clean canonical string
5690 // (`"<n>h"`). Pinning the literal value here surfaces a future
5691 // drift (a relaxation to 24h, a tightening to 5m) as a
5692 // deliberate test edit, not a silent contract narrowing.
5693 //
5694 // The three typed-`Duration` caps on the validation surface
5695 // (`LIMITS_WALL_CLOCK_MAX` per-process, `POLICY_TIMEOUT_MAX`
5696 // per-edge, `POLICY_BREAKER_WINDOW_MAX` per-breaker) share a
5697 // single uniform top edge at the codec's largest emitted unit
5698 // — a structural-property invariant the equality assertions
5699 // here enshrine, so a future drift on any of the three
5700 // surfaces as a deliberate test edit. Same shape every other
5701 // typed-cap value pin uses
5702 // (`policy_timeout_cap_pins_canonical_value`,
5703 // `circuit_breaker_window_cap_pins_canonical_value`).
5704 assert_eq!(LIMITS_WALL_CLOCK_MAX, Duration::from_secs(3600));
5705 assert_eq!(LIMITS_WALL_CLOCK_MAX.as_millis(), 3_600_000);
5706 assert_eq!(LIMITS_WALL_CLOCK_MAX, crate::POLICY_TIMEOUT_MAX);
5707 assert_eq!(LIMITS_WALL_CLOCK_MAX, crate::POLICY_BREAKER_WINDOW_MAX);
5708 }
5709
5710 #[test]
5711 fn wall_clock_cap_value_round_trips_through_codec() {
5712 // The codec round-trip property the cap arm preserves: the
5713 // [`LIMITS_WALL_CLOCK_MAX`] constant itself round-trips through
5714 // the in-module duration codec — every value at the cap
5715 // renders to a clean canonical string (`"1h"`) and parses back
5716 // to the same `Duration`. Pin this so a future drift between
5717 // the cap constant and the codec's largest emitted unit
5718 // surfaces here. Same shape every other typed boundary pin on
5719 // this surface uses
5720 // (`wasm32_memory_cap_matches_parsed_4_gib`,
5721 // `policy_timeout_cap_value_round_trips_through_codec`).
5722 let l = LimitsSpec {
5723 wall_clock: Some(LIMITS_WALL_CLOCK_MAX),
5724 ..Default::default()
5725 };
5726 let json = serde_json::to_string(&l).unwrap();
5727 assert!(
5728 json.contains("\"1h\""),
5729 "the LIMITS_WALL_CLOCK_MAX value must render to the canonical \"1h\" form (got: {json})"
5730 );
5731 let back: LimitsSpec = serde_json::from_str(&json).unwrap();
5732 assert_eq!(back.wall_clock, Some(LIMITS_WALL_CLOCK_MAX));
5733 l.validate()
5734 .expect("LIMITS_WALL_CLOCK_MAX itself must pass validate");
5735 }
5736
5737 // ── value-shape: :cpu upper bound — 128-core schedulability ceiling ─────
5738 //
5739 // The third `LimitsSpec` axis brought to a top-edge cap, peer to
5740 // the `:memory` wasm32 ceiling and the `:wall-clock` 1h ceiling.
5741 // Mirrors the test discipline those peers carry: the
5742 // fail-before-pass-after pin, the one-millicore-boundary pin, the
5743 // far-above-cap sweep, the inclusive-at-cap positive control, the
5744 // production-band positive-control sweep, the cross-arm zero-then-
5745 // cap ordering pin, the diagnostic-shape pin carrying the offending
5746 // value verbatim, and the cap-value literal-identity + codec
5747 // round-trip pins anchoring the constant.
5748
5749 #[test]
5750 fn validate_rejects_cpu_above_cap() {
5751 // The fail-before-pass-after pin: 128_001m = 128 cores + 1
5752 // millicore is structurally one canonical-tick past the
5753 // [`LIMITS_CPU_MILLICORES_MAX`] ceiling — a `u32` magnitude the
5754 // millicore codec round-trips losslessly as `"128001m"`, and
5755 // that silently passed validate on every pre-gate codebase
5756 // because the typed slot's only check was the zero-floor arm.
5757 // The Kubernetes scheduler consuming the value (via the
5758 // `pleme-computeunit` chart's `resources.requests.cpu`
5759 // projection) cannot bind the pod to any node, far from the
5760 // source caixa.lisp.
5761 let m = LIMITS_CPU_MILLICORES_MAX + 1;
5762 let l = LimitsSpec {
5763 cpu: Some(m),
5764 ..Default::default()
5765 };
5766 assert_eq!(
5767 l.validate().unwrap_err(),
5768 LimitsError::CpuExceedsCap { millicores: m }
5769 );
5770 }
5771
5772 #[test]
5773 fn validate_rejects_cpu_far_above_cap() {
5774 // The "obvious authoring footgun" case: a `(:cpu "1000000m")`
5775 // (1000 cores) or `(:cpu "4294967295m")` (≈ u32::MAX) — values
5776 // the millicore codec accepts cleanly, the codec round-trips
5777 // losslessly through serde, but the Kubernetes scheduler
5778 // cannot bind to any node. Until this gate landed validate
5779 // accepted them. Pin the common above-cap values (1000 cores,
5780 // 10_000 cores, u32::MAX) so a future relaxation that drops
5781 // the upper bound surfaces here. Peer of
5782 // `validate_rejects_memory_8_gib` /
5783 // `validate_rejects_wall_clock_far_above_cap`.
5784 for m in [1_000_000_u32, 10_000_000, u32::MAX] {
5785 let l = LimitsSpec {
5786 cpu: Some(m),
5787 ..Default::default()
5788 };
5789 assert_eq!(
5790 l.validate().unwrap_err(),
5791 LimitsError::CpuExceedsCap { millicores: m }
5792 );
5793 }
5794 }
5795
5796 #[test]
5797 fn validate_accepts_cpu_at_cap() {
5798 // The boundary value — exactly [`LIMITS_CPU_MILLICORES_MAX`]
5799 // (128 cores = 128_000m) — must validate. The cap is inclusive
5800 // on the top edge, matching the discipline on every sibling
5801 // capped axis ([`LIMITS_MEMORY_WASM32_MAX_BYTES`],
5802 // [`LIMITS_WALL_CLOCK_MAX`], [`crate::POLICY_TIMEOUT_MAX`],
5803 // [`crate::POLICY_BREAKER_WINDOW_MAX`],
5804 // [`crate::POLICY_RATE_LIMIT_MAX`]). Pin the boundary
5805 // explicitly so a future off-by-one tightening
5806 // (`>= LIMITS_CPU_MILLICORES_MAX` instead of `>`) surfaces here
5807 // as a test failure rather than a silent contract narrowing.
5808 let l = LimitsSpec {
5809 cpu: Some(LIMITS_CPU_MILLICORES_MAX),
5810 ..Default::default()
5811 };
5812 l.validate()
5813 .expect("cpu == LIMITS_CPU_MILLICORES_MAX must validate");
5814 }
5815
5816 #[test]
5817 fn validate_accepts_cpu_typical_values() {
5818 // The documented production-playbook band positive-control
5819 // sweep — every value the canonical caixa Servico runs in
5820 // (100m..=2000m) must pass, plus a sweep through the larger
5821 // burstable / multi-component-host band (4000m, 8000m, 16000m,
5822 // 32000m, 64000m, 128000m) the cap accepts. Mirrors
5823 // `accepts_wall_clock_typical_values` on the sibling
5824 // `:wall-clock` axis.
5825 for m in [
5826 1_u32, // smallest non-zero
5827 100, // typical small worker
5828 500, // canonical test default (peer to limits/flux/helm)
5829 1_000, // 1 core, single-threaded wasm32 saturation
5830 2_000, // 2 cores
5831 4_000, // typical burstable
5832 8_000, // upper realistic per-Servico band
5833 16_000, // documented heavy-Servico ceiling
5834 32_000, // wide-node multi-component-host
5835 64_000, // half the cap
5836 128_000, // exactly at cap
5837 ] {
5838 let l = LimitsSpec {
5839 cpu: Some(m),
5840 ..Default::default()
5841 };
5842 l.validate()
5843 .unwrap_or_else(|e| panic!("cpu={m}m must validate; got {e:?}"));
5844 }
5845 }
5846
5847 #[test]
5848 fn cpu_zero_takes_precedence_over_cap() {
5849 // The cross-arm ordering pin: `Some(0)` is structurally outside
5850 // both `>= 1` (zero-floor) and `<= LIMITS_CPU_MILLICORES_MAX`
5851 // (cap), but the zero-floor diagnostic is the more
5852 // self-locating one (it directly names the omit-axis
5853 // remediation), so the validate gate must fire on zero first.
5854 // Same shape every other zero-then-cap ordering on this surface
5855 // uses (`MemoryZero` then `MemoryExceedsWasm32Cap`,
5856 // `WallClockZero` then `WallClockExceedsCap`).
5857 let l = LimitsSpec {
5858 cpu: Some(0),
5859 ..Default::default()
5860 };
5861 assert_eq!(
5862 l.validate().unwrap_err(),
5863 LimitsError::CpuZero,
5864 "Some(0) must surface the zero-floor diagnostic, not the cap diagnostic"
5865 );
5866 }
5867
5868 #[test]
5869 fn validate_rejects_cpu_cap_after_earlier_axes() {
5870 // Cross-axis ordering: when both an above-cap `:cpu` and an
5871 // earlier-axis violation are present, the earlier axis must
5872 // fire first. The validate sequence is :memory → :fuel →
5873 // :wall-clock → :cpu, so a paired memory-zero + cpu-above-cap
5874 // input surfaces `MemoryZero`, never the cpu-cap diagnostic.
5875 // Pins the canonical axis order so a future refactor that
5876 // reorders the arms surfaces here as a test failure rather
5877 // than a silent diagnostic regression. Peer of
5878 // `validate_rejects_first_zero_axis_deterministically` and
5879 // `validate_rejects_memory_cap_before_other_axes`.
5880 let l = LimitsSpec {
5881 memory: Some(0),
5882 fuel: None,
5883 wall_clock: None,
5884 cpu: Some(LIMITS_CPU_MILLICORES_MAX + 1),
5885 };
5886 assert_eq!(
5887 l.validate().unwrap_err(),
5888 LimitsError::MemoryZero,
5889 "earlier-axis violation must take precedence over later-axis cap violation"
5890 );
5891 }
5892
5893 #[test]
5894 fn cpu_cap_diagnostic_carries_offending_value() {
5895 // The diagnostic-shape pin: the offending millicore count is
5896 // carried verbatim into the [`LimitsError::CpuExceedsCap`]
5897 // variant so the surfaced error message names the value the
5898 // author wrote, not just the cap. Same self-locating
5899 // diagnostic shape every other typed-cap arm on this surface
5900 // carries (`MemoryExceedsWasm32Cap` carries the offending byte
5901 // count verbatim, `WallClockExceedsCap` carries the offending
5902 // `Duration` verbatim).
5903 let m = 256_000_u32; // 256 cores — double the cap
5904 let l = LimitsSpec {
5905 cpu: Some(m),
5906 ..Default::default()
5907 };
5908 let err = l.validate().unwrap_err();
5909 assert!(
5910 matches!(err, LimitsError::CpuExceedsCap { millicores } if millicores == m),
5911 "got {err:?}"
5912 );
5913 let msg = err.to_string();
5914 assert!(
5915 msg.contains("256000"),
5916 ":limits :cpu cap diagnostic must carry the offending value verbatim (got: {msg})"
5917 );
5918 }
5919
5920 #[test]
5921 fn cpu_cap_pins_canonical_value() {
5922 // The [`LIMITS_CPU_MILLICORES_MAX`] constant pins the value at
5923 // exactly 128 cores (128_000 millicores) — the largest
5924 // commercially-common non-metal cloud Kubernetes node vCPU
5925 // count. Pinning the literal value here surfaces a future
5926 // drift (a relaxation to 256 cores, a tightening to 64 cores)
5927 // as a deliberate test edit, not a silent contract narrowing.
5928 // Same shape every other typed-cap value pin uses
5929 // (`wall_clock_cap_pins_canonical_value`,
5930 // `wasm32_memory_cap_matches_parsed_4_gib`).
5931 assert_eq!(LIMITS_CPU_MILLICORES_MAX, 128_000);
5932 assert_eq!(LIMITS_CPU_MILLICORES_MAX, 128 * 1000);
5933 }
5934
5935 #[test]
5936 fn cpu_cap_value_round_trips_through_codec() {
5937 // The codec round-trip property the cap arm preserves: the
5938 // [`LIMITS_CPU_MILLICORES_MAX`] constant itself round-trips
5939 // through the in-module millicore codec — the cap value
5940 // renders to a clean canonical string (`"128000m"`) and parses
5941 // back to the same `u32`. Pin this so a future drift between
5942 // the cap constant and the codec's accepted magnitude surfaces
5943 // here. Same shape every other typed boundary pin on this
5944 // surface uses (`wasm32_memory_cap_matches_parsed_4_gib`,
5945 // `wall_clock_cap_value_round_trips_through_codec`).
5946 let l = LimitsSpec {
5947 cpu: Some(LIMITS_CPU_MILLICORES_MAX),
5948 ..Default::default()
5949 };
5950 let json = serde_json::to_string(&l).unwrap();
5951 assert!(
5952 json.contains("\"128000m\""),
5953 "the LIMITS_CPU_MILLICORES_MAX value must render to the canonical \"128000m\" form (got: {json})"
5954 );
5955 let back: LimitsSpec = serde_json::from_str(&json).unwrap();
5956 assert_eq!(back.cpu, Some(LIMITS_CPU_MILLICORES_MAX));
5957 l.validate()
5958 .expect("LIMITS_CPU_MILLICORES_MAX itself must pass validate");
5959 }
5960
5961 // ── value-shape: :fuel upper bound — 10^12 no-op-budget ceiling ────────
5962 //
5963 // The fourth and final `LimitsSpec` axis brought to a top-edge
5964 // cap, closing the open edge the 857dfcc CPU-cap commit body
5965 // explicitly named: "three of the four axes carry a top-and-bottom
5966 // edge gate; only `:fuel` remains with a zero-floor-only shape."
5967 // Mirrors the test discipline every sibling capped axis carries:
5968 // the fail-before-pass-after pin, the one-instruction-boundary
5969 // pin, the far-above-cap sweep, the inclusive-at-cap positive
5970 // control, the production-band positive-control sweep, the
5971 // cross-arm zero-then-cap ordering pin, the cross-axis
5972 // earlier-then-later precedence pin, the diagnostic-shape pin
5973 // carrying the offending value verbatim, and the cap-value
5974 // literal-identity + codec round-trip pins anchoring the
5975 // constant.
5976
5977 #[test]
5978 fn validate_rejects_fuel_above_cap() {
5979 // The fail-before-pass-after pin: `LIMITS_FUEL_MAX + 1` =
5980 // one wasm-instruction past the structural ceiling — a `u64`
5981 // magnitude the typed slot round-trips losslessly through
5982 // serde, and that silently passed validate on every pre-gate
5983 // codebase because the typed slot's only check was the
5984 // zero-floor arm. The wasm-engine consuming the value (via
5985 // `Store::set_fuel` projection in the M2.5 host runtime)
5986 // accepts the magnitude but the sibling `:wall-clock` 1h cap
5987 // fires before the fuel counter could ever drain — the typed
5988 // `:fuel` slot becomes a no-op budget far from the source
5989 // caixa.lisp.
5990 let f = LIMITS_FUEL_MAX + 1;
5991 let l = LimitsSpec {
5992 fuel: Some(f),
5993 ..Default::default()
5994 };
5995 assert_eq!(
5996 l.validate().unwrap_err(),
5997 LimitsError::FuelExceedsCap { fuel: f }
5998 );
5999 }
6000
6001 #[test]
6002 fn validate_rejects_fuel_far_above_cap() {
6003 // The "obvious authoring footgun" case: a `(:fuel
6004 // 1000000000000000)` (10^15 instructions), a paste-from-binary
6005 // `u64::MAX`, or a hex-literal-confused-for-decimal magnitude
6006 // — values the `u64` slot accepts cleanly, the codec
6007 // round-trips losslessly through serde, but the wasm-engine
6008 // can never honor as a meaningful counter. Until this gate
6009 // landed validate accepted them. Pin the common above-cap
6010 // values (10x cap, 1000x cap, `u64::MAX`) so a future
6011 // relaxation that drops the upper bound surfaces here. Peer
6012 // of `validate_rejects_cpu_far_above_cap` /
6013 // `validate_rejects_memory_8_gib` /
6014 // `validate_rejects_wall_clock_far_above_cap`.
6015 for f in [LIMITS_FUEL_MAX * 10, LIMITS_FUEL_MAX * 1_000, u64::MAX] {
6016 let l = LimitsSpec {
6017 fuel: Some(f),
6018 ..Default::default()
6019 };
6020 assert_eq!(
6021 l.validate().unwrap_err(),
6022 LimitsError::FuelExceedsCap { fuel: f }
6023 );
6024 }
6025 }
6026
6027 #[test]
6028 fn validate_accepts_fuel_at_cap() {
6029 // The boundary value — exactly [`LIMITS_FUEL_MAX`] (10^12
6030 // wasm instructions) — must validate. The cap is inclusive
6031 // on the top edge, matching the discipline on every sibling
6032 // capped axis ([`LIMITS_MEMORY_WASM32_MAX_BYTES`],
6033 // [`LIMITS_WALL_CLOCK_MAX`], [`LIMITS_CPU_MILLICORES_MAX`],
6034 // [`crate::POLICY_TIMEOUT_MAX`],
6035 // [`crate::POLICY_BREAKER_WINDOW_MAX`],
6036 // [`crate::POLICY_RATE_LIMIT_MAX`]). Pin the boundary
6037 // explicitly so a future off-by-one tightening
6038 // (`>= LIMITS_FUEL_MAX` instead of `>`) surfaces here as a
6039 // test failure rather than a silent contract narrowing.
6040 let l = LimitsSpec {
6041 fuel: Some(LIMITS_FUEL_MAX),
6042 ..Default::default()
6043 };
6044 l.validate().expect("fuel == LIMITS_FUEL_MAX must validate");
6045 }
6046
6047 #[test]
6048 fn validate_accepts_fuel_typical_values() {
6049 // The documented production-playbook band positive-control
6050 // sweep — every value the canonical caixa Servico runs in
6051 // (10^6..=10^9 fuel-units) must pass, plus a sweep through
6052 // the larger compute-bound-Servico band (10^10, 10^11) the
6053 // cap accepts. The canonical fixture is `1_000_000` =
6054 // wasmtime's documented `Store::set_fuel(1_000_000)` example.
6055 // Mirrors `validate_accepts_cpu_typical_values` on the
6056 // sibling `:cpu` axis.
6057 for f in [
6058 1_u64, // smallest non-zero
6059 1_000, // tiny per-call budget
6060 1_000_000, // canonical fixture (10^6) — wasmtime book example
6061 10_000_000, // typical small-Servico (10^7)
6062 100_000_000, // typical heavier-Servico (10^8)
6063 1_000_000_000, // 1 billion — upper realistic per-call (10^9)
6064 100_000_000_000, // 10^11 — heavy compute-bound (10x below cap)
6065 500_000_000_000, // half the cap
6066 1_000_000_000_000, // exactly at cap (10^12)
6067 ] {
6068 let l = LimitsSpec {
6069 fuel: Some(f),
6070 ..Default::default()
6071 };
6072 l.validate()
6073 .unwrap_or_else(|e| panic!("fuel={f} must validate; got {e:?}"));
6074 }
6075 }
6076
6077 #[test]
6078 fn fuel_zero_takes_precedence_over_cap() {
6079 // The cross-arm ordering pin: `Some(0)` is structurally
6080 // outside both `>= 1` (zero-floor) and `<= LIMITS_FUEL_MAX`
6081 // (cap), but the zero-floor diagnostic is the more
6082 // self-locating one (it directly names the omit-axis
6083 // remediation and the wasmtime-traps-at-zero semantics), so
6084 // the validate gate must fire on zero first. Same shape every
6085 // other zero-then-cap ordering on this surface uses
6086 // (`MemoryZero` then `MemoryExceedsWasm32Cap`,
6087 // `WallClockZero` then `WallClockExceedsCap`, `CpuZero` then
6088 // `CpuExceedsCap`).
6089 let l = LimitsSpec {
6090 fuel: Some(0),
6091 ..Default::default()
6092 };
6093 assert_eq!(
6094 l.validate().unwrap_err(),
6095 LimitsError::FuelZero,
6096 "Some(0) must surface the zero-floor diagnostic, not the cap diagnostic"
6097 );
6098 }
6099
6100 #[test]
6101 fn validate_rejects_fuel_cap_after_earlier_axes() {
6102 // Cross-axis ordering: when both an above-cap `:fuel` and an
6103 // earlier-axis violation are present, the earlier axis must
6104 // fire first. The validate sequence is :memory → :fuel →
6105 // :wall-clock → :cpu, so a paired memory-zero + fuel-above-
6106 // cap input surfaces `MemoryZero`, never the fuel-cap
6107 // diagnostic. Pins the canonical axis order so a future
6108 // refactor that reorders the arms surfaces here as a test
6109 // failure rather than a silent diagnostic regression. Peer
6110 // of `validate_rejects_cpu_cap_after_earlier_axes`.
6111 let l = LimitsSpec {
6112 memory: Some(0),
6113 fuel: Some(LIMITS_FUEL_MAX + 1),
6114 wall_clock: None,
6115 cpu: None,
6116 };
6117 assert_eq!(
6118 l.validate().unwrap_err(),
6119 LimitsError::MemoryZero,
6120 "earlier-axis violation must take precedence over later-axis cap violation"
6121 );
6122 }
6123
6124 #[test]
6125 fn validate_rejects_fuel_cap_before_later_axes() {
6126 // Cross-axis ordering on the other side: when both an
6127 // above-cap `:fuel` and a later-axis violation are present,
6128 // the `:fuel` cap must fire before the `:wall-clock` /
6129 // `:cpu` zero-floor diagnostics. The validate sequence is
6130 // :memory → :fuel → :wall-clock → :cpu, so a paired
6131 // fuel-above-cap + wall-clock-zero input surfaces
6132 // `FuelExceedsCap`, not `WallClockZero`. Pins the canonical
6133 // axis order on the new arm's downstream side, peer to the
6134 // upstream pin `validate_rejects_fuel_cap_after_earlier_axes`.
6135 let l = LimitsSpec {
6136 memory: None,
6137 fuel: Some(LIMITS_FUEL_MAX + 1),
6138 wall_clock: Some(Duration::ZERO),
6139 cpu: Some(0),
6140 };
6141 assert_eq!(
6142 l.validate().unwrap_err(),
6143 LimitsError::FuelExceedsCap {
6144 fuel: LIMITS_FUEL_MAX + 1
6145 },
6146 ":fuel cap diagnostic must take precedence over later-axis zero-floor diagnostics"
6147 );
6148 }
6149
6150 #[test]
6151 fn fuel_cap_diagnostic_carries_offending_value() {
6152 // The diagnostic-shape pin: the offending fuel count is
6153 // carried verbatim into the [`LimitsError::FuelExceedsCap`]
6154 // variant so the surfaced error message names the value the
6155 // author wrote, not just the cap. Same self-locating
6156 // diagnostic shape every other typed-cap arm on this surface
6157 // carries (`MemoryExceedsWasm32Cap` carries the offending
6158 // byte count verbatim, `WallClockExceedsCap` carries the
6159 // offending `Duration` verbatim, `CpuExceedsCap` carries the
6160 // offending millicore count verbatim).
6161 let f = 5_000_000_000_000_u64; // 5 trillion — 5x the cap
6162 let l = LimitsSpec {
6163 fuel: Some(f),
6164 ..Default::default()
6165 };
6166 let err = l.validate().unwrap_err();
6167 assert!(
6168 matches!(err, LimitsError::FuelExceedsCap { fuel } if fuel == f),
6169 "got {err:?}"
6170 );
6171 let msg = err.to_string();
6172 assert!(
6173 msg.contains("5000000000000"),
6174 ":limits :fuel cap diagnostic must carry the offending value verbatim (got: {msg})"
6175 );
6176 }
6177
6178 #[test]
6179 fn fuel_cap_pins_canonical_value() {
6180 // The [`LIMITS_FUEL_MAX`] constant pins the value at exactly
6181 // 10^12 (1 trillion wasm instructions) — the round-number
6182 // ceiling above the operational envelope the sibling
6183 // [`LIMITS_WALL_CLOCK_MAX`] (1h) × wasmtime's fuel-tracked
6184 // execution rate (~10^9 fuel/sec) yields. Pinning the
6185 // literal value here surfaces a future drift (a relaxation
6186 // to 10^15, a tightening to 10^9) as a deliberate test edit,
6187 // not a silent contract narrowing. Same shape every other
6188 // typed-cap value pin uses (`cpu_cap_pins_canonical_value`,
6189 // `wall_clock_cap_pins_canonical_value`,
6190 // `wasm32_memory_cap_matches_parsed_4_gib`).
6191 assert_eq!(LIMITS_FUEL_MAX, 1_000_000_000_000);
6192 assert_eq!(LIMITS_FUEL_MAX, 10_u64.pow(12));
6193 }
6194
6195 #[test]
6196 fn fuel_cap_value_round_trips_through_serde() {
6197 // The serde round-trip property the cap arm preserves: the
6198 // [`LIMITS_FUEL_MAX`] constant itself round-trips through
6199 // the in-module `u64` serde codec — the cap value renders as
6200 // the bare integer literal and parses back to the same
6201 // `u64`. Pin this so a future drift between the cap constant
6202 // and the codec's accepted magnitude (a future custom u64
6203 // serializer that introduces lossy formatting) surfaces
6204 // here. Same shape every other typed boundary pin on this
6205 // surface uses (`wasm32_memory_cap_matches_parsed_4_gib`,
6206 // `wall_clock_cap_value_round_trips_through_codec`,
6207 // `cpu_cap_value_round_trips_through_codec`).
6208 let l = LimitsSpec {
6209 fuel: Some(LIMITS_FUEL_MAX),
6210 ..Default::default()
6211 };
6212 let json = serde_json::to_string(&l).unwrap();
6213 assert!(
6214 json.contains("1000000000000"),
6215 "the LIMITS_FUEL_MAX value must render verbatim as the bare integer 10^12 \
6216 (got: {json})"
6217 );
6218 let back: LimitsSpec = serde_json::from_str(&json).unwrap();
6219 assert_eq!(back.fuel, Some(LIMITS_FUEL_MAX));
6220 l.validate()
6221 .expect("LIMITS_FUEL_MAX itself must pass validate");
6222 }
6223
6224 // ── per-`:limits :memory` accessor pins (LimitsSpec::memory) ─────────
6225
6226 #[test]
6227 fn limits_memory_returns_option_u64_byte_equal_across_permutations() {
6228 // The canonical per-`:limits` `:memory` Lunatic-per-process
6229 // wasm32-linear-memory byte-cap scalar pin: [`LimitsSpec::memory`]
6230 // must return the `:limits :memory` typed `u64` verbatim as an
6231 // `Option<u64>`, byte-equal to the raw field access across the
6232 // three canonical shape-arms — `None` (no cap declared —
6233 // engine-default applies), `Some(LIMITS_MEMORY_WASM32_PAGE_BYTES)`
6234 // (the structural minimum a validated `:limits :memory` may
6235 // carry, one wasm32 linear-memory page), `Some(64 * 1024 *
6236 // 1024)` (the canonical 64 MiB byte-cap the module-level
6237 // docstring names).
6238 //
6239 // Peer of the sibling per-`:politicas` [`crate::MeshPolicy::mtls_required`]
6240 // (c0110f1) / [`crate::MeshPolicy::retries`] (bdfb399) /
6241 // [`crate::MeshPolicy::timeout`] (7073d0f) accessor pin trio on
6242 // the sibling `Option<Copy-T>`-return axis, extended to the
6243 // peer per-`:limits` typed-`u64` optional-scalar shape —
6244 // first `Option<Copy-T>`-return accessor on the M2 slot family.
6245 // Pins against a future silent detour that re-derived the cap
6246 // from a peer axis (an accidental `.fuel`-collapse that
6247 // assumed the two `Option<u64>` axes carry the same value), a
6248 // `None` → `Some(0)` "zero means unbounded" collapse (the
6249 // canonical `Option<u64>` → `u64` collapse footgun the
6250 // [`LimitsError::MemoryZero`] validate arm guards on the peer
6251 // zero-floor axis), or a per-arm variant swap that landed on
6252 // one consumer without the other.
6253 for memory in [
6254 None,
6255 Some(LIMITS_MEMORY_WASM32_PAGE_BYTES),
6256 Some(64 * 1024 * 1024),
6257 ] {
6258 let l = LimitsSpec {
6259 memory,
6260 ..LimitsSpec::default()
6261 };
6262 assert_eq!(
6263 l.memory(),
6264 memory,
6265 "LimitsSpec::memory must return :limits :memory verbatim \
6266 (got {:?}, expected {memory:?})",
6267 l.memory(),
6268 );
6269 assert_eq!(
6270 l.memory(),
6271 l.memory,
6272 "LimitsSpec::memory must byte-equal the raw .memory \
6273 field access across every value in the accept-set",
6274 );
6275 }
6276 }
6277
6278 #[test]
6279 fn limits_is_empty_memory_arm_routes_through_accessor() {
6280 // Composition pin: [`LimitsSpec::is_empty`]'s `memory` arm
6281 // must key off [`LimitsSpec::memory`], not the raw `.memory`
6282 // field access. Structurally: setting ONLY the `memory` slot
6283 // on an otherwise-default LimitsSpec must flip `is_empty()`
6284 // from `true` (all-`None`) to `false` (one axis carries a
6285 // value); the flip must be observed across every value in the
6286 // accept-set since the emptiness semantic reads "any axis
6287 // carries a value" — not "any axis carries a value above a
6288 // threshold" — the same non-collapsing shape the sibling M3
6289 // [`crate::MeshPolicy::is_empty`] predicate carries on its
6290 // peer `Option<Copy-T>`-typed slot surfaces.
6291 //
6292 // Pins against a future silent detour that re-derived the
6293 // emptiness predicate off a peer axis (an accidental
6294 // `.fuel.is_none()`-only chain that dropped the `memory` arm
6295 // entirely), an accessor-side detour that no longer names the
6296 // substrate-primitive typed dispatch (an accidental
6297 // `self.memory.unwrap_or(0) == 0` fallback in the accessor
6298 // that would silently classify both `None` and `Some(0)` as
6299 // the same value), or a threshold collapse (a
6300 // `self.memory().is_some_and(|m| m > 0)` that would silently
6301 // classify `Some(0)` as unset).
6302 //
6303 // Peer of the sibling per-`:politicas`
6304 // [`crate::MeshPolicy::is_empty`] `mtls_required` arm
6305 // accessor-composition pin (c0110f1) on the sibling optional-
6306 // scalar axis — same "the emptiness / shape-gate predicate
6307 // must route through the substrate-primitive typed dispatch"
6308 // discipline extended onto the peer per-`:limits` emptiness
6309 // predicate.
6310 let empty = LimitsSpec::default();
6311 assert!(
6312 empty.is_empty(),
6313 "LimitsSpec::default() must be is_empty() — every axis \
6314 defaults to None",
6315 );
6316 for memory in [
6317 Some(LIMITS_MEMORY_WASM32_PAGE_BYTES),
6318 Some(64 * 1024 * 1024),
6319 Some(LIMITS_MEMORY_WASM32_MAX_BYTES),
6320 ] {
6321 let l = LimitsSpec {
6322 memory,
6323 ..LimitsSpec::default()
6324 };
6325 assert!(
6326 !l.is_empty(),
6327 "LimitsSpec::is_empty must return false when :memory \
6328 is {memory:?} — the emptiness predicate reads \"any \
6329 axis carries a value\", not \"any axis carries a \
6330 value above a threshold\"",
6331 );
6332 assert_eq!(
6333 l.memory().is_none(),
6334 l.is_empty(),
6335 "when :memory is the only set axis, is_empty() must \
6336 equal memory().is_none() — the accessor and the \
6337 emptiness predicate must route through the same \
6338 substrate-primitive typed dispatch on the :memory \
6339 arm",
6340 );
6341 }
6342 }
6343
6344 #[test]
6345 fn limits_memory_projects_option_u64_by_copy() {
6346 // The by-copy pin: [`LimitsSpec::memory`] returns `Option<u64>`
6347 // by copy — `Option<u64>` is `Copy` and the accessor must
6348 // return by value, not by reference. Peer of the sibling per-
6349 // `:politicas` [`crate::MeshPolicy::mtls_required`] (c0110f1)
6350 // borrow-invariant pin on the peer `Option<bool>` shape,
6351 // extended onto the peer `Option<u64>` copy-invariant shape —
6352 // the accessor's returned `Option<u64>` must outlive `&self`
6353 // (multiple calls must return equal values from a dropped-
6354 // `&self` copy, since the returned Option carries no borrow),
6355 // and calling the accessor twice on the same LimitsSpec must
6356 // yield the same `Option<u64>` verbatim (idempotent, no side
6357 // effects on `&self`).
6358 //
6359 // Pins against a future silent detour that returned
6360 // `Option<&u64>` (which would type-check but silently break
6361 // every downstream caller — the future `wasmtime::Store::limiter`
6362 // wire path consumes `Option<u64>` by value and `&u64` would
6363 // fold to a detached copy at the call site), an accidental
6364 // `Option::as_ref()` projection (`self.memory.as_ref()` would
6365 // also type-check but return `Option<&u64>`), or a one-arm-
6366 // only accessor that reads `Some(*m)` in the Some arm but
6367 // reads a fresh `Default::default()` in the None arm.
6368 for memory in [
6369 None,
6370 Some(LIMITS_MEMORY_WASM32_PAGE_BYTES),
6371 Some(64 * 1024 * 1024),
6372 Some(LIMITS_MEMORY_WASM32_MAX_BYTES),
6373 ] {
6374 let l = LimitsSpec {
6375 memory,
6376 ..LimitsSpec::default()
6377 };
6378 let first = l.memory();
6379 let second = l.memory();
6380 assert_eq!(
6381 first, second,
6382 "LimitsSpec::memory must be idempotent — two \
6383 successive calls on the same &self must return the \
6384 same Option<u64>",
6385 );
6386 assert_eq!(
6387 first, memory,
6388 "LimitsSpec::memory must return :limits :memory \
6389 verbatim by copy — got {first:?}, expected {memory:?}",
6390 );
6391 }
6392 }
6393
6394 #[test]
6395 #[allow(clippy::too_many_lines)]
6396 fn validate_memory_arms_route_through_lifted_memory_accessor() {
6397 // Composition pin: every value-shape gate in
6398 // [`LimitsSpec::validate`] on the `:memory` axis (the
6399 // zero-floor `MemoryZero` arm, the sub-page `MemoryBelowWasm32Page`
6400 // arm, the above-cap `MemoryExceedsWasm32Cap` arm, the
6401 // non-page-multiple `MemoryNotPageMultiple` arm) must key off
6402 // [`LimitsSpec::memory`], not the raw `self.memory` field
6403 // access. Peer of the sibling per-`:politicas`
6404 // [`crate::AplicacaoSpec::validate_politicas`] `:timeout` /
6405 // `:retries` arm converge pin (1017b9d) on the sibling M3
6406 // mesh-slot family, extended onto the M2 per-`:limits`
6407 // `:memory` axis; peer of the sibling per-`:limits` `:fuel` /
6408 // `:wall-clock` / `:cpu` arms in the same fan-out that
6409 // already route through `self.fuel()` / `self.wall_clock()`
6410 // / `self.cpu()` at :880 / :888 / :942.
6411 //
6412 // Assertion shape: for each memory value in the
6413 // accept-and-refuse set, `LimitsSpec::memory()` must byte-
6414 // equal the raw `.memory` field it borrows from, and the
6415 // validate call on a `LimitsSpec { memory: <v>, ..default() }`
6416 // fixture must surface the same variant/Ok discriminant the
6417 // accessor-composed spec surfaces. Together they catch any
6418 // future silent detour — an accessor drift that no longer
6419 // shipped the raw slot verbatim, a validate-branch rebrand to
6420 // a peer-axis field read, an accidental `Option`-collapse in
6421 // any of the four arms — at caixa-core build time rather than
6422 // at a downstream runtime declared-but-inert-limits divergence
6423 // at the wasmtime `Store::limiter` boundary.
6424 //
6425 // `#[allow(clippy::too_many_lines)]` per the same discipline
6426 // peer over-100-line composition pins in this module accept
6427 // (see e.g. `limits_is_empty_memory_arm_routes_through_accessor`,
6428 // `limits_memory_returns_option_u64_byte_equal_across_permutations`).
6429 for memory in [
6430 None,
6431 Some(0), // → MemoryZero
6432 Some(1), // → MemoryBelowWasm32Page (sub-page)
6433 Some(LIMITS_MEMORY_WASM32_PAGE_BYTES - 1), // → MemoryBelowWasm32Page (at-under-page)
6434 Some(LIMITS_MEMORY_WASM32_PAGE_BYTES), // → Ok (at-page-floor)
6435 Some(LIMITS_MEMORY_WASM32_PAGE_BYTES + 1), // → MemoryNotPageMultiple (one-past-page)
6436 Some(2 * LIMITS_MEMORY_WASM32_PAGE_BYTES), // → Ok (multi-page)
6437 Some(LIMITS_MEMORY_WASM32_MAX_BYTES), // → Ok (at-cap)
6438 Some(LIMITS_MEMORY_WASM32_MAX_BYTES + 1), // → MemoryExceedsWasm32Cap (one-past-cap)
6439 ] {
6440 let l = LimitsSpec {
6441 memory,
6442 ..LimitsSpec::default()
6443 };
6444 // (1) The accessor must byte-equal the raw field it wraps.
6445 assert_eq!(
6446 l.memory(),
6447 l.memory,
6448 "LimitsSpec::memory() must byte-equal the raw \
6449 .memory field for {memory:?} — an accessor detour \
6450 that dropped the raw slot's Option<u64> verbatim \
6451 would silently split validate's :memory arms from \
6452 every peer emit-site consumer that also routes \
6453 through the accessor (the future wasmtime \
6454 Store::limiter wire path, the caixa-helm \
6455 resources.limits.memory materializer)",
6456 );
6457 // (2) Two successive validate() calls must yield the same
6458 // variant/Ok discriminant — the accessor-projected reads
6459 // and the raw-projected reads must produce identical
6460 // validation outcomes.
6461 let first = l.validate();
6462 let second = l.validate();
6463 assert_eq!(
6464 first, second,
6465 "LimitsSpec::validate must be idempotent on :memory \
6466 {memory:?} — two successive calls must surface the \
6467 same variant/Ok discriminant, catching any accessor \
6468 detour that would introduce a value-dependent side \
6469 effect on the &self projection",
6470 );
6471 }
6472 // (3) The specific arm-order shape the four converged sites
6473 // encode: `MemoryZero` (raw-`Some(0)`) precedes the page-floor
6474 // arm, which precedes the cap arm, which precedes the page-
6475 // multiple arm. Each arm must fire off the accessor-projected
6476 // read on its specific fixture value.
6477 assert_eq!(
6478 LimitsSpec {
6479 memory: Some(0),
6480 ..LimitsSpec::default()
6481 }
6482 .validate(),
6483 Err(LimitsError::MemoryZero),
6484 "MemoryZero must fire on Some(0) via the accessor projection",
6485 );
6486 assert_eq!(
6487 LimitsSpec {
6488 memory: Some(1),
6489 ..LimitsSpec::default()
6490 }
6491 .validate(),
6492 Err(LimitsError::MemoryBelowWasm32Page { bytes: 1 }),
6493 "MemoryBelowWasm32Page must fire on Some(1) via the accessor projection",
6494 );
6495 assert_eq!(
6496 LimitsSpec {
6497 memory: Some(LIMITS_MEMORY_WASM32_MAX_BYTES + 1),
6498 ..LimitsSpec::default()
6499 }
6500 .validate(),
6501 Err(LimitsError::MemoryExceedsWasm32Cap {
6502 bytes: LIMITS_MEMORY_WASM32_MAX_BYTES + 1
6503 }),
6504 "MemoryExceedsWasm32Cap must fire on one-past-cap via the accessor projection",
6505 );
6506 assert_eq!(
6507 LimitsSpec {
6508 memory: Some(LIMITS_MEMORY_WASM32_PAGE_BYTES + 1),
6509 ..LimitsSpec::default()
6510 }
6511 .validate(),
6512 Err(LimitsError::MemoryNotPageMultiple {
6513 bytes: LIMITS_MEMORY_WASM32_PAGE_BYTES + 1
6514 }),
6515 "MemoryNotPageMultiple must fire on one-past-page-floor via the accessor projection",
6516 );
6517 assert_eq!(
6518 LimitsSpec {
6519 memory: Some(LIMITS_MEMORY_WASM32_PAGE_BYTES),
6520 ..LimitsSpec::default()
6521 }
6522 .validate(),
6523 Ok(()),
6524 "at-page-floor must pass validate via the accessor projection",
6525 );
6526 assert_eq!(
6527 LimitsSpec {
6528 memory: Some(LIMITS_MEMORY_WASM32_MAX_BYTES),
6529 ..LimitsSpec::default()
6530 }
6531 .validate(),
6532 Ok(()),
6533 "at-cap must pass validate via the accessor projection",
6534 );
6535 }
6536
6537 // ── per-`:limits :fuel` accessor pins (LimitsSpec::fuel) ─────────
6538
6539 #[test]
6540 fn limits_fuel_returns_option_u64_byte_equal_across_permutations() {
6541 // The canonical per-`:limits` `:fuel` wasmtime-per-call
6542 // wasm-instruction budget scalar pin: [`LimitsSpec::fuel`]
6543 // must return the `:limits :fuel` typed `u64` verbatim as an
6544 // `Option<u64>`, byte-equal to the raw field access across
6545 // the three canonical shape-arms — `None` (no fuel budget
6546 // declared — engine-default applies), `Some(1)` (the
6547 // structural minimum a validated `:limits :fuel` may carry,
6548 // one wasm instruction; wasmtime traps the first instruction
6549 // at `fuel=0`, so `Some(1)` is the smallest budget that
6550 // executes any code), `Some(1_000_000)` (the canonical 10⁶
6551 // fuel-unit budget the in-tree `Caixa::template` and the
6552 // wasmtime book's `Store::set_fuel(1_000_000)` example both
6553 // carry).
6554 //
6555 // Peer of the sibling per-`:limits` [`LimitsSpec::memory`]
6556 // (620c067) accessor byte-equality pin on the peer typed-`u64`
6557 // optional-scalar axis, extended to the wasm-instruction-budget
6558 // shape — second `Option<Copy-T>`-return accessor on the M2
6559 // slot family. Pins against a future silent detour that
6560 // re-derived the fuel budget from a peer axis (an accidental
6561 // `.memory`-collapse that assumed the two `Option<u64>` axes
6562 // carry the same value — the two axes share a shape but not
6563 // a semantic, `:memory` counts linear-memory bytes and `:fuel`
6564 // counts wasm instructions), a `None` → `Some(0)` "zero means
6565 // unbounded" collapse (the canonical `Option<u64>` → `u64`
6566 // collapse footgun the [`LimitsError::FuelZero`] validate arm
6567 // guards on the peer zero-floor axis; wasmtime interprets
6568 // `fuel=0` as "trap the first instruction" not "no bound"), or
6569 // a per-arm variant swap that landed on one consumer without
6570 // the other.
6571 for fuel in [None, Some(1_u64), Some(1_000_000_u64)] {
6572 let l = LimitsSpec {
6573 fuel,
6574 ..LimitsSpec::default()
6575 };
6576 assert_eq!(
6577 l.fuel(),
6578 fuel,
6579 "LimitsSpec::fuel must return :limits :fuel verbatim \
6580 (got {:?}, expected {fuel:?})",
6581 l.fuel(),
6582 );
6583 assert_eq!(
6584 l.fuel(),
6585 l.fuel,
6586 "LimitsSpec::fuel must byte-equal the raw .fuel \
6587 field access across every value in the accept-set",
6588 );
6589 }
6590 }
6591
6592 #[test]
6593 fn limits_is_empty_fuel_arm_routes_through_accessor() {
6594 // Composition pin: [`LimitsSpec::is_empty`]'s `fuel` arm
6595 // must key off [`LimitsSpec::fuel`], not the raw `.fuel`
6596 // field access. Structurally: setting ONLY the `fuel` slot
6597 // on an otherwise-default LimitsSpec must flip `is_empty()`
6598 // from `true` (all-`None`) to `false` (one axis carries a
6599 // value); the flip must be observed across every value in
6600 // the accept-set since the emptiness semantic reads "any
6601 // axis carries a value" — not "any axis carries a value
6602 // above a threshold" — the same non-collapsing shape the
6603 // sibling M3 [`crate::MeshPolicy::is_empty`] predicate
6604 // carries on its peer `Option<Copy-T>`-typed slot surfaces
6605 // and the sibling per-`:limits` [`LimitsSpec::memory`]
6606 // (620c067) `is_empty()` accessor-composition pin carries on
6607 // the peer `Option<u64>` axis.
6608 //
6609 // Pins against a future silent detour that re-derived the
6610 // emptiness predicate off a peer axis (an accidental
6611 // `.memory.is_none()`-only chain that dropped the `fuel` arm
6612 // entirely), an accessor-side detour that no longer names the
6613 // substrate-primitive typed dispatch (an accidental
6614 // `self.fuel.unwrap_or(0) == 0` fallback in the accessor
6615 // that would silently classify both `None` and `Some(0)` as
6616 // the same value — a footgun the [`LimitsError::FuelZero`]
6617 // validate arm explicitly closes since `fuel=0` traps rather
6618 // than expresses "unbounded"), or a threshold collapse (a
6619 // `self.fuel().is_some_and(|f| f > 0)` that would silently
6620 // classify `Some(0)` as unset).
6621 //
6622 // Peer of the sibling per-`:limits` [`LimitsSpec::memory`]
6623 // (620c067) `is_empty` composition pin on the peer
6624 // `Option<u64>` axis — same "the emptiness predicate must
6625 // route through the substrate-primitive typed dispatch"
6626 // discipline extended onto the peer per-`:limits` `:fuel`
6627 // arm.
6628 let empty = LimitsSpec::default();
6629 assert!(
6630 empty.is_empty(),
6631 "LimitsSpec::default() must be is_empty() — every axis \
6632 defaults to None",
6633 );
6634 for fuel in [Some(1_u64), Some(1_000_000_u64), Some(LIMITS_FUEL_MAX)] {
6635 let l = LimitsSpec {
6636 fuel,
6637 ..LimitsSpec::default()
6638 };
6639 assert!(
6640 !l.is_empty(),
6641 "LimitsSpec::is_empty must return false when :fuel \
6642 is {fuel:?} — the emptiness predicate reads \"any \
6643 axis carries a value\", not \"any axis carries a \
6644 value above a threshold\"",
6645 );
6646 assert_eq!(
6647 l.fuel().is_none(),
6648 l.is_empty(),
6649 "when :fuel is the only set axis, is_empty() must \
6650 equal fuel().is_none() — the accessor and the \
6651 emptiness predicate must route through the same \
6652 substrate-primitive typed dispatch on the :fuel \
6653 arm",
6654 );
6655 }
6656 }
6657
6658 #[test]
6659 fn limits_fuel_projects_option_u64_by_copy() {
6660 // The by-copy pin: [`LimitsSpec::fuel`] returns `Option<u64>`
6661 // by copy — `Option<u64>` is `Copy` and the accessor must
6662 // return by value, not by reference. Peer of the sibling per-
6663 // `:limits` [`LimitsSpec::memory`] (620c067) copy-invariant
6664 // pin on the peer `Option<u64>` shape — the accessor's
6665 // returned `Option<u64>` must outlive `&self` (multiple calls
6666 // must return equal values from a dropped-`&self` copy, since
6667 // the returned Option carries no borrow), and calling the
6668 // accessor twice on the same LimitsSpec must yield the same
6669 // `Option<u64>` verbatim (idempotent, no side effects on
6670 // `&self`).
6671 //
6672 // Pins against a future silent detour that returned
6673 // `Option<&u64>` (which would type-check but silently break
6674 // every downstream caller — the future `wasmtime::Store::set_fuel`
6675 // wire path consumes `u64` by value and `&u64` would fold to
6676 // a detached copy at the call site), an accidental
6677 // `Option::as_ref()` projection (`self.fuel.as_ref()` would
6678 // also type-check but return `Option<&u64>`), or a one-arm-
6679 // only accessor that reads `Some(*f)` in the Some arm but
6680 // reads a fresh `Default::default()` in the None arm.
6681 for fuel in [
6682 None,
6683 Some(1_u64),
6684 Some(1_000_000_u64),
6685 Some(LIMITS_FUEL_MAX),
6686 ] {
6687 let l = LimitsSpec {
6688 fuel,
6689 ..LimitsSpec::default()
6690 };
6691 let first = l.fuel();
6692 let second = l.fuel();
6693 assert_eq!(
6694 first, second,
6695 "LimitsSpec::fuel must be idempotent — two \
6696 successive calls on the same &self must return the \
6697 same Option<u64>",
6698 );
6699 assert_eq!(
6700 first, fuel,
6701 "LimitsSpec::fuel must return :limits :fuel \
6702 verbatim by copy — got {first:?}, expected {fuel:?}",
6703 );
6704 }
6705 }
6706
6707 // ── per-`:limits :wall-clock` accessor pins (LimitsSpec::wall_clock) ─
6708
6709 #[test]
6710 fn limits_wall_clock_returns_option_duration_byte_equal_across_permutations() {
6711 // The canonical per-`:limits` `:wall-clock` wasmtime-per-call
6712 // wall-clock deadline scalar pin: [`LimitsSpec::wall_clock`]
6713 // must return the `:limits :wall-clock` typed `Duration`
6714 // verbatim as an `Option<Duration>`, byte-equal to the raw
6715 // field access across the three canonical shape-arms — `None`
6716 // (no wall-clock deadline declared — engine-default applies),
6717 // `Some(Duration::from_millis(1))` (the structural minimum a
6718 // validated `:limits :wall-clock` may carry, the
6719 // integer-millisecond floor
6720 // [`LimitsError::WallClockNotCanonical`] rejects everything
6721 // sub-ms; `Duration::ZERO` is separately rejected by
6722 // [`LimitsError::WallClockZero`]), `Some(Duration::from_secs(30))`
6723 // (the canonical 30s deadline the module-level docstring
6724 // names).
6725 //
6726 // Peer of the sibling per-`:limits` [`LimitsSpec::memory`]
6727 // (620c067) / [`LimitsSpec::fuel`] (795dee7) accessor
6728 // byte-equality pins on the peer typed-`u64` optional-scalar
6729 // axes, extended to the wall-clock-deadline `Option<Duration>`
6730 // shape — third `Option<Copy-T>`-return accessor on the M2 slot
6731 // family. Sibling to [`crate::MeshPolicy::timeout`] (7073d0f) on
6732 // the M3 mesh-slot family's peer `Option<Duration>` accessor
6733 // axis — same typed-`Duration` shape extended from the M3
6734 // per-call-timeout axis to the M2 per-outermost-call-deadline
6735 // axis. Pins against a future silent detour that re-derived the
6736 // wall-clock deadline from a peer axis (an accidental
6737 // `.fuel`-collapse that assumed the wall-clock deadline and
6738 // the fuel budget carry the same value — the two axes serve
6739 // different sandboxing purposes, wall-clock tracks scheduler
6740 // real time and fuel tracks wasm instructions), a `None` →
6741 // `Some(Duration::ZERO)` "zero means unbounded" collapse (the
6742 // canonical `Option<Duration>` → `Duration` collapse footgun
6743 // the [`LimitsError::WallClockZero`] validate arm guards on the
6744 // peer zero-floor axis; a zero deadline traps the first
6745 // instruction), or a per-arm variant swap that landed on one
6746 // consumer without the other.
6747 for wall_clock in [
6748 None,
6749 Some(Duration::from_millis(1)),
6750 Some(Duration::from_secs(30)),
6751 ] {
6752 let l = LimitsSpec {
6753 wall_clock,
6754 ..LimitsSpec::default()
6755 };
6756 assert_eq!(
6757 l.wall_clock(),
6758 wall_clock,
6759 "LimitsSpec::wall_clock must return :limits :wall-clock verbatim \
6760 (got {:?}, expected {wall_clock:?})",
6761 l.wall_clock(),
6762 );
6763 assert_eq!(
6764 l.wall_clock(),
6765 l.wall_clock,
6766 "LimitsSpec::wall_clock must byte-equal the raw .wall_clock \
6767 field access across every value in the accept-set",
6768 );
6769 }
6770 }
6771
6772 #[test]
6773 fn limits_is_empty_wall_clock_arm_routes_through_accessor() {
6774 // Composition pin: [`LimitsSpec::is_empty`]'s `wall_clock` arm
6775 // must key off [`LimitsSpec::wall_clock`], not the raw
6776 // `.wall_clock` field access. Structurally: setting ONLY the
6777 // `wall_clock` slot on an otherwise-default LimitsSpec must
6778 // flip `is_empty()` from `true` (all-`None`) to `false` (one
6779 // axis carries a value); the flip must be observed across every
6780 // value in the accept-set since the emptiness semantic reads
6781 // "any axis carries a value" — not "any axis carries a value
6782 // above a threshold" — the same non-collapsing shape the
6783 // sibling M3 [`crate::MeshPolicy::is_empty`] predicate carries
6784 // on its peer `Option<Copy-T>`-typed slot surfaces and the
6785 // sibling per-`:limits` [`LimitsSpec::memory`] (620c067) /
6786 // [`LimitsSpec::fuel`] (795dee7) `is_empty()` accessor-
6787 // composition pins carry on the peer `Option<u64>` axes.
6788 //
6789 // Pins against a future silent detour that re-derived the
6790 // emptiness predicate off a peer axis (an accidental
6791 // `.memory.is_none()`-only chain that dropped the `wall_clock`
6792 // arm entirely), an accessor-side detour that no longer names
6793 // the substrate-primitive typed dispatch (an accidental
6794 // `self.wall_clock.unwrap_or(Duration::ZERO).is_zero()` fallback
6795 // in the accessor that would silently classify both `None` and
6796 // `Some(Duration::ZERO)` as the same value — a footgun the
6797 // [`LimitsError::WallClockZero`] validate arm explicitly closes
6798 // since a zero deadline traps rather than expresses
6799 // "unbounded"), or a threshold collapse (a
6800 // `self.wall_clock().is_some_and(|w| !w.is_zero())` that would
6801 // silently classify `Some(Duration::ZERO)` as unset).
6802 //
6803 // Peer of the sibling per-`:limits` [`LimitsSpec::memory`]
6804 // (620c067) / [`LimitsSpec::fuel`] (795dee7) `is_empty`
6805 // composition pins on the peer `Option<u64>` axes — same "the
6806 // emptiness predicate must route through the substrate-
6807 // primitive typed dispatch" discipline extended onto the peer
6808 // per-`:limits` `:wall-clock` arm.
6809 let empty = LimitsSpec::default();
6810 assert!(
6811 empty.is_empty(),
6812 "LimitsSpec::default() must be is_empty() — every axis \
6813 defaults to None",
6814 );
6815 for wall_clock in [
6816 Some(Duration::from_millis(1)),
6817 Some(Duration::from_secs(30)),
6818 Some(LIMITS_WALL_CLOCK_MAX),
6819 ] {
6820 let l = LimitsSpec {
6821 wall_clock,
6822 ..LimitsSpec::default()
6823 };
6824 assert!(
6825 !l.is_empty(),
6826 "LimitsSpec::is_empty must return false when :wall-clock \
6827 is {wall_clock:?} — the emptiness predicate reads \"any \
6828 axis carries a value\", not \"any axis carries a \
6829 value above a threshold\"",
6830 );
6831 assert_eq!(
6832 l.wall_clock().is_none(),
6833 l.is_empty(),
6834 "when :wall-clock is the only set axis, is_empty() must \
6835 equal wall_clock().is_none() — the accessor and the \
6836 emptiness predicate must route through the same \
6837 substrate-primitive typed dispatch on the :wall-clock \
6838 arm",
6839 );
6840 }
6841 }
6842
6843 #[test]
6844 fn limits_wall_clock_projects_option_duration_by_copy() {
6845 // The by-copy pin: [`LimitsSpec::wall_clock`] returns
6846 // `Option<Duration>` by copy — `Duration` is `Copy` (so
6847 // `Option<Duration>` is `Copy`) and the accessor must return by
6848 // value, not by reference. Peer of the sibling per-`:limits`
6849 // [`LimitsSpec::memory`] (620c067) / [`LimitsSpec::fuel`]
6850 // (795dee7) copy-invariant pins on the peer `Option<u64>`
6851 // shape, extended onto the peer `Option<Duration>` shape — the
6852 // accessor's returned `Option<Duration>` must outlive `&self`
6853 // (multiple calls must return equal values from a dropped-
6854 // `&self` copy, since the returned Option carries no borrow),
6855 // and calling the accessor twice on the same LimitsSpec must
6856 // yield the same `Option<Duration>` verbatim (idempotent, no
6857 // side effects on `&self`).
6858 //
6859 // Pins against a future silent detour that returned
6860 // `Option<&Duration>` (which would type-check but silently
6861 // break every downstream caller — the future
6862 // `wasmtime::Store::epoch_deadline_*` wire path consumes
6863 // `Duration` by value and `&Duration` would fold to a detached
6864 // copy at the call site), an accidental `Option::as_ref()`
6865 // projection (`self.wall_clock.as_ref()` would also type-check
6866 // but return `Option<&Duration>`), or a one-arm-only accessor
6867 // that reads `Some(*w)` in the Some arm but reads a fresh
6868 // `Default::default()` (which would collapse to
6869 // `Duration::ZERO`, not `None`) in the None arm.
6870 for wall_clock in [
6871 None,
6872 Some(Duration::from_millis(1)),
6873 Some(Duration::from_secs(30)),
6874 Some(LIMITS_WALL_CLOCK_MAX),
6875 ] {
6876 let l = LimitsSpec {
6877 wall_clock,
6878 ..LimitsSpec::default()
6879 };
6880 let first = l.wall_clock();
6881 let second = l.wall_clock();
6882 assert_eq!(
6883 first, second,
6884 "LimitsSpec::wall_clock must be idempotent — two \
6885 successive calls on the same &self must return the \
6886 same Option<Duration>",
6887 );
6888 assert_eq!(
6889 first, wall_clock,
6890 "LimitsSpec::wall_clock must return :limits :wall-clock \
6891 verbatim by copy — got {first:?}, expected {wall_clock:?}",
6892 );
6893 }
6894 }
6895
6896 // ── per-`:limits :cpu` accessor pins (LimitsSpec::cpu) ───────────
6897
6898 #[test]
6899 fn limits_cpu_returns_option_u32_byte_equal_across_permutations() {
6900 // The canonical per-`:limits` `:cpu` Kubernetes-millicore
6901 // soft cgroup-share scalar pin: [`LimitsSpec::cpu`] must return
6902 // the `:limits :cpu` typed `u32` verbatim as an `Option<u32>`,
6903 // byte-equal to the raw field access across the three canonical
6904 // shape-arms — `None` (no cgroup share declared —
6905 // scheduler-default applies), `Some(1)` (the structural minimum
6906 // a validated `:limits :cpu` may carry, one millicore; a zero
6907 // cgroup share is separately rejected by
6908 // [`LimitsError::CpuZero`]), `Some(500)` (the canonical 500m
6909 // half-a-core share the in-tree
6910 // `limits_slot_propagates_into_values_block` smoke test carries
6911 // as the load-bearing example, peer to the `caixa-flux`
6912 // projector's identical 500m default).
6913 //
6914 // Peer of the sibling per-`:limits` [`LimitsSpec::memory`]
6915 // (620c067) / [`LimitsSpec::fuel`] (795dee7) /
6916 // [`LimitsSpec::wall_clock`] (8cb717b) accessor byte-equality
6917 // pins on the peer typed-`u64` / `u64` / `Duration`
6918 // optional-scalar axes, extended to the cgroup-cpu-share
6919 // `Option<u32>` shape — fourth and final `Option<Copy-T>`-return
6920 // accessor on the M2 slot family, closing the M2 `:limits`
6921 // `Option<Copy-T>` accessor axis. Sibling to
6922 // [`crate::MeshPolicy::retries`] (bdfb399) on the M3 mesh-slot
6923 // family's peer `Option<u32>` accessor axis — same typed-`u32`
6924 // shape extended from the M3 per-edge-transient-failure-retry-
6925 // budget axis to the M2 per-process-cgroup-cpu-share axis.
6926 // Pins against a future silent detour that re-derived the cpu
6927 // share from a peer axis (an accidental `.retries`-collapse that
6928 // assumed the two `Option<u32>` axes carry the same value — the
6929 // two axes share a shape but not a semantic, M2 `:cpu` counts
6930 // millicores of soft cgroup share and M3 `:retries` counts
6931 // per-edge transient-failure retry budget), a `None` → `Some(0)`
6932 // "zero means unbounded" collapse (the canonical `Option<u32>` →
6933 // `u32` collapse footgun the [`LimitsError::CpuZero`] validate
6934 // arm guards on the peer zero-floor axis; a zero cgroup share
6935 // starves the process rather than expressing "unbounded"), or a
6936 // per-arm variant swap that landed on one consumer without the
6937 // other.
6938 for cpu in [None, Some(1_u32), Some(500_u32)] {
6939 let l = LimitsSpec {
6940 cpu,
6941 ..LimitsSpec::default()
6942 };
6943 assert_eq!(
6944 l.cpu(),
6945 cpu,
6946 "LimitsSpec::cpu must return :limits :cpu verbatim \
6947 (got {:?}, expected {cpu:?})",
6948 l.cpu(),
6949 );
6950 assert_eq!(
6951 l.cpu(),
6952 l.cpu,
6953 "LimitsSpec::cpu must byte-equal the raw .cpu \
6954 field access across every value in the accept-set",
6955 );
6956 }
6957 }
6958
6959 #[test]
6960 fn limits_is_empty_cpu_arm_routes_through_accessor() {
6961 // Composition pin: [`LimitsSpec::is_empty`]'s `cpu` arm must key
6962 // off [`LimitsSpec::cpu`], not the raw `.cpu` field access.
6963 // Structurally: setting ONLY the `cpu` slot on an
6964 // otherwise-default LimitsSpec must flip `is_empty()` from
6965 // `true` (all-`None`) to `false` (one axis carries a value);
6966 // the flip must be observed across every value in the
6967 // accept-set since the emptiness semantic reads "any axis
6968 // carries a value" — not "any axis carries a value above a
6969 // threshold" — the same non-collapsing shape the sibling M3
6970 // [`crate::MeshPolicy::is_empty`] predicate carries on its
6971 // peer `Option<Copy-T>`-typed slot surfaces and the sibling
6972 // per-`:limits` [`LimitsSpec::memory`] (620c067) /
6973 // [`LimitsSpec::fuel`] (795dee7) / [`LimitsSpec::wall_clock`]
6974 // (8cb717b) `is_empty()` accessor-composition pins carry on the
6975 // peer `Option<u64>` / `Option<u64>` / `Option<Duration>` axes.
6976 //
6977 // Pins against a future silent detour that re-derived the
6978 // emptiness predicate off a peer axis (an accidental
6979 // `.memory.is_none()`-only chain that dropped the `cpu` arm
6980 // entirely), an accessor-side detour that no longer names the
6981 // substrate-primitive typed dispatch (an accidental
6982 // `self.cpu.unwrap_or(0) == 0` fallback in the accessor that
6983 // would silently classify both `None` and `Some(0)` as the same
6984 // value — a footgun the [`LimitsError::CpuZero`] validate arm
6985 // explicitly closes since a zero cgroup share starves the
6986 // process rather than expressing "unbounded"), or a threshold
6987 // collapse (a `self.cpu().is_some_and(|m| m > 0)` that would
6988 // silently classify `Some(0)` as unset).
6989 //
6990 // Peer of the sibling per-`:limits` [`LimitsSpec::memory`]
6991 // (620c067) / [`LimitsSpec::fuel`] (795dee7) /
6992 // [`LimitsSpec::wall_clock`] (8cb717b) `is_empty` composition
6993 // pins on the peer `Option<u64>` / `Option<u64>` /
6994 // `Option<Duration>` axes — same "the emptiness predicate must
6995 // route through the substrate-primitive typed dispatch"
6996 // discipline extended onto the peer per-`:limits` `:cpu` arm.
6997 // Closes the M2 `:limits` `is_empty`-composition family — every
6998 // arm now routes through its typed accessor, no open-coded
6999 // field access remains.
7000 let empty = LimitsSpec::default();
7001 assert!(
7002 empty.is_empty(),
7003 "LimitsSpec::default() must be is_empty() — every axis \
7004 defaults to None",
7005 );
7006 for cpu in [Some(1_u32), Some(500_u32), Some(LIMITS_CPU_MILLICORES_MAX)] {
7007 let l = LimitsSpec {
7008 cpu,
7009 ..LimitsSpec::default()
7010 };
7011 assert!(
7012 !l.is_empty(),
7013 "LimitsSpec::is_empty must return false when :cpu \
7014 is {cpu:?} — the emptiness predicate reads \"any \
7015 axis carries a value\", not \"any axis carries a \
7016 value above a threshold\"",
7017 );
7018 assert_eq!(
7019 l.cpu().is_none(),
7020 l.is_empty(),
7021 "when :cpu is the only set axis, is_empty() must \
7022 equal cpu().is_none() — the accessor and the \
7023 emptiness predicate must route through the same \
7024 substrate-primitive typed dispatch on the :cpu \
7025 arm",
7026 );
7027 }
7028 }
7029
7030 #[test]
7031 fn limits_cpu_projects_option_u32_by_copy() {
7032 // The by-copy pin: [`LimitsSpec::cpu`] returns `Option<u32>` by
7033 // copy — `Option<u32>` is `Copy` and the accessor must return
7034 // by value, not by reference. Peer of the sibling per-`:limits`
7035 // [`LimitsSpec::memory`] (620c067) / [`LimitsSpec::fuel`]
7036 // (795dee7) / [`LimitsSpec::wall_clock`] (8cb717b)
7037 // copy-invariant pins on the peer `Option<u64>` / `Option<u64>`
7038 // / `Option<Duration>` shapes, extended onto the peer
7039 // `Option<u32>` copy-invariant shape — the accessor's returned
7040 // `Option<u32>` must outlive `&self` (multiple calls must
7041 // return equal values from a dropped-`&self` copy, since the
7042 // returned Option carries no borrow), and calling the accessor
7043 // twice on the same LimitsSpec must yield the same
7044 // `Option<u32>` verbatim (idempotent, no side effects on
7045 // `&self`).
7046 //
7047 // Pins against a future silent detour that returned
7048 // `Option<&u32>` (which would type-check but silently break
7049 // every downstream caller — the future K8s pod-spec
7050 // `resources.requests.cpu` wire path consumes `u32` by value
7051 // and `&u32` would fold to a detached copy at the call site),
7052 // an accidental `Option::as_ref()` projection
7053 // (`self.cpu.as_ref()` would also type-check but return
7054 // `Option<&u32>`), or a one-arm-only accessor that reads
7055 // `Some(*m)` in the Some arm but reads a fresh
7056 // `Default::default()` in the None arm.
7057 for cpu in [
7058 None,
7059 Some(1_u32),
7060 Some(500_u32),
7061 Some(LIMITS_CPU_MILLICORES_MAX),
7062 ] {
7063 let l = LimitsSpec {
7064 cpu,
7065 ..LimitsSpec::default()
7066 };
7067 let first = l.cpu();
7068 let second = l.cpu();
7069 assert_eq!(
7070 first, second,
7071 "LimitsSpec::cpu must be idempotent — two \
7072 successive calls on the same &self must return the \
7073 same Option<u32>",
7074 );
7075 assert_eq!(
7076 first, cpu,
7077 "LimitsSpec::cpu must return :limits :cpu \
7078 verbatim by copy — got {first:?}, expected {cpu:?}",
7079 );
7080 }
7081 }
7082
7083 // ── LimitsError ctor macro-family equivalence pins ───────────────
7084 //
7085 // Peer discipline of the sibling `layout_violation_ctors!`
7086 // `*_ctor_matches_struct_literal_wrap` pin family (131ca0d) on
7087 // [`LayoutError`], and the sibling `aplicacao_field_reason_ctors!`
7088 // / `contrato_target_ctors!` / `contrato_empty_pair_ctors!` /
7089 // `contrato_pair_value_reason_ctors!` `*_ctor_matches_struct_literal_wrap`
7090 // pin families (981060b / 14b81d5 / 8580068 / 14e13f1) on
7091 // [`AplicacaoError`]. A silent regression that de-folded one variant
7092 // and re-inlined the pre-lift struct-literal at one wire-up site
7093 // (or dropped a field, or diverged the string conversion on one
7094 // arm) trips the affected variant's pin first, so every future edit
7095 // to a variant on the three shared envelopes lands in exactly one
7096 // place.
7097
7098 #[test]
7099 fn non_integer_byte_magnitude_ctor_matches_struct_literal_wrap() {
7100 let value = "1.5KiB";
7101 assert_eq!(
7102 LimitsError::non_integer_byte_magnitude(value),
7103 LimitsError::NonIntegerByteMagnitude {
7104 value: value.to_string(),
7105 },
7106 );
7107 }
7108
7109 #[test]
7110 fn leading_zero_byte_magnitude_ctor_matches_struct_literal_wrap() {
7111 let value = "064MiB";
7112 assert_eq!(
7113 LimitsError::leading_zero_byte_magnitude(value),
7114 LimitsError::LeadingZeroByteMagnitude {
7115 value: value.to_string(),
7116 },
7117 );
7118 }
7119
7120 #[test]
7121 fn non_integer_duration_magnitude_ctor_matches_struct_literal_wrap() {
7122 let value = "1.5s";
7123 assert_eq!(
7124 LimitsError::non_integer_duration_magnitude(value),
7125 LimitsError::NonIntegerDurationMagnitude {
7126 value: value.to_string(),
7127 },
7128 );
7129 }
7130
7131 #[test]
7132 fn leading_zero_duration_magnitude_ctor_matches_struct_literal_wrap() {
7133 let value = "030s";
7134 assert_eq!(
7135 LimitsError::leading_zero_duration_magnitude(value),
7136 LimitsError::LeadingZeroDurationMagnitude {
7137 value: value.to_string(),
7138 },
7139 );
7140 }
7141
7142 #[test]
7143 fn non_integer_millicore_magnitude_ctor_matches_struct_literal_wrap() {
7144 let value = "1.5";
7145 assert_eq!(
7146 LimitsError::non_integer_millicore_magnitude(value),
7147 LimitsError::NonIntegerMillicoreMagnitude {
7148 value: value.to_string(),
7149 },
7150 );
7151 }
7152
7153 #[test]
7154 fn leading_zero_millicore_magnitude_ctor_matches_struct_literal_wrap() {
7155 let value = "0500m";
7156 assert_eq!(
7157 LimitsError::leading_zero_millicore_magnitude(value),
7158 LimitsError::LeadingZeroMillicoreMagnitude {
7159 value: value.to_string(),
7160 },
7161 );
7162 }
7163
7164 #[test]
7165 fn unknown_byte_unit_ctor_matches_struct_literal_wrap() {
7166 // Per-variant byte-equality pin on the `limits_codec_unit_only_ctors!`
7167 // macro's `unknown_byte_unit => UnknownByteUnit` arm. Pins the ctor's
7168 // byte-identity against the open-coded pre-lift struct-literal on the
7169 // same `unit: &str` fixture (the `parse_byte_size` unit-dispatch
7170 // fallthrough hits this arm on any authored unit outside the
7171 // `KB | MB | GB | KiB | MiB | GiB | "" | B` alphabet — pick a
7172 // typography-space suffix so the pin exercises the same Unicode-
7173 // whitespace-in-alpha class the two codecs share). A silent regression
7174 // that de-folded the variant and re-inlined the struct-literal at the
7175 // wire-up (or swapped `.to_string()` for a different `String`
7176 // conversion, or dropped the field) trips the assertion under
7177 // `PartialEq`.
7178 let unit = "TiB";
7179 assert_eq!(
7180 LimitsError::unknown_byte_unit(unit),
7181 LimitsError::UnknownByteUnit {
7182 unit: unit.to_string(),
7183 },
7184 );
7185 }
7186
7187 #[test]
7188 fn unknown_duration_unit_ctor_matches_struct_literal_wrap() {
7189 // Per-variant byte-equality pin on the `limits_codec_unit_only_ctors!`
7190 // macro's `unknown_duration_unit => UnknownDurationUnit` arm. Pins the
7191 // ctor's byte-identity against the open-coded pre-lift struct-literal
7192 // on the same `unit: &str` fixture (the `parse_duration` reverse-map
7193 // arm on [`crate::render::DurationUnitError::UnknownUnit`] hits this
7194 // arm on any authored unit outside the `ms | s | "" | m | h`
7195 // alphabet). Peer of the sibling `unknown_byte_unit` pin above on the
7196 // same shared `{ unit: String }` envelope.
7197 let unit = "d";
7198 assert_eq!(
7199 LimitsError::unknown_duration_unit(unit),
7200 LimitsError::UnknownDurationUnit {
7201 unit: unit.to_string(),
7202 },
7203 );
7204 }
7205
7206 #[test]
7207 fn limits_codec_unit_only_ctors_route_unit_verbatim_across_every_variant() {
7208 // Cross-variant sweep: routes each per-variant `unit: &str` scalar
7209 // through the sole `$ctor => $variant` axis the
7210 // `limits_codec_unit_only_ctors!` macro exposes across a boundary-
7211 // covering fixture set (empty string; the ASCII fallthrough shape the
7212 // two codec wire-up sites actually raise; a Unicode-whitespace-in-
7213 // alpha shape covered by the sibling `parse_*` reject-whitespace
7214 // primitive but plausibly reachable from a future consumer that
7215 // pre-strips whitespace before invoking the ctor directly; a
7216 // multi-byte non-ASCII unit alphabet extension). Any wrapper-side
7217 // truncation, silent `.into()` divergence, per-arm constant
7218 // substitution, or accidental cross-variant field swap on either
7219 // ctor surfaces here on the first fixture the two implementations
7220 // disagree on rather than at a downstream diagnostic-shape drift
7221 // (`LimitsError::to_string()` embeds the offending unit verbatim
7222 // through the `Display`/`Error` derive — a divergence at the ctor
7223 // layer flows straight to the surface diagnostic).
7224 for unit in ["", "TiB", "\u{00A0}", "μs"] {
7225 assert_eq!(
7226 LimitsError::unknown_byte_unit(unit),
7227 LimitsError::UnknownByteUnit {
7228 unit: unit.to_string(),
7229 },
7230 );
7231 assert_eq!(
7232 LimitsError::unknown_duration_unit(unit),
7233 LimitsError::UnknownDurationUnit {
7234 unit: unit.to_string(),
7235 },
7236 );
7237 }
7238 }
7239
7240 #[test]
7241 fn whitespace_in_byte_size_ctor_matches_struct_literal_wrap() {
7242 let value = " 64MiB";
7243 let byte: u8 = 0x20;
7244 assert_eq!(
7245 LimitsError::whitespace_in_byte_size(value, byte),
7246 LimitsError::WhitespaceInByteSize {
7247 value: value.to_string(),
7248 byte,
7249 },
7250 );
7251 }
7252
7253 #[test]
7254 fn whitespace_in_duration_ctor_matches_struct_literal_wrap() {
7255 let value = " 30s";
7256 let byte: u8 = 0x09;
7257 assert_eq!(
7258 LimitsError::whitespace_in_duration(value, byte),
7259 LimitsError::WhitespaceInDuration {
7260 value: value.to_string(),
7261 byte,
7262 },
7263 );
7264 }
7265
7266 #[test]
7267 fn whitespace_in_millicores_ctor_matches_struct_literal_wrap() {
7268 let value = " 500m";
7269 let byte: u8 = 0x0A;
7270 assert_eq!(
7271 LimitsError::whitespace_in_millicores(value, byte),
7272 LimitsError::WhitespaceInMillicores {
7273 value: value.to_string(),
7274 byte,
7275 },
7276 );
7277 }
7278
7279 #[test]
7280 fn non_ascii_whitespace_in_byte_size_ctor_matches_struct_literal_wrap() {
7281 let value = "\u{00A0}64MiB";
7282 let ch = '\u{00A0}';
7283 assert_eq!(
7284 LimitsError::non_ascii_whitespace_in_byte_size(value, ch),
7285 LimitsError::NonAsciiWhitespaceInByteSize {
7286 value: value.to_string(),
7287 ch,
7288 codepoint: ch as u32,
7289 },
7290 );
7291 }
7292
7293 #[test]
7294 fn non_ascii_whitespace_in_duration_ctor_matches_struct_literal_wrap() {
7295 let value = "30s\u{2028}";
7296 let ch = '\u{2028}';
7297 assert_eq!(
7298 LimitsError::non_ascii_whitespace_in_duration(value, ch),
7299 LimitsError::NonAsciiWhitespaceInDuration {
7300 value: value.to_string(),
7301 ch,
7302 codepoint: ch as u32,
7303 },
7304 );
7305 }
7306
7307 #[test]
7308 fn non_ascii_whitespace_in_millicores_ctor_matches_struct_literal_wrap() {
7309 let value = "500\u{2003}m";
7310 let ch = '\u{2003}';
7311 assert_eq!(
7312 LimitsError::non_ascii_whitespace_in_millicores(value, ch),
7313 LimitsError::NonAsciiWhitespaceInMillicores {
7314 value: value.to_string(),
7315 ch,
7316 codepoint: ch as u32,
7317 },
7318 );
7319 }
7320
7321 #[test]
7322 fn limits_codec_value_char_ctors_route_codepoint_through_ch_as_u32_uniformly() {
7323 // Cross-family sweep: the load-bearing `codepoint = ch as u32`
7324 // derivation is now spelled once — inside the
7325 // `limits_codec_value_char_ctors!` macro body — rather than
7326 // three times at each wire-up. A silent regression that
7327 // de-folded one variant and re-inlined the derivation with a
7328 // different width (`ch as u16`, `ch as i32`) or dropped it
7329 // entirely trips here on the very first codepoint the two
7330 // implementations disagree on. Every non-ASCII Unicode
7331 // whitespace codepoint the sibling
7332 // `crate::render::find_non_ascii_whitespace_char` predicate
7333 // yields is a valid `char`, so `ch as u32` covers the full
7334 // domain the wire-ups reach.
7335 for ch in [
7336 '\u{00A0}', // NBSP
7337 '\u{2028}', // LINE SEPARATOR
7338 '\u{2003}', // EM SPACE
7339 '\u{202F}', // NARROW NO-BREAK SPACE
7340 '\u{3000}', // IDEOGRAPHIC SPACE
7341 ] {
7342 let value = format!("prefix{ch}suffix");
7343 let expected_codepoint = ch as u32;
7344 assert!(matches!(
7345 LimitsError::non_ascii_whitespace_in_byte_size(&value, ch),
7346 LimitsError::NonAsciiWhitespaceInByteSize { codepoint, .. } if codepoint == expected_codepoint,
7347 ));
7348 assert!(matches!(
7349 LimitsError::non_ascii_whitespace_in_duration(&value, ch),
7350 LimitsError::NonAsciiWhitespaceInDuration { codepoint, .. } if codepoint == expected_codepoint,
7351 ));
7352 assert!(matches!(
7353 LimitsError::non_ascii_whitespace_in_millicores(&value, ch),
7354 LimitsError::NonAsciiWhitespaceInMillicores { codepoint, .. } if codepoint == expected_codepoint,
7355 ));
7356 }
7357 }
7358
7359 // ── limits_scalar_ctors! per-variant + cross-axis pins ──────────────────
7360 //
7361 // Per-variant byte-equality pins guaranteeing every generated ctor arm in
7362 // the [`limits_scalar_ctors!`] macro produces a `LimitsError` structurally
7363 // identical to the pre-lift `Self::<variant> { <field>: <val> }` one-line
7364 // struct-literal on the same `Copy`-`u64 | u32 | Duration` fixture, plus
7365 // one cross-axis sweep that routes each per-variant `<field>: <ty>` scalar
7366 // through the sole `$field:ident: $ty:ty` axis the macro exposes so any
7367 // wrapper-side truncation / re-order / silent `.into()` / silent constant-
7368 // substitution on any one variant surfaces here rather than at a
7369 // downstream per-`:limits` diagnostic-shape drift, plus one `const`-eval
7370 // pin that fires at compile time if any future edit silently drops the
7371 // `const` qualifier from the macro body. Peer of the sibling per-variant
7372 // pins on [`crate::supervisor::supervisor_scalar_ctors!`] (f0f77a2, the
7373 // 4-variant `SupervisorError` `{ <field>: RestartStrategy | u32 |
7374 // Duration }` fold on the per-`:supervisor` scalar axis) and the peer
7375 // [`crate::aplicacao::aplicacao_policy_scalar_ctors!`] (7ef425e, the
7376 // 8-variant `AplicacaoError` `{ <field>: Duration | u32 }` fold on the
7377 // per-`:politicas` per-axis cap / canonical-form arms).
7378 #[test]
7379 fn memory_below_wasm32_page_ctor_matches_struct_literal_wrap() {
7380 let bytes = LIMITS_MEMORY_WASM32_PAGE_BYTES - 1;
7381 assert_eq!(
7382 LimitsError::memory_below_wasm32_page(bytes),
7383 LimitsError::MemoryBelowWasm32Page { bytes },
7384 "generated memory_below_wasm32_page ctor must produce byte-equal \
7385 `LimitsError::MemoryBelowWasm32Page` to the pre-lift struct-literal \
7386 wrap on the same `Copy`-`u64` fixture",
7387 );
7388 }
7389
7390 #[test]
7391 fn memory_exceeds_wasm32_cap_ctor_matches_struct_literal_wrap() {
7392 let bytes = LIMITS_MEMORY_WASM32_MAX_BYTES + LIMITS_MEMORY_WASM32_PAGE_BYTES;
7393 assert_eq!(
7394 LimitsError::memory_exceeds_wasm32_cap(bytes),
7395 LimitsError::MemoryExceedsWasm32Cap { bytes },
7396 "generated memory_exceeds_wasm32_cap ctor must produce byte-equal \
7397 `LimitsError::MemoryExceedsWasm32Cap` to the pre-lift struct-literal \
7398 wrap on the same `Copy`-`u64` fixture",
7399 );
7400 }
7401
7402 #[test]
7403 fn memory_not_page_multiple_ctor_matches_struct_literal_wrap() {
7404 let bytes = LIMITS_MEMORY_WASM32_PAGE_BYTES + 1;
7405 assert_eq!(
7406 LimitsError::memory_not_page_multiple(bytes),
7407 LimitsError::MemoryNotPageMultiple { bytes },
7408 "generated memory_not_page_multiple ctor must produce byte-equal \
7409 `LimitsError::MemoryNotPageMultiple` to the pre-lift struct-literal \
7410 wrap on the same `Copy`-`u64` fixture",
7411 );
7412 }
7413
7414 #[test]
7415 fn fuel_exceeds_cap_ctor_matches_struct_literal_wrap() {
7416 let fuel = LIMITS_FUEL_MAX + 1;
7417 assert_eq!(
7418 LimitsError::fuel_exceeds_cap(fuel),
7419 LimitsError::FuelExceedsCap { fuel },
7420 "generated fuel_exceeds_cap ctor must produce byte-equal \
7421 `LimitsError::FuelExceedsCap` to the pre-lift struct-literal wrap \
7422 on the same `Copy`-`u64` fixture",
7423 );
7424 }
7425
7426 #[test]
7427 fn wall_clock_not_canonical_ctor_matches_struct_literal_wrap() {
7428 let wall_clock = Duration::from_micros(1_500);
7429 assert_eq!(
7430 LimitsError::wall_clock_not_canonical(wall_clock),
7431 LimitsError::WallClockNotCanonical { wall_clock },
7432 "generated wall_clock_not_canonical ctor must produce byte-equal \
7433 `LimitsError::WallClockNotCanonical` to the pre-lift struct-literal \
7434 wrap on the same `Copy`-`Duration` fixture",
7435 );
7436 }
7437
7438 #[test]
7439 fn wall_clock_exceeds_cap_ctor_matches_struct_literal_wrap() {
7440 let wall_clock = LIMITS_WALL_CLOCK_MAX + Duration::from_millis(1);
7441 assert_eq!(
7442 LimitsError::wall_clock_exceeds_cap(wall_clock),
7443 LimitsError::WallClockExceedsCap { wall_clock },
7444 "generated wall_clock_exceeds_cap ctor must produce byte-equal \
7445 `LimitsError::WallClockExceedsCap` to the pre-lift struct-literal \
7446 wrap on the same `Copy`-`Duration` fixture",
7447 );
7448 }
7449
7450 #[test]
7451 fn cpu_exceeds_cap_ctor_matches_struct_literal_wrap() {
7452 let millicores = LIMITS_CPU_MILLICORES_MAX + 1;
7453 assert_eq!(
7454 LimitsError::cpu_exceeds_cap(millicores),
7455 LimitsError::CpuExceedsCap { millicores },
7456 "generated cpu_exceeds_cap ctor must produce byte-equal \
7457 `LimitsError::CpuExceedsCap` to the pre-lift struct-literal wrap \
7458 on the same `Copy`-`u32` fixture",
7459 );
7460 }
7461
7462 #[test]
7463 fn limits_scalar_ctors_route_field_through_copy_uniformly() {
7464 // Cross-axis routing pin: sweep each generated `<field>: <ty>`
7465 // constructor input axis through a non-default `Copy` fixture against
7466 // every arm in the [`limits_scalar_ctors!`] macro, so any wrapper-
7467 // side silent `.into()` / silent constant-substitution / silent field
7468 // re-name away from the canonical `bytes | fuel | wall_clock |
7469 // millicores` axes on any one variant, or a `u64 | u32 | Duration`
7470 // axis silently rerouted through some other `Copy` coercion, surfaces
7471 // here rather than at a downstream per-`:limits` diagnostic-shape
7472 // drift. Peer of the sibling
7473 // `supervisor_scalar_ctors_route_field_through_copy_uniformly`
7474 // (f0f77a2) and
7475 // `aplicacao_policy_scalar_ctors_route_field_through_copy_uniformly`
7476 // (7ef425e) cross-axis routing pins on the sibling `SupervisorError`
7477 // / `AplicacaoError` envelopes' per-axis ctor families.
7478 //
7479 // Fixtures picked out of each variant's accept-set boundary rather
7480 // than the default value so a silent constant-substitution to a
7481 // per-variant sentinel surfaces here on the structural-equality
7482 // assertion: the three `:memory` axes pick the below-page / above-cap
7483 // / page-plus-one shapes; the `:fuel` cap picks the above-cap shape;
7484 // the two `:wall-clock` axes pick sub-millisecond and above-cap
7485 // `Duration` shapes; the `:cpu` cap picks the above-cap millicore
7486 // shape.
7487 let below_page = LIMITS_MEMORY_WASM32_PAGE_BYTES - 137;
7488 let above_mem_cap = LIMITS_MEMORY_WASM32_MAX_BYTES + LIMITS_MEMORY_WASM32_PAGE_BYTES;
7489 let page_plus_one = LIMITS_MEMORY_WASM32_PAGE_BYTES + 1;
7490 let above_fuel_cap = LIMITS_FUEL_MAX + 137;
7491 let sub_ms = Duration::from_micros(1_500);
7492 let above_hour = LIMITS_WALL_CLOCK_MAX + Duration::from_secs(1);
7493 let above_cpu_cap = LIMITS_CPU_MILLICORES_MAX + 137;
7494 assert_eq!(
7495 LimitsError::memory_below_wasm32_page(below_page),
7496 LimitsError::MemoryBelowWasm32Page { bytes: below_page },
7497 );
7498 assert_eq!(
7499 LimitsError::memory_exceeds_wasm32_cap(above_mem_cap),
7500 LimitsError::MemoryExceedsWasm32Cap {
7501 bytes: above_mem_cap,
7502 },
7503 );
7504 assert_eq!(
7505 LimitsError::memory_not_page_multiple(page_plus_one),
7506 LimitsError::MemoryNotPageMultiple {
7507 bytes: page_plus_one,
7508 },
7509 );
7510 assert_eq!(
7511 LimitsError::fuel_exceeds_cap(above_fuel_cap),
7512 LimitsError::FuelExceedsCap {
7513 fuel: above_fuel_cap,
7514 },
7515 );
7516 assert_eq!(
7517 LimitsError::wall_clock_not_canonical(sub_ms),
7518 LimitsError::WallClockNotCanonical { wall_clock: sub_ms },
7519 );
7520 assert_eq!(
7521 LimitsError::wall_clock_exceeds_cap(above_hour),
7522 LimitsError::WallClockExceedsCap {
7523 wall_clock: above_hour,
7524 },
7525 );
7526 assert_eq!(
7527 LimitsError::cpu_exceeds_cap(above_cpu_cap),
7528 LimitsError::CpuExceedsCap {
7529 millicores: above_cpu_cap,
7530 },
7531 );
7532 }
7533
7534 #[test]
7535 fn limits_scalar_ctors_are_const_zero_runtime_work() {
7536 // Const-eval pin: the [`limits_scalar_ctors!`] macro spells every
7537 // generated ctor `const fn` so a caller can pin a `LimitsError` at
7538 // compile time — the same zero-runtime-work property the pre-lift
7539 // `|<field>| LimitsError::<Variant> { <field> }` closure carried on
7540 // its `Copy`-pass-through construction path (no `.to_string()` /
7541 // `.into()` allocation, no branching). If any future edit silently
7542 // drops the `const` qualifier from the macro body the per-arm `const`
7543 // bindings below fail to compile, which surfaces the regression at
7544 // the substrate-primitive definition rather than at some downstream
7545 // consumer that had come to rely on the `const`-constructibility.
7546 // Peer of the sibling
7547 // `supervisor_scalar_ctors_are_const_zero_runtime_work` (f0f77a2) and
7548 // `aplicacao_policy_scalar_ctors_are_const_zero_runtime_work`
7549 // (7ef425e) const-eval pins on the sibling `SupervisorError` /
7550 // `AplicacaoError` envelopes' per-axis ctor families.
7551 const MEM_BELOW: LimitsError = LimitsError::memory_below_wasm32_page(1);
7552 const MEM_CAP: LimitsError =
7553 LimitsError::memory_exceeds_wasm32_cap(LIMITS_MEMORY_WASM32_MAX_BYTES + 1);
7554 const MEM_NOT_MULTIPLE: LimitsError =
7555 LimitsError::memory_not_page_multiple(LIMITS_MEMORY_WASM32_PAGE_BYTES + 1);
7556 const FUEL_CAP: LimitsError = LimitsError::fuel_exceeds_cap(LIMITS_FUEL_MAX + 1);
7557 const WALL_NC: LimitsError =
7558 LimitsError::wall_clock_not_canonical(Duration::from_micros(1));
7559 const WALL_CAP: LimitsError =
7560 LimitsError::wall_clock_exceeds_cap(Duration::from_secs(3_601));
7561 const CPU_CAP: LimitsError = LimitsError::cpu_exceeds_cap(LIMITS_CPU_MILLICORES_MAX + 1);
7562 assert!(matches!(
7563 MEM_BELOW,
7564 LimitsError::MemoryBelowWasm32Page { .. }
7565 ));
7566 assert!(matches!(
7567 MEM_CAP,
7568 LimitsError::MemoryExceedsWasm32Cap { .. }
7569 ));
7570 assert!(matches!(
7571 MEM_NOT_MULTIPLE,
7572 LimitsError::MemoryNotPageMultiple { .. }
7573 ));
7574 assert!(matches!(FUEL_CAP, LimitsError::FuelExceedsCap { .. }));
7575 assert!(matches!(WALL_NC, LimitsError::WallClockNotCanonical { .. }));
7576 assert!(matches!(WALL_CAP, LimitsError::WallClockExceedsCap { .. }));
7577 assert!(matches!(CPU_CAP, LimitsError::CpuExceedsCap { .. }));
7578 }
7579
7580 #[test]
7581 fn bad_millicores_ctor_matches_tuple_literal_wrap_on_str_binding() {
7582 // Per-variant byte-equality pin on the newly lifted
7583 // [`LimitsError::bad_millicores`] tuple-newtype ctor over its `&str`
7584 // wire-up shape — the three [`parse_millicores`] sites that opened
7585 // the pre-lift `LimitsError::BadMillicores(s.into())` block against
7586 // the codec-scoped `s: &str` binding (empty-`:cpu`, bare-`m`-
7587 // magnitude fallthrough, non-digit-only garbage fallthrough). A
7588 // silent regression that de-folded the variant and re-inlined the
7589 // tuple-newtype block at one of the three wire-ups (or swapped
7590 // `.into()` for a divergent `String` conversion, or routed one arm
7591 // through a peer variant) trips the assertion under `PartialEq`.
7592 // Peer of the sibling `*_ctor_matches_struct_literal_wrap` pin
7593 // family on the same [`LimitsError`] envelope.
7594 let value = "500x";
7595 assert_eq!(
7596 LimitsError::bad_millicores(value),
7597 LimitsError::BadMillicores(value.to_string()),
7598 "generated bad_millicores ctor over a `&str` binding must \
7599 produce byte-equal `LimitsError::BadMillicores` to the \
7600 pre-lift tuple-newtype wrap on the same `&str` fixture",
7601 );
7602 }
7603
7604 #[test]
7605 fn bad_millicores_ctor_matches_tuple_literal_wrap_on_string_binding() {
7606 // Peer to the sibling `&str`-binding pin above, on the
7607 // `String` wire-up shape — the two [`parse_millicores`] sites that
7608 // opened the pre-lift `LimitsError::BadMillicores(format!(...))`
7609 // block against a codec-scoped `String` binding (digit-only
7610 // magnitude overflows u32, bare-core-shorthand × 1000 overflow).
7611 // Pins that the `impl Into<String>` bound routes both wire-up
7612 // shapes through the same substrate primitive without silently
7613 // rerouting one arm through a divergent conversion. A silent
7614 // regression that de-folded one of the two sites trips this pin
7615 // under `PartialEq`.
7616 let value: String = format!("{} (digit-only magnitude overflows u32)", u32::MAX);
7617 assert_eq!(
7618 LimitsError::bad_millicores(value.clone()),
7619 LimitsError::BadMillicores(value.clone()),
7620 "generated bad_millicores ctor over a `String` binding must \
7621 produce byte-equal `LimitsError::BadMillicores` to the \
7622 pre-lift tuple-newtype wrap on the same `String` fixture",
7623 );
7624 }
7625
7626 #[test]
7627 fn bad_byte_magnitude_ctor_matches_tuple_literal_wrap_on_str_binding() {
7628 // Per-variant byte-equality pin on the newly lifted
7629 // [`LimitsError::bad_byte_magnitude`] tuple-newtype ctor over its
7630 // `&str` wire-up shape — the sole [`parse_byte_size`] site that
7631 // opened the pre-lift `LimitsError::BadByteMagnitude(num_part.into())`
7632 // block against a codec-scoped `&str` binding (non-digit-only garbage
7633 // fallthrough after the numeric-shape gate). A silent regression
7634 // that de-folded the variant and re-inlined the tuple-newtype block
7635 // at the wire-up (or swapped `.into()` for a divergent `String`
7636 // conversion, or routed one arm through a peer variant) trips the
7637 // assertion under `PartialEq`. Direct sibling to the peer
7638 // `bad_millicores_ctor_matches_tuple_literal_wrap_on_str_binding`
7639 // pin on the [`parse_millicores`] codec surface.
7640 let value = "abc";
7641 assert_eq!(
7642 LimitsError::bad_byte_magnitude(value),
7643 LimitsError::BadByteMagnitude(value.to_string()),
7644 "generated bad_byte_magnitude ctor over a `&str` binding must \
7645 produce byte-equal `LimitsError::BadByteMagnitude` to the \
7646 pre-lift tuple-newtype wrap on the same `&str` fixture",
7647 );
7648 }
7649
7650 #[test]
7651 fn bad_byte_magnitude_ctor_matches_tuple_literal_wrap_on_string_binding() {
7652 // Peer to the sibling `&str`-binding pin above, on the
7653 // `String` wire-up shape — the two [`parse_byte_size`] sites that
7654 // opened the pre-lift `LimitsError::BadByteMagnitude(format!(...))`
7655 // block against a codec-scoped `String` binding (digit-only
7656 // magnitude overflows u64, magnitude × unit overflows u64). Pins
7657 // that the `impl Into<String>` bound routes both wire-up shapes
7658 // through the same substrate primitive without silently rerouting
7659 // one arm through a divergent conversion. A silent regression
7660 // that de-folded one of the two sites trips this pin under
7661 // `PartialEq`. Direct sibling to the peer
7662 // `bad_millicores_ctor_matches_tuple_literal_wrap_on_string_binding`
7663 // pin on the [`parse_millicores`] codec surface.
7664 let value: String = format!("{} (digit-only magnitude overflows u64)", u64::MAX);
7665 assert_eq!(
7666 LimitsError::bad_byte_magnitude(value.clone()),
7667 LimitsError::BadByteMagnitude(value.clone()),
7668 "generated bad_byte_magnitude ctor over a `String` binding must \
7669 produce byte-equal `LimitsError::BadByteMagnitude` to the \
7670 pre-lift tuple-newtype wrap on the same `String` fixture",
7671 );
7672 }
7673
7674 #[test]
7675 fn bad_duration_magnitude_ctor_matches_tuple_literal_wrap_on_str_binding() {
7676 // Per-variant byte-equality pin on the newly lifted
7677 // [`LimitsError::bad_duration_magnitude`] tuple-newtype ctor over its
7678 // `&str` wire-up shape — the sole [`parse_duration`] site that opened
7679 // the pre-lift `LimitsError::BadDurationMagnitude(num_part.into())`
7680 // block against a codec-scoped `&str` binding (non-digit-only garbage
7681 // fallthrough after the numeric-shape gate). A silent regression that
7682 // de-folded the variant and re-inlined the tuple-newtype block at the
7683 // wire-up (or swapped `.into()` for a divergent `String` conversion,
7684 // or routed one arm through a peer variant) trips the assertion under
7685 // `PartialEq`. Direct sibling to the peer
7686 // `bad_millicores_ctor_matches_tuple_literal_wrap_on_str_binding` /
7687 // `bad_byte_magnitude_ctor_matches_tuple_literal_wrap_on_str_binding`
7688 // pins on the [`parse_millicores`] / [`parse_byte_size`] codec
7689 // surfaces — closes the last un-lifted `(String)` tuple-newtype
7690 // variant on the paired codec-magnitude family.
7691 let value = "abc";
7692 assert_eq!(
7693 LimitsError::bad_duration_magnitude(value),
7694 LimitsError::BadDurationMagnitude(value.to_string()),
7695 "generated bad_duration_magnitude ctor over a `&str` binding must \
7696 produce byte-equal `LimitsError::BadDurationMagnitude` to the \
7697 pre-lift tuple-newtype wrap on the same `&str` fixture",
7698 );
7699 }
7700
7701 #[test]
7702 fn bad_duration_magnitude_ctor_matches_tuple_literal_wrap_on_string_binding() {
7703 // Peer to the sibling `&str`-binding pin above, on the
7704 // `String` wire-up shape — the two [`parse_duration`] sites that
7705 // opened the pre-lift `LimitsError::BadDurationMagnitude(format!(...))`
7706 // block against a codec-scoped `String` binding (digit-only magnitude
7707 // overflows u64, magnitude × unit overflows u64). Pins that the
7708 // `impl Into<String>` bound routes both wire-up shapes through the
7709 // same substrate primitive without silently rerouting one arm through
7710 // a divergent conversion. A silent regression that de-folded one of
7711 // the two sites trips this pin under `PartialEq`. Direct sibling to
7712 // the peer `bad_millicores_ctor_matches_tuple_literal_wrap_on_string_binding`
7713 // / `bad_byte_magnitude_ctor_matches_tuple_literal_wrap_on_string_binding`
7714 // pins on the [`parse_millicores`] / [`parse_byte_size`] codec
7715 // surfaces.
7716 let value: String = format!("{} (digit-only magnitude overflows u64)", u64::MAX);
7717 assert_eq!(
7718 LimitsError::bad_duration_magnitude(value.clone()),
7719 LimitsError::BadDurationMagnitude(value.clone()),
7720 "generated bad_duration_magnitude ctor over a `String` binding must \
7721 produce byte-equal `LimitsError::BadDurationMagnitude` to the \
7722 pre-lift tuple-newtype wrap on the same `String` fixture",
7723 );
7724 }
7725
7726 #[test]
7727 fn empty_byte_size_ctor_matches_tuple_literal_wrap_on_str_binding() {
7728 // Per-variant byte-equality pin on the newly lifted
7729 // [`LimitsError::empty_byte_size`] tuple-newtype ctor over its `&str`
7730 // wire-up shape — the sole [`parse_byte_size`] site that opened the
7731 // pre-lift `LimitsError::EmptyByteSize(s.into())` block against the
7732 // codec-scoped `s: &str` binding after the outer `s.trim()` /
7733 // `is_empty()` gate on the codec entry surface. A silent regression
7734 // that de-folded the variant and re-inlined the tuple-newtype block
7735 // at the wire-up (or swapped `.into()` for a divergent `String`
7736 // conversion, or routed the arm through a peer variant) trips the
7737 // assertion under `PartialEq`. Direct sibling to the peer
7738 // `bad_byte_magnitude_ctor_matches_tuple_literal_wrap_on_str_binding`
7739 // pin on the same [`parse_byte_size`] codec surface but on the
7740 // bad-magnitude axis rather than the empty-shape axis of the same
7741 // `(String)` tuple-newtype codec-magnitude family.
7742 let value = "";
7743 assert_eq!(
7744 LimitsError::empty_byte_size(value),
7745 LimitsError::EmptyByteSize(value.to_string()),
7746 "generated empty_byte_size ctor over a `&str` binding must \
7747 produce byte-equal `LimitsError::EmptyByteSize` to the \
7748 pre-lift tuple-newtype wrap on the same `&str` fixture",
7749 );
7750 }
7751}