aidens_schedule_kit/
lib.rs1use 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}