Skip to main content

aidens_schedule_kit/
lib.rs

1//! One-shot schedule occurrence construction for P11.
2
3use aidens_contracts::{ArtifactId, CanonicalToolSideEffectClass, ScheduleOccurrenceV1};
4use chrono::{DateTime, Utc};
5use thiserror::Error;
6
7#[derive(Debug, Error)]
8pub enum ScheduleError {
9    #[error("schedule id must not be empty")]
10    EmptyScheduleId,
11    #[error("occurrence key must not be empty")]
12    EmptyOccurrenceKey,
13}
14
15pub fn one_shot_occurrence(
16    namespace_id: ArtifactId,
17    schedule_id: impl Into<String>,
18    occurrence_key: impl Into<String>,
19    due_at: DateTime<Utc>,
20    payload: serde_json::Value,
21    risk: CanonicalToolSideEffectClass,
22) -> Result<ScheduleOccurrenceV1, ScheduleError> {
23    let schedule_id = schedule_id.into();
24    let occurrence_key = occurrence_key.into();
25    if schedule_id.trim().is_empty() {
26        return Err(ScheduleError::EmptyScheduleId);
27    }
28    if occurrence_key.trim().is_empty() {
29        return Err(ScheduleError::EmptyOccurrenceKey);
30    }
31    Ok(ScheduleOccurrenceV1::new(
32        namespace_id,
33        schedule_id,
34        occurrence_key,
35        due_at,
36        payload,
37        risk,
38    ))
39}
40
41#[cfg(test)]
42mod tests {
43    use super::*;
44    use aidens_contracts::DaemonNamespaceV1;
45
46    #[test]
47    fn occurrence_identity_includes_schedule_key_and_payload_digest() {
48        let ns = DaemonNamespaceV1::new("schedule-test", "target/p11", "daemon");
49        let occurrence = one_shot_occurrence(
50            ns.namespace_id,
51            "daily-refresh",
52            "2026-04-27T00:00:00Z",
53            Utc::now(),
54            serde_json::json!({"task":"refresh"}),
55            CanonicalToolSideEffectClass::ReadOnly,
56        )
57        .unwrap();
58        assert!(occurrence.identity_is_not_timestamp_only());
59        assert!(occurrence.idempotency_key.contains("daily-refresh"));
60        assert!(occurrence.idempotency_key.contains("2026-04-27T00:00:00Z"));
61    }
62
63    #[test]
64    fn empty_occurrence_key_is_rejected_before_recurring_logic_exists() {
65        let ns = DaemonNamespaceV1::new("schedule-test-empty", "target/p11", "daemon");
66        let err = one_shot_occurrence(
67            ns.namespace_id,
68            "daily-refresh",
69            "",
70            Utc::now(),
71            serde_json::json!({}),
72            CanonicalToolSideEffectClass::ReadOnly,
73        )
74        .unwrap_err();
75        assert!(matches!(err, ScheduleError::EmptyOccurrenceKey));
76    }
77}