1#![forbid(unsafe_code)]
14
15use async_trait::async_trait;
16
17use std::sync::{Arc, Mutex};
18
19use serde_json::{Value, json};
20use wm_cognitive::{ReflexArgs, ReflexDispatchTable, ReflexId};
21use wm_cognitive::{Tier, TimescaleBus};
22use wm_core::{Context, EffectRow, Gana, Tool, ToolStats};
23use wm_workspace::{CoreId, EventType, GlobalWorkspace, Salience};
24
25fn parse_core_id(s: &str) -> Result<CoreId, wm_core::CoreError> {
28 match s.to_lowercase().as_str() {
29 "citta" => Ok(CoreId::Citta),
30 "dream" => Ok(CoreId::Dream),
31 "brain_wave" | "brainwave" => Ok(CoreId::BrainWave),
32 "autonomous" => Ok(CoreId::Autonomous),
33 "dispatch" => Ok(CoreId::Dispatch),
34 "reflex" => Ok(CoreId::Reflex),
35 "self_model" | "selfmodel" => Ok(CoreId::SelfModel),
36 "drive" => Ok(CoreId::Drive),
37 "homeostasis" => Ok(CoreId::Homeostasis),
38 "sensor" => Ok(CoreId::Sensor),
39 _ => {
40 if let Some(rest) = s.strip_prefix("custom_") {
41 rest.parse::<u16>().map(CoreId::Custom).map_err(|_| {
42 wm_core::CoreError::InvalidArgs(format!("invalid custom core ID: {s}"))
43 })
44 } else {
45 Err(wm_core::CoreError::InvalidArgs(format!(
46 "unknown core ID: {s}"
47 )))
48 }
49 }
50 }
51}
52
53fn parse_event_type(s: &str) -> Result<EventType, wm_core::CoreError> {
54 match s.to_lowercase().as_str() {
55 "error" => Ok(EventType::Error),
56 "reward" => Ok(EventType::Reward),
57 "attention_request" | "attention" => Ok(EventType::AttentionRequest),
58 "novel_detection" | "novel" => Ok(EventType::NovelDetection),
59 "threshold_crossing" | "threshold" => Ok(EventType::ThresholdCrossing),
60 "drive_update" | "drive" => Ok(EventType::DriveUpdate),
61 "safety_alert" | "safety" => Ok(EventType::SafetyAlert),
62 _ => Err(wm_core::CoreError::InvalidArgs(format!(
63 "unknown event type: {s}"
64 ))),
65 }
66}
67
68fn parse_tier(s: &str) -> Result<Tier, wm_core::CoreError> {
69 match s.to_lowercase().as_str() {
70 "reflex" | "0" => Ok(Tier::Reflex),
71 "reactive" | "1" => Ok(Tier::Reactive),
72 "planning" | "2" => Ok(Tier::Planning),
73 "consolidation" | "3" => Ok(Tier::Consolidation),
74 "evolutionary" | "4" => Ok(Tier::Evolutionary),
75 _ => Err(wm_core::CoreError::InvalidArgs(format!(
76 "unknown tier: {s}"
77 ))),
78 }
79}
80
81const fn command_name(cmd: wm_cognitive::ReflexCommand) -> &'static str {
82 match cmd {
83 wm_cognitive::ReflexCommand::EmergencyStop => "emergency_stop",
84 wm_cognitive::ReflexCommand::ReducePower => "reduce_power",
85 wm_cognitive::ReflexCommand::ApplyCorrection => "apply_correction",
86 wm_cognitive::ReflexCommand::IssueAlert => "issue_alert",
87 wm_cognitive::ReflexCommand::Drop => "drop",
88 wm_cognitive::ReflexCommand::NoOp => "noop",
89 wm_cognitive::ReflexCommand::Custom => "custom",
90 }
91}
92
93pub struct ReflexDispatchTool {
96 table: Arc<Mutex<ReflexDispatchTable>>,
97 stats: ToolStats,
98 effects: EffectRow,
99}
100
101impl ReflexDispatchTool {
102 pub fn new(table: Arc<Mutex<ReflexDispatchTable>>) -> Self {
103 Self {
104 table,
105 stats: ToolStats::default(),
106 effects: EffectRow::pure(),
107 }
108 }
109}
110
111#[async_trait]
112impl Tool for ReflexDispatchTool {
113 fn name(&self) -> &str {
114 "reflex.dispatch"
115 }
116 fn gana(&self) -> Gana {
117 Gana::Heart
118 }
119 fn effects(&self) -> &EffectRow {
120 &self.effects
121 }
122 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
123 let reflex_id: ReflexId = args
124 .get("reflex_id")
125 .and_then(serde_json::Value::as_u64)
126 .ok_or_else(|| {
127 wm_core::CoreError::InvalidArgs("reflex_id (number 0-255) required".into())
128 })? as ReflexId;
129
130 let sensor_id = args
131 .get("sensor_id")
132 .and_then(serde_json::Value::as_u64)
133 .unwrap_or(0) as u16;
134
135 let timestamp_ns = args
136 .get("timestamp_ns")
137 .and_then(serde_json::Value::as_u64)
138 .unwrap_or(0);
139
140 let payload_hex = args
141 .get("payload")
142 .and_then(serde_json::Value::as_str)
143 .unwrap_or("");
144
145 let mut reflex_args = ReflexArgs::new(sensor_id, timestamp_ns);
146 if !payload_hex.is_empty() {
147 let payload = hex_decode(payload_hex)?;
148 reflex_args.set_payload(&payload).map_err(|e| {
149 wm_core::CoreError::InvalidArgs(format!("reflex payload error: {e}"))
150 })?;
151 }
152
153 let mut table = self
154 .table
155 .lock()
156 .map_err(|e| wm_core::CoreError::Governance(format!("reflex table lock error: {e}")))?;
157 let result = table
158 .dispatch(reflex_id, &reflex_args)
159 .map_err(|e| wm_core::CoreError::Tool(format!("reflex dispatch error: {e}")))?;
160
161 Ok(json!({
162 "reflex_id": reflex_id,
163 "actuator_id": result.actuator_id,
164 "command": command_name(result.command),
165 "priority": result.priority,
166 "payload": hex_encode(result.payload()),
167 "dispatch_count": table.dispatch_count(),
168 }))
169 }
170 fn stats(&self) -> &ToolStats {
171 &self.stats
172 }
173}
174
175pub struct ReflexStatusTool {
178 table: Arc<Mutex<ReflexDispatchTable>>,
179 stats: ToolStats,
180 effects: EffectRow,
181}
182
183impl ReflexStatusTool {
184 pub fn new(table: Arc<Mutex<ReflexDispatchTable>>) -> Self {
185 Self {
186 table,
187 stats: ToolStats::default(),
188 effects: EffectRow::pure(),
189 }
190 }
191}
192
193#[async_trait]
194impl Tool for ReflexStatusTool {
195 fn name(&self) -> &str {
196 "reflex.status"
197 }
198 fn gana(&self) -> Gana {
199 Gana::Heart
200 }
201 fn effects(&self) -> &EffectRow {
202 &self.effects
203 }
204 async fn call(&self, _ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
205 let table = self
206 .table
207 .lock()
208 .map_err(|e| wm_core::CoreError::Governance(format!("reflex table lock error: {e}")))?;
209 let registered_handlers = table.registered_count();
210 let safety_mask = format!("{:#010x}", table.safety_mask());
211 let dispatch_count = table.dispatch_count();
212 let builtins: Vec<Value> = wm_cognitive::reflex::builtins::BUILTINS
213 .iter()
214 .map(|b| {
215 json!({
216 "id": b.id,
217 "name": b.name,
218 "registered": table.is_registered(b.id),
219 })
220 })
221 .collect();
222 drop(table);
223 Ok(json!({
224 "registered_handlers": registered_handlers,
225 "safety_mask": safety_mask,
226 "dispatch_count": dispatch_count,
227 "builtins": builtins,
228 }))
229 }
230 fn stats(&self) -> &ToolStats {
231 &self.stats
232 }
233}
234
235pub struct WorkspaceSpotlightTool {
238 workspace: Arc<Mutex<GlobalWorkspace>>,
239 stats: ToolStats,
240 effects: EffectRow,
241}
242
243impl WorkspaceSpotlightTool {
244 pub fn new(workspace: Arc<Mutex<GlobalWorkspace>>) -> Self {
245 Self {
246 workspace,
247 stats: ToolStats::default(),
248 effects: EffectRow::pure(),
249 }
250 }
251}
252
253#[async_trait]
254impl Tool for WorkspaceSpotlightTool {
255 fn name(&self) -> &str {
256 "workspace.spotlight"
257 }
258 fn gana(&self) -> Gana {
259 Gana::Ghost
260 }
261 fn effects(&self) -> &EffectRow {
262 &self.effects
263 }
264 async fn call(&self, _ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
265 let ws = self
266 .workspace
267 .lock()
268 .map_err(|e| wm_core::CoreError::Governance(format!("workspace lock error: {e}")))?;
269 match ws.spotlight() {
270 Some(entry) => Ok(json!({
271 "core": entry.core.to_string(),
272 "event_type": entry.winning_event_type.to_string(),
273 "salience": {
274 "urgency": entry.salience.urgency,
275 "novelty": entry.salience.novelty,
276 "confidence": entry.salience.confidence,
277 "composite": entry.salience.composite(),
278 },
279 "strength": ws.spotlight_strength(),
280 "age_ms": entry.age().as_millis(),
281 "candidates": entry.candidates,
282 "transfers": ws.spotlight_transfers(),
283 "arbitration_cycles": ws.arbitration_cycles(),
284 })),
285 None => Ok(json!({
286 "spotlight": null,
287 "transfers": ws.spotlight_transfers(),
288 "arbitration_cycles": ws.arbitration_cycles(),
289 })),
290 }
291 }
292 fn stats(&self) -> &ToolStats {
293 &self.stats
294 }
295}
296
297pub struct WorkspaceEventsTool {
300 workspace: Arc<Mutex<GlobalWorkspace>>,
301 stats: ToolStats,
302 effects: EffectRow,
303}
304
305impl WorkspaceEventsTool {
306 pub fn new(workspace: Arc<Mutex<GlobalWorkspace>>) -> Self {
307 Self {
308 workspace,
309 stats: ToolStats::default(),
310 effects: EffectRow::pure(),
311 }
312 }
313}
314
315#[async_trait]
316impl Tool for WorkspaceEventsTool {
317 fn name(&self) -> &str {
318 "workspace.events"
319 }
320 fn gana(&self) -> Gana {
321 Gana::Ghost
322 }
323 fn effects(&self) -> &EffectRow {
324 &self.effects
325 }
326 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
327 let count = args
328 .get("count")
329 .and_then(serde_json::Value::as_u64)
330 .unwrap_or(10) as usize;
331 let count = count.min(256);
332
333 let ws = self
334 .workspace
335 .lock()
336 .map_err(|e| wm_core::CoreError::Governance(format!("workspace lock error: {e}")))?;
337 let events: Vec<Value> = ws
338 .recent_events(count)
339 .iter()
340 .map(|e| {
341 json!({
342 "core": e.core.to_string(),
343 "event_type": e.event_type.to_string(),
344 "salience": {
345 "urgency": e.salience.urgency,
346 "novelty": e.salience.novelty,
347 "confidence": e.salience.confidence,
348 "composite": e.composite_salience(),
349 },
350 "payload": e.payload,
351 "age_ms": e.age().as_millis(),
352 })
353 })
354 .collect();
355
356 Ok(json!({
357 "events": events,
358 "total_published": ws.events_published(),
359 "backlog_len": ws.backlog().len(),
360 }))
361 }
362 fn stats(&self) -> &ToolStats {
363 &self.stats
364 }
365}
366
367pub struct WorkspacePublishTool {
370 workspace: Arc<Mutex<GlobalWorkspace>>,
371 stats: ToolStats,
372 effects: EffectRow,
373}
374
375impl WorkspacePublishTool {
376 pub fn new(workspace: Arc<Mutex<GlobalWorkspace>>) -> Self {
377 Self {
378 workspace,
379 stats: ToolStats::default(),
380 effects: EffectRow::pure(),
381 }
382 }
383}
384
385#[async_trait]
386impl Tool for WorkspacePublishTool {
387 fn name(&self) -> &str {
388 "workspace.publish"
389 }
390 fn gana(&self) -> Gana {
391 Gana::Ghost
392 }
393 fn effects(&self) -> &EffectRow {
394 &self.effects
395 }
396 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
397 let core_str = args
398 .get("core")
399 .and_then(serde_json::Value::as_str)
400 .ok_or_else(|| wm_core::CoreError::InvalidArgs("core (string) required".into()))?;
401 let core = parse_core_id(core_str)?;
402
403 let event_type_str = args
404 .get("event_type")
405 .and_then(serde_json::Value::as_str)
406 .ok_or_else(|| {
407 wm_core::CoreError::InvalidArgs("event_type (string) required".into())
408 })?;
409 let event_type = parse_event_type(event_type_str)?;
410
411 let urgency = args
412 .get("urgency")
413 .and_then(serde_json::Value::as_f64)
414 .unwrap_or(0.5) as f32;
415 let novelty = args
416 .get("novelty")
417 .and_then(serde_json::Value::as_f64)
418 .unwrap_or(0.5) as f32;
419 let confidence = args
420 .get("confidence")
421 .and_then(serde_json::Value::as_f64)
422 .unwrap_or(0.5) as f32;
423 let payload = args.get("payload").cloned().unwrap_or_else(|| json!({}));
424
425 let event = wm_workspace::WorkspaceEvent::new(
426 core,
427 event_type,
428 Salience::new(urgency, novelty, confidence),
429 payload,
430 );
431
432 let mut ws = self
433 .workspace
434 .lock()
435 .map_err(|e| wm_core::CoreError::Governance(format!("workspace lock error: {e}")))?;
436 let won = ws.publish(&event);
437
438 Ok(json!({
439 "won_spotlight": won,
440 "spotlight_core": ws.spotlight_core().map(|c| c.to_string()),
441 "spotlight_strength": ws.spotlight_strength(),
442 "events_published": ws.events_published(),
443 }))
444 }
445 fn stats(&self) -> &ToolStats {
446 &self.stats
447 }
448}
449
450pub struct WorkspaceStatsTool {
453 workspace: Arc<Mutex<GlobalWorkspace>>,
454 stats: ToolStats,
455 effects: EffectRow,
456}
457
458impl WorkspaceStatsTool {
459 pub fn new(workspace: Arc<Mutex<GlobalWorkspace>>) -> Self {
460 Self {
461 workspace,
462 stats: ToolStats::default(),
463 effects: EffectRow::pure(),
464 }
465 }
466}
467
468#[async_trait]
469impl Tool for WorkspaceStatsTool {
470 fn name(&self) -> &str {
471 "workspace.stats"
472 }
473 fn gana(&self) -> Gana {
474 Gana::Ghost
475 }
476 fn effects(&self) -> &EffectRow {
477 &self.effects
478 }
479 async fn call(&self, _ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
480 let ws = self
481 .workspace
482 .lock()
483 .map_err(|e| wm_core::CoreError::Governance(format!("workspace lock error: {e}")))?;
484 let stats = ws.stats();
485 let events_published = stats.events_published;
486 let spotlight_transfers = stats.spotlight_transfers;
487 let arbitration_cycles = stats.arbitration_cycles;
488 let events_per_core: serde_json::Map<_, _> = stats
489 .events_per_core
490 .iter()
491 .map(|(c, n)| (c.to_string(), json!(n)))
492 .collect();
493 let events_per_type: serde_json::Map<_, _> = stats
494 .events_per_type
495 .iter()
496 .map(|(t, n)| (t.to_string(), json!(n)))
497 .collect();
498 drop(ws);
499 Ok(json!({
500 "events_published": events_published,
501 "spotlight_transfers": spotlight_transfers,
502 "arbitration_cycles": arbitration_cycles,
503 "events_per_core": events_per_core,
504 "events_per_type": events_per_type,
505 }))
506 }
507 fn stats(&self) -> &ToolStats {
508 &self.stats
509 }
510}
511
512pub struct TimescaleStatusTool {
515 bus: Arc<Mutex<TimescaleBus>>,
516 stats: ToolStats,
517 effects: EffectRow,
518}
519
520impl TimescaleStatusTool {
521 pub fn new(bus: Arc<Mutex<TimescaleBus>>) -> Self {
522 Self {
523 bus,
524 stats: ToolStats::default(),
525 effects: EffectRow::pure(),
526 }
527 }
528}
529
530#[async_trait]
531impl Tool for TimescaleStatusTool {
532 fn name(&self) -> &str {
533 "timescale.status"
534 }
535 fn gana(&self) -> Gana {
536 Gana::Dipper
537 }
538 fn effects(&self) -> &EffectRow {
539 &self.effects
540 }
541 async fn call(&self, _ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
542 let bus = self.bus.lock().map_err(|e| {
543 wm_core::CoreError::Governance(format!("timescale bus lock error: {e}"))
544 })?;
545 let tiers: Vec<Value> = Tier::all()
546 .iter()
547 .map(|t| {
548 let config = bus.tier_config(*t);
549 json!({
550 "name": t.name(),
551 "index": t.index(),
552 "active": bus.is_tier_active(*t),
553 "hook_count": bus.hook_count(*t),
554 "interval_ms": config.interval.as_millis(),
555 "budget_ms": config.budget.as_millis(),
556 })
557 })
558 .collect();
559 let brain_wave = format!("{:?}", bus.brain_wave());
560 let total_hooks = bus.total_hook_count();
561 let total_ticks = bus.total_ticks();
562 let total_timeouts = bus.total_timeouts();
563 let active_tiers: Vec<_> = bus.active_tiers().iter().map(|t| t.name()).collect();
564 let inactive_tiers: Vec<_> = bus.inactive_tiers().iter().map(|t| t.name()).collect();
565 drop(bus);
566 Ok(json!({
567 "brain_wave": brain_wave,
568 "total_hooks": total_hooks,
569 "total_ticks": total_ticks,
570 "total_timeouts": total_timeouts,
571 "active_tiers": active_tiers,
572 "inactive_tiers": inactive_tiers,
573 "tiers": tiers,
574 }))
575 }
576 fn stats(&self) -> &ToolStats {
577 &self.stats
578 }
579}
580
581pub struct TimescaleHooksTool {
584 bus: Arc<Mutex<TimescaleBus>>,
585 stats: ToolStats,
586 effects: EffectRow,
587}
588
589impl TimescaleHooksTool {
590 pub fn new(bus: Arc<Mutex<TimescaleBus>>) -> Self {
591 Self {
592 bus,
593 stats: ToolStats::default(),
594 effects: EffectRow::pure(),
595 }
596 }
597}
598
599#[async_trait]
600impl Tool for TimescaleHooksTool {
601 fn name(&self) -> &str {
602 "timescale.hooks"
603 }
604 fn gana(&self) -> Gana {
605 Gana::Dipper
606 }
607 fn effects(&self) -> &EffectRow {
608 &self.effects
609 }
610 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
611 let tier_str = args
612 .get("tier")
613 .and_then(serde_json::Value::as_str)
614 .unwrap_or("reflex");
615 let tier = parse_tier(tier_str)?;
616
617 let bus = self.bus.lock().map_err(|e| {
618 wm_core::CoreError::Governance(format!("timescale bus lock error: {e}"))
619 })?;
620 let hooks: Vec<Value> = bus
621 .tier_stats(tier)
622 .iter()
623 .map(|(id, name, snap)| {
624 json!({
625 "id": id,
626 "name": name,
627 "tick_count": snap.tick_count,
628 "success_count": snap.success_count,
629 "timeout_count": snap.timeout_count,
630 "error_count": snap.error_count,
631 "last_duration_us": snap.last_duration_us,
632 "avg_duration_us": snap.avg_duration_us,
633 })
634 })
635 .collect();
636 let active = bus.is_tier_active(tier);
637 let hook_count = bus.hook_count(tier);
638 drop(bus);
639 Ok(json!({
640 "tier": tier.name(),
641 "active": active,
642 "hook_count": hook_count,
643 "hooks": hooks,
644 }))
645 }
646 fn stats(&self) -> &ToolStats {
647 &self.stats
648 }
649}
650
651fn hex_encode(data: &[u8]) -> String {
654 use std::fmt::Write;
655 let mut s = String::with_capacity(data.len() * 2);
656 for b in data {
657 let _ = write!(s, "{b:02x}");
658 }
659 s
660}
661
662fn hex_decode(s: &str) -> Result<Vec<u8>, wm_core::CoreError> {
663 if s.len() % 2 != 0 {
664 return Err(wm_core::CoreError::InvalidArgs(
665 "payload hex must have even length".into(),
666 ));
667 }
668 (0..s.len())
669 .step_by(2)
670 .map(|i| {
671 u8::from_str_radix(&s[i..i + 2], 16)
672 .map_err(|e| wm_core::CoreError::InvalidArgs(format!("invalid hex: {e}")))
673 })
674 .collect()
675}
676
677pub fn register_v4(
681 registry: &wm_dispatch::ToolRegistry,
682 reflex_table: Arc<Mutex<ReflexDispatchTable>>,
683 timescale_bus: Arc<Mutex<TimescaleBus>>,
684 workspace: Arc<Mutex<GlobalWorkspace>>,
685) -> wm_dispatch::ToolRegistry {
686 registry
687 .register(Arc::new(ReflexDispatchTool::new(Arc::clone(&reflex_table))))
688 .register(Arc::new(ReflexStatusTool::new(reflex_table)))
689 .register(Arc::new(WorkspaceSpotlightTool::new(Arc::clone(
690 &workspace,
691 ))))
692 .register(Arc::new(WorkspaceEventsTool::new(Arc::clone(&workspace))))
693 .register(Arc::new(WorkspacePublishTool::new(Arc::clone(&workspace))))
694 .register(Arc::new(WorkspaceStatsTool::new(workspace)))
695 .register(Arc::new(TimescaleStatusTool::new(Arc::clone(
696 ×cale_bus,
697 ))))
698 .register(Arc::new(TimescaleHooksTool::new(timescale_bus)))
699}
700
701#[cfg(test)]
704mod tests {
705 use super::*;
706 use wm_cognitive::reflex::{ReflexDispatchTable, builtins};
707 use wm_workspace::GlobalWorkspace;
708
709 fn test_reflex_table() -> Arc<Mutex<ReflexDispatchTable>> {
710 let mut table = ReflexDispatchTable::permissive();
711 builtins::register_builtins(&mut table);
712 Arc::new(Mutex::new(table))
713 }
714
715 fn test_workspace() -> Arc<Mutex<GlobalWorkspace>> {
716 Arc::new(Mutex::new(GlobalWorkspace::new()))
717 }
718
719 fn test_timescale_bus() -> Arc<Mutex<TimescaleBus>> {
720 Arc::new(Mutex::new(TimescaleBus::default()))
721 }
722
723 #[tokio::test]
724 async fn reflex_dispatch_e_stop() {
725 let table = test_reflex_table();
726 let tool = ReflexDispatchTool::new(Arc::clone(&table));
727 let mut ctx = Context::new(wm_core::BrainWave::Gamma);
728 let result = tool.call(&mut ctx, json!({"reflex_id": 0})).await.unwrap();
729 assert_eq!(result["command"], "emergency_stop");
730 assert_eq!(result["priority"], 255);
731 }
732
733 #[tokio::test]
734 async fn reflex_dispatch_with_payload() {
735 let table = test_reflex_table();
736 let tool = ReflexDispatchTool::new(Arc::clone(&table));
737 let mut ctx = Context::new(wm_core::BrainWave::Gamma);
738 let result = tool
739 .call(&mut ctx, json!({"reflex_id": 0, "payload": "deadbeef"}))
740 .await
741 .unwrap();
742 assert_eq!(result["command"], "emergency_stop");
743 }
744
745 #[tokio::test]
746 async fn reflex_dispatch_not_registered() {
747 let table = test_reflex_table();
748 let tool = ReflexDispatchTool::new(Arc::clone(&table));
749 let mut ctx = Context::new(wm_core::BrainWave::Gamma);
750 let err = tool
751 .call(&mut ctx, json!({"reflex_id": 200}))
752 .await
753 .unwrap_err();
754 assert!(err.to_string().contains("no handler registered"));
755 }
756
757 #[tokio::test]
758 async fn reflex_dispatch_missing_id() {
759 let table = test_reflex_table();
760 let tool = ReflexDispatchTool::new(Arc::clone(&table));
761 let mut ctx = Context::new(wm_core::BrainWave::Gamma);
762 let err = tool.call(&mut ctx, json!({})).await.unwrap_err();
763 assert!(err.to_string().contains("reflex_id"));
764 }
765
766 #[tokio::test]
767 async fn reflex_status_shows_builtins() {
768 let table = test_reflex_table();
769 let tool = ReflexStatusTool::new(Arc::clone(&table));
770 let mut ctx = Context::new(wm_core::BrainWave::Gamma);
771 let result = tool.call(&mut ctx, json!({})).await.unwrap();
772 assert_eq!(result["registered_handlers"], 8);
773 assert_eq!(result["dispatch_count"].as_u64().unwrap(), 0);
774 }
775
776 #[tokio::test]
777 async fn workspace_spotlight_empty() {
778 let ws = test_workspace();
779 let tool = WorkspaceSpotlightTool::new(Arc::clone(&ws));
780 let mut ctx = Context::new(wm_core::BrainWave::Gamma);
781 let result = tool.call(&mut ctx, json!({})).await.unwrap();
782 assert!(result["spotlight"].is_null());
783 }
784
785 #[tokio::test]
786 async fn workspace_spotlight_after_publish() {
787 let ws = test_workspace();
788 let pub_tool = WorkspacePublishTool::new(Arc::clone(&ws));
789 let spot_tool = WorkspaceSpotlightTool::new(Arc::clone(&ws));
790 let mut ctx = Context::new(wm_core::BrainWave::Gamma);
791
792 pub_tool
793 .call(
794 &mut ctx,
795 json!({
796 "core": "reflex",
797 "event_type": "safety_alert",
798 "urgency": 0.9,
799 "novelty": 0.8,
800 "confidence": 0.95,
801 }),
802 )
803 .await
804 .unwrap();
805
806 let result = spot_tool.call(&mut ctx, json!({})).await.unwrap();
807 assert_eq!(result["core"], "reflex");
808 assert_eq!(result["event_type"], "safety_alert");
809 }
810
811 #[tokio::test]
812 async fn workspace_publish_wins_spotlight() {
813 let ws = test_workspace();
814 let tool = WorkspacePublishTool::new(Arc::clone(&ws));
815 let mut ctx = Context::new(wm_core::BrainWave::Gamma);
816 let result = tool
817 .call(
818 &mut ctx,
819 json!({
820 "core": "citta",
821 "event_type": "attention_request",
822 "urgency": 0.7,
823 "novelty": 0.6,
824 "confidence": 0.8,
825 }),
826 )
827 .await
828 .unwrap();
829 assert_eq!(result["won_spotlight"], true);
830 assert_eq!(result["spotlight_core"], "citta");
831 }
832
833 #[tokio::test]
834 async fn workspace_publish_missing_core() {
835 let ws = test_workspace();
836 let tool = WorkspacePublishTool::new(Arc::clone(&ws));
837 let mut ctx = Context::new(wm_core::BrainWave::Gamma);
838 let err = tool
839 .call(&mut ctx, json!({"event_type": "error"}))
840 .await
841 .unwrap_err();
842 assert!(err.to_string().contains("core"));
843 }
844
845 #[tokio::test]
846 async fn workspace_publish_invalid_core() {
847 let ws = test_workspace();
848 let tool = WorkspacePublishTool::new(Arc::clone(&ws));
849 let mut ctx = Context::new(wm_core::BrainWave::Gamma);
850 let err = tool
851 .call(
852 &mut ctx,
853 json!({"core": "nonexistent", "event_type": "error"}),
854 )
855 .await
856 .unwrap_err();
857 assert!(err.to_string().contains("unknown core"));
858 }
859
860 #[tokio::test]
861 async fn workspace_events_after_publish() {
862 let ws = test_workspace();
863 let pub_tool = WorkspacePublishTool::new(Arc::clone(&ws));
864 let events_tool = WorkspaceEventsTool::new(Arc::clone(&ws));
865 let mut ctx = Context::new(wm_core::BrainWave::Gamma);
866
867 for i in 0..3 {
868 pub_tool
869 .call(
870 &mut ctx,
871 json!({
872 "core": "citta",
873 "event_type": "attention_request",
874 "urgency": f64::from(i).mul_add(0.1, 0.5),
875 "novelty": 0.5,
876 "confidence": 0.5,
877 }),
878 )
879 .await
880 .unwrap();
881 }
882
883 let result = events_tool
884 .call(&mut ctx, json!({"count": 10}))
885 .await
886 .unwrap();
887 assert_eq!(result["total_published"], 3);
888 assert_eq!(result["events"].as_array().unwrap().len(), 3);
889 }
890
891 #[tokio::test]
892 async fn workspace_stats_shows_counts() {
893 let ws = test_workspace();
894 let pub_tool = WorkspacePublishTool::new(Arc::clone(&ws));
895 let stats_tool = WorkspaceStatsTool::new(Arc::clone(&ws));
896 let mut ctx = Context::new(wm_core::BrainWave::Gamma);
897
898 pub_tool
899 .call(
900 &mut ctx,
901 json!({
902 "core": "citta",
903 "event_type": "attention_request",
904 "urgency": 0.7,
905 "novelty": 0.5,
906 "confidence": 0.8,
907 }),
908 )
909 .await
910 .unwrap();
911
912 let result = stats_tool.call(&mut ctx, json!({})).await.unwrap();
913 assert_eq!(result["events_published"], 1);
914 assert_eq!(result["spotlight_transfers"], 1);
915 }
916
917 #[tokio::test]
918 async fn timescale_status_default() {
919 let bus = test_timescale_bus();
920 let tool = TimescaleStatusTool::new(Arc::clone(&bus));
921 let mut ctx = Context::new(wm_core::BrainWave::Gamma);
922 let result = tool.call(&mut ctx, json!({})).await.unwrap();
923 assert_eq!(result["brain_wave"], "Gamma");
924 assert_eq!(result["total_hooks"], 0);
925 let tiers = result["tiers"].as_array().unwrap();
926 assert_eq!(tiers.len(), 5);
927 }
928
929 #[tokio::test]
930 async fn timescale_hooks_empty_tier() {
931 let bus = test_timescale_bus();
932 let tool = TimescaleHooksTool::new(Arc::clone(&bus));
933 let mut ctx = Context::new(wm_core::BrainWave::Gamma);
934 let result = tool
935 .call(&mut ctx, json!({"tier": "reflex"}))
936 .await
937 .unwrap();
938 assert_eq!(result["tier"], "Reflex");
939 assert_eq!(result["hook_count"], 0);
940 }
941
942 #[tokio::test]
943 async fn timescale_hooks_invalid_tier() {
944 let bus = test_timescale_bus();
945 let tool = TimescaleHooksTool::new(Arc::clone(&bus));
946 let mut ctx = Context::new(wm_core::BrainWave::Gamma);
947 let err = tool
948 .call(&mut ctx, json!({"tier": "nonexistent"}))
949 .await
950 .unwrap_err();
951 assert!(err.to_string().contains("unknown tier"));
952 }
953
954 #[tokio::test]
955 async fn parse_core_id_all_builtins() {
956 assert_eq!(parse_core_id("citta").unwrap(), CoreId::Citta);
957 assert_eq!(parse_core_id("CITTA").unwrap(), CoreId::Citta);
958 assert_eq!(parse_core_id("dream").unwrap(), CoreId::Dream);
959 assert_eq!(parse_core_id("reflex").unwrap(), CoreId::Reflex);
960 assert_eq!(parse_core_id("brain_wave").unwrap(), CoreId::BrainWave);
961 assert_eq!(parse_core_id("brainwave").unwrap(), CoreId::BrainWave);
962 assert_eq!(parse_core_id("self_model").unwrap(), CoreId::SelfModel);
963 assert_eq!(parse_core_id("custom_42").unwrap(), CoreId::Custom(42));
964 }
965
966 #[tokio::test]
967 async fn parse_event_type_all_types() {
968 assert_eq!(parse_event_type("error").unwrap(), EventType::Error);
969 assert_eq!(parse_event_type("reward").unwrap(), EventType::Reward);
970 assert_eq!(
971 parse_event_type("attention_request").unwrap(),
972 EventType::AttentionRequest
973 );
974 assert_eq!(
975 parse_event_type("attention").unwrap(),
976 EventType::AttentionRequest
977 );
978 assert_eq!(
979 parse_event_type("novel_detection").unwrap(),
980 EventType::NovelDetection
981 );
982 assert_eq!(
983 parse_event_type("safety_alert").unwrap(),
984 EventType::SafetyAlert
985 );
986 }
987
988 #[tokio::test]
989 async fn parse_tier_all_tiers() {
990 assert_eq!(parse_tier("reflex").unwrap(), Tier::Reflex);
991 assert_eq!(parse_tier("reactive").unwrap(), Tier::Reactive);
992 assert_eq!(parse_tier("planning").unwrap(), Tier::Planning);
993 assert_eq!(parse_tier("consolidation").unwrap(), Tier::Consolidation);
994 assert_eq!(parse_tier("evolutionary").unwrap(), Tier::Evolutionary);
995 assert_eq!(parse_tier("0").unwrap(), Tier::Reflex);
996 assert_eq!(parse_tier("4").unwrap(), Tier::Evolutionary);
997 }
998
999 #[tokio::test]
1000 async fn hex_roundtrip() {
1001 let data = vec![0xde, 0xad, 0xbe, 0xef];
1002 let encoded = hex_encode(&data);
1003 assert_eq!(encoded, "deadbeef");
1004 let decoded = hex_decode(&encoded).unwrap();
1005 assert_eq!(decoded, data);
1006 }
1007
1008 #[tokio::test]
1009 async fn hex_decode_odd_length_fails() {
1010 assert!(hex_decode("abc").is_err());
1011 }
1012
1013 #[tokio::test]
1014 async fn register_v4_creates_8_tools() {
1015 let table = test_reflex_table();
1016 let bus = test_timescale_bus();
1017 let ws = test_workspace();
1018 let registry = wm_dispatch::ToolRegistry::new();
1019 let reg = register_v4(®istry, table, bus, ws);
1020 assert_eq!(reg.len(), 8);
1021 }
1022}