#![allow(clippy::pedantic, clippy::nursery, missing_docs)]
use super::*;
use crate::routine::{RoutinePayload, RoutineProvenance, RoutineScope, RoutineSuspend};
fn provenance() -> RoutineProvenance {
RoutineProvenance {
creator_persona: "persona-1".to_owned(),
conversation_id: "conv-1".to_owned(),
}
}
fn valid_spec() -> RoutineSpec {
RoutineSpec {
display_name: None,
description: Some("daily greeting".to_owned()),
scope: RoutineScope::Private,
schedule: RoutineSchedule::Cron {
expression: "0 9 * * *".to_owned(),
timezone: None,
},
payload: RoutinePayload {
prompt: "Post a short good-morning message to the team channel.".to_owned(),
},
provenance: provenance(),
suspend: None,
}
}
fn routine(spec: RoutineSpec) -> Routine {
let mut r = Routine::new("greeting", spec);
r.metadata.namespace = Some("polychrome".to_owned());
r.metadata.uid = Some("uid-1".to_owned());
r
}
#[test]
fn validate_spec_accepts_the_worked_example() {
assert_eq!(validate_spec(&valid_spec()), Ok(()));
}
#[test]
fn plan_applies_the_worked_example() {
assert_eq!(plan(&routine(valid_spec())), RoutineAction::Apply);
}
#[test]
fn validate_spec_rejects_an_empty_prompt() {
let mut spec = valid_spec();
spec.payload.prompt = String::new();
let err = validate_spec(&spec).unwrap_err();
assert!(err.contains("payload.prompt"), "{err}");
}
#[test]
fn validate_spec_rejects_a_whitespace_only_prompt() {
let mut spec = valid_spec();
spec.payload.prompt = " \n\t ".to_owned();
let err = validate_spec(&spec).unwrap_err();
assert!(err.contains("payload.prompt"), "{err}");
}
#[test]
fn validate_spec_accepts_private_scope() {
let mut spec = valid_spec();
spec.scope = RoutineScope::Private;
assert_eq!(validate_spec(&spec), Ok(()));
}
#[test]
fn validate_spec_accepts_public_scope() {
let mut spec = valid_spec();
spec.scope = RoutineScope::Public;
assert_eq!(validate_spec(&spec), Ok(()));
}
#[test]
fn old_reserved_scope_values_fail_to_deserialize_before_validation() {
for raw in ["instance", "persona", "shared"] {
let yaml = serde_json::json!({
"scope": raw,
"schedule": { "cron": { "expression": "0 9 * * *" } },
"payload": { "prompt": "hi" },
"provenance": { "creatorPersona": "persona-1", "conversationId": "conv-1" },
});
assert!(
serde_json::from_value::<RoutineSpec>(yaml).is_err(),
"{raw} must fail to deserialize, not reach validate_spec"
);
}
}
#[test]
fn validate_spec_rejects_bad_cron_expression() {
let mut spec = valid_spec();
spec.schedule = RoutineSchedule::Cron {
expression: "not a cron expression".to_owned(),
timezone: None,
};
let err = validate_spec(&spec).unwrap_err();
assert!(err.contains("not a valid cron expression"), "{err}");
}
#[test]
fn validate_spec_rejects_a_bogus_timezone() {
let mut spec = valid_spec();
spec.schedule = RoutineSchedule::Cron {
expression: "0 9 * * *".to_owned(),
timezone: Some("Not/AZone".to_owned()),
};
let err = validate_spec(&spec).unwrap_err();
assert!(err.contains("Not/AZone"), "{err}");
}
#[test]
fn validate_spec_accepts_standard_cron_sunday_zero() {
let mut spec = valid_spec();
spec.schedule = RoutineSchedule::Cron {
expression: "0 9 * * 0".to_owned(),
timezone: None,
};
assert_eq!(validate_spec(&spec), Ok(()));
}
#[test]
fn validate_spec_accepts_standard_cron_sunday_seven() {
let mut spec = valid_spec();
spec.schedule = RoutineSchedule::Cron {
expression: "0 9 * * 7".to_owned(),
timezone: None,
};
assert_eq!(validate_spec(&spec), Ok(()));
}
#[test]
fn validate_spec_accepts_a_standard_cron_weekday_range() {
let mut spec = valid_spec();
spec.schedule = RoutineSchedule::Cron {
expression: "30 11 * * 1-5".to_owned(),
timezone: None,
};
assert_eq!(validate_spec(&spec), Ok(()));
}
#[test]
fn validate_spec_still_rejects_a_day_of_week_value_out_of_range_either_way() {
let mut spec = valid_spec();
spec.schedule = RoutineSchedule::Cron {
expression: "0 9 * * 8".to_owned(),
timezone: None,
};
let err = validate_spec(&spec).unwrap_err();
assert!(err.contains("not a valid cron expression"), "{err}");
}
#[test]
fn validate_spec_accepts_a_once_schedule() {
let mut spec = valid_spec();
spec.schedule = RoutineSchedule::Once {
at: "2026-08-01T15:00:00".to_owned(),
timezone: None,
};
assert_eq!(validate_spec(&spec), Ok(()));
}
#[test]
fn validate_spec_accepts_a_once_schedule_with_a_named_timezone() {
let mut spec = valid_spec();
spec.schedule = RoutineSchedule::Once {
at: "2026-08-01T15:00:00".to_owned(),
timezone: Some("America/New_York".to_owned()),
};
assert_eq!(validate_spec(&spec), Ok(()));
}
#[test]
fn validate_spec_rejects_a_bad_once_instant() {
let mut spec = valid_spec();
spec.schedule = RoutineSchedule::Once {
at: "not-a-timestamp".to_owned(),
timezone: None,
};
let err = validate_spec(&spec).unwrap_err();
assert!(err.contains("not-a-timestamp"), "{err}");
}
#[test]
fn validate_spec_rejects_a_once_instant_carrying_an_offset() {
let mut spec = valid_spec();
spec.schedule = RoutineSchedule::Once {
at: "2026-08-01T15:00:00Z".to_owned(),
timezone: None,
};
let err = validate_spec(&spec).unwrap_err();
assert!(err.contains("no UTC offset"), "{err}");
}
#[test]
fn validate_spec_rejects_a_once_instant_carrying_a_numeric_offset() {
let mut spec = valid_spec();
spec.schedule = RoutineSchedule::Once {
at: "2026-08-01T15:00:00+02:00".to_owned(),
timezone: None,
};
let err = validate_spec(&spec).unwrap_err();
assert!(err.contains("no UTC offset"), "{err}");
}
#[test]
fn validate_spec_rejects_an_unknown_once_timezone() {
let mut spec = valid_spec();
spec.schedule = RoutineSchedule::Once {
at: "2026-08-01T15:00:00".to_owned(),
timezone: Some("Not/AZone".to_owned()),
};
let err = validate_spec(&spec).unwrap_err();
assert!(err.contains("not a known IANA zone name"), "{err}");
}
#[test]
fn validate_spec_rejects_a_once_instant_in_a_spring_forward_gap() {
let mut spec = valid_spec();
spec.schedule = RoutineSchedule::Once {
at: "2026-03-08T02:30:00".to_owned(),
timezone: Some("America/New_York".to_owned()),
};
let err = validate_spec(&spec).unwrap_err();
assert!(err.contains("does not exist"), "{err}");
}
#[test]
fn validate_spec_accepts_no_suspend_at_all() {
let spec = valid_spec();
assert_eq!(
spec.suspend, None,
"the worked example is active by default"
);
assert_eq!(validate_spec(&spec), Ok(()));
}
#[test]
fn validate_spec_accepts_a_well_formed_suspend() {
let mut spec = valid_spec();
spec.suspend = Some(RoutineSuspend {
paused_by: "persona-1".to_owned(),
paused_at: "2026-07-23T00:00:00Z".to_owned(),
reason: Some("rotating out old announcements".to_owned()),
});
assert_eq!(validate_spec(&spec), Ok(()));
}
#[test]
fn validate_spec_accepts_a_suspend_with_no_reason() {
let mut spec = valid_spec();
spec.suspend = Some(RoutineSuspend {
paused_by: "persona-1".to_owned(),
paused_at: "2026-07-23T00:00:00Z".to_owned(),
reason: None,
});
assert_eq!(validate_spec(&spec), Ok(()));
}
#[test]
fn validate_spec_rejects_an_empty_paused_by() {
let mut spec = valid_spec();
spec.suspend = Some(RoutineSuspend {
paused_by: " ".to_owned(),
paused_at: "2026-07-23T00:00:00Z".to_owned(),
reason: None,
});
let err = validate_spec(&spec).unwrap_err();
assert!(err.contains("pausedBy"), "{err}");
}
#[test]
fn validate_spec_rejects_a_bad_paused_at_instant() {
let mut spec = valid_spec();
spec.suspend = Some(RoutineSuspend {
paused_by: "persona-1".to_owned(),
paused_at: "not-a-timestamp".to_owned(),
reason: None,
});
let err = validate_spec(&spec).unwrap_err();
assert!(err.contains("not-a-timestamp"), "{err}");
assert!(err.contains("RFC3339"), "{err}");
}
#[test]
fn validate_spec_accepts_the_checked_in_greeting_example() {
let yaml = include_str!("../../../../manifests/examples/greeting/routine.yaml"); let doc: serde_yaml_ng::Value = serde_yaml_ng::from_str(yaml).expect("valid YAML");
let spec_value = doc.get("spec").expect("spec key").clone();
let json = serde_json::to_value(spec_value).expect("yaml value converts to json value");
let spec: RoutineSpec = serde_json::from_value(json).expect("spec deserializes");
assert_eq!(validate_spec(&spec), Ok(()));
assert_eq!(spec.scope, RoutineScope::Private);
}
#[test]
fn plan_reports_invalid_with_the_specific_reason() {
let mut spec = valid_spec();
spec.payload.prompt = String::new();
match plan(&routine(spec)) {
RoutineAction::Invalid(reason) => {
assert!(reason.contains("payload.prompt"), "{reason}");
}
other => panic!("expected Invalid, got {other:?}"),
}
}
#[test]
fn deleting_routine_plans_noop() {
let mut r = routine(valid_spec());
r.metadata.deletion_timestamp = Some(k8s_openapi::apimachinery::pkg::apis::meta::v1::Time(
"2026-06-10T00:00:00Z".parse().unwrap(),
));
assert_eq!(plan(&r), RoutineAction::Noop);
}
#[test]
fn reconcile_apply_plans_apply_for_a_valid_spec() {
assert_eq!(plan(&routine(valid_spec())), RoutineAction::Apply);
}
mod fake_routines {
use std::sync::{Arc, Mutex};
use http::{Method, StatusCode};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct Observed {
pub(super) method: Method,
pub(super) path: String,
}
pub(super) fn client(
requests: Arc<Mutex<Vec<Observed>>>,
child_owner_uid: &'static str,
) -> kube::Client {
let svc = tower::service_fn(move |req: http::Request<kube::client::Body>| {
let requests = requests.clone();
async move {
let method = req.method().clone();
let path = req.uri().path().to_owned();
requests
.lock()
.expect("requests lock poisoned")
.push(Observed {
method: method.clone(),
path: path.clone(),
});
let payload = if method == Method::GET && path.contains("/serviceaccounts/") {
serde_json::to_vec(&serde_json::json!({
"apiVersion": "v1",
"kind": "ServiceAccount",
"metadata": {
"name": "greeting-trigger",
"uid": "sa-uid",
"resourceVersion": "sa-rv",
"ownerReferences": [{
"apiVersion": "polychrome.dev/v1alpha1",
"kind": "Routine",
"name": "greeting",
"uid": child_owner_uid,
"controller": true,
}],
},
}))
.unwrap()
} else if method == Method::GET && path.contains("/cronjobs/") {
serde_json::to_vec(&serde_json::json!({
"apiVersion": "batch/v1",
"kind": "CronJob",
"metadata": {
"name": "greeting",
"uid": "cron-uid",
"resourceVersion": "cron-rv",
"ownerReferences": [{
"apiVersion": "polychrome.dev/v1alpha1",
"kind": "Routine",
"name": "greeting",
"uid": child_owner_uid,
"controller": true,
}],
},
}))
.unwrap()
} else if method == Method::DELETE {
serde_json::to_vec(&serde_json::json!({
"apiVersion": "v1",
"kind": "Status",
"status": "Success",
}))
.unwrap()
} else {
serde_json::to_vec(&serde_json::json!({
"apiVersion": "polychrome.dev/v1alpha1",
"kind": "Routine",
"metadata": { "name": "greeting" },
"spec": {
"scope": "private",
"schedule": { "cron": { "expression": "0 9 * * *" } },
"payload": {
"prompt": "Post a short good-morning message to the team channel.",
},
"provenance": {
"creatorPersona": "persona-1",
"conversationId": "conv-1",
},
},
"status": {},
}))
.unwrap()
};
let resp = http::Response::builder()
.status(StatusCode::OK)
.header("content-type", "application/json")
.body(kube::client::Body::from(payload))
.expect("build fake response");
Ok::<_, std::convert::Infallible>(resp)
}
});
kube::Client::new(svc, "polychrome")
}
}
#[tokio::test]
async fn reconcile_deletes_both_legacy_children_never_applies() {
let requests = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
let client = fake_routines::client(requests.clone(), "uid-1");
let ctx = std::sync::Arc::new(Context { client });
let r = std::sync::Arc::new(routine(valid_spec()));
let action = reconcile(r, ctx).await.expect("reconcile succeeds");
assert_eq!(
action,
Action::await_change(),
"nothing owned, so nothing to periodically requeue for"
);
let seen = requests.lock().expect("lock").clone();
let deletes: Vec<_> = seen
.iter()
.filter(|r| r.method == http::Method::DELETE)
.collect();
assert_eq!(deletes.len(), 2, "exactly one DELETE per child: {seen:?}");
assert!(
deletes
.iter()
.any(|r| r.path.ends_with("/serviceaccounts/greeting-trigger")),
"deletes the per-routine trigger ServiceAccount: {seen:?}"
);
assert!(
deletes
.iter()
.any(|r| r.path.ends_with("/cronjobs/greeting")),
"deletes the routine's own-named CronJob: {seen:?}"
);
assert!(
seen.iter()
.all(|r| r.method != http::Method::PATCH || r.path.ends_with("/status")),
"the only non-DELETE call is the status patch, never an apply of either child: {seen:?}"
);
}
#[tokio::test]
async fn reconcile_refuses_to_delete_foreign_same_named_legacy_children() {
let requests = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
let client = fake_routines::client(requests.clone(), "prior-routine-uid");
let ctx = std::sync::Arc::new(Context { client });
reconcile(std::sync::Arc::new(routine(valid_spec())), ctx)
.await
.expect("foreign legacy children are skipped");
let seen = requests.lock().expect("lock");
assert!(
seen.iter()
.all(|request| request.method != http::Method::DELETE),
"a current Routine must not delete a prior or foreign owner's same-named resource: {seen:?}"
);
}
#[test]
fn legacy_delete_is_pinned_to_the_observed_owned_incarnation() {
let mut routine = routine(valid_spec());
routine.metadata.uid = Some("routine-uid".to_owned());
let mut child = k8s_openapi::api::batch::v1::CronJob::default();
child.metadata.name = Some("greeting".to_owned());
child.metadata.uid = Some("cron-uid".to_owned());
child.metadata.resource_version = Some("cron-rv".to_owned());
child.metadata.owner_references = routine.controller_owner_ref(&()).map(|owner| vec![owner]);
let params = legacy_delete_params(&routine, &child).expect("owned live legacy child");
let preconditions = params.preconditions.expect("delete preconditions");
assert_eq!(preconditions.uid.as_deref(), Some("cron-uid"));
assert_eq!(preconditions.resource_version.as_deref(), Some("cron-rv"));
routine.metadata.uid = Some("replacement-routine".to_owned());
assert!(
legacy_delete_params(&routine, &child).is_none(),
"a recreated routine must not delete the prior incarnation's child"
);
}