use greentic_deploy_spec::{Environment, RevisionRuntimeBlock, RuntimeConfig, SchemaVersion};
pub fn materialize_runtime_config(env: &Environment) -> RuntimeConfig {
let revisions = env
.traffic_splits
.iter()
.flat_map(|split| {
split.entries.iter().map(move |entry| RevisionRuntimeBlock {
deployment_id: split.deployment_id,
revision_id: entry.revision_id,
bundle_id: split.bundle_id.clone(),
pack_list_refs: Vec::new(),
pack_config_refs: Vec::new(),
weight_bps: entry.weight_bps,
})
})
.collect();
RuntimeConfig {
schema: SchemaVersion::new(SchemaVersion::RUNTIME_CONFIG_V1),
env_id: env.environment_id.clone(),
revisions,
}
}
#[cfg(test)]
mod tests {
use super::*;
use greentic_deploy_spec::{
BundleId, DeploymentId, EnvId, EnvironmentHostConfig, RevisionId, TrafficSplit,
TrafficSplitEntry,
};
use std::path::PathBuf;
fn env(env_id: &str, traffic_splits: Vec<TrafficSplit>) -> Environment {
let id = EnvId::try_from(env_id).unwrap();
Environment {
schema: SchemaVersion::new(SchemaVersion::ENVIRONMENT_V1),
environment_id: id.clone(),
name: env_id.to_string(),
host_config: EnvironmentHostConfig {
env_id: id,
region: None,
tenant_org_id: None,
},
packs: Vec::new(),
credentials_ref: None,
bundles: Vec::new(),
revisions: Vec::new(),
traffic_splits,
revocation: Default::default(),
retention: Default::default(),
health: Default::default(),
}
}
fn split(
env_id: &str,
bundle: &str,
deployment_id: DeploymentId,
entries: Vec<(RevisionId, u32)>,
) -> TrafficSplit {
TrafficSplit {
schema: SchemaVersion::new(SchemaVersion::TRAFFIC_SPLIT_V1),
env_id: EnvId::try_from(env_id).unwrap(),
deployment_id,
bundle_id: BundleId::new(bundle),
generation: 0,
entries: entries
.into_iter()
.map(|(revision_id, weight_bps)| TrafficSplitEntry {
revision_id,
weight_bps,
})
.collect(),
updated_at: chrono::Utc::now(),
updated_by: "test".to_string(),
idempotency_key: "k".to_string(),
authorization_ref: PathBuf::from("auth.json"),
previous_split_ref: None,
}
}
#[test]
fn empty_env_yields_no_blocks() {
let cfg = materialize_runtime_config(&env("local", Vec::new()));
assert_eq!(cfg.schema.as_str(), SchemaVersion::RUNTIME_CONFIG_V1);
assert_eq!(cfg.env_id.as_str(), "local");
assert!(cfg.revisions.is_empty());
}
#[test]
fn single_split_projects_one_block_per_entry_preserving_weights() {
let did = DeploymentId::new();
let (rid1, rid2) = (RevisionId::new(), RevisionId::new());
let env = env(
"local",
vec![split(
"local",
"fast2flow",
did,
vec![(rid1, 9_000), (rid2, 1_000)],
)],
);
let cfg = materialize_runtime_config(&env);
assert_eq!(cfg.revisions.len(), 2);
assert_eq!(cfg.revisions[0].revision_id, rid1);
assert_eq!(cfg.revisions[0].deployment_id, did);
assert_eq!(cfg.revisions[0].bundle_id, BundleId::new("fast2flow"));
assert_eq!(cfg.revisions[0].weight_bps, 9_000);
assert_eq!(cfg.revisions[1].weight_bps, 1_000);
assert!(cfg.revisions[0].pack_list_refs.is_empty());
assert!(cfg.revisions[0].pack_config_refs.is_empty());
let sum: u32 = cfg.revisions.iter().map(|b| b.weight_bps).sum();
assert_eq!(sum, 10_000);
}
#[test]
fn multiple_deployments_each_contribute_their_split() {
let (did1, did2) = (DeploymentId::new(), DeploymentId::new());
let (rid1, rid2) = (RevisionId::new(), RevisionId::new());
let env = env(
"local",
vec![
split("local", "fast2flow", did1, vec![(rid1, 10_000)]),
split("local", "llm-router", did2, vec![(rid2, 10_000)]),
],
);
let cfg = materialize_runtime_config(&env);
assert_eq!(cfg.revisions.len(), 2);
let deployments: Vec<DeploymentId> =
cfg.revisions.iter().map(|b| b.deployment_id).collect();
assert!(deployments.contains(&did1));
assert!(deployments.contains(&did2));
}
#[test]
fn zero_weight_entry_is_preserved_for_cookie_stickiness() {
let did = DeploymentId::new();
let (rid1, rid2) = (RevisionId::new(), RevisionId::new());
let env = env(
"local",
vec![split(
"local",
"fast2flow",
did,
vec![(rid1, 10_000), (rid2, 0)],
)],
);
let cfg = materialize_runtime_config(&env);
assert_eq!(cfg.revisions.len(), 2);
assert_eq!(cfg.revisions[1].revision_id, rid2);
assert_eq!(cfg.revisions[1].weight_bps, 0);
}
}