caixa_helm/lib.rs
1//! caixa-helm — typed renderer that emits a per-program `lareira-<name>`
2//! Helm chart from a [`Caixa`] manifest plus its `servicos/<name>.computeunit.yaml`.
3//!
4//! ## Output shape
5//!
6//! Every chart emitted here mirrors the canonical
7//! `pleme-io/helmworks/charts/lareira-<name>/` layout, which is *thin*:
8//!
9//! Chart.yaml ; metadata + dependency on pleme-computeunit
10//! values.yaml ; pleme-computeunit values block (the typed L2 ComputeUnit shape)
11//! README.md ; one-line elevator pitch for the chart
12//!
13//! There are no `templates/` — the rendering is delegated to the
14//! `pleme-computeunit` library chart in helmworks (per `theory/META-FRAMEWORK.md`
15//! §I, Layer 3 → Layer 2 transformation). caixa-helm's job is to derive the
16//! values block from a Caixa, not to render Kubernetes objects directly.
17//!
18//! ## Why a separate crate
19//!
20//! Same pattern as [`caixa_flake`] (renders flake.nix) and [`caixa_pangea`]
21//! (renders pangea Ruby) — `caixa-<target>` crates take a typed Caixa and
22//! emit the canonical source for `<target>`. Naming is uniform across the
23//! workspace.
24//!
25//! ## V0 contract
26//!
27//! ```rust,ignore
28//! use caixa_core::Caixa;
29//! use caixa_helm::{ChartDir, render_chart_for_servico};
30//!
31//! let caixa: Caixa = Caixa::from_lisp(src)?;
32//! let cu_yaml: serde_yaml::Value =
33//! serde_yaml::from_str(std::fs::read_to_string("servicos/hello-rio.computeunit.yaml")?)?;
34//! let dir: ChartDir = render_chart_for_servico(&caixa, &cu_yaml)?;
35//! dir.write_to(std::path::Path::new("/tmp/lareira-hello-rio"))?;
36//! ```
37//!
38//! ## What this is NOT
39//!
40//! - Not a chart for the `caixa-operator` itself — that lives in
41//! `pleme-io/caixa/operator-chart/`.
42//! - Not a Helm CLI wrapper — emitting bytes only; consumers (`feira chart`,
43//! eventually) drive the I/O.
44//! - Not a renderer of K8s resources — `pleme-computeunit` library chart owns
45//! the templates that turn this values block into ComputeUnit + Service +
46//! ScaledObject + ConfigMap.
47
48#![allow(clippy::module_name_repetitions)]
49
50use std::collections::BTreeMap;
51use std::path::Path;
52
53use caixa_core::{Caixa, MappingExt, lareira_chart_name};
54use serde::{Deserialize, Serialize};
55use thiserror::Error;
56
57/// Errors caixa-helm can raise.
58#[derive(Debug, Error)]
59pub enum Error {
60 /// The caixa's `:kind` doesn't match what `caixa-helm` targets
61 /// (this renderer only emits per-program `lareira-<nome>` charts
62 /// for `:kind Servico`). Lifted from a prior `NotAServico(CaixaKind)`
63 /// arm to wrap [`caixa_core::KindMismatch`] so the diagnostic
64 /// names the offending caixa's `:nome` (not just its kind),
65 /// shared verbatim with `caixa-flux` and `caixa-mesh`.
66 #[error("{0}")]
67 NotAServico(#[from] caixa_core::KindMismatch),
68 /// The caixa's `:servicos` list doesn't carry exactly one entry —
69 /// the V0 contract every Servico-kind caixa satisfies (one
70 /// ComputeUnit YAML pointer per Servico, matching the one Helm
71 /// chart this renderer emits). Lifted from a prior
72 /// `UnsupportedServicoCount(usize)` arm to wrap
73 /// [`caixa_core::ServicoCountMismatch`] so the diagnostic names
74 /// the offending caixa's `:nome` (not just the count), shared
75 /// verbatim with `caixa-flux` (the peer per-Servico renderer
76 /// running the same V0 invariant on the programs.yaml-entry axis).
77 #[error("{0}")]
78 UnsupportedServicoCount(#[from] caixa_core::ServicoCountMismatch),
79 #[error("computeunit yaml missing required field: {0}")]
80 MissingField(&'static str),
81 #[error("yaml: {0}")]
82 Yaml(#[from] serde_yaml::Error),
83 #[error("render: {0}")]
84 Render(#[from] caixa_core::RenderError),
85 #[error("io: {0}")]
86 Io(#[from] std::io::Error),
87}
88
89/// One file in the rendered chart — `(path, contents)` pair every
90/// [`render_chart_for_servico`]-rendered `lareira-<nome>` chart-tree
91/// leaf lands at (`Chart.yaml`, `values.yaml`, `README.md`).
92///
93/// Type-aliased to the canonical [`caixa_core::RenderedFile`] so the
94/// substrate-side "one rendered leaf artifact" shape lives at one
95/// struct definition across every per-target renderer — the peer
96/// [`caixa_flux::BundleFile`] alias resolves to the same canonical, so
97/// a future rebrand on either axis (a per-artifact hash / provenance
98/// field addition, a per-artifact write-mode discriminator once
99/// per-cluster-writer sandboxing lands) lands at one caixa-core `pub
100/// struct RenderedFile` edit and reaches both crates by construction.
101/// Prior to this lift both crates carried an inline `pub struct
102/// <Xxx>File { pub path: PathBuf, pub contents: String }` with
103/// identical `#[derive(Debug, Clone, PartialEq, Eq)]` shapes and no
104/// per-type impls; a future per-target renderer (`caixa-otel`, the
105/// future `mesh.pleme.io/v1alpha1/Aplicacao` CR materializer, the
106/// future per-Supervisor reconciler renderer) would have carried a
107/// third and fourth clone of the same record. Type aliases preserve
108/// every existing struct-literal construction site
109/// (`ChartFile { path, contents }`), every field-access site (`f.path`,
110/// `f.contents`), and every derive-fed navigator by construction —
111/// Rust type aliases inherit the aliased type's `#[derive]`-generated
112/// `Debug`/`Clone`/`PartialEq`/`Eq` impls with no per-alias glue.
113pub type ChartFile = caixa_core::RenderedFile;
114
115/// The rendered chart — a flat list of files, plus the chart name.
116#[derive(Debug, Clone, PartialEq, Eq)]
117pub struct ChartDir {
118 /// Chart name — e.g. `lareira-hello-rio`. Used as the output dir name.
119 pub name: String,
120 pub files: Vec<ChartFile>,
121}
122
123impl ChartDir {
124 /// Write every file to `<dest>/<self.name>/`. Creates parent dirs.
125 pub fn write_to(&self, dest: &Path) -> Result<(), Error> {
126 let root = dest.join(&self.name);
127 std::fs::create_dir_all(&root)?;
128 for f in &self.files {
129 let target = root.join(&f.path);
130 if let Some(parent) = target.parent() {
131 std::fs::create_dir_all(parent)?;
132 }
133 std::fs::write(&target, &f.contents)?;
134 }
135 Ok(())
136 }
137}
138
139/// Top-level `Chart.yaml` shape for a generated lareira-<name> chart.
140///
141/// Mirrors `helmworks/charts/lareira-hello-world/Chart.yaml` 1:1 in
142/// structural slots — versions, deps, keywords, maintainers.
143#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
144pub struct ChartYaml {
145 #[serde(rename = "apiVersion")]
146 pub api_version: String,
147 pub name: String,
148 pub description: String,
149 #[serde(rename = "type")]
150 pub chart_type: String,
151 pub version: String,
152 #[serde(rename = "appVersion")]
153 pub app_version: String,
154 #[serde(default, skip_serializing_if = "Vec::is_empty")]
155 pub keywords: Vec<String>,
156 #[serde(default, skip_serializing_if = "Vec::is_empty")]
157 pub maintainers: Vec<Maintainer>,
158 #[serde(default, skip_serializing_if = "Option::is_none")]
159 pub home: Option<String>,
160 pub dependencies: Vec<ChartDependency>,
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
164pub struct Maintainer {
165 pub name: String,
166 #[serde(default, skip_serializing_if = "Option::is_none")]
167 pub email: Option<String>,
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
171pub struct ChartDependency {
172 pub name: String,
173 pub version: String,
174 pub repository: String,
175 #[serde(default, skip_serializing_if = "Option::is_none")]
176 pub alias: Option<String>,
177}
178
179/// Repository for the `pleme-computeunit` library chart. Defaults to the
180/// helmworks file:// path used by lareira-* charts; consumers can override
181/// via `RenderOpts::library_repo` to point at the published OCI registry.
182pub const DEFAULT_LIBRARY_REPO: &str = "file://../pleme-computeunit";
183pub const DEFAULT_LIBRARY_VERSION: &str = "~0.1.0";
184/// Canonical Helm library-chart name every `lareira-<nome>` chart depends
185/// on — re-export of the lifted [`caixa_core::DEFAULT_LIBRARY_NAME`] so
186/// the load-bearing string lives in exactly one place across every
187/// caixa renderer (caixa-helm's `RenderOpts::library_name` default
188/// here + caixa-flux's `cluster_bundle` `helmrelease.yaml` wrap key).
189/// A future per-edition library-chart fork — every entry on the
190/// absorption-roadmap that names a per-cluster / per-namespace /
191/// per-tenant variant of the canonical library chart — reaches both
192/// consumers through one `&'static str` by construction. Same shape
193/// as the [`caixa_core::DEFAULT_NAMESPACE`] (a085b26) /
194/// [`caixa_core::DEFAULT_SERVICO_PORT`] (1e22add) lifts on the peer
195/// canonical-K8s-axis-constant surface.
196pub use caixa_core::DEFAULT_LIBRARY_NAME;
197
198/// Canonical Helm 3 `Chart.yaml` `apiVersion` every rendered
199/// `lareira-<nome>` chart declares. Re-export of the lifted
200/// [`caixa_core::HELM_CHART_API_VERSION`] so the Helm-side
201/// chart-schema apiVersion — the discriminator the Helm binary's
202/// chart-schema parser (`helm dependency build`, `helm lint`,
203/// `helm template`) consults to select the schema that reads the
204/// rendered Chart.yaml — lives in exactly one place across every
205/// caixa renderer. The single production-code call site consuming
206/// it is [`build_chart_yaml`]'s `api_version` field assignment; a
207/// drifted local `pub const HELM_CHART_API_VERSION: &str = "…"` at
208/// this crate (or any sibling per-chart-schema renderer the
209/// absorption roadmap acknowledges — the future per-Aplicacao
210/// library chart, the future per-cluster snapshot chart) would
211/// silently reroute the rendered Chart.yaml through a stale
212/// chart-schema parser at `helm template` time far from the
213/// rebrand commit's source, so the equality + `&'static` static-data
214/// identity pin
215/// (`helm_chart_api_version_re_export_points_at_caixa_core_canonical`)
216/// closes the drift footgun at caixa-helm build time. Same shape as
217/// the [`DEFAULT_LIBRARY_NAME`] / [`KUBE_KEY_SPEC`] re-exports on the
218/// sibling canonical-Helm-load-bearing-string / canonical-K8s-CR-key
219/// axes.
220pub use caixa_core::HELM_CHART_API_VERSION;
221
222/// Canonical Helm 3 `Chart.yaml` `type` field per-chart-kind
223/// discriminator scalar-value every rendered `lareira-<nome>` chart
224/// declares. Re-export of the lifted
225/// [`caixa_core::HELM_CHART_TYPE_APPLICATION`] so the Helm chart-schema
226/// per-chart-kind discriminator — the scalar Helm's per-release install-
227/// shape dispatch loop keys off to select the per-chart-kind install
228/// pathway — lives in exactly one place across every caixa renderer.
229/// The single production-code call site consuming it is
230/// [`build_chart_yaml`]'s `chart_type` field assignment (the sole
231/// emitter site the prior inline `"application".into()` literal sat at);
232/// a drifted local `pub const HELM_CHART_TYPE_APPLICATION: &str = "…"`
233/// at this crate (or any sibling per-chart renderer the absorption
234/// roadmap acknowledges — the future per-Aplicacao library chart, the
235/// future per-cluster snapshot chart) would surface as one of two
236/// silent failure modes at `helm install` time: a value outside the
237/// schema's admitted set (`{"application", "library"}`) that Helm's
238/// chart-schema parser silently treats as the default `application`
239/// shape (masking the schema violation with no process-log signal), or
240/// an accidental collapse onto the sibling `"library"` shape that Helm
241/// refuses to install directly ("Error: library charts cannot be
242/// installed") with no field naming the chart-kind-drift root cause.
243/// The equality + `&'static` static-data identity pin
244/// (`helm_chart_type_application_re_export_points_at_caixa_core_canonical`)
245/// closes the drift footgun at caixa-helm build time. Peer to the
246/// [`HELM_CHART_API_VERSION`] re-export on the sibling canonical-Helm-
247/// chart-schema-axis — completes the per-Chart.yaml `(apiVersion, type)`
248/// canonical-scalar-axis re-export pair every rendered `lareira-<nome>`
249/// chart declares at its top-level Chart.yaml body.
250pub use caixa_core::HELM_CHART_TYPE_APPLICATION;
251
252/// Canonical Helm 3 `Chart.yaml` `type` field per-chart-kind discriminator
253/// scalar-value the sibling library-chart shape lands on — re-export of
254/// the lifted [`caixa_core::HELM_CHART_TYPE_LIBRARY`] so the second and
255/// only other arm of the Helm chart-schema's closed set `{"application",
256/// "library"}` lives in exactly one place across every caixa renderer.
257/// No production emitter here consumes it today — the caixa-helm
258/// renderer emits per-Servico `application`-typed `lareira-<nome>`
259/// charts, and the sibling [`DEFAULT_LIBRARY_NAME`] `pleme-computeunit`
260/// library chart is out-of-tree at `pleme-io/helmworks` (not this
261/// crate's authority) — but every future substrate-side per-chart-kind
262/// classifier and every emitter for the future per-Aplicacao library
263/// chart the [`HELM_CHART_TYPE_APPLICATION`] docstring names as a
264/// trajectory item reads the same `&'static str` by construction.
265/// Peer to the [`HELM_CHART_TYPE_APPLICATION`] re-export on the sibling
266/// closed-set arm — completes the two-arm re-export pair of the Helm
267/// chart-schema's per-chart-kind axis at the caixa-helm surface so
268/// consumers reaching for either shape read from one canonical source
269/// per arm. The paired identity pins
270/// (`helm_chart_type_library_re_export_points_at_caixa_core_canonical`)
271/// close the drift footgun at caixa-helm build time.
272pub use caixa_core::HELM_CHART_TYPE_LIBRARY;
273
274/// Canonical Helm 3 `Chart.yaml` top-level YAML axis-key naming the
275/// per-chart-kind discriminator field — re-export of the lifted
276/// [`caixa_core::HELM_CHART_KEY_TYPE`] so the top-level per-chart-kind
277/// discriminator YAML key the sibling [`HELM_CHART_TYPE_APPLICATION`] /
278/// [`HELM_CHART_TYPE_LIBRARY`] axis-value re-exports carry the closed-
279/// set admitted scalars for lives in exactly one place across every
280/// caixa renderer. The production emitter today is [`ChartYaml`]'s
281/// `chart_type` field `#[serde(rename = "type")]` attribute — Rust's
282/// attribute grammar admits only string literals so the const cannot
283/// substitute for the literal syntactically, but the drift-detection
284/// pin at
285/// [`tests::chart_yaml_serializes_type_axis_under_lifted_helm_chart_key_type`]
286/// round-trips a rendered `Chart.yaml` through
287/// `serde_yaml::from_str::<serde_yaml::Value>` and asserts the top-
288/// level `Mapping::get(HELM_CHART_KEY_TYPE)` resolves, closing the
289/// drift the attribute-literal-only grammar leaves silent (a future
290/// refactor that dropped the `#[serde(rename = "type")]` attribute
291/// would silently serialize the field as Rust's default snake_case
292/// `chart_type:`, which Helm's chart-schema parser silently ignores
293/// as an unknown top-level key, defaulting the per-chart-kind axis
294/// to `application` with no process-log drift-signal). Peer to the
295/// [`HELM_CHART_TYPE_APPLICATION`] / [`HELM_CHART_TYPE_LIBRARY`]
296/// re-exports on the sibling per-chart-kind axis-value canonical
297/// surface — completes the per-Chart.yaml per-chart-kind
298/// discriminator axis's `(key, value-set)` canonical re-export trio
299/// at the caixa-helm surface.
300pub use caixa_core::HELM_CHART_KEY_TYPE;
301
302/// Canonical Helm 3 `Chart.yaml` top-level YAML axis-key naming the
303/// per-chart underlying-application-version field — re-export of the
304/// lifted [`caixa_core::HELM_CHART_KEY_APP_VERSION`] so the top-level
305/// per-chart-app-version YAML key the [`ChartYaml`] `app_version`
306/// field's `#[serde(rename = "appVersion")]` attribute encodes lives
307/// in exactly one place across every caixa renderer. Rust's attribute
308/// grammar admits only string literals so the const cannot substitute
309/// for the literal syntactically, but the drift-detection pin at
310/// [`tests::chart_yaml_serializes_app_version_axis_under_lifted_helm_chart_key_app_version`]
311/// round-trips a rendered `Chart.yaml` and asserts the top-level
312/// `Mapping::get(HELM_CHART_KEY_APP_VERSION)` resolves, closing the
313/// drift the attribute-literal-only grammar leaves silent (a future
314/// refactor that dropped the `#[serde(rename = "appVersion")]`
315/// attribute would silently serialize the field as Rust's default
316/// snake_case `app_version:`, which Helm's chart-schema parser
317/// silently drops from the parsed chart-metadata shape, and every
318/// downstream Artifact Hub / `helm search` per-chart index falls back
319/// to "no application version" for the rendered chart far from the
320/// drift site). Peer to [`HELM_CHART_KEY_TYPE`] on the sibling
321/// per-Chart.yaml top-level YAML axis-key re-export surface —
322/// completes the per-Chart.yaml top-level YAML axis-key re-export
323/// pair at the caixa-helm surface for the two serde-rename-literal-
324/// only axes on this crate's [`ChartYaml`] struct that Rust's
325/// attribute-argument grammar leaves un-substitutable syntactically
326/// (the third top-level axis-key `apiVersion` re-exports through
327/// [`HELM_CHART_KEY_API_VERSION`] below, whose byte-shape coincides
328/// with the sibling [`caixa_core::KUBE_KEY_API_VERSION`] by Helm's
329/// design decision to inherit the K8s CR top-level shape verbatim —
330/// the paired substrate-side
331/// [`caixa_core`-side
332/// `helm_chart_key_api_version_matches_kube_key_api_version`] pin
333/// makes the byte-shape coincidence load-bearing rather than
334/// accidental).
335pub use caixa_core::HELM_CHART_KEY_APP_VERSION;
336
337/// Canonical Helm 3 `Chart.yaml` top-level YAML axis-key naming the
338/// per-chart chart-schema-apiVersion field — re-export of the lifted
339/// [`caixa_core::HELM_CHART_KEY_API_VERSION`] so the top-level chart-
340/// schema-apiVersion YAML key the [`ChartYaml`] `api_version` field's
341/// `#[serde(rename = "apiVersion")]` attribute encodes lives in
342/// exactly one place across every caixa renderer. Rust's attribute
343/// grammar admits only string literals so the const cannot substitute
344/// for the literal syntactically, but the drift-detection pin at
345/// [`tests::chart_yaml_serializes_api_version_axis_under_lifted_helm_chart_key_api_version`]
346/// round-trips a rendered `Chart.yaml` and asserts the top-level
347/// `Mapping::get(HELM_CHART_KEY_API_VERSION)` resolves, closing the
348/// drift the attribute-literal-only grammar leaves silent (a future
349/// refactor that dropped the `#[serde(rename = "apiVersion")]`
350/// attribute would silently serialize the field as Rust's default
351/// snake_case `api_version:`, which Helm's chart-schema parser
352/// rejects at `helm lint` / `helm dependency build` / `helm template`
353/// time with an "apiVersion is required" error far from the drift
354/// site). Peer to [`HELM_CHART_KEY_TYPE`] / [`HELM_CHART_KEY_APP_VERSION`]
355/// on the sibling per-Chart.yaml top-level YAML axis-key re-export
356/// surface — completes the per-Chart.yaml top-level YAML axis-key
357/// re-export trio at the caixa-helm surface for the three serde-
358/// rename-literal-only axes on this crate's [`ChartYaml`] struct.
359pub use caixa_core::HELM_CHART_KEY_API_VERSION;
360
361/// Canonical Helm 3 `Chart.yaml` top-level YAML axis-key naming the
362/// per-chart dependency-list field — re-export of the lifted
363/// [`caixa_core::HELM_CHART_KEY_DEPENDENCIES`] so the load-bearing serde
364/// field-name at [`ChartYaml`]'s `dependencies` field (the parent
365/// list-container the already-re-exported per-`dependencies[]`-entry
366/// sub-mapping tetrad [`HELM_CHART_DEPENDENCY_KEY_NAME`] /
367/// [`HELM_CHART_DEPENDENCY_KEY_VERSION`] /
368/// [`HELM_CHART_DEPENDENCY_KEY_REPOSITORY`] /
369/// [`HELM_CHART_DEPENDENCY_KEY_ALIAS`] mounts one level down under)
370/// lives in exactly one place across every caixa renderer. The Rust
371/// field name and the wire key coincide by default (no
372/// `#[serde(rename)]` attribute today), so the const doesn't substitute
373/// for the field-name syntactically at the struct definition, but the
374/// drift-detection pin at
375/// [`tests::chart_yaml_serializes_dependencies_axis_under_lifted_helm_chart_key_dependencies`]
376/// round-trips a rendered `Chart.yaml` through
377/// `serde_yaml::from_str::<serde_yaml::Value>` and asserts the top-
378/// level `Mapping::get(HELM_CHART_KEY_DEPENDENCIES)` resolves — closing
379/// the drift a future field rename (`dependencies` → `deps` /
380/// `chartDependencies`) or a `#[serde(rename_all = "camelCase")]`
381/// attribute addition on [`ChartYaml`] would otherwise leave silent
382/// (Helm's chart-schema parser silently drops the entire dep list from
383/// the parsed chart-metadata, `helm dependency build` finds no chart to
384/// vendor, and every rendered `lareira-<nome>` chart's install fails at
385/// apply time far from the drift site). Peer to
386/// [`HELM_CHART_KEY_TYPE`] / [`HELM_CHART_KEY_APP_VERSION`] /
387/// [`HELM_CHART_KEY_API_VERSION`] on the sibling per-Chart.yaml
388/// top-level YAML axis-key re-export surface — extends the per-
389/// Chart.yaml top-level YAML axis-key re-export trio at the caixa-helm
390/// surface onto the fourth top-level axis-key, the parent list-
391/// container whose per-entry sub-mapping tetrad is already re-exported
392/// under [`HELM_CHART_DEPENDENCY_KEY_*`].
393pub use caixa_core::HELM_CHART_KEY_DEPENDENCIES;
394
395/// Canonical Helm 3 `Chart.yaml` per-`dependencies[]`-entry sub-mapping
396/// YAML axis-key naming the per-dep chart-name field — re-export of the
397/// lifted [`caixa_core::HELM_CHART_DEPENDENCY_KEY_NAME`] so the load-
398/// bearing serde field-name at [`ChartDependency`]'s `name` field lives
399/// in exactly one place across every caixa renderer. Peer to
400/// [`HELM_CHART_DEPENDENCY_KEY_VERSION`] /
401/// [`HELM_CHART_DEPENDENCY_KEY_REPOSITORY`] /
402/// [`HELM_CHART_DEPENDENCY_KEY_ALIAS`] on the sibling per-dep sub-key
403/// axes. The drift-detection round-trip pin at
404/// [`tests::chart_dependency_serializes_tetrad_under_lifted_helm_chart_dependency_keys`]
405/// serializes a fully-populated [`ChartDependency`] and asserts each of
406/// the four per-dep sub-mapping wire keys resolves — closing the drift
407/// a rename of the Rust field or a `#[serde(rename_all)]` attribute
408/// addition would otherwise leave silent.
409pub use caixa_core::HELM_CHART_DEPENDENCY_KEY_NAME;
410
411/// Canonical Helm 3 `Chart.yaml` per-`dependencies[]`-entry sub-mapping
412/// YAML axis-key naming the per-dep chart-version-constraint field —
413/// re-export of the lifted [`caixa_core::HELM_CHART_DEPENDENCY_KEY_VERSION`]
414/// so the load-bearing serde field-name at [`ChartDependency`]'s
415/// `version` field lives in exactly one place across every caixa
416/// renderer. Peer to [`HELM_CHART_DEPENDENCY_KEY_NAME`] on the sibling
417/// per-dep sub-key axes. See [`HELM_CHART_DEPENDENCY_KEY_NAME`] for the
418/// shared per-entry-sub-mapping lift rationale.
419pub use caixa_core::HELM_CHART_DEPENDENCY_KEY_VERSION;
420
421/// Canonical Helm 3 `Chart.yaml` per-`dependencies[]`-entry sub-mapping
422/// YAML axis-key naming the per-dep chart-registry URL field — re-export
423/// of the lifted [`caixa_core::HELM_CHART_DEPENDENCY_KEY_REPOSITORY`]
424/// so the load-bearing serde field-name at [`ChartDependency`]'s
425/// `repository` field lives in exactly one place across every caixa
426/// renderer. Peer to [`HELM_CHART_DEPENDENCY_KEY_NAME`] on the sibling
427/// per-dep sub-key axes.
428pub use caixa_core::HELM_CHART_DEPENDENCY_KEY_REPOSITORY;
429
430/// Canonical Helm 3 `Chart.yaml` per-`dependencies[]`-entry sub-mapping
431/// YAML axis-key naming the per-dep chart-alias override field —
432/// re-export of the lifted [`caixa_core::HELM_CHART_DEPENDENCY_KEY_ALIAS`]
433/// so the load-bearing serde field-name at [`ChartDependency`]'s
434/// `alias` field lives in exactly one place across every caixa
435/// renderer. Peer to [`HELM_CHART_DEPENDENCY_KEY_NAME`] on the sibling
436/// per-dep sub-key axes.
437pub use caixa_core::HELM_CHART_DEPENDENCY_KEY_ALIAS;
438
439/// Canonical Helm 3 per-chart-directory metadata-file filename every
440/// rendered `lareira-<nome>` chart carries at its top-level directory —
441/// re-export of the lifted [`caixa_core::HELM_CHART_YAML_FILENAME`] so
442/// the fixed lookup name Helm's chart-schema parser (`helm dependency
443/// build`, `helm lint`, `helm template`, `helm install`) consults at
444/// chart-open time to locate the per-chart schema-body scalars
445/// ([`HELM_CHART_API_VERSION`], [`HELM_CHART_TYPE_APPLICATION`], the
446/// name/version/dependencies fields) lives in exactly one place across
447/// every caixa renderer. The single production-code call site
448/// consuming it is [`render_chart_for_servico`]'s `ChartDir` assembly
449/// where the metadata file's per-`ChartFile` `path` axis is set (the
450/// sole emitter site the prior inline `PathBuf::from("Chart.yaml")`
451/// literal sat at); every test-side round-trip navigator that reaches
452/// into the rendered `ChartDir` by the metadata filename (the
453/// per-chart-metadata-field sweep tests +
454/// [`ChartDir::write_to`] post-write existence pin) now consults the
455/// same `&'static str`, so a rebrand of the Helm 3 metadata-file axis
456/// (any per-fork `Chartfile.yaml` / Helm 4 metadata-file rename the
457/// upstream packaging spec might adopt) lands at one const and reaches
458/// every consumer by construction. A drifted local `pub const
459/// HELM_CHART_YAML_FILENAME: &str = "…"` at this crate — the canonical
460/// drift footgun where a sibling local `pub const` could happen to
461/// carry the same string at the source while pointing at a different
462/// `&'static` allocation — surfaces as one of two silent failure modes
463/// at chart-consumption time: Helm's chart-schema parser refuses to
464/// open the rendered chart-directory ("Error: Chart.yaml file is
465/// missing") far from the drift commit, or the sibling
466/// [`caixa_flux::cluster_bundle`]'s future per-chart-directory
467/// resolver — a per-cluster snapshot bundle that re-lists the
468/// chart-dir contents by filename — silently returns `None` at
469/// cluster-side `feira app deploy` time. The equality + `&'static`
470/// static-data identity pin
471/// (`helm_chart_yaml_filename_re_export_points_at_caixa_core_canonical`)
472/// closes the drift footgun at caixa-helm build time. Peer to the
473/// [`HELM_CHART_API_VERSION`] / [`HELM_CHART_TYPE_APPLICATION`]
474/// re-exports on the sibling canonical-Helm-chart-schema-body-axis
475/// surface — completes the per-`lareira-<nome>`-chart-directory
476/// `(filename, apiVersion, type)` canonical-scalar-axis re-export
477/// triple every rendered chart declares at its top-level metadata file.
478pub use caixa_core::HELM_CHART_YAML_FILENAME;
479
480/// Canonical Helm 3 per-chart-directory values-file filename every
481/// rendered `lareira-<nome>` chart carries at its top-level directory —
482/// re-export of the lifted [`caixa_core::HELM_VALUES_YAML_FILENAME`] so
483/// the fixed lookup name Helm's chart-schema parser (`helm dependency
484/// build`, `helm lint`, `helm template`, `helm install`) consults at
485/// chart-open time to locate the per-chart values block that
486/// [`HELM_VALUES_KEY_ENABLED`] toggles under its
487/// [`DEFAULT_LIBRARY_NAME`] wrap key lives in exactly one place across
488/// every caixa renderer. The single production-code call site
489/// consuming it is [`render_chart_for_servico`]'s `ChartDir` assembly
490/// where the values file's per-`ChartFile` `path` axis is set (the
491/// sole emitter site the prior inline `PathBuf::from("values.yaml")`
492/// literal sat at); every test-side round-trip navigator that reaches
493/// into the rendered `ChartDir` by the values filename (the
494/// per-chart-values-field sweep tests +
495/// [`ChartDir::write_to`] post-write existence pin) now consults the
496/// same `&'static str`, so a rebrand of the Helm 3 values-file axis
497/// (any per-fork `defaults.yaml` / Helm 4 values-file rename the
498/// upstream packaging spec might adopt) lands at one const and reaches
499/// every consumer by construction. A drifted local `pub const
500/// HELM_VALUES_YAML_FILENAME: &str = "…"` at this crate — the canonical
501/// drift footgun where a sibling local `pub const` could happen to
502/// carry the same string at the source while pointing at a different
503/// `&'static` allocation — surfaces as one of two silent failure modes
504/// at chart-consumption time: Helm's per-chart values-loader silently
505/// falls back to the empty values block (`helm template` emits the
506/// library chart under its admission-time defaults, the workload comes
507/// up disabled or without any per-Servico M2 overlay applied) far from
508/// the drift commit, or the sibling
509/// [`caixa_flux::cluster_bundle`]'s future per-chart-directory
510/// resolver — a per-cluster snapshot bundle that re-lists the
511/// chart-dir contents by filename to route per-cluster values overlays
512/// through the canonical values file — silently returns `None` at
513/// cluster-side `feira app deploy` time. The equality + `&'static`
514/// static-data identity pin
515/// (`helm_values_yaml_filename_re_export_points_at_caixa_core_canonical`)
516/// closes the drift footgun at caixa-helm build time. Peer to the
517/// [`HELM_CHART_YAML_FILENAME`] re-export on the sibling
518/// canonical-Helm-per-chart-directory-metadata-file-axis surface —
519/// completes the per-`lareira-<nome>`-chart-directory
520/// `(Chart.yaml, values.yaml)` canonical-per-chart-directory-filename-
521/// axis re-export pair every rendered chart declares as its two
522/// schema-load-bearing `ChartDir::files` entries.
523pub use caixa_core::HELM_VALUES_YAML_FILENAME;
524
525/// Canonical `lareira-<nome>` chart-directory human-facing readme
526/// filename every rendered chart carries at its top-level directory —
527/// re-export of the lifted [`caixa_core::HELM_CHART_README_FILENAME`] so
528/// the third leg of the canonical `{Chart.yaml, values.yaml, README.md}`
529/// per-`lareira-<nome>` chart-directory `ChartFile` triple lives in
530/// exactly one place across every caixa renderer. The single
531/// production-code call site consuming it is
532/// [`render_chart_for_servico`]'s `ChartDir` assembly where the readme
533/// file's per-`ChartFile` `path` axis is set (the sole emitter site the
534/// prior inline `"README.md"` string literal sat at); every test-side
535/// round-trip navigator that reaches into the rendered `ChartDir` by
536/// the readme filename (the [`render_chart_for_servico`] files-vec-
537/// membership pin + the [`ChartDir::write_to`] post-write existence
538/// pin — two sites) now consults the same `&'static str`. A drifted
539/// local `pub const HELM_CHART_README_FILENAME: &str = "…"` at this
540/// crate — the canonical drift footgun where a sibling local
541/// `pub const` could happen to carry the same string at the source
542/// while pointing at a different `&'static` allocation — surfaces as
543/// GitHub / Artifact Hub / any downstream per-chart README-surfacing
544/// UI silently falling back to "no README available" for the rendered
545/// `lareira-<nome>` chart far from the drift commit's source, with no
546/// field naming the readme-filename-drift root cause. The equality +
547/// `&'static` static-data identity pin
548/// (`helm_chart_readme_filename_re_export_points_at_caixa_core_canonical`)
549/// closes the drift footgun at caixa-helm build time. Peer to the
550/// [`HELM_CHART_YAML_FILENAME`] / [`HELM_VALUES_YAML_FILENAME`]
551/// re-exports on the sibling canonical-Helm-per-chart-directory-
552/// filename axes — completes the per-`lareira-<nome>`-chart-directory
553/// `(Chart.yaml, values.yaml, README.md)` canonical-per-chart-directory-
554/// filename-axis re-export triple every rendered chart declares as its
555/// three `ChartDir::files` entries.
556pub use caixa_core::HELM_CHART_README_FILENAME;
557
558/// Canonical K8s CR top-level `spec` key. Re-export of the canonical
559/// [`caixa_core::KUBE_KEY_SPEC`] so the per-kind body key lives in
560/// exactly one place across every caixa renderer — caixa-helm's
561/// `build_values_yaml` (the upstream ComputeUnit YAML's `spec.*` axis
562/// the rendered `lareira-<nome>` chart's values block re-routes
563/// through the library alias) now consults the same `&'static str` as
564/// the peer caixa-flux / caixa-mesh renderers' `KUBE_KEY_SPEC`
565/// re-exports. The prior inline `"spec"` literal at the production-
566/// code call site would have let a typo (e.g. `"Spec"`, `"specs"`,
567/// `"spec_"`) silently emit a values block that drops every typed
568/// ComputeUnit-side field (`module`, `trigger`, `capabilities`,
569/// `resources`, `serviceAccount`) at the rendered chart's landing
570/// site — the `Error::MissingField("spec")` diagnostic now threads
571/// the same `&'static str` through the diagnostic surface so the
572/// error message stays byte-identical to the key it failed to find.
573/// Same shape as the [`DEFAULT_LIBRARY_NAME`] re-export on the
574/// sibling canonical-Helm-load-bearing-string axis.
575pub use caixa_core::KUBE_KEY_SPEC;
576
577/// Canonical `pleme-computeunit` library-chart values-block enable-toggle
578/// key — re-export of the lifted [`caixa_core::HELM_VALUES_KEY_ENABLED`]
579/// so the values-block toggle every rendered `lareira-<nome>` chart's
580/// values.yaml carries under its [`DEFAULT_LIBRARY_NAME`] wrap key lives
581/// in exactly one place across every caixa renderer. The single
582/// production-code call site consuming it is [`build_values_yaml`]'s
583/// `block.insert(HELM_VALUES_KEY_ENABLED.to_string(), …)` (formerly an
584/// inline `"enabled".to_string()` literal at `caixa-helm/src/lib.rs:389`);
585/// the peer test-fixture navigators pinning the default-off round-trip
586/// (`values_yaml_wraps_under_pleme_computeunit_key`,
587/// `values_yaml_wrap_key_follows_library_name_override`) also consult the
588/// re-export so a rebrand of the library-chart's per-values enable-toggle
589/// axis lands at one const and reaches every consumer by construction.
590/// A drifted local `pub const HELM_VALUES_KEY_ENABLED: &str = "…"` (or
591/// any sibling per-renderer variant that inlined a stale
592/// `"enabled"` / `"enable"` / `"disabled"` literal) would silently emit
593/// a values block whose per-values enable-toggle lands under one key
594/// while [`caixa_flux::cluster_bundle`]'s `HelmRelease`
595/// `spec.values.<library>.enabled` per-cluster override lands under
596/// another — Helm's per-values merge treats them as sibling scalars, the
597/// enable-toggle the library chart's own template consults never sees the
598/// flip, and the workload silently comes up with the library chart's
599/// admission-time defaults instead of the per-cluster override the
600/// operator set. Same shape as the [`DEFAULT_LIBRARY_NAME`] /
601/// [`KUBE_KEY_SPEC`] / [`HELM_CHART_API_VERSION`] re-exports on the
602/// sibling canonical-Helm-load-bearing-string / canonical-K8s-CR-body-
603/// key / canonical-Helm-chart-schema-apiVersion axes.
604pub use caixa_core::HELM_VALUES_KEY_ENABLED;
605
606/// Canonical substrate-side default for the
607/// `values.<library>.enabled` scalar-value toggle every
608/// [`render_chart_for_servico`]-emitted standalone `lareira-<nome>`
609/// chart's `values.yaml` document seeds inside its per-caixa
610/// [`DEFAULT_LIBRARY_NAME`] wrap block to leave the paired
611/// [`DEFAULT_LIBRARY_NAME`] child chart opted-out at the per-cluster
612/// `helm template` / `helm install` apply step. Re-export of the
613/// canonical [`caixa_core::STANDALONE_LAREIRA_ENABLED_DEFAULT`] so the
614/// substrate-side default the standalone per-chart path seeds under
615/// the sibling [`HELM_VALUES_KEY_ENABLED`] leaf-scalar-key lives in
616/// exactly one place across every caixa renderer. Consumed by
617/// [`RenderOpts::default`]'s `enabled_default` field seed (formerly
618/// an inline `false` scalar-value literal at
619/// `caixa-helm/src/lib.rs:700`); the peer test-fixture navigators
620/// pinning the default-off round-trip also consult the re-export so a
621/// rebrand of the per-values-block child-chart-enablement-toggle scalar
622/// on the standalone per-chart path lands at one const and reaches
623/// every consumer by construction. Semantically distinct from — and
624/// inverse of — the peer
625/// [`caixa_flux::CLUSTER_BUNDLE_LAREIRA_ENABLED_DEFAULT`] on the
626/// composition per-cluster-`HelmRelease` values-overlay path (which
627/// force-ons the child chart under the substrate-side composition
628/// path); the two peer scalar-value defaults name mirror-symmetric
629/// per-path child-chart-enablement-toggle-scalar-value defaults at the
630/// exact same `values.<library>.enabled` sub-block position on the
631/// standalone per-chart-`values.yaml` path (this re-export) and the
632/// composition per-cluster-`HelmRelease` values-overlay path (the peer
633/// re-export). Same shape as the [`DEFAULT_LIBRARY_NAME`] /
634/// [`KUBE_KEY_SPEC`] / [`HELM_VALUES_KEY_ENABLED`] re-exports on the
635/// sibling canonical-Helm-load-bearing-string / canonical-K8s-CR-body-
636/// key / canonical-Helm-per-values-block-enable-toggle-key axes. See
637/// [`caixa_core::STANDALONE_LAREIRA_ENABLED_DEFAULT`] for the full lift
638/// rationale.
639pub use caixa_core::STANDALONE_LAREIRA_ENABLED_DEFAULT;
640
641/// Local re-export of the canonical
642/// [`caixa_core::COMPUTEUNIT_SPEC_KEY_MODULE`] — the
643/// `wasm.pleme.io/v1alpha1/ComputeUnit` CRD per-CR wasm-module-reference
644/// `spec.module` sub-block key every rendered `values.yaml`'s
645/// [`DEFAULT_LIBRARY_NAME`]-wrapped block carries so the
646/// `pleme-computeunit` library chart's per-Servico module-source axis
647/// binds to the exact source the caixa.lisp's `:servicos` fixture
648/// pins. Two per-values drift-detection navigators in this crate's
649/// test module (the canonical-wrap-key round-trip + the
650/// `library-name`-override wrap-key round-trip) now consult the same
651/// `&'static str` as the peer caixa-flux writer's per-Servico
652/// `programs[]`-entry module-source navigators. Same re-export shape
653/// as the peer [`HELM_VALUES_KEY_ENABLED`] / [`KUBE_KEY_SPEC`]
654/// surfaces on the sibling canonical-Helm-load-bearing-string /
655/// canonical-K8s-CR-body-key axes — extends the discipline the
656/// M2-typed-slot / K8s-CR key re-export families establish onto the
657/// substrate-side ComputeUnit-CRD per-`spec.*` sub-block axis. See
658/// [`caixa_core::COMPUTEUNIT_SPEC_KEY_MODULE`] for the full lift
659/// rationale.
660pub use caixa_core::COMPUTEUNIT_SPEC_KEY_MODULE;
661
662/// Local re-export of the canonical
663/// [`caixa_core::COMPUTEUNIT_SPEC_KEY_TRIGGER`] — the
664/// `wasm.pleme.io/v1alpha1/ComputeUnit` CRD per-CR invocation-shape
665/// `spec.trigger` sub-block key every rendered `values.yaml`'s
666/// [`DEFAULT_LIBRARY_NAME`]-wrapped block carries so the
667/// `pleme-computeunit` library chart's per-Servico
668/// `trigger.service.{port, paths, breathability}` routing binds to
669/// the exact axis the caixa.lisp's `:servicos` fixture pins. Peer of
670/// [`COMPUTEUNIT_SPEC_KEY_MODULE`] on the same ComputeUnit CRD
671/// per-`spec.*` sub-block axis — see
672/// [`caixa_core::COMPUTEUNIT_SPEC_KEY_TRIGGER`] for the full lift
673/// rationale.
674pub use caixa_core::COMPUTEUNIT_SPEC_KEY_TRIGGER;
675
676/// Local re-export of the canonical
677/// [`caixa_core::COMPUTEUNIT_SPEC_KEY_CAPABILITIES`] — the
678/// `wasm.pleme.io/v1alpha1/ComputeUnit` CRD per-CR WASI-capability-list
679/// `spec.capabilities` sub-block key every rendered `values.yaml`'s
680/// [`DEFAULT_LIBRARY_NAME`]-wrapped block carries so the
681/// `pleme-computeunit` library chart's per-Servico WASI-preview-2
682/// capability-token binding fires exactly against the axis the
683/// caixa.lisp's `:servicos` fixture pins. Peer of
684/// [`COMPUTEUNIT_SPEC_KEY_MODULE`] and [`COMPUTEUNIT_SPEC_KEY_TRIGGER`]
685/// on the same ComputeUnit CRD per-`spec.*` sub-block axis — completes
686/// the substrate-side ComputeUnit-CRD per-`spec.*` sub-block re-export
687/// triple in this crate. See
688/// [`caixa_core::COMPUTEUNIT_SPEC_KEY_CAPABILITIES`] for the full lift
689/// rationale.
690pub use caixa_core::COMPUTEUNIT_SPEC_KEY_CAPABILITIES;
691
692/// Local re-export of the canonical
693/// [`caixa_core::servico_spec_and_m2_overlay_entries`] — the composed
694/// per-Servico value-block splice helper this crate's
695/// [`build_values_yaml`] and the peer
696/// [`caixa_flux::programs_yaml_entry`] both now route their two-step
697/// `spec.*` field-splice + M2 typed-slot overlay through. The single
698/// production-code call site consuming it is [`build_values_yaml`]'s
699/// inner splice loop (formerly two hand-written for-loops chained
700/// around `string_keyed_entries` + `servico_m2_overlay`); re-exported
701/// so the shared composition contract lives in exactly one place
702/// across both per-Servico renderers — a future author reading
703/// `caixa_helm::build_values_yaml` finds the composition helper
704/// immediately without an extra `use caixa_core::…` line, and a
705/// rebrand of the composition axis (e.g. a swap of the `or_insert`
706/// precedence rule once per-Aplicacao operator overrides land)
707/// reaches both renderers through one canonical `&'static` function
708/// pointer. Same shape as the peer [`COMPUTEUNIT_SPEC_KEY_MODULE`] /
709/// [`COMPUTEUNIT_SPEC_KEY_TRIGGER`] / [`COMPUTEUNIT_SPEC_KEY_CAPABILITIES`]
710/// re-exports on the sibling canonical-ComputeUnit-CRD `spec.*` axis
711/// — extends the shared-composition discipline the per-`spec.*`
712/// sub-block re-export triple establishes onto the composed
713/// spec.*+M2 splice axis every per-Servico renderer navigates.
714pub use caixa_core::servico_spec_and_m2_overlay_entries;
715
716/// Knobs that don't come from the Caixa manifest.
717#[derive(Debug, Clone)]
718pub struct RenderOpts {
719 /// Where the library chart lives. Default = `file://../pleme-computeunit`.
720 pub library_repo: String,
721 pub library_version: String,
722 pub library_name: String,
723 /// Whether the rendered values block is `enabled: false` by default
724 /// (matching `lareira-hello-world` so cluster operators flip it on
725 /// per-cluster). Default: [`STANDALONE_LAREIRA_ENABLED_DEFAULT`]
726 /// (`false` — the substrate's chosen standalone per-chart
727 /// opt-out seed, inverse of the composition per-cluster-`HelmRelease`
728 /// values-overlay path's [`caixa_flux::CLUSTER_BUNDLE_LAREIRA_ENABLED_DEFAULT`]
729 /// force-on).
730 pub enabled_default: bool,
731}
732
733impl Default for RenderOpts {
734 fn default() -> Self {
735 Self {
736 library_repo: DEFAULT_LIBRARY_REPO.into(),
737 library_version: DEFAULT_LIBRARY_VERSION.into(),
738 library_name: DEFAULT_LIBRARY_NAME.into(),
739 enabled_default: STANDALONE_LAREIRA_ENABLED_DEFAULT,
740 }
741 }
742}
743
744/// Render a per-program lareira-<name> chart from a Caixa Servico + its
745/// loaded ComputeUnit YAML.
746///
747/// The ComputeUnit YAML is passed in as a `serde_yaml::Value` because the
748/// authoritative schema lives in the wasm-operator's CRD — we don't want
749/// caixa-helm to drift from that schema. It's enough that we can locate
750/// `spec` and pass it through.
751pub fn render_chart_for_servico(
752 caixa: &Caixa,
753 computeunit_yaml: &serde_yaml::Value,
754) -> Result<ChartDir, Error> {
755 render_chart_for_servico_with(caixa, computeunit_yaml, &RenderOpts::default())
756}
757
758/// `render_chart_for_servico` with explicit options.
759pub fn render_chart_for_servico_with(
760 caixa: &Caixa,
761 computeunit_yaml: &serde_yaml::Value,
762 opts: &RenderOpts,
763) -> Result<ChartDir, Error> {
764 caixa_core::require_v0_servico_shape::<Error>(caixa)?;
765
766 // Canonical typed `&str`-read of the per-`Caixa` `:nome`
767 // universal-axis DNS-1123-label caixa-identity scalar into
768 // the per-chart-directory `lareira-<nome>` identity composer.
769 // Peer of the sibling 4a363bf / 54bf2f3 `caixa.nome.clone()`
770 // converges on the outer-Caixa `:nome` `String`-carry axis
771 // in caixa-flux / caixa-mesh and the sibling eb912de
772 // `caixa.versao.clone()` converge on the co-resident
773 // `Caixa::versao` `String`-carry axis in this crate — this
774 // extends the "one typed dispatch on the substrate primitive,
775 // thin projections at each consumer" discipline onto the
776 // non-`.clone()` raw-field-access axis of `Caixa::nome` in
777 // caixa-helm.
778 let chart_name = lareira_chart_name(caixa.nome());
779 let chart_yaml = build_chart_yaml(caixa, &chart_name, opts);
780 let values_yaml = build_values_yaml(caixa, computeunit_yaml, opts)?;
781 let readme = build_readme(caixa, &chart_name);
782
783 // Each per-artifact leaf routes through the canonical
784 // [`caixa_core::RenderedFile::new`] `impl Into<PathBuf>` /
785 // `impl Into<String>` constructor (re-exported by the peer
786 // [`ChartFile`] alias since Rust inherent methods travel through
787 // type aliases to the aliased type at name resolution). The prior
788 // three inline `ChartFile { path: PathBuf::from(FILENAME_CONST),
789 // contents: <body> }` blocks each re-derived the same
790 // `PathBuf::from(&str)` wrap + the same two-field assembly — a
791 // byte-identical duplicate of the peer [`caixa_flux::cluster_bundle`]
792 // Flux v2 CR trio's three per-CR emit sites. Sweeping both trios
793 // onto [`RenderedFile::new`] collapses the six substrate-side
794 // per-artifact-construction sites onto one canonical constructor,
795 // so a future rebrand on the record shape (a per-artifact hash /
796 // provenance field addition, a per-artifact write-mode discriminator
797 // once per-cluster-writer sandboxing lands, the
798 // [`caixa_core::is_sandboxed_relative_path`] discipline the
799 // [`RenderedFile`] docstring acknowledges is not yet run at emit
800 // time) reaches every per-target renderer through one caixa-core
801 // edit instead of a coordinated six-site rewrite.
802 Ok(ChartDir {
803 name: chart_name,
804 files: vec![
805 ChartFile::new(
806 HELM_CHART_YAML_FILENAME,
807 serde_yaml::to_string(&chart_yaml)?,
808 ),
809 ChartFile::new(HELM_VALUES_YAML_FILENAME, values_yaml),
810 ChartFile::new(HELM_CHART_README_FILENAME, readme),
811 ],
812 })
813}
814
815fn build_chart_yaml(caixa: &Caixa, chart_name: &str, opts: &RenderOpts) -> ChartYaml {
816 let description = caixa
817 .descricao()
818 .map(str::to_owned)
819 .unwrap_or_else(|| format!("Generated chart for caixa Servico {}", caixa.nome()));
820 let keywords: Vec<String> = caixa
821 .etiquetas()
822 .iter()
823 .cloned()
824 .chain(
825 caixa_core::LAREIRA_CHART_KEYWORDS
826 .iter()
827 .copied()
828 .map(String::from),
829 )
830 .collect::<Vec<_>>()
831 .into_iter()
832 .collect::<std::collections::BTreeSet<_>>()
833 .into_iter()
834 .collect();
835 let maintainers = caixa
836 .autores()
837 .iter()
838 .map(|a| Maintainer {
839 name: a.clone(),
840 email: None,
841 })
842 .collect();
843 // Canonical typed `String`-carry of the per-`Caixa` `:versao`
844 // universal-axis SemVer-2 pinned-version scalar into the two
845 // per-`Chart.yaml` version-carrier fields Helm's chart-schema
846 // parser routes per-chart identity through — `Chart.yaml`'s
847 // top-level `version:` (the axis Helm's per-chart resolver keys
848 // per-release reconciliation off, the paired `HelmRelease`
849 // `spec.chart.spec.version` binds through, and every `helm
850 // template <chart>` / `helm install <release> <chart>` /
851 // `helm upgrade <release> <chart> --version` invocation names
852 // through) and `Chart.yaml`'s top-level `appVersion:` (the
853 // axis Helm chart-consumers key per-application-version
854 // documentation / release-note / OCI-tag / operator-side
855 // per-Caixa CR revision off), both routing through the typed
856 // [`caixa_core::Caixa::versao`] accessor's canonical
857 // `to_string()` extension of `&self.versao`. Peer of the
858 // sibling 4a363bf / 54bf2f3 `caixa.nome.clone()` converges
859 // on the outer-Caixa `:nome` `String`-carry axis in caixa-flux
860 // / caixa-mesh — this converges the last unlifted per-Caixa
861 // `.versao.clone()` raw-field `String`-carry axis in
862 // caixa-helm on the same "one typed dispatch per axis"
863 // discipline.
864 let versao = caixa.versao().to_string();
865 ChartYaml {
866 api_version: HELM_CHART_API_VERSION.into(),
867 name: chart_name.into(),
868 description,
869 chart_type: HELM_CHART_TYPE_APPLICATION.into(),
870 version: versao.clone(),
871 app_version: versao,
872 keywords,
873 maintainers,
874 home: caixa.repositorio().map(str::to_owned),
875 dependencies: vec![ChartDependency {
876 name: opts.library_name.clone(),
877 version: opts.library_version.clone(),
878 repository: opts.library_repo.clone(),
879 alias: None,
880 }],
881 }
882}
883
884fn build_values_yaml(
885 caixa: &Caixa,
886 computeunit_yaml: &serde_yaml::Value,
887 opts: &RenderOpts,
888) -> Result<String, Error> {
889 // The library chart consumes its values under the key matching its
890 // Helm chart `dependencies[].name` (Helm's per-dep alias convention
891 // — when no `alias:` is set on the dependency, values are scoped
892 // under the dependency's `name`). This renderer wires both axes
893 // through the same `opts.library_name`: the chart's dep `name:`
894 // (build_chart_yaml at line 277) and this site's values wrap key
895 // both consult one `&str`, so a future fork that overrides
896 // `RenderOpts::library_name` to point at `acme-computeunit` /
897 // `pleme-computeunit-mirror` / the future per-edition library name
898 // reaches both axes by construction. Until this lift landed the
899 // wrap key was hardcoded `"pleme-computeunit"` while the dep name
900 // followed `opts.library_name`, so an override silently emitted
901 // values keyed under one name (the literal) while the rendered
902 // Chart.yaml's dep was declared under another (the override) —
903 // Helm's per-dep values router would route nothing to the
904 // configured dependency at `helm template` / `helm install` time,
905 // and every typed value the values block carries (`enabled`,
906 // `module`, `trigger`, the M2 overlay's `:limits`/`:behavior`/
907 // `:upgrade-from`) would silently no-op at the rendered chart's
908 // landing site. The wrap key now reads from the same `&str` the
909 // dep name reads from, structurally closing the drift footgun
910 // peer with the [`caixa_core::DEFAULT_NAMESPACE`] /
911 // [`caixa_core::DEFAULT_SERVICO_PORT`] lifts on the sibling
912 // canonical-K8s-axis constants (where two production-code call
913 // sites of the same load-bearing value would drift apart on
914 // any rebrand without a shared source of truth).
915 let library_alias = opts.library_name.as_str();
916 let spec = computeunit_yaml
917 .get(KUBE_KEY_SPEC)
918 .ok_or(Error::MissingField(KUBE_KEY_SPEC))?;
919
920 // Prepend a comment header so the file is human-friendly.
921 let header = format!(
922 "# Auto-generated by caixa-helm from caixa.lisp + servicos/{nome}.computeunit.yaml.\n\
923 # Edits to this file are overwritten by `feira chart`.\n\
924 #\n\
925 # `{library_alias}:` is the alias under which the library chart\n\
926 # in pleme-io/helmworks/charts/{library_alias} consumes its values.\n\n",
927 nome = caixa.nome()
928 );
929
930 let mut block = BTreeMap::new();
931 block.insert(
932 HELM_VALUES_KEY_ENABLED.to_string(),
933 serde_yaml::Value::Bool(opts.enabled_default),
934 );
935 // Two-step per-Servico value-block splice — the `spec.*` field
936 // splice (module / trigger / capabilities / config / resources /
937 // serviceAccount) and the M2 typed-slot overlay (limits / behavior
938 // / upgradeFrom, `or_insert` semantics so `spec.*` wins on
939 // collision) now route through the canonical
940 // [`caixa_core::servico_spec_and_m2_overlay_entries`] composition
941 // helper — the two prior inline for-loops chained around
942 // `string_keyed_entries` + `servico_m2_overlay` this call site
943 // (and the peer [`caixa_flux::programs_yaml_entry`] site) each
944 // re-derived collapse onto one canonical composition, so a future
945 // change to the per-Servico splice / overlay shape (the M4 typed
946 // per-edge policy overlay slot addition MESH-COMPOSITION §III.2 #3
947 // acknowledges, a change to the precedence rule once per-Aplicacao
948 // operator overrides land, a canonicalization pass on the merged
949 // key set) reaches both renderers by construction instead of a
950 // coordinated two-file rewrite. See the helper's docstring for the
951 // full lift rationale. The target `BTreeMap` re-sorts by key on
952 // insert, so the final rendered values block stays byte-identical
953 // to the prior inline block's alphabetical shape.
954 for (k, v) in caixa_core::servico_spec_and_m2_overlay_entries(caixa, spec)? {
955 block.entry(k).or_insert(v);
956 }
957
958 let mut wrapped = serde_yaml::Mapping::new();
959 wrapped.insert_str_key(library_alias, serde_yaml::to_value(block)?);
960 let body = serde_yaml::to_string(&serde_yaml::Value::Mapping(wrapped))?;
961 Ok(format!("{header}{body}"))
962}
963
964fn build_readme(caixa: &Caixa, chart_name: &str) -> String {
965 let descricao = caixa
966 .descricao()
967 .map(str::to_owned)
968 .unwrap_or_else(|| format!("caixa Servico {}", caixa.nome()));
969 format!(
970 "# {chart_name}\n\
971 \n\
972 {descricao}\n\
973 \n\
974 ## Origin\n\
975 \n\
976 Generated by `caixa-helm` from `{repo}/caixa.lisp` v{versao}.\n\
977 Edits here are overwritten by `feira chart`.\n\
978 \n\
979 ## Install\n\
980 \n\
981 ```bash\n\
982 helm dependency build\n\
983 helm template {chart_name} . --values values.yaml\n\
984 ```\n\
985 \n\
986 ## License\n\
987 \n\
988 {license}.\n",
989 chart_name = chart_name,
990 descricao = descricao,
991 repo = caixa.repositorio().unwrap_or(caixa.nome()),
992 versao = caixa.versao(),
993 license = caixa.licenca().unwrap_or("MIT"),
994 )
995}
996
997#[cfg(test)]
998mod tests {
999 use super::*;
1000 use caixa_core::{
1001 Caixa, CaixaKind, M2_BEHAVIOR_KEY_ON_CALL, M2_BEHAVIOR_KEY_ON_INIT, M2_KEY_BEHAVIOR,
1002 M2_KEY_LIMITS, M2_KEY_UPGRADE_FROM, M2_LIMITS_KEY_CPU, M2_LIMITS_KEY_FUEL,
1003 M2_LIMITS_KEY_MEMORY, M2_LIMITS_KEY_WALL_CLOCK,
1004 };
1005 use std::path::PathBuf;
1006
1007 fn sample_caixa() -> Caixa {
1008 Caixa {
1009 nome: "hello-rio".into(),
1010 versao: "0.1.0".into(),
1011 kind: CaixaKind::Servico,
1012 edicao: Some("2026".into()),
1013 descricao: Some("Canonical Rust→wasm32-wasip2 caixa Servico.".into()),
1014 repositorio: Some("github:pleme-io/hello-rio".into()),
1015 licenca: Some("MIT".into()),
1016 autores: vec!["pleme-io".into()],
1017 etiquetas: vec!["hello-world".into(), "wasm".into(), "rust".into()],
1018 deps: vec![],
1019 deps_dev: vec![],
1020 exe: vec![],
1021 bibliotecas: vec![],
1022 servicos: vec!["servicos/hello-rio.computeunit.yaml".into()],
1023 limits: None,
1024 behavior: None,
1025 upgrade_from: vec![],
1026 estrategia: None,
1027 max_restarts: None,
1028 restart_window: None,
1029 children: vec![],
1030 membros: vec![],
1031 contratos: vec![],
1032 politicas: None,
1033 placement: None,
1034 entrada: None,
1035 ci: None,
1036 }
1037 }
1038
1039 fn sample_cu_yaml() -> serde_yaml::Value {
1040 serde_yaml::from_str(
1041 r#"
1042apiVersion: wasm.pleme.io/v1alpha1
1043kind: ComputeUnit
1044metadata:
1045 name: hello-rio
1046spec:
1047 module:
1048 source: oci://ghcr.io/pleme-io/hello-rio:v0.1.0
1049 trigger:
1050 service:
1051 port: 8080
1052 paths: ["/", "/hello", "/healthz"]
1053 breathability:
1054 enabled: true
1055 minReplicas: 0
1056 maxReplicas: 5
1057 cooldownPeriod: 600
1058 capabilities:
1059 - http-in:0.0.0.0:8080
1060 - env
1061"#,
1062 )
1063 .unwrap()
1064 }
1065
1066 #[test]
1067 fn renders_three_files() {
1068 let dir = render_chart_for_servico(&sample_caixa(), &sample_cu_yaml()).unwrap();
1069 assert_eq!(dir.name, "lareira-hello-rio");
1070 let names: Vec<_> = dir
1071 .files
1072 .iter()
1073 .map(|f| f.path.to_string_lossy().to_string())
1074 .collect();
1075 assert!(names.contains(&HELM_CHART_YAML_FILENAME.to_string()));
1076 assert!(names.contains(&HELM_VALUES_YAML_FILENAME.to_string()));
1077 assert!(names.contains(&HELM_CHART_README_FILENAME.to_string()));
1078 }
1079
1080 #[test]
1081 fn chart_yaml_metadata_propagates() {
1082 let dir = render_chart_for_servico(&sample_caixa(), &sample_cu_yaml()).unwrap();
1083 let chart_file = dir
1084 .files
1085 .iter()
1086 .find(|f| f.path == PathBuf::from(HELM_CHART_YAML_FILENAME))
1087 .unwrap();
1088 let chart: ChartYaml = serde_yaml::from_str(&chart_file.contents).unwrap();
1089 assert_eq!(chart.api_version, "v2");
1090 assert_eq!(chart.name, "lareira-hello-rio");
1091 assert_eq!(chart.version, "0.1.0");
1092 assert_eq!(chart.app_version, "0.1.0");
1093 assert_eq!(chart.dependencies.len(), 1);
1094 assert_eq!(chart.dependencies[0].name, DEFAULT_LIBRARY_NAME);
1095 assert!(chart.keywords.contains(&"caixa-servico".to_string()));
1096 assert!(chart.keywords.contains(&"hello-world".to_string()));
1097 assert_eq!(chart.maintainers[0].name, "pleme-io");
1098 }
1099
1100 #[test]
1101 fn chart_yaml_keywords_union_pins_every_lareira_chart_keywords_entry() {
1102 // Structural pin: `build_chart_yaml`'s substrate-fixed
1103 // chart-keyword union routes through the canonical
1104 // `caixa_core::LAREIRA_CHART_KEYWORDS` array — every rendered
1105 // `lareira-<nome>` chart's emitted `Chart.yaml` `keywords:`
1106 // sequence carries every substrate-fixed entry the array
1107 // declares. A future substrate-fixed keyword addition
1108 // (an `"opentelemetry"` entry once the caixa-otel collector-
1109 // pipeline chart lands, a `"lunatic"` entry once the wasm-
1110 // process-runtime marker lands, a `"gen_server"` entry once
1111 // the OTP-shape callback marker lands per the
1112 // [`caixa_core::behavior`] surface) that lands in the array
1113 // reaches this crate's production emit site by construction
1114 // through the shared `&[&str]` reference — a drift where the
1115 // production emit at `build_chart_yaml` re-inlines the pre-
1116 // lift `["lareira", "wasm", "tatara-lisp", "caixa-servico"]`
1117 // literal set (or drops a per-entry axis on a rebrand
1118 // sweep) fails this pin at caixa-helm build time rather than
1119 // surfacing as a `helm search hub caixa-servico` miss on the
1120 // Artifact Hub keyword-search index at chart-publish time
1121 // downstream. Peer to
1122 // [`chart_yaml_metadata_propagates`] on the
1123 // per-`Chart.yaml`-body-field propagation surface.
1124 let dir = render_chart_for_servico(&sample_caixa(), &sample_cu_yaml()).unwrap();
1125 let chart_file = dir
1126 .files
1127 .iter()
1128 .find(|f| f.path == PathBuf::from(HELM_CHART_YAML_FILENAME))
1129 .unwrap();
1130 let chart: ChartYaml = serde_yaml::from_str(&chart_file.contents).unwrap();
1131 for keyword in caixa_core::LAREIRA_CHART_KEYWORDS {
1132 assert!(
1133 chart.keywords.contains(&(*keyword).to_string()),
1134 "rendered Chart.yaml keywords {:?} must contain the \
1135 substrate-fixed LAREIRA_CHART_KEYWORDS entry {keyword:?}",
1136 chart.keywords,
1137 );
1138 }
1139 }
1140
1141 #[test]
1142 fn values_yaml_wraps_under_pleme_computeunit_key() {
1143 let dir = render_chart_for_servico(&sample_caixa(), &sample_cu_yaml()).unwrap();
1144 let values = dir
1145 .files
1146 .iter()
1147 .find(|f| f.path == PathBuf::from(HELM_VALUES_YAML_FILENAME))
1148 .unwrap();
1149 let parsed: serde_yaml::Value = serde_yaml::from_str(&values.contents).unwrap();
1150 let cu_block = parsed
1151 .get(DEFAULT_LIBRARY_NAME)
1152 .expect("must wrap under DEFAULT_LIBRARY_NAME");
1153 assert_eq!(
1154 cu_block.get(HELM_VALUES_KEY_ENABLED),
1155 Some(&serde_yaml::Value::Bool(false))
1156 );
1157 assert!(cu_block.get(COMPUTEUNIT_SPEC_KEY_MODULE).is_some());
1158 assert!(cu_block.get(COMPUTEUNIT_SPEC_KEY_TRIGGER).is_some());
1159 assert!(cu_block.get(COMPUTEUNIT_SPEC_KEY_CAPABILITIES).is_some());
1160 }
1161
1162 #[test]
1163 fn values_yaml_wrap_key_follows_library_name_override() {
1164 // Pinning the canonical alignment between the Helm chart's
1165 // `dependencies[].name` axis (build_chart_yaml at line 277) and
1166 // the values block's wrap key (build_values_yaml at the
1167 // `wrapped.insert(...)` site): both consult the same
1168 // `opts.library_name`, so an override on either axis reaches the
1169 // other by construction. Helm's per-dep alias convention — when
1170 // no `alias:` is set on a dependency, values are scoped under
1171 // its `name:` — makes wrap-key drift a silent value-routing
1172 // no-op at `helm template` / `helm install` time, so the
1173 // structural pin is load-bearing.
1174 let opts = RenderOpts {
1175 library_name: "acme-computeunit".into(),
1176 ..RenderOpts::default()
1177 };
1178 let dir = render_chart_for_servico_with(&sample_caixa(), &sample_cu_yaml(), &opts).unwrap();
1179 let values = dir
1180 .files
1181 .iter()
1182 .find(|f| f.path == PathBuf::from(HELM_VALUES_YAML_FILENAME))
1183 .unwrap();
1184 let parsed: serde_yaml::Value = serde_yaml::from_str(&values.contents).unwrap();
1185 assert!(
1186 parsed.get("acme-computeunit").is_some(),
1187 "values wrap key must follow opts.library_name override \
1188 (got top-level keys: {keys:?})",
1189 keys = parsed
1190 .as_mapping()
1191 .map(|m| m
1192 .keys()
1193 .filter_map(|k| k.as_str().map(str::to_string))
1194 .collect::<Vec<_>>())
1195 .unwrap_or_default()
1196 );
1197 assert!(
1198 parsed.get(DEFAULT_LIBRARY_NAME).is_none(),
1199 "values wrap key must not retain the default `{DEFAULT_LIBRARY_NAME}` literal \
1200 when opts.library_name overrides it"
1201 );
1202 let cu_block = parsed.get("acme-computeunit").unwrap();
1203 assert_eq!(
1204 cu_block.get(HELM_VALUES_KEY_ENABLED),
1205 Some(&serde_yaml::Value::Bool(false))
1206 );
1207 assert!(cu_block.get(COMPUTEUNIT_SPEC_KEY_MODULE).is_some());
1208 assert!(cu_block.get(COMPUTEUNIT_SPEC_KEY_TRIGGER).is_some());
1209 assert!(cu_block.get(COMPUTEUNIT_SPEC_KEY_CAPABILITIES).is_some());
1210 }
1211
1212 #[test]
1213 fn values_yaml_wrap_key_matches_chart_dependency_name() {
1214 // The structural invariant the lift defends: every rendered
1215 // chart's values.yaml wrap key equals its Chart.yaml
1216 // `dependencies[0].name`. Sweeping the canonical default + a
1217 // typed override on the same axis pins the alignment across the
1218 // accepted set of `RenderOpts::library_name` values rather than
1219 // at a single canonical literal.
1220 for library_name in [DEFAULT_LIBRARY_NAME, "acme-computeunit", "fork-pleme-cu"] {
1221 let opts = RenderOpts {
1222 library_name: library_name.into(),
1223 ..RenderOpts::default()
1224 };
1225 let dir =
1226 render_chart_for_servico_with(&sample_caixa(), &sample_cu_yaml(), &opts).unwrap();
1227 let chart_file = dir
1228 .files
1229 .iter()
1230 .find(|f| f.path == PathBuf::from(HELM_CHART_YAML_FILENAME))
1231 .unwrap();
1232 let chart: ChartYaml = serde_yaml::from_str(&chart_file.contents).unwrap();
1233 let dep_name = &chart.dependencies[0].name;
1234 let values = dir
1235 .files
1236 .iter()
1237 .find(|f| f.path == PathBuf::from(HELM_VALUES_YAML_FILENAME))
1238 .unwrap();
1239 let parsed: serde_yaml::Value = serde_yaml::from_str(&values.contents).unwrap();
1240 assert!(
1241 parsed.get(dep_name.as_str()).is_some(),
1242 "values.yaml wrap key must match Chart.yaml dependencies[0].name {dep_name:?} \
1243 (library_name = {library_name:?}); Helm's per-dep alias convention scopes \
1244 values under the dep's `name` when no `alias:` is set, so any drift between \
1245 the two axes silently routes the values block nowhere"
1246 );
1247 }
1248 }
1249
1250 #[test]
1251 fn values_yaml_header_comment_follows_library_name_override() {
1252 // The human-facing values.yaml header's `<library_alias>:` /
1253 // `pleme-io/helmworks/charts/<library_alias>` references both
1254 // resolve through `opts.library_name`, peer with the wrap key
1255 // itself, so an override leaves the header self-consistent
1256 // with the rendered structure rather than naming a drifted
1257 // default literal.
1258 let opts = RenderOpts {
1259 library_name: "acme-computeunit".into(),
1260 ..RenderOpts::default()
1261 };
1262 let dir = render_chart_for_servico_with(&sample_caixa(), &sample_cu_yaml(), &opts).unwrap();
1263 let values = dir
1264 .files
1265 .iter()
1266 .find(|f| f.path == PathBuf::from(HELM_VALUES_YAML_FILENAME))
1267 .unwrap();
1268 assert!(
1269 values.contents.contains("`acme-computeunit:`"),
1270 "header must name the overriding library alias verbatim \
1271 (got: {contents:?})",
1272 contents = values.contents
1273 );
1274 assert!(
1275 values
1276 .contents
1277 .contains("pleme-io/helmworks/charts/acme-computeunit"),
1278 "header's helmworks path must follow the overriding library alias \
1279 (got: {contents:?})",
1280 contents = values.contents
1281 );
1282 assert!(
1283 !values.contents.contains("`pleme-computeunit:`"),
1284 "header must not retain the default library alias literal \
1285 when overridden (got: {contents:?})",
1286 contents = values.contents
1287 );
1288 }
1289
1290 #[test]
1291 fn refuses_non_servico() {
1292 let mut c = sample_caixa();
1293 c.kind = CaixaKind::Biblioteca;
1294 c.servicos = vec![];
1295 let err = render_chart_for_servico(&c, &sample_cu_yaml()).unwrap_err();
1296 assert!(matches!(err, Error::NotAServico(_)));
1297 }
1298
1299 #[test]
1300 fn kind_mismatch_error_names_offending_caixa_nome() {
1301 // Pinning the lifted [`caixa_core::KindMismatch`] view's
1302 // load-bearing property: a kind-mismatched caixa surfaces a
1303 // diagnostic that *names the offending caixa* (`hello-rio`),
1304 // not just the rejected kind. Before the lift the renderer
1305 // raised `Error::NotAServico(CaixaKind::Biblioteca)` whose
1306 // Display said "caixa :kind must be Servico for caixa-helm
1307 // rendering, got Biblioteca" — the user had to grep their
1308 // source tree for which caixa.lisp triggered it. After the
1309 // lift the wrapped KindMismatch carries the `:nome`, the
1310 // renderer's `#[error("{0}")]` arm prints it through, and
1311 // the diagnostic is self-locating.
1312 let mut c = sample_caixa();
1313 c.kind = CaixaKind::Biblioteca;
1314 c.servicos = vec![];
1315 let err = render_chart_for_servico(&c, &sample_cu_yaml()).unwrap_err();
1316 let msg = format!("{err}");
1317 assert!(
1318 msg.contains("hello-rio"),
1319 "kind-mismatch diagnostic must name the offending caixa nome \
1320 (got: {msg:?})"
1321 );
1322 assert!(
1323 msg.contains("Servico"),
1324 "diagnostic must name the expected kind (got: {msg:?})"
1325 );
1326 assert!(
1327 msg.contains("Biblioteca"),
1328 "diagnostic must name the actual kind (got: {msg:?})"
1329 );
1330 }
1331
1332 #[test]
1333 fn kind_mismatch_carries_typed_view_via_from_conversion() {
1334 // The renderer's `Error::NotAServico` variant wraps the typed
1335 // [`caixa_core::KindMismatch`] view via `#[from]`, so the `?`
1336 // operator at the call site converts without manual glue.
1337 // Pinning the typed payload (not just the variant) so a
1338 // future refactor can't silently switch the variant to a
1339 // raw-`CaixaKind` payload (which would regress the lift's
1340 // shared-shape contract with caixa-flux + caixa-mesh).
1341 let mut c = sample_caixa();
1342 c.kind = CaixaKind::Aplicacao;
1343 c.servicos = vec![];
1344 let err = render_chart_for_servico(&c, &sample_cu_yaml()).unwrap_err();
1345 match err {
1346 Error::NotAServico(km) => {
1347 assert_eq!(km.nome, "hello-rio");
1348 assert_eq!(km.expected, CaixaKind::Servico);
1349 assert_eq!(km.actual, CaixaKind::Aplicacao);
1350 }
1351 other => panic!("expected Error::NotAServico, got {other:?}"),
1352 }
1353 }
1354
1355 #[test]
1356 fn servico_count_mismatch_carries_typed_view_with_nome() {
1357 // Peer to the [`KindMismatch`]-lift pin above on the V0
1358 // `:servicos`-singularity axis: a Servico-kind caixa whose
1359 // `:servicos` list is non-singleton fails
1360 // [`render_chart_for_servico`] with the renderer's
1361 // `Error::UnsupportedServicoCount` variant wrapping the typed
1362 // [`caixa_core::ServicoCountMismatch`] view (carrying the
1363 // offending caixa's `:nome` + the actual count). Before the
1364 // lift the variant carried only `usize` — the user had to grep
1365 // their source tree for which `caixa.lisp` triggered it; after
1366 // the lift the wrapped typed view names the offending caixa
1367 // verbatim. Pins both the variant routing (via `#[from]`) and
1368 // the typed payload so a future refactor can't silently switch
1369 // back to the raw-`usize` payload (which would regress the
1370 // shared-shape contract with caixa-flux on the peer
1371 // programs.yaml-entry path).
1372 let mut c = sample_caixa();
1373 c.servicos = vec![
1374 "servicos/hello-rio.computeunit.yaml".into(),
1375 "servicos/extra.computeunit.yaml".into(),
1376 ];
1377 let err = render_chart_for_servico(&c, &sample_cu_yaml()).unwrap_err();
1378 match err {
1379 Error::UnsupportedServicoCount(scm) => {
1380 assert_eq!(scm.nome, "hello-rio");
1381 assert_eq!(scm.count, 2);
1382 }
1383 other => panic!("expected Error::UnsupportedServicoCount, got {other:?}"),
1384 }
1385 }
1386
1387 #[test]
1388 fn servico_count_mismatch_diagnostic_names_offending_caixa_nome() {
1389 // The renderer's `#[error("{0}")] UnsupportedServicoCount(
1390 // #[from] ServicoCountMismatch)` arm prints the typed view's
1391 // Display through verbatim, so the offending caixa's `:nome`
1392 // appears in the rendered diagnostic. Pinning the
1393 // self-locating property end-to-end (renderer entry-point →
1394 // typed view's Display → final diagnostic string) so a future
1395 // refactor that re-wraps the variant in a Display impl that
1396 // drops the `:nome` surfaces here as a test failure rather
1397 // than as silent fragmentation of the diagnostic. Peer to the
1398 // `kind_mismatch_error_names_offending_caixa_nome` test above
1399 // on the sibling V0 Servico-shape axis.
1400 let mut c = sample_caixa();
1401 c.servicos = vec![
1402 "servicos/hello-rio.computeunit.yaml".into(),
1403 "servicos/extra.computeunit.yaml".into(),
1404 ];
1405 let err = render_chart_for_servico(&c, &sample_cu_yaml()).unwrap_err();
1406 let msg = format!("{err}");
1407 assert!(
1408 msg.contains("hello-rio"),
1409 ":servicos-count-mismatch diagnostic must name the offending caixa nome \
1410 (got: {msg:?})"
1411 );
1412 assert!(
1413 msg.contains("2"),
1414 "diagnostic must name the actual count (got: {msg:?})"
1415 );
1416 assert!(
1417 msg.contains(":servicos"),
1418 "diagnostic must name the offending field axis (got: {msg:?})"
1419 );
1420 }
1421
1422 #[test]
1423 fn limits_slot_propagates_into_values_block() {
1424 use caixa_core::LimitsSpec;
1425 use std::time::Duration;
1426 let mut c = sample_caixa();
1427 c.limits = Some(LimitsSpec {
1428 memory: Some(64 * 1024 * 1024),
1429 fuel: Some(1_000_000),
1430 wall_clock: Some(Duration::from_secs(30)),
1431 cpu: Some(500),
1432 });
1433 let dir = render_chart_for_servico(&c, &sample_cu_yaml()).unwrap();
1434 let values = dir
1435 .files
1436 .iter()
1437 .find(|f| f.path == PathBuf::from(HELM_VALUES_YAML_FILENAME))
1438 .unwrap();
1439 let parsed: serde_yaml::Value = serde_yaml::from_str(&values.contents).unwrap();
1440 let cu_block = parsed.get(DEFAULT_LIBRARY_NAME).unwrap();
1441 let limits = cu_block.get(M2_KEY_LIMITS).expect("limits must propagate");
1442 assert_eq!(
1443 limits.get(M2_LIMITS_KEY_MEMORY).and_then(|m| m.as_str()),
1444 Some("64MiB")
1445 );
1446 assert_eq!(
1447 limits.get(M2_LIMITS_KEY_FUEL).and_then(|m| m.as_u64()),
1448 Some(1_000_000)
1449 );
1450 assert_eq!(
1451 limits
1452 .get(M2_LIMITS_KEY_WALL_CLOCK)
1453 .and_then(|m| m.as_str()),
1454 Some("30s")
1455 );
1456 assert_eq!(
1457 limits.get(M2_LIMITS_KEY_CPU).and_then(|m| m.as_str()),
1458 Some("500m")
1459 );
1460 }
1461
1462 #[test]
1463 fn behavior_slot_propagates_into_values_block() {
1464 use caixa_core::BehaviorSpec;
1465 let mut c = sample_caixa();
1466 c.behavior = Some(BehaviorSpec {
1467 on_init: Some(PathBuf::from("lib/init.lisp")),
1468 on_call: Some(PathBuf::from("lib/handlers.lisp")),
1469 ..Default::default()
1470 });
1471 let dir = render_chart_for_servico(&c, &sample_cu_yaml()).unwrap();
1472 let values = dir
1473 .files
1474 .iter()
1475 .find(|f| f.path == PathBuf::from(HELM_VALUES_YAML_FILENAME))
1476 .unwrap();
1477 let parsed: serde_yaml::Value = serde_yaml::from_str(&values.contents).unwrap();
1478 let cu_block = parsed.get(DEFAULT_LIBRARY_NAME).unwrap();
1479 let behavior = cu_block
1480 .get(M2_KEY_BEHAVIOR)
1481 .expect("behavior must propagate");
1482 assert_eq!(
1483 behavior
1484 .get(M2_BEHAVIOR_KEY_ON_INIT)
1485 .and_then(|v| v.as_str()),
1486 Some("lib/init.lisp")
1487 );
1488 assert_eq!(
1489 behavior
1490 .get(M2_BEHAVIOR_KEY_ON_CALL)
1491 .and_then(|v| v.as_str()),
1492 Some("lib/handlers.lisp")
1493 );
1494 }
1495
1496 #[test]
1497 fn upgrade_from_slot_propagates_into_values_block() {
1498 use caixa_core::{UpgradeFromEntry, UpgradeInstruction};
1499 let mut c = sample_caixa();
1500 c.upgrade_from = vec![UpgradeFromEntry {
1501 from: "0.0.9".into(),
1502 instructions: vec![UpgradeInstruction::LoadModule {
1503 module: "hello-rio".into(),
1504 }],
1505 }];
1506 let dir = render_chart_for_servico(&c, &sample_cu_yaml()).unwrap();
1507 let values = dir
1508 .files
1509 .iter()
1510 .find(|f| f.path == PathBuf::from(HELM_VALUES_YAML_FILENAME))
1511 .unwrap();
1512 let parsed: serde_yaml::Value = serde_yaml::from_str(&values.contents).unwrap();
1513 let cu_block = parsed.get(DEFAULT_LIBRARY_NAME).unwrap();
1514 assert!(cu_block.get(M2_KEY_UPGRADE_FROM).is_some());
1515 }
1516
1517 #[test]
1518 fn empty_m2_slots_do_not_appear() {
1519 // Existing caixa with no M2 slots → values.yaml carries no
1520 // limits/behavior/upgradeFrom keys (forward-compat invariant).
1521 let dir = render_chart_for_servico(&sample_caixa(), &sample_cu_yaml()).unwrap();
1522 let values = dir
1523 .files
1524 .iter()
1525 .find(|f| f.path == PathBuf::from(HELM_VALUES_YAML_FILENAME))
1526 .unwrap();
1527 let parsed: serde_yaml::Value = serde_yaml::from_str(&values.contents).unwrap();
1528 let cu_block = parsed.get(DEFAULT_LIBRARY_NAME).unwrap();
1529 assert!(cu_block.get(M2_KEY_LIMITS).is_none());
1530 assert!(cu_block.get(M2_KEY_BEHAVIOR).is_none());
1531 assert!(cu_block.get(M2_KEY_UPGRADE_FROM).is_none());
1532 }
1533
1534 #[test]
1535 fn write_to_creates_files() {
1536 let dir = render_chart_for_servico(&sample_caixa(), &sample_cu_yaml()).unwrap();
1537 let tmp = tempfile::tempdir().unwrap();
1538 dir.write_to(tmp.path()).unwrap();
1539 let chart_root = tmp.path().join("lareira-hello-rio");
1540 assert!(chart_root.join(HELM_CHART_YAML_FILENAME).exists());
1541 assert!(chart_root.join(HELM_VALUES_YAML_FILENAME).exists());
1542 assert!(chart_root.join(HELM_CHART_README_FILENAME).exists());
1543 }
1544
1545 #[test]
1546 fn default_library_name_re_export_points_at_caixa_core_canonical() {
1547 // The renderer's `pub const DEFAULT_LIBRARY_NAME` was lifted to a
1548 // re-export of [`caixa_core::DEFAULT_LIBRARY_NAME`] so the Helm
1549 // library-chart name lives in exactly one place across every
1550 // caixa renderer (caixa-helm's `RenderOpts::library_name`
1551 // default here + caixa-flux's `cluster_bundle` `helmrelease.yaml`
1552 // wrap key on the sibling deploy-path crate). Pin the equality
1553 // here so any local re-introduction of a sibling `pub const
1554 // DEFAULT_LIBRARY_NAME: &str = "…"` (the canonical drift footgun
1555 // the prior `DEFAULT_NAMESPACE` / `DEFAULT_SERVICO_PORT` lift
1556 // commits' bodies acknowledged as the recurring shape) is a
1557 // build-time test failure naming the offending drift, not a
1558 // silent apply-time wrap-key mismatch routing the per-cluster
1559 // `enabled: true` override nowhere on `helm template` /
1560 // `helm install`. Peer to
1561 // `caixa_flux::tests::default_library_name_re_export_points_at_caixa_core_canonical`
1562 // on the sibling renderer crate.
1563 caixa_core::assert_str_reexport_identity(
1564 "DEFAULT_LIBRARY_NAME",
1565 DEFAULT_LIBRARY_NAME,
1566 caixa_core::DEFAULT_LIBRARY_NAME,
1567 );
1568 }
1569
1570 #[test]
1571 fn kube_key_spec_re_export_points_at_caixa_core_canonical() {
1572 // The renderer's `KUBE_KEY_SPEC` was lifted from the production-
1573 // code inline `"spec"` literal at `build_values_yaml`'s
1574 // `computeunit_yaml.get("spec")` ComputeUnit-side spec read (+
1575 // its matching `Error::MissingField("spec")` diagnostic) to a
1576 // re-export of [`caixa_core::KUBE_KEY_SPEC`] so the canonical
1577 // K8s-CR top-level spec-axis string lives in exactly one place
1578 // across every caixa renderer. Pin the equality + static-data
1579 // identity here so any local re-introduction of a sibling
1580 // `pub const KUBE_KEY_SPEC: &str = "…"` (the canonical drift
1581 // footgun where a sibling local `pub const` could happen to
1582 // carry the same string at the source while pointing at a
1583 // different `&'static` allocation) is a build-time test
1584 // failure naming the offending drift. Peer to
1585 // [`default_library_name_re_export_points_at_caixa_core_canonical`]
1586 // on the sibling re-export axis +
1587 // `caixa_flux::tests::kube_key_spec_re_export_points_at_caixa_core_canonical`
1588 // / `caixa_mesh::tests::kube_key_spec_re_export_points_at_caixa_core_canonical`
1589 // on the sibling renderer crates.
1590 caixa_core::assert_str_reexport_identity(
1591 "KUBE_KEY_SPEC",
1592 KUBE_KEY_SPEC,
1593 caixa_core::KUBE_KEY_SPEC,
1594 );
1595 }
1596
1597 #[test]
1598 fn helm_chart_api_version_re_export_points_at_caixa_core_canonical() {
1599 // The renderer's `HELM_CHART_API_VERSION` was lifted from the
1600 // production-code inline `"v2".into()` literal at
1601 // [`build_chart_yaml`]'s `api_version` field assignment (formerly
1602 // `caixa-helm/src/lib.rs:298`) to a re-export of
1603 // [`caixa_core::HELM_CHART_API_VERSION`] so the Helm 3
1604 // chart-schema apiVersion the rendered Chart.yaml declares lives
1605 // in exactly one place across every caixa renderer. Pin the
1606 // equality + `&'static` static-data identity here so any local
1607 // re-introduction of a sibling `pub const HELM_CHART_API_VERSION:
1608 // &str = "…"` at this crate — the canonical drift footgun where
1609 // a sibling local `pub const` could happen to carry the same
1610 // string at the source while pointing at a different `&'static`
1611 // allocation — is a build-time test failure naming the offending
1612 // drift, not a silent chart-schema-parser reroute at
1613 // `helm template` time far from the drift site. Peer to
1614 // [`kube_key_spec_re_export_points_at_caixa_core_canonical`] /
1615 // [`default_library_name_re_export_points_at_caixa_core_canonical`]
1616 // on the sibling re-export axes.
1617 caixa_core::assert_str_reexport_identity(
1618 "HELM_CHART_API_VERSION",
1619 HELM_CHART_API_VERSION,
1620 caixa_core::HELM_CHART_API_VERSION,
1621 );
1622 }
1623
1624 #[test]
1625 fn chart_yaml_uses_lifted_helm_chart_api_version() {
1626 // Fail-before-pass-after pin on the production-code
1627 // substitution: [`build_chart_yaml`]'s `api_version` field
1628 // consults the lifted [`HELM_CHART_API_VERSION`] re-export at
1629 // its assignment site, so the rendered Chart.yaml's top-level
1630 // `apiVersion` axis is byte-identical to the canonical constant
1631 // by construction. Before the lift the field carried an inline
1632 // `"v2".into()` literal at [`build_chart_yaml`]; a future
1633 // refactor that accidentally reverted the substitution — or
1634 // any parallel per-renderer variant that inlined a stale
1635 // Helm 2 `"v1"` literal — would silently reroute the rendered
1636 // Chart.yaml through the wrong chart-schema parser at
1637 // `helm dependency build` / `helm template` time, so this pin
1638 // trips at caixa-helm build time. Peer to
1639 // `values_yaml_wrap_key_matches_chart_dependency_name` on the
1640 // sibling structural-cross-axis-invariant surface.
1641 let dir = render_chart_for_servico(&sample_caixa(), &sample_cu_yaml()).unwrap();
1642 let chart_file = dir
1643 .files
1644 .iter()
1645 .find(|f| f.path == PathBuf::from(HELM_CHART_YAML_FILENAME))
1646 .unwrap();
1647 let chart: ChartYaml = serde_yaml::from_str(&chart_file.contents).unwrap();
1648 assert_eq!(
1649 chart.api_version, HELM_CHART_API_VERSION,
1650 "rendered Chart.yaml `apiVersion` must equal the lifted \
1651 HELM_CHART_API_VERSION verbatim — a drifted value silently \
1652 reroutes the rendered chart through the wrong Helm chart-schema \
1653 parser at `helm template` time"
1654 );
1655 }
1656
1657 #[test]
1658 fn helm_chart_type_application_re_export_points_at_caixa_core_canonical() {
1659 // The renderer's `HELM_CHART_TYPE_APPLICATION` was lifted from
1660 // the production-code inline `"application".into()` literal at
1661 // [`build_chart_yaml`]'s `chart_type` field assignment (formerly
1662 // `caixa-helm/src/lib.rs:354`) to a re-export of
1663 // [`caixa_core::HELM_CHART_TYPE_APPLICATION`] so the Helm 3
1664 // chart-schema per-chart-kind discriminator scalar-value the
1665 // rendered `lareira-<nome>` chart declares lives in exactly one
1666 // place across every caixa renderer. Pin the equality +
1667 // `&'static` static-data identity here so any local
1668 // re-introduction of a sibling `pub const
1669 // HELM_CHART_TYPE_APPLICATION: &str = "…"` at this crate — the
1670 // canonical drift footgun where a sibling local `pub const`
1671 // could happen to carry the same string at the source while
1672 // pointing at a different `&'static` allocation — is a
1673 // build-time test failure naming the offending drift, not a
1674 // silent per-release install-shape dispatch reroute at
1675 // `helm install` time far from the drift site. Peer to
1676 // [`helm_chart_api_version_re_export_points_at_caixa_core_canonical`]
1677 // on the sibling canonical-Helm-chart-schema-axis re-export
1678 // surface — completes the per-Chart.yaml `(apiVersion, type)`
1679 // canonical-scalar-axis re-export pair every rendered
1680 // `lareira-<nome>` chart declares at its top-level Chart.yaml
1681 // body.
1682 caixa_core::assert_str_reexport_identity(
1683 "HELM_CHART_TYPE_APPLICATION",
1684 HELM_CHART_TYPE_APPLICATION,
1685 caixa_core::HELM_CHART_TYPE_APPLICATION,
1686 );
1687 }
1688
1689 #[test]
1690 fn helm_chart_type_library_re_export_points_at_caixa_core_canonical() {
1691 // Re-export identity pin on the peer closed-set arm the
1692 // renderer's `HELM_CHART_TYPE_LIBRARY` alias resolves to. Peer
1693 // of `helm_chart_type_application_re_export_points_at_caixa_core_canonical`
1694 // on the sibling closed-set arm — the two pins together enshrine
1695 // the two-arm `{"application", "library"}` closed set at the
1696 // caixa-helm re-export surface as byte-identical `&'static`
1697 // static-data views onto the canonical caixa-core lifts, so any
1698 // local re-introduction of a sibling `pub const
1699 // HELM_CHART_TYPE_LIBRARY: &str = "…"` at this crate (the same
1700 // drift footgun the peer pin closes on the sibling arm) is a
1701 // build-time test failure naming the offending drift. The pin
1702 // also structurally forbids the two arms from converging on the
1703 // same `&'static` allocation — a future rebrand that
1704 // accidentally aliased `HELM_CHART_TYPE_LIBRARY` at the
1705 // [`caixa_core::HELM_CHART_TYPE_APPLICATION`] canonical would
1706 // pass this identity check but trip the caixa-core-side
1707 // `helm_chart_type_application_and_library_are_distinct` pin
1708 // paired to the two arms' distinctness contract.
1709 caixa_core::assert_str_reexport_identity(
1710 "HELM_CHART_TYPE_LIBRARY",
1711 HELM_CHART_TYPE_LIBRARY,
1712 caixa_core::HELM_CHART_TYPE_LIBRARY,
1713 );
1714 }
1715
1716 #[test]
1717 fn chart_yaml_uses_lifted_helm_chart_type_application() {
1718 // Fail-before-pass-after pin on the production-code substitution:
1719 // [`build_chart_yaml`]'s `chart_type` field consults the lifted
1720 // [`HELM_CHART_TYPE_APPLICATION`] re-export at its assignment
1721 // site, so the rendered Chart.yaml's top-level `type` axis is
1722 // byte-identical to the canonical constant by construction.
1723 // Before the lift the field carried an inline `"application".into()`
1724 // literal at [`build_chart_yaml`]; a future refactor that
1725 // accidentally reverted the substitution — or any parallel
1726 // per-renderer variant that inlined a `"library"` literal (the
1727 // sibling closed-set value from the Helm chart-schema's
1728 // per-chart-kind enum) — would silently reroute the rendered
1729 // Chart.yaml through the wrong per-release install-shape
1730 // dispatch at `helm install` time (Helm refuses to install a
1731 // `library` chart directly), so this pin trips at caixa-helm
1732 // build time. Peer to `chart_yaml_uses_lifted_helm_chart_api_version`
1733 // on the sibling per-Chart.yaml top-level `(apiVersion, type)`
1734 // canonical-scalar-axis pin pair — extends the per-Chart.yaml
1735 // top-level canonical-scalar-axis production-emit-pin
1736 // discipline from the `apiVersion` half onto the sibling `type`
1737 // half.
1738 let dir = render_chart_for_servico(&sample_caixa(), &sample_cu_yaml()).unwrap();
1739 let chart_file = dir
1740 .files
1741 .iter()
1742 .find(|f| f.path == PathBuf::from(HELM_CHART_YAML_FILENAME))
1743 .unwrap();
1744 let chart: ChartYaml = serde_yaml::from_str(&chart_file.contents).unwrap();
1745 assert_eq!(
1746 chart.chart_type, HELM_CHART_TYPE_APPLICATION,
1747 "rendered Chart.yaml `type` must equal the lifted \
1748 HELM_CHART_TYPE_APPLICATION verbatim — a drifted value \
1749 silently reroutes the rendered chart through the wrong \
1750 per-release install-shape dispatch at `helm install` time \
1751 (Helm refuses to install a `library` chart directly, or \
1752 silently treats an unrecognized value as the default \
1753 `application` shape masking the schema violation)"
1754 );
1755 }
1756
1757 #[test]
1758 fn helm_chart_key_type_re_export_points_at_caixa_core_canonical() {
1759 // The renderer's `HELM_CHART_KEY_TYPE` was lifted from the
1760 // sole production-side inline `"type"` literal at [`ChartYaml`]'s
1761 // `chart_type` field `#[serde(rename = "type")]` attribute
1762 // (formerly `caixa-helm/src/lib.rs:149`) to a re-export of
1763 // [`caixa_core::HELM_CHART_KEY_TYPE`] so the Helm 3 top-level
1764 // per-chart-kind discriminator YAML axis-key lives in exactly
1765 // one place across every caixa renderer. Pin the equality +
1766 // `&'static` static-data identity here so any local
1767 // re-introduction of a sibling `pub const HELM_CHART_KEY_TYPE:
1768 // &str = "…"` at this crate — the canonical drift footgun
1769 // where a sibling local `pub const` could happen to carry the
1770 // same string at the source while pointing at a different
1771 // `&'static` allocation — is a build-time test failure naming
1772 // the offending drift, not a silent Helm-chart-schema-parser
1773 // per-chart-kind-defaulting reroute at `helm dependency build`
1774 // / `helm lint` / `helm template` / `helm install` time far
1775 // from the drift site. Peer to
1776 // [`helm_chart_type_application_re_export_points_at_caixa_core_canonical`]
1777 // / [`helm_chart_type_library_re_export_points_at_caixa_core_canonical`]
1778 // on the sibling per-chart-kind axis-value re-export surface —
1779 // completes the per-Chart.yaml per-chart-kind discriminator
1780 // axis's `(key, value-set)` canonical re-export trio at the
1781 // caixa-helm surface.
1782 caixa_core::assert_str_reexport_identity(
1783 "HELM_CHART_KEY_TYPE",
1784 HELM_CHART_KEY_TYPE,
1785 caixa_core::HELM_CHART_KEY_TYPE,
1786 );
1787 }
1788
1789 #[test]
1790 fn helm_chart_key_app_version_re_export_points_at_caixa_core_canonical() {
1791 // The renderer's `HELM_CHART_KEY_APP_VERSION` was lifted from
1792 // the sole production-side inline `"appVersion"` literal at
1793 // [`ChartYaml`]'s `app_version` field
1794 // `#[serde(rename = "appVersion")]` attribute (formerly
1795 // `caixa-helm/src/lib.rs:152`) to a re-export of
1796 // [`caixa_core::HELM_CHART_KEY_APP_VERSION`] so the Helm 3
1797 // top-level per-chart-app-version YAML axis-key lives in
1798 // exactly one place across every caixa renderer. Pin the
1799 // equality + `&'static` static-data identity here so any
1800 // local re-introduction of a sibling `pub const
1801 // HELM_CHART_KEY_APP_VERSION: &str = "…"` at this crate — the
1802 // canonical drift footgun where a sibling local `pub const`
1803 // could happen to carry the same string at the source while
1804 // pointing at a different `&'static` allocation — is a
1805 // build-time test failure naming the offending drift, not a
1806 // silent Helm-chart-schema-parser field-drop at every
1807 // downstream Artifact Hub / `helm search` chart-consumer far
1808 // from the drift site. Peer to
1809 // [`helm_chart_key_type_re_export_points_at_caixa_core_canonical`]
1810 // on the sibling per-Chart.yaml top-level YAML axis-key
1811 // re-export surface — completes the per-Chart.yaml top-level
1812 // YAML axis-key re-export pair at the caixa-helm surface for
1813 // the two serde-rename-literal-only axes.
1814 caixa_core::assert_str_reexport_identity(
1815 "HELM_CHART_KEY_APP_VERSION",
1816 HELM_CHART_KEY_APP_VERSION,
1817 caixa_core::HELM_CHART_KEY_APP_VERSION,
1818 );
1819 }
1820
1821 #[test]
1822 fn chart_yaml_serializes_type_axis_under_lifted_helm_chart_key_type() {
1823 // Fail-before-pass-after drift-detection pin on the
1824 // `#[serde(rename = "type")]` attribute at [`ChartYaml`]'s
1825 // `chart_type` field. Rust's attribute grammar admits only
1826 // string literals so the lifted [`HELM_CHART_KEY_TYPE`]
1827 // constant cannot substitute for the literal syntactically at
1828 // the attribute-argument site — a future refactor that
1829 // dropped the `#[serde(rename = "type")]` attribute (or
1830 // changed the target key to `"Type"` / `"kind"` /
1831 // `"chartType"`) would silently serialize the field under
1832 // Rust's default snake_case `chart_type:` key, which Helm's
1833 // chart-schema parser silently ignores as an unknown top-
1834 // level key, defaulting the per-chart-kind axis to
1835 // `application` with no process-log signal. This pin closes
1836 // the drift by round-tripping a rendered `Chart.yaml` through
1837 // `serde_yaml::from_str::<serde_yaml::Value>` and asserting
1838 // the top-level `Mapping::get(HELM_CHART_KEY_TYPE)` resolves
1839 // (rather than serializing through the [`ChartYaml`]-typed
1840 // deserializer that would silently absorb the rename drift
1841 // via `#[serde(default)]` fall-through at the struct-side).
1842 // Peer to
1843 // [`chart_yaml_uses_lifted_helm_chart_type_application`] on
1844 // the sibling per-Chart.yaml per-chart-kind axis-value
1845 // production-emit pin — the two pins together enforce the
1846 // full `(key, value)` production-emit pair at the caixa-helm
1847 // surface for the per-Chart.yaml per-chart-kind discriminator
1848 // axis.
1849 let dir = render_chart_for_servico(&sample_caixa(), &sample_cu_yaml()).unwrap();
1850 let chart_file = dir
1851 .files
1852 .iter()
1853 .find(|f| f.path == PathBuf::from(HELM_CHART_YAML_FILENAME))
1854 .unwrap();
1855 let doc: serde_yaml::Value = serde_yaml::from_str(&chart_file.contents).unwrap();
1856 let mapping = doc.as_mapping().expect(
1857 "rendered Chart.yaml must be a top-level YAML mapping per \
1858 the Helm 3 chart-schema shape",
1859 );
1860 assert!(
1861 mapping.contains_key(serde_yaml::Value::String(HELM_CHART_KEY_TYPE.to_string())),
1862 "rendered Chart.yaml must carry a top-level {HELM_CHART_KEY_TYPE:?} \
1863 axis-key — a drift on the `#[serde(rename = {HELM_CHART_KEY_TYPE:?})]` \
1864 attribute at ChartYaml's `chart_type` field silently reroutes the \
1865 per-chart-kind discriminator axis through Rust's default snake_case \
1866 serialization (`chart_type:`), which Helm's chart-schema parser \
1867 silently ignores as an unknown top-level key (defaulting the \
1868 per-chart-kind axis to `application` with no process-log signal)"
1869 );
1870 }
1871
1872 #[test]
1873 fn chart_yaml_serializes_app_version_axis_under_lifted_helm_chart_key_app_version() {
1874 // Fail-before-pass-after drift-detection pin on the
1875 // `#[serde(rename = "appVersion")]` attribute at [`ChartYaml`]'s
1876 // `app_version` field. Same attribute-literal-only-grammar
1877 // constraint the peer
1878 // [`chart_yaml_serializes_type_axis_under_lifted_helm_chart_key_type`]
1879 // pin closes on the sibling per-Chart.yaml top-level YAML
1880 // axis-key applies here: a future refactor that dropped the
1881 // `#[serde(rename = "appVersion")]` attribute (or changed the
1882 // target key to `"AppVersion"` / `"applicationVersion"` /
1883 // `"appversion"`) would silently serialize the field under
1884 // Rust's default snake_case `app_version:` key, which Helm's
1885 // chart-schema parser silently drops from the parsed
1886 // chart-metadata shape, and every downstream Artifact Hub /
1887 // `helm search` per-chart index falls back to "no application
1888 // version" for the rendered chart. This pin closes the drift
1889 // by round-tripping a rendered `Chart.yaml` through
1890 // `serde_yaml::from_str::<serde_yaml::Value>` (rather than
1891 // through the [`ChartYaml`]-typed deserializer that would
1892 // silently absorb the rename drift). Peer to
1893 // [`chart_yaml_serializes_type_axis_under_lifted_helm_chart_key_type`]
1894 // on the sibling per-Chart.yaml top-level YAML axis-key
1895 // serialization pin surface — completes the per-Chart.yaml
1896 // top-level YAML axis-key production-emit pin pair at the
1897 // caixa-helm surface for the two serde-rename-literal-only
1898 // axes.
1899 let dir = render_chart_for_servico(&sample_caixa(), &sample_cu_yaml()).unwrap();
1900 let chart_file = dir
1901 .files
1902 .iter()
1903 .find(|f| f.path == PathBuf::from(HELM_CHART_YAML_FILENAME))
1904 .unwrap();
1905 let doc: serde_yaml::Value = serde_yaml::from_str(&chart_file.contents).unwrap();
1906 let mapping = doc.as_mapping().expect(
1907 "rendered Chart.yaml must be a top-level YAML mapping per \
1908 the Helm 3 chart-schema shape",
1909 );
1910 assert!(
1911 mapping.contains_key(serde_yaml::Value::String(
1912 HELM_CHART_KEY_APP_VERSION.to_string()
1913 )),
1914 "rendered Chart.yaml must carry a top-level \
1915 {HELM_CHART_KEY_APP_VERSION:?} axis-key — a drift on the \
1916 `#[serde(rename = {HELM_CHART_KEY_APP_VERSION:?})]` attribute at \
1917 ChartYaml's `app_version` field silently reroutes the per-chart \
1918 underlying-application-version axis through Rust's default \
1919 snake_case serialization (`app_version:`), which Helm's \
1920 chart-schema parser silently drops from the parsed chart-metadata \
1921 shape (every downstream Artifact Hub / `helm search` per-chart \
1922 index falls back to \"no application version\" for the rendered \
1923 chart with no process-log signal at the substrate-side emitter site)"
1924 );
1925 }
1926
1927 #[test]
1928 fn chart_yaml_serializes_api_version_axis_under_lifted_helm_chart_key_api_version() {
1929 // Fail-before-pass-after drift-detection pin on the
1930 // `#[serde(rename = "apiVersion")]` attribute at [`ChartYaml`]'s
1931 // `api_version` field. Same attribute-literal-only-grammar
1932 // constraint the peer
1933 // [`chart_yaml_serializes_type_axis_under_lifted_helm_chart_key_type`]
1934 // / [`chart_yaml_serializes_app_version_axis_under_lifted_helm_chart_key_app_version`]
1935 // pins close on the sibling per-Chart.yaml top-level YAML
1936 // axis-keys applies here: a future refactor that dropped the
1937 // `#[serde(rename = "apiVersion")]` attribute (or changed the
1938 // target key to `"ApiVersion"` / `"apiversion"` /
1939 // `"schemaVersion"`) would silently serialize the field under
1940 // Rust's default snake_case `api_version:` key, which Helm's
1941 // chart-schema parser rejects at `helm lint` / `helm
1942 // dependency build` / `helm template` time with an "apiVersion
1943 // is required" error far from the drift site — every downstream
1944 // `lareira-<nome>` chart consumer drops with no field naming
1945 // the serde-rename-drift root cause. This pin closes the drift
1946 // by round-tripping a rendered `Chart.yaml` through
1947 // `serde_yaml::from_str::<serde_yaml::Value>` (rather than
1948 // through the [`ChartYaml`]-typed deserializer that would
1949 // silently absorb the rename drift via `#[serde(default)]`
1950 // fall-through at the struct-side). Peer to
1951 // [`chart_yaml_serializes_type_axis_under_lifted_helm_chart_key_type`]
1952 // / [`chart_yaml_serializes_app_version_axis_under_lifted_helm_chart_key_app_version`]
1953 // on the sibling per-Chart.yaml top-level YAML axis-key
1954 // serialization pin surface — completes the per-Chart.yaml
1955 // top-level YAML axis-key production-emit pin trio at the
1956 // caixa-helm surface for the three serde-rename-literal-only
1957 // axes.
1958 let dir = render_chart_for_servico(&sample_caixa(), &sample_cu_yaml()).unwrap();
1959 let chart_file = dir
1960 .files
1961 .iter()
1962 .find(|f| f.path == PathBuf::from(HELM_CHART_YAML_FILENAME))
1963 .unwrap();
1964 let doc: serde_yaml::Value = serde_yaml::from_str(&chart_file.contents).unwrap();
1965 let mapping = doc.as_mapping().expect(
1966 "rendered Chart.yaml must be a top-level YAML mapping per \
1967 the Helm 3 chart-schema shape",
1968 );
1969 assert!(
1970 mapping.contains_key(serde_yaml::Value::String(
1971 HELM_CHART_KEY_API_VERSION.to_string()
1972 )),
1973 "rendered Chart.yaml must carry a top-level \
1974 {HELM_CHART_KEY_API_VERSION:?} axis-key — a drift on the \
1975 `#[serde(rename = {HELM_CHART_KEY_API_VERSION:?})]` attribute at \
1976 ChartYaml's `api_version` field silently reroutes the per-chart \
1977 chart-schema-apiVersion axis through Rust's default snake_case \
1978 serialization (`api_version:`), which Helm's chart-schema parser \
1979 rejects at `helm lint` / `helm template` time with an \"apiVersion \
1980 is required\" error far from the drift site"
1981 );
1982 }
1983
1984 #[test]
1985 fn chart_dependency_serializes_tetrad_under_lifted_helm_chart_dependency_keys() {
1986 // Fail-before-pass-after drift-detection pin on the per-
1987 // `dependencies[]`-entry sub-mapping serde field-name tetrad
1988 // at [`ChartDependency`]. The four fields are identity-mapped
1989 // to their target wire keys today (no `#[serde(rename)]` or
1990 // `#[serde(rename_all)]` attribute on the struct), so a drift
1991 // would surface as one of two shapes: a rename of the Rust
1992 // field (`pub name` → `pub nome`) that silently rebrands the
1993 // wire key, or a `#[serde(rename_all = "camelCase")]` attribute
1994 // addition that stays a no-op on the four lowercase-identity
1995 // fields today but silently activates on a future field
1996 // addition (e.g. an `import_values: Option<Vec<String>>` axis
1997 // matching Helm 3's per-dep `import-values` sub-key). Either
1998 // shape silently reroutes the per-dep sub-mapping through a
1999 // Helm-per-dep-resolver drop at `helm dependency build` time
2000 // far from the drift site (Helm silently drops the drifted
2001 // per-dep sub-mapping field and the per-dep resolver falls
2002 // back to the parsed-shape defaults). This pin closes the
2003 // drift by serializing a fully-populated [`ChartDependency`]
2004 // (`alias` set to a non-`None` value so the
2005 // `#[serde(skip_serializing_if = "Option::is_none")]`
2006 // attribute doesn't elide the axis from the emitted YAML)
2007 // through `serde_yaml::to_value` and asserting each of the
2008 // four per-dep sub-mapping wire keys resolves via
2009 // `Mapping::get(HELM_CHART_DEPENDENCY_KEY_*)`. Peer to
2010 // [`chart_yaml_serializes_type_axis_under_lifted_helm_chart_key_type`]
2011 // / [`chart_yaml_serializes_app_version_axis_under_lifted_helm_chart_key_app_version`]
2012 // / [`chart_yaml_serializes_api_version_axis_under_lifted_helm_chart_key_api_version`]
2013 // on the sibling per-Chart.yaml top-level serde-rename-literal-
2014 // only axis-key drift-detection pin trio (cc44e4b / d29bc23) —
2015 // extends the drift-detection discipline from the per-Chart.yaml
2016 // top-level serde-rename-literal axes onto the per-
2017 // `dependencies[]`-entry sub-mapping serde-field-name tetrad.
2018 let dep = ChartDependency {
2019 name: "pleme-computeunit".into(),
2020 version: "~0.1.0".into(),
2021 repository: "file://../pleme-computeunit".into(),
2022 alias: Some("acme-alias".into()),
2023 };
2024 let doc = serde_yaml::to_value(&dep).unwrap();
2025 let mapping = doc.as_mapping().expect(
2026 "ChartDependency must serialize to a top-level YAML mapping per \
2027 the Helm 3 per-dep sub-mapping shape",
2028 );
2029 for key in [
2030 HELM_CHART_DEPENDENCY_KEY_NAME,
2031 HELM_CHART_DEPENDENCY_KEY_VERSION,
2032 HELM_CHART_DEPENDENCY_KEY_REPOSITORY,
2033 HELM_CHART_DEPENDENCY_KEY_ALIAS,
2034 ] {
2035 assert!(
2036 mapping.contains_key(serde_yaml::Value::String(key.to_string())),
2037 "serialized ChartDependency must carry a top-level {key:?} \
2038 axis-key — a drift on the `ChartDependency` struct's serde \
2039 field-name (a Rust-side rename, an added \
2040 `#[serde(rename_all)]` attribute, an added `#[serde(rename)]` \
2041 per-field override) silently reroutes the per-dep sub-mapping \
2042 through a Helm-per-dep-resolver drop at `helm dependency \
2043 build` time far from the drift site (Helm silently drops the \
2044 drifted per-dep sub-mapping field and the per-dep resolver \
2045 falls back to the parsed-shape defaults); the emitted mapping \
2046 keys are {keys:?}",
2047 keys = mapping
2048 .keys()
2049 .filter_map(|k| k.as_str().map(str::to_string))
2050 .collect::<Vec<_>>()
2051 );
2052 }
2053 }
2054
2055 #[test]
2056 fn chart_yaml_serializes_dependencies_axis_under_lifted_helm_chart_key_dependencies() {
2057 // Fail-before-pass-after drift-detection pin on the top-level
2058 // per-chart dependency-list YAML axis-key at [`ChartYaml`]'s
2059 // `dependencies` field. The Rust field name and the emitted
2060 // wire key coincide by default today (no `#[serde(rename)]`
2061 // attribute on the field, no `#[serde(rename_all = "…")]`
2062 // attribute on the struct — so serde emits `dependencies:`
2063 // verbatim as the top-level list-container YAML key). A future
2064 // hostile refactor could silently rebrand the wire key in
2065 // three shapes:
2066 //
2067 // - a rename of the Rust field itself (`pub dependencies:
2068 // Vec<ChartDependency>` → `pub deps: Vec<ChartDependency>`
2069 // / `pub chart_dependencies: …`), which serde would then
2070 // serialize as `deps:` / `chart_dependencies:` verbatim;
2071 // - an added `#[serde(rename_all = "camelCase")]` /
2072 // `"snake_case"` / `"kebab-case"` attribute on the struct
2073 // itself — a no-op on the four identity-mapped top-level
2074 // lowercase keys (`name` / `description` / `version` /
2075 // `dependencies`) today but silently activates on a future
2076 // multi-word field addition (e.g. an `icon_url` axis
2077 // matching Helm 3's per-chart `icon:` future-schema slot);
2078 // - an added `#[serde(rename = "deps")]` per-field override
2079 // at the site of the `dependencies` field.
2080 //
2081 // Under any of the three shapes Helm's chart-schema parser
2082 // silently drops the entire per-chart dep list from the
2083 // parsed chart-metadata (unknown top-level YAML keys silently
2084 // ignored per the Helm 3 chart-schema fallthrough), `helm
2085 // dependency build` finds no chart to vendor, and every
2086 // rendered `lareira-<nome>` chart's install fails with
2087 // `template: no template ... associated with template ...`
2088 // far from the drift site with no field naming the top-level-
2089 // list-key-drift root cause. This pin closes the drift by
2090 // round-tripping a rendered `Chart.yaml` through
2091 // `serde_yaml::from_str::<serde_yaml::Value>` and asserting
2092 // the top-level `Mapping::get(HELM_CHART_KEY_DEPENDENCIES)`
2093 // resolves — rather than through the [`ChartYaml`]-typed
2094 // deserializer that would silently absorb any of the three
2095 // drift shapes via `#[serde(default)]` fall-through at the
2096 // struct-side. Peer to
2097 // [`chart_yaml_serializes_type_axis_under_lifted_helm_chart_key_type`]
2098 // / [`chart_yaml_serializes_app_version_axis_under_lifted_helm_chart_key_app_version`]
2099 // / [`chart_yaml_serializes_api_version_axis_under_lifted_helm_chart_key_api_version`]
2100 // on the sibling per-Chart.yaml top-level YAML axis-key
2101 // serialization pin surface (d29bc23, cc44e4b) — extends the
2102 // per-Chart.yaml top-level YAML axis-key production-emit pin
2103 // trio those closed onto the fourth top-level axis-key, the
2104 // parent list-container the already-lifted per-
2105 // `dependencies[]`-entry sub-mapping tetrad
2106 // [`HELM_CHART_DEPENDENCY_KEY_NAME`] /
2107 // [`HELM_CHART_DEPENDENCY_KEY_VERSION`] /
2108 // [`HELM_CHART_DEPENDENCY_KEY_REPOSITORY`] /
2109 // [`HELM_CHART_DEPENDENCY_KEY_ALIAS`] mounts one level down.
2110 let dir = render_chart_for_servico(&sample_caixa(), &sample_cu_yaml()).unwrap();
2111 let chart_file = dir
2112 .files
2113 .iter()
2114 .find(|f| f.path == PathBuf::from(HELM_CHART_YAML_FILENAME))
2115 .unwrap();
2116 let doc: serde_yaml::Value = serde_yaml::from_str(&chart_file.contents).unwrap();
2117 let mapping = doc.as_mapping().expect(
2118 "rendered Chart.yaml must be a top-level YAML mapping per \
2119 the Helm 3 chart-schema shape",
2120 );
2121 assert!(
2122 mapping.contains_key(serde_yaml::Value::String(
2123 HELM_CHART_KEY_DEPENDENCIES.to_string()
2124 )),
2125 "rendered Chart.yaml must carry a top-level \
2126 {HELM_CHART_KEY_DEPENDENCIES:?} axis-key — a drift on the \
2127 `ChartYaml.dependencies` field's serde field-name (a Rust-side \
2128 rename to `deps` / `chart_dependencies`, an added \
2129 `#[serde(rename_all)]` attribute on the enclosing struct, an \
2130 added `#[serde(rename)]` per-field override) silently reroutes \
2131 the per-chart dependency-list axis through an unrecognized \
2132 top-level YAML key (`deps:` / `chartDependencies:` / \
2133 `chart_dependencies:`), which Helm's chart-schema parser \
2134 silently drops from the parsed chart-metadata shape (every \
2135 rendered `lareira-<nome>` chart's install fails with \
2136 `template: no template ... associated with template ...` at \
2137 `helm dependency build` / `helm template` time far from the \
2138 drift site with no field naming the top-level-list-key-drift \
2139 root cause); the emitted top-level mapping keys are {keys:?}",
2140 keys = mapping
2141 .keys()
2142 .filter_map(|k| k.as_str().map(str::to_string))
2143 .collect::<Vec<_>>()
2144 );
2145 }
2146
2147 #[test]
2148 fn helm_chart_key_dependencies_re_export_points_at_caixa_core_canonical() {
2149 // The renderer's [`HELM_CHART_KEY_DEPENDENCIES`] was lifted onto
2150 // a re-export of [`caixa_core::HELM_CHART_KEY_DEPENDENCIES`] so
2151 // the Helm 3 per-Chart.yaml top-level dependency-list-container
2152 // YAML axis-key — the parent whose per-`dependencies[]`-entry
2153 // sub-mapping tetrad [`HELM_CHART_DEPENDENCY_KEY_NAME`] /
2154 // [`HELM_CHART_DEPENDENCY_KEY_VERSION`] /
2155 // [`HELM_CHART_DEPENDENCY_KEY_REPOSITORY`] /
2156 // [`HELM_CHART_DEPENDENCY_KEY_ALIAS`] mounts one level down —
2157 // lives in exactly one place across every caixa renderer. Pin
2158 // the equality + `&'static` static-data identity here so any
2159 // local re-introduction of a sibling `pub const
2160 // HELM_CHART_KEY_DEPENDENCIES: &str = "…"` at this crate — the
2161 // canonical drift footgun where a sibling local `pub const`
2162 // could happen to carry the same string at the source while
2163 // pointing at a different `&'static` allocation — is a build-
2164 // time test failure naming the offending drift, not a silent
2165 // per-Chart.yaml top-level-list-key reroute at `helm dependency
2166 // build` / `helm lint` / `helm template` time far from the
2167 // drift site. Peer to
2168 // [`helm_chart_yaml_filename_re_export_points_at_caixa_core_canonical`]
2169 // and every other `helm_chart_*_re_export_points_at_caixa_core_canonical`
2170 // pin on the sibling canonical-Helm-chart-schema-body-axis
2171 // re-export surface — extends the per-Chart.yaml top-level
2172 // YAML axis-key re-export identity discipline onto the fourth
2173 // top-level axis-key at the caixa-helm surface.
2174 caixa_core::assert_str_reexport_identity(
2175 "HELM_CHART_KEY_DEPENDENCIES",
2176 HELM_CHART_KEY_DEPENDENCIES,
2177 caixa_core::HELM_CHART_KEY_DEPENDENCIES,
2178 );
2179 }
2180
2181 #[test]
2182 fn render_opts_default_library_name_follows_lifted_constant() {
2183 // [`RenderOpts::default()`] sets `library_name` from
2184 // [`DEFAULT_LIBRARY_NAME`]; pin that the lift preserves the
2185 // default-knob value bit-for-bit. A future refactor that
2186 // detaches `RenderOpts::default()` from the lifted constant —
2187 // accidentally re-introducing an inline `"pleme-computeunit"`
2188 // literal in the impl — would silently break the shared-shape
2189 // contract with caixa-flux (which uses the same constant
2190 // directly for its `helmrelease.yaml` wrap key); this test
2191 // surfaces the regression at build time rather than at
2192 // apply time as a silent values-routing no-op.
2193 let opts = RenderOpts::default();
2194 assert_eq!(opts.library_name, caixa_core::DEFAULT_LIBRARY_NAME);
2195 assert_eq!(opts.library_name, "pleme-computeunit");
2196 }
2197
2198 #[test]
2199 fn helm_chart_yaml_filename_re_export_points_at_caixa_core_canonical() {
2200 // The renderer's `HELM_CHART_YAML_FILENAME` was lifted from the
2201 // seven production + test-side inline `"Chart.yaml"` /
2202 // `PathBuf::from("Chart.yaml")` / `chart_root.join("Chart.yaml")`
2203 // literals across [`render_chart_for_servico`]'s `ChartDir`
2204 // metadata-file `path` emit site + every test-side round-trip
2205 // navigator that reaches into the rendered `ChartDir` by the
2206 // metadata filename to a re-export of
2207 // [`caixa_core::HELM_CHART_YAML_FILENAME`] so the Helm 3
2208 // per-chart-directory metadata-file filename lives in exactly one
2209 // place across every caixa renderer. Pin the equality +
2210 // `&'static` static-data identity here so any local
2211 // re-introduction of a sibling `pub const
2212 // HELM_CHART_YAML_FILENAME: &str = "…"` at this crate — the
2213 // canonical drift footgun where a sibling local `pub const` could
2214 // happen to carry the same string at the source while pointing
2215 // at a different `&'static` allocation — is a build-time test
2216 // failure naming the offending drift, not a silent
2217 // Helm-chart-schema-parser "Chart.yaml file is missing" reroute
2218 // at `helm dependency build` / `helm lint` / `helm template` /
2219 // `helm install` time far from the drift site. Peer to
2220 // [`helm_chart_api_version_re_export_points_at_caixa_core_canonical`]
2221 // / [`helm_chart_type_application_re_export_points_at_caixa_core_canonical`]
2222 // on the sibling canonical-Helm-chart-schema-body-axis re-export
2223 // surfaces — completes the per-`lareira-<nome>`-chart-directory
2224 // `(filename, apiVersion, type)` canonical-scalar-axis re-export
2225 // triple every rendered chart declares at its top-level metadata
2226 // file.
2227 caixa_core::assert_str_reexport_identity(
2228 "HELM_CHART_YAML_FILENAME",
2229 HELM_CHART_YAML_FILENAME,
2230 caixa_core::HELM_CHART_YAML_FILENAME,
2231 );
2232 }
2233
2234 #[test]
2235 fn helm_values_yaml_filename_re_export_points_at_caixa_core_canonical() {
2236 // The renderer's `HELM_VALUES_YAML_FILENAME` was lifted from the
2237 // twelve production + test-side inline `"values.yaml"` /
2238 // `PathBuf::from("values.yaml")` / `chart_root.join("values.yaml")`
2239 // literals across [`render_chart_for_servico`]'s `ChartDir`
2240 // values-file `path` emit site + every test-side round-trip
2241 // navigator that reaches into the rendered `ChartDir` by the
2242 // values filename to a re-export of
2243 // [`caixa_core::HELM_VALUES_YAML_FILENAME`] so the Helm 3
2244 // per-chart-directory values-file filename lives in exactly one
2245 // place across every caixa renderer. Pin the equality +
2246 // `&'static` static-data identity here so any local
2247 // re-introduction of a sibling `pub const
2248 // HELM_VALUES_YAML_FILENAME: &str = "…"` at this crate — the
2249 // canonical drift footgun where a sibling local `pub const` could
2250 // happen to carry the same string at the source while pointing
2251 // at a different `&'static` allocation — is a build-time test
2252 // failure naming the offending drift, not a silent
2253 // Helm-per-chart-values-loader fall-through to the empty values
2254 // block at `helm template` / `helm install` time far from the
2255 // drift site (where the workload silently comes up under the
2256 // library chart's admission-time defaults with no per-Servico
2257 // M2 overlay applied). Peer to
2258 // [`helm_chart_yaml_filename_re_export_points_at_caixa_core_canonical`]
2259 // on the sibling canonical-Helm-per-chart-directory-metadata-file-
2260 // axis re-export surface — completes the
2261 // per-`lareira-<nome>`-chart-directory `(Chart.yaml, values.yaml)`
2262 // canonical-per-chart-directory-filename-axis re-export pair
2263 // every rendered chart declares as its two schema-load-bearing
2264 // `ChartDir::files` entries.
2265 caixa_core::assert_str_reexport_identity(
2266 "HELM_VALUES_YAML_FILENAME",
2267 HELM_VALUES_YAML_FILENAME,
2268 caixa_core::HELM_VALUES_YAML_FILENAME,
2269 );
2270 }
2271
2272 #[test]
2273 fn helm_chart_readme_filename_re_export_points_at_caixa_core_canonical() {
2274 // The renderer's `HELM_CHART_README_FILENAME` was lifted from the
2275 // three production + test-side inline `"README.md"` literals
2276 // across [`render_chart_for_servico`]'s `ChartDir` readme-file
2277 // `path` emit site + every test-side round-trip navigator that
2278 // reaches into the rendered `ChartDir` by the readme filename
2279 // (the [`renders_three_files`] files-vec-membership pin + the
2280 // [`ChartDir::write_to`] post-write existence pin) to a
2281 // re-export of [`caixa_core::HELM_CHART_README_FILENAME`] so the
2282 // per-`lareira-<nome>` chart-directory human-facing readme
2283 // filename lives in exactly one place across every caixa
2284 // renderer. Pin the equality + `&'static` static-data identity
2285 // here so any local re-introduction of a sibling `pub const
2286 // HELM_CHART_README_FILENAME: &str = "…"` at this crate — the
2287 // canonical drift footgun where a sibling local `pub const`
2288 // could happen to carry the same string at the source while
2289 // pointing at a different `&'static` allocation — is a build-
2290 // time test failure naming the offending drift, not a silent
2291 // GitHub / Artifact Hub / any per-chart README-surfacing UI
2292 // fall-through to "no README available" at chart-consumption
2293 // time far from the drift site. Peer to
2294 // [`helm_chart_yaml_filename_re_export_points_at_caixa_core_canonical`]
2295 // / [`helm_values_yaml_filename_re_export_points_at_caixa_core_canonical`]
2296 // on the sibling canonical-Helm-per-chart-directory-filename
2297 // axis re-export surfaces — completes the
2298 // per-`lareira-<nome>`-chart-directory `(Chart.yaml,
2299 // values.yaml, README.md)` canonical-per-chart-directory-
2300 // filename-axis re-export triple every rendered chart declares
2301 // as its three `ChartDir::files` entries.
2302 caixa_core::assert_str_reexport_identity(
2303 "HELM_CHART_README_FILENAME",
2304 HELM_CHART_README_FILENAME,
2305 caixa_core::HELM_CHART_README_FILENAME,
2306 );
2307 }
2308
2309 #[test]
2310 fn helm_values_key_enabled_re_export_points_at_caixa_core_canonical() {
2311 // The renderer's `HELM_VALUES_KEY_ENABLED` was lifted from the
2312 // production-code inline `"enabled".to_string()` literal at
2313 // [`build_values_yaml`]'s
2314 // `block.insert("enabled".to_string(), Value::Bool(…))` values-
2315 // block-toggle insert (formerly `caixa-helm/src/lib.rs:389`) plus
2316 // its two test-side round-trip navigators
2317 // (`values_yaml_wraps_under_pleme_computeunit_key`,
2318 // `values_yaml_wrap_key_follows_library_name_override`) to a
2319 // re-export of [`caixa_core::HELM_VALUES_KEY_ENABLED`] so the
2320 // canonical `pleme-computeunit` library-chart values-block
2321 // enable-toggle key lives in exactly one place across every
2322 // caixa renderer. Pin the equality + `&'static` static-data
2323 // identity here so any local re-introduction of a sibling
2324 // `pub const HELM_VALUES_KEY_ENABLED: &str = "…"` at this crate
2325 // — the canonical drift footgun where a sibling local
2326 // `pub const` could happen to carry the same string at the
2327 // source while pointing at a different `&'static` allocation —
2328 // is a build-time test failure naming the offending drift, not
2329 // a silent per-values enable-toggle reroute at `helm template` /
2330 // `helm install` time far from the drift site (where the
2331 // workload silently comes up with the library chart's
2332 // admission-time defaults instead of the per-cluster override
2333 // the operator set). Peer to
2334 // [`helm_chart_api_version_re_export_points_at_caixa_core_canonical`]
2335 // / [`kube_key_spec_re_export_points_at_caixa_core_canonical`] /
2336 // [`default_library_name_re_export_points_at_caixa_core_canonical`]
2337 // on the sibling re-export axes +
2338 // `caixa_flux::tests::helm_values_key_enabled_re_export_points_at_caixa_core_canonical`
2339 // on the peer bundle-path renderer crate.
2340 caixa_core::assert_str_reexport_identity(
2341 "HELM_VALUES_KEY_ENABLED",
2342 HELM_VALUES_KEY_ENABLED,
2343 caixa_core::HELM_VALUES_KEY_ENABLED,
2344 );
2345 }
2346
2347 #[test]
2348 fn values_yaml_enable_toggle_key_pins_lifted_helm_values_key_enabled() {
2349 // Fail-before-pass-after pin on the production-code substitution:
2350 // [`build_values_yaml`]'s `block.insert(…, Value::Bool(…))`
2351 // consults the lifted [`HELM_VALUES_KEY_ENABLED`] re-export at
2352 // its insert site, so the rendered `values.yaml`'s per-values
2353 // enable-toggle axis is byte-identical to the canonical constant
2354 // by construction. Before the lift the field carried an inline
2355 // `"enabled".to_string()` literal; a future refactor that
2356 // accidentally reverted the substitution — or any parallel per-
2357 // renderer variant that inlined a stale `"enable"` /
2358 // `"disabled"` literal — would silently emit a values block
2359 // whose per-values enable-toggle lands under one key while
2360 // [`caixa_flux::cluster_bundle`]'s `HelmRelease`
2361 // `spec.values.<library>.enabled` per-cluster override lands
2362 // under another, so this pin trips at caixa-helm build time.
2363 // Peer to `chart_yaml_uses_lifted_helm_chart_api_version` on the
2364 // sibling structural-cross-axis-invariant surface — both close
2365 // the drift between a rendered-value navigator's `.get(…)` /
2366 // struct-field read on the constant and the production-code
2367 // emit site that consumes the same constant.
2368 let dir = render_chart_for_servico(&sample_caixa(), &sample_cu_yaml()).unwrap();
2369 let values = dir
2370 .files
2371 .iter()
2372 .find(|f| f.path == PathBuf::from(HELM_VALUES_YAML_FILENAME))
2373 .unwrap();
2374 let parsed: serde_yaml::Value = serde_yaml::from_str(&values.contents).unwrap();
2375 let cu_block = parsed
2376 .get(DEFAULT_LIBRARY_NAME)
2377 .expect("must wrap under DEFAULT_LIBRARY_NAME");
2378 assert_eq!(
2379 cu_block.get(HELM_VALUES_KEY_ENABLED),
2380 Some(&serde_yaml::Value::Bool(false)),
2381 "rendered values.yaml `{DEFAULT_LIBRARY_NAME}.{HELM_VALUES_KEY_ENABLED}` must \
2382 equal the default-off toggle the lifted HELM_VALUES_KEY_ENABLED axis carries — \
2383 a drifted enable-toggle key silently splits the per-values enable-flip across \
2384 two sibling scalar names on the caixa-helm / caixa-flux consumer split"
2385 );
2386 }
2387
2388 #[test]
2389 fn computeunit_spec_key_module_re_export_points_at_caixa_core_canonical() {
2390 // The renderer's `COMPUTEUNIT_SPEC_KEY_MODULE` was lifted from
2391 // the two inline `"module"` test-side call sites in this crate
2392 // (`values_yaml_wraps_under_pleme_computeunit_key`'s per-values
2393 // module-block present-check + the peer navigator on the
2394 // `library_name`-override wrap-key axis
2395 // `values_yaml_wrap_key_follows_library_name_override`) — every
2396 // per-Servico ComputeUnit CRD `spec.module` sub-block readback
2397 // in this crate now navigates through the same `&'static str`
2398 // re-exported to a re-export of
2399 // [`caixa_core::COMPUTEUNIT_SPEC_KEY_MODULE`] so the canonical
2400 // ComputeUnit-CRD per-`spec.*` wasm-module-reference axis lives
2401 // in exactly one place across every caixa renderer. Pin the
2402 // equality + static-data identity here so any local re-
2403 // introduction of a sibling `pub const COMPUTEUNIT_SPEC_KEY_MODULE:
2404 // &str = "…"` at this crate is a build-time test failure naming
2405 // the offending drift, not a silent per-Servico wasm-runtime-
2406 // binding drop at cluster-apply time. Peer to
2407 // [`helm_values_key_enabled_re_export_points_at_caixa_core_canonical`]
2408 // /
2409 // [`kube_key_spec_re_export_points_at_caixa_core_canonical`]
2410 // on the sibling canonical-Helm-load-bearing-string /
2411 // canonical-K8s-CR-body-key re-export axes +
2412 // `caixa_flux::tests::computeunit_spec_key_module_re_export_points_at_caixa_core_canonical`
2413 // on the peer per-Servico renderer crate.
2414 caixa_core::assert_str_reexport_identity(
2415 "COMPUTEUNIT_SPEC_KEY_MODULE",
2416 COMPUTEUNIT_SPEC_KEY_MODULE,
2417 caixa_core::COMPUTEUNIT_SPEC_KEY_MODULE,
2418 );
2419 }
2420
2421 #[test]
2422 fn computeunit_spec_key_trigger_re_export_points_at_caixa_core_canonical() {
2423 // Peer to
2424 // [`computeunit_spec_key_module_re_export_points_at_caixa_core_canonical`]
2425 // on the same ComputeUnit-CRD per-`spec.*` sub-block re-export
2426 // surface — pins the per-Servico invocation-shape sub-block
2427 // key's identity on the same trajectory.
2428 caixa_core::assert_str_reexport_identity(
2429 "COMPUTEUNIT_SPEC_KEY_TRIGGER",
2430 COMPUTEUNIT_SPEC_KEY_TRIGGER,
2431 caixa_core::COMPUTEUNIT_SPEC_KEY_TRIGGER,
2432 );
2433 }
2434
2435 #[test]
2436 fn computeunit_spec_key_capabilities_re_export_points_at_caixa_core_canonical() {
2437 // Peer to
2438 // [`computeunit_spec_key_module_re_export_points_at_caixa_core_canonical`]
2439 // and
2440 // [`computeunit_spec_key_trigger_re_export_points_at_caixa_core_canonical`]
2441 // on the same ComputeUnit-CRD per-`spec.*` sub-block re-export
2442 // surface — completes the substrate-side ComputeUnit-CRD
2443 // per-`spec.*` sub-block re-export triple in this crate on the
2444 // WASI-capability-token-list axis.
2445 caixa_core::assert_str_reexport_identity(
2446 "COMPUTEUNIT_SPEC_KEY_CAPABILITIES",
2447 COMPUTEUNIT_SPEC_KEY_CAPABILITIES,
2448 caixa_core::COMPUTEUNIT_SPEC_KEY_CAPABILITIES,
2449 );
2450 }
2451
2452 #[test]
2453 fn chart_file_alias_resolves_to_caixa_core_rendered_file() {
2454 // Type-alias identity pin: the [`ChartFile`] alias at this
2455 // crate's boundary resolves to the canonical
2456 // [`caixa_core::RenderedFile`] the substrate-side "one rendered
2457 // leaf artifact" shape lives at. `let _: ChartFile = <a
2458 // RenderedFile>` type-checks *iff* [`ChartFile`] is the aliased
2459 // canonical (not a sibling pub-struct re-declaration that
2460 // happens to carry the same field pair — that would compile
2461 // past the struct-literal navigators below but fail this
2462 // assignment). A drifted local `pub struct ChartFile { pub
2463 // path: PathBuf, pub contents: String }` at this crate — the
2464 // canonical drift footgun that would carry the same field pair
2465 // at the source while pointing at a different struct
2466 // definition — trips this pin at caixa-helm build time rather
2467 // than surfacing as a downstream `caixa_core::RenderedFile`
2468 // consumer refusing a `ChartFile`-shaped value at type-check
2469 // time far from the drift commit. Peer to the sibling
2470 // [`caixa_flux::BundleFile`]-alias-identity pin on the same
2471 // per-target-renderer canonical [`caixa_core::RenderedFile`]
2472 // re-export surface — both crates' per-artifact leaf type now
2473 // resolves through the same canonical struct definition, so a
2474 // future rebrand on the record shape lands at one caixa-core
2475 // edit and reaches both consumers by construction.
2476 let canonical: caixa_core::RenderedFile = caixa_core::RenderedFile {
2477 path: PathBuf::from(HELM_CHART_YAML_FILENAME),
2478 contents: String::new(),
2479 };
2480 let aliased: ChartFile = canonical.clone();
2481 assert_eq!(aliased, canonical);
2482 // Struct-literal construction still resolves through the alias
2483 // — the pre-lift `ChartFile { path, contents }` shape at every
2484 // production emit site (three sites in
2485 // `render_chart_for_servico_with`'s `ChartDir::files` assembly)
2486 // continues to compile, and the derive tuple travels through
2487 // the alias so downstream `ChartDir::files.iter().find(|f|
2488 // f.path == PathBuf::from(HELM_VALUES_YAML_FILENAME))`
2489 // navigators keep matching by `PartialEq` on `PathBuf`.
2490 let via_alias = ChartFile {
2491 path: PathBuf::from(HELM_VALUES_YAML_FILENAME),
2492 contents: format!("{DEFAULT_LIBRARY_NAME}:\n enabled: false\n"),
2493 };
2494 assert_eq!(via_alias.path.to_string_lossy(), HELM_VALUES_YAML_FILENAME);
2495 }
2496
2497 #[test]
2498 fn chart_file_new_constructor_travels_through_alias_to_canonical() {
2499 // Inherent-method-through-alias pin: the canonical
2500 // [`caixa_core::RenderedFile::new`] `impl Into<PathBuf>` /
2501 // `impl Into<String>` constructor every per-artifact leaf in
2502 // [`render_chart_for_servico_with`] now routes through
2503 // resolves at `ChartFile::new(…)` — Rust inherent methods
2504 // travel through a `pub type ChartFile = caixa_core::RenderedFile`
2505 // alias to the aliased canonical at name resolution, so a
2506 // drifted local `pub struct ChartFile { pub path: PathBuf, pub
2507 // contents: String }` at this crate would carry the field
2508 // pair the sibling type-alias-identity pin above still
2509 // accepts (both records share `pub path` / `pub contents`
2510 // shape) while dropping the constructor — the six sweep sites
2511 // in [`render_chart_for_servico_with`] would stop compiling
2512 // and the failing calls would name `ChartFile` directly,
2513 // making the drift-source unambiguous. This test pins the
2514 // constructor's per-alias reachability + the byte-identical
2515 // record shape against a `HELM_CHART_YAML_FILENAME`-keyed
2516 // probe so the pin fires at caixa-helm build time.
2517 let via_alias_new: ChartFile = ChartFile::new(HELM_CHART_YAML_FILENAME, "apiVersion: v2\n");
2518 let via_canonical_new = caixa_core::RenderedFile::new(
2519 HELM_CHART_YAML_FILENAME,
2520 String::from("apiVersion: v2\n"),
2521 );
2522 assert_eq!(via_alias_new, via_canonical_new);
2523 assert_eq!(via_alias_new.path, PathBuf::from(HELM_CHART_YAML_FILENAME));
2524 assert_eq!(via_alias_new.contents, "apiVersion: v2\n");
2525 }
2526
2527 #[test]
2528 fn render_opts_default_library_version_follows_lifted_constant() {
2529 // Peer of [`render_opts_default_library_name_follows_lifted_constant`]
2530 // (which pins the same alignment on the sibling
2531 // [`RenderOpts::library_name`] / [`DEFAULT_LIBRARY_NAME`] axis). The
2532 // [`RenderOpts::default()`] impl sets `library_version` from
2533 // [`DEFAULT_LIBRARY_VERSION`]; a future refactor that detached the
2534 // default-knob from the lifted constant — accidentally re-inlining
2535 // `"~0.1.0"` in the impl body — would silently split the value the
2536 // default knob threads into every rendered `Chart.yaml`
2537 // `dependencies[0].version` axis from the const the const's callers
2538 // (and this crate's future per-`DEFAULT_LIBRARY_VERSION` drift pins)
2539 // read. The two `Chart.yaml`-dep `(name, version)` scalar-axes now
2540 // share the same "default-knob follows lifted constant, byte for
2541 // byte" pin discipline the peer library-name axis carries.
2542 let opts = RenderOpts::default();
2543 assert_eq!(opts.library_version, DEFAULT_LIBRARY_VERSION);
2544 assert_eq!(opts.library_version, "~0.1.0");
2545 }
2546
2547 #[test]
2548 fn render_opts_default_enabled_default_follows_lifted_constant() {
2549 // Peer of [`render_opts_default_library_name_follows_lifted_constant`]
2550 // /
2551 // [`render_opts_default_library_version_follows_lifted_constant`]
2552 // / [`render_opts_default_library_repo_follows_lifted_constant`] —
2553 // the fourth leg of the [`RenderOpts::default()`]-body
2554 // default-knob-follows-lifted-constant quartet. The
2555 // [`RenderOpts::default()`] impl seeds `enabled_default` from
2556 // [`STANDALONE_LAREIRA_ENABLED_DEFAULT`]; every rendered
2557 // `lareira-<nome>` chart's `values.yaml` under-`<library>.enabled`
2558 // scalar reads through this knob, so a future refactor that
2559 // detached the default-knob from the lifted constant —
2560 // accidentally re-inlining `false` in the impl body — would
2561 // silently split the `bool` the default seed writes from the
2562 // const the drift-detection pin
2563 // [`standalone_lareira_enabled_default_pins_canonical_value`]
2564 // (in caixa-core) reads. The rendered values block would then
2565 // carry one `bool` at the emit site while the const-consuming
2566 // sibling test-fixture navigators (this crate's future per-
2567 // `STANDALONE_LAREIRA_ENABLED_DEFAULT` drift pins) read another,
2568 // and the substrate's chosen mirror-symmetric standalone /
2569 // composition per-values-block child-chart-enablement-toggle-
2570 // scalar-value default pair would silently disagree on the
2571 // standalone-path half. Peer with the sibling
2572 // `caixa_flux::tests::cluster_bundle_lareira_enabled_default_re_export_matches_caixa_core_canonical_value`
2573 // pin on the composition-path half of the same
2574 // [`HELM_VALUES_KEY_ENABLED`] scalar-axis pair.
2575 let opts = RenderOpts::default();
2576 assert_eq!(opts.enabled_default, STANDALONE_LAREIRA_ENABLED_DEFAULT);
2577 assert!(!opts.enabled_default);
2578 }
2579
2580 #[test]
2581 fn standalone_lareira_enabled_default_re_export_matches_caixa_core_canonical_value() {
2582 // The renderer's `STANDALONE_LAREIRA_ENABLED_DEFAULT` was lifted
2583 // from the [`RenderOpts::default()`] impl-body inline `false`
2584 // scalar-value literal at `caixa-helm/src/lib.rs:700` to a
2585 // re-export of [`caixa_core::STANDALONE_LAREIRA_ENABLED_DEFAULT`]
2586 // so the substrate-side default the standalone per-chart path
2587 // seeds under the sibling [`HELM_VALUES_KEY_ENABLED`]
2588 // leaf-scalar-key lives in exactly one place across every caixa
2589 // renderer (this crate's standalone per-chart path + the peer
2590 // `caixa_flux::cluster_bundle`'s composition per-cluster-
2591 // `HelmRelease` values-overlay path, which reads through the
2592 // inverse [`caixa_flux::CLUSTER_BUNDLE_LAREIRA_ENABLED_DEFAULT`]
2593 // re-export). Pin the equality here so any local re-introduction
2594 // of a sibling `pub const STANDALONE_LAREIRA_ENABLED_DEFAULT:
2595 // bool = …` at this crate (the canonical drift footgun the peer
2596 // `CLUSTER_BUNDLE_LAREIRA_ENABLED_DEFAULT` re-export identity
2597 // pin's rationale names as the recurring shape) is a build-time
2598 // test failure naming the offending drift, not a silent
2599 // apply-time toggle-mismatch routing the standalone per-chart
2600 // `values.<library>.enabled` seed onto one substrate-side
2601 // opt-out convention while the peer composition-path override
2602 // routes onto another. Peer to the sibling
2603 // `caixa_flux::tests::cluster_bundle_lareira_enabled_default_re_export_matches_caixa_core_canonical_value`
2604 // on the composition-path half of the same
2605 // [`HELM_VALUES_KEY_ENABLED`] scalar-axis pair — the two
2606 // per-path re-export identity pins together lock the two peer
2607 // scalar-value defaults' per-crate re-exports onto their shared
2608 // caixa-core canonical.
2609 assert_eq!(
2610 STANDALONE_LAREIRA_ENABLED_DEFAULT,
2611 caixa_core::STANDALONE_LAREIRA_ENABLED_DEFAULT,
2612 "STANDALONE_LAREIRA_ENABLED_DEFAULT re-export must remain the \
2613 same `bool` as its caixa-core canonical — a drifted local \
2614 `pub const STANDALONE_LAREIRA_ENABLED_DEFAULT: bool = …` at \
2615 caixa-helm would silently split the substrate's chosen \
2616 standalone per-chart opt-out seed from the peer \
2617 composition-path force-on inversion the caixa-core canonical \
2618 encodes."
2619 );
2620 assert!(
2621 !STANDALONE_LAREIRA_ENABLED_DEFAULT,
2622 "STANDALONE_LAREIRA_ENABLED_DEFAULT must remain `false` — the \
2623 standalone per-chart path is the substrate-side opt-out path \
2624 where cluster operators must opt each caixa in per-cluster, \
2625 inverse of the composition per-cluster-HelmRelease values-\
2626 overlay path's opt-in force-on."
2627 );
2628 }
2629
2630 #[test]
2631 fn render_opts_default_library_repo_follows_lifted_constant() {
2632 // Peer of [`render_opts_default_library_name_follows_lifted_constant`]
2633 // /
2634 // [`render_opts_default_library_version_follows_lifted_constant`] —
2635 // the third leg of the per-`Chart.yaml`-dep
2636 // `(repository, name, version)` default-knob triple. The
2637 // [`RenderOpts::default()`] impl seeds `library_repo` from
2638 // [`DEFAULT_LIBRARY_REPO`]; every rendered `lareira-<nome>` chart's
2639 // `Chart.yaml` `dependencies[0].repository` field reads through
2640 // this knob, so a future refactor that detached the default-knob
2641 // from the lifted constant — re-inlining
2642 // `"file://../pleme-computeunit"` in the impl body — would silently
2643 // split the URL the default seed writes from the const the
2644 // drift-detection pin below
2645 // ([`default_library_repo_ends_with_lifted_default_library_name`])
2646 // reads.
2647 let opts = RenderOpts::default();
2648 assert_eq!(opts.library_repo, DEFAULT_LIBRARY_REPO);
2649 assert_eq!(opts.library_repo, "file://../pleme-computeunit");
2650 }
2651
2652 #[test]
2653 fn default_library_version_parses_as_valid_semver_requirement() {
2654 // Structural pin: [`DEFAULT_LIBRARY_VERSION`] carries a Cargo-shaped
2655 // semver-requirement string that lands verbatim in every rendered
2656 // `lareira-<nome>` chart's `Chart.yaml` `dependencies[0].version`
2657 // field. Helm 3's chart-schema parser (`helm dependency build`,
2658 // `helm lint`, `helm template`, `helm install`) validates the
2659 // scalar against the same `semver::VersionReq` grammar
2660 // [`caixa_core::parse_requirement`] wraps, and rejects a malformed
2661 // shape (`"~0.1.,0"` — paste-from-typography stray comma;
2662 // `"v0.1.0"` — accidental Zig-style publish-tag prefix leaking back
2663 // from [`caixa_core::DEFAULT_PUBLISH_TAG_PREFIX`] into the
2664 // requirement axis; `"0.1"` with a trailing sigil dropped by a
2665 // fat-fingered edit) with the load-bearing `Error: found operator
2666 // …, expected version` diagnostic surfacing at chart-consumption
2667 // time — far from the constant-drift commit's source, with no
2668 // field naming the offending caixa or the drifted default. Routing
2669 // through [`caixa_core::parse_requirement`] here — the same
2670 // requirement-parser entry-point every peer typed `:versao`
2671 // requirement slot (`:deps`, `:deps-dev`, `:membros`, `:children`)
2672 // routes through via
2673 // [`caixa_core::require_valid_versao_requirement`] — closes the
2674 // drift structurally at caixa-helm build time and pins the const's
2675 // accepted set to exactly the set the peer author-facing
2676 // requirement axes accept: any shape a caixa author cannot write
2677 // in `:deps :versao` is a shape the substrate cannot seed as the
2678 // library-chart-dep default. Peer of the sibling
2679 // [`default_library_repo_ends_with_lifted_default_library_name`]
2680 // structural pin on the co-resident `(name, version)` per-Chart.yaml
2681 // dep-scalar pair.
2682 caixa_core::parse_requirement(DEFAULT_LIBRARY_VERSION).unwrap_or_else(|e| {
2683 panic!(
2684 "DEFAULT_LIBRARY_VERSION {DEFAULT_LIBRARY_VERSION:?} must parse as a valid \
2685 semver::VersionReq — every rendered lareira-<nome> chart's Chart.yaml \
2686 dependencies[0].version axis lands this scalar verbatim, and Helm 3's \
2687 chart-schema parser rejects a malformed shape at chart-consumption time \
2688 far from the constant-drift commit's source: {e}",
2689 )
2690 });
2691 }
2692
2693 #[test]
2694 fn default_library_repo_ends_with_lifted_default_library_name() {
2695 // Structural cross-const coherence pin: [`DEFAULT_LIBRARY_REPO`]
2696 // embeds the [`DEFAULT_LIBRARY_NAME`] byte-string verbatim as its
2697 // trailing directory-name component (the canonical
2698 // `file://../<library-chart-name>` shape every sibling
2699 // `lareira-<nome>` chart's `Chart.yaml` `dependencies[0]` entry
2700 // consults for a two-axis `(name, repository)` per-dep tuple that
2701 // Helm's per-chart-dep resolver `(chart-source-scheme + chart-name)`
2702 // navigator round-trips). The two axes must stay coupled: the
2703 // library-chart-directory on disk (the repo's trailing component)
2704 // and the library-chart's declared `name:` in its own
2705 // [`DEFAULT_LIBRARY_NAME`]-published `Chart.yaml` are the same
2706 // load-bearing chart-name identity. Prior to this pin the two
2707 // consts were independently authored — a future substrate-side
2708 // library-chart rebrand (`pleme-computeunit` → `pleme-cu` on a
2709 // shorter-form migration, `pleme-computeunit` →
2710 // `caixa-computeunit` on a substrate-alignment migration, a
2711 // per-edition library-chart fork the [`DEFAULT_LIBRARY_NAME`]
2712 // docstring names as a trajectory item) on the
2713 // [`caixa_core::DEFAULT_LIBRARY_NAME`] canonical without a
2714 // coordinated edit on this crate's [`DEFAULT_LIBRARY_REPO`] would
2715 // silently emit rendered `Chart.yaml` documents whose
2716 // `dependencies[0].name` names the new chart while
2717 // `dependencies[0].repository` points at the old directory —
2718 // `helm dependency build` would refuse to resolve the dep ("chart
2719 // <new-name> not found in file://../<old-name>") at chart-
2720 // consumption time, far from the constant-rebrand commit's source,
2721 // with no field naming the two-axis coherence drift root cause.
2722 // Pinning the structural `ends_with(DEFAULT_LIBRARY_NAME)` invariant
2723 // here surfaces the drift as a caixa-helm build-time test failure
2724 // and forces the coordinated `(REPO, NAME)` edit to move together.
2725 // Peer of the sibling
2726 // [`default_library_version_parses_as_valid_semver_requirement`]
2727 // structural pin on the co-resident `(name, version)` per-Chart.yaml
2728 // dep-scalar pair — completes the `(repository, name, version)`
2729 // per-Chart.yaml-dep default-triple's structural pin surface.
2730 assert!(
2731 DEFAULT_LIBRARY_REPO.ends_with(DEFAULT_LIBRARY_NAME),
2732 "DEFAULT_LIBRARY_REPO {DEFAULT_LIBRARY_REPO:?} must terminate with the lifted \
2733 DEFAULT_LIBRARY_NAME {DEFAULT_LIBRARY_NAME:?} — the two-axis (repository, name) \
2734 per-Chart.yaml-dep tuple must resolve to the same library-chart identity on \
2735 disk, so a rebrand on either axis must move both",
2736 );
2737 }
2738
2739 #[test]
2740 fn chart_yaml_version_routes_through_caixa_versao_accessor() {
2741 // Fail-before-pass-after pin: the emit-side per-`Chart.yaml`
2742 // top-level `version:` scalar the [`build_chart_yaml`] fn
2743 // writes must derive from the typed
2744 // [`caixa_core::Caixa::versao`] accessor byte-for-byte.
2745 // Before this converge the emit site carried a raw
2746 // `caixa.versao.clone()` field access at
2747 // [`build_chart_yaml`]'s per-`Chart.yaml` version-field
2748 // insert position — one of the two production-code
2749 // `String`-carry sites of `Caixa::versao` on this fn's
2750 // emit path — and a future extension of the accessor
2751 // (a build-metadata canonicalization pass the CAIXA-SDLC
2752 // §I SemVer-2 pin acknowledges, an OCI-tag normalization
2753 // the M4 registry-alignment slot lands, a per-edition
2754 // pre-release-tag overlay the sibling `Caixa::edicao`
2755 // universal-axis 4-digit-ASCII-decimal-year scalar
2756 // dispatches through) that landed on the accessor but
2757 // not on this emit site would silently split the
2758 // per-`Chart.yaml` `version:` axis (the discriminator
2759 // Helm's per-chart resolver keys per-release
2760 // reconciliation off, the paired `HelmRelease`
2761 // `spec.chart.spec.version` binds through, and every
2762 // `helm template <chart>` / `helm install <release>
2763 // <chart>` / `helm upgrade <release> <chart>
2764 // --version` invocation names through) from every peer
2765 // read-side consumer of `Caixa::versao` (the
2766 // README-body `v{versao}` scalar at
2767 // [`build_readme`]:958 the paired `feira chart`
2768 // Nord-themed emit round-trips through, every peer
2769 // per-axis navigator via `Caixa::versao()`, the
2770 // `caixa_flux::programs_yaml_entry` per-entry
2771 // `versao:` scalar at caixa-flux/src/lib.rs:2031, the
2772 // `caixa_feira::cmd::publish` per-tag `caixa {nome} v{versao}`
2773 // git-tag scalar at caixa-feira/src/cmd/publish.rs:72).
2774 // Byte-equal today (the accessor is `&self.versao`);
2775 // the pin catches any future accessor extension whose
2776 // emit-side write regresses to the raw field. Peer to
2777 // [`chart_yaml_app_version_routes_through_caixa_versao_accessor`]
2778 // on the sibling per-`Chart.yaml` `appVersion:` axis.
2779 let caixa = sample_caixa();
2780 let dir = render_chart_for_servico(&caixa, &sample_cu_yaml()).unwrap();
2781 let chart_file = dir
2782 .files
2783 .iter()
2784 .find(|f| f.path == PathBuf::from(HELM_CHART_YAML_FILENAME))
2785 .expect("Chart.yaml present");
2786 let chart: ChartYaml = serde_yaml::from_str(&chart_file.contents).unwrap();
2787 assert_eq!(
2788 chart.version.as_str(),
2789 caixa.versao(),
2790 "Chart.yaml `version:` must derive from the typed \
2791 `caixa_core::Caixa::versao` accessor byte-for-byte — a regression \
2792 that re-inlines `caixa.versao.clone()` at the emit site silently \
2793 splits the per-`Chart.yaml` `version:` axis from every future \
2794 accessor extension (SemVer-2 build-metadata canonicalization, \
2795 OCI-tag normalization, per-edition pre-release-tag overlay) that \
2796 lands on the accessor",
2797 );
2798 }
2799
2800 #[test]
2801 fn chart_yaml_app_version_routes_through_caixa_versao_accessor() {
2802 // Fail-before-pass-after pin: the emit-side per-`Chart.yaml`
2803 // top-level `appVersion:` scalar the [`build_chart_yaml`] fn
2804 // writes must derive from the typed
2805 // [`caixa_core::Caixa::versao`] accessor byte-for-byte.
2806 // Same single-source `let versao = caixa.versao().to_string()`
2807 // binding as the peer `version:` sibling pin — this test
2808 // pins the derived `Chart.yaml` `appVersion:` axis (the
2809 // axis Helm chart-consumers key per-application-version
2810 // documentation / release-note / OCI-tag / operator-side
2811 // per-Caixa CR revision off). Peer to
2812 // [`chart_yaml_version_routes_through_caixa_versao_accessor`]
2813 // on the sibling per-`Chart.yaml` `version:` axis — the
2814 // two together pin every per-`Chart.yaml` version-carrier
2815 // field on the typed accessor.
2816 let caixa = sample_caixa();
2817 let dir = render_chart_for_servico(&caixa, &sample_cu_yaml()).unwrap();
2818 let chart_file = dir
2819 .files
2820 .iter()
2821 .find(|f| f.path == PathBuf::from(HELM_CHART_YAML_FILENAME))
2822 .expect("Chart.yaml present");
2823 let chart: ChartYaml = serde_yaml::from_str(&chart_file.contents).unwrap();
2824 assert_eq!(
2825 chart.app_version.as_str(),
2826 caixa.versao(),
2827 "Chart.yaml `appVersion:` must derive from the typed \
2828 `caixa_core::Caixa::versao` accessor byte-for-byte — a regression \
2829 that re-inlines `caixa.versao.clone()` at the emit site silently \
2830 splits the per-`Chart.yaml` `appVersion:` axis from every future \
2831 accessor extension (SemVer-2 build-metadata canonicalization, \
2832 OCI-tag normalization, per-edition pre-release-tag overlay) that \
2833 lands on the accessor",
2834 );
2835 }
2836
2837 #[test]
2838 fn chart_yaml_name_routes_through_caixa_nome_accessor() {
2839 // Emit-path pin: the per-`Chart.yaml` top-level `name:`
2840 // scalar the [`build_chart_yaml`] fn writes must derive
2841 // from the typed [`caixa_core::Caixa::nome`] accessor
2842 // byte-for-byte through the substrate-canonical
2843 // [`caixa_core::lareira_chart_name`] identity composer.
2844 // Before this converge the outer `lareira_chart_name(&caixa.nome)`
2845 // call at [`render_chart_for_servico_with`] carried a raw
2846 // `&caixa.nome` borrow-then-deref of the underlying `String`
2847 // field, bypassing the typed accessor. Peer of the sibling
2848 // eb912de `caixa.versao().to_string()` converge on the
2849 // co-resident `Caixa::versao` `String`-carry axis in this
2850 // crate and the sibling 4a363bf / 54bf2f3 `caixa.nome().to_string()`
2851 // converges on the outer-Caixa `:nome` `String`-carry axis
2852 // in caixa-flux / caixa-mesh — extends the "one typed
2853 // dispatch on the substrate primitive, thin projections at
2854 // each consumer" discipline onto the non-`.clone()` raw-
2855 // field-access axis of `Caixa::nome` in caixa-helm. Byte-
2856 // equal today (the accessor is `&self.nome`); the pin
2857 // catches any future accessor extension (a per-cluster
2858 // alias overlay, an M4 CR-materializer name rewrite, a
2859 // future `:nome-suffix` slot) whose emit-side write
2860 // regresses to the raw `&caixa.nome` field access.
2861 let caixa = sample_caixa();
2862 let dir = render_chart_for_servico(&caixa, &sample_cu_yaml()).unwrap();
2863 let chart_file = dir
2864 .files
2865 .iter()
2866 .find(|f| f.path == PathBuf::from(HELM_CHART_YAML_FILENAME))
2867 .expect("Chart.yaml present");
2868 let chart: ChartYaml = serde_yaml::from_str(&chart_file.contents).unwrap();
2869 assert_eq!(
2870 chart.name,
2871 caixa_core::lareira_chart_name(caixa.nome()),
2872 "Chart.yaml `name:` must derive from the typed \
2873 `caixa_core::Caixa::nome` accessor through \
2874 `caixa_core::lareira_chart_name` byte-for-byte — a regression \
2875 that re-inlines `lareira_chart_name(&caixa.nome)` at the emit \
2876 site silently splits the per-`Chart.yaml` `name:` axis from \
2877 every future accessor extension (per-cluster alias overlay, \
2878 M4 CR-materializer name rewrite, `:nome-suffix` slot) that \
2879 lands on the accessor",
2880 );
2881 }
2882
2883 #[test]
2884 fn chart_yaml_description_fallback_routes_through_caixa_nome_accessor() {
2885 // Emit-path pin: on a `:descricao`-null caixa the
2886 // [`build_chart_yaml`] `description:` fallback substitutes
2887 // `format!("Generated chart for caixa Servico {}", caixa.nome())`,
2888 // which must derive its terminal identity byte-string from the
2889 // typed [`caixa_core::Caixa::nome`] accessor. Before this
2890 // converge the fallback carried a raw `caixa.nome` Display of
2891 // the underlying `String` field, bypassing the typed accessor.
2892 // Byte-equal today; the pin catches any future accessor
2893 // extension whose fallback emit regresses to the raw field.
2894 let caixa = Caixa {
2895 descricao: None,
2896 ..sample_caixa()
2897 };
2898 let dir = render_chart_for_servico(&caixa, &sample_cu_yaml()).unwrap();
2899 let chart_file = dir
2900 .files
2901 .iter()
2902 .find(|f| f.path == PathBuf::from(HELM_CHART_YAML_FILENAME))
2903 .expect("Chart.yaml present");
2904 let chart: ChartYaml = serde_yaml::from_str(&chart_file.contents).unwrap();
2905 assert_eq!(
2906 chart.description,
2907 format!("Generated chart for caixa Servico {}", caixa.nome()),
2908 "Chart.yaml `description:` `:descricao`-null fallback must \
2909 derive from the typed `caixa_core::Caixa::nome` accessor \
2910 byte-for-byte — a regression that re-inlines \
2911 `format!(\"Generated chart for caixa Servico {{}}\", caixa.nome)` \
2912 at the emit site silently splits the per-`Chart.yaml` \
2913 `description:` axis from every future accessor extension \
2914 that lands on the accessor",
2915 );
2916 }
2917
2918 #[test]
2919 fn values_yaml_header_nome_routes_through_caixa_nome_accessor() {
2920 // Emit-path pin: the [`build_values_yaml`] `# Auto-generated
2921 // by caixa-helm from caixa.lisp + servicos/{nome}.computeunit.yaml.`
2922 // comment header carries the parent-caixa's `:nome` identity
2923 // byte-string verbatim through the typed
2924 // [`caixa_core::Caixa::nome`] accessor. Before this converge
2925 // the site carried a raw `nome = caixa.nome` Display of the
2926 // underlying `String` field, bypassing the typed accessor.
2927 // Byte-equal today; the pin catches any future accessor
2928 // extension whose header-emit regresses to the raw field.
2929 let caixa = sample_caixa();
2930 let dir = render_chart_for_servico(&caixa, &sample_cu_yaml()).unwrap();
2931 let values_file = dir
2932 .files
2933 .iter()
2934 .find(|f| f.path == PathBuf::from(HELM_VALUES_YAML_FILENAME))
2935 .expect("values.yaml present");
2936 let expected = format!("servicos/{}.computeunit.yaml", caixa.nome());
2937 assert!(
2938 values_file.contents.contains(&expected),
2939 "values.yaml header comment must carry the typed \
2940 `caixa_core::Caixa::nome` accessor's byte-string \
2941 ({expected:?}) verbatim — a regression that re-inlines \
2942 `caixa.nome` in the header format silently splits the \
2943 values.yaml provenance-annotation axis from every future \
2944 accessor extension that lands on the accessor. \
2945 Full contents:\n{contents}",
2946 contents = values_file.contents,
2947 );
2948 }
2949
2950 #[test]
2951 fn readme_descricao_fallback_routes_through_caixa_nome_accessor() {
2952 // Emit-path pin: on a `:descricao`-null caixa the
2953 // [`build_readme`] descricao-line fallback substitutes
2954 // `format!("caixa Servico {}", caixa.nome())`, which must
2955 // derive its terminal identity byte-string from the typed
2956 // [`caixa_core::Caixa::nome`] accessor. Before this converge
2957 // the fallback carried a raw `caixa.nome` Display of the
2958 // underlying `String` field, bypassing the typed accessor.
2959 // Byte-equal today; the pin catches any future accessor
2960 // extension whose fallback emit regresses to the raw field.
2961 let caixa = Caixa {
2962 descricao: None,
2963 ..sample_caixa()
2964 };
2965 let dir = render_chart_for_servico(&caixa, &sample_cu_yaml()).unwrap();
2966 let readme_file = dir
2967 .files
2968 .iter()
2969 .find(|f| f.path == PathBuf::from(HELM_CHART_README_FILENAME))
2970 .expect("README.md present");
2971 let expected = format!("caixa Servico {}", caixa.nome());
2972 assert!(
2973 readme_file.contents.contains(&expected),
2974 "README.md `:descricao`-null fallback must carry the typed \
2975 `caixa_core::Caixa::nome` accessor's byte-string ({expected:?}) \
2976 verbatim — a regression that re-inlines `format!(\"caixa \
2977 Servico {{}}\", caixa.nome)` at the emit site silently \
2978 splits the README fallback-descricao axis from every future \
2979 accessor extension. Full contents:\n{contents}",
2980 contents = readme_file.contents,
2981 );
2982 }
2983
2984 #[test]
2985 fn readme_body_version_routes_through_caixa_versao_accessor() {
2986 // Emit-path pin: the per-`README.md` `Origin` line the
2987 // [`build_readme`] fn writes carries the terminal
2988 // `v{versao}` scalar the `feira chart` Nord-themed emit
2989 // round-trips through — that scalar must derive from the
2990 // typed [`caixa_core::Caixa::versao`] accessor byte-for-byte.
2991 // Before this converge the emit site carried a raw
2992 // `caixa.versao` `Display` field-access, bypassing the
2993 // typed accessor. Sibling of the 162e2e2 (caixa-flux) /
2994 // 980c059 (caixa-mesh) / 22461ef (caixa-helm) `Caixa::nome`
2995 // Display-axis converges — this closes the co-resident
2996 // `Caixa::versao` Display-axis in caixa-helm the eb912de
2997 // `caixa.versao().to_string()` `String`-carry converge
2998 // left open on the read-only Display-borrow arm. Byte-equal
2999 // today (the accessor is `&self.versao`); the pin catches
3000 // any future accessor extension (SemVer-2 build-metadata
3001 // canonicalization, OCI-tag normalization, per-edition
3002 // pre-release-tag overlay) whose emit-side Display regresses
3003 // to the raw field.
3004 let caixa = sample_caixa();
3005 let dir = render_chart_for_servico(&caixa, &sample_cu_yaml()).unwrap();
3006 let readme_file = dir
3007 .files
3008 .iter()
3009 .find(|f| f.path == PathBuf::from(HELM_CHART_README_FILENAME))
3010 .expect("README.md present");
3011 let expected = format!("caixa.lisp` v{}.", caixa.versao());
3012 assert!(
3013 readme_file.contents.contains(&expected),
3014 "README.md `Origin`-line `v{{versao}}` scalar must derive from \
3015 the typed `caixa_core::Caixa::versao` accessor byte-for-byte \
3016 ({expected:?}) — a regression that re-inlines `caixa.versao` \
3017 in the format silently splits the README origin-line version \
3018 axis from every future accessor extension (SemVer-2 \
3019 build-metadata canonicalization, OCI-tag normalization, \
3020 per-edition pre-release-tag overlay) that lands on the \
3021 accessor. Full contents:\n{contents}",
3022 contents = readme_file.contents,
3023 );
3024 }
3025
3026 #[test]
3027 fn readme_repositorio_fallback_routes_through_caixa_nome_accessor() {
3028 // Emit-path pin: on a `:repositorio`-null caixa the
3029 // [`build_readme`] `repo` interpolation falls back to
3030 // `caixa.nome()`, which must derive from the typed
3031 // [`caixa_core::Caixa::nome`] accessor. Before this converge
3032 // the fallback carried a raw `caixa.nome.as_str()` on the
3033 // underlying `String` field, bypassing the typed accessor.
3034 // Byte-equal today; the pin catches any future accessor
3035 // extension whose fallback emit regresses to the raw field.
3036 let caixa = Caixa {
3037 repositorio: None,
3038 ..sample_caixa()
3039 };
3040 let dir = render_chart_for_servico(&caixa, &sample_cu_yaml()).unwrap();
3041 let readme_file = dir
3042 .files
3043 .iter()
3044 .find(|f| f.path == PathBuf::from(HELM_CHART_README_FILENAME))
3045 .expect("README.md present");
3046 let expected = format!(
3047 "Generated by `caixa-helm` from `{}/caixa.lisp`",
3048 caixa.nome()
3049 );
3050 assert!(
3051 readme_file.contents.contains(&expected),
3052 "README.md `:repositorio`-null fallback must derive its \
3053 `repo` interpolation from the typed \
3054 `caixa_core::Caixa::nome` accessor byte-for-byte \
3055 ({expected:?}) — a regression that re-inlines \
3056 `caixa.nome.as_str()` at the emit site silently splits \
3057 the README origin-line repo axis from every future \
3058 accessor extension. Full contents:\n{contents}",
3059 contents = readme_file.contents,
3060 );
3061 }
3062}