use greentic_deploy_spec::{Environment, RevisionRuntimeBlock, RuntimeConfig, SchemaVersion};
use std::path::PathBuf;
pub fn materialize_runtime_config(env: &Environment) -> RuntimeConfig {
let revisions = env
.traffic_splits
.iter()
.flat_map(|split| {
split.entries.iter().map(move |entry| {
let pack_list_refs = env
.revisions
.iter()
.find(|r| {
r.revision_id == entry.revision_id && r.deployment_id == split.deployment_id
})
.filter(|r| !r.pack_list_lock_ref.as_os_str().is_empty())
.map(|r| vec![r.pack_list_lock_ref.clone()])
.unwrap_or_default();
RevisionRuntimeBlock {
deployment_id: split.deployment_id,
revision_id: entry.revision_id,
bundle_id: split.bundle_id.clone(),
pack_list_refs,
pack_config_refs: Vec::<PathBuf>::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, Revision, RevisionId,
RevisionLifecycle, 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,
listen_addr: None,
},
packs: Vec::new(),
credentials_ref: None,
bundles: Vec::new(),
revisions: Vec::new(),
traffic_splits,
messaging_endpoints: Vec::new(),
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,
}
}
fn revision(
env_id: &str,
bundle: &str,
deployment_id: DeploymentId,
revision_id: RevisionId,
pack_list_lock_ref: PathBuf,
) -> Revision {
Revision {
schema: SchemaVersion::new(SchemaVersion::REVISION_V1),
revision_id,
env_id: EnvId::try_from(env_id).unwrap(),
bundle_id: BundleId::new(bundle),
deployment_id,
sequence: 1,
created_at: chrono::Utc::now(),
bundle_digest: "sha256:00".to_string(),
pack_list: Vec::new(),
pack_list_lock_ref,
config_digest: "sha256:00".to_string(),
signature_sidecar_ref: PathBuf::from("rev.sig"),
lifecycle: RevisionLifecycle::Ready,
staged_at: None,
warmed_at: None,
drain_seconds: 30,
abort_metrics: Vec::new(),
}
}
#[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);
}
#[test]
fn split_entry_emits_matching_revisions_pack_list_lock_ref() {
let did = DeploymentId::new();
let rid = RevisionId::new();
let lock_ref = PathBuf::from(format!("revisions/{rid}/pack-list.lock"));
let mut env = env(
"local",
vec![split("local", "fast2flow", did, vec![(rid, 10_000)])],
);
env.revisions
.push(revision("local", "fast2flow", did, rid, lock_ref.clone()));
let cfg = materialize_runtime_config(&env);
assert_eq!(cfg.revisions.len(), 1);
assert_eq!(cfg.revisions[0].pack_list_refs, vec![lock_ref]);
assert!(cfg.revisions[0].pack_config_refs.is_empty());
}
#[test]
fn split_entry_without_matching_revision_emits_no_refs() {
let did = DeploymentId::new();
let rid = RevisionId::new();
let env = env(
"local",
vec![split("local", "fast2flow", did, vec![(rid, 10_000)])],
);
let cfg = materialize_runtime_config(&env);
assert_eq!(cfg.revisions.len(), 1);
assert_eq!(cfg.revisions[0].weight_bps, 10_000);
assert!(cfg.revisions[0].pack_list_refs.is_empty());
}
#[test]
fn revision_with_empty_lock_ref_emits_no_refs() {
let did = DeploymentId::new();
let rid = RevisionId::new();
let mut env = env(
"local",
vec![split("local", "fast2flow", did, vec![(rid, 10_000)])],
);
env.revisions
.push(revision("local", "fast2flow", did, rid, PathBuf::new()));
let cfg = materialize_runtime_config(&env);
assert_eq!(cfg.revisions.len(), 1);
assert!(cfg.revisions[0].pack_list_refs.is_empty());
}
}