blokctl 0.2.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 (169) hide show
  1. package/dist/commands/build/index.d.ts +2 -0
  2. package/dist/commands/build/index.js +210 -0
  3. package/dist/commands/config/index.d.ts +1 -0
  4. package/dist/commands/config/index.js +46 -0
  5. package/dist/commands/cost/index.d.ts +1 -0
  6. package/dist/commands/cost/index.js +74 -0
  7. package/dist/commands/create/node.d.ts +2 -0
  8. package/dist/commands/create/node.js +541 -0
  9. package/dist/commands/create/project.d.ts +2 -0
  10. package/dist/commands/create/project.js +941 -0
  11. package/dist/commands/create/utils/Examples.d.ts +39 -0
  12. package/dist/commands/create/utils/Examples.js +983 -0
  13. package/dist/commands/create/workflow.d.ts +2 -0
  14. package/dist/commands/create/workflow.js +109 -0
  15. package/dist/commands/deploy/index.d.ts +2 -0
  16. package/dist/commands/deploy/index.js +176 -0
  17. package/dist/commands/dev/index.d.ts +2 -0
  18. package/dist/commands/dev/index.js +190 -0
  19. package/dist/commands/generate/GenerationAnalytics.d.ts +61 -0
  20. package/dist/commands/generate/GenerationAnalytics.js +162 -0
  21. package/dist/commands/generate/GenerationAnalytics.test.d.ts +1 -0
  22. package/dist/commands/generate/GenerationAnalytics.test.js +407 -0
  23. package/dist/commands/generate/NodeFileWriter.d.ts +5 -0
  24. package/dist/commands/generate/NodeFileWriter.js +240 -0
  25. package/dist/commands/generate/NodeGenerator.d.ts +20 -0
  26. package/dist/commands/generate/NodeGenerator.js +181 -0
  27. package/dist/commands/generate/NodeGenerator.test.d.ts +1 -0
  28. package/dist/commands/generate/NodeGenerator.test.js +101 -0
  29. package/dist/commands/generate/PromptVersioning.d.ts +25 -0
  30. package/dist/commands/generate/PromptVersioning.js +71 -0
  31. package/dist/commands/generate/PromptVersioning.test.d.ts +1 -0
  32. package/dist/commands/generate/PromptVersioning.test.js +120 -0
  33. package/dist/commands/generate/RegisterNode.d.ts +3 -0
  34. package/dist/commands/generate/RegisterNode.js +37 -0
  35. package/dist/commands/generate/RuntimeGenerator.d.ts +40 -0
  36. package/dist/commands/generate/RuntimeGenerator.js +369 -0
  37. package/dist/commands/generate/RuntimeGenerator.test.d.ts +1 -0
  38. package/dist/commands/generate/RuntimeGenerator.test.js +553 -0
  39. package/dist/commands/generate/TriggerGenerator.d.ts +22 -0
  40. package/dist/commands/generate/TriggerGenerator.js +220 -0
  41. package/dist/commands/generate/TriggerGenerator.test.d.ts +1 -0
  42. package/dist/commands/generate/TriggerGenerator.test.js +209 -0
  43. package/dist/commands/generate/WorkflowGenerator.d.ts +20 -0
  44. package/dist/commands/generate/WorkflowGenerator.js +131 -0
  45. package/dist/commands/generate/WorkflowGenerator.test.d.ts +1 -0
  46. package/dist/commands/generate/WorkflowGenerator.test.js +77 -0
  47. package/dist/commands/generate/e2e/NodeGenerator.e2e.test.d.ts +1 -0
  48. package/dist/commands/generate/e2e/NodeGenerator.e2e.test.js +216 -0
  49. package/dist/commands/generate/e2e/RuntimeGenerator.e2e.test.d.ts +1 -0
  50. package/dist/commands/generate/e2e/RuntimeGenerator.e2e.test.js +759 -0
  51. package/dist/commands/generate/e2e/TriggerGenerator.e2e.test.d.ts +1 -0
  52. package/dist/commands/generate/e2e/TriggerGenerator.e2e.test.js +295 -0
  53. package/dist/commands/generate/e2e/WorkflowGenerator.e2e.test.d.ts +1 -0
  54. package/dist/commands/generate/e2e/WorkflowGenerator.e2e.test.js +353 -0
  55. package/dist/commands/generate/index.d.ts +1 -0
  56. package/dist/commands/generate/index.js +418 -0
  57. package/dist/commands/generate/prompts/create-fn-node.system.d.ts +5 -0
  58. package/dist/commands/generate/prompts/create-fn-node.system.js +256 -0
  59. package/dist/commands/generate/prompts/create-node-manifest.system.d.ts +4 -0
  60. package/dist/commands/generate/prompts/create-node-manifest.system.js +41 -0
  61. package/dist/commands/generate/prompts/create-node.system.d.ts +5 -0
  62. package/dist/commands/generate/prompts/create-node.system.js +114 -0
  63. package/dist/commands/generate/prompts/create-readme.system.d.ts +4 -0
  64. package/dist/commands/generate/prompts/create-readme.system.js +83 -0
  65. package/dist/commands/generate/prompts/create-runtime.system.d.ts +5 -0
  66. package/dist/commands/generate/prompts/create-runtime.system.js +284 -0
  67. package/dist/commands/generate/prompts/create-trigger.system.d.ts +5 -0
  68. package/dist/commands/generate/prompts/create-trigger.system.js +293 -0
  69. package/dist/commands/generate/prompts/create-workflow.system.d.ts +5 -0
  70. package/dist/commands/generate/prompts/create-workflow.system.js +476 -0
  71. package/dist/commands/generate/prompts/register-node.system.d.ts +4 -0
  72. package/dist/commands/generate/prompts/register-node.system.js +26 -0
  73. package/dist/commands/generate/validators/CompilationValidator.d.ts +9 -0
  74. package/dist/commands/generate/validators/CompilationValidator.js +86 -0
  75. package/dist/commands/generate/validators/CompilationValidator.test.d.ts +1 -0
  76. package/dist/commands/generate/validators/CompilationValidator.test.js +161 -0
  77. package/dist/commands/generate/validators/NodeValidator.d.ts +18 -0
  78. package/dist/commands/generate/validators/NodeValidator.js +217 -0
  79. package/dist/commands/generate/validators/NodeValidator.test.d.ts +1 -0
  80. package/dist/commands/generate/validators/NodeValidator.test.js +281 -0
  81. package/dist/commands/generate/validators/WorkflowValidator.d.ts +6 -0
  82. package/dist/commands/generate/validators/WorkflowValidator.js +301 -0
  83. package/dist/commands/generate/validators/WorkflowValidator.test.d.ts +1 -0
  84. package/dist/commands/generate/validators/WorkflowValidator.test.js +647 -0
  85. package/dist/commands/generate/validators/index.d.ts +4 -0
  86. package/dist/commands/generate/validators/index.js +2 -0
  87. package/dist/commands/graph/index.d.ts +1 -0
  88. package/dist/commands/graph/index.js +69 -0
  89. package/dist/commands/install/index.d.ts +1 -0
  90. package/dist/commands/install/index.js +4 -0
  91. package/dist/commands/install/node.d.ts +4 -0
  92. package/dist/commands/install/node.js +136 -0
  93. package/dist/commands/install/workflow.d.ts +4 -0
  94. package/dist/commands/install/workflow.js +62 -0
  95. package/dist/commands/login/index.d.ts +2 -0
  96. package/dist/commands/login/index.js +77 -0
  97. package/dist/commands/logout/index.d.ts +2 -0
  98. package/dist/commands/logout/index.js +20 -0
  99. package/dist/commands/marketplace/runtime.d.ts +54 -0
  100. package/dist/commands/marketplace/runtime.js +350 -0
  101. package/dist/commands/migrate/index.d.ts +1 -0
  102. package/dist/commands/migrate/index.js +14 -0
  103. package/dist/commands/migrate/node.d.ts +2 -0
  104. package/dist/commands/migrate/node.js +110 -0
  105. package/dist/commands/monitor/index.d.ts +1 -0
  106. package/dist/commands/monitor/index.js +28 -0
  107. package/dist/commands/monitor/monitor-component.d.ts +1 -0
  108. package/dist/commands/monitor/monitor-component.js +271 -0
  109. package/dist/commands/monitor/static/index.html +2124 -0
  110. package/dist/commands/monitor/static-web-server.d.ts +1 -0
  111. package/dist/commands/monitor/static-web-server.js +89 -0
  112. package/dist/commands/profile/index.d.ts +1 -0
  113. package/dist/commands/profile/index.js +112 -0
  114. package/dist/commands/publish/index.d.ts +1 -0
  115. package/dist/commands/publish/index.js +4 -0
  116. package/dist/commands/publish/node.d.ts +4 -0
  117. package/dist/commands/publish/node.js +231 -0
  118. package/dist/commands/publish/workflow.d.ts +4 -0
  119. package/dist/commands/publish/workflow.js +165 -0
  120. package/dist/commands/search/docs.d.ts +17 -0
  121. package/dist/commands/search/docs.js +179 -0
  122. package/dist/commands/search/index.d.ts +1 -0
  123. package/dist/commands/search/index.js +5 -0
  124. package/dist/commands/search/indexer.d.ts +10 -0
  125. package/dist/commands/search/indexer.js +265 -0
  126. package/dist/commands/search/nodes.d.ts +4 -0
  127. package/dist/commands/search/nodes.js +101 -0
  128. package/dist/commands/search/workflow.d.ts +4 -0
  129. package/dist/commands/search/workflow.js +100 -0
  130. package/dist/commands/trace/index.d.ts +1 -0
  131. package/dist/commands/trace/index.js +26 -0
  132. package/dist/commands/trace/startStudio.d.ts +8 -0
  133. package/dist/commands/trace/startStudio.js +116 -0
  134. package/dist/index.d.ts +17 -0
  135. package/dist/index.js +186 -0
  136. package/dist/services/commander.d.ts +9 -0
  137. package/dist/services/commander.js +20 -0
  138. package/dist/services/constants.d.ts +1 -0
  139. package/dist/services/constants.js +3 -0
  140. package/dist/services/local-token-manager.d.ts +14 -0
  141. package/dist/services/local-token-manager.js +99 -0
  142. package/dist/services/non-interactive.d.ts +5 -0
  143. package/dist/services/non-interactive.js +30 -0
  144. package/dist/services/package-manager.d.ts +35 -0
  145. package/dist/services/package-manager.js +111 -0
  146. package/dist/services/posthog.d.ts +31 -0
  147. package/dist/services/posthog.js +159 -0
  148. package/dist/services/registry-manager.d.ts +9 -0
  149. package/dist/services/registry-manager.js +26 -0
  150. package/dist/services/runtime-detector.d.ts +23 -0
  151. package/dist/services/runtime-detector.js +181 -0
  152. package/dist/services/runtime-setup.d.ts +36 -0
  153. package/dist/services/runtime-setup.js +250 -0
  154. package/dist/services/utils.d.ts +2 -0
  155. package/dist/services/utils.js +29 -0
  156. package/dist/services/workflow-loader.d.ts +30 -0
  157. package/dist/services/workflow-loader.js +46 -0
  158. package/dist/studio-dist/assets/charts-Dso0hPUR.js +68 -0
  159. package/dist/studio-dist/assets/graph-CsV2nWGn.js +23 -0
  160. package/dist/studio-dist/assets/icons-zP8LLgPh.js +311 -0
  161. package/dist/studio-dist/assets/index-CLyEkXMx.css +1 -0
  162. package/dist/studio-dist/assets/index-CNXFX_ar.js +27 -0
  163. package/dist/studio-dist/assets/react-vendor--Eh9ivFN.js +17 -0
  164. package/dist/studio-dist/assets/tanstack-query-CiM1U6F5.js +1 -0
  165. package/dist/studio-dist/assets/tanstack-router-Btjy0MKq.js +25 -0
  166. package/dist/studio-dist/assets/tanstack-table-DhwRvuH2.js +22 -0
  167. package/dist/studio-dist/favicon.svg +5 -0
  168. package/dist/studio-dist/index.html +21 -0
  169. package/package.json +75 -0
@@ -0,0 +1,295 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ vi.mock("ai", () => ({
3
+ generateText: vi.fn(),
4
+ }));
5
+ vi.mock("@ai-sdk/openai", () => ({
6
+ createOpenAI: vi.fn(() => (model) => ({ model })),
7
+ }));
8
+ vi.mock("../validators/CompilationValidator.js", () => ({
9
+ validateCode: vi.fn(),
10
+ }));
11
+ import { generateText } from "ai";
12
+ import TriggerGenerator from "../TriggerGenerator.js";
13
+ import * as CompilationValidator from "../validators/CompilationValidator.js";
14
+ const mockedGenerateText = vi.mocked(generateText);
15
+ const mockedValidateCode = vi.mocked(CompilationValidator.validateCode);
16
+ const VALID_QUEUE_TRIGGER = `
17
+ import { TriggerBase, type GlobalOptions, NodeMap, Runner } from "@blok/runner";
18
+ import { type Context, DefaultLogger } from "@blok/shared";
19
+
20
+ export default class KafkaQueueTrigger extends TriggerBase {
21
+ private nodeMap: GlobalOptions = <GlobalOptions>{};
22
+
23
+ constructor() {
24
+ super();
25
+ this.loadNodes();
26
+ this.loadWorkflows();
27
+ }
28
+
29
+ private loadNodes(): void {
30
+ this.nodeMap.nodes = new NodeMap();
31
+ }
32
+
33
+ private loadWorkflows(): void {
34
+ this.nodeMap.workflows = [];
35
+ }
36
+
37
+ async startConsumer(): Promise<void> {
38
+ const ctx = this.createContext();
39
+ ctx.request = { body: {}, headers: {} };
40
+ }
41
+ }
42
+ `;
43
+ const VALID_CRON_TRIGGER = `
44
+ import { TriggerBase, type GlobalOptions, NodeMap } from "@blok/runner";
45
+
46
+ export default class DailyCronTrigger extends TriggerBase {
47
+ private nodeMap: GlobalOptions = <GlobalOptions>{};
48
+
49
+ constructor() {
50
+ super();
51
+ this.loadNodes();
52
+ this.loadWorkflows();
53
+ }
54
+
55
+ private loadNodes(): void {}
56
+ private loadWorkflows(): void {}
57
+
58
+ async startScheduler(): Promise<void> {
59
+ const ctx = this.createContext();
60
+ ctx.request = { body: { scheduledTime: new Date().toISOString() } };
61
+ }
62
+ }
63
+ `;
64
+ const VALID_WEBHOOK_TRIGGER = `
65
+ import { TriggerBase, type GlobalOptions, NodeMap } from "@blok/runner";
66
+
67
+ export default class GitHubWebhookTrigger extends TriggerBase {
68
+ private nodeMap: GlobalOptions = <GlobalOptions>{};
69
+
70
+ constructor() {
71
+ super();
72
+ this.loadNodes();
73
+ this.loadWorkflows();
74
+ }
75
+
76
+ private loadNodes(): void {}
77
+ private loadWorkflows(): void {}
78
+
79
+ async listen(): Promise<void> {
80
+ const ctx = this.createContext();
81
+ ctx.request = { body: {}, headers: { "x-github-event": "push" }, method: "POST" };
82
+ }
83
+ }
84
+ `;
85
+ const INVALID_TRIGGER_NO_BASE = `
86
+ export default class BrokenTrigger {
87
+ constructor() {
88
+ this.loadNodes();
89
+ this.loadWorkflows();
90
+ }
91
+
92
+ private loadNodes() {}
93
+ private loadWorkflows() {}
94
+
95
+ async start() {
96
+ const ctx = this.createContext();
97
+ }
98
+ }
99
+ `;
100
+ const INVALID_TRIGGER_NO_METHODS = `
101
+ import { TriggerBase } from "@blok/runner";
102
+
103
+ export default class MinimalTrigger extends TriggerBase {
104
+ constructor() {
105
+ super();
106
+ }
107
+
108
+ async start() {
109
+ const ctx = this.createContext();
110
+ ctx.request = { body: {} };
111
+ }
112
+ }
113
+ `;
114
+ const INVALID_TRIGGER_NO_CONTEXT = `
115
+ import { TriggerBase } from "@blok/runner";
116
+
117
+ export default class NoContextTrigger extends TriggerBase {
118
+ constructor() {
119
+ super();
120
+ this.loadNodes();
121
+ this.loadWorkflows();
122
+ }
123
+
124
+ private loadNodes() {}
125
+ private loadWorkflows() {}
126
+
127
+ async start() {
128
+ console.log("Started without context");
129
+ }
130
+ }
131
+ `;
132
+ describe("TriggerGenerator E2E", () => {
133
+ let generator;
134
+ beforeEach(() => {
135
+ generator = new TriggerGenerator();
136
+ vi.clearAllMocks();
137
+ vi.spyOn(console, "log").mockImplementation(() => { });
138
+ });
139
+ afterEach(() => {
140
+ vi.restoreAllMocks();
141
+ });
142
+ describe("successful generation - queue trigger", () => {
143
+ it("should generate a valid queue trigger on first attempt", async () => {
144
+ mockedGenerateText.mockResolvedValueOnce({ text: VALID_QUEUE_TRIGGER });
145
+ mockedValidateCode.mockReturnValue({ success: true, errors: [], warnings: [] });
146
+ const result = await generator.generateTrigger("kafka-queue", "queue", "Create a Kafka queue trigger that consumes user events", "test-api-key");
147
+ expect(result.triggerName).toBe("kafka-queue");
148
+ expect(result.triggerType).toBe("queue");
149
+ expect(result.validationResult).toBeDefined();
150
+ expect(result.validationResult.valid).toBe(true);
151
+ expect(result.validationResult.attempts).toBe(1);
152
+ expect(mockedGenerateText).toHaveBeenCalledTimes(1);
153
+ });
154
+ });
155
+ describe("successful generation - cron trigger", () => {
156
+ it("should generate a valid cron trigger on first attempt", async () => {
157
+ mockedGenerateText.mockResolvedValueOnce({ text: VALID_CRON_TRIGGER });
158
+ mockedValidateCode.mockReturnValue({ success: true, errors: [], warnings: [] });
159
+ const result = await generator.generateTrigger("daily-cron", "cron", "Create a daily cron trigger", "test-api-key");
160
+ expect(result.validationResult.valid).toBe(true);
161
+ expect(result.validationResult.attempts).toBe(1);
162
+ expect(result.code).toContain("DailyCronTrigger");
163
+ });
164
+ });
165
+ describe("successful generation - webhook trigger", () => {
166
+ it("should generate a valid webhook trigger on first attempt", async () => {
167
+ mockedGenerateText.mockResolvedValueOnce({ text: VALID_WEBHOOK_TRIGGER });
168
+ mockedValidateCode.mockReturnValue({ success: true, errors: [], warnings: [] });
169
+ const result = await generator.generateTrigger("github-webhook", "webhook", "Create a GitHub webhook trigger", "test-api-key");
170
+ expect(result.validationResult.valid).toBe(true);
171
+ expect(result.code).toContain("GitHubWebhookTrigger");
172
+ });
173
+ });
174
+ describe("trigger-specific prompt guidance", () => {
175
+ it("should include queue-specific guidance in prompt", async () => {
176
+ mockedGenerateText.mockResolvedValueOnce({ text: VALID_QUEUE_TRIGGER });
177
+ mockedValidateCode.mockReturnValue({ success: true, errors: [], warnings: [] });
178
+ await generator.generateTrigger("kafka-trigger", "queue", "Process messages", "test-api-key");
179
+ const callArgs = mockedGenerateText.mock.calls[0][0];
180
+ const prompt = callArgs.prompt;
181
+ expect(prompt).toContain("message queue broker");
182
+ expect(prompt).toContain("ack/nack");
183
+ });
184
+ it("should include cron-specific guidance in prompt", async () => {
185
+ mockedGenerateText.mockResolvedValueOnce({ text: VALID_CRON_TRIGGER });
186
+ mockedValidateCode.mockReturnValue({ success: true, errors: [], warnings: [] });
187
+ await generator.generateTrigger("cron-trigger", "cron", "Run daily", "test-api-key");
188
+ const callArgs = mockedGenerateText.mock.calls[0][0];
189
+ const prompt = callArgs.prompt;
190
+ expect(prompt).toContain("cron expressions");
191
+ expect(prompt).toContain("timezone");
192
+ });
193
+ it("should include webhook-specific guidance in prompt", async () => {
194
+ mockedGenerateText.mockResolvedValueOnce({ text: VALID_WEBHOOK_TRIGGER });
195
+ mockedValidateCode.mockReturnValue({ success: true, errors: [], warnings: [] });
196
+ await generator.generateTrigger("webhook-trigger", "webhook", "Handle webhooks", "test-api-key");
197
+ const callArgs = mockedGenerateText.mock.calls[0][0];
198
+ const prompt = callArgs.prompt;
199
+ expect(prompt).toContain("signature verification");
200
+ });
201
+ });
202
+ describe("validation feedback loop", () => {
203
+ it("should retry when trigger fails compilation", async () => {
204
+ mockedGenerateText.mockResolvedValueOnce({ text: INVALID_TRIGGER_NO_BASE });
205
+ mockedGenerateText.mockResolvedValueOnce({ text: VALID_QUEUE_TRIGGER });
206
+ mockedValidateCode.mockReturnValueOnce({ success: false, errors: ["Syntax error"], warnings: [] });
207
+ mockedValidateCode.mockReturnValueOnce({ success: true, errors: [], warnings: [] });
208
+ const result = await generator.generateTrigger("kafka-queue", "queue", "Create a queue trigger", "test-api-key");
209
+ expect(result.validationResult.valid).toBe(true);
210
+ expect(result.validationResult.attempts).toBe(2);
211
+ expect(mockedGenerateText).toHaveBeenCalledTimes(2);
212
+ });
213
+ it("should retry when trigger fails structural validation", async () => {
214
+ mockedGenerateText.mockResolvedValueOnce({ text: INVALID_TRIGGER_NO_CONTEXT });
215
+ mockedGenerateText.mockResolvedValueOnce({ text: VALID_QUEUE_TRIGGER });
216
+ mockedValidateCode.mockReturnValueOnce({ success: true, errors: [], warnings: [] });
217
+ mockedValidateCode.mockReturnValueOnce({ success: true, errors: [], warnings: [] });
218
+ const result = await generator.generateTrigger("kafka-queue", "queue", "Create a queue trigger", "test-api-key");
219
+ expect(result.validationResult.valid).toBe(true);
220
+ expect(result.validationResult.attempts).toBe(2);
221
+ });
222
+ it("should include errors in feedback prompt on retry", async () => {
223
+ mockedGenerateText.mockResolvedValueOnce({ text: INVALID_TRIGGER_NO_BASE });
224
+ mockedGenerateText.mockResolvedValueOnce({ text: VALID_QUEUE_TRIGGER });
225
+ mockedValidateCode.mockReturnValueOnce({ success: false, errors: ["Missing TriggerBase import"], warnings: [] });
226
+ mockedValidateCode.mockReturnValueOnce({ success: true, errors: [], warnings: [] });
227
+ await generator.generateTrigger("kafka-queue", "queue", "Create a queue trigger", "test-api-key");
228
+ const secondCallArgs = mockedGenerateText.mock.calls[1][0];
229
+ const prompt = secondCallArgs.prompt;
230
+ expect(prompt).toContain("validation errors");
231
+ expect(prompt).toContain("Missing TriggerBase import");
232
+ });
233
+ it("should exhaust all 3 attempts when trigger keeps failing", async () => {
234
+ mockedGenerateText.mockResolvedValue({ text: INVALID_TRIGGER_NO_CONTEXT });
235
+ mockedValidateCode.mockReturnValue({ success: true, errors: [], warnings: [] });
236
+ const result = await generator.generateTrigger("broken-trigger", "queue", "Create a broken trigger", "test-api-key");
237
+ expect(result.validationResult.valid).toBe(false);
238
+ expect(result.validationResult.attempts).toBe(3);
239
+ expect(mockedGenerateText).toHaveBeenCalledTimes(3);
240
+ });
241
+ it("should succeed on third attempt", async () => {
242
+ mockedGenerateText.mockResolvedValueOnce({ text: INVALID_TRIGGER_NO_BASE });
243
+ mockedGenerateText.mockResolvedValueOnce({ text: INVALID_TRIGGER_NO_METHODS });
244
+ mockedGenerateText.mockResolvedValueOnce({ text: VALID_QUEUE_TRIGGER });
245
+ mockedValidateCode.mockReturnValueOnce({ success: false, errors: ["Missing base class"], warnings: [] });
246
+ mockedValidateCode.mockReturnValueOnce({ success: false, errors: ["Missing methods"], warnings: [] });
247
+ mockedValidateCode.mockReturnValueOnce({ success: true, errors: [], warnings: [] });
248
+ const result = await generator.generateTrigger("kafka-queue", "queue", "Create a queue trigger", "test-api-key");
249
+ expect(result.validationResult.valid).toBe(true);
250
+ expect(result.validationResult.attempts).toBe(3);
251
+ });
252
+ });
253
+ describe("markdown fence cleanup", () => {
254
+ it("should strip markdown TypeScript fences from LLM response", async () => {
255
+ const wrappedCode = "```typescript\n" + VALID_QUEUE_TRIGGER + "\n```";
256
+ mockedGenerateText.mockResolvedValueOnce({ text: wrappedCode });
257
+ mockedValidateCode.mockReturnValue({ success: true, errors: [], warnings: [] });
258
+ const result = await generator.generateTrigger("kafka-queue", "queue", "Create a queue trigger", "test-api-key");
259
+ expect(result.code).not.toContain("```");
260
+ });
261
+ });
262
+ describe("temperature and model configuration", () => {
263
+ it("should use temperature 0.2 for deterministic output", async () => {
264
+ mockedGenerateText.mockResolvedValueOnce({ text: VALID_QUEUE_TRIGGER });
265
+ mockedValidateCode.mockReturnValue({ success: true, errors: [], warnings: [] });
266
+ await generator.generateTrigger("test-trigger", "queue", "Create a test trigger", "test-api-key");
267
+ const callArgs = mockedGenerateText.mock.calls[0][0];
268
+ expect(callArgs.temperature).toBe(0.2);
269
+ });
270
+ it("should use the trigger system prompt", async () => {
271
+ mockedGenerateText.mockResolvedValueOnce({ text: VALID_QUEUE_TRIGGER });
272
+ mockedValidateCode.mockReturnValue({ success: true, errors: [], warnings: [] });
273
+ await generator.generateTrigger("test-trigger", "queue", "Create a test trigger", "test-api-key");
274
+ const callArgs = mockedGenerateText.mock.calls[0][0];
275
+ const system = callArgs.system;
276
+ expect(system).toContain("TriggerBase");
277
+ expect(system).toContain("createContext");
278
+ expect(system).toContain("loadNodes");
279
+ });
280
+ });
281
+ describe("analytics integration", () => {
282
+ it("should include prompt version in validation result", async () => {
283
+ mockedGenerateText.mockResolvedValueOnce({ text: VALID_QUEUE_TRIGGER });
284
+ mockedValidateCode.mockReturnValue({ success: true, errors: [], warnings: [] });
285
+ const result = await generator.generateTrigger("test-trigger", "queue", "Create a test trigger", "test-api-key");
286
+ expect(result.validationResult.promptVersion).toContain("create-trigger@");
287
+ });
288
+ it("should include duration in validation result", async () => {
289
+ mockedGenerateText.mockResolvedValueOnce({ text: VALID_QUEUE_TRIGGER });
290
+ mockedValidateCode.mockReturnValue({ success: true, errors: [], warnings: [] });
291
+ const result = await generator.generateTrigger("test-trigger", "queue", "Create a test trigger", "test-api-key");
292
+ expect(result.validationResult.durationMs).toBeGreaterThanOrEqual(0);
293
+ });
294
+ });
295
+ });
@@ -0,0 +1,353 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ vi.mock("ai", () => ({
3
+ generateText: vi.fn(),
4
+ }));
5
+ vi.mock("@ai-sdk/openai", () => ({
6
+ createOpenAI: vi.fn(() => (model) => ({ model })),
7
+ }));
8
+ import { generateText } from "ai";
9
+ import WorkflowGenerator from "../WorkflowGenerator.js";
10
+ const mockedGenerateText = vi.mocked(generateText);
11
+ const VALID_HTTP_WORKFLOW = JSON.stringify({
12
+ name: "User API",
13
+ description: "Fetches user data from external API",
14
+ version: "1.0.0",
15
+ trigger: {
16
+ http: {
17
+ method: "GET",
18
+ path: "/api/users/:id",
19
+ accept: "application/json",
20
+ },
21
+ },
22
+ steps: [
23
+ {
24
+ name: "fetch-user",
25
+ node: "@blok/api-call",
26
+ type: "module",
27
+ },
28
+ ],
29
+ nodes: {
30
+ "fetch-user": {
31
+ inputs: {
32
+ url: "https://api.example.com/users/${ctx.request.params.id}",
33
+ method: "GET",
34
+ headers: { Authorization: "Bearer ${ctx.env.API_KEY}" },
35
+ },
36
+ },
37
+ },
38
+ }, null, 2);
39
+ const VALID_QUEUE_WORKFLOW = JSON.stringify({
40
+ name: "Event Processor",
41
+ description: "Processes events from Kafka queue",
42
+ version: "1.0.0",
43
+ trigger: {
44
+ queue: {
45
+ provider: "kafka",
46
+ topic: "user-events",
47
+ consumerGroup: "event-processor",
48
+ ack: true,
49
+ },
50
+ },
51
+ steps: [
52
+ {
53
+ name: "process-event",
54
+ node: "event-handler",
55
+ type: "module",
56
+ },
57
+ ],
58
+ nodes: {
59
+ "process-event": {
60
+ inputs: {
61
+ eventType: "${ctx.request.body.type}",
62
+ payload: "${ctx.request.body.data}",
63
+ },
64
+ },
65
+ },
66
+ }, null, 2);
67
+ const VALID_CRON_WORKFLOW = JSON.stringify({
68
+ name: "Daily Report",
69
+ description: "Generates daily reports every morning",
70
+ version: "1.0.0",
71
+ trigger: {
72
+ cron: {
73
+ schedule: "0 8 * * *",
74
+ timezone: "America/New_York",
75
+ overlap: false,
76
+ },
77
+ },
78
+ steps: [
79
+ {
80
+ name: "generate-report",
81
+ node: "report-generator",
82
+ type: "module",
83
+ },
84
+ ],
85
+ nodes: {
86
+ "generate-report": {
87
+ inputs: {
88
+ format: "html",
89
+ recipients: ["admin@example.com"],
90
+ },
91
+ },
92
+ },
93
+ }, null, 2);
94
+ const VALID_WEBHOOK_WORKFLOW = JSON.stringify({
95
+ name: "GitHub Handler",
96
+ description: "Handles GitHub webhook events",
97
+ version: "1.0.0",
98
+ trigger: {
99
+ webhook: {
100
+ source: "github",
101
+ events: ["push", "pull_request.*"],
102
+ secret: "${process.env.GITHUB_WEBHOOK_SECRET}",
103
+ },
104
+ },
105
+ steps: [
106
+ {
107
+ name: "handle-event",
108
+ node: "github-event-handler",
109
+ type: "module",
110
+ },
111
+ ],
112
+ nodes: {
113
+ "handle-event": {
114
+ inputs: {
115
+ event: "${ctx.request.body}",
116
+ eventType: "${ctx.request.headers['x-github-event']}",
117
+ },
118
+ },
119
+ },
120
+ }, null, 2);
121
+ const INVALID_WORKFLOW_MISSING_STEPS = JSON.stringify({
122
+ name: "Broken Workflow",
123
+ version: "1.0.0",
124
+ trigger: { http: { method: "GET" } },
125
+ steps: [],
126
+ nodes: {},
127
+ });
128
+ const INVALID_WORKFLOW_BAD_TRIGGER = JSON.stringify({
129
+ name: "Bad Trigger Workflow",
130
+ version: "1.0.0",
131
+ trigger: { invalid_trigger: {} },
132
+ steps: [{ name: "step1", node: "node1", type: "module" }],
133
+ nodes: { step1: { inputs: {} } },
134
+ });
135
+ const INVALID_WORKFLOW_MISMATCHED_NODES = JSON.stringify({
136
+ name: "Mismatched Workflow",
137
+ version: "1.0.0",
138
+ trigger: { http: { method: "GET" } },
139
+ steps: [
140
+ { name: "step1", node: "node1", type: "module" },
141
+ { name: "step2", node: "node2", type: "module" },
142
+ ],
143
+ nodes: {
144
+ step1: { inputs: {} },
145
+ },
146
+ });
147
+ describe("WorkflowGenerator E2E", () => {
148
+ let generator;
149
+ beforeEach(() => {
150
+ generator = new WorkflowGenerator();
151
+ vi.clearAllMocks();
152
+ vi.spyOn(console, "log").mockImplementation(() => { });
153
+ });
154
+ afterEach(() => {
155
+ vi.restoreAllMocks();
156
+ });
157
+ describe("successful generation - HTTP trigger", () => {
158
+ it("should generate a valid HTTP workflow on first attempt", async () => {
159
+ mockedGenerateText.mockResolvedValueOnce({
160
+ text: VALID_HTTP_WORKFLOW,
161
+ });
162
+ const result = await generator.generateWorkflow("user-api", "Create an API that fetches user data", "test-api-key", "http");
163
+ expect(result.workflowName).toBe("user-api");
164
+ expect(result.triggerType).toBe("http");
165
+ expect(result.validationResult).toBeDefined();
166
+ expect(result.validationResult.valid).toBe(true);
167
+ expect(result.validationResult.attempts).toBe(1);
168
+ expect(mockedGenerateText).toHaveBeenCalledTimes(1);
169
+ const parsed = JSON.parse(result.json);
170
+ expect(parsed.name).toBe("User API");
171
+ expect(parsed.trigger.http).toBeDefined();
172
+ });
173
+ });
174
+ describe("successful generation - Queue trigger", () => {
175
+ it("should generate a valid queue workflow on first attempt", async () => {
176
+ mockedGenerateText.mockResolvedValueOnce({
177
+ text: VALID_QUEUE_WORKFLOW,
178
+ });
179
+ const result = await generator.generateWorkflow("event-processor", "Process events from Kafka", "test-api-key", "queue");
180
+ expect(result.validationResult.valid).toBe(true);
181
+ expect(result.validationResult.attempts).toBe(1);
182
+ const parsed = JSON.parse(result.json);
183
+ expect(parsed.trigger.queue.provider).toBe("kafka");
184
+ expect(parsed.trigger.queue.topic).toBe("user-events");
185
+ });
186
+ });
187
+ describe("successful generation - Cron trigger", () => {
188
+ it("should generate a valid cron workflow on first attempt", async () => {
189
+ mockedGenerateText.mockResolvedValueOnce({
190
+ text: VALID_CRON_WORKFLOW,
191
+ });
192
+ const result = await generator.generateWorkflow("daily-report", "Generate daily reports at 8am", "test-api-key", "cron");
193
+ expect(result.validationResult.valid).toBe(true);
194
+ const parsed = JSON.parse(result.json);
195
+ expect(parsed.trigger.cron.schedule).toBe("0 8 * * *");
196
+ expect(parsed.trigger.cron.timezone).toBe("America/New_York");
197
+ });
198
+ });
199
+ describe("successful generation - Webhook trigger", () => {
200
+ it("should generate a valid webhook workflow on first attempt", async () => {
201
+ mockedGenerateText.mockResolvedValueOnce({
202
+ text: VALID_WEBHOOK_WORKFLOW,
203
+ });
204
+ const result = await generator.generateWorkflow("github-handler", "Handle GitHub webhook events for push and PR", "test-api-key", "webhook");
205
+ expect(result.validationResult.valid).toBe(true);
206
+ const parsed = JSON.parse(result.json);
207
+ expect(parsed.trigger.webhook.source).toBe("github");
208
+ expect(parsed.trigger.webhook.events).toContain("push");
209
+ });
210
+ });
211
+ describe("markdown fence cleanup", () => {
212
+ it("should strip markdown JSON fences from LLM response", async () => {
213
+ const wrappedJson = "```json\n" + VALID_HTTP_WORKFLOW + "\n```";
214
+ mockedGenerateText.mockResolvedValueOnce({
215
+ text: wrappedJson,
216
+ });
217
+ const result = await generator.generateWorkflow("test-workflow", "Create a test workflow", "test-api-key", "http");
218
+ expect(result.validationResult.valid).toBe(true);
219
+ expect(result.json).not.toContain("```");
220
+ });
221
+ });
222
+ describe("validation feedback loop", () => {
223
+ it("should retry with feedback on first failure and succeed on second attempt", async () => {
224
+ mockedGenerateText.mockResolvedValueOnce({
225
+ text: INVALID_WORKFLOW_MISSING_STEPS,
226
+ });
227
+ mockedGenerateText.mockResolvedValueOnce({
228
+ text: VALID_HTTP_WORKFLOW,
229
+ });
230
+ const result = await generator.generateWorkflow("user-api", "Create a user API", "test-api-key", "http");
231
+ expect(result.validationResult.valid).toBe(true);
232
+ expect(result.validationResult.attempts).toBe(2);
233
+ expect(mockedGenerateText).toHaveBeenCalledTimes(2);
234
+ });
235
+ it("should include validation errors in feedback prompt", async () => {
236
+ mockedGenerateText.mockResolvedValueOnce({
237
+ text: INVALID_WORKFLOW_BAD_TRIGGER,
238
+ });
239
+ mockedGenerateText.mockResolvedValueOnce({
240
+ text: VALID_HTTP_WORKFLOW,
241
+ });
242
+ await generator.generateWorkflow("test-workflow", "Create a test workflow", "test-api-key", "http");
243
+ const secondCallArgs = mockedGenerateText.mock.calls[1][0];
244
+ const prompt = secondCallArgs.prompt;
245
+ expect(prompt).toContain("validation errors");
246
+ expect(prompt).toContain("invalid_trigger");
247
+ });
248
+ it("should exhaust all 3 attempts when workflow keeps failing", async () => {
249
+ mockedGenerateText.mockResolvedValue({
250
+ text: INVALID_WORKFLOW_MISSING_STEPS,
251
+ });
252
+ const result = await generator.generateWorkflow("broken-workflow", "Create something broken", "test-api-key", "http");
253
+ expect(result.validationResult.valid).toBe(false);
254
+ expect(result.validationResult.attempts).toBe(3);
255
+ expect(mockedGenerateText).toHaveBeenCalledTimes(3);
256
+ });
257
+ it("should fix mismatched nodes on retry", async () => {
258
+ mockedGenerateText.mockResolvedValueOnce({
259
+ text: INVALID_WORKFLOW_MISMATCHED_NODES,
260
+ });
261
+ mockedGenerateText.mockResolvedValueOnce({
262
+ text: VALID_HTTP_WORKFLOW,
263
+ });
264
+ const result = await generator.generateWorkflow("user-api", "Create a user API", "test-api-key", "http");
265
+ expect(result.validationResult.valid).toBe(true);
266
+ expect(result.validationResult.attempts).toBe(2);
267
+ });
268
+ });
269
+ describe("invalid JSON handling", () => {
270
+ it("should handle LLM returning invalid JSON", async () => {
271
+ mockedGenerateText.mockResolvedValue({
272
+ text: "This is not valid JSON at all {{{",
273
+ });
274
+ const result = await generator.generateWorkflow("broken-workflow", "Create something", "test-api-key", "http");
275
+ expect(result.validationResult.valid).toBe(false);
276
+ expect(result.validationResult.errors.some((e) => e.includes("Invalid JSON"))).toBe(true);
277
+ expect(result.validationResult.attempts).toBe(3);
278
+ });
279
+ });
280
+ describe("trigger type enforcement", () => {
281
+ it("should include trigger type context in prompt for queue", async () => {
282
+ mockedGenerateText.mockResolvedValueOnce({
283
+ text: VALID_QUEUE_WORKFLOW,
284
+ });
285
+ await generator.generateWorkflow("event-processor", "Process events", "test-api-key", "queue");
286
+ const callArgs = mockedGenerateText.mock.calls[0][0];
287
+ const prompt = callArgs.prompt;
288
+ expect(prompt).toContain("queue");
289
+ expect(prompt).toContain('MUST use the "queue" trigger type');
290
+ });
291
+ it("should not enforce trigger type for auto mode", async () => {
292
+ mockedGenerateText.mockResolvedValueOnce({
293
+ text: VALID_HTTP_WORKFLOW,
294
+ });
295
+ await generator.generateWorkflow("auto-workflow", "Create an API", "test-api-key", "auto");
296
+ const callArgs = mockedGenerateText.mock.calls[0][0];
297
+ const prompt = callArgs.prompt;
298
+ expect(prompt).not.toContain("MUST use the");
299
+ });
300
+ });
301
+ describe("conditional workflow generation", () => {
302
+ it("should validate conditional workflows with if-else routing", async () => {
303
+ const conditionalWorkflow = JSON.stringify({
304
+ name: "CRUD Router",
305
+ description: "Routes CRUD operations",
306
+ version: "1.0.0",
307
+ trigger: {
308
+ http: {
309
+ method: "*",
310
+ path: "/:function?/:id?",
311
+ accept: "application/json",
312
+ },
313
+ },
314
+ steps: [
315
+ {
316
+ name: "route-request",
317
+ node: "@blok/if-else",
318
+ type: "module",
319
+ },
320
+ ],
321
+ nodes: {
322
+ "route-request": {
323
+ conditions: [
324
+ {
325
+ type: "if",
326
+ condition: 'ctx.request.method.toLowerCase() === "get"',
327
+ steps: [{ name: "get-data", node: "data-fetcher", type: "module" }],
328
+ },
329
+ {
330
+ type: "if",
331
+ condition: 'ctx.request.method.toLowerCase() === "post"',
332
+ steps: [{ name: "create-data", node: "data-creator", type: "module" }],
333
+ },
334
+ {
335
+ type: "else",
336
+ steps: [{ name: "error-handler", node: "error", type: "module" }],
337
+ },
338
+ ],
339
+ },
340
+ "get-data": { inputs: {} },
341
+ "create-data": { inputs: { body: "${ctx.request.body}" } },
342
+ "error-handler": { inputs: { message: "Method not allowed", code: 405 } },
343
+ },
344
+ }, null, 2);
345
+ mockedGenerateText.mockResolvedValueOnce({
346
+ text: conditionalWorkflow,
347
+ });
348
+ const result = await generator.generateWorkflow("crud-router", "Create a CRUD router with conditional routing", "test-api-key", "http");
349
+ expect(result.validationResult.valid).toBe(true);
350
+ expect(result.validationResult.attempts).toBe(1);
351
+ });
352
+ });
353
+ });
@@ -0,0 +1 @@
1
+ export {};