busy-cli 0.1.2
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/README.md +129 -0
- package/dist/builders/context.d.ts +50 -0
- package/dist/builders/context.d.ts.map +1 -0
- package/dist/builders/context.js +190 -0
- package/dist/cache/index.d.ts +100 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +270 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +463 -0
- package/dist/commands/package.d.ts +96 -0
- package/dist/commands/package.d.ts.map +1 -0
- package/dist/commands/package.js +285 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/loader.d.ts +6 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +361 -0
- package/dist/merge.d.ts +16 -0
- package/dist/merge.d.ts.map +1 -0
- package/dist/merge.js +102 -0
- package/dist/package/manifest.d.ts +59 -0
- package/dist/package/manifest.d.ts.map +1 -0
- package/dist/package/manifest.js +265 -0
- package/dist/parser.d.ts +28 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +220 -0
- package/dist/parsers/frontmatter.d.ts +14 -0
- package/dist/parsers/frontmatter.d.ts.map +1 -0
- package/dist/parsers/frontmatter.js +110 -0
- package/dist/parsers/imports.d.ts +48 -0
- package/dist/parsers/imports.d.ts.map +1 -0
- package/dist/parsers/imports.js +147 -0
- package/dist/parsers/links.d.ts +12 -0
- package/dist/parsers/links.d.ts.map +1 -0
- package/dist/parsers/links.js +79 -0
- package/dist/parsers/localdefs.d.ts +6 -0
- package/dist/parsers/localdefs.d.ts.map +1 -0
- package/dist/parsers/localdefs.js +132 -0
- package/dist/parsers/operations.d.ts +32 -0
- package/dist/parsers/operations.d.ts.map +1 -0
- package/dist/parsers/operations.js +313 -0
- package/dist/parsers/sections.d.ts +15 -0
- package/dist/parsers/sections.d.ts.map +1 -0
- package/dist/parsers/sections.js +173 -0
- package/dist/parsers/tools.d.ts +30 -0
- package/dist/parsers/tools.d.ts.map +1 -0
- package/dist/parsers/tools.js +178 -0
- package/dist/parsers/triggers.d.ts +35 -0
- package/dist/parsers/triggers.d.ts.map +1 -0
- package/dist/parsers/triggers.js +219 -0
- package/dist/providers/base.d.ts +60 -0
- package/dist/providers/base.d.ts.map +1 -0
- package/dist/providers/base.js +34 -0
- package/dist/providers/github.d.ts +18 -0
- package/dist/providers/github.d.ts.map +1 -0
- package/dist/providers/github.js +109 -0
- package/dist/providers/gitlab.d.ts +18 -0
- package/dist/providers/gitlab.d.ts.map +1 -0
- package/dist/providers/gitlab.js +101 -0
- package/dist/providers/index.d.ts +13 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +17 -0
- package/dist/providers/local.d.ts +31 -0
- package/dist/providers/local.d.ts.map +1 -0
- package/dist/providers/local.js +116 -0
- package/dist/providers/url.d.ts +16 -0
- package/dist/providers/url.d.ts.map +1 -0
- package/dist/providers/url.js +45 -0
- package/dist/registry/index.d.ts +99 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +320 -0
- package/dist/types/schema.d.ts +3259 -0
- package/dist/types/schema.d.ts.map +1 -0
- package/dist/types/schema.js +258 -0
- package/dist/utils/logger.d.ts +19 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +23 -0
- package/dist/utils/slugify.d.ts +14 -0
- package/dist/utils/slugify.d.ts.map +1 -0
- package/dist/utils/slugify.js +28 -0
- package/package.json +61 -0
- package/src/__tests__/cache.test.ts +393 -0
- package/src/__tests__/cli-package.test.ts +667 -0
- package/src/__tests__/fixtures/automated-workflow.busy.md +84 -0
- package/src/__tests__/fixtures/concept.busy.md +30 -0
- package/src/__tests__/fixtures/document.busy.md +44 -0
- package/src/__tests__/fixtures/simple-operation.busy.md +45 -0
- package/src/__tests__/fixtures/tool-document.busy.md +71 -0
- package/src/__tests__/fixtures/tool.busy.md +54 -0
- package/src/__tests__/imports.test.ts +244 -0
- package/src/__tests__/integration.test.ts +432 -0
- package/src/__tests__/operations.test.ts +408 -0
- package/src/__tests__/package-manifest.test.ts +455 -0
- package/src/__tests__/providers.test.ts +672 -0
- package/src/__tests__/registry.test.ts +402 -0
- package/src/__tests__/schema.test.ts +467 -0
- package/src/__tests__/tools.test.ts +376 -0
- package/src/__tests__/triggers.test.ts +312 -0
- package/src/builders/context.ts +294 -0
- package/src/cache/index.ts +312 -0
- package/src/cli/index.ts +514 -0
- package/src/commands/package.ts +392 -0
- package/src/index.ts +46 -0
- package/src/loader.ts +474 -0
- package/src/merge.ts +126 -0
- package/src/package/manifest.ts +349 -0
- package/src/parser.ts +278 -0
- package/src/parsers/frontmatter.ts +135 -0
- package/src/parsers/imports.ts +196 -0
- package/src/parsers/links.ts +108 -0
- package/src/parsers/localdefs.ts +166 -0
- package/src/parsers/operations.ts +404 -0
- package/src/parsers/sections.ts +230 -0
- package/src/parsers/tools.ts +215 -0
- package/src/parsers/triggers.ts +252 -0
- package/src/providers/base.ts +77 -0
- package/src/providers/github.ts +129 -0
- package/src/providers/gitlab.ts +121 -0
- package/src/providers/index.ts +25 -0
- package/src/providers/local.ts +129 -0
- package/src/providers/url.ts +56 -0
- package/src/registry/index.ts +408 -0
- package/src/types/schema.ts +369 -0
- package/src/utils/logger.ts +25 -0
- package/src/utils/slugify.ts +31 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Parsing Tests - Match busy-python Tool and ToolDocument models
|
|
3
|
+
*
|
|
4
|
+
* busy-python Tool model:
|
|
5
|
+
* - name: str
|
|
6
|
+
* - description: str
|
|
7
|
+
* - inputs: list[str]
|
|
8
|
+
* - outputs: list[str]
|
|
9
|
+
* - examples: Optional[list[str]]
|
|
10
|
+
* - providers: Optional[dict[str, dict[str, Any]]] (provider_name -> {action, parameters})
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect } from 'vitest';
|
|
14
|
+
import { parseTools, parseToolProviders } from '../parsers/tools';
|
|
15
|
+
import type { Tool, ToolDocument } from '../types/schema';
|
|
16
|
+
|
|
17
|
+
describe('parseTools', () => {
|
|
18
|
+
it('should parse tool with all fields', () => {
|
|
19
|
+
const content = `
|
|
20
|
+
# [Tools]
|
|
21
|
+
|
|
22
|
+
## send_email
|
|
23
|
+
|
|
24
|
+
Send an email to one or more recipients.
|
|
25
|
+
|
|
26
|
+
### [Inputs]
|
|
27
|
+
- to: Recipient email address(es)
|
|
28
|
+
- subject: Email subject line
|
|
29
|
+
- body: Email body content
|
|
30
|
+
- cc: (Optional) CC recipients
|
|
31
|
+
|
|
32
|
+
### [Outputs]
|
|
33
|
+
- message_id: The sent message ID
|
|
34
|
+
- thread_id: The thread ID
|
|
35
|
+
|
|
36
|
+
### [Examples]
|
|
37
|
+
- send_email(to="user@example.com", subject="Hello", body="Hi there")
|
|
38
|
+
- send_email(to="team@company.com", subject="Update", body="Status report", cc="manager@company.com")
|
|
39
|
+
|
|
40
|
+
### [Providers]
|
|
41
|
+
|
|
42
|
+
#### composio
|
|
43
|
+
Action: GMAIL_SEND_EMAIL
|
|
44
|
+
Parameters:
|
|
45
|
+
to: to
|
|
46
|
+
subject: subject
|
|
47
|
+
body: body
|
|
48
|
+
|
|
49
|
+
#### mcp
|
|
50
|
+
Action: gmail/sendEmail
|
|
51
|
+
Parameters:
|
|
52
|
+
recipient: to
|
|
53
|
+
title: subject
|
|
54
|
+
content: body
|
|
55
|
+
`;
|
|
56
|
+
|
|
57
|
+
const tools = parseTools(content);
|
|
58
|
+
expect(tools).toHaveLength(1);
|
|
59
|
+
|
|
60
|
+
const tool = tools[0];
|
|
61
|
+
expect(tool.name).toBe('send_email');
|
|
62
|
+
expect(tool.description).toBe('Send an email to one or more recipients.');
|
|
63
|
+
expect(tool.inputs).toHaveLength(4);
|
|
64
|
+
expect(tool.outputs).toHaveLength(2);
|
|
65
|
+
expect(tool.examples).toHaveLength(2);
|
|
66
|
+
expect(tool.providers).toHaveProperty('composio');
|
|
67
|
+
expect(tool.providers).toHaveProperty('mcp');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should parse multiple tools', () => {
|
|
71
|
+
const content = `
|
|
72
|
+
# [Tools]
|
|
73
|
+
|
|
74
|
+
## list_emails
|
|
75
|
+
|
|
76
|
+
List emails from inbox.
|
|
77
|
+
|
|
78
|
+
### [Inputs]
|
|
79
|
+
- limit: Maximum number to return
|
|
80
|
+
|
|
81
|
+
### [Outputs]
|
|
82
|
+
- emails: List of email objects
|
|
83
|
+
|
|
84
|
+
## get_email
|
|
85
|
+
|
|
86
|
+
Get a single email by ID.
|
|
87
|
+
|
|
88
|
+
### [Inputs]
|
|
89
|
+
- message_id: The email message ID
|
|
90
|
+
|
|
91
|
+
### [Outputs]
|
|
92
|
+
- email: The email object
|
|
93
|
+
`;
|
|
94
|
+
|
|
95
|
+
const tools = parseTools(content);
|
|
96
|
+
expect(tools).toHaveLength(2);
|
|
97
|
+
expect(tools.map((t) => t.name)).toEqual(['list_emails', 'get_email']);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should handle tool without providers', () => {
|
|
101
|
+
const content = `
|
|
102
|
+
# [Tools]
|
|
103
|
+
|
|
104
|
+
## local_tool
|
|
105
|
+
|
|
106
|
+
A tool without external providers.
|
|
107
|
+
|
|
108
|
+
### [Inputs]
|
|
109
|
+
- data: Input data
|
|
110
|
+
|
|
111
|
+
### [Outputs]
|
|
112
|
+
- result: Output result
|
|
113
|
+
`;
|
|
114
|
+
|
|
115
|
+
const tools = parseTools(content);
|
|
116
|
+
expect(tools).toHaveLength(1);
|
|
117
|
+
expect(tools[0].providers).toBeUndefined();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should handle tool without examples', () => {
|
|
121
|
+
const content = `
|
|
122
|
+
# [Tools]
|
|
123
|
+
|
|
124
|
+
## simple_tool
|
|
125
|
+
|
|
126
|
+
Simple tool description.
|
|
127
|
+
|
|
128
|
+
### [Inputs]
|
|
129
|
+
- input: The input
|
|
130
|
+
|
|
131
|
+
### [Outputs]
|
|
132
|
+
- output: The output
|
|
133
|
+
`;
|
|
134
|
+
|
|
135
|
+
const tools = parseTools(content);
|
|
136
|
+
expect(tools).toHaveLength(1);
|
|
137
|
+
expect(tools[0].examples).toBeUndefined();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should parse tool inputs as string array', () => {
|
|
141
|
+
const content = `
|
|
142
|
+
# [Tools]
|
|
143
|
+
|
|
144
|
+
## process
|
|
145
|
+
|
|
146
|
+
### [Inputs]
|
|
147
|
+
- data: The raw data to process
|
|
148
|
+
- format: Output format (json, xml, csv)
|
|
149
|
+
- validate: Whether to validate (default: true)
|
|
150
|
+
`;
|
|
151
|
+
|
|
152
|
+
const tools = parseTools(content);
|
|
153
|
+
expect(tools[0].inputs).toEqual([
|
|
154
|
+
'data: The raw data to process',
|
|
155
|
+
'format: Output format (json, xml, csv)',
|
|
156
|
+
'validate: Whether to validate (default: true)',
|
|
157
|
+
]);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should handle Tools section without bracket notation', () => {
|
|
161
|
+
const content = `
|
|
162
|
+
# Tools
|
|
163
|
+
|
|
164
|
+
## my_tool
|
|
165
|
+
|
|
166
|
+
Tool description.
|
|
167
|
+
|
|
168
|
+
### Inputs
|
|
169
|
+
- input: Data
|
|
170
|
+
|
|
171
|
+
### Outputs
|
|
172
|
+
- output: Result
|
|
173
|
+
`;
|
|
174
|
+
|
|
175
|
+
const tools = parseTools(content);
|
|
176
|
+
expect(tools).toHaveLength(1);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should return empty array for document without Tools section', () => {
|
|
180
|
+
const content = `
|
|
181
|
+
# [Operations]
|
|
182
|
+
|
|
183
|
+
## SomeOp
|
|
184
|
+
|
|
185
|
+
Not a tool document.
|
|
186
|
+
`;
|
|
187
|
+
|
|
188
|
+
const tools = parseTools(content);
|
|
189
|
+
expect(tools).toHaveLength(0);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('parseToolProviders', () => {
|
|
194
|
+
it('should parse provider with action and parameters', () => {
|
|
195
|
+
const content = `
|
|
196
|
+
### [Providers]
|
|
197
|
+
|
|
198
|
+
#### composio
|
|
199
|
+
Action: GMAIL_SEND_EMAIL
|
|
200
|
+
Parameters:
|
|
201
|
+
to: recipient
|
|
202
|
+
subject: title
|
|
203
|
+
body: content
|
|
204
|
+
`;
|
|
205
|
+
|
|
206
|
+
const providers = parseToolProviders(content);
|
|
207
|
+
expect(providers).toHaveProperty('composio');
|
|
208
|
+
expect(providers.composio.action).toBe('GMAIL_SEND_EMAIL');
|
|
209
|
+
expect(providers.composio.parameters).toEqual({
|
|
210
|
+
to: 'recipient',
|
|
211
|
+
subject: 'title',
|
|
212
|
+
body: 'content',
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should parse multiple providers', () => {
|
|
217
|
+
const content = `
|
|
218
|
+
### [Providers]
|
|
219
|
+
|
|
220
|
+
#### composio
|
|
221
|
+
Action: COMPOSIO_ACTION
|
|
222
|
+
|
|
223
|
+
#### mcp
|
|
224
|
+
Action: mcp/action
|
|
225
|
+
|
|
226
|
+
#### custom
|
|
227
|
+
Action: custom.action
|
|
228
|
+
`;
|
|
229
|
+
|
|
230
|
+
const providers = parseToolProviders(content);
|
|
231
|
+
expect(Object.keys(providers)).toHaveLength(3);
|
|
232
|
+
expect(providers.composio.action).toBe('COMPOSIO_ACTION');
|
|
233
|
+
expect(providers.mcp.action).toBe('mcp/action');
|
|
234
|
+
expect(providers.custom.action).toBe('custom.action');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should handle provider without parameters', () => {
|
|
238
|
+
const content = `
|
|
239
|
+
### [Providers]
|
|
240
|
+
|
|
241
|
+
#### simple
|
|
242
|
+
Action: SIMPLE_ACTION
|
|
243
|
+
`;
|
|
244
|
+
|
|
245
|
+
const providers = parseToolProviders(content);
|
|
246
|
+
expect(providers.simple.action).toBe('SIMPLE_ACTION');
|
|
247
|
+
expect(providers.simple.parameters).toBeUndefined();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should return empty object for content without providers', () => {
|
|
251
|
+
const content = `
|
|
252
|
+
### [Inputs]
|
|
253
|
+
- data: Input
|
|
254
|
+
`;
|
|
255
|
+
|
|
256
|
+
const providers = parseToolProviders(content);
|
|
257
|
+
expect(providers).toEqual({});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should handle Providers section without bracket notation', () => {
|
|
261
|
+
const content = `
|
|
262
|
+
### Providers
|
|
263
|
+
|
|
264
|
+
#### test
|
|
265
|
+
Action: TEST_ACTION
|
|
266
|
+
`;
|
|
267
|
+
|
|
268
|
+
const providers = parseToolProviders(content);
|
|
269
|
+
expect(providers).toHaveProperty('test');
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe('ToolDocument Integration', () => {
|
|
274
|
+
it('should parse complete tool document', () => {
|
|
275
|
+
const content = `
|
|
276
|
+
---
|
|
277
|
+
Name: GmailTools
|
|
278
|
+
Type: [Tool]
|
|
279
|
+
Description: Gmail integration tools for email management.
|
|
280
|
+
Provider: composio
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
# [Imports]
|
|
284
|
+
|
|
285
|
+
[Tool]: ../core/tool.busy.md
|
|
286
|
+
|
|
287
|
+
# [Tools]
|
|
288
|
+
|
|
289
|
+
## send_email
|
|
290
|
+
|
|
291
|
+
Send an email message.
|
|
292
|
+
|
|
293
|
+
### [Inputs]
|
|
294
|
+
- to: Recipient
|
|
295
|
+
- subject: Subject
|
|
296
|
+
- body: Body
|
|
297
|
+
|
|
298
|
+
### [Outputs]
|
|
299
|
+
- message_id: Sent message ID
|
|
300
|
+
|
|
301
|
+
### [Providers]
|
|
302
|
+
|
|
303
|
+
#### composio
|
|
304
|
+
Action: GMAIL_SEND_EMAIL
|
|
305
|
+
Parameters:
|
|
306
|
+
to: to
|
|
307
|
+
subject: subject
|
|
308
|
+
body: body
|
|
309
|
+
|
|
310
|
+
## list_emails
|
|
311
|
+
|
|
312
|
+
List inbox emails.
|
|
313
|
+
|
|
314
|
+
### [Inputs]
|
|
315
|
+
- limit: Max count
|
|
316
|
+
|
|
317
|
+
### [Outputs]
|
|
318
|
+
- emails: Email list
|
|
319
|
+
|
|
320
|
+
### [Providers]
|
|
321
|
+
|
|
322
|
+
#### composio
|
|
323
|
+
Action: GMAIL_LIST_EMAILS
|
|
324
|
+
Parameters:
|
|
325
|
+
max_results: limit
|
|
326
|
+
`;
|
|
327
|
+
|
|
328
|
+
// This would be parsed by parseDocument and return a ToolDocument
|
|
329
|
+
const tools = parseTools(content);
|
|
330
|
+
expect(tools).toHaveLength(2);
|
|
331
|
+
|
|
332
|
+
const sendEmail = tools.find((t) => t.name === 'send_email');
|
|
333
|
+
expect(sendEmail?.providers?.composio?.action).toBe('GMAIL_SEND_EMAIL');
|
|
334
|
+
|
|
335
|
+
const listEmails = tools.find((t) => t.name === 'list_emails');
|
|
336
|
+
expect(listEmails?.providers?.composio?.action).toBe('GMAIL_LIST_EMAILS');
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
describe('Tool Provider Parameter Mapping', () => {
|
|
341
|
+
it('should support direct parameter mapping', () => {
|
|
342
|
+
const content = `
|
|
343
|
+
#### provider
|
|
344
|
+
Action: ACTION
|
|
345
|
+
Parameters:
|
|
346
|
+
api_to: to
|
|
347
|
+
api_subject: subject
|
|
348
|
+
`;
|
|
349
|
+
|
|
350
|
+
const providers = parseToolProviders(content);
|
|
351
|
+
expect(providers.provider.parameters).toEqual({
|
|
352
|
+
api_to: 'to',
|
|
353
|
+
api_subject: 'subject',
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('should support nested parameter values', () => {
|
|
358
|
+
const content = `
|
|
359
|
+
#### provider
|
|
360
|
+
Action: ACTION
|
|
361
|
+
Parameters:
|
|
362
|
+
recipient:
|
|
363
|
+
email: to
|
|
364
|
+
name: sender_name
|
|
365
|
+
content:
|
|
366
|
+
subject: subject
|
|
367
|
+
body: body
|
|
368
|
+
`;
|
|
369
|
+
|
|
370
|
+
const providers = parseToolProviders(content);
|
|
371
|
+
expect(providers.provider.parameters).toEqual({
|
|
372
|
+
recipient: { email: 'to', name: 'sender_name' },
|
|
373
|
+
content: { subject: 'subject', body: 'body' },
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
});
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trigger Parsing Tests - Match busy-python Trigger model
|
|
3
|
+
*
|
|
4
|
+
* busy-python supports two trigger formats:
|
|
5
|
+
* 1. Time-based (alarm): "Set alarm for <time> to run <Operation>"
|
|
6
|
+
* 2. Event-based: "When <event> [from <filter>], run <Operation>"
|
|
7
|
+
*
|
|
8
|
+
* Triggers can appear in:
|
|
9
|
+
* - # [Triggers] section as bullet points
|
|
10
|
+
* - Frontmatter as a Triggers array
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect } from 'vitest';
|
|
14
|
+
import { parseTriggers, parseTriggerDeclaration, parseTimeSpec } from '../parsers/triggers';
|
|
15
|
+
import type { Trigger } from '../types/schema';
|
|
16
|
+
|
|
17
|
+
describe('parseTriggerDeclaration', () => {
|
|
18
|
+
describe('Alarm Triggers (Time-based)', () => {
|
|
19
|
+
it('should parse simple alarm trigger', () => {
|
|
20
|
+
const text = 'Set alarm for 6am to run DailyReview';
|
|
21
|
+
|
|
22
|
+
const trigger = parseTriggerDeclaration(text);
|
|
23
|
+
expect(trigger.triggerType).toBe('alarm');
|
|
24
|
+
expect(trigger.operation).toBe('DailyReview');
|
|
25
|
+
expect(trigger.rawText).toBe(text);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should convert time to cron expression', () => {
|
|
29
|
+
const text = 'Set alarm for 6am each morning to run DailyReview';
|
|
30
|
+
|
|
31
|
+
const trigger = parseTriggerDeclaration(text);
|
|
32
|
+
expect(trigger.schedule).toBe('0 6 * * *');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should handle PM times', () => {
|
|
36
|
+
const text = 'Set alarm for 3pm to run AfternoonCheck';
|
|
37
|
+
|
|
38
|
+
const trigger = parseTriggerDeclaration(text);
|
|
39
|
+
expect(trigger.schedule).toBe('0 15 * * *');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should handle 12-hour format edge cases', () => {
|
|
43
|
+
const trigger12am = parseTriggerDeclaration('Set alarm for 12am to run Midnight');
|
|
44
|
+
expect(trigger12am.schedule).toBe('0 0 * * *');
|
|
45
|
+
|
|
46
|
+
const trigger12pm = parseTriggerDeclaration('Set alarm for 12pm to run Noon');
|
|
47
|
+
expect(trigger12pm.schedule).toBe('0 12 * * *');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should handle weekly schedules', () => {
|
|
51
|
+
const text = 'Set alarm for 9am on Monday to run WeeklySync';
|
|
52
|
+
|
|
53
|
+
const trigger = parseTriggerDeclaration(text);
|
|
54
|
+
expect(trigger.schedule).toBe('0 9 * * 1');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should handle multiple days', () => {
|
|
58
|
+
const text = 'Set alarm for 8am on Monday, Wednesday, Friday to run StandupReminder';
|
|
59
|
+
|
|
60
|
+
const trigger = parseTriggerDeclaration(text);
|
|
61
|
+
expect(trigger.schedule).toBe('0 8 * * 1,3,5');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('Event Triggers', () => {
|
|
66
|
+
it('should parse simple event trigger', () => {
|
|
67
|
+
const text = 'When gmail.message.received, run ProcessEmail';
|
|
68
|
+
|
|
69
|
+
const trigger = parseTriggerDeclaration(text);
|
|
70
|
+
expect(trigger.triggerType).toBe('event');
|
|
71
|
+
expect(trigger.eventType).toBe('gmail.message.received');
|
|
72
|
+
expect(trigger.operation).toBe('ProcessEmail');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should parse event trigger with filter', () => {
|
|
76
|
+
const text = 'When gmail.message.received from *@lead.com, run ProcessLead';
|
|
77
|
+
|
|
78
|
+
const trigger = parseTriggerDeclaration(text);
|
|
79
|
+
expect(trigger.triggerType).toBe('event');
|
|
80
|
+
expect(trigger.eventType).toBe('gmail.message.received');
|
|
81
|
+
expect(trigger.filter).toEqual({ from: '*@lead.com' });
|
|
82
|
+
expect(trigger.operation).toBe('ProcessLead');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should handle various event types', () => {
|
|
86
|
+
const events = [
|
|
87
|
+
'When slack.message.posted, run NotifyTeam',
|
|
88
|
+
'When github.pr.opened, run ReviewPR',
|
|
89
|
+
'When calendar.event.created, run ScheduleReminder',
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
for (const text of events) {
|
|
93
|
+
const trigger = parseTriggerDeclaration(text);
|
|
94
|
+
expect(trigger.triggerType).toBe('event');
|
|
95
|
+
expect(trigger.eventType).not.toBeUndefined();
|
|
96
|
+
expect(trigger.operation).not.toBeUndefined();
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should handle complex filter patterns', () => {
|
|
101
|
+
const text = 'When email.received from *@company.com, run InternalEmail';
|
|
102
|
+
|
|
103
|
+
const trigger = parseTriggerDeclaration(text);
|
|
104
|
+
expect(trigger.filter?.from).toBe('*@company.com');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('queueWhenPaused', () => {
|
|
109
|
+
it('should default to true', () => {
|
|
110
|
+
const trigger = parseTriggerDeclaration('When event.test, run TestOp');
|
|
111
|
+
expect(trigger.queueWhenPaused).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('parseTimeSpec', () => {
|
|
117
|
+
it('should parse simple hour AM', () => {
|
|
118
|
+
expect(parseTimeSpec('6am')).toBe('0 6 * * *');
|
|
119
|
+
expect(parseTimeSpec('9am')).toBe('0 9 * * *');
|
|
120
|
+
expect(parseTimeSpec('11am')).toBe('0 11 * * *');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should parse simple hour PM', () => {
|
|
124
|
+
expect(parseTimeSpec('1pm')).toBe('0 13 * * *');
|
|
125
|
+
expect(parseTimeSpec('6pm')).toBe('0 18 * * *');
|
|
126
|
+
expect(parseTimeSpec('11pm')).toBe('0 23 * * *');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should handle 12am and 12pm', () => {
|
|
130
|
+
expect(parseTimeSpec('12am')).toBe('0 0 * * *');
|
|
131
|
+
expect(parseTimeSpec('12pm')).toBe('0 12 * * *');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should handle times with "each morning"', () => {
|
|
135
|
+
expect(parseTimeSpec('6am each morning')).toBe('0 6 * * *');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should handle times with "every day"', () => {
|
|
139
|
+
expect(parseTimeSpec('3pm every day')).toBe('0 15 * * *');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should handle weekday specifications', () => {
|
|
143
|
+
expect(parseTimeSpec('9am on Monday')).toBe('0 9 * * 1');
|
|
144
|
+
expect(parseTimeSpec('9am on Tuesday')).toBe('0 9 * * 2');
|
|
145
|
+
expect(parseTimeSpec('9am on Wednesday')).toBe('0 9 * * 3');
|
|
146
|
+
expect(parseTimeSpec('9am on Thursday')).toBe('0 9 * * 4');
|
|
147
|
+
expect(parseTimeSpec('9am on Friday')).toBe('0 9 * * 5');
|
|
148
|
+
expect(parseTimeSpec('9am on Saturday')).toBe('0 9 * * 6');
|
|
149
|
+
expect(parseTimeSpec('9am on Sunday')).toBe('0 9 * * 0');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should handle multiple weekdays', () => {
|
|
153
|
+
expect(parseTimeSpec('8am on Monday, Wednesday, Friday')).toBe('0 8 * * 1,3,5');
|
|
154
|
+
expect(parseTimeSpec('10am on Tuesday, Thursday')).toBe('0 10 * * 2,4');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should handle weekdays modifier', () => {
|
|
158
|
+
expect(parseTimeSpec('9am on weekdays')).toBe('0 9 * * 1-5');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should handle weekends modifier', () => {
|
|
162
|
+
expect(parseTimeSpec('10am on weekends')).toBe('0 10 * * 0,6');
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('parseTriggers', () => {
|
|
167
|
+
it('should parse triggers from Triggers section', () => {
|
|
168
|
+
const content = `
|
|
169
|
+
# [Triggers]
|
|
170
|
+
|
|
171
|
+
- Set alarm for 6am to run DailyReview
|
|
172
|
+
- When gmail.message.received from *@lead.com, run ProcessLead
|
|
173
|
+
`;
|
|
174
|
+
|
|
175
|
+
const triggers = parseTriggers(content);
|
|
176
|
+
expect(triggers).toHaveLength(2);
|
|
177
|
+
expect(triggers[0].triggerType).toBe('alarm');
|
|
178
|
+
expect(triggers[1].triggerType).toBe('event');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should parse triggers from frontmatter', () => {
|
|
182
|
+
const content = `
|
|
183
|
+
---
|
|
184
|
+
Name: AutomatedDoc
|
|
185
|
+
Type: [Document]
|
|
186
|
+
Description: Document with triggers
|
|
187
|
+
Triggers:
|
|
188
|
+
- event_type: gmail.message.received
|
|
189
|
+
filter:
|
|
190
|
+
from: "*@important.com"
|
|
191
|
+
operation: ProcessImportant
|
|
192
|
+
queue_when_paused: false
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
# Content
|
|
196
|
+
`;
|
|
197
|
+
|
|
198
|
+
const triggers = parseTriggers(content);
|
|
199
|
+
expect(triggers).toHaveLength(1);
|
|
200
|
+
expect(triggers[0].eventType).toBe('gmail.message.received');
|
|
201
|
+
expect(triggers[0].filter).toEqual({ from: '*@important.com' });
|
|
202
|
+
expect(triggers[0].queueWhenPaused).toBe(false);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should combine frontmatter and section triggers', () => {
|
|
206
|
+
const content = `
|
|
207
|
+
---
|
|
208
|
+
Name: Mixed
|
|
209
|
+
Type: [Document]
|
|
210
|
+
Description: Mixed triggers
|
|
211
|
+
Triggers:
|
|
212
|
+
- event_type: webhook.received
|
|
213
|
+
operation: HandleWebhook
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
# [Triggers]
|
|
217
|
+
|
|
218
|
+
- Set alarm for 9am to run MorningTask
|
|
219
|
+
`;
|
|
220
|
+
|
|
221
|
+
const triggers = parseTriggers(content);
|
|
222
|
+
expect(triggers).toHaveLength(2);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should return empty array for document without triggers', () => {
|
|
226
|
+
const content = `
|
|
227
|
+
---
|
|
228
|
+
Name: NoTriggers
|
|
229
|
+
Type: [Document]
|
|
230
|
+
Description: No triggers here
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
# [Operations]
|
|
234
|
+
|
|
235
|
+
## SomeOp
|
|
236
|
+
|
|
237
|
+
### [Steps]
|
|
238
|
+
1. Do something
|
|
239
|
+
`;
|
|
240
|
+
|
|
241
|
+
const triggers = parseTriggers(content);
|
|
242
|
+
expect(triggers).toHaveLength(0);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should handle Triggers section without bracket notation', () => {
|
|
246
|
+
const content = `
|
|
247
|
+
# Triggers
|
|
248
|
+
|
|
249
|
+
- When test.event, run TestOp
|
|
250
|
+
`;
|
|
251
|
+
|
|
252
|
+
const triggers = parseTriggers(content);
|
|
253
|
+
expect(triggers).toHaveLength(1);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('should parse frontmatter alarm triggers', () => {
|
|
257
|
+
const content = `
|
|
258
|
+
---
|
|
259
|
+
Name: Scheduled
|
|
260
|
+
Type: [Document]
|
|
261
|
+
Description: Scheduled document
|
|
262
|
+
Triggers:
|
|
263
|
+
- schedule: "0 6 * * *"
|
|
264
|
+
operation: DailyTask
|
|
265
|
+
---
|
|
266
|
+
`;
|
|
267
|
+
|
|
268
|
+
const triggers = parseTriggers(content);
|
|
269
|
+
expect(triggers).toHaveLength(1);
|
|
270
|
+
expect(triggers[0].triggerType).toBe('alarm');
|
|
271
|
+
expect(triggers[0].schedule).toBe('0 6 * * *');
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe('Trigger Format Validation', () => {
|
|
276
|
+
it('should match busy-python alarm pattern', () => {
|
|
277
|
+
// busy-python pattern: r"(?i)set\s+alarm\s+for\s+(.+?)\s+to\s+run\s+(\w+)"
|
|
278
|
+
const pattern = /(?:set\s+alarm\s+for\s+)(.+?)\s+to\s+run\s+(\w+)/i;
|
|
279
|
+
|
|
280
|
+
const testCases = [
|
|
281
|
+
{ input: 'Set alarm for 6am to run DailyReview', time: '6am', op: 'DailyReview' },
|
|
282
|
+
{ input: 'set alarm for 3pm each day to run Check', time: '3pm each day', op: 'Check' },
|
|
283
|
+
{ input: 'SET ALARM FOR 9am on Monday to run Weekly', time: '9am on Monday', op: 'Weekly' },
|
|
284
|
+
];
|
|
285
|
+
|
|
286
|
+
for (const { input, time, op } of testCases) {
|
|
287
|
+
const match = pattern.exec(input);
|
|
288
|
+
expect(match).not.toBeNull();
|
|
289
|
+
expect(match?.[1]).toBe(time);
|
|
290
|
+
expect(match?.[2]).toBe(op);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('should match busy-python event pattern', () => {
|
|
295
|
+
// busy-python pattern: r"(?i)when\s+([\w.]+)(?:\s+from\s+(.+?))?,\s*run\s+(\w+)"
|
|
296
|
+
const pattern = /(?:when\s+)([\w.]+)(?:\s+from\s+(.+?))?,\s*run\s+(\w+)/i;
|
|
297
|
+
|
|
298
|
+
const testCases = [
|
|
299
|
+
{ input: 'When gmail.message.received, run Process', event: 'gmail.message.received', filter: undefined, op: 'Process' },
|
|
300
|
+
{ input: 'When email.sent from *@test.com, run Log', event: 'email.sent', filter: '*@test.com', op: 'Log' },
|
|
301
|
+
{ input: 'WHEN slack.posted, RUN Notify', event: 'slack.posted', filter: undefined, op: 'Notify' },
|
|
302
|
+
];
|
|
303
|
+
|
|
304
|
+
for (const { input, event, filter, op } of testCases) {
|
|
305
|
+
const match = pattern.exec(input);
|
|
306
|
+
expect(match).not.toBeNull();
|
|
307
|
+
expect(match?.[1]).toBe(event);
|
|
308
|
+
expect(match?.[2]).toBe(filter);
|
|
309
|
+
expect(match?.[3]).toBe(op);
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
});
|