1use aidens_contracts::{ArtifactId, CanonicalToolSideEffectClass, WakeSignalV1};
4use thiserror::Error;
5
6#[derive(Debug, Error)]
7pub enum WakeError {
8 #[error("wake source must not be empty")]
9 EmptySource,
10 #[error("wake signal key must not be empty")]
11 EmptySignalKey,
12}
13
14pub fn wake_signal(
15 namespace_id: ArtifactId,
16 source: impl Into<String>,
17 signal_key: impl Into<String>,
18 payload: serde_json::Value,
19 risk: CanonicalToolSideEffectClass,
20) -> Result<WakeSignalV1, WakeError> {
21 let source = source.into();
22 let signal_key = signal_key.into();
23 if source.trim().is_empty() {
24 return Err(WakeError::EmptySource);
25 }
26 if signal_key.trim().is_empty() {
27 return Err(WakeError::EmptySignalKey);
28 }
29 Ok(WakeSignalV1::new(
30 namespace_id,
31 source,
32 signal_key,
33 payload,
34 risk,
35 ))
36}
37
38#[cfg(test)]
39mod tests {
40 use super::*;
41 use aidens_contracts::DaemonNamespaceV1;
42
43 #[test]
44 fn wake_signal_identity_uses_source_key_and_payload() {
45 let ns = DaemonNamespaceV1::new("wake-test", "target/p11", "daemon");
46 let signal = wake_signal(
47 ns.namespace_id,
48 "filesystem",
49 "README.md:modified",
50 serde_json::json!({"path":"README.md"}),
51 CanonicalToolSideEffectClass::ReadOnly,
52 )
53 .unwrap();
54 assert!(signal.idempotency_key.contains("filesystem"));
55 assert!(signal.idempotency_key.contains("README.md:modified"));
56 assert!(signal.idempotency_key.contains(&signal.payload_digest));
57 }
58
59 #[test]
60 fn empty_signal_key_is_rejected() {
61 let ns = DaemonNamespaceV1::new("wake-test-empty", "target/p11", "daemon");
62 let err = wake_signal(
63 ns.namespace_id,
64 "filesystem",
65 "",
66 serde_json::json!({}),
67 CanonicalToolSideEffectClass::ReadOnly,
68 )
69 .unwrap_err();
70 assert!(matches!(err, WakeError::EmptySignalKey));
71 }
72}