@jacques-ai/server 0.0.7-alpha.1
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.
- package/dist/config/config.d.ts +34 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +32 -0
- package/dist/config/config.js.map +1 -0
- package/dist/connection/applescript.d.ts +46 -0
- package/dist/connection/applescript.d.ts.map +1 -0
- package/dist/connection/applescript.js +62 -0
- package/dist/connection/applescript.js.map +1 -0
- package/dist/connection/applescript.test.d.ts +5 -0
- package/dist/connection/applescript.test.d.ts.map +1 -0
- package/dist/connection/applescript.test.js +64 -0
- package/dist/connection/applescript.test.js.map +1 -0
- package/dist/connection/constants.d.ts +88 -0
- package/dist/connection/constants.d.ts.map +1 -0
- package/dist/connection/constants.js +110 -0
- package/dist/connection/constants.js.map +1 -0
- package/dist/connection/git-info.d.ts +30 -0
- package/dist/connection/git-info.d.ts.map +1 -0
- package/dist/connection/git-info.js +52 -0
- package/dist/connection/git-info.js.map +1 -0
- package/dist/connection/git-info.test.d.ts +5 -0
- package/dist/connection/git-info.test.d.ts.map +1 -0
- package/dist/connection/git-info.test.js +35 -0
- package/dist/connection/git-info.test.js.map +1 -0
- package/dist/connection/index.d.ts +19 -0
- package/dist/connection/index.d.ts.map +1 -0
- package/dist/connection/index.js +36 -0
- package/dist/connection/index.js.map +1 -0
- package/dist/connection/process-detection.d.ts +58 -0
- package/dist/connection/process-detection.d.ts.map +1 -0
- package/dist/connection/process-detection.js +239 -0
- package/dist/connection/process-detection.js.map +1 -0
- package/dist/connection/process-detection.test.d.ts +5 -0
- package/dist/connection/process-detection.test.d.ts.map +1 -0
- package/dist/connection/process-detection.test.js +43 -0
- package/dist/connection/process-detection.test.js.map +1 -0
- package/dist/connection/session-discovery.d.ts +55 -0
- package/dist/connection/session-discovery.d.ts.map +1 -0
- package/dist/connection/session-discovery.js +311 -0
- package/dist/connection/session-discovery.js.map +1 -0
- package/dist/connection/terminal-key.d.ts +126 -0
- package/dist/connection/terminal-key.d.ts.map +1 -0
- package/dist/connection/terminal-key.js +271 -0
- package/dist/connection/terminal-key.js.map +1 -0
- package/dist/connection/terminal-key.test.d.ts +5 -0
- package/dist/connection/terminal-key.test.d.ts.map +1 -0
- package/dist/connection/terminal-key.test.js +221 -0
- package/dist/connection/terminal-key.test.js.map +1 -0
- package/dist/connection/worktree.d.ts +114 -0
- package/dist/connection/worktree.d.ts.map +1 -0
- package/dist/connection/worktree.js +320 -0
- package/dist/connection/worktree.js.map +1 -0
- package/dist/connection/worktree.test.d.ts +5 -0
- package/dist/connection/worktree.test.d.ts.map +1 -0
- package/dist/connection/worktree.test.js +113 -0
- package/dist/connection/worktree.test.js.map +1 -0
- package/dist/focus-watcher.d.ts +51 -0
- package/dist/focus-watcher.d.ts.map +1 -0
- package/dist/focus-watcher.js +169 -0
- package/dist/focus-watcher.js.map +1 -0
- package/dist/handlers/event-handler.d.ts +93 -0
- package/dist/handlers/event-handler.d.ts.map +1 -0
- package/dist/handlers/event-handler.js +196 -0
- package/dist/handlers/event-handler.js.map +1 -0
- package/dist/handlers/event-handler.test.d.ts +5 -0
- package/dist/handlers/event-handler.test.d.ts.map +1 -0
- package/dist/handlers/event-handler.test.js +305 -0
- package/dist/handlers/event-handler.test.js.map +1 -0
- package/dist/handlers/session-handler.d.ts +23 -0
- package/dist/handlers/session-handler.d.ts.map +1 -0
- package/dist/handlers/session-handler.js +104 -0
- package/dist/handlers/session-handler.js.map +1 -0
- package/dist/handlers/session-handler.test.d.ts +5 -0
- package/dist/handlers/session-handler.test.d.ts.map +1 -0
- package/dist/handlers/session-handler.test.js +89 -0
- package/dist/handlers/session-handler.test.js.map +1 -0
- package/dist/handlers/settings-handler.d.ts +32 -0
- package/dist/handlers/settings-handler.d.ts.map +1 -0
- package/dist/handlers/settings-handler.js +127 -0
- package/dist/handlers/settings-handler.js.map +1 -0
- package/dist/handlers/settings-handler.test.d.ts +5 -0
- package/dist/handlers/settings-handler.test.d.ts.map +1 -0
- package/dist/handlers/settings-handler.test.js +105 -0
- package/dist/handlers/settings-handler.test.js.map +1 -0
- package/dist/handlers/window-handler.d.ts +30 -0
- package/dist/handlers/window-handler.d.ts.map +1 -0
- package/dist/handlers/window-handler.js +486 -0
- package/dist/handlers/window-handler.js.map +1 -0
- package/dist/handlers/window-handler.test.d.ts +8 -0
- package/dist/handlers/window-handler.test.d.ts.map +1 -0
- package/dist/handlers/window-handler.test.js +167 -0
- package/dist/handlers/window-handler.test.js.map +1 -0
- package/dist/handlers/worktree-handler.d.ts +28 -0
- package/dist/handlers/worktree-handler.d.ts.map +1 -0
- package/dist/handlers/worktree-handler.js +268 -0
- package/dist/handlers/worktree-handler.js.map +1 -0
- package/dist/handlers/worktree-handler.test.d.ts +8 -0
- package/dist/handlers/worktree-handler.test.d.ts.map +1 -0
- package/dist/handlers/worktree-handler.test.js +118 -0
- package/dist/handlers/worktree-handler.test.js.map +1 -0
- package/dist/handlers/ws-utils.d.ts +12 -0
- package/dist/handlers/ws-utils.d.ts.map +1 -0
- package/dist/handlers/ws-utils.js +16 -0
- package/dist/handlers/ws-utils.js.map +1 -0
- package/dist/http-api.d.ts +26 -0
- package/dist/http-api.d.ts.map +1 -0
- package/dist/http-api.js +148 -0
- package/dist/http-api.js.map +1 -0
- package/dist/logger.d.ts +42 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +147 -0
- package/dist/logger.js.map +1 -0
- package/dist/logging/logger-factory.d.ts +51 -0
- package/dist/logging/logger-factory.d.ts.map +1 -0
- package/dist/logging/logger-factory.js +59 -0
- package/dist/logging/logger-factory.js.map +1 -0
- package/dist/mcp/search-tool.d.ts +65 -0
- package/dist/mcp/search-tool.d.ts.map +1 -0
- package/dist/mcp/search-tool.js +176 -0
- package/dist/mcp/search-tool.js.map +1 -0
- package/dist/mcp/server.d.ts +9 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +152 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/process-scanner.d.ts +96 -0
- package/dist/process-scanner.d.ts.map +1 -0
- package/dist/process-scanner.js +194 -0
- package/dist/process-scanner.js.map +1 -0
- package/dist/routes/__tests__/archive-routes.test.d.ts +5 -0
- package/dist/routes/__tests__/archive-routes.test.d.ts.map +1 -0
- package/dist/routes/__tests__/archive-routes.test.js +158 -0
- package/dist/routes/__tests__/archive-routes.test.js.map +1 -0
- package/dist/routes/__tests__/config-routes.test.d.ts +5 -0
- package/dist/routes/__tests__/config-routes.test.d.ts.map +1 -0
- package/dist/routes/__tests__/config-routes.test.js +112 -0
- package/dist/routes/__tests__/config-routes.test.js.map +1 -0
- package/dist/routes/__tests__/http-utils.test.d.ts +5 -0
- package/dist/routes/__tests__/http-utils.test.d.ts.map +1 -0
- package/dist/routes/__tests__/http-utils.test.js +102 -0
- package/dist/routes/__tests__/http-utils.test.js.map +1 -0
- package/dist/routes/__tests__/notification-routes.test.d.ts +5 -0
- package/dist/routes/__tests__/notification-routes.test.d.ts.map +1 -0
- package/dist/routes/__tests__/notification-routes.test.js +91 -0
- package/dist/routes/__tests__/notification-routes.test.js.map +1 -0
- package/dist/routes/__tests__/project-routes.test.d.ts +5 -0
- package/dist/routes/__tests__/project-routes.test.d.ts.map +1 -0
- package/dist/routes/__tests__/project-routes.test.js +168 -0
- package/dist/routes/__tests__/project-routes.test.js.map +1 -0
- package/dist/routes/__tests__/session-routes.test.d.ts +5 -0
- package/dist/routes/__tests__/session-routes.test.d.ts.map +1 -0
- package/dist/routes/__tests__/session-routes.test.js +198 -0
- package/dist/routes/__tests__/session-routes.test.js.map +1 -0
- package/dist/routes/__tests__/source-routes.test.d.ts +5 -0
- package/dist/routes/__tests__/source-routes.test.d.ts.map +1 -0
- package/dist/routes/__tests__/source-routes.test.js +142 -0
- package/dist/routes/__tests__/source-routes.test.js.map +1 -0
- package/dist/routes/__tests__/sync-routes.test.d.ts +5 -0
- package/dist/routes/__tests__/sync-routes.test.d.ts.map +1 -0
- package/dist/routes/__tests__/sync-routes.test.js +77 -0
- package/dist/routes/__tests__/sync-routes.test.js.map +1 -0
- package/dist/routes/__tests__/test-helpers.d.ts +47 -0
- package/dist/routes/__tests__/test-helpers.d.ts.map +1 -0
- package/dist/routes/__tests__/test-helpers.js +97 -0
- package/dist/routes/__tests__/test-helpers.js.map +1 -0
- package/dist/routes/archive-routes.d.ts +15 -0
- package/dist/routes/archive-routes.d.ts.map +1 -0
- package/dist/routes/archive-routes.js +181 -0
- package/dist/routes/archive-routes.js.map +1 -0
- package/dist/routes/claude-routes.d.ts +9 -0
- package/dist/routes/claude-routes.d.ts.map +1 -0
- package/dist/routes/claude-routes.js +47 -0
- package/dist/routes/claude-routes.js.map +1 -0
- package/dist/routes/config-routes.d.ts +9 -0
- package/dist/routes/config-routes.d.ts.map +1 -0
- package/dist/routes/config-routes.js +56 -0
- package/dist/routes/config-routes.js.map +1 -0
- package/dist/routes/config-store.d.ts +41 -0
- package/dist/routes/config-store.d.ts.map +1 -0
- package/dist/routes/config-store.js +52 -0
- package/dist/routes/config-store.js.map +1 -0
- package/dist/routes/http-utils.d.ts +32 -0
- package/dist/routes/http-utils.d.ts.map +1 -0
- package/dist/routes/http-utils.js +123 -0
- package/dist/routes/http-utils.js.map +1 -0
- package/dist/routes/index.d.ts +19 -0
- package/dist/routes/index.d.ts.map +1 -0
- package/dist/routes/index.js +17 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/notification-routes.d.ts +10 -0
- package/dist/routes/notification-routes.d.ts.map +1 -0
- package/dist/routes/notification-routes.js +64 -0
- package/dist/routes/notification-routes.js.map +1 -0
- package/dist/routes/project-routes.d.ts +22 -0
- package/dist/routes/project-routes.d.ts.map +1 -0
- package/dist/routes/project-routes.js +415 -0
- package/dist/routes/project-routes.js.map +1 -0
- package/dist/routes/session-routes.d.ts +18 -0
- package/dist/routes/session-routes.d.ts.map +1 -0
- package/dist/routes/session-routes.js +609 -0
- package/dist/routes/session-routes.js.map +1 -0
- package/dist/routes/source-routes.d.ts +12 -0
- package/dist/routes/source-routes.d.ts.map +1 -0
- package/dist/routes/source-routes.js +119 -0
- package/dist/routes/source-routes.js.map +1 -0
- package/dist/routes/static-routes.d.ts +12 -0
- package/dist/routes/static-routes.d.ts.map +1 -0
- package/dist/routes/static-routes.js +52 -0
- package/dist/routes/static-routes.js.map +1 -0
- package/dist/routes/sync-routes.d.ts +9 -0
- package/dist/routes/sync-routes.d.ts.map +1 -0
- package/dist/routes/sync-routes.js +78 -0
- package/dist/routes/sync-routes.js.map +1 -0
- package/dist/routes/tile-routes.d.ts +10 -0
- package/dist/routes/tile-routes.d.ts.map +1 -0
- package/dist/routes/tile-routes.js +108 -0
- package/dist/routes/tile-routes.js.map +1 -0
- package/dist/routes/types.d.ts +17 -0
- package/dist/routes/types.d.ts.map +1 -0
- package/dist/routes/types.js +5 -0
- package/dist/routes/types.js.map +1 -0
- package/dist/routes/usage-routes.d.ts +8 -0
- package/dist/routes/usage-routes.d.ts.map +1 -0
- package/dist/routes/usage-routes.js +18 -0
- package/dist/routes/usage-routes.js.map +1 -0
- package/dist/server.d.ts +8 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +173 -0
- package/dist/server.js.map +1 -0
- package/dist/services/branch-divergence-service.d.ts +48 -0
- package/dist/services/branch-divergence-service.d.ts.map +1 -0
- package/dist/services/branch-divergence-service.js +156 -0
- package/dist/services/branch-divergence-service.js.map +1 -0
- package/dist/services/broadcast-service.d.ts +68 -0
- package/dist/services/broadcast-service.d.ts.map +1 -0
- package/dist/services/broadcast-service.js +78 -0
- package/dist/services/broadcast-service.js.map +1 -0
- package/dist/services/broadcast-service.test.d.ts +5 -0
- package/dist/services/broadcast-service.test.d.ts.map +1 -0
- package/dist/services/broadcast-service.test.js +130 -0
- package/dist/services/broadcast-service.test.js.map +1 -0
- package/dist/services/chat-service.d.ts +72 -0
- package/dist/services/chat-service.d.ts.map +1 -0
- package/dist/services/chat-service.js +342 -0
- package/dist/services/chat-service.js.map +1 -0
- package/dist/services/chat-system-prompt.d.ts +14 -0
- package/dist/services/chat-system-prompt.d.ts.map +1 -0
- package/dist/services/chat-system-prompt.js +68 -0
- package/dist/services/chat-system-prompt.js.map +1 -0
- package/dist/services/notification-service.d.ts +115 -0
- package/dist/services/notification-service.d.ts.map +1 -0
- package/dist/services/notification-service.js +424 -0
- package/dist/services/notification-service.js.map +1 -0
- package/dist/services/notification-service.test.d.ts +5 -0
- package/dist/services/notification-service.test.d.ts.map +1 -0
- package/dist/services/notification-service.test.js +918 -0
- package/dist/services/notification-service.test.js.map +1 -0
- package/dist/session/cleanup-service.d.ts +51 -0
- package/dist/session/cleanup-service.d.ts.map +1 -0
- package/dist/session/cleanup-service.js +98 -0
- package/dist/session/cleanup-service.js.map +1 -0
- package/dist/session/cleanup-service.test.d.ts +5 -0
- package/dist/session/cleanup-service.test.d.ts.map +1 -0
- package/dist/session/cleanup-service.test.js +121 -0
- package/dist/session/cleanup-service.test.js.map +1 -0
- package/dist/session/process-monitor.d.ts +79 -0
- package/dist/session/process-monitor.d.ts.map +1 -0
- package/dist/session/process-monitor.js +270 -0
- package/dist/session/process-monitor.js.map +1 -0
- package/dist/session/process-monitor.test.d.ts +5 -0
- package/dist/session/process-monitor.test.d.ts.map +1 -0
- package/dist/session/process-monitor.test.js +367 -0
- package/dist/session/process-monitor.test.js.map +1 -0
- package/dist/session/session-factory.d.ts +29 -0
- package/dist/session/session-factory.d.ts.map +1 -0
- package/dist/session/session-factory.js +123 -0
- package/dist/session/session-factory.js.map +1 -0
- package/dist/session/session-factory.test.d.ts +5 -0
- package/dist/session/session-factory.test.d.ts.map +1 -0
- package/dist/session/session-factory.test.js +299 -0
- package/dist/session/session-factory.test.js.map +1 -0
- package/dist/session-registry.d.ts +168 -0
- package/dist/session-registry.d.ts.map +1 -0
- package/dist/session-registry.js +626 -0
- package/dist/session-registry.js.map +1 -0
- package/dist/session-registry.test.d.ts +5 -0
- package/dist/session-registry.test.d.ts.map +1 -0
- package/dist/session-registry.test.js +582 -0
- package/dist/session-registry.test.js.map +1 -0
- package/dist/start-server.d.ts +31 -0
- package/dist/start-server.d.ts.map +1 -0
- package/dist/start-server.js +408 -0
- package/dist/start-server.js.map +1 -0
- package/dist/terminal-activator.d.ts +29 -0
- package/dist/terminal-activator.d.ts.map +1 -0
- package/dist/terminal-activator.js +264 -0
- package/dist/terminal-activator.js.map +1 -0
- package/dist/terminal-activator.test.d.ts +9 -0
- package/dist/terminal-activator.test.d.ts.map +1 -0
- package/dist/terminal-activator.test.js +95 -0
- package/dist/terminal-activator.test.js.map +1 -0
- package/dist/terminal-launcher.d.ts +51 -0
- package/dist/terminal-launcher.d.ts.map +1 -0
- package/dist/terminal-launcher.js +298 -0
- package/dist/terminal-launcher.js.map +1 -0
- package/dist/terminal-launcher.test.d.ts +8 -0
- package/dist/terminal-launcher.test.d.ts.map +1 -0
- package/dist/terminal-launcher.test.js +222 -0
- package/dist/terminal-launcher.test.js.map +1 -0
- package/dist/types.d.ts +796 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +15 -0
- package/dist/types.js.map +1 -0
- package/dist/unix-socket.d.ts +68 -0
- package/dist/unix-socket.d.ts.map +1 -0
- package/dist/unix-socket.js +180 -0
- package/dist/unix-socket.js.map +1 -0
- package/dist/usage-limits.d.ts +13 -0
- package/dist/usage-limits.d.ts.map +1 -0
- package/dist/usage-limits.js +112 -0
- package/dist/usage-limits.js.map +1 -0
- package/dist/watchers/handoff-watcher.d.ts +74 -0
- package/dist/watchers/handoff-watcher.d.ts.map +1 -0
- package/dist/watchers/handoff-watcher.js +124 -0
- package/dist/watchers/handoff-watcher.js.map +1 -0
- package/dist/watchers/handoff-watcher.test.d.ts +8 -0
- package/dist/watchers/handoff-watcher.test.d.ts.map +1 -0
- package/dist/watchers/handoff-watcher.test.js +142 -0
- package/dist/watchers/handoff-watcher.test.js.map +1 -0
- package/dist/websocket.d.ts +107 -0
- package/dist/websocket.d.ts.map +1 -0
- package/dist/websocket.js +268 -0
- package/dist/websocket.js.map +1 -0
- package/dist/window-manager/index.d.ts +28 -0
- package/dist/window-manager/index.d.ts.map +1 -0
- package/dist/window-manager/index.js +56 -0
- package/dist/window-manager/index.js.map +1 -0
- package/dist/window-manager/layouts.d.ts +42 -0
- package/dist/window-manager/layouts.d.ts.map +1 -0
- package/dist/window-manager/layouts.js +133 -0
- package/dist/window-manager/layouts.js.map +1 -0
- package/dist/window-manager/linux-manager.d.ts +45 -0
- package/dist/window-manager/linux-manager.d.ts.map +1 -0
- package/dist/window-manager/linux-manager.js +299 -0
- package/dist/window-manager/linux-manager.js.map +1 -0
- package/dist/window-manager/macos-manager.d.ts +103 -0
- package/dist/window-manager/macos-manager.d.ts.map +1 -0
- package/dist/window-manager/macos-manager.js +637 -0
- package/dist/window-manager/macos-manager.js.map +1 -0
- package/dist/window-manager/smart-layouts.d.ts +116 -0
- package/dist/window-manager/smart-layouts.d.ts.map +1 -0
- package/dist/window-manager/smart-layouts.js +188 -0
- package/dist/window-manager/smart-layouts.js.map +1 -0
- package/dist/window-manager/smart-layouts.test.d.ts +8 -0
- package/dist/window-manager/smart-layouts.test.d.ts.map +1 -0
- package/dist/window-manager/smart-layouts.test.js +311 -0
- package/dist/window-manager/smart-layouts.test.js.map +1 -0
- package/dist/window-manager/tile-state.d.ts +87 -0
- package/dist/window-manager/tile-state.d.ts.map +1 -0
- package/dist/window-manager/tile-state.js +136 -0
- package/dist/window-manager/tile-state.js.map +1 -0
- package/dist/window-manager/tile-state.test.d.ts +8 -0
- package/dist/window-manager/tile-state.test.d.ts.map +1 -0
- package/dist/window-manager/tile-state.test.js +179 -0
- package/dist/window-manager/tile-state.test.js.map +1 -0
- package/dist/window-manager/types.d.ts +104 -0
- package/dist/window-manager/types.d.ts.map +1 -0
- package/dist/window-manager/types.js +8 -0
- package/dist/window-manager/types.js.map +1 -0
- package/dist/window-manager/windows-manager.d.ts +44 -0
- package/dist/window-manager/windows-manager.d.ts.map +1 -0
- package/dist/window-manager/windows-manager.js +281 -0
- package/dist/window-manager/windows-manager.js.map +1 -0
- package/dist/window-manager/windows-manager.test.d.ts +8 -0
- package/dist/window-manager/windows-manager.test.d.ts.map +1 -0
- package/dist/window-manager/windows-manager.test.js +183 -0
- package/dist/window-manager/windows-manager.test.js.map +1 -0
- package/gui-dist/assets/index-BmYIHRYe.js +142 -0
- package/gui-dist/assets/index-D_N5RH8O.css +1 -0
- package/gui-dist/assets/vendor-icons-ByXNrcwf.js +336 -0
- package/gui-dist/assets/vendor-markdown-DWPYwU1x.js +22 -0
- package/gui-dist/assets/vendor-react-CpILBTDM.js +59 -0
- package/gui-dist/index.html +17 -0
- package/gui-dist/jacsub.png +0 -0
- package/package.json +67 -0
|
@@ -0,0 +1,918 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notification Service Tests
|
|
3
|
+
*/
|
|
4
|
+
import { jest } from '@jest/globals';
|
|
5
|
+
// Mock node-notifier before importing the service
|
|
6
|
+
jest.unstable_mockModule('node-notifier', () => ({
|
|
7
|
+
default: {
|
|
8
|
+
notify: jest.fn(),
|
|
9
|
+
},
|
|
10
|
+
}));
|
|
11
|
+
// Mock fs for settings persistence
|
|
12
|
+
const mockExistsSync = jest.fn();
|
|
13
|
+
const mockReadFileSync = jest.fn();
|
|
14
|
+
const mockWriteFileSync = jest.fn();
|
|
15
|
+
const mockMkdirSync = jest.fn();
|
|
16
|
+
jest.unstable_mockModule('fs', () => ({
|
|
17
|
+
existsSync: mockExistsSync,
|
|
18
|
+
readFileSync: mockReadFileSync,
|
|
19
|
+
writeFileSync: mockWriteFileSync,
|
|
20
|
+
mkdirSync: mockMkdirSync,
|
|
21
|
+
}));
|
|
22
|
+
// Mock fs/promises for scanForErrors
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
24
|
+
const mockFhRead = jest.fn();
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
|
+
const mockFhClose = jest.fn();
|
|
27
|
+
const mockFsOpen = jest.fn();
|
|
28
|
+
const mockFsStat = jest.fn();
|
|
29
|
+
jest.unstable_mockModule('fs/promises', () => ({
|
|
30
|
+
open: mockFsOpen,
|
|
31
|
+
stat: mockFsStat,
|
|
32
|
+
}));
|
|
33
|
+
// Mock @jacques-ai/core for plan detection
|
|
34
|
+
const mockParseJSONL = jest.fn();
|
|
35
|
+
const mockDetectModeAndPlans = jest.fn();
|
|
36
|
+
jest.unstable_mockModule('@jacques-ai/core', () => ({
|
|
37
|
+
parseJSONL: mockParseJSONL,
|
|
38
|
+
detectModeAndPlans: mockDetectModeAndPlans,
|
|
39
|
+
}));
|
|
40
|
+
// Import after mocks
|
|
41
|
+
const { NotificationService } = await import('./notification-service.js');
|
|
42
|
+
const notifierModule = await import('node-notifier');
|
|
43
|
+
const notifier = notifierModule.default;
|
|
44
|
+
import { createLogger } from '../logging/logger-factory.js';
|
|
45
|
+
// Helper to create a mock session
|
|
46
|
+
function createMockSession(overrides = {}) {
|
|
47
|
+
return {
|
|
48
|
+
session_id: 'test-session-1',
|
|
49
|
+
source: 'claude_code',
|
|
50
|
+
session_title: 'Test Session',
|
|
51
|
+
transcript_path: null,
|
|
52
|
+
cwd: '/test/project',
|
|
53
|
+
project: 'project',
|
|
54
|
+
model: null,
|
|
55
|
+
workspace: null,
|
|
56
|
+
terminal: null,
|
|
57
|
+
terminal_key: 'TTY:/dev/ttys001',
|
|
58
|
+
status: 'working',
|
|
59
|
+
last_activity: Date.now(),
|
|
60
|
+
registered_at: Date.now(),
|
|
61
|
+
context_metrics: null,
|
|
62
|
+
autocompact: null,
|
|
63
|
+
...overrides,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
describe('NotificationService', () => {
|
|
67
|
+
let service;
|
|
68
|
+
let broadcastCalls;
|
|
69
|
+
const silentLogger = createLogger({ silent: true });
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
broadcastCalls = [];
|
|
72
|
+
// Default: no config file exists
|
|
73
|
+
mockExistsSync.mockReturnValue(false);
|
|
74
|
+
mockReadFileSync.mockReturnValue('{}');
|
|
75
|
+
mockWriteFileSync.mockImplementation(() => { });
|
|
76
|
+
mockMkdirSync.mockImplementation(() => { });
|
|
77
|
+
notifier.notify.mockClear();
|
|
78
|
+
// Reset fs/promises and core mocks
|
|
79
|
+
mockFsOpen.mockReset();
|
|
80
|
+
mockFsStat.mockReset();
|
|
81
|
+
mockFhRead.mockReset();
|
|
82
|
+
mockFhClose.mockReset();
|
|
83
|
+
mockParseJSONL.mockReset();
|
|
84
|
+
mockDetectModeAndPlans.mockReset();
|
|
85
|
+
service = new NotificationService({
|
|
86
|
+
broadcast: (msg) => broadcastCalls.push(msg),
|
|
87
|
+
logger: silentLogger,
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
describe('settings', () => {
|
|
91
|
+
it('should return default settings when no config exists', () => {
|
|
92
|
+
const settings = service.getSettings();
|
|
93
|
+
expect(settings.enabled).toBe(false);
|
|
94
|
+
expect(settings.categories.context).toBe(true);
|
|
95
|
+
expect(settings.contextThresholds).toEqual([50, 70]);
|
|
96
|
+
expect(settings.largeOperationThreshold).toBe(50_000);
|
|
97
|
+
});
|
|
98
|
+
it('should load settings from config file', () => {
|
|
99
|
+
mockExistsSync.mockReturnValue(true);
|
|
100
|
+
mockReadFileSync.mockReturnValue(JSON.stringify({
|
|
101
|
+
notifications: {
|
|
102
|
+
enabled: true,
|
|
103
|
+
categories: { context: false },
|
|
104
|
+
largeOperationThreshold: 100_000,
|
|
105
|
+
},
|
|
106
|
+
}));
|
|
107
|
+
const svc = new NotificationService({
|
|
108
|
+
broadcast: () => { },
|
|
109
|
+
logger: silentLogger,
|
|
110
|
+
});
|
|
111
|
+
const settings = svc.getSettings();
|
|
112
|
+
expect(settings.enabled).toBe(true);
|
|
113
|
+
expect(settings.categories.context).toBe(false);
|
|
114
|
+
// Other categories should use defaults (operation is disabled by default)
|
|
115
|
+
expect(settings.categories.operation).toBe(false);
|
|
116
|
+
expect(settings.largeOperationThreshold).toBe(100_000);
|
|
117
|
+
});
|
|
118
|
+
it('should update and persist settings', () => {
|
|
119
|
+
const updated = service.updateSettings({ enabled: true });
|
|
120
|
+
expect(updated.enabled).toBe(true);
|
|
121
|
+
expect(mockWriteFileSync).toHaveBeenCalled();
|
|
122
|
+
});
|
|
123
|
+
it('should merge category updates', () => {
|
|
124
|
+
const updated = service.updateSettings({
|
|
125
|
+
categories: { context: false },
|
|
126
|
+
});
|
|
127
|
+
expect(updated.categories.context).toBe(false);
|
|
128
|
+
// Other categories should remain unchanged (operation is disabled by default)
|
|
129
|
+
expect(updated.categories.operation).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
describe('context threshold notifications', () => {
|
|
133
|
+
it('should fire when crossing a threshold upward', () => {
|
|
134
|
+
const session = createMockSession({
|
|
135
|
+
context_metrics: {
|
|
136
|
+
used_percentage: 55,
|
|
137
|
+
remaining_percentage: 45,
|
|
138
|
+
total_input_tokens: 100000,
|
|
139
|
+
total_output_tokens: 10000,
|
|
140
|
+
context_window_size: 200000,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
// First call with 0% -> 55% should fire 50% threshold
|
|
144
|
+
service.onContextUpdate(session);
|
|
145
|
+
expect(broadcastCalls).toHaveLength(1);
|
|
146
|
+
expect(broadcastCalls[0].notification.category).toBe('context');
|
|
147
|
+
expect(broadcastCalls[0].notification.title).toBe('Context reached 55%');
|
|
148
|
+
expect(broadcastCalls[0].notification.priority).toBe('medium');
|
|
149
|
+
});
|
|
150
|
+
it('should fire both 50% and 70% when jumping past both', () => {
|
|
151
|
+
const session = createMockSession({
|
|
152
|
+
context_metrics: {
|
|
153
|
+
used_percentage: 75,
|
|
154
|
+
remaining_percentage: 25,
|
|
155
|
+
total_input_tokens: 150000,
|
|
156
|
+
total_output_tokens: 10000,
|
|
157
|
+
context_window_size: 200000,
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
service.onContextUpdate(session);
|
|
161
|
+
// Should fire 50% and 70% (no 90% — it's not in thresholds)
|
|
162
|
+
expect(broadcastCalls).toHaveLength(2);
|
|
163
|
+
expect(broadcastCalls[0].notification.title).toBe('Context reached 75%');
|
|
164
|
+
expect(broadcastCalls[1].notification.title).toBe('Context reached 75%');
|
|
165
|
+
});
|
|
166
|
+
it('should only fire at 50% and 70% thresholds (not 90%)', () => {
|
|
167
|
+
const session = createMockSession({
|
|
168
|
+
context_metrics: {
|
|
169
|
+
used_percentage: 95,
|
|
170
|
+
remaining_percentage: 5,
|
|
171
|
+
total_input_tokens: 190000,
|
|
172
|
+
total_output_tokens: 10000,
|
|
173
|
+
context_window_size: 200000,
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
service.onContextUpdate(session);
|
|
177
|
+
// Default thresholds are [50, 70] — no 90%
|
|
178
|
+
expect(broadcastCalls).toHaveLength(2);
|
|
179
|
+
expect(broadcastCalls[0].notification.title).toBe('Context reached 95%');
|
|
180
|
+
expect(broadcastCalls[1].notification.title).toBe('Context reached 95%');
|
|
181
|
+
});
|
|
182
|
+
it('should not re-fire same threshold for same session', () => {
|
|
183
|
+
const session50 = createMockSession({
|
|
184
|
+
context_metrics: {
|
|
185
|
+
used_percentage: 55,
|
|
186
|
+
remaining_percentage: 45,
|
|
187
|
+
total_input_tokens: 100000,
|
|
188
|
+
total_output_tokens: 10000,
|
|
189
|
+
context_window_size: 200000,
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
service.onContextUpdate(session50);
|
|
193
|
+
expect(broadcastCalls).toHaveLength(1);
|
|
194
|
+
// Same session, same percentage - should not fire again
|
|
195
|
+
const session55 = createMockSession({
|
|
196
|
+
context_metrics: {
|
|
197
|
+
used_percentage: 58,
|
|
198
|
+
remaining_percentage: 42,
|
|
199
|
+
total_input_tokens: 110000,
|
|
200
|
+
total_output_tokens: 10000,
|
|
201
|
+
context_window_size: 200000,
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
service.onContextUpdate(session55);
|
|
205
|
+
expect(broadcastCalls).toHaveLength(1); // still 1
|
|
206
|
+
});
|
|
207
|
+
it('should set correct priority for different thresholds', () => {
|
|
208
|
+
// Jump from 0 to 75
|
|
209
|
+
const session = createMockSession({
|
|
210
|
+
context_metrics: {
|
|
211
|
+
used_percentage: 75,
|
|
212
|
+
remaining_percentage: 25,
|
|
213
|
+
total_input_tokens: 150000,
|
|
214
|
+
total_output_tokens: 10000,
|
|
215
|
+
context_window_size: 200000,
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
service.onContextUpdate(session);
|
|
219
|
+
expect(broadcastCalls[0].notification.priority).toBe('medium'); // 50%
|
|
220
|
+
expect(broadcastCalls[1].notification.priority).toBe('high'); // 70%
|
|
221
|
+
});
|
|
222
|
+
it('should not fire when context percentage is null', () => {
|
|
223
|
+
const session = createMockSession({ context_metrics: null });
|
|
224
|
+
service.onContextUpdate(session);
|
|
225
|
+
expect(broadcastCalls).toHaveLength(0);
|
|
226
|
+
});
|
|
227
|
+
it('should include sessionId in context notifications', () => {
|
|
228
|
+
const session = createMockSession({
|
|
229
|
+
context_metrics: {
|
|
230
|
+
used_percentage: 55,
|
|
231
|
+
remaining_percentage: 45,
|
|
232
|
+
total_input_tokens: 100000,
|
|
233
|
+
total_output_tokens: 10000,
|
|
234
|
+
context_window_size: 200000,
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
service.onContextUpdate(session);
|
|
238
|
+
expect(broadcastCalls[0].notification.sessionId).toBe('test-session-1');
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
describe('category gating', () => {
|
|
242
|
+
it('should not fire when category is disabled', () => {
|
|
243
|
+
service.updateSettings({
|
|
244
|
+
categories: { context: false },
|
|
245
|
+
});
|
|
246
|
+
const session = createMockSession({
|
|
247
|
+
context_metrics: {
|
|
248
|
+
used_percentage: 55,
|
|
249
|
+
remaining_percentage: 45,
|
|
250
|
+
total_input_tokens: 100000,
|
|
251
|
+
total_output_tokens: 10000,
|
|
252
|
+
context_window_size: 200000,
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
service.onContextUpdate(session);
|
|
256
|
+
expect(broadcastCalls).toHaveLength(0);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
describe('cooldowns', () => {
|
|
260
|
+
it('should respect cooldown period for same key', () => {
|
|
261
|
+
// Enable operation category (disabled by default)
|
|
262
|
+
service.updateSettings({ categories: { operation: true } });
|
|
263
|
+
const session = createMockSession({
|
|
264
|
+
context_metrics: {
|
|
265
|
+
used_percentage: 55,
|
|
266
|
+
remaining_percentage: 45,
|
|
267
|
+
total_input_tokens: 100000,
|
|
268
|
+
total_output_tokens: 10000,
|
|
269
|
+
context_window_size: 200000,
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
service.onContextUpdate(session);
|
|
273
|
+
expect(broadcastCalls).toHaveLength(1);
|
|
274
|
+
// Threshold deduplication prevents re-fire anyway, but cooldown also applies
|
|
275
|
+
// Test with operations instead
|
|
276
|
+
service.onClaudeOperation({
|
|
277
|
+
id: 'op-1',
|
|
278
|
+
operation: 'llm-handoff',
|
|
279
|
+
phase: 'complete',
|
|
280
|
+
totalTokens: 100_000,
|
|
281
|
+
});
|
|
282
|
+
expect(broadcastCalls).toHaveLength(2);
|
|
283
|
+
// Same key within cooldown should not fire
|
|
284
|
+
service.onClaudeOperation({
|
|
285
|
+
id: 'op-1',
|
|
286
|
+
operation: 'llm-handoff',
|
|
287
|
+
phase: 'complete',
|
|
288
|
+
totalTokens: 100_000,
|
|
289
|
+
});
|
|
290
|
+
expect(broadcastCalls).toHaveLength(2); // still 2
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
describe('desktop notifications', () => {
|
|
294
|
+
it('should call node-notifier with wait: true when enabled', () => {
|
|
295
|
+
service.updateSettings({ enabled: true });
|
|
296
|
+
const session = createMockSession({
|
|
297
|
+
context_metrics: {
|
|
298
|
+
used_percentage: 55,
|
|
299
|
+
remaining_percentage: 45,
|
|
300
|
+
total_input_tokens: 100000,
|
|
301
|
+
total_output_tokens: 10000,
|
|
302
|
+
context_window_size: 200000,
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
service.onContextUpdate(session);
|
|
306
|
+
expect(notifier.notify).toHaveBeenCalledWith(expect.objectContaining({
|
|
307
|
+
title: expect.any(String),
|
|
308
|
+
subtitle: expect.any(String),
|
|
309
|
+
sound: 'Sosumi',
|
|
310
|
+
wait: true,
|
|
311
|
+
actions: ['Focus'],
|
|
312
|
+
}), expect.any(Function));
|
|
313
|
+
});
|
|
314
|
+
it('should not call node-notifier when disabled', () => {
|
|
315
|
+
// Default is disabled
|
|
316
|
+
const session = createMockSession({
|
|
317
|
+
context_metrics: {
|
|
318
|
+
used_percentage: 55,
|
|
319
|
+
remaining_percentage: 45,
|
|
320
|
+
total_input_tokens: 100000,
|
|
321
|
+
total_output_tokens: 10000,
|
|
322
|
+
context_window_size: 200000,
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
service.onContextUpdate(session);
|
|
326
|
+
// Should still broadcast to GUI
|
|
327
|
+
expect(broadcastCalls).toHaveLength(1);
|
|
328
|
+
// But not call notifier
|
|
329
|
+
expect(notifier.notify).not.toHaveBeenCalled();
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
describe('click-to-focus callback', () => {
|
|
333
|
+
it('should call focusTerminal when notification is clicked (activate response)', () => {
|
|
334
|
+
const focusTerminal = jest.fn();
|
|
335
|
+
const svc = new NotificationService({
|
|
336
|
+
broadcast: (msg) => broadcastCalls.push(msg),
|
|
337
|
+
focusTerminal,
|
|
338
|
+
logger: silentLogger,
|
|
339
|
+
});
|
|
340
|
+
svc.updateSettings({ enabled: true });
|
|
341
|
+
const session = createMockSession({
|
|
342
|
+
context_metrics: {
|
|
343
|
+
used_percentage: 55,
|
|
344
|
+
remaining_percentage: 45,
|
|
345
|
+
total_input_tokens: 100000,
|
|
346
|
+
total_output_tokens: 10000,
|
|
347
|
+
context_window_size: 200000,
|
|
348
|
+
},
|
|
349
|
+
});
|
|
350
|
+
svc.onContextUpdate(session);
|
|
351
|
+
// Simulate the notifier callback with 'activate' response
|
|
352
|
+
const notifyCall = notifier.notify.mock.calls[0];
|
|
353
|
+
const callback = notifyCall[1];
|
|
354
|
+
callback(null, 'activate');
|
|
355
|
+
expect(focusTerminal).toHaveBeenCalledWith('test-session-1');
|
|
356
|
+
});
|
|
357
|
+
it('should NOT call focusTerminal on dismiss response', () => {
|
|
358
|
+
const focusTerminal = jest.fn();
|
|
359
|
+
const svc = new NotificationService({
|
|
360
|
+
broadcast: (msg) => broadcastCalls.push(msg),
|
|
361
|
+
focusTerminal,
|
|
362
|
+
logger: silentLogger,
|
|
363
|
+
});
|
|
364
|
+
svc.updateSettings({ enabled: true });
|
|
365
|
+
const session = createMockSession({
|
|
366
|
+
context_metrics: {
|
|
367
|
+
used_percentage: 55,
|
|
368
|
+
remaining_percentage: 45,
|
|
369
|
+
total_input_tokens: 100000,
|
|
370
|
+
total_output_tokens: 10000,
|
|
371
|
+
context_window_size: 200000,
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
svc.onContextUpdate(session);
|
|
375
|
+
const notifyCall = notifier.notify.mock.calls[0];
|
|
376
|
+
const callback = notifyCall[1];
|
|
377
|
+
callback(null, 'dismissed');
|
|
378
|
+
expect(focusTerminal).not.toHaveBeenCalled();
|
|
379
|
+
});
|
|
380
|
+
it('should NOT call focusTerminal when sessionId is missing', () => {
|
|
381
|
+
const focusTerminal = jest.fn();
|
|
382
|
+
const svc = new NotificationService({
|
|
383
|
+
broadcast: (msg) => broadcastCalls.push(msg),
|
|
384
|
+
focusTerminal,
|
|
385
|
+
logger: silentLogger,
|
|
386
|
+
});
|
|
387
|
+
svc.updateSettings({ enabled: true, categories: { operation: true } });
|
|
388
|
+
// Operations don't have sessionId
|
|
389
|
+
svc.onClaudeOperation({
|
|
390
|
+
id: 'op-click-test',
|
|
391
|
+
operation: 'llm-handoff',
|
|
392
|
+
phase: 'complete',
|
|
393
|
+
totalTokens: 100_000,
|
|
394
|
+
});
|
|
395
|
+
const notifyCall = notifier.notify.mock.calls[0];
|
|
396
|
+
const callback = notifyCall[1];
|
|
397
|
+
callback(null, 'activate');
|
|
398
|
+
expect(focusTerminal).not.toHaveBeenCalled();
|
|
399
|
+
});
|
|
400
|
+
it('should call focusTerminal when Focus action is clicked', () => {
|
|
401
|
+
const focusTerminal = jest.fn();
|
|
402
|
+
const svc = new NotificationService({
|
|
403
|
+
broadcast: (msg) => broadcastCalls.push(msg),
|
|
404
|
+
focusTerminal,
|
|
405
|
+
logger: silentLogger,
|
|
406
|
+
});
|
|
407
|
+
svc.updateSettings({ enabled: true });
|
|
408
|
+
const session = createMockSession({
|
|
409
|
+
context_metrics: {
|
|
410
|
+
used_percentage: 55,
|
|
411
|
+
remaining_percentage: 45,
|
|
412
|
+
total_input_tokens: 100000,
|
|
413
|
+
total_output_tokens: 10000,
|
|
414
|
+
context_window_size: 200000,
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
svc.onContextUpdate(session);
|
|
418
|
+
const notifyCall = notifier.notify.mock.calls[0];
|
|
419
|
+
const callback = notifyCall[1];
|
|
420
|
+
callback(null, 'Focus');
|
|
421
|
+
expect(focusTerminal).toHaveBeenCalledWith('test-session-1');
|
|
422
|
+
});
|
|
423
|
+
it('should handle focusTerminal callback errors gracefully', () => {
|
|
424
|
+
const focusTerminal = jest.fn().mockImplementation(() => {
|
|
425
|
+
throw new Error('Terminal not found');
|
|
426
|
+
});
|
|
427
|
+
const svc = new NotificationService({
|
|
428
|
+
broadcast: (msg) => broadcastCalls.push(msg),
|
|
429
|
+
focusTerminal,
|
|
430
|
+
logger: silentLogger,
|
|
431
|
+
});
|
|
432
|
+
svc.updateSettings({ enabled: true });
|
|
433
|
+
const session = createMockSession({
|
|
434
|
+
context_metrics: {
|
|
435
|
+
used_percentage: 55,
|
|
436
|
+
remaining_percentage: 45,
|
|
437
|
+
total_input_tokens: 100000,
|
|
438
|
+
total_output_tokens: 10000,
|
|
439
|
+
context_window_size: 200000,
|
|
440
|
+
},
|
|
441
|
+
});
|
|
442
|
+
svc.onContextUpdate(session);
|
|
443
|
+
const notifyCall = notifier.notify.mock.calls[0];
|
|
444
|
+
const callback = notifyCall[1];
|
|
445
|
+
// Should not throw
|
|
446
|
+
expect(() => callback(null, 'activate')).not.toThrow();
|
|
447
|
+
expect(focusTerminal).toHaveBeenCalled();
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
describe('plan ready notifications', () => {
|
|
451
|
+
it('should fire plan notification with session context', () => {
|
|
452
|
+
service.onPlanReady('test-session-1', 'Refactor auth system');
|
|
453
|
+
expect(broadcastCalls).toHaveLength(1);
|
|
454
|
+
expect(broadcastCalls[0].notification.category).toBe('plan');
|
|
455
|
+
expect(broadcastCalls[0].notification.title).toBe('Plan: Refactor auth system');
|
|
456
|
+
});
|
|
457
|
+
it('should include sessionId in plan notification', () => {
|
|
458
|
+
service.onPlanReady('test-session-1', 'My Plan');
|
|
459
|
+
expect(broadcastCalls[0].notification.sessionId).toBe('test-session-1');
|
|
460
|
+
});
|
|
461
|
+
it('should respect plan category toggle (disabled)', () => {
|
|
462
|
+
service.updateSettings({
|
|
463
|
+
categories: { plan: false },
|
|
464
|
+
});
|
|
465
|
+
service.onPlanReady('test-session-1', 'My Plan');
|
|
466
|
+
expect(broadcastCalls).toHaveLength(0);
|
|
467
|
+
});
|
|
468
|
+
it('should respect plan cooldown period for same session', () => {
|
|
469
|
+
service.onPlanReady('test-session-1', 'Plan A');
|
|
470
|
+
expect(broadcastCalls).toHaveLength(1);
|
|
471
|
+
// Same session within cooldown window — key includes Date.now() so
|
|
472
|
+
// if called within the same ms tick, the key is identical and cooldown blocks it.
|
|
473
|
+
// Different sessions should work:
|
|
474
|
+
service.onPlanReady('test-session-2', 'Plan B');
|
|
475
|
+
expect(broadcastCalls).toHaveLength(2);
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
describe('claude operations', () => {
|
|
479
|
+
it('should fire for large operations', () => {
|
|
480
|
+
// Enable operation category (disabled by default)
|
|
481
|
+
service.updateSettings({ categories: { operation: true } });
|
|
482
|
+
service.onClaudeOperation({
|
|
483
|
+
id: 'op-1',
|
|
484
|
+
operation: 'llm-handoff',
|
|
485
|
+
phase: 'complete',
|
|
486
|
+
totalTokens: 100_000,
|
|
487
|
+
userPromptPreview: 'Fix the auth bug',
|
|
488
|
+
});
|
|
489
|
+
expect(broadcastCalls).toHaveLength(1);
|
|
490
|
+
expect(broadcastCalls[0].notification.category).toBe('operation');
|
|
491
|
+
expect(broadcastCalls[0].notification.title).toContain('100k');
|
|
492
|
+
expect(broadcastCalls[0].notification.priority).toBe('high');
|
|
493
|
+
});
|
|
494
|
+
it('should not fire for small operations', () => {
|
|
495
|
+
service.onClaudeOperation({
|
|
496
|
+
id: 'op-2',
|
|
497
|
+
operation: 'llm-handoff',
|
|
498
|
+
phase: 'complete',
|
|
499
|
+
totalTokens: 10_000,
|
|
500
|
+
});
|
|
501
|
+
expect(broadcastCalls).toHaveLength(0);
|
|
502
|
+
});
|
|
503
|
+
it('should not fire for start phase', () => {
|
|
504
|
+
service.onClaudeOperation({
|
|
505
|
+
id: 'op-3',
|
|
506
|
+
operation: 'llm-handoff',
|
|
507
|
+
phase: 'start',
|
|
508
|
+
totalTokens: 100_000,
|
|
509
|
+
});
|
|
510
|
+
expect(broadcastCalls).toHaveLength(0);
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
describe('handoff notifications', () => {
|
|
514
|
+
it('should fire when handoff is ready', () => {
|
|
515
|
+
service.onHandoffReady('test-session', '/project/.jacques/handoffs/2024-01-01-handoff.md');
|
|
516
|
+
expect(broadcastCalls).toHaveLength(1);
|
|
517
|
+
expect(broadcastCalls[0].notification.category).toBe('handoff');
|
|
518
|
+
expect(broadcastCalls[0].notification.title).toBe('Handoff Ready');
|
|
519
|
+
expect(broadcastCalls[0].notification.body).toContain('2024-01-01-handoff.md');
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
describe('session removal cleanup', () => {
|
|
523
|
+
it('should clean up tracking state for removed sessions', () => {
|
|
524
|
+
// Fire a notification for a session at 70%
|
|
525
|
+
const session70 = createMockSession({
|
|
526
|
+
context_metrics: {
|
|
527
|
+
used_percentage: 75,
|
|
528
|
+
remaining_percentage: 25,
|
|
529
|
+
total_input_tokens: 150000,
|
|
530
|
+
total_output_tokens: 10000,
|
|
531
|
+
context_window_size: 200000,
|
|
532
|
+
},
|
|
533
|
+
});
|
|
534
|
+
service.onContextUpdate(session70);
|
|
535
|
+
// Should fire 50% and 70% thresholds
|
|
536
|
+
expect(broadcastCalls.length).toBeGreaterThanOrEqual(2);
|
|
537
|
+
const countBefore = broadcastCalls.length;
|
|
538
|
+
// Remove the session
|
|
539
|
+
service.onSessionRemoved('test-session-1');
|
|
540
|
+
// Use a different session to verify cleanup works
|
|
541
|
+
const session2 = createMockSession({
|
|
542
|
+
session_id: 'test-session-2',
|
|
543
|
+
session_title: 'Session 2',
|
|
544
|
+
context_metrics: {
|
|
545
|
+
used_percentage: 55,
|
|
546
|
+
remaining_percentage: 45,
|
|
547
|
+
total_input_tokens: 100000,
|
|
548
|
+
total_output_tokens: 10000,
|
|
549
|
+
context_window_size: 200000,
|
|
550
|
+
},
|
|
551
|
+
});
|
|
552
|
+
service.onContextUpdate(session2);
|
|
553
|
+
// New session should fire without being blocked
|
|
554
|
+
expect(broadcastCalls.length).toBe(countBefore + 1);
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
describe('notification history', () => {
|
|
558
|
+
it('should maintain a history of notifications', () => {
|
|
559
|
+
const session = createMockSession({
|
|
560
|
+
context_metrics: {
|
|
561
|
+
used_percentage: 75,
|
|
562
|
+
remaining_percentage: 25,
|
|
563
|
+
total_input_tokens: 150000,
|
|
564
|
+
total_output_tokens: 10000,
|
|
565
|
+
context_window_size: 200000,
|
|
566
|
+
},
|
|
567
|
+
});
|
|
568
|
+
service.onContextUpdate(session);
|
|
569
|
+
const history = service.getHistory();
|
|
570
|
+
expect(history).toHaveLength(2); // 50%, 70%
|
|
571
|
+
// Newest first — both show the actual percentage
|
|
572
|
+
expect(history[0].title).toBe('Context reached 75%');
|
|
573
|
+
expect(history[1].title).toBe('Context reached 75%');
|
|
574
|
+
});
|
|
575
|
+
it('should cap history at MAX_NOTIFICATION_HISTORY', () => {
|
|
576
|
+
// Fire many notifications using operations with unique keys
|
|
577
|
+
for (let i = 0; i < 60; i++) {
|
|
578
|
+
service.onClaudeOperation({
|
|
579
|
+
id: `op-${i}`,
|
|
580
|
+
operation: 'llm-handoff',
|
|
581
|
+
phase: 'complete',
|
|
582
|
+
totalTokens: 100_000,
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
const history = service.getHistory();
|
|
586
|
+
expect(history.length).toBeLessThanOrEqual(50);
|
|
587
|
+
});
|
|
588
|
+
it('should include sessionId in all session-related notifications', () => {
|
|
589
|
+
service.onHandoffReady('sess-abc', '/some/path/handoff.md');
|
|
590
|
+
expect(broadcastCalls[0].notification.sessionId).toBe('sess-abc');
|
|
591
|
+
service.onPlanReady('sess-xyz', 'Plan title');
|
|
592
|
+
expect(broadcastCalls[1].notification.sessionId).toBe('sess-xyz');
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
describe('broadcast message shape', () => {
|
|
596
|
+
it('should broadcast notification_fired with complete NotificationItem', () => {
|
|
597
|
+
const session = createMockSession({
|
|
598
|
+
context_metrics: {
|
|
599
|
+
used_percentage: 55,
|
|
600
|
+
remaining_percentage: 45,
|
|
601
|
+
total_input_tokens: 100000,
|
|
602
|
+
total_output_tokens: 10000,
|
|
603
|
+
context_window_size: 200000,
|
|
604
|
+
},
|
|
605
|
+
});
|
|
606
|
+
service.onContextUpdate(session);
|
|
607
|
+
expect(broadcastCalls).toHaveLength(1);
|
|
608
|
+
const msg = broadcastCalls[0];
|
|
609
|
+
expect(msg.type).toBe('notification_fired');
|
|
610
|
+
expect(msg.notification).toEqual(expect.objectContaining({
|
|
611
|
+
id: expect.stringMatching(/^notif-/),
|
|
612
|
+
category: 'context',
|
|
613
|
+
title: expect.any(String),
|
|
614
|
+
body: expect.any(String),
|
|
615
|
+
priority: expect.stringMatching(/^(low|medium|high|critical)$/),
|
|
616
|
+
timestamp: expect.any(Number),
|
|
617
|
+
sessionId: 'test-session-1',
|
|
618
|
+
}));
|
|
619
|
+
});
|
|
620
|
+
it('should include projectName and branchName when getSession is provided', () => {
|
|
621
|
+
const svc = new NotificationService({
|
|
622
|
+
broadcast: (msg) => broadcastCalls.push(msg),
|
|
623
|
+
getSession: (id) => id === 'test-session-1' ? {
|
|
624
|
+
project: 'my-project',
|
|
625
|
+
git_branch: 'feat/auth',
|
|
626
|
+
session_title: 'Test Session',
|
|
627
|
+
} : undefined,
|
|
628
|
+
logger: silentLogger,
|
|
629
|
+
});
|
|
630
|
+
const session = createMockSession({
|
|
631
|
+
context_metrics: {
|
|
632
|
+
used_percentage: 55,
|
|
633
|
+
remaining_percentage: 45,
|
|
634
|
+
total_input_tokens: 100000,
|
|
635
|
+
total_output_tokens: 10000,
|
|
636
|
+
context_window_size: 200000,
|
|
637
|
+
},
|
|
638
|
+
});
|
|
639
|
+
svc.onContextUpdate(session);
|
|
640
|
+
expect(broadcastCalls).toHaveLength(1);
|
|
641
|
+
expect(broadcastCalls[0].notification.projectName).toBe('my-project');
|
|
642
|
+
expect(broadcastCalls[0].notification.branchName).toBe('feat/auth');
|
|
643
|
+
});
|
|
644
|
+
it('should omit projectName and branchName when getSession is not provided', () => {
|
|
645
|
+
const session = createMockSession({
|
|
646
|
+
context_metrics: {
|
|
647
|
+
used_percentage: 55,
|
|
648
|
+
remaining_percentage: 45,
|
|
649
|
+
total_input_tokens: 100000,
|
|
650
|
+
total_output_tokens: 10000,
|
|
651
|
+
context_window_size: 200000,
|
|
652
|
+
},
|
|
653
|
+
});
|
|
654
|
+
service.onContextUpdate(session);
|
|
655
|
+
expect(broadcastCalls).toHaveLength(1);
|
|
656
|
+
expect(broadcastCalls[0].notification.projectName).toBeUndefined();
|
|
657
|
+
expect(broadcastCalls[0].notification.branchName).toBeUndefined();
|
|
658
|
+
});
|
|
659
|
+
});
|
|
660
|
+
describe('bug alert (scanForErrors)', () => {
|
|
661
|
+
beforeEach(() => {
|
|
662
|
+
// Enable bug-alert category (disabled by default)
|
|
663
|
+
service.updateSettings({ categories: { 'bug-alert': true } });
|
|
664
|
+
});
|
|
665
|
+
function makeErrorEntry() {
|
|
666
|
+
return JSON.stringify({
|
|
667
|
+
type: 'assistant',
|
|
668
|
+
message: {
|
|
669
|
+
content: [
|
|
670
|
+
{ type: 'tool_result', is_error: true, content: 'Error: something failed' },
|
|
671
|
+
],
|
|
672
|
+
},
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
function makeNormalEntry() {
|
|
676
|
+
return JSON.stringify({
|
|
677
|
+
type: 'assistant',
|
|
678
|
+
message: {
|
|
679
|
+
content: [
|
|
680
|
+
{ type: 'text', text: 'All good' },
|
|
681
|
+
],
|
|
682
|
+
},
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
it('should fire bug-alert after reaching error threshold (default 5)', async () => {
|
|
686
|
+
const lines = Array.from({ length: 5 }, () => makeErrorEntry()).join('\n');
|
|
687
|
+
const buf = Buffer.from(lines);
|
|
688
|
+
mockFsStat.mockResolvedValue({ size: buf.length });
|
|
689
|
+
mockFsOpen.mockResolvedValue({ read: mockFhRead, close: mockFhClose });
|
|
690
|
+
mockFhRead.mockImplementation((buffer) => {
|
|
691
|
+
buf.copy(buffer);
|
|
692
|
+
return Promise.resolve({ bytesRead: buf.length });
|
|
693
|
+
});
|
|
694
|
+
mockFhClose.mockResolvedValue(undefined);
|
|
695
|
+
await service.scanForErrors('sess-1', '/fake/path.jsonl');
|
|
696
|
+
expect(broadcastCalls).toHaveLength(1);
|
|
697
|
+
expect(broadcastCalls[0].notification.category).toBe('bug-alert');
|
|
698
|
+
expect(broadcastCalls[0].notification.title).toBe('5 tool errors');
|
|
699
|
+
});
|
|
700
|
+
it('should not fire bug-alert below threshold', async () => {
|
|
701
|
+
const lines = Array.from({ length: 3 }, () => makeErrorEntry()).join('\n');
|
|
702
|
+
const buf = Buffer.from(lines);
|
|
703
|
+
mockFsStat.mockResolvedValue({ size: buf.length });
|
|
704
|
+
mockFsOpen.mockResolvedValue({ read: mockFhRead, close: mockFhClose });
|
|
705
|
+
mockFhRead.mockImplementation((buffer) => {
|
|
706
|
+
buf.copy(buffer);
|
|
707
|
+
return Promise.resolve({ bytesRead: buf.length });
|
|
708
|
+
});
|
|
709
|
+
mockFhClose.mockResolvedValue(undefined);
|
|
710
|
+
await service.scanForErrors('sess-1', '/fake/path.jsonl');
|
|
711
|
+
expect(broadcastCalls).toHaveLength(0);
|
|
712
|
+
});
|
|
713
|
+
it('should reset counter after firing', async () => {
|
|
714
|
+
// First batch: 5 errors → fires
|
|
715
|
+
const batch1 = Array.from({ length: 5 }, () => makeErrorEntry()).join('\n');
|
|
716
|
+
const buf1 = Buffer.from(batch1);
|
|
717
|
+
mockFsStat.mockResolvedValue({ size: buf1.length });
|
|
718
|
+
mockFsOpen.mockResolvedValue({ read: mockFhRead, close: mockFhClose });
|
|
719
|
+
mockFhRead.mockImplementation((buffer) => {
|
|
720
|
+
buf1.copy(buffer);
|
|
721
|
+
return Promise.resolve({ bytesRead: buf1.length });
|
|
722
|
+
});
|
|
723
|
+
mockFhClose.mockResolvedValue(undefined);
|
|
724
|
+
await service.scanForErrors('sess-1', '/fake/path.jsonl');
|
|
725
|
+
expect(broadcastCalls).toHaveLength(1);
|
|
726
|
+
// Second batch: 2 more errors → should not fire (reset to 0, accumulated 2 < 5)
|
|
727
|
+
const batch2 = Array.from({ length: 2 }, () => makeErrorEntry()).join('\n');
|
|
728
|
+
const buf2 = Buffer.from(batch2);
|
|
729
|
+
const newSize = buf1.length + buf2.length;
|
|
730
|
+
mockFsStat.mockResolvedValue({ size: newSize });
|
|
731
|
+
mockFsOpen.mockResolvedValue({ read: mockFhRead, close: mockFhClose });
|
|
732
|
+
mockFhRead.mockImplementation((buffer) => {
|
|
733
|
+
buf2.copy(buffer);
|
|
734
|
+
return Promise.resolve({ bytesRead: buf2.length });
|
|
735
|
+
});
|
|
736
|
+
mockFhClose.mockResolvedValue(undefined);
|
|
737
|
+
await service.scanForErrors('sess-1', '/fake/path.jsonl');
|
|
738
|
+
// Cooldown blocks the same key within 120s, but counter should be 2 (not fire)
|
|
739
|
+
expect(broadcastCalls).toHaveLength(1); // still 1
|
|
740
|
+
});
|
|
741
|
+
it('should only read new content via byte offset', async () => {
|
|
742
|
+
const lines = Array.from({ length: 2 }, () => makeErrorEntry()).join('\n');
|
|
743
|
+
const buf = Buffer.from(lines);
|
|
744
|
+
mockFsStat.mockResolvedValue({ size: buf.length });
|
|
745
|
+
mockFsOpen.mockResolvedValue({ read: mockFhRead, close: mockFhClose });
|
|
746
|
+
mockFhRead.mockImplementation((buffer) => {
|
|
747
|
+
buf.copy(buffer);
|
|
748
|
+
return Promise.resolve({ bytesRead: buf.length });
|
|
749
|
+
});
|
|
750
|
+
mockFhClose.mockResolvedValue(undefined);
|
|
751
|
+
await service.scanForErrors('sess-1', '/fake/path.jsonl');
|
|
752
|
+
// Second call with same size → no read needed
|
|
753
|
+
await service.scanForErrors('sess-1', '/fake/path.jsonl');
|
|
754
|
+
// fsOpen should only be called once (second call skips because size unchanged)
|
|
755
|
+
expect(mockFsOpen).toHaveBeenCalledTimes(1);
|
|
756
|
+
});
|
|
757
|
+
it('should skip malformed JSONL lines', async () => {
|
|
758
|
+
const lines = [
|
|
759
|
+
makeErrorEntry(),
|
|
760
|
+
'not valid json',
|
|
761
|
+
makeErrorEntry(),
|
|
762
|
+
'{ broken',
|
|
763
|
+
makeErrorEntry(),
|
|
764
|
+
].join('\n');
|
|
765
|
+
const buf = Buffer.from(lines);
|
|
766
|
+
mockFsStat.mockResolvedValue({ size: buf.length });
|
|
767
|
+
mockFsOpen.mockResolvedValue({ read: mockFhRead, close: mockFhClose });
|
|
768
|
+
mockFhRead.mockImplementation((buffer) => {
|
|
769
|
+
buf.copy(buffer);
|
|
770
|
+
return Promise.resolve({ bytesRead: buf.length });
|
|
771
|
+
});
|
|
772
|
+
mockFhClose.mockResolvedValue(undefined);
|
|
773
|
+
await service.scanForErrors('sess-1', '/fake/path.jsonl');
|
|
774
|
+
// 3 errors < 5 threshold → no fire, but no crash either
|
|
775
|
+
expect(broadcastCalls).toHaveLength(0);
|
|
776
|
+
});
|
|
777
|
+
it('should use high priority when 10+ errors', async () => {
|
|
778
|
+
const lines = Array.from({ length: 10 }, () => makeErrorEntry()).join('\n');
|
|
779
|
+
const buf = Buffer.from(lines);
|
|
780
|
+
mockFsStat.mockResolvedValue({ size: buf.length });
|
|
781
|
+
mockFsOpen.mockResolvedValue({ read: mockFhRead, close: mockFhClose });
|
|
782
|
+
mockFhRead.mockImplementation((buffer) => {
|
|
783
|
+
buf.copy(buffer);
|
|
784
|
+
return Promise.resolve({ bytesRead: buf.length });
|
|
785
|
+
});
|
|
786
|
+
mockFhClose.mockResolvedValue(undefined);
|
|
787
|
+
await service.scanForErrors('sess-1', '/fake/path.jsonl');
|
|
788
|
+
expect(broadcastCalls).toHaveLength(1);
|
|
789
|
+
expect(broadcastCalls[0].notification.priority).toBe('high');
|
|
790
|
+
});
|
|
791
|
+
it('should ignore non-error tool results', async () => {
|
|
792
|
+
const lines = Array.from({ length: 10 }, () => makeNormalEntry()).join('\n');
|
|
793
|
+
const buf = Buffer.from(lines);
|
|
794
|
+
mockFsStat.mockResolvedValue({ size: buf.length });
|
|
795
|
+
mockFsOpen.mockResolvedValue({ read: mockFhRead, close: mockFhClose });
|
|
796
|
+
mockFhRead.mockImplementation((buffer) => {
|
|
797
|
+
buf.copy(buffer);
|
|
798
|
+
return Promise.resolve({ bytesRead: buf.length });
|
|
799
|
+
});
|
|
800
|
+
mockFhClose.mockResolvedValue(undefined);
|
|
801
|
+
await service.scanForErrors('sess-1', '/fake/path.jsonl');
|
|
802
|
+
expect(broadcastCalls).toHaveLength(0);
|
|
803
|
+
});
|
|
804
|
+
});
|
|
805
|
+
describe('plan detection (checkForNewPlans)', () => {
|
|
806
|
+
it('should fire for newly discovered plans', async () => {
|
|
807
|
+
mockParseJSONL.mockResolvedValue([]);
|
|
808
|
+
mockDetectModeAndPlans.mockReturnValue({
|
|
809
|
+
mode: 'planning',
|
|
810
|
+
planRefs: [
|
|
811
|
+
{ title: 'Refactor auth', source: 'embedded', messageIndex: 0 },
|
|
812
|
+
],
|
|
813
|
+
});
|
|
814
|
+
await service.checkForNewPlans('sess-1', '/fake/transcript.jsonl');
|
|
815
|
+
expect(broadcastCalls).toHaveLength(1);
|
|
816
|
+
expect(broadcastCalls[0].notification.category).toBe('plan');
|
|
817
|
+
expect(broadcastCalls[0].notification.title).toContain('Refactor auth');
|
|
818
|
+
});
|
|
819
|
+
it('should not re-notify for duplicate plan titles', async () => {
|
|
820
|
+
mockParseJSONL.mockResolvedValue([]);
|
|
821
|
+
mockDetectModeAndPlans.mockReturnValue({
|
|
822
|
+
mode: 'planning',
|
|
823
|
+
planRefs: [
|
|
824
|
+
{ title: 'Refactor auth', source: 'embedded', messageIndex: 0 },
|
|
825
|
+
],
|
|
826
|
+
});
|
|
827
|
+
await service.checkForNewPlans('sess-1', '/fake/transcript.jsonl');
|
|
828
|
+
expect(broadcastCalls).toHaveLength(1);
|
|
829
|
+
// Advance past 30s debounce
|
|
830
|
+
const originalNow = Date.now;
|
|
831
|
+
Date.now = () => originalNow() + 31_000;
|
|
832
|
+
try {
|
|
833
|
+
await service.checkForNewPlans('sess-1', '/fake/transcript.jsonl');
|
|
834
|
+
// Same plan title → should not fire again
|
|
835
|
+
// (cooldown on plan key also applies, but the knownPlanTitles check prevents the call)
|
|
836
|
+
expect(broadcastCalls).toHaveLength(1);
|
|
837
|
+
}
|
|
838
|
+
finally {
|
|
839
|
+
Date.now = originalNow;
|
|
840
|
+
}
|
|
841
|
+
});
|
|
842
|
+
it('should respect 30s debounce', async () => {
|
|
843
|
+
mockParseJSONL.mockResolvedValue([]);
|
|
844
|
+
mockDetectModeAndPlans.mockReturnValue({
|
|
845
|
+
mode: null,
|
|
846
|
+
planRefs: [],
|
|
847
|
+
});
|
|
848
|
+
await service.checkForNewPlans('sess-1', '/fake/transcript.jsonl');
|
|
849
|
+
// Immediate second call should be debounced
|
|
850
|
+
await service.checkForNewPlans('sess-1', '/fake/transcript.jsonl');
|
|
851
|
+
// parseJSONL should only be called once (debounce blocks second call)
|
|
852
|
+
expect(mockParseJSONL).toHaveBeenCalledTimes(1);
|
|
853
|
+
});
|
|
854
|
+
it('should fire for each new plan in a session', async () => {
|
|
855
|
+
mockParseJSONL.mockResolvedValue([]);
|
|
856
|
+
mockDetectModeAndPlans.mockReturnValue({
|
|
857
|
+
mode: 'planning',
|
|
858
|
+
planRefs: [
|
|
859
|
+
{ title: 'Plan A', source: 'embedded', messageIndex: 0 },
|
|
860
|
+
{ title: 'Plan B', source: 'write', messageIndex: 5 },
|
|
861
|
+
],
|
|
862
|
+
});
|
|
863
|
+
// Each plan uses a unique Date.now()-based key, but within the same tick
|
|
864
|
+
// they may share a timestamp. Use different sessions to avoid cooldown.
|
|
865
|
+
// Actually, onPlanReady uses `${sessionId}-plan-${Date.now()}` which
|
|
866
|
+
// can collide in the same ms. Let's verify both are discovered even
|
|
867
|
+
// if cooldown blocks the second fire (the knownPlanTitles tracks both).
|
|
868
|
+
await service.checkForNewPlans('sess-1', '/fake/transcript.jsonl');
|
|
869
|
+
// At minimum Plan A fires; Plan B may be cooldown-blocked (same ms key).
|
|
870
|
+
// Verify at least one plan fired and both titles are tracked.
|
|
871
|
+
expect(broadcastCalls.length).toBeGreaterThanOrEqual(1);
|
|
872
|
+
expect(broadcastCalls[0].notification.title).toContain('Plan A');
|
|
873
|
+
});
|
|
874
|
+
});
|
|
875
|
+
describe('bugAlertThreshold setting', () => {
|
|
876
|
+
it('should persist bugAlertThreshold to config', () => {
|
|
877
|
+
mockWriteFileSync.mockClear();
|
|
878
|
+
service.updateSettings({ bugAlertThreshold: 10 });
|
|
879
|
+
expect(mockWriteFileSync).toHaveBeenCalled();
|
|
880
|
+
const lastCall = mockWriteFileSync.mock.calls[mockWriteFileSync.mock.calls.length - 1];
|
|
881
|
+
const writtenData = JSON.parse(lastCall[1]);
|
|
882
|
+
expect(writtenData.notifications.bugAlertThreshold).toBe(10);
|
|
883
|
+
});
|
|
884
|
+
it('should update bugAlertThreshold via updateSettings', () => {
|
|
885
|
+
const updated = service.updateSettings({ bugAlertThreshold: 3 });
|
|
886
|
+
expect(updated.bugAlertThreshold).toBe(3);
|
|
887
|
+
});
|
|
888
|
+
it('should default bugAlertThreshold to 5', () => {
|
|
889
|
+
const settings = service.getSettings();
|
|
890
|
+
expect(settings.bugAlertThreshold).toBe(5);
|
|
891
|
+
});
|
|
892
|
+
});
|
|
893
|
+
describe('session removal cleanup (extended)', () => {
|
|
894
|
+
it('should clean up plan and error tracking state', async () => {
|
|
895
|
+
// Set up some plan tracking state
|
|
896
|
+
mockParseJSONL.mockResolvedValue([]);
|
|
897
|
+
mockDetectModeAndPlans.mockReturnValue({
|
|
898
|
+
mode: 'planning',
|
|
899
|
+
planRefs: [{ title: 'My Plan', source: 'embedded', messageIndex: 0 }],
|
|
900
|
+
});
|
|
901
|
+
await service.checkForNewPlans('sess-cleanup', '/fake/path.jsonl');
|
|
902
|
+
expect(broadcastCalls).toHaveLength(1);
|
|
903
|
+
// Remove session
|
|
904
|
+
service.onSessionRemoved('sess-cleanup');
|
|
905
|
+
// After removal + debounce bypass, the same plan should fire again
|
|
906
|
+
const originalNow = Date.now;
|
|
907
|
+
Date.now = () => originalNow() + 31_000;
|
|
908
|
+
try {
|
|
909
|
+
await service.checkForNewPlans('sess-cleanup', '/fake/path.jsonl');
|
|
910
|
+
expect(broadcastCalls).toHaveLength(2); // Fires again because state was cleaned
|
|
911
|
+
}
|
|
912
|
+
finally {
|
|
913
|
+
Date.now = originalNow;
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
});
|
|
917
|
+
});
|
|
918
|
+
//# sourceMappingURL=notification-service.test.js.map
|