@delorenj/claude-notifications 1.2.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/DO.md +5 -0
  2. package/FIXES-APPLIED.md +195 -0
  3. package/INTEGRATION.md +445 -0
  4. package/LAYOUT-INTEGRATION.md +191 -0
  5. package/QUICK-REFERENCE.md +195 -0
  6. package/README.md +145 -14
  7. package/TASK.md +15 -0
  8. package/ZELLIJ-NOTIFY.md +523 -0
  9. package/_bmad-output/implementation-artifacts/spec-install-multi-cli-hooks.md +241 -0
  10. package/bin/claude-notifications.js +424 -305
  11. package/bin/claude-notify.js +47 -1
  12. package/bin/zellij-notify.js +346 -0
  13. package/bun.lock +35 -0
  14. package/diagnose-zellij.sh +105 -0
  15. package/examples/settings-with-zellij.json +18 -0
  16. package/examples/settings-zellij-only.json +18 -0
  17. package/examples/zellij-notify-examples.sh +143 -0
  18. package/lib/adapters/_stub.js +35 -0
  19. package/lib/adapters/auggie.js +10 -0
  20. package/lib/adapters/claude-code.js +181 -0
  21. package/lib/adapters/codex.js +10 -0
  22. package/lib/adapters/copilot.js +10 -0
  23. package/lib/adapters/gemini.js +10 -0
  24. package/lib/adapters/index.js +240 -0
  25. package/lib/adapters/kimi.js +10 -0
  26. package/lib/adapters/opencode.js +14 -0
  27. package/lib/adapters/vibe.js +10 -0
  28. package/lib/config.js +121 -5
  29. package/lib/tui.js +115 -0
  30. package/lib/zellij.js +248 -0
  31. package/package.json +6 -4
  32. package/postinstall.js +28 -25
  33. package/preuninstall.js +18 -9
  34. package/test/adapters/claude-code.test.js +144 -0
  35. package/test/adapters/patches.test.js +81 -0
  36. package/test/adapters/registry.test.js +89 -0
  37. package/test/adapters/stubs.test.js +46 -0
  38. package/test/cli-json.test.js +79 -0
  39. package/test/helpers/fake-fs.js +59 -0
  40. package/test-integration.sh +113 -0
  41. package/test-notification-plugin.kdl +34 -0
  42. package/test-updated-layout.sh +75 -0
  43. package/test-zellij-cli.sh +72 -0
  44. package/test.sh +1 -1
  45. package/zellij-plugin/.cargo/config.toml +5 -0
  46. package/zellij-plugin/.github/workflows/ci.yml +97 -0
  47. package/zellij-plugin/Cargo.lock +3558 -0
  48. package/zellij-plugin/Cargo.toml +40 -0
  49. package/zellij-plugin/README.md +290 -0
  50. package/zellij-plugin/build.sh +179 -0
  51. package/zellij-plugin/configs/examples/accessibility.kdl +31 -0
  52. package/zellij-plugin/configs/examples/catppuccin.kdl +32 -0
  53. package/zellij-plugin/configs/examples/default.kdl +34 -0
  54. package/zellij-plugin/configs/examples/minimal.kdl +22 -0
  55. package/zellij-plugin/docs/CONFIGURATION.md +191 -0
  56. package/zellij-plugin/docs/INTEGRATION.md +333 -0
  57. package/zellij-plugin/src/animation.rs +451 -0
  58. package/zellij-plugin/src/colors.rs +407 -0
  59. package/zellij-plugin/src/config.rs +664 -0
  60. package/zellij-plugin/src/event_bridge.rs +339 -0
  61. package/zellij-plugin/src/main.rs +420 -0
  62. package/zellij-plugin/src/notification.rs +466 -0
  63. package/zellij-plugin/src/queue.rs +399 -0
  64. package/zellij-plugin/src/renderer.rs +477 -0
  65. package/zellij-plugin/src/state.rs +338 -0
  66. package/zellij-plugin/src/tests.rs +413 -0
  67. package/ruv-swarm-mcp.db +0 -0
@@ -0,0 +1,413 @@
1
+ //! Integration tests for Zellij Visual Notifications
2
+ //!
3
+ //! These tests verify the complete notification flow and component interactions.
4
+
5
+ #[cfg(test)]
6
+ mod integration_tests {
7
+ use crate::animation::{AnimationEngine, easing};
8
+ use crate::colors::{Color, ColorManager, generate_gradient, generate_pulse_gradient};
9
+ use crate::config::{AnimationConfig, AnimationStyle, Config, ThemeConfig};
10
+ use crate::event_bridge::{EventBridge, create_test_message};
11
+ use crate::notification::{Notification, NotificationBuilder, NotificationType, Priority};
12
+ use crate::queue::NotificationQueue;
13
+ use crate::renderer::Renderer;
14
+ use crate::state::{PluginState, VisualNotificationState, VisualState};
15
+
16
+ // ==================== Integration Tests ====================
17
+
18
+ #[test]
19
+ fn test_full_notification_flow() {
20
+ // Create components
21
+ let config = Config::default();
22
+ let mut queue = NotificationQueue::new(100, 300_000);
23
+ let mut event_bridge = EventBridge::new();
24
+ let animation_engine = AnimationEngine::new(&config.animation);
25
+ let color_manager = ColorManager::new(&config.theme);
26
+
27
+ // Simulate receiving a notification message
28
+ let json = r#"{
29
+ "type": "success",
30
+ "message": "Build completed in 5.2s",
31
+ "pane_id": 1
32
+ }"#;
33
+
34
+ // Parse the message
35
+ let result = event_bridge.parse_notification(json);
36
+ assert!(result.is_ok());
37
+ let notification = result.unwrap();
38
+
39
+ // Enqueue the notification
40
+ queue.enqueue(notification.clone());
41
+ assert_eq!(queue.len(), 1);
42
+
43
+ // Process the notification
44
+ let dequeued = queue.dequeue_ready();
45
+ assert!(dequeued.is_some());
46
+ let processed = dequeued.unwrap();
47
+
48
+ // Verify notification properties
49
+ assert_eq!(processed.notification_type, NotificationType::Success);
50
+ assert!(processed.message.contains("Build completed"));
51
+ assert_eq!(processed.pane_id, Some(1));
52
+
53
+ // Create visual state for the pane
54
+ let mut visual_state = VisualState::new();
55
+ let color = color_manager.get_notification_color(&processed.notification_type);
56
+ assert!(color.is_some());
57
+
58
+ visual_state.border_color = color;
59
+ visual_state.notification_type = Some(processed.notification_type.clone());
60
+ visual_state.notification_message = Some(processed.message.clone());
61
+
62
+ // Verify visual state
63
+ assert!(visual_state.has_notification());
64
+ assert!(visual_state.border_color.is_some());
65
+ }
66
+
67
+ #[test]
68
+ fn test_notification_priority_flow() {
69
+ let mut queue = NotificationQueue::new(100, 300_000);
70
+
71
+ // Enqueue notifications in different order
72
+ queue.enqueue(Notification::info("Low priority").with_priority(Priority::Low));
73
+ queue.enqueue(Notification::success("Normal priority").with_priority(Priority::Normal));
74
+ queue.enqueue(Notification::error("Critical priority").with_priority(Priority::Critical));
75
+ queue.enqueue(Notification::warning("High priority").with_priority(Priority::High));
76
+
77
+ // Should dequeue in priority order
78
+ let first = queue.dequeue_ready().unwrap();
79
+ assert_eq!(first.priority, Priority::Critical);
80
+
81
+ let second = queue.dequeue_ready().unwrap();
82
+ assert_eq!(second.priority, Priority::High);
83
+
84
+ let third = queue.dequeue_ready().unwrap();
85
+ assert_eq!(third.priority, Priority::Normal);
86
+
87
+ let fourth = queue.dequeue_ready().unwrap();
88
+ assert_eq!(fourth.priority, Priority::Low);
89
+ }
90
+
91
+ #[test]
92
+ fn test_animation_lifecycle() {
93
+ let config = AnimationConfig {
94
+ enabled: true,
95
+ style: AnimationStyle::Pulse,
96
+ speed: 50,
97
+ cycles: 2,
98
+ duration_ms: 1000,
99
+ };
100
+ let engine = AnimationEngine::new(&config);
101
+
102
+ let mut state = VisualState::new();
103
+
104
+ // Start animation
105
+ engine.start_animation(&mut state, 0, AnimationStyle::Pulse);
106
+ assert!(state.is_animating);
107
+ assert_eq!(state.animation_start_tick, 0);
108
+
109
+ // Update animation midway
110
+ engine.update_animation(&mut state, 50);
111
+ let brightness = engine.get_brightness(&state, 50);
112
+ assert!(brightness > 0.0 && brightness <= 1.0);
113
+
114
+ // Animation should continue
115
+ assert!(engine.should_continue(&state, 50));
116
+
117
+ // After total ticks, animation should stop
118
+ engine.update_animation(&mut state, 500);
119
+ assert!(!engine.should_continue(&state, 500));
120
+ }
121
+
122
+ #[test]
123
+ fn test_theme_color_consistency() {
124
+ let themes = vec![
125
+ "default", "dracula", "nord", "catppuccin", "gruvbox", "tokyo-night"
126
+ ];
127
+
128
+ for theme_name in themes {
129
+ let theme = ThemeConfig::from_preset(theme_name);
130
+ let manager = ColorManager::new(&theme);
131
+
132
+ // All notification types should have colors
133
+ for notif_type in [
134
+ NotificationType::Success,
135
+ NotificationType::Error,
136
+ NotificationType::Warning,
137
+ NotificationType::Info,
138
+ NotificationType::Attention,
139
+ ] {
140
+ let color = manager.get_notification_color(&notif_type);
141
+ assert!(color.is_some(), "Theme {} missing color for {:?}", theme_name, notif_type);
142
+ }
143
+
144
+ // Colors should be different
145
+ let success = manager.get_notification_color(&NotificationType::Success).unwrap();
146
+ let error = manager.get_notification_color(&NotificationType::Error).unwrap();
147
+ assert_ne!(success, error, "Success and Error colors should differ");
148
+ }
149
+ }
150
+
151
+ #[test]
152
+ fn test_state_machine_transitions() {
153
+ let mut state = VisualState::new();
154
+ assert_eq!(state.state, VisualNotificationState::Idle);
155
+
156
+ // Transition: Idle -> Active
157
+ state.set_notification(
158
+ NotificationType::Success,
159
+ "Test".to_string(),
160
+ "#22c55e".to_string(),
161
+ "+".to_string(),
162
+ );
163
+ assert_eq!(state.state, VisualNotificationState::Active);
164
+ assert!(state.has_notification());
165
+
166
+ // Transition: Active -> Fading
167
+ state.acknowledge();
168
+ assert_eq!(state.state, VisualNotificationState::Fading);
169
+
170
+ // Clear should go to Idle
171
+ state.clear();
172
+ assert_eq!(state.state, VisualNotificationState::Idle);
173
+ assert!(!state.has_notification());
174
+ }
175
+
176
+ #[test]
177
+ fn test_color_interpolation_smooth() {
178
+ let start = Color::new(0, 0, 0);
179
+ let end = Color::new(255, 255, 255);
180
+
181
+ let gradient = generate_gradient(&start, &end, 11);
182
+
183
+ // Check gradient is smooth
184
+ for i in 0..gradient.len() - 1 {
185
+ let diff = (gradient[i + 1].r as i32 - gradient[i].r as i32).abs();
186
+ assert!(diff <= 30, "Gradient step too large at index {}", i);
187
+ }
188
+
189
+ // Check endpoints
190
+ assert_eq!(gradient[0].r, 0);
191
+ assert_eq!(gradient[10].r, 255);
192
+ }
193
+
194
+ #[test]
195
+ fn test_event_bridge_error_recovery() {
196
+ let mut bridge = EventBridge::new();
197
+
198
+ // Cause errors
199
+ for _ in 0..4 {
200
+ let _ = bridge.parse_notification("invalid json");
201
+ }
202
+
203
+ // Should not be in error state yet
204
+ assert_ne!(
205
+ bridge.health_status().error_count,
206
+ 5,
207
+ "Should not reach max errors"
208
+ );
209
+
210
+ // One more error
211
+ let _ = bridge.parse_notification("invalid");
212
+
213
+ // Now in error state
214
+ let health = bridge.health_status();
215
+ assert_eq!(health.error_count, 5);
216
+
217
+ // Recovery
218
+ bridge.reset_errors();
219
+ let health = bridge.health_status();
220
+ assert_eq!(health.error_count, 0);
221
+ }
222
+
223
+ #[test]
224
+ fn test_queue_expiry_cleanup() {
225
+ let mut queue = NotificationQueue::new(100, 1000); // 1 second TTL
226
+ queue.update_timestamp(0);
227
+
228
+ // Add notification
229
+ let mut notif = Notification::info("Expiring");
230
+ notif.timestamp = 0;
231
+ notif.ttl_ms = 1000;
232
+ queue.enqueue(notif);
233
+
234
+ assert_eq!(queue.len(), 1);
235
+
236
+ // Not expired yet
237
+ queue.update_timestamp(500);
238
+ queue.cleanup_expired();
239
+ assert_eq!(queue.len(), 1);
240
+
241
+ // Now expired
242
+ queue.update_timestamp(1500);
243
+ queue.cleanup_expired();
244
+ assert_eq!(queue.len(), 0);
245
+ }
246
+
247
+ #[test]
248
+ fn test_pane_specific_notifications() {
249
+ let mut queue = NotificationQueue::new(100, 300_000);
250
+
251
+ // Add notifications for different panes
252
+ queue.enqueue(Notification::success("Pane 1").for_pane(1));
253
+ queue.enqueue(Notification::error("Pane 2").for_pane(2));
254
+ queue.enqueue(Notification::info("Pane 1 again").for_pane(1));
255
+ queue.enqueue(Notification::warning("Pane 3").for_pane(3));
256
+
257
+ // Check pane-specific queries
258
+ assert!(queue.has_notifications_for_pane(1));
259
+ assert!(queue.has_notifications_for_pane(2));
260
+ assert!(queue.has_notifications_for_pane(3));
261
+ assert!(!queue.has_notifications_for_pane(4));
262
+
263
+ // Get notifications for pane 1
264
+ let pane1_notifs = queue.get_for_pane(1);
265
+ assert_eq!(pane1_notifs.len(), 2);
266
+
267
+ // Remove notifications for pane 1
268
+ queue.remove_for_pane(1);
269
+ assert!(!queue.has_notifications_for_pane(1));
270
+ assert_eq!(queue.len(), 2);
271
+ }
272
+
273
+ // ==================== Component Tests ====================
274
+
275
+ #[test]
276
+ fn test_notification_builder_chain() {
277
+ let notif = NotificationBuilder::new()
278
+ .notification_type(NotificationType::Error)
279
+ .message("Build failed")
280
+ .title("CI")
281
+ .pane_id(5)
282
+ .tab_index(2)
283
+ .source("github-actions")
284
+ .command("cargo build")
285
+ .exit_code(1)
286
+ .duration(15000)
287
+ .build();
288
+
289
+ assert_eq!(notif.notification_type, NotificationType::Error);
290
+ assert_eq!(notif.message, "Build failed");
291
+ assert_eq!(notif.title, Some("CI".to_string()));
292
+ assert_eq!(notif.pane_id, Some(5));
293
+ assert_eq!(notif.tab_index, Some(2));
294
+ assert_eq!(notif.source, "github-actions");
295
+ assert_eq!(notif.metadata.command, Some("cargo build".to_string()));
296
+ assert_eq!(notif.metadata.exit_code, Some(1));
297
+ assert_eq!(notif.metadata.duration_ms, Some(15000));
298
+ }
299
+
300
+ #[test]
301
+ fn test_easing_functions() {
302
+ // Test all easing functions at boundaries
303
+ assert_eq!(easing::linear(0.0), 0.0);
304
+ assert_eq!(easing::linear(1.0), 1.0);
305
+
306
+ assert_eq!(easing::ease_in(0.0), 0.0);
307
+ assert!((easing::ease_in(1.0) - 1.0).abs() < 0.001);
308
+
309
+ assert_eq!(easing::ease_out(0.0), 0.0);
310
+ assert!((easing::ease_out(1.0) - 1.0).abs() < 0.001);
311
+
312
+ assert_eq!(easing::ease_in_out(0.0), 0.0);
313
+ assert!((easing::ease_in_out(1.0) - 1.0).abs() < 0.001);
314
+
315
+ // Midpoint characteristics
316
+ assert!(easing::ease_in(0.5) < 0.5); // Slow start
317
+ assert!(easing::ease_out(0.5) > 0.5); // Slow end
318
+ }
319
+
320
+ #[test]
321
+ fn test_pulse_gradient() {
322
+ let base = Color::new(100, 100, 100);
323
+ let bright = Color::new(200, 200, 200);
324
+
325
+ let gradient = generate_pulse_gradient(&base, &bright, 10);
326
+ assert_eq!(gradient.len(), 10);
327
+
328
+ // Should start near base, go to bright, then back toward base
329
+ // First element should be at base (100)
330
+ assert!(gradient[0].r <= 110);
331
+ // Last element should be returning toward base (around 120 due to interpolation)
332
+ assert!(gradient[gradient.len() - 1].r <= 130);
333
+ // Middle should be brighter than ends
334
+ assert!(gradient[gradient.len() / 2].r > gradient[0].r);
335
+ }
336
+
337
+ #[test]
338
+ fn test_config_validation() {
339
+ let mut config = Config::default();
340
+ assert!(config.validate().is_ok());
341
+
342
+ // Invalid timeout
343
+ config.notification_timeout_ms = 100;
344
+ assert!(config.validate().is_err());
345
+
346
+ // Reset and test queue size
347
+ config.notification_timeout_ms = 5000;
348
+ config.queue_max_size = 0;
349
+ assert!(config.validate().is_err());
350
+
351
+ // Reset and test animation speed
352
+ config.queue_max_size = 100;
353
+ config.animation.speed = 150;
354
+ assert!(config.validate().is_err());
355
+ }
356
+
357
+ #[test]
358
+ fn test_renderer_icon_mapping() {
359
+ let renderer = Renderer::default();
360
+ let config = Config::default();
361
+ let color_manager = ColorManager::new(&config.theme);
362
+
363
+ // All notification types should have distinct icons
364
+ let types = vec![
365
+ NotificationType::Success,
366
+ NotificationType::Error,
367
+ NotificationType::Warning,
368
+ NotificationType::Info,
369
+ NotificationType::Attention,
370
+ NotificationType::Progress,
371
+ ];
372
+
373
+ let mut seen_icons = std::collections::HashSet::new();
374
+ for t in &types {
375
+ let icon = t.icon().unwrap();
376
+ assert!(!icon.is_empty());
377
+ // Icons should be unique (except Attention and Warning may share)
378
+ if *t != NotificationType::Attention && *t != NotificationType::Warning {
379
+ seen_icons.insert(icon);
380
+ }
381
+ }
382
+ }
383
+
384
+ // ==================== Performance Tests ====================
385
+
386
+ #[test]
387
+ fn test_queue_performance() {
388
+ let mut queue = NotificationQueue::new(1000, 300_000);
389
+
390
+ // Enqueue many notifications
391
+ for i in 0..1000 {
392
+ queue.enqueue(Notification::info(&format!("Message {}", i)));
393
+ }
394
+
395
+ assert_eq!(queue.len(), 1000);
396
+
397
+ // Dequeue all
398
+ while queue.dequeue_ready().is_some() {}
399
+
400
+ assert!(queue.is_empty());
401
+ }
402
+
403
+ #[test]
404
+ fn test_color_interpolation_performance() {
405
+ let c1 = Color::from_hex("#ff0000");
406
+ let c2 = Color::from_hex("#00ff00");
407
+
408
+ // Many interpolations
409
+ for _ in 0..10000 {
410
+ let _ = c1.interpolate(&c2, 0.5);
411
+ }
412
+ }
413
+ }
package/ruv-swarm-mcp.db DELETED
Binary file