use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct Condition {
#[serde(rename = "type")]
pub type_: String,
pub status: ConditionStatus,
pub reason: String,
#[serde(default)]
pub message: String,
pub last_transition_time: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub observed_generation: Option<i64>,
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, JsonSchema)]
pub enum ConditionStatus {
True,
False,
Unknown,
}
impl From<bool> for ConditionStatus {
fn from(value: bool) -> Self {
if value { Self::True } else { Self::False }
}
}
pub fn upsert_condition(
conditions: &mut Vec<Condition>,
type_: &str,
status: ConditionStatus,
reason: &str,
message: &str,
now: &str,
) {
if let Some(existing) = conditions.iter_mut().find(|c| c.type_ == type_) {
if existing.status != status {
existing.status = status;
now.clone_into(&mut existing.last_transition_time);
}
reason.clone_into(&mut existing.reason);
message.clone_into(&mut existing.message);
} else {
conditions.push(Condition {
type_: type_.to_owned(),
status,
reason: reason.to_owned(),
message: message.to_owned(),
last_transition_time: now.to_owned(),
observed_generation: None,
});
}
}
#[cfg(test)]
mod tests {
use super::{Condition, ConditionStatus, upsert_condition};
#[test]
fn upsert_condition_inserts_a_fresh_condition() {
let mut conditions = Vec::new();
upsert_condition(
&mut conditions,
"Progressing",
ConditionStatus::True,
"CreatingExecutionUnit",
"waiting for the sandbox claim",
"2026-07-11T00:00:00Z",
);
assert_eq!(conditions.len(), 1);
assert_eq!(conditions[0].type_, "Progressing");
assert_eq!(conditions[0].status, ConditionStatus::True);
assert_eq!(conditions[0].last_transition_time, "2026-07-11T00:00:00Z");
let json = serde_json::to_value(&conditions[0]).unwrap();
assert_eq!(json["type"], "Progressing");
assert_eq!(json["status"], "True");
}
#[test]
fn upsert_condition_only_bumps_transition_time_on_a_real_flip() {
let mut conditions = vec![Condition {
type_: "Ready".to_owned(),
status: ConditionStatus::False,
reason: "WaitingForHarness".to_owned(),
message: String::new(),
last_transition_time: "2026-07-11T00:00:00Z".to_owned(),
observed_generation: None,
}];
upsert_condition(
&mut conditions,
"Ready",
ConditionStatus::False,
"WaitingForHarness",
"still waiting",
"2026-07-11T00:05:00Z",
);
assert_eq!(conditions[0].last_transition_time, "2026-07-11T00:00:00Z");
assert_eq!(conditions[0].message, "still waiting");
upsert_condition(
&mut conditions,
"Ready",
ConditionStatus::True,
"HarnessReady",
"the harness is dialable",
"2026-07-11T00:10:00Z",
);
assert_eq!(conditions[0].status, ConditionStatus::True);
assert_eq!(conditions[0].last_transition_time, "2026-07-11T00:10:00Z");
}
#[test]
fn ready_and_progressing_are_independent_conditions() {
let mut conditions = Vec::new();
upsert_condition(
&mut conditions,
"Progressing",
ConditionStatus::True,
"CreatingExecutionUnit",
"",
"2026-07-11T00:00:00Z",
);
upsert_condition(
&mut conditions,
"Ready",
ConditionStatus::False,
"WaitingForHarness",
"",
"2026-07-11T00:00:00Z",
);
assert_eq!(conditions.len(), 2);
upsert_condition(
&mut conditions,
"Ready",
ConditionStatus::True,
"HarnessReady",
"",
"2026-07-11T00:01:00Z",
);
upsert_condition(
&mut conditions,
"Progressing",
ConditionStatus::False,
"HarnessReady",
"",
"2026-07-11T00:01:00Z",
);
assert_eq!(conditions.len(), 2);
let ready = conditions.iter().find(|c| c.type_ == "Ready").unwrap();
let progressing = conditions
.iter()
.find(|c| c.type_ == "Progressing")
.unwrap();
assert_eq!(ready.status, ConditionStatus::True);
assert_eq!(progressing.status, ConditionStatus::False);
}
}