Files
MyAiDesk/nomifun-tauri/crates/backend/nomifun-idmm/src/events.rs
T
freedak f7a720204a Update: 将子项目从 submodule 转为完整内容
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用
- 添加所有子项目的完整源代码
- 保留原始 .git 为 .git.bak 备份
2026-07-04 19:20:46 +08:00

110 lines
3.6 KiB
Rust

//! WebSocket event emission for IDMM. Mirrors `nomifun_requirement::events`.
//! Event names follow the `domain.camelCaseAction` convention.
use std::sync::Arc;
use nomifun_api_types::{IdmmState, InterventionRecord, WebSocketMessage};
use nomifun_realtime::EventBroadcaster;
use tracing::error;
/// Emits IDMM status + intervention events through the shared broadcaster.
#[derive(Clone)]
pub struct IdmmEventEmitter {
broadcaster: Arc<dyn EventBroadcaster>,
}
impl IdmmEventEmitter {
pub fn new(broadcaster: Arc<dyn EventBroadcaster>) -> Self {
Self { broadcaster }
}
/// `idmm.statusChanged` — armed/disabled/intervening transitions.
pub fn emit_status_changed(&self, state: &IdmmState) {
self.broadcast("idmm.statusChanged", state);
}
/// `idmm.intervention` — one intervention happened (detected → action → outcome).
pub fn emit_intervention(&self, record: &InterventionRecord) {
self.broadcast("idmm.intervention", record);
}
fn broadcast<T: serde::Serialize>(&self, event_name: &str, payload: &T) {
let value = match serde_json::to_value(payload) {
Ok(v) => v,
Err(e) => {
error!(event = event_name, error = %e, "IDMM event serialize failed");
return;
}
};
self.broadcaster.broadcast(WebSocketMessage::new(event_name, value));
}
}
#[cfg(test)]
mod tests {
use super::*;
use nomifun_api_types::{IdmmRunState, IdmmTargetKind};
use std::sync::Mutex;
#[derive(Default)]
struct CapturingBroadcaster {
events: Mutex<Vec<WebSocketMessage<serde_json::Value>>>,
}
impl EventBroadcaster for CapturingBroadcaster {
fn broadcast(&self, event: WebSocketMessage<serde_json::Value>) {
self.events.lock().unwrap().push(event);
}
}
#[test]
fn emits_status_changed_with_payload() {
let bc = Arc::new(CapturingBroadcaster::default());
let emitter = IdmmEventEmitter::new(bc.clone());
let st = IdmmState {
kind: IdmmTargetKind::Conversation,
target_id: "c1".into(),
enabled: true,
fault_enabled: false,
decision_enabled: true,
run_state: IdmmRunState::Armed,
interventions_count: 0,
last_signal: None,
last_intervention_at: None,
sidecar_provider_resolved: false,
config: None,
};
emitter.emit_status_changed(&st);
let evs = bc.events.lock().unwrap();
assert_eq!(evs.len(), 1);
assert_eq!(evs[0].name, "idmm.statusChanged");
assert_eq!(evs[0].data["target_id"], "c1");
assert_eq!(evs[0].data["run_state"], "armed");
}
#[test]
fn emits_intervention_with_payload() {
let bc = Arc::new(CapturingBroadcaster::default());
let emitter = IdmmEventEmitter::new(bc.clone());
let rec = InterventionRecord {
id: "idmmrec_x".into(),
target_kind: "conversation".into(),
target_id: "t1".into(),
watch: "fault".into(),
at: 123,
stall_class: "provider_error".into(),
tier_used: "rule".into(),
category: None,
action: "retry".into(),
detail: None,
outcome: "applied".into(),
reason: Some("transient 500".into()),
confidence: None,
bypass_model: None,
};
emitter.emit_intervention(&rec);
let evs = bc.events.lock().unwrap();
assert_eq!(evs[0].name, "idmm.intervention");
assert_eq!(evs[0].data["action"], "retry");
}
}