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.
- package/dist/commands/build/index.d.ts +2 -0
- package/dist/commands/build/index.js +210 -0
- package/dist/commands/config/index.d.ts +1 -0
- package/dist/commands/config/index.js +46 -0
- package/dist/commands/cost/index.d.ts +1 -0
- package/dist/commands/cost/index.js +74 -0
- package/dist/commands/create/node.d.ts +2 -0
- package/dist/commands/create/node.js +541 -0
- package/dist/commands/create/project.d.ts +2 -0
- package/dist/commands/create/project.js +941 -0
- package/dist/commands/create/utils/Examples.d.ts +39 -0
- package/dist/commands/create/utils/Examples.js +983 -0
- package/dist/commands/create/workflow.d.ts +2 -0
- package/dist/commands/create/workflow.js +109 -0
- package/dist/commands/deploy/index.d.ts +2 -0
- package/dist/commands/deploy/index.js +176 -0
- package/dist/commands/dev/index.d.ts +2 -0
- package/dist/commands/dev/index.js +190 -0
- package/dist/commands/generate/GenerationAnalytics.d.ts +61 -0
- package/dist/commands/generate/GenerationAnalytics.js +162 -0
- package/dist/commands/generate/GenerationAnalytics.test.d.ts +1 -0
- package/dist/commands/generate/GenerationAnalytics.test.js +407 -0
- package/dist/commands/generate/NodeFileWriter.d.ts +5 -0
- package/dist/commands/generate/NodeFileWriter.js +240 -0
- package/dist/commands/generate/NodeGenerator.d.ts +20 -0
- package/dist/commands/generate/NodeGenerator.js +181 -0
- package/dist/commands/generate/NodeGenerator.test.d.ts +1 -0
- package/dist/commands/generate/NodeGenerator.test.js +101 -0
- package/dist/commands/generate/PromptVersioning.d.ts +25 -0
- package/dist/commands/generate/PromptVersioning.js +71 -0
- package/dist/commands/generate/PromptVersioning.test.d.ts +1 -0
- package/dist/commands/generate/PromptVersioning.test.js +120 -0
- package/dist/commands/generate/RegisterNode.d.ts +3 -0
- package/dist/commands/generate/RegisterNode.js +37 -0
- package/dist/commands/generate/RuntimeGenerator.d.ts +40 -0
- package/dist/commands/generate/RuntimeGenerator.js +369 -0
- package/dist/commands/generate/RuntimeGenerator.test.d.ts +1 -0
- package/dist/commands/generate/RuntimeGenerator.test.js +553 -0
- package/dist/commands/generate/TriggerGenerator.d.ts +22 -0
- package/dist/commands/generate/TriggerGenerator.js +220 -0
- package/dist/commands/generate/TriggerGenerator.test.d.ts +1 -0
- package/dist/commands/generate/TriggerGenerator.test.js +209 -0
- package/dist/commands/generate/WorkflowGenerator.d.ts +20 -0
- package/dist/commands/generate/WorkflowGenerator.js +131 -0
- package/dist/commands/generate/WorkflowGenerator.test.d.ts +1 -0
- package/dist/commands/generate/WorkflowGenerator.test.js +77 -0
- package/dist/commands/generate/e2e/NodeGenerator.e2e.test.d.ts +1 -0
- package/dist/commands/generate/e2e/NodeGenerator.e2e.test.js +216 -0
- package/dist/commands/generate/e2e/RuntimeGenerator.e2e.test.d.ts +1 -0
- package/dist/commands/generate/e2e/RuntimeGenerator.e2e.test.js +759 -0
- package/dist/commands/generate/e2e/TriggerGenerator.e2e.test.d.ts +1 -0
- package/dist/commands/generate/e2e/TriggerGenerator.e2e.test.js +295 -0
- package/dist/commands/generate/e2e/WorkflowGenerator.e2e.test.d.ts +1 -0
- package/dist/commands/generate/e2e/WorkflowGenerator.e2e.test.js +353 -0
- package/dist/commands/generate/index.d.ts +1 -0
- package/dist/commands/generate/index.js +418 -0
- package/dist/commands/generate/prompts/create-fn-node.system.d.ts +5 -0
- package/dist/commands/generate/prompts/create-fn-node.system.js +256 -0
- package/dist/commands/generate/prompts/create-node-manifest.system.d.ts +4 -0
- package/dist/commands/generate/prompts/create-node-manifest.system.js +41 -0
- package/dist/commands/generate/prompts/create-node.system.d.ts +5 -0
- package/dist/commands/generate/prompts/create-node.system.js +114 -0
- package/dist/commands/generate/prompts/create-readme.system.d.ts +4 -0
- package/dist/commands/generate/prompts/create-readme.system.js +83 -0
- package/dist/commands/generate/prompts/create-runtime.system.d.ts +5 -0
- package/dist/commands/generate/prompts/create-runtime.system.js +284 -0
- package/dist/commands/generate/prompts/create-trigger.system.d.ts +5 -0
- package/dist/commands/generate/prompts/create-trigger.system.js +293 -0
- package/dist/commands/generate/prompts/create-workflow.system.d.ts +5 -0
- package/dist/commands/generate/prompts/create-workflow.system.js +476 -0
- package/dist/commands/generate/prompts/register-node.system.d.ts +4 -0
- package/dist/commands/generate/prompts/register-node.system.js +26 -0
- package/dist/commands/generate/validators/CompilationValidator.d.ts +9 -0
- package/dist/commands/generate/validators/CompilationValidator.js +86 -0
- package/dist/commands/generate/validators/CompilationValidator.test.d.ts +1 -0
- package/dist/commands/generate/validators/CompilationValidator.test.js +161 -0
- package/dist/commands/generate/validators/NodeValidator.d.ts +18 -0
- package/dist/commands/generate/validators/NodeValidator.js +217 -0
- package/dist/commands/generate/validators/NodeValidator.test.d.ts +1 -0
- package/dist/commands/generate/validators/NodeValidator.test.js +281 -0
- package/dist/commands/generate/validators/WorkflowValidator.d.ts +6 -0
- package/dist/commands/generate/validators/WorkflowValidator.js +301 -0
- package/dist/commands/generate/validators/WorkflowValidator.test.d.ts +1 -0
- package/dist/commands/generate/validators/WorkflowValidator.test.js +647 -0
- package/dist/commands/generate/validators/index.d.ts +4 -0
- package/dist/commands/generate/validators/index.js +2 -0
- package/dist/commands/graph/index.d.ts +1 -0
- package/dist/commands/graph/index.js +69 -0
- package/dist/commands/install/index.d.ts +1 -0
- package/dist/commands/install/index.js +4 -0
- package/dist/commands/install/node.d.ts +4 -0
- package/dist/commands/install/node.js +136 -0
- package/dist/commands/install/workflow.d.ts +4 -0
- package/dist/commands/install/workflow.js +62 -0
- package/dist/commands/login/index.d.ts +2 -0
- package/dist/commands/login/index.js +77 -0
- package/dist/commands/logout/index.d.ts +2 -0
- package/dist/commands/logout/index.js +20 -0
- package/dist/commands/marketplace/runtime.d.ts +54 -0
- package/dist/commands/marketplace/runtime.js +350 -0
- package/dist/commands/migrate/index.d.ts +1 -0
- package/dist/commands/migrate/index.js +14 -0
- package/dist/commands/migrate/node.d.ts +2 -0
- package/dist/commands/migrate/node.js +110 -0
- package/dist/commands/monitor/index.d.ts +1 -0
- package/dist/commands/monitor/index.js +28 -0
- package/dist/commands/monitor/monitor-component.d.ts +1 -0
- package/dist/commands/monitor/monitor-component.js +271 -0
- package/dist/commands/monitor/static/index.html +2124 -0
- package/dist/commands/monitor/static-web-server.d.ts +1 -0
- package/dist/commands/monitor/static-web-server.js +89 -0
- package/dist/commands/profile/index.d.ts +1 -0
- package/dist/commands/profile/index.js +112 -0
- package/dist/commands/publish/index.d.ts +1 -0
- package/dist/commands/publish/index.js +4 -0
- package/dist/commands/publish/node.d.ts +4 -0
- package/dist/commands/publish/node.js +231 -0
- package/dist/commands/publish/workflow.d.ts +4 -0
- package/dist/commands/publish/workflow.js +165 -0
- package/dist/commands/search/docs.d.ts +17 -0
- package/dist/commands/search/docs.js +179 -0
- package/dist/commands/search/index.d.ts +1 -0
- package/dist/commands/search/index.js +5 -0
- package/dist/commands/search/indexer.d.ts +10 -0
- package/dist/commands/search/indexer.js +265 -0
- package/dist/commands/search/nodes.d.ts +4 -0
- package/dist/commands/search/nodes.js +101 -0
- package/dist/commands/search/workflow.d.ts +4 -0
- package/dist/commands/search/workflow.js +100 -0
- package/dist/commands/trace/index.d.ts +1 -0
- package/dist/commands/trace/index.js +26 -0
- package/dist/commands/trace/startStudio.d.ts +8 -0
- package/dist/commands/trace/startStudio.js +116 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +186 -0
- package/dist/services/commander.d.ts +9 -0
- package/dist/services/commander.js +20 -0
- package/dist/services/constants.d.ts +1 -0
- package/dist/services/constants.js +3 -0
- package/dist/services/local-token-manager.d.ts +14 -0
- package/dist/services/local-token-manager.js +99 -0
- package/dist/services/non-interactive.d.ts +5 -0
- package/dist/services/non-interactive.js +30 -0
- package/dist/services/package-manager.d.ts +35 -0
- package/dist/services/package-manager.js +111 -0
- package/dist/services/posthog.d.ts +31 -0
- package/dist/services/posthog.js +159 -0
- package/dist/services/registry-manager.d.ts +9 -0
- package/dist/services/registry-manager.js +26 -0
- package/dist/services/runtime-detector.d.ts +23 -0
- package/dist/services/runtime-detector.js +181 -0
- package/dist/services/runtime-setup.d.ts +36 -0
- package/dist/services/runtime-setup.js +250 -0
- package/dist/services/utils.d.ts +2 -0
- package/dist/services/utils.js +29 -0
- package/dist/services/workflow-loader.d.ts +30 -0
- package/dist/services/workflow-loader.js +46 -0
- package/dist/studio-dist/assets/charts-Dso0hPUR.js +68 -0
- package/dist/studio-dist/assets/graph-CsV2nWGn.js +23 -0
- package/dist/studio-dist/assets/icons-zP8LLgPh.js +311 -0
- package/dist/studio-dist/assets/index-CLyEkXMx.css +1 -0
- package/dist/studio-dist/assets/index-CNXFX_ar.js +27 -0
- package/dist/studio-dist/assets/react-vendor--Eh9ivFN.js +17 -0
- package/dist/studio-dist/assets/tanstack-query-CiM1U6F5.js +1 -0
- package/dist/studio-dist/assets/tanstack-router-Btjy0MKq.js +25 -0
- package/dist/studio-dist/assets/tanstack-table-DhwRvuH2.js +22 -0
- package/dist/studio-dist/favicon.svg +5 -0
- package/dist/studio-dist/index.html +21 -0
- 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
|
+
});
|