@usejarvis/brain 0.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 (266) hide show
  1. package/LICENSE +153 -0
  2. package/README.md +278 -0
  3. package/bin/jarvis.ts +413 -0
  4. package/package.json +74 -0
  5. package/scripts/ensure-bun.cjs +8 -0
  6. package/src/actions/README.md +421 -0
  7. package/src/actions/app-control/desktop-controller.test.ts +26 -0
  8. package/src/actions/app-control/desktop-controller.ts +438 -0
  9. package/src/actions/app-control/interface.ts +64 -0
  10. package/src/actions/app-control/linux.ts +273 -0
  11. package/src/actions/app-control/macos.ts +54 -0
  12. package/src/actions/app-control/sidecar-launcher.test.ts +23 -0
  13. package/src/actions/app-control/sidecar-launcher.ts +286 -0
  14. package/src/actions/app-control/windows.ts +44 -0
  15. package/src/actions/browser/cdp.ts +138 -0
  16. package/src/actions/browser/chrome-launcher.ts +252 -0
  17. package/src/actions/browser/session.ts +437 -0
  18. package/src/actions/browser/stealth.ts +49 -0
  19. package/src/actions/index.ts +20 -0
  20. package/src/actions/terminal/executor.ts +157 -0
  21. package/src/actions/terminal/wsl-bridge.ts +126 -0
  22. package/src/actions/test.ts +93 -0
  23. package/src/actions/tools/agents.ts +321 -0
  24. package/src/actions/tools/builtin.ts +846 -0
  25. package/src/actions/tools/commitments.ts +192 -0
  26. package/src/actions/tools/content.ts +217 -0
  27. package/src/actions/tools/delegate.ts +147 -0
  28. package/src/actions/tools/desktop.test.ts +55 -0
  29. package/src/actions/tools/desktop.ts +305 -0
  30. package/src/actions/tools/goals.ts +376 -0
  31. package/src/actions/tools/local-tools-guard.ts +20 -0
  32. package/src/actions/tools/registry.ts +171 -0
  33. package/src/actions/tools/research.ts +111 -0
  34. package/src/actions/tools/sidecar-list.ts +57 -0
  35. package/src/actions/tools/sidecar-route.ts +105 -0
  36. package/src/actions/tools/workflows.ts +216 -0
  37. package/src/agents/agent.ts +132 -0
  38. package/src/agents/delegation.ts +107 -0
  39. package/src/agents/hierarchy.ts +113 -0
  40. package/src/agents/index.ts +19 -0
  41. package/src/agents/messaging.ts +125 -0
  42. package/src/agents/orchestrator.ts +576 -0
  43. package/src/agents/role-discovery.ts +61 -0
  44. package/src/agents/sub-agent-runner.ts +307 -0
  45. package/src/agents/task-manager.ts +151 -0
  46. package/src/authority/approval-delivery.ts +59 -0
  47. package/src/authority/approval.ts +196 -0
  48. package/src/authority/audit.ts +158 -0
  49. package/src/authority/authority.test.ts +519 -0
  50. package/src/authority/deferred-executor.ts +103 -0
  51. package/src/authority/emergency.ts +66 -0
  52. package/src/authority/engine.ts +297 -0
  53. package/src/authority/index.ts +12 -0
  54. package/src/authority/learning.ts +111 -0
  55. package/src/authority/tool-action-map.ts +74 -0
  56. package/src/awareness/analytics.ts +466 -0
  57. package/src/awareness/awareness.test.ts +332 -0
  58. package/src/awareness/capture-engine.ts +305 -0
  59. package/src/awareness/context-graph.ts +130 -0
  60. package/src/awareness/context-tracker.ts +349 -0
  61. package/src/awareness/index.ts +25 -0
  62. package/src/awareness/intelligence.ts +321 -0
  63. package/src/awareness/ocr-engine.ts +88 -0
  64. package/src/awareness/service.ts +528 -0
  65. package/src/awareness/struggle-detector.ts +342 -0
  66. package/src/awareness/suggestion-engine.ts +476 -0
  67. package/src/awareness/types.ts +201 -0
  68. package/src/cli/autostart.ts +241 -0
  69. package/src/cli/deps.ts +449 -0
  70. package/src/cli/doctor.ts +230 -0
  71. package/src/cli/helpers.ts +401 -0
  72. package/src/cli/onboard.ts +580 -0
  73. package/src/comms/README.md +329 -0
  74. package/src/comms/auth-error.html +48 -0
  75. package/src/comms/channels/discord.ts +228 -0
  76. package/src/comms/channels/signal.ts +56 -0
  77. package/src/comms/channels/telegram.ts +316 -0
  78. package/src/comms/channels/whatsapp.ts +60 -0
  79. package/src/comms/channels.test.ts +173 -0
  80. package/src/comms/desktop-notify.ts +114 -0
  81. package/src/comms/example.ts +129 -0
  82. package/src/comms/index.ts +129 -0
  83. package/src/comms/streaming.ts +142 -0
  84. package/src/comms/voice.test.ts +152 -0
  85. package/src/comms/voice.ts +291 -0
  86. package/src/comms/websocket.test.ts +409 -0
  87. package/src/comms/websocket.ts +473 -0
  88. package/src/config/README.md +387 -0
  89. package/src/config/index.ts +6 -0
  90. package/src/config/loader.test.ts +137 -0
  91. package/src/config/loader.ts +142 -0
  92. package/src/config/types.ts +260 -0
  93. package/src/daemon/README.md +232 -0
  94. package/src/daemon/agent-service-interface.ts +9 -0
  95. package/src/daemon/agent-service.ts +600 -0
  96. package/src/daemon/api-routes.ts +2119 -0
  97. package/src/daemon/background-agent-service.ts +396 -0
  98. package/src/daemon/background-agent.test.ts +78 -0
  99. package/src/daemon/channel-service.ts +201 -0
  100. package/src/daemon/commitment-executor.ts +297 -0
  101. package/src/daemon/event-classifier.ts +239 -0
  102. package/src/daemon/event-coalescer.ts +123 -0
  103. package/src/daemon/event-reactor.ts +214 -0
  104. package/src/daemon/health.ts +220 -0
  105. package/src/daemon/index.ts +1004 -0
  106. package/src/daemon/llm-settings.ts +316 -0
  107. package/src/daemon/observer-service.ts +150 -0
  108. package/src/daemon/pid.ts +98 -0
  109. package/src/daemon/research-queue.ts +155 -0
  110. package/src/daemon/services.ts +175 -0
  111. package/src/daemon/ws-service.ts +788 -0
  112. package/src/goals/accountability.ts +240 -0
  113. package/src/goals/awareness-bridge.ts +185 -0
  114. package/src/goals/estimator.ts +185 -0
  115. package/src/goals/events.ts +28 -0
  116. package/src/goals/goals.test.ts +400 -0
  117. package/src/goals/integration.test.ts +329 -0
  118. package/src/goals/nl-builder.test.ts +220 -0
  119. package/src/goals/nl-builder.ts +256 -0
  120. package/src/goals/rhythm.test.ts +177 -0
  121. package/src/goals/rhythm.ts +275 -0
  122. package/src/goals/service.test.ts +135 -0
  123. package/src/goals/service.ts +348 -0
  124. package/src/goals/types.ts +106 -0
  125. package/src/goals/workflow-bridge.ts +96 -0
  126. package/src/integrations/google-api.ts +134 -0
  127. package/src/integrations/google-auth.ts +175 -0
  128. package/src/llm/README.md +291 -0
  129. package/src/llm/anthropic.ts +386 -0
  130. package/src/llm/gemini.ts +371 -0
  131. package/src/llm/index.ts +19 -0
  132. package/src/llm/manager.ts +153 -0
  133. package/src/llm/ollama.ts +307 -0
  134. package/src/llm/openai.ts +350 -0
  135. package/src/llm/provider.test.ts +231 -0
  136. package/src/llm/provider.ts +60 -0
  137. package/src/llm/test.ts +87 -0
  138. package/src/observers/README.md +278 -0
  139. package/src/observers/calendar.ts +113 -0
  140. package/src/observers/clipboard.ts +136 -0
  141. package/src/observers/email.ts +109 -0
  142. package/src/observers/example.ts +58 -0
  143. package/src/observers/file-watcher.ts +124 -0
  144. package/src/observers/index.ts +159 -0
  145. package/src/observers/notifications.ts +197 -0
  146. package/src/observers/observers.test.ts +203 -0
  147. package/src/observers/processes.ts +225 -0
  148. package/src/personality/README.md +61 -0
  149. package/src/personality/adapter.ts +196 -0
  150. package/src/personality/index.ts +20 -0
  151. package/src/personality/learner.ts +209 -0
  152. package/src/personality/model.ts +132 -0
  153. package/src/personality/personality.test.ts +236 -0
  154. package/src/roles/README.md +252 -0
  155. package/src/roles/authority.ts +119 -0
  156. package/src/roles/example-usage.ts +198 -0
  157. package/src/roles/index.ts +42 -0
  158. package/src/roles/loader.ts +143 -0
  159. package/src/roles/prompt-builder.ts +194 -0
  160. package/src/roles/test-multi.ts +102 -0
  161. package/src/roles/test-role.yaml +77 -0
  162. package/src/roles/test-utils.ts +93 -0
  163. package/src/roles/test.ts +106 -0
  164. package/src/roles/tool-guide.ts +190 -0
  165. package/src/roles/types.ts +36 -0
  166. package/src/roles/utils.ts +200 -0
  167. package/src/scripts/google-setup.ts +168 -0
  168. package/src/sidecar/connection.ts +179 -0
  169. package/src/sidecar/index.ts +6 -0
  170. package/src/sidecar/manager.ts +542 -0
  171. package/src/sidecar/protocol.ts +85 -0
  172. package/src/sidecar/rpc.ts +161 -0
  173. package/src/sidecar/scheduler.ts +136 -0
  174. package/src/sidecar/types.ts +112 -0
  175. package/src/sidecar/validator.ts +144 -0
  176. package/src/vault/README.md +110 -0
  177. package/src/vault/awareness.ts +341 -0
  178. package/src/vault/commitments.ts +299 -0
  179. package/src/vault/content-pipeline.ts +260 -0
  180. package/src/vault/conversations.ts +173 -0
  181. package/src/vault/entities.ts +180 -0
  182. package/src/vault/extractor.test.ts +356 -0
  183. package/src/vault/extractor.ts +345 -0
  184. package/src/vault/facts.ts +190 -0
  185. package/src/vault/goals.ts +477 -0
  186. package/src/vault/index.ts +87 -0
  187. package/src/vault/keychain.ts +99 -0
  188. package/src/vault/observations.ts +115 -0
  189. package/src/vault/relationships.ts +178 -0
  190. package/src/vault/retrieval.test.ts +126 -0
  191. package/src/vault/retrieval.ts +227 -0
  192. package/src/vault/schema.ts +658 -0
  193. package/src/vault/settings.ts +38 -0
  194. package/src/vault/vectors.ts +92 -0
  195. package/src/vault/workflows.ts +403 -0
  196. package/src/workflows/auto-suggest.ts +290 -0
  197. package/src/workflows/engine.ts +366 -0
  198. package/src/workflows/events.ts +24 -0
  199. package/src/workflows/executor.ts +207 -0
  200. package/src/workflows/nl-builder.ts +198 -0
  201. package/src/workflows/nodes/actions/agent-task.ts +73 -0
  202. package/src/workflows/nodes/actions/calendar-action.ts +85 -0
  203. package/src/workflows/nodes/actions/code-execution.ts +73 -0
  204. package/src/workflows/nodes/actions/discord.ts +77 -0
  205. package/src/workflows/nodes/actions/file-write.ts +73 -0
  206. package/src/workflows/nodes/actions/gmail.ts +69 -0
  207. package/src/workflows/nodes/actions/http-request.ts +117 -0
  208. package/src/workflows/nodes/actions/notification.ts +85 -0
  209. package/src/workflows/nodes/actions/run-tool.ts +55 -0
  210. package/src/workflows/nodes/actions/send-message.ts +82 -0
  211. package/src/workflows/nodes/actions/shell-command.ts +76 -0
  212. package/src/workflows/nodes/actions/telegram.ts +60 -0
  213. package/src/workflows/nodes/builtin.ts +119 -0
  214. package/src/workflows/nodes/error/error-handler.ts +37 -0
  215. package/src/workflows/nodes/error/fallback.ts +47 -0
  216. package/src/workflows/nodes/error/retry.ts +82 -0
  217. package/src/workflows/nodes/logic/delay.ts +42 -0
  218. package/src/workflows/nodes/logic/if-else.ts +41 -0
  219. package/src/workflows/nodes/logic/loop.ts +90 -0
  220. package/src/workflows/nodes/logic/merge.ts +38 -0
  221. package/src/workflows/nodes/logic/race.ts +40 -0
  222. package/src/workflows/nodes/logic/switch.ts +59 -0
  223. package/src/workflows/nodes/logic/template-render.ts +53 -0
  224. package/src/workflows/nodes/logic/variable-get.ts +37 -0
  225. package/src/workflows/nodes/logic/variable-set.ts +59 -0
  226. package/src/workflows/nodes/registry.ts +99 -0
  227. package/src/workflows/nodes/transform/aggregate.ts +99 -0
  228. package/src/workflows/nodes/transform/csv-parse.ts +70 -0
  229. package/src/workflows/nodes/transform/json-parse.ts +63 -0
  230. package/src/workflows/nodes/transform/map-filter.ts +84 -0
  231. package/src/workflows/nodes/transform/regex-match.ts +89 -0
  232. package/src/workflows/nodes/triggers/calendar.ts +33 -0
  233. package/src/workflows/nodes/triggers/clipboard.ts +32 -0
  234. package/src/workflows/nodes/triggers/cron.ts +40 -0
  235. package/src/workflows/nodes/triggers/email.ts +40 -0
  236. package/src/workflows/nodes/triggers/file-change.ts +45 -0
  237. package/src/workflows/nodes/triggers/git.ts +46 -0
  238. package/src/workflows/nodes/triggers/manual.ts +23 -0
  239. package/src/workflows/nodes/triggers/poll.ts +81 -0
  240. package/src/workflows/nodes/triggers/process.ts +44 -0
  241. package/src/workflows/nodes/triggers/screen-event.ts +37 -0
  242. package/src/workflows/nodes/triggers/webhook.ts +39 -0
  243. package/src/workflows/safe-eval.ts +139 -0
  244. package/src/workflows/template.ts +118 -0
  245. package/src/workflows/triggers/cron.ts +311 -0
  246. package/src/workflows/triggers/manager.ts +285 -0
  247. package/src/workflows/triggers/observer-bridge.ts +172 -0
  248. package/src/workflows/triggers/poller.ts +201 -0
  249. package/src/workflows/triggers/screen-condition.ts +218 -0
  250. package/src/workflows/triggers/triggers.test.ts +740 -0
  251. package/src/workflows/triggers/webhook.ts +191 -0
  252. package/src/workflows/types.ts +133 -0
  253. package/src/workflows/variables.ts +72 -0
  254. package/src/workflows/workflows.test.ts +383 -0
  255. package/src/workflows/yaml.ts +104 -0
  256. package/ui/dist/index-j75njzc1.css +1199 -0
  257. package/ui/dist/index-p2zh407q.js +80603 -0
  258. package/ui/dist/index.html +13 -0
  259. package/ui/public/openwakeword/models/embedding_model.onnx +0 -0
  260. package/ui/public/openwakeword/models/hey_jarvis_v0.1.onnx +0 -0
  261. package/ui/public/openwakeword/models/melspectrogram.onnx +0 -0
  262. package/ui/public/openwakeword/models/silero_vad.onnx +0 -0
  263. package/ui/public/ort/ort-wasm-simd-threaded.jsep.mjs +106 -0
  264. package/ui/public/ort/ort-wasm-simd-threaded.jsep.wasm +0 -0
  265. package/ui/public/ort/ort-wasm-simd-threaded.mjs +59 -0
  266. package/ui/public/ort/ort-wasm-simd-threaded.wasm +0 -0
@@ -0,0 +1,740 @@
1
+ /**
2
+ * Trigger System Tests — Phase 2: Workflow Automation Engine
3
+ *
4
+ * Tests for CronScheduler, WebhookManager, ScreenConditionEvaluator,
5
+ * and TriggerManager basic instantiation.
6
+ */
7
+
8
+ import { test, expect, describe, beforeEach, afterEach, mock } from 'bun:test';
9
+ import { CronScheduler } from './cron.ts';
10
+ import { WebhookManager } from './webhook.ts';
11
+ import { ScreenConditionEvaluator } from './screen-condition.ts';
12
+ import { TriggerManager } from './manager.ts';
13
+ import { NodeRegistry } from '../nodes/registry.ts';
14
+ import { WorkflowEngine } from '../engine.ts';
15
+ import type { WorkflowDefinition } from '../types.ts';
16
+ import { DEFAULT_WORKFLOW_SETTINGS } from '../types.ts';
17
+ import { initDatabase } from '../../vault/schema.ts';
18
+
19
+ // ────────────────────────────────────────────────────────────────────────────
20
+ // CronScheduler
21
+ // ────────────────────────────────────────────────────────────────────────────
22
+
23
+ describe('CronScheduler — matches()', () => {
24
+ test('every minute: * * * * *', () => {
25
+ const date = new Date('2026-03-02T10:30:00');
26
+ expect(CronScheduler.matches('* * * * *', date)).toBe(true);
27
+ });
28
+
29
+ test('specific minute and hour: 30 10 * * *', () => {
30
+ const match = new Date('2026-03-02T10:30:00');
31
+ const noMatch = new Date('2026-03-02T10:31:00');
32
+
33
+ expect(CronScheduler.matches('30 10 * * *', match)).toBe(true);
34
+ expect(CronScheduler.matches('30 10 * * *', noMatch)).toBe(false);
35
+ });
36
+
37
+ test('specific day of month: 0 9 1 * *', () => {
38
+ const firstOfMonth = new Date('2026-03-01T09:00:00');
39
+ const secondOfMonth = new Date('2026-03-02T09:00:00');
40
+
41
+ expect(CronScheduler.matches('0 9 1 * *', firstOfMonth)).toBe(true);
42
+ expect(CronScheduler.matches('0 9 1 * *', secondOfMonth)).toBe(false);
43
+ });
44
+
45
+ test('specific month: 0 0 * 6 *', () => {
46
+ const june = new Date('2026-06-15T00:00:00');
47
+ const march = new Date('2026-03-15T00:00:00');
48
+
49
+ expect(CronScheduler.matches('0 0 * 6 *', june)).toBe(true);
50
+ expect(CronScheduler.matches('0 0 * 6 *', march)).toBe(false);
51
+ });
52
+
53
+ test('day of week (Monday=1): 0 8 * * 1', () => {
54
+ // 2026-03-02 is a Monday
55
+ const monday = new Date('2026-03-02T08:00:00');
56
+ const tuesday = new Date('2026-03-03T08:00:00');
57
+
58
+ expect(CronScheduler.matches('0 8 * * 1', monday)).toBe(true);
59
+ expect(CronScheduler.matches('0 8 * * 1', tuesday)).toBe(false);
60
+ });
61
+
62
+ test('comma-separated values: 0,30 * * * *', () => {
63
+ const onHour = new Date('2026-03-02T10:00:00');
64
+ const halfPast = new Date('2026-03-02T10:30:00');
65
+ const other = new Date('2026-03-02T10:15:00');
66
+
67
+ expect(CronScheduler.matches('0,30 * * * *', onHour)).toBe(true);
68
+ expect(CronScheduler.matches('0,30 * * * *', halfPast)).toBe(true);
69
+ expect(CronScheduler.matches('0,30 * * * *', other)).toBe(false);
70
+ });
71
+
72
+ test('step values: */15 * * * *', () => {
73
+ const min0 = new Date('2026-03-02T10:00:00');
74
+ const min15 = new Date('2026-03-02T10:15:00');
75
+ const min30 = new Date('2026-03-02T10:30:00');
76
+ const min45 = new Date('2026-03-02T10:45:00');
77
+ const min10 = new Date('2026-03-02T10:10:00');
78
+
79
+ expect(CronScheduler.matches('*/15 * * * *', min0)).toBe(true);
80
+ expect(CronScheduler.matches('*/15 * * * *', min15)).toBe(true);
81
+ expect(CronScheduler.matches('*/15 * * * *', min30)).toBe(true);
82
+ expect(CronScheduler.matches('*/15 * * * *', min45)).toBe(true);
83
+ expect(CronScheduler.matches('*/15 * * * *', min10)).toBe(false);
84
+ });
85
+
86
+ test('range: 0 9-17 * * *', () => {
87
+ const at9 = new Date('2026-03-02T09:00:00');
88
+ const at13 = new Date('2026-03-02T13:00:00');
89
+ const at17 = new Date('2026-03-02T17:00:00');
90
+ const at18 = new Date('2026-03-02T18:00:00');
91
+
92
+ expect(CronScheduler.matches('0 9-17 * * *', at9)).toBe(true);
93
+ expect(CronScheduler.matches('0 9-17 * * *', at13)).toBe(true);
94
+ expect(CronScheduler.matches('0 9-17 * * *', at17)).toBe(true);
95
+ expect(CronScheduler.matches('0 9-17 * * *', at18)).toBe(false);
96
+ });
97
+
98
+ test('invalid expression returns false', () => {
99
+ expect(CronScheduler.matches('bad expression', new Date())).toBe(false);
100
+ expect(CronScheduler.matches('* * *', new Date())).toBe(false);
101
+ });
102
+ });
103
+
104
+ describe('CronScheduler — nextRun()', () => {
105
+ test('every minute: next run is 1 minute ahead', () => {
106
+ const from = new Date('2026-03-02T10:30:00');
107
+ const next = CronScheduler.nextRun('* * * * *', from);
108
+
109
+ expect(next).not.toBeNull();
110
+ expect(next!.getMinutes()).toBe(31);
111
+ expect(next!.getHours()).toBe(10);
112
+ });
113
+
114
+ test('hourly: 0 * * * *', () => {
115
+ const from = new Date('2026-03-02T10:30:00');
116
+ const next = CronScheduler.nextRun('0 * * * *', from);
117
+
118
+ expect(next).not.toBeNull();
119
+ expect(next!.getMinutes()).toBe(0);
120
+ expect(next!.getHours()).toBe(11);
121
+ });
122
+
123
+ test('next run respects day boundary', () => {
124
+ // Expression: midnight
125
+ const from = new Date('2026-03-02T23:45:00');
126
+ const next = CronScheduler.nextRun('0 0 * * *', from);
127
+
128
+ expect(next).not.toBeNull();
129
+ expect(next!.getDate()).toBe(3); // Next day
130
+ expect(next!.getHours()).toBe(0);
131
+ expect(next!.getMinutes()).toBe(0);
132
+ });
133
+
134
+ test('next run respects month boundary', () => {
135
+ // Expression: 1st of month at 6am
136
+ const from = new Date('2026-03-15T06:01:00');
137
+ const next = CronScheduler.nextRun('0 6 1 * *', from);
138
+
139
+ expect(next).not.toBeNull();
140
+ expect(next!.getMonth()).toBe(3); // April (0-indexed)
141
+ expect(next!.getDate()).toBe(1);
142
+ expect(next!.getHours()).toBe(6);
143
+ });
144
+
145
+ test('specific future minute in same hour', () => {
146
+ const from = new Date('2026-03-02T10:10:00');
147
+ const next = CronScheduler.nextRun('30 10 * * *', from);
148
+
149
+ expect(next).not.toBeNull();
150
+ expect(next!.getHours()).toBe(10);
151
+ expect(next!.getMinutes()).toBe(30);
152
+ expect(next!.getDate()).toBe(2);
153
+ });
154
+ });
155
+
156
+ describe('CronScheduler — schedule() / cancel()', () => {
157
+ let scheduler: CronScheduler;
158
+
159
+ beforeEach(() => {
160
+ scheduler = new CronScheduler();
161
+ });
162
+
163
+ afterEach(() => {
164
+ scheduler.cancelAll();
165
+ });
166
+
167
+ test('getJobs() returns registered jobs', () => {
168
+ scheduler.schedule('job1', '* * * * *', () => {});
169
+ scheduler.schedule('job2', '0 * * * *', () => {});
170
+
171
+ const jobs = scheduler.getJobs();
172
+ expect(jobs).toHaveLength(2);
173
+
174
+ const ids = jobs.map(j => j.id);
175
+ expect(ids).toContain('job1');
176
+ expect(ids).toContain('job2');
177
+ });
178
+
179
+ test('cancel() removes a specific job', () => {
180
+ scheduler.schedule('job1', '* * * * *', () => {});
181
+ scheduler.schedule('job2', '* * * * *', () => {});
182
+
183
+ scheduler.cancel('job1');
184
+
185
+ const jobs = scheduler.getJobs();
186
+ expect(jobs).toHaveLength(1);
187
+ expect(jobs[0]!.id).toBe('job2');
188
+ });
189
+
190
+ test('cancelAll() removes all jobs', () => {
191
+ scheduler.schedule('j1', '* * * * *', () => {});
192
+ scheduler.schedule('j2', '* * * * *', () => {});
193
+
194
+ scheduler.cancelAll();
195
+
196
+ expect(scheduler.getJobs()).toHaveLength(0);
197
+ });
198
+
199
+ test('re-scheduling same id cancels the previous job', () => {
200
+ let count = 0;
201
+ scheduler.schedule('dup', '* * * * *', () => { count++; });
202
+ scheduler.schedule('dup', '0 * * * *', () => {}); // Replaces previous
203
+
204
+ const jobs = scheduler.getJobs();
205
+ expect(jobs).toHaveLength(1);
206
+ expect(jobs[0]!.expression).toBe('0 * * * *');
207
+ });
208
+
209
+ test('invalid expression throws', () => {
210
+ expect(() => scheduler.schedule('bad', 'not valid', () => {})).toThrow();
211
+ });
212
+
213
+ test('job info has nextRun set', () => {
214
+ scheduler.schedule('timed', '0 12 * * *', () => {});
215
+ const jobs = scheduler.getJobs();
216
+ expect(jobs[0]!.nextRun).toBeGreaterThan(Date.now() - 1000);
217
+ });
218
+ });
219
+
220
+ // ────────────────────────────────────────────────────────────────────────────
221
+ // WebhookManager
222
+ // ────────────────────────────────────────────────────────────────────────────
223
+
224
+ describe('WebhookManager — register()', () => {
225
+ let manager: WebhookManager;
226
+
227
+ beforeEach(() => {
228
+ manager = new WebhookManager();
229
+ });
230
+
231
+ test('returns expected path', () => {
232
+ const path = manager.register('wf_123');
233
+ expect(path).toBe('/webhooks/wf_123');
234
+ });
235
+
236
+ test('getRoutes() returns registered routes', () => {
237
+ manager.register('wf_abc');
238
+ manager.register('wf_def');
239
+
240
+ const routes = manager.getRoutes();
241
+ expect(routes.size).toBe(2);
242
+ expect(routes.has('wf_abc')).toBe(true);
243
+ expect(routes.has('wf_def')).toBe(true);
244
+ });
245
+
246
+ test('unregister() removes route', () => {
247
+ manager.register('wf_to_remove');
248
+ manager.unregister('wf_to_remove');
249
+
250
+ expect(manager.getRoutes().size).toBe(0);
251
+ });
252
+
253
+ test('route has correct metadata', () => {
254
+ const before = Date.now();
255
+ manager.register('wf_meta', 'my-secret');
256
+ const after = Date.now();
257
+
258
+ const route = manager.getRoutes().get('wf_meta')!;
259
+ expect(route.workflowId).toBe('wf_meta');
260
+ expect(route.path).toBe('/webhooks/wf_meta');
261
+ expect(route.secret).toBe('my-secret');
262
+ expect(route.registeredAt).toBeGreaterThanOrEqual(before);
263
+ expect(route.registeredAt).toBeLessThanOrEqual(after);
264
+ });
265
+ });
266
+
267
+ describe('WebhookManager — handleRequest()', () => {
268
+ let manager: WebhookManager;
269
+
270
+ beforeEach(() => {
271
+ manager = new WebhookManager();
272
+ });
273
+
274
+ test('404 for unregistered workflow', async () => {
275
+ const req = new Request('http://localhost/webhooks/unknown', { method: 'POST', body: '{}' });
276
+ const res = await manager.handleRequest('unknown', req);
277
+
278
+ expect(res.status).toBe(404);
279
+ const body = await res.json() as { error: string };
280
+ expect(body.error).toMatch(/not found/i);
281
+ });
282
+
283
+ test('fires callback with parsed JSON body', async () => {
284
+ manager.register('wf_cb');
285
+
286
+ let receivedWorkflowId = '';
287
+ let receivedData: Record<string, unknown> = {};
288
+
289
+ manager.setTriggerCallback((wfId, data) => {
290
+ receivedWorkflowId = wfId;
291
+ receivedData = data;
292
+ });
293
+
294
+ const req = new Request('http://localhost/webhooks/wf_cb', {
295
+ method: 'POST',
296
+ headers: { 'Content-Type': 'application/json' },
297
+ body: JSON.stringify({ event: 'test', value: 42 }),
298
+ });
299
+
300
+ const res = await manager.handleRequest('wf_cb', req);
301
+
302
+ expect(res.status).toBe(200);
303
+ expect(receivedWorkflowId).toBe('wf_cb');
304
+ expect(receivedData.event).toBe('test');
305
+ expect(receivedData.value).toBe(42);
306
+ });
307
+
308
+ test('200 on valid request without secret', async () => {
309
+ manager.register('wf_nosecret');
310
+
311
+ const req = new Request('http://localhost/webhooks/wf_nosecret', {
312
+ method: 'POST',
313
+ body: '{"hello":"world"}',
314
+ });
315
+
316
+ const res = await manager.handleRequest('wf_nosecret', req);
317
+ expect(res.status).toBe(200);
318
+ });
319
+
320
+ test('401 when secret required but header missing', async () => {
321
+ manager.register('wf_protected', 'my-secret');
322
+
323
+ const req = new Request('http://localhost/webhooks/wf_protected', {
324
+ method: 'POST',
325
+ body: '{"test":true}',
326
+ });
327
+
328
+ const res = await manager.handleRequest('wf_protected', req);
329
+ expect(res.status).toBe(401);
330
+
331
+ const body = await res.json() as { error: string };
332
+ expect(body.error).toMatch(/missing signature/i);
333
+ });
334
+
335
+ test('401 when secret is wrong', async () => {
336
+ manager.register('wf_wrong_sig', 'correct-secret');
337
+
338
+ const req = new Request('http://localhost/webhooks/wf_wrong_sig', {
339
+ method: 'POST',
340
+ headers: { 'x-jarvis-signature': 'deadbeefdeadbeef' },
341
+ body: '{"test":true}',
342
+ });
343
+
344
+ const res = await manager.handleRequest('wf_wrong_sig', req);
345
+ expect(res.status).toBe(401);
346
+
347
+ const body = await res.json() as { error: string };
348
+ expect(body.error).toMatch(/invalid signature/i);
349
+ });
350
+
351
+ test('200 when HMAC signature is valid', async () => {
352
+ const secret = 'super-secret';
353
+ manager.register('wf_signed', secret);
354
+
355
+ const bodyStr = JSON.stringify({ event: 'push' });
356
+
357
+ // Compute the expected HMAC manually
358
+ const encoder = new TextEncoder();
359
+ const keyMaterial = await crypto.subtle.importKey(
360
+ 'raw',
361
+ encoder.encode(secret),
362
+ { name: 'HMAC', hash: 'SHA-256' },
363
+ false,
364
+ ['sign'],
365
+ );
366
+ const sig = await crypto.subtle.sign('HMAC', keyMaterial, encoder.encode(bodyStr));
367
+ const hexSig = Array.from(new Uint8Array(sig)).map(b => b.toString(16).padStart(2, '0')).join('');
368
+
369
+ const req = new Request('http://localhost/webhooks/wf_signed', {
370
+ method: 'POST',
371
+ headers: { 'x-jarvis-signature': hexSig },
372
+ body: bodyStr,
373
+ });
374
+
375
+ const res = await manager.handleRequest('wf_signed', req);
376
+ expect(res.status).toBe(200);
377
+ });
378
+
379
+ test('response contains ok:true and workflowId', async () => {
380
+ manager.register('wf_response_check');
381
+
382
+ const req = new Request('http://localhost/webhooks/wf_response_check', {
383
+ method: 'POST',
384
+ body: '',
385
+ });
386
+
387
+ const res = await manager.handleRequest('wf_response_check', req);
388
+ const body = await res.json() as { ok: boolean; workflowId: string };
389
+
390
+ expect(body.ok).toBe(true);
391
+ expect(body.workflowId).toBe('wf_response_check');
392
+ });
393
+
394
+ test('non-JSON body is wrapped as string', async () => {
395
+ manager.register('wf_rawbody');
396
+
397
+ let receivedData: Record<string, unknown> = {};
398
+ manager.setTriggerCallback((_, data) => { receivedData = data; });
399
+
400
+ const req = new Request('http://localhost/webhooks/wf_rawbody', {
401
+ method: 'POST',
402
+ body: 'plain text payload',
403
+ });
404
+
405
+ await manager.handleRequest('wf_rawbody', req);
406
+ expect(receivedData.body).toBe('plain text payload');
407
+ });
408
+ });
409
+
410
+ // ────────────────────────────────────────────────────────────────────────────
411
+ // ScreenConditionEvaluator
412
+ // ────────────────────────────────────────────────────────────────────────────
413
+
414
+ describe('ScreenConditionEvaluator', () => {
415
+ let evaluator: ScreenConditionEvaluator;
416
+
417
+ beforeEach(() => {
418
+ evaluator = new ScreenConditionEvaluator(null);
419
+ });
420
+
421
+ // ── text_present ──
422
+
423
+ test('text_present: matches when text is in OCR output', async () => {
424
+ const result = await evaluator.evaluate(
425
+ { type: 'text_present', text: 'Welcome' },
426
+ 'Welcome to JARVIS',
427
+ );
428
+ expect(result).toBe(true);
429
+ });
430
+
431
+ test('text_present: false when text is absent', async () => {
432
+ const result = await evaluator.evaluate(
433
+ { type: 'text_present', text: 'Login' },
434
+ 'Welcome to JARVIS',
435
+ );
436
+ expect(result).toBe(false);
437
+ });
438
+
439
+ test('text_present: case-insensitive by default', async () => {
440
+ const result = await evaluator.evaluate(
441
+ { type: 'text_present', text: 'WELCOME' },
442
+ 'welcome to jarvis',
443
+ );
444
+ expect(result).toBe(true);
445
+ });
446
+
447
+ test('text_present: case-sensitive when configured', async () => {
448
+ const result = await evaluator.evaluate(
449
+ { type: 'text_present', text: 'WELCOME', caseSensitive: true },
450
+ 'welcome to jarvis',
451
+ );
452
+ expect(result).toBe(false);
453
+ });
454
+
455
+ test('text_present: false when ocrText is empty', async () => {
456
+ const result = await evaluator.evaluate(
457
+ { type: 'text_present', text: 'anything' },
458
+ '',
459
+ );
460
+ expect(result).toBe(false);
461
+ });
462
+
463
+ test('text_present: false when text field is missing', async () => {
464
+ const result = await evaluator.evaluate(
465
+ { type: 'text_present' },
466
+ 'some OCR text',
467
+ );
468
+ expect(result).toBe(false);
469
+ });
470
+
471
+ // ── text_absent ──
472
+
473
+ test('text_absent: true when text is not in OCR output', async () => {
474
+ const result = await evaluator.evaluate(
475
+ { type: 'text_absent', text: 'Error' },
476
+ 'Everything is fine',
477
+ );
478
+ expect(result).toBe(true);
479
+ });
480
+
481
+ test('text_absent: false when text IS present', async () => {
482
+ const result = await evaluator.evaluate(
483
+ { type: 'text_absent', text: 'Error' },
484
+ 'An Error occurred',
485
+ );
486
+ expect(result).toBe(false);
487
+ });
488
+
489
+ test('text_absent: true when ocrText is empty', async () => {
490
+ const result = await evaluator.evaluate(
491
+ { type: 'text_absent', text: 'Error' },
492
+ '',
493
+ );
494
+ expect(result).toBe(true);
495
+ });
496
+
497
+ // ── app_active ──
498
+
499
+ test('app_active: true when app name matches exactly', async () => {
500
+ const result = await evaluator.evaluate(
501
+ { type: 'app_active', appName: 'Chrome' },
502
+ undefined,
503
+ 'Chrome',
504
+ );
505
+ expect(result).toBe(true);
506
+ });
507
+
508
+ test('app_active: true when app name is a substring', async () => {
509
+ const result = await evaluator.evaluate(
510
+ { type: 'app_active', appName: 'Chrome' },
511
+ undefined,
512
+ 'Google Chrome',
513
+ );
514
+ expect(result).toBe(true);
515
+ });
516
+
517
+ test('app_active: false when wrong app is active', async () => {
518
+ const result = await evaluator.evaluate(
519
+ { type: 'app_active', appName: 'Firefox' },
520
+ undefined,
521
+ 'Google Chrome',
522
+ );
523
+ expect(result).toBe(false);
524
+ });
525
+
526
+ test('app_active: case-insensitive by default', async () => {
527
+ const result = await evaluator.evaluate(
528
+ { type: 'app_active', appName: 'chrome' },
529
+ undefined,
530
+ 'Google Chrome',
531
+ );
532
+ expect(result).toBe(true);
533
+ });
534
+
535
+ test('app_active: false when appName field is missing', async () => {
536
+ const result = await evaluator.evaluate(
537
+ { type: 'app_active' },
538
+ undefined,
539
+ 'Chrome',
540
+ );
541
+ expect(result).toBe(false);
542
+ });
543
+
544
+ test('app_active: false when no active app provided', async () => {
545
+ const result = await evaluator.evaluate(
546
+ { type: 'app_active', appName: 'Chrome' },
547
+ );
548
+ expect(result).toBe(false);
549
+ });
550
+
551
+ // ── llm_check / visual_match without LLM ──
552
+
553
+ test('visual_match: false when no LLM manager', async () => {
554
+ const result = await evaluator.evaluate(
555
+ { type: 'visual_match', description: 'Login form is visible' },
556
+ 'some OCR text',
557
+ );
558
+ expect(result).toBe(false);
559
+ });
560
+
561
+ test('llm_check: false when no LLM manager', async () => {
562
+ const result = await evaluator.evaluate(
563
+ { type: 'llm_check', prompt: 'Is a modal dialog open?' },
564
+ 'some OCR text',
565
+ );
566
+ expect(result).toBe(false);
567
+ });
568
+
569
+ test('llm_check with mock LLM returning "yes"', async () => {
570
+ const mockLlm = {
571
+ complete: mock(async (_prompt: string) => ({ text: 'yes' })),
572
+ };
573
+
574
+ const evalWithLlm = new ScreenConditionEvaluator(mockLlm);
575
+ const result = await evalWithLlm.evaluate(
576
+ { type: 'llm_check', prompt: 'Is there a login form?' },
577
+ 'Username: [____] Password: [____] [Login]',
578
+ 'Chrome',
579
+ );
580
+
581
+ expect(result).toBe(true);
582
+ expect(mockLlm.complete).toHaveBeenCalledTimes(1);
583
+ });
584
+
585
+ test('llm_check with mock LLM returning "no"', async () => {
586
+ const mockLlm = {
587
+ complete: mock(async (_prompt: string) => ({ text: 'no' })),
588
+ };
589
+
590
+ const evalWithLlm = new ScreenConditionEvaluator(mockLlm);
591
+ const result = await evalWithLlm.evaluate(
592
+ { type: 'llm_check', prompt: 'Is there a login form?' },
593
+ 'Welcome to JARVIS dashboard',
594
+ 'Chrome',
595
+ );
596
+
597
+ expect(result).toBe(false);
598
+ });
599
+ });
600
+
601
+ // ────────────────────────────────────────────────────────────────────────────
602
+ // TriggerManager — basic instantiation
603
+ // ────────────────────────────────────────────────────────────────────────────
604
+
605
+ describe('TriggerManager', () => {
606
+ beforeEach(() => initDatabase(':memory:'));
607
+
608
+ function makeEngine(): WorkflowEngine {
609
+ const nodeRegistry = new NodeRegistry();
610
+ // toolRegistry minimal stub
611
+ const toolRegistry = {} as import('../nodes/registry.ts').ExecutionContext['toolRegistry'];
612
+ return new WorkflowEngine(nodeRegistry, toolRegistry, null);
613
+ }
614
+
615
+ test('instantiates without error', () => {
616
+ const engine = makeEngine();
617
+ const manager = new TriggerManager(engine);
618
+ expect(manager).toBeTruthy();
619
+ expect(manager.name).toBe('trigger-manager');
620
+ });
621
+
622
+ test('initial status is stopped', () => {
623
+ const engine = makeEngine();
624
+ const manager = new TriggerManager(engine);
625
+ expect(manager.status()).toBe('stopped');
626
+ });
627
+
628
+ test('getCronScheduler() returns CronScheduler instance', () => {
629
+ const engine = makeEngine();
630
+ const manager = new TriggerManager(engine);
631
+ expect(manager.getCronScheduler()).toBeDefined();
632
+ expect(typeof manager.getCronScheduler().schedule).toBe('function');
633
+ });
634
+
635
+ test('getWebhookManager() returns WebhookManager instance', () => {
636
+ const engine = makeEngine();
637
+ const manager = new TriggerManager(engine);
638
+ expect(manager.getWebhookManager()).toBeDefined();
639
+ expect(typeof manager.getWebhookManager().register).toBe('function');
640
+ });
641
+
642
+ test('start() transitions to running after reloadAll()', async () => {
643
+ const engine = makeEngine();
644
+ await engine.start();
645
+
646
+ const manager = new TriggerManager(engine);
647
+ await manager.start();
648
+
649
+ expect(manager.status()).toBe('running');
650
+ await manager.stop();
651
+ });
652
+
653
+ test('stop() transitions to stopped', async () => {
654
+ const engine = makeEngine();
655
+ await engine.start();
656
+
657
+ const manager = new TriggerManager(engine);
658
+ await manager.start();
659
+ await manager.stop();
660
+
661
+ expect(manager.status()).toBe('stopped');
662
+ });
663
+
664
+ test('registerWorkflow() registers cron trigger', () => {
665
+ const engine = makeEngine();
666
+ const manager = new TriggerManager(engine);
667
+
668
+ const definition: WorkflowDefinition = {
669
+ nodes: [
670
+ {
671
+ id: 'n1',
672
+ type: 'trigger.cron',
673
+ label: 'Every Hour',
674
+ position: { x: 0, y: 0 },
675
+ config: { expression: '0 * * * *' },
676
+ },
677
+ ],
678
+ edges: [],
679
+ settings: DEFAULT_WORKFLOW_SETTINGS,
680
+ };
681
+
682
+ manager.registerWorkflow('wf_cron_test', definition);
683
+
684
+ const jobs = manager.getCronScheduler().getJobs();
685
+ expect(jobs).toHaveLength(1);
686
+ expect(jobs[0]!.expression).toBe('0 * * * *');
687
+
688
+ manager.getCronScheduler().cancelAll();
689
+ });
690
+
691
+ test('registerWorkflow() registers webhook trigger', () => {
692
+ const engine = makeEngine();
693
+ const manager = new TriggerManager(engine);
694
+
695
+ const definition: WorkflowDefinition = {
696
+ nodes: [
697
+ {
698
+ id: 'n1',
699
+ type: 'trigger.webhook',
700
+ label: 'Inbound Hook',
701
+ position: { x: 0, y: 0 },
702
+ config: { secret: 'test-secret' },
703
+ },
704
+ ],
705
+ edges: [],
706
+ settings: DEFAULT_WORKFLOW_SETTINGS,
707
+ };
708
+
709
+ manager.registerWorkflow('wf_webhook_test', definition);
710
+
711
+ const routes = manager.getWebhookManager().getRoutes();
712
+ expect(routes.has('wf_webhook_test')).toBe(true);
713
+ expect(routes.get('wf_webhook_test')!.secret).toBe('test-secret');
714
+ });
715
+
716
+ test('unregisterWorkflow() cleans up cron jobs', () => {
717
+ const engine = makeEngine();
718
+ const manager = new TriggerManager(engine);
719
+
720
+ const definition: WorkflowDefinition = {
721
+ nodes: [
722
+ {
723
+ id: 'n1',
724
+ type: 'trigger.cron',
725
+ label: 'Every Minute',
726
+ position: { x: 0, y: 0 },
727
+ config: { expression: '* * * * *' },
728
+ },
729
+ ],
730
+ edges: [],
731
+ settings: DEFAULT_WORKFLOW_SETTINGS,
732
+ };
733
+
734
+ manager.registerWorkflow('wf_cleanup', definition);
735
+ expect(manager.getCronScheduler().getJobs()).toHaveLength(1);
736
+
737
+ manager.unregisterWorkflow('wf_cleanup');
738
+ expect(manager.getCronScheduler().getJobs()).toHaveLength(0);
739
+ });
740
+ });