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,759 @@
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 RuntimeGenerator from "../RuntimeGenerator.js";
10
+ const mockedGenerateText = vi.mocked(generateText);
11
+ const VALID_GO_RUNTIME = `
12
+ // FILE: sdk/blok.go
13
+ package sdk
14
+
15
+ type Context struct {
16
+ ID string
17
+ WorkflowName string
18
+ Request Request
19
+ Response Response
20
+ }
21
+
22
+ type Request struct {
23
+ Body interface{}
24
+ Headers map[string]string
25
+ }
26
+
27
+ type Response struct {
28
+ Data interface{}
29
+ Success bool
30
+ }
31
+
32
+ type ExecutionRequest struct {
33
+ NodeName string
34
+ Context Context
35
+ Config map[string]interface{}
36
+ }
37
+
38
+ type ExecutionResult struct {
39
+ success bool
40
+ data interface{}
41
+ errors interface{}
42
+ }
43
+
44
+ type NodeHandler interface {
45
+ Execute(ctx Context, config map[string]interface{}) (interface{}, error)
46
+ }
47
+
48
+ type NodeRegistry struct {
49
+ nodes map[string]NodeHandler
50
+ }
51
+
52
+ func NewRegistry() *NodeRegistry {
53
+ return &NodeRegistry{nodes: make(map[string]NodeHandler)}
54
+ }
55
+
56
+ func (r *NodeRegistry) Register(name string, handler NodeHandler) {
57
+ r.nodes[name] = handler
58
+ }
59
+
60
+ func (r *NodeRegistry) Get(name string) (NodeHandler, bool) {
61
+ h, ok := r.nodes[name]
62
+ return h, ok
63
+ }
64
+
65
+ // FILE: server/main.go
66
+ package main
67
+
68
+ import (
69
+ "encoding/json"
70
+ "net/http"
71
+ )
72
+
73
+ func main() {
74
+ http.HandleFunc("/execute", handleExecute)
75
+ http.HandleFunc("/health", handleHealth)
76
+ http.ListenAndServe(":8080", nil)
77
+ }
78
+
79
+ func handleExecute(w http.ResponseWriter, r *http.Request) {
80
+ json.NewEncoder(w).Encode(map[string]interface{}{"success": true, "data": nil, "errors": nil})
81
+ }
82
+
83
+ func handleHealth(w http.ResponseWriter, r *http.Request) {
84
+ json.NewEncoder(w).Encode(map[string]interface{}{"status": "healthy"})
85
+ }
86
+
87
+ // FILE: nodes/hello-world/main.go
88
+ package helloworld
89
+
90
+ import "fmt"
91
+
92
+ type HelloWorldNode struct{}
93
+
94
+ func (n *HelloWorldNode) Execute(ctx interface{}, config map[string]interface{}) (interface{}, error) {
95
+ fmt.Println("Hello World!")
96
+ return map[string]string{"message": "Hello World"}, nil
97
+ }
98
+
99
+ // FILE: go.mod
100
+ module github.com/blok/runtime-go
101
+
102
+ go 1.21
103
+
104
+ // FILE: Dockerfile
105
+ FROM golang:1.21-alpine AS builder
106
+ WORKDIR /app
107
+ COPY . .
108
+ RUN go build -o runtime ./server/main.go
109
+
110
+ FROM alpine:3.18
111
+ COPY --from=builder /app/runtime /runtime
112
+ EXPOSE 8080
113
+ CMD ["/runtime"]
114
+ `;
115
+ const VALID_JAVA_RUNTIME = `
116
+ // FILE: src/main/java/com/blok/runtime/Blok.java
117
+ package com.blok.runtime;
118
+
119
+ import java.util.Map;
120
+
121
+ public class Blok {
122
+ public static class Context {
123
+ public String id;
124
+ public String workflowName;
125
+ public Request request;
126
+ public Response response;
127
+ }
128
+
129
+ public static class Request {
130
+ public Object body;
131
+ public Map<String, String> headers;
132
+ }
133
+
134
+ public static class Response {
135
+ public Object data;
136
+ public boolean success;
137
+ public Object errors;
138
+ }
139
+
140
+ public static class ExecutionRequest {
141
+ public String nodeName;
142
+ public Context context;
143
+ public Map<String, Object> config;
144
+ }
145
+
146
+ public static class ExecutionResult {
147
+ public boolean success;
148
+ public Object data;
149
+ public Object errors;
150
+ }
151
+ }
152
+
153
+ // FILE: src/main/java/com/blok/runtime/NodeRegistry.java
154
+ package com.blok.runtime;
155
+
156
+ import java.util.HashMap;
157
+ import java.util.Map;
158
+
159
+ public interface NodeHandler {
160
+ Object Execute(Blok.Context ctx, Map<String, Object> config) throws Exception;
161
+ }
162
+
163
+ // FILE: src/main/java/com/blok/server/RuntimeServer.java
164
+ package com.blok.server;
165
+
166
+ import com.sun.net.httpserver.HttpServer;
167
+ import java.net.InetSocketAddress;
168
+
169
+ public class RuntimeServer {
170
+ public static void main(String[] args) throws Exception {
171
+ HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
172
+ server.createContext("/execute", exchange -> {
173
+ exchange.sendResponseHeaders(200, 0);
174
+ exchange.getResponseBody().close();
175
+ });
176
+ server.createContext("/health", exchange -> {
177
+ exchange.sendResponseHeaders(200, 0);
178
+ exchange.getResponseBody().close();
179
+ });
180
+ server.start();
181
+ }
182
+ }
183
+
184
+ // FILE: pom.xml
185
+ <project>
186
+ <modelVersion>4.0.0</modelVersion>
187
+ <groupId>com.blok</groupId>
188
+ <artifactId>runtime-java</artifactId>
189
+ <version>1.0.0</version>
190
+ </project>
191
+
192
+ // FILE: Dockerfile
193
+ FROM maven:3.9-eclipse-temurin-21 AS builder
194
+ WORKDIR /app
195
+ COPY . .
196
+ RUN mvn clean package
197
+
198
+ FROM eclipse-temurin:21-jre
199
+ COPY --from=builder /app/target/*.jar /app.jar
200
+ EXPOSE 8080
201
+ CMD ["java", "-jar", "/app.jar"]
202
+ `;
203
+ const VALID_RUST_RUNTIME = `
204
+ // FILE: src/lib.rs
205
+ pub struct Context {
206
+ pub id: String,
207
+ pub workflow_name: String,
208
+ pub request: Request,
209
+ pub response: Response,
210
+ }
211
+
212
+ pub struct Request {
213
+ pub body: serde_json::Value,
214
+ pub headers: std::collections::HashMap<String, String>,
215
+ }
216
+
217
+ pub struct Response {
218
+ pub data: serde_json::Value,
219
+ pub success: bool,
220
+ pub errors: Option<serde_json::Value>,
221
+ }
222
+
223
+ pub struct ExecutionRequest {
224
+ pub node_name: String,
225
+ pub context: Context,
226
+ pub config: std::collections::HashMap<String, serde_json::Value>,
227
+ }
228
+
229
+ pub struct ExecutionResult {
230
+ pub success: bool,
231
+ pub data: serde_json::Value,
232
+ pub errors: Option<serde_json::Value>,
233
+ }
234
+
235
+ pub trait NodeHandler: Send + Sync {
236
+ fn execute(&self, ctx: &Context, config: &std::collections::HashMap<String, serde_json::Value>) -> Result<serde_json::Value, String>;
237
+ }
238
+
239
+ // FILE: src/registry.rs
240
+ use std::collections::HashMap;
241
+ use crate::NodeHandler;
242
+
243
+ pub struct NodeRegistry {
244
+ nodes: HashMap<String, Box<dyn NodeHandler>>,
245
+ }
246
+
247
+ impl NodeRegistry {
248
+ pub fn new() -> Self {
249
+ NodeRegistry { nodes: HashMap::new() }
250
+ }
251
+
252
+ pub fn register(&mut self, name: &str, handler: Box<dyn NodeHandler>) {
253
+ self.nodes.insert(name.to_string(), handler);
254
+ }
255
+ }
256
+
257
+ // FILE: src/main.rs
258
+ use actix_web::{web, App, HttpServer, HttpResponse};
259
+
260
+ async fn execute_handler() -> HttpResponse {
261
+ HttpResponse::Ok().json(serde_json::json!({"success": true, "data": null, "errors": null}))
262
+ }
263
+
264
+ async fn health_handler() -> HttpResponse {
265
+ HttpResponse::Ok().json(serde_json::json!({"status": "healthy"}))
266
+ }
267
+
268
+ #[actix_web::main]
269
+ async fn main() -> std::io::Result<()> {
270
+ HttpServer::new(|| {
271
+ App::new()
272
+ .route("/execute", web::post().to(execute_handler))
273
+ .route("/health", web::get().to(health_handler))
274
+ })
275
+ .bind("0.0.0.0:8080")?
276
+ .run()
277
+ .await
278
+ }
279
+
280
+ // FILE: Cargo.toml
281
+ [package]
282
+ name = "blok-runtime-rust"
283
+ version = "0.1.0"
284
+ edition = "2021"
285
+
286
+ [dependencies]
287
+ actix-web = "4"
288
+ serde = { version = "1", features = ["derive"] }
289
+ serde_json = "1"
290
+
291
+ // FILE: Dockerfile
292
+ FROM rust:1.73 AS builder
293
+ WORKDIR /app
294
+ COPY . .
295
+ RUN cargo build --release
296
+
297
+ FROM debian:bookworm-slim
298
+ COPY --from=builder /app/target/release/blok-runtime-rust /runtime
299
+ EXPOSE 8080
300
+ CMD ["/runtime"]
301
+ `;
302
+ const VALID_PYTHON_RUNTIME = `
303
+ // FILE: blok/__init__.py
304
+ class Context:
305
+ def __init__(self):
306
+ self.id = ""
307
+ self.workflow_name = ""
308
+ self.request = {"body": {}, "headers": {}}
309
+ self.response = {"data": None, "success": True, "errors": None}
310
+
311
+ class ExecutionRequest:
312
+ def __init__(self, node_name, context, config=None):
313
+ self.node_name = node_name
314
+ self.context = context
315
+ self.config = config or {}
316
+
317
+ class ExecutionResult:
318
+ def __init__(self, success=True, data=None, errors=None):
319
+ self.success = success
320
+ self.data = data
321
+ self.errors = errors
322
+
323
+ // FILE: blok/registry.py
324
+ class NodeHandler:
325
+ def execute(self, ctx, config):
326
+ raise NotImplementedError
327
+
328
+ class NodeRegistry:
329
+ def __init__(self):
330
+ self._nodes = {}
331
+
332
+ def register(self, name, handler):
333
+ self._nodes[name] = handler
334
+
335
+ def get(self, name):
336
+ return self._nodes.get(name)
337
+
338
+ // FILE: server.py
339
+ from flask import Flask, request, jsonify
340
+ from blok import Context
341
+
342
+ app = Flask(__name__)
343
+
344
+ @app.route("/execute", methods=["POST"])
345
+ def execute():
346
+ return jsonify({"success": True, "data": None, "errors": None})
347
+
348
+ @app.route("/health", methods=["GET"])
349
+ def health():
350
+ return jsonify({"status": "healthy"})
351
+
352
+ if __name__ == "__main__":
353
+ app.run(host="0.0.0.0", port=8080)
354
+
355
+ // FILE: nodes/hello_world.py
356
+ from blok.registry import NodeHandler
357
+
358
+ class HelloWorldNode(NodeHandler):
359
+ def execute(self, ctx, config):
360
+ return {"message": "Hello World"}
361
+
362
+ // FILE: requirements.txt
363
+ flask>=3.0.0
364
+ gunicorn>=21.2.0
365
+
366
+ // FILE: Dockerfile
367
+ FROM python:3.12-slim
368
+ WORKDIR /app
369
+ COPY requirements.txt .
370
+ RUN pip install -r requirements.txt
371
+ COPY . .
372
+ EXPOSE 8080
373
+ CMD ["gunicorn", "-b", "0.0.0.0:8080", "server:app"]
374
+ `;
375
+ const INVALID_RUNTIME_NO_FILES = `
376
+ package main
377
+
378
+ import "fmt"
379
+
380
+ func main() {
381
+ fmt.Println("Hello World - no file markers, no endpoints, no registry")
382
+ }
383
+ `;
384
+ const INVALID_RUNTIME_MISSING_ENDPOINTS = `
385
+ // FILE: sdk/blok.go
386
+ package sdk
387
+
388
+ type Context struct {
389
+ ID string
390
+ }
391
+
392
+ type NodeHandler interface {
393
+ Run(ctx Context) error
394
+ }
395
+
396
+ type NodeRegistry struct {
397
+ nodes map[string]NodeHandler
398
+ }
399
+
400
+ func NewRegistry() *NodeRegistry {
401
+ return &NodeRegistry{nodes: make(map[string]NodeHandler)}
402
+ }
403
+
404
+ func (r *NodeRegistry) Register(name string, handler NodeHandler) {
405
+ r.nodes[name] = handler
406
+ }
407
+
408
+ // FILE: server/main.go
409
+ package main
410
+
411
+ func main() {
412
+ // Missing required HTTP endpoints
413
+ println("server started")
414
+ }
415
+
416
+ // FILE: Dockerfile
417
+ FROM golang:1.21-alpine
418
+ WORKDIR /app
419
+ COPY . .
420
+ CMD ["go", "run", "."]
421
+ `;
422
+ const INVALID_RUNTIME_NO_HANDLER = `
423
+ // FILE: main.go
424
+ package main
425
+
426
+ import "net/http"
427
+
428
+ func main() {
429
+ http.HandleFunc("/execute", func(w http.ResponseWriter, r *http.Request) {
430
+ w.Write([]byte("ok"))
431
+ })
432
+ http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
433
+ w.Write([]byte("healthy"))
434
+ })
435
+ http.ListenAndServe(":8080", nil)
436
+ }
437
+
438
+ // FILE: types.go
439
+ package main
440
+
441
+ type Context struct {
442
+ ID string
443
+ Data map[string]interface{}
444
+ }
445
+
446
+ type ExecutionResult struct {
447
+ success bool
448
+ data interface{}
449
+ errors interface{}
450
+ }
451
+
452
+ // FILE: Dockerfile
453
+ FROM golang:1.21-alpine
454
+ WORKDIR /app
455
+ COPY . .
456
+ CMD ["go", "run", "."]
457
+ `;
458
+ describe("RuntimeGenerator E2E", () => {
459
+ let generator;
460
+ beforeEach(() => {
461
+ generator = new RuntimeGenerator();
462
+ vi.clearAllMocks();
463
+ vi.spyOn(console, "log").mockImplementation(() => { });
464
+ });
465
+ afterEach(() => {
466
+ vi.restoreAllMocks();
467
+ });
468
+ describe("successful generation - Go runtime", () => {
469
+ it("should generate a valid Go runtime on first attempt", async () => {
470
+ mockedGenerateText.mockResolvedValueOnce({ text: VALID_GO_RUNTIME });
471
+ const result = await generator.generateRuntime("go", "Create a Go runtime SDK for Blok", "test-api-key");
472
+ expect(result.language).toBe("go");
473
+ expect(result.validationResult).toBeDefined();
474
+ expect(result.validationResult.valid).toBe(true);
475
+ expect(result.validationResult.attempts).toBe(1);
476
+ expect(mockedGenerateText).toHaveBeenCalledTimes(1);
477
+ });
478
+ it("should parse multiple files from Go runtime output", async () => {
479
+ mockedGenerateText.mockResolvedValueOnce({ text: VALID_GO_RUNTIME });
480
+ const result = await generator.generateRuntime("go", "Create a Go runtime SDK", "test-api-key");
481
+ expect(result.files.length).toBeGreaterThanOrEqual(4);
482
+ const filePaths = result.files.map((f) => f.path);
483
+ expect(filePaths).toContain("sdk/blok.go");
484
+ expect(filePaths).toContain("server/main.go");
485
+ expect(filePaths).toContain("go.mod");
486
+ expect(filePaths).toContain("Dockerfile");
487
+ });
488
+ });
489
+ describe("successful generation - Java runtime", () => {
490
+ it("should generate a valid Java runtime on first attempt", async () => {
491
+ mockedGenerateText.mockResolvedValueOnce({ text: VALID_JAVA_RUNTIME });
492
+ const result = await generator.generateRuntime("java", "Create a Java runtime SDK for Blok", "test-api-key");
493
+ expect(result.validationResult.valid).toBe(true);
494
+ expect(result.validationResult.attempts).toBe(1);
495
+ expect(result.files.length).toBeGreaterThanOrEqual(4);
496
+ });
497
+ it("should parse Java-specific files", async () => {
498
+ mockedGenerateText.mockResolvedValueOnce({ text: VALID_JAVA_RUNTIME });
499
+ const result = await generator.generateRuntime("java", "Create a Java runtime", "test-api-key");
500
+ const filePaths = result.files.map((f) => f.path);
501
+ expect(filePaths.some((p) => p.endsWith(".java"))).toBe(true);
502
+ expect(filePaths.some((p) => p === "pom.xml")).toBe(true);
503
+ expect(filePaths.some((p) => p === "Dockerfile")).toBe(true);
504
+ });
505
+ });
506
+ describe("successful generation - Rust runtime", () => {
507
+ it("should generate a valid Rust runtime on first attempt", async () => {
508
+ mockedGenerateText.mockResolvedValueOnce({ text: VALID_RUST_RUNTIME });
509
+ const result = await generator.generateRuntime("rust", "Create a Rust runtime SDK for Blok", "test-api-key");
510
+ expect(result.validationResult.valid).toBe(true);
511
+ expect(result.validationResult.attempts).toBe(1);
512
+ });
513
+ it("should parse Rust-specific files", async () => {
514
+ mockedGenerateText.mockResolvedValueOnce({ text: VALID_RUST_RUNTIME });
515
+ const result = await generator.generateRuntime("rust", "Create a Rust runtime", "test-api-key");
516
+ const filePaths = result.files.map((f) => f.path);
517
+ expect(filePaths.some((p) => p.endsWith(".rs"))).toBe(true);
518
+ expect(filePaths.some((p) => p === "Cargo.toml")).toBe(true);
519
+ expect(filePaths.some((p) => p === "Dockerfile")).toBe(true);
520
+ });
521
+ });
522
+ describe("successful generation - Python runtime", () => {
523
+ it("should generate a valid Python runtime on first attempt", async () => {
524
+ mockedGenerateText.mockResolvedValueOnce({ text: VALID_PYTHON_RUNTIME });
525
+ const result = await generator.generateRuntime("python", "Create a Python runtime SDK for Blok", "test-api-key");
526
+ expect(result.validationResult.valid).toBe(true);
527
+ expect(result.validationResult.attempts).toBe(1);
528
+ });
529
+ it("should parse Python-specific files", async () => {
530
+ mockedGenerateText.mockResolvedValueOnce({ text: VALID_PYTHON_RUNTIME });
531
+ const result = await generator.generateRuntime("python", "Create a Python runtime", "test-api-key");
532
+ const filePaths = result.files.map((f) => f.path);
533
+ expect(filePaths.some((p) => p.endsWith(".py"))).toBe(true);
534
+ expect(filePaths.some((p) => p === "requirements.txt")).toBe(true);
535
+ expect(filePaths.some((p) => p === "Dockerfile")).toBe(true);
536
+ });
537
+ });
538
+ describe("structural validation", () => {
539
+ it("should fail when output has no file markers", async () => {
540
+ mockedGenerateText.mockResolvedValue({ text: INVALID_RUNTIME_NO_FILES });
541
+ const result = await generator.generateRuntime("go", "Create a runtime", "test-api-key");
542
+ expect(result.validationResult.valid).toBe(false);
543
+ expect(result.validationResult.errors.some((e) => e.includes("multiple files"))).toBe(true);
544
+ });
545
+ it("should fail when /execute endpoint is missing", async () => {
546
+ mockedGenerateText.mockResolvedValue({ text: INVALID_RUNTIME_MISSING_ENDPOINTS });
547
+ const result = await generator.generateRuntime("go", "Create a runtime", "test-api-key");
548
+ expect(result.validationResult.valid).toBe(false);
549
+ expect(result.validationResult.errors.some((e) => e.includes("/execute"))).toBe(true);
550
+ });
551
+ it("should fail when NodeHandler/Registry is missing", async () => {
552
+ mockedGenerateText.mockResolvedValue({ text: INVALID_RUNTIME_NO_HANDLER });
553
+ const result = await generator.generateRuntime("go", "Create a runtime", "test-api-key");
554
+ expect(result.validationResult.valid).toBe(false);
555
+ expect(result.validationResult.errors.some((e) => e.toLowerCase().includes("nodehandler") || e.toLowerCase().includes("registry"))).toBe(true);
556
+ });
557
+ });
558
+ describe("validation feedback loop", () => {
559
+ it("should retry with feedback on first failure and succeed on second attempt", async () => {
560
+ mockedGenerateText.mockResolvedValueOnce({ text: INVALID_RUNTIME_NO_FILES });
561
+ mockedGenerateText.mockResolvedValueOnce({ text: VALID_GO_RUNTIME });
562
+ const result = await generator.generateRuntime("go", "Create a Go runtime", "test-api-key");
563
+ expect(result.validationResult.valid).toBe(true);
564
+ expect(result.validationResult.attempts).toBe(2);
565
+ expect(mockedGenerateText).toHaveBeenCalledTimes(2);
566
+ });
567
+ it("should include error feedback in retry prompt", async () => {
568
+ mockedGenerateText.mockResolvedValueOnce({ text: INVALID_RUNTIME_NO_FILES });
569
+ mockedGenerateText.mockResolvedValueOnce({ text: VALID_GO_RUNTIME });
570
+ await generator.generateRuntime("go", "Create a Go runtime", "test-api-key");
571
+ const secondCallArgs = mockedGenerateText.mock.calls[1][0];
572
+ const prompt = secondCallArgs.prompt;
573
+ expect(prompt).toContain("validation errors");
574
+ expect(prompt).toContain("Previous code:");
575
+ });
576
+ it("should exhaust all 3 attempts when runtime keeps failing", async () => {
577
+ mockedGenerateText.mockResolvedValue({ text: INVALID_RUNTIME_NO_FILES });
578
+ const result = await generator.generateRuntime("go", "Create a broken runtime", "test-api-key");
579
+ expect(result.validationResult.valid).toBe(false);
580
+ expect(result.validationResult.attempts).toBe(3);
581
+ expect(mockedGenerateText).toHaveBeenCalledTimes(3);
582
+ });
583
+ it("should succeed on third attempt", async () => {
584
+ mockedGenerateText.mockResolvedValueOnce({ text: INVALID_RUNTIME_NO_FILES });
585
+ mockedGenerateText.mockResolvedValueOnce({ text: INVALID_RUNTIME_MISSING_ENDPOINTS });
586
+ mockedGenerateText.mockResolvedValueOnce({ text: VALID_GO_RUNTIME });
587
+ const result = await generator.generateRuntime("go", "Create a Go runtime", "test-api-key");
588
+ expect(result.validationResult.valid).toBe(true);
589
+ expect(result.validationResult.attempts).toBe(3);
590
+ });
591
+ });
592
+ describe("language-specific prompt enhancement", () => {
593
+ it("should include Go-specific guidance in prompt", async () => {
594
+ mockedGenerateText.mockResolvedValueOnce({ text: VALID_GO_RUNTIME });
595
+ await generator.generateRuntime("go", "Create a runtime", "test-api-key");
596
+ const callArgs = mockedGenerateText.mock.calls[0][0];
597
+ const prompt = callArgs.prompt;
598
+ expect(prompt).toContain("go.mod");
599
+ expect(prompt).toContain("Go module");
600
+ });
601
+ it("should include Java-specific guidance in prompt", async () => {
602
+ mockedGenerateText.mockResolvedValueOnce({ text: VALID_JAVA_RUNTIME });
603
+ await generator.generateRuntime("java", "Create a runtime", "test-api-key");
604
+ const callArgs = mockedGenerateText.mock.calls[0][0];
605
+ const prompt = callArgs.prompt;
606
+ expect(prompt).toContain("Maven");
607
+ expect(prompt).toContain("pom.xml");
608
+ });
609
+ it("should include Rust-specific guidance in prompt", async () => {
610
+ mockedGenerateText.mockResolvedValueOnce({ text: VALID_RUST_RUNTIME });
611
+ await generator.generateRuntime("rust", "Create a runtime", "test-api-key");
612
+ const callArgs = mockedGenerateText.mock.calls[0][0];
613
+ const prompt = callArgs.prompt;
614
+ expect(prompt).toContain("Cargo");
615
+ expect(prompt).toContain("Cargo.toml");
616
+ });
617
+ it("should include Python-specific guidance in prompt", async () => {
618
+ mockedGenerateText.mockResolvedValueOnce({ text: VALID_PYTHON_RUNTIME });
619
+ await generator.generateRuntime("python", "Create a runtime", "test-api-key");
620
+ const callArgs = mockedGenerateText.mock.calls[0][0];
621
+ const prompt = callArgs.prompt;
622
+ expect(prompt).toContain("Flask");
623
+ expect(prompt).toContain("requirements.txt");
624
+ });
625
+ });
626
+ describe("markdown fence cleanup", () => {
627
+ it("should strip markdown fences from LLM response", async () => {
628
+ const wrappedCode = `\`\`\`go\n${VALID_GO_RUNTIME}\n\`\`\``;
629
+ mockedGenerateText.mockResolvedValueOnce({ text: wrappedCode });
630
+ const result = await generator.generateRuntime("go", "Create a Go runtime", "test-api-key");
631
+ expect(result.rawCode).not.toContain("```");
632
+ expect(result.validationResult.valid).toBe(true);
633
+ });
634
+ });
635
+ describe("temperature and model configuration", () => {
636
+ it("should use temperature 0.2 for deterministic output", async () => {
637
+ mockedGenerateText.mockResolvedValueOnce({ text: VALID_GO_RUNTIME });
638
+ await generator.generateRuntime("go", "Create a runtime", "test-api-key");
639
+ const callArgs = mockedGenerateText.mock.calls[0][0];
640
+ expect(callArgs.temperature).toBe(0.2);
641
+ });
642
+ it("should use GPT-4o model", async () => {
643
+ mockedGenerateText.mockResolvedValueOnce({ text: VALID_GO_RUNTIME });
644
+ await generator.generateRuntime("go", "Create a runtime", "test-api-key");
645
+ expect(mockedGenerateText).toHaveBeenCalledTimes(1);
646
+ });
647
+ });
648
+ describe("analytics integration", () => {
649
+ it("should include prompt version in validation result", async () => {
650
+ mockedGenerateText.mockResolvedValueOnce({ text: VALID_GO_RUNTIME });
651
+ const result = await generator.generateRuntime("go", "Create a runtime", "test-api-key");
652
+ expect(result.validationResult.promptVersion).toContain("create-runtime@");
653
+ });
654
+ it("should include duration in validation result", async () => {
655
+ mockedGenerateText.mockResolvedValueOnce({ text: VALID_GO_RUNTIME });
656
+ const result = await generator.generateRuntime("go", "Create a runtime", "test-api-key");
657
+ expect(result.validationResult.durationMs).toBeGreaterThanOrEqual(0);
658
+ });
659
+ });
660
+ describe("multi-file parsing", () => {
661
+ it("should parse files separated by // FILE: markers", async () => {
662
+ const code = [
663
+ "// FILE: main.go",
664
+ "package main",
665
+ "",
666
+ "func main() {}",
667
+ "",
668
+ "// FILE: lib.go",
669
+ "package lib",
670
+ "",
671
+ 'func Hello() string { return "hello" }',
672
+ ].join("\n");
673
+ const files = generator.parseFiles(code, "go");
674
+ expect(files).toHaveLength(2);
675
+ expect(files[0].path).toBe("main.go");
676
+ expect(files[0].content).toContain("package main");
677
+ expect(files[1].path).toBe("lib.go");
678
+ expect(files[1].content).toContain("package lib");
679
+ });
680
+ it("should handle code with no file markers as a single file", async () => {
681
+ const code = "package main\n\nfunc main() {}";
682
+ const files = generator.parseFiles(code, "go");
683
+ expect(files).toHaveLength(1);
684
+ expect(files[0].path).toBe("runtime.go");
685
+ });
686
+ it("should use correct file extension per language", async () => {
687
+ const code = "print('hello')";
688
+ expect(generator.parseFiles(code, "python")[0].path).toBe("runtime.py");
689
+ expect(generator.parseFiles(code, "rust")[0].path).toBe("runtime.rs");
690
+ expect(generator.parseFiles(code, "java")[0].path).toBe("runtime.java");
691
+ expect(generator.parseFiles(code, "csharp")[0].path).toBe("runtime.cs");
692
+ expect(generator.parseFiles(code, "php")[0].path).toBe("runtime.php");
693
+ expect(generator.parseFiles(code, "ruby")[0].path).toBe("runtime.rb");
694
+ });
695
+ });
696
+ describe("validateRuntimeStructure", () => {
697
+ it("should pass for valid Go runtime code", () => {
698
+ const result = generator.validateRuntimeStructure(VALID_GO_RUNTIME, "go");
699
+ expect(result.valid).toBe(true);
700
+ expect(result.errors).toHaveLength(0);
701
+ });
702
+ it("should pass for valid Java runtime code", () => {
703
+ const result = generator.validateRuntimeStructure(VALID_JAVA_RUNTIME, "java");
704
+ expect(result.valid).toBe(true);
705
+ expect(result.errors).toHaveLength(0);
706
+ });
707
+ it("should pass for valid Rust runtime code", () => {
708
+ const result = generator.validateRuntimeStructure(VALID_RUST_RUNTIME, "rust");
709
+ expect(result.valid).toBe(true);
710
+ expect(result.errors).toHaveLength(0);
711
+ });
712
+ it("should pass for valid Python runtime code", () => {
713
+ const result = generator.validateRuntimeStructure(VALID_PYTHON_RUNTIME, "python");
714
+ expect(result.valid).toBe(true);
715
+ expect(result.errors).toHaveLength(0);
716
+ });
717
+ it("should detect missing file markers", () => {
718
+ const result = generator.validateRuntimeStructure(INVALID_RUNTIME_NO_FILES, "go");
719
+ expect(result.valid).toBe(false);
720
+ expect(result.errors.some((e) => e.includes("multiple files"))).toBe(true);
721
+ });
722
+ it("should detect missing /execute endpoint", () => {
723
+ const result = generator.validateRuntimeStructure(INVALID_RUNTIME_MISSING_ENDPOINTS, "go");
724
+ expect(result.valid).toBe(false);
725
+ expect(result.errors.some((e) => e.includes("/execute"))).toBe(true);
726
+ });
727
+ it("should detect missing /health endpoint", () => {
728
+ const codeWithoutHealth = VALID_GO_RUNTIME.replace("/health", "/status");
729
+ const result = generator.validateRuntimeStructure(codeWithoutHealth, "go");
730
+ expect(result.valid).toBe(false);
731
+ expect(result.errors.some((e) => e.includes("/health"))).toBe(true);
732
+ });
733
+ it("should add Go-specific warning for missing go.mod", () => {
734
+ const code = `
735
+ // FILE: main.go
736
+ package main
737
+
738
+ import "net/http"
739
+
740
+ func main() {
741
+ http.HandleFunc("/execute", func(w http.ResponseWriter, r *http.Request) {})
742
+ http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {})
743
+ }
744
+
745
+ type Context struct { ID string }
746
+ type NodeHandler interface { Execute() }
747
+ type NodeRegistry struct {}
748
+ func (r *NodeRegistry) Register() {}
749
+
750
+ type ExecutionResult struct { success bool; data interface{}; errors interface{} }
751
+
752
+ // FILE: Dockerfile
753
+ FROM golang:1.21
754
+ `;
755
+ const result = generator.validateRuntimeStructure(code, "go");
756
+ expect(result.warnings.some((w) => w.includes("go.mod"))).toBe(true);
757
+ });
758
+ });
759
+ });