@better-openclaw/core 1.0.30 → 1.0.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/deployers/coolify.test.cjs +156 -0
- package/dist/deployers/coolify.test.cjs.map +1 -0
- package/dist/deployers/coolify.test.d.cts +1 -0
- package/dist/deployers/coolify.test.d.mts +1 -0
- package/dist/deployers/coolify.test.mjs +157 -0
- package/dist/deployers/coolify.test.mjs.map +1 -0
- package/dist/deployers/dokploy.test.cjs +108 -0
- package/dist/deployers/dokploy.test.cjs.map +1 -0
- package/dist/deployers/dokploy.test.d.cts +1 -0
- package/dist/deployers/dokploy.test.d.mts +1 -0
- package/dist/deployers/dokploy.test.mjs +109 -0
- package/dist/deployers/dokploy.test.mjs.map +1 -0
- package/dist/frameworks/frameworks.test.cjs +94 -0
- package/dist/frameworks/frameworks.test.cjs.map +1 -0
- package/dist/frameworks/frameworks.test.d.cts +1 -0
- package/dist/frameworks/frameworks.test.d.mts +1 -0
- package/dist/frameworks/frameworks.test.mjs +94 -0
- package/dist/frameworks/frameworks.test.mjs.map +1 -0
- package/dist/generators/cloud-init.test.cjs +58 -0
- package/dist/generators/cloud-init.test.cjs.map +1 -0
- package/dist/generators/cloud-init.test.d.cts +1 -0
- package/dist/generators/cloud-init.test.d.mts +1 -0
- package/dist/generators/cloud-init.test.mjs +59 -0
- package/dist/generators/cloud-init.test.mjs.map +1 -0
- package/dist/generators/get-shit-done.test.cjs +48 -0
- package/dist/generators/get-shit-done.test.cjs.map +1 -0
- package/dist/generators/get-shit-done.test.d.cts +1 -0
- package/dist/generators/get-shit-done.test.d.mts +1 -0
- package/dist/generators/get-shit-done.test.mjs +49 -0
- package/dist/generators/get-shit-done.test.mjs.map +1 -0
- package/dist/generators/grafana.test.cjs +74 -0
- package/dist/generators/grafana.test.cjs.map +1 -0
- package/dist/generators/grafana.test.d.cts +1 -0
- package/dist/generators/grafana.test.d.mts +1 -0
- package/dist/generators/grafana.test.mjs +74 -0
- package/dist/generators/grafana.test.mjs.map +1 -0
- package/dist/generators/n8n-workflows.test.cjs +75 -0
- package/dist/generators/n8n-workflows.test.cjs.map +1 -0
- package/dist/generators/n8n-workflows.test.d.cts +1 -0
- package/dist/generators/n8n-workflows.test.d.mts +1 -0
- package/dist/generators/n8n-workflows.test.mjs +76 -0
- package/dist/generators/n8n-workflows.test.mjs.map +1 -0
- package/dist/generators/openclaw-install-script.test.cjs +35 -0
- package/dist/generators/openclaw-install-script.test.cjs.map +1 -0
- package/dist/generators/openclaw-install-script.test.d.cts +1 -0
- package/dist/generators/openclaw-install-script.test.d.mts +1 -0
- package/dist/generators/openclaw-install-script.test.mjs +36 -0
- package/dist/generators/openclaw-install-script.test.mjs.map +1 -0
- package/dist/generators/postgres-init.test.cjs +111 -0
- package/dist/generators/postgres-init.test.cjs.map +1 -0
- package/dist/generators/postgres-init.test.d.cts +1 -0
- package/dist/generators/postgres-init.test.d.mts +1 -0
- package/dist/generators/postgres-init.test.mjs +112 -0
- package/dist/generators/postgres-init.test.mjs.map +1 -0
- package/dist/generators/prometheus.test.cjs +99 -0
- package/dist/generators/prometheus.test.cjs.map +1 -0
- package/dist/generators/prometheus.test.d.cts +1 -0
- package/dist/generators/prometheus.test.d.mts +1 -0
- package/dist/generators/prometheus.test.mjs +99 -0
- package/dist/generators/prometheus.test.mjs.map +1 -0
- package/dist/generators/stack-manifest.test.cjs +97 -0
- package/dist/generators/stack-manifest.test.cjs.map +1 -0
- package/dist/generators/stack-manifest.test.d.cts +1 -0
- package/dist/generators/stack-manifest.test.d.mts +1 -0
- package/dist/generators/stack-manifest.test.mjs +98 -0
- package/dist/generators/stack-manifest.test.mjs.map +1 -0
- package/dist/index.cjs +0 -2
- package/dist/index.d.cts +1 -2
- package/dist/index.d.mts +1 -2
- package/dist/index.mjs +1 -2
- package/dist/logger/index.cjs +0 -2
- package/dist/logger/index.d.cts +1 -2
- package/dist/logger/index.d.mts +1 -2
- package/dist/logger/index.mjs +1 -2
- package/dist/port-scanner.test.cjs +155 -0
- package/dist/port-scanner.test.cjs.map +1 -0
- package/dist/port-scanner.test.d.cts +1 -0
- package/dist/port-scanner.test.d.mts +1 -0
- package/dist/port-scanner.test.mjs +156 -0
- package/dist/port-scanner.test.mjs.map +1 -0
- package/package.json +1 -1
- package/src/deployers/coolify.test.ts +180 -0
- package/src/deployers/dokploy.test.ts +120 -0
- package/src/frameworks/frameworks.test.ts +119 -0
- package/src/generators/cloud-init.test.ts +70 -0
- package/src/generators/get-shit-done.test.ts +54 -0
- package/src/generators/grafana.test.ts +90 -0
- package/src/generators/n8n-workflows.test.ts +80 -0
- package/src/generators/openclaw-install-script.test.ts +42 -0
- package/src/generators/postgres-init.test.ts +116 -0
- package/src/generators/prometheus.test.ts +108 -0
- package/src/generators/stack-manifest.test.ts +104 -0
- package/src/index.ts +3 -2
- package/src/logger/index.ts +2 -1
- package/src/port-scanner.test.ts +167 -0
package/dist/logger/index.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { OperationsLogger, StepTracker } from "./logger.mjs";
|
|
2
2
|
import { CallbackSink } from "./sinks/callback-sink.mjs";
|
|
3
3
|
import { ConsoleSink } from "./sinks/console-sink.mjs";
|
|
4
|
-
|
|
5
|
-
export { CallbackSink, ConsoleSink, FileSink, OperationsLogger, StepTracker };
|
|
4
|
+
export { CallbackSink, ConsoleSink, OperationsLogger, StepTracker };
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
const require_test_CTcmp4Su = require("./test.CTcmp4Su-BWSPM8ZQ.cjs");
|
|
2
|
+
const require_port_scanner = require("./port-scanner.cjs");
|
|
3
|
+
//#region src/port-scanner.test.ts
|
|
4
|
+
/**
|
|
5
|
+
* Tests for port scanner conflict detection and reassignment.
|
|
6
|
+
*
|
|
7
|
+
* We mock isPortAvailable via vi.mock to avoid actual port scanning
|
|
8
|
+
* during tests. The focus is on the conflict detection logic.
|
|
9
|
+
*/
|
|
10
|
+
require_test_CTcmp4Su.vi.mock("node:net", () => {
|
|
11
|
+
const portsInUse = new Set([
|
|
12
|
+
80,
|
|
13
|
+
443,
|
|
14
|
+
3e3
|
|
15
|
+
]);
|
|
16
|
+
return { default: {
|
|
17
|
+
Socket: class MockSocket {
|
|
18
|
+
connect(port) {
|
|
19
|
+
setTimeout(() => {
|
|
20
|
+
if (portsInUse.has(port)) this.connectCb?.();
|
|
21
|
+
else this.errorCb?.();
|
|
22
|
+
}, 1);
|
|
23
|
+
}
|
|
24
|
+
once(event, cb) {
|
|
25
|
+
if (event === "connect") this.connectCb = cb;
|
|
26
|
+
if (event === "error") this.errorCb = cb;
|
|
27
|
+
if (event === "timeout") this.timeoutCb = cb;
|
|
28
|
+
}
|
|
29
|
+
setTimeout() {}
|
|
30
|
+
removeAllListeners() {}
|
|
31
|
+
destroy() {}
|
|
32
|
+
},
|
|
33
|
+
createServer() {
|
|
34
|
+
return {
|
|
35
|
+
once(event, cb) {
|
|
36
|
+
if (event === "error") this.errorCb = cb;
|
|
37
|
+
},
|
|
38
|
+
listen(_port, _host, cb) {
|
|
39
|
+
cb();
|
|
40
|
+
},
|
|
41
|
+
close(cb) {
|
|
42
|
+
cb();
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
} };
|
|
47
|
+
});
|
|
48
|
+
function makeService(id, ports) {
|
|
49
|
+
return {
|
|
50
|
+
id,
|
|
51
|
+
name: id,
|
|
52
|
+
description: `${id} service`,
|
|
53
|
+
icon: "",
|
|
54
|
+
category: "test",
|
|
55
|
+
image: `${id}:latest`,
|
|
56
|
+
ports,
|
|
57
|
+
volumes: [],
|
|
58
|
+
environment: [],
|
|
59
|
+
dependencies: [],
|
|
60
|
+
conflicts: [],
|
|
61
|
+
skills: [],
|
|
62
|
+
memoryMB: 256,
|
|
63
|
+
docsUrl: ""
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
require_test_CTcmp4Su.describe("formatPortConflicts", () => {
|
|
67
|
+
require_test_CTcmp4Su.it("returns empty array when no reassignments", () => {
|
|
68
|
+
require_test_CTcmp4Su.globalExpect(require_port_scanner.formatPortConflicts([makeService("redis", [{
|
|
69
|
+
host: 6379,
|
|
70
|
+
container: 6379,
|
|
71
|
+
exposed: true,
|
|
72
|
+
description: "Redis"
|
|
73
|
+
}])], /* @__PURE__ */ new Map())).toEqual([]);
|
|
74
|
+
});
|
|
75
|
+
require_test_CTcmp4Su.it("returns conflicts with suggested ports", () => {
|
|
76
|
+
const conflicts = require_port_scanner.formatPortConflicts([makeService("redis", [{
|
|
77
|
+
host: 6379,
|
|
78
|
+
container: 6379,
|
|
79
|
+
exposed: true,
|
|
80
|
+
description: "Redis"
|
|
81
|
+
}])], new Map([["redis", new Map([[6379, 7379]])]]));
|
|
82
|
+
require_test_CTcmp4Su.globalExpect(conflicts).toHaveLength(1);
|
|
83
|
+
require_test_CTcmp4Su.globalExpect(conflicts[0]).toEqual({
|
|
84
|
+
port: 6379,
|
|
85
|
+
serviceId: "redis",
|
|
86
|
+
description: "Redis",
|
|
87
|
+
suggestedPort: 7379
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
require_test_CTcmp4Su.it("handles multiple services with multiple port reassignments", () => {
|
|
91
|
+
const conflicts = require_port_scanner.formatPortConflicts([makeService("web", [{
|
|
92
|
+
host: 80,
|
|
93
|
+
container: 80,
|
|
94
|
+
exposed: true,
|
|
95
|
+
description: "HTTP"
|
|
96
|
+
}, {
|
|
97
|
+
host: 443,
|
|
98
|
+
container: 443,
|
|
99
|
+
exposed: true,
|
|
100
|
+
description: "HTTPS"
|
|
101
|
+
}]), makeService("api", [{
|
|
102
|
+
host: 8080,
|
|
103
|
+
container: 8080,
|
|
104
|
+
exposed: true,
|
|
105
|
+
description: "API"
|
|
106
|
+
}])], new Map([["web", new Map([[80, 1080], [443, 1443]])]]));
|
|
107
|
+
require_test_CTcmp4Su.globalExpect(conflicts).toHaveLength(2);
|
|
108
|
+
require_test_CTcmp4Su.globalExpect(conflicts[0].port).toBe(80);
|
|
109
|
+
require_test_CTcmp4Su.globalExpect(conflicts[0].suggestedPort).toBe(1080);
|
|
110
|
+
require_test_CTcmp4Su.globalExpect(conflicts[1].port).toBe(443);
|
|
111
|
+
require_test_CTcmp4Su.globalExpect(conflicts[1].suggestedPort).toBe(1443);
|
|
112
|
+
});
|
|
113
|
+
require_test_CTcmp4Su.it("uses service name as fallback when port has no description", () => {
|
|
114
|
+
require_test_CTcmp4Su.globalExpect(require_port_scanner.formatPortConflicts([makeService("myapp", [{
|
|
115
|
+
host: 9e3,
|
|
116
|
+
container: 9e3,
|
|
117
|
+
exposed: true,
|
|
118
|
+
description: ""
|
|
119
|
+
}])], new Map([["myapp", new Map([[9e3, 1e4]])]]))[0].description).toBe("myapp port");
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
require_test_CTcmp4Su.describe("scanPortConflicts", () => {
|
|
123
|
+
require_test_CTcmp4Su.it("returns empty map for services with no ports", async () => {
|
|
124
|
+
require_test_CTcmp4Su.globalExpect((await require_port_scanner.scanPortConflicts([makeService("redis", [])])).size).toBe(0);
|
|
125
|
+
});
|
|
126
|
+
require_test_CTcmp4Su.it("returns empty map for services with no exposed ports", async () => {
|
|
127
|
+
require_test_CTcmp4Su.globalExpect((await require_port_scanner.scanPortConflicts([makeService("redis", [{
|
|
128
|
+
host: 6379,
|
|
129
|
+
container: 6379,
|
|
130
|
+
exposed: false,
|
|
131
|
+
description: "Redis"
|
|
132
|
+
}])])).size).toBe(0);
|
|
133
|
+
});
|
|
134
|
+
require_test_CTcmp4Su.it("detects inter-service port conflicts", async () => {
|
|
135
|
+
const result = await require_port_scanner.scanPortConflicts([makeService("web1", [{
|
|
136
|
+
host: 8080,
|
|
137
|
+
container: 80,
|
|
138
|
+
exposed: true,
|
|
139
|
+
description: "HTTP"
|
|
140
|
+
}]), makeService("web2", [{
|
|
141
|
+
host: 8080,
|
|
142
|
+
container: 80,
|
|
143
|
+
exposed: true,
|
|
144
|
+
description: "HTTP"
|
|
145
|
+
}])]);
|
|
146
|
+
require_test_CTcmp4Su.globalExpect(result.has("web1")).toBe(false);
|
|
147
|
+
require_test_CTcmp4Su.globalExpect(result.has("web2")).toBe(true);
|
|
148
|
+
const web2Reassignments = result.get("web2");
|
|
149
|
+
require_test_CTcmp4Su.globalExpect(web2Reassignments.has(8080)).toBe(true);
|
|
150
|
+
require_test_CTcmp4Su.globalExpect(web2Reassignments.get(8080)).toBeGreaterThan(8080);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
//#endregion
|
|
154
|
+
|
|
155
|
+
//# sourceMappingURL=port-scanner.test.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"port-scanner.test.cjs","names":["vi","describe","formatPortConflicts","scanPortConflicts"],"sources":["../src/port-scanner.test.ts"],"sourcesContent":["import { describe, expect, it, vi } from \"vitest\";\nimport { formatPortConflicts, scanPortConflicts } from \"./port-scanner.js\";\nimport type { ServiceDefinition } from \"./types.js\";\n\n/**\n * Tests for port scanner conflict detection and reassignment.\n *\n * We mock isPortAvailable via vi.mock to avoid actual port scanning\n * during tests. The focus is on the conflict detection logic.\n */\n\n// Mock the internal isPortAvailable to control port availability\nvi.mock(\"node:net\", () => {\n\tconst portsInUse = new Set([80, 443, 3000]);\n\n\treturn {\n\t\tdefault: {\n\t\t\tSocket: class MockSocket {\n\t\t\t\tconnect(port: number) {\n\t\t\t\t\t// Simulate: ports in portsInUse are \"in use\"\n\t\t\t\t\tsetTimeout(() => {\n\t\t\t\t\t\tif (portsInUse.has(port)) {\n\t\t\t\t\t\t\t(this as any).connectCb?.();\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t(this as any).errorCb?.();\n\t\t\t\t\t\t}\n\t\t\t\t\t}, 1);\n\t\t\t\t}\n\t\t\t\tonce(event: string, cb: () => void) {\n\t\t\t\t\tif (event === \"connect\") (this as any).connectCb = cb;\n\t\t\t\t\tif (event === \"error\") (this as any).errorCb = cb;\n\t\t\t\t\tif (event === \"timeout\") (this as any).timeoutCb = cb;\n\t\t\t\t}\n\t\t\t\tsetTimeout() {}\n\t\t\t\tremoveAllListeners() {}\n\t\t\t\tdestroy() {}\n\t\t\t},\n\t\t\tcreateServer() {\n\t\t\t\treturn {\n\t\t\t\t\tonce(event: string, cb: (err?: Error) => void) {\n\t\t\t\t\t\tif (event === \"error\") (this as any).errorCb = cb;\n\t\t\t\t\t},\n\t\t\t\t\tlisten(_port: number, _host: string, cb: () => void) {\n\t\t\t\t\t\tcb();\n\t\t\t\t\t},\n\t\t\t\t\tclose(cb: () => void) {\n\t\t\t\t\t\tcb();\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t},\n\t\t},\n\t};\n});\n\nfunction makeService(\n\tid: string,\n\tports: { host: number; container: number; exposed: boolean; description: string }[],\n): ServiceDefinition {\n\treturn {\n\t\tid,\n\t\tname: id,\n\t\tdescription: `${id} service`,\n\t\ticon: \"\",\n\t\tcategory: \"test\",\n\t\timage: `${id}:latest`,\n\t\tports,\n\t\tvolumes: [],\n\t\tenvironment: [],\n\t\tdependencies: [],\n\t\tconflicts: [],\n\t\tskills: [],\n\t\tmemoryMB: 256,\n\t\tdocsUrl: \"\",\n\t} as ServiceDefinition;\n}\n\ndescribe(\"formatPortConflicts\", () => {\n\tit(\"returns empty array when no reassignments\", () => {\n\t\tconst services = [\n\t\t\tmakeService(\"redis\", [{ host: 6379, container: 6379, exposed: true, description: \"Redis\" }]),\n\t\t];\n\t\tconst reassignments = new Map();\n\t\tconst conflicts = formatPortConflicts(services, reassignments);\n\t\texpect(conflicts).toEqual([]);\n\t});\n\n\tit(\"returns conflicts with suggested ports\", () => {\n\t\tconst services = [\n\t\t\tmakeService(\"redis\", [{ host: 6379, container: 6379, exposed: true, description: \"Redis\" }]),\n\t\t];\n\t\tconst reassignments = new Map([[\"redis\", new Map([[6379, 7379]])]]);\n\t\tconst conflicts = formatPortConflicts(services, reassignments);\n\t\texpect(conflicts).toHaveLength(1);\n\t\texpect(conflicts[0]).toEqual({\n\t\t\tport: 6379,\n\t\t\tserviceId: \"redis\",\n\t\t\tdescription: \"Redis\",\n\t\t\tsuggestedPort: 7379,\n\t\t});\n\t});\n\n\tit(\"handles multiple services with multiple port reassignments\", () => {\n\t\tconst services = [\n\t\t\tmakeService(\"web\", [\n\t\t\t\t{ host: 80, container: 80, exposed: true, description: \"HTTP\" },\n\t\t\t\t{ host: 443, container: 443, exposed: true, description: \"HTTPS\" },\n\t\t\t]),\n\t\t\tmakeService(\"api\", [{ host: 8080, container: 8080, exposed: true, description: \"API\" }]),\n\t\t];\n\t\tconst reassignments = new Map([\n\t\t\t[\n\t\t\t\t\"web\",\n\t\t\t\tnew Map([\n\t\t\t\t\t[80, 1080],\n\t\t\t\t\t[443, 1443],\n\t\t\t\t]),\n\t\t\t],\n\t\t]);\n\t\tconst conflicts = formatPortConflicts(services, reassignments);\n\t\texpect(conflicts).toHaveLength(2);\n\t\texpect(conflicts[0]!.port).toBe(80);\n\t\texpect(conflicts[0]!.suggestedPort).toBe(1080);\n\t\texpect(conflicts[1]!.port).toBe(443);\n\t\texpect(conflicts[1]!.suggestedPort).toBe(1443);\n\t});\n\n\tit(\"uses service name as fallback when port has no description\", () => {\n\t\tconst services = [\n\t\t\tmakeService(\"myapp\", [{ host: 9000, container: 9000, exposed: true, description: \"\" }]),\n\t\t];\n\t\tconst reassignments = new Map([[\"myapp\", new Map([[9000, 10000]])]]);\n\t\tconst conflicts = formatPortConflicts(services, reassignments);\n\t\texpect(conflicts[0]!.description).toBe(\"myapp port\");\n\t});\n});\n\ndescribe(\"scanPortConflicts\", () => {\n\tit(\"returns empty map for services with no ports\", async () => {\n\t\tconst services = [makeService(\"redis\", [])];\n\t\tconst result = await scanPortConflicts(services);\n\t\texpect(result.size).toBe(0);\n\t});\n\n\tit(\"returns empty map for services with no exposed ports\", async () => {\n\t\tconst services = [\n\t\t\tmakeService(\"redis\", [{ host: 6379, container: 6379, exposed: false, description: \"Redis\" }]),\n\t\t];\n\t\tconst result = await scanPortConflicts(services);\n\t\texpect(result.size).toBe(0);\n\t});\n\n\tit(\"detects inter-service port conflicts\", async () => {\n\t\t// Two services claiming the same host port\n\t\tconst services = [\n\t\t\tmakeService(\"web1\", [{ host: 8080, container: 80, exposed: true, description: \"HTTP\" }]),\n\t\t\tmakeService(\"web2\", [{ host: 8080, container: 80, exposed: true, description: \"HTTP\" }]),\n\t\t];\n\t\tconst result = await scanPortConflicts(services);\n\n\t\t// First service wins, second gets reassigned\n\t\texpect(result.has(\"web1\")).toBe(false); // web1 keeps its port\n\t\texpect(result.has(\"web2\")).toBe(true); // web2 gets reassigned\n\t\tconst web2Reassignments = result.get(\"web2\")!;\n\t\texpect(web2Reassignments.has(8080)).toBe(true);\n\t\texpect(web2Reassignments.get(8080)).toBeGreaterThan(8080);\n\t});\n});\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { a as describe, n as vi, o as it, t as globalExpect } from "./test.CTcmp4Su-BRa7-bTj.mjs";
|
|
2
|
+
import { formatPortConflicts, scanPortConflicts } from "./port-scanner.mjs";
|
|
3
|
+
//#region src/port-scanner.test.ts
|
|
4
|
+
/**
|
|
5
|
+
* Tests for port scanner conflict detection and reassignment.
|
|
6
|
+
*
|
|
7
|
+
* We mock isPortAvailable via vi.mock to avoid actual port scanning
|
|
8
|
+
* during tests. The focus is on the conflict detection logic.
|
|
9
|
+
*/
|
|
10
|
+
vi.mock("node:net", () => {
|
|
11
|
+
const portsInUse = new Set([
|
|
12
|
+
80,
|
|
13
|
+
443,
|
|
14
|
+
3e3
|
|
15
|
+
]);
|
|
16
|
+
return { default: {
|
|
17
|
+
Socket: class MockSocket {
|
|
18
|
+
connect(port) {
|
|
19
|
+
setTimeout(() => {
|
|
20
|
+
if (portsInUse.has(port)) this.connectCb?.();
|
|
21
|
+
else this.errorCb?.();
|
|
22
|
+
}, 1);
|
|
23
|
+
}
|
|
24
|
+
once(event, cb) {
|
|
25
|
+
if (event === "connect") this.connectCb = cb;
|
|
26
|
+
if (event === "error") this.errorCb = cb;
|
|
27
|
+
if (event === "timeout") this.timeoutCb = cb;
|
|
28
|
+
}
|
|
29
|
+
setTimeout() {}
|
|
30
|
+
removeAllListeners() {}
|
|
31
|
+
destroy() {}
|
|
32
|
+
},
|
|
33
|
+
createServer() {
|
|
34
|
+
return {
|
|
35
|
+
once(event, cb) {
|
|
36
|
+
if (event === "error") this.errorCb = cb;
|
|
37
|
+
},
|
|
38
|
+
listen(_port, _host, cb) {
|
|
39
|
+
cb();
|
|
40
|
+
},
|
|
41
|
+
close(cb) {
|
|
42
|
+
cb();
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
} };
|
|
47
|
+
});
|
|
48
|
+
function makeService(id, ports) {
|
|
49
|
+
return {
|
|
50
|
+
id,
|
|
51
|
+
name: id,
|
|
52
|
+
description: `${id} service`,
|
|
53
|
+
icon: "",
|
|
54
|
+
category: "test",
|
|
55
|
+
image: `${id}:latest`,
|
|
56
|
+
ports,
|
|
57
|
+
volumes: [],
|
|
58
|
+
environment: [],
|
|
59
|
+
dependencies: [],
|
|
60
|
+
conflicts: [],
|
|
61
|
+
skills: [],
|
|
62
|
+
memoryMB: 256,
|
|
63
|
+
docsUrl: ""
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
describe("formatPortConflicts", () => {
|
|
67
|
+
it("returns empty array when no reassignments", () => {
|
|
68
|
+
globalExpect(formatPortConflicts([makeService("redis", [{
|
|
69
|
+
host: 6379,
|
|
70
|
+
container: 6379,
|
|
71
|
+
exposed: true,
|
|
72
|
+
description: "Redis"
|
|
73
|
+
}])], /* @__PURE__ */ new Map())).toEqual([]);
|
|
74
|
+
});
|
|
75
|
+
it("returns conflicts with suggested ports", () => {
|
|
76
|
+
const conflicts = formatPortConflicts([makeService("redis", [{
|
|
77
|
+
host: 6379,
|
|
78
|
+
container: 6379,
|
|
79
|
+
exposed: true,
|
|
80
|
+
description: "Redis"
|
|
81
|
+
}])], new Map([["redis", new Map([[6379, 7379]])]]));
|
|
82
|
+
globalExpect(conflicts).toHaveLength(1);
|
|
83
|
+
globalExpect(conflicts[0]).toEqual({
|
|
84
|
+
port: 6379,
|
|
85
|
+
serviceId: "redis",
|
|
86
|
+
description: "Redis",
|
|
87
|
+
suggestedPort: 7379
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
it("handles multiple services with multiple port reassignments", () => {
|
|
91
|
+
const conflicts = formatPortConflicts([makeService("web", [{
|
|
92
|
+
host: 80,
|
|
93
|
+
container: 80,
|
|
94
|
+
exposed: true,
|
|
95
|
+
description: "HTTP"
|
|
96
|
+
}, {
|
|
97
|
+
host: 443,
|
|
98
|
+
container: 443,
|
|
99
|
+
exposed: true,
|
|
100
|
+
description: "HTTPS"
|
|
101
|
+
}]), makeService("api", [{
|
|
102
|
+
host: 8080,
|
|
103
|
+
container: 8080,
|
|
104
|
+
exposed: true,
|
|
105
|
+
description: "API"
|
|
106
|
+
}])], new Map([["web", new Map([[80, 1080], [443, 1443]])]]));
|
|
107
|
+
globalExpect(conflicts).toHaveLength(2);
|
|
108
|
+
globalExpect(conflicts[0].port).toBe(80);
|
|
109
|
+
globalExpect(conflicts[0].suggestedPort).toBe(1080);
|
|
110
|
+
globalExpect(conflicts[1].port).toBe(443);
|
|
111
|
+
globalExpect(conflicts[1].suggestedPort).toBe(1443);
|
|
112
|
+
});
|
|
113
|
+
it("uses service name as fallback when port has no description", () => {
|
|
114
|
+
globalExpect(formatPortConflicts([makeService("myapp", [{
|
|
115
|
+
host: 9e3,
|
|
116
|
+
container: 9e3,
|
|
117
|
+
exposed: true,
|
|
118
|
+
description: ""
|
|
119
|
+
}])], new Map([["myapp", new Map([[9e3, 1e4]])]]))[0].description).toBe("myapp port");
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
describe("scanPortConflicts", () => {
|
|
123
|
+
it("returns empty map for services with no ports", async () => {
|
|
124
|
+
globalExpect((await scanPortConflicts([makeService("redis", [])])).size).toBe(0);
|
|
125
|
+
});
|
|
126
|
+
it("returns empty map for services with no exposed ports", async () => {
|
|
127
|
+
globalExpect((await scanPortConflicts([makeService("redis", [{
|
|
128
|
+
host: 6379,
|
|
129
|
+
container: 6379,
|
|
130
|
+
exposed: false,
|
|
131
|
+
description: "Redis"
|
|
132
|
+
}])])).size).toBe(0);
|
|
133
|
+
});
|
|
134
|
+
it("detects inter-service port conflicts", async () => {
|
|
135
|
+
const result = await scanPortConflicts([makeService("web1", [{
|
|
136
|
+
host: 8080,
|
|
137
|
+
container: 80,
|
|
138
|
+
exposed: true,
|
|
139
|
+
description: "HTTP"
|
|
140
|
+
}]), makeService("web2", [{
|
|
141
|
+
host: 8080,
|
|
142
|
+
container: 80,
|
|
143
|
+
exposed: true,
|
|
144
|
+
description: "HTTP"
|
|
145
|
+
}])]);
|
|
146
|
+
globalExpect(result.has("web1")).toBe(false);
|
|
147
|
+
globalExpect(result.has("web2")).toBe(true);
|
|
148
|
+
const web2Reassignments = result.get("web2");
|
|
149
|
+
globalExpect(web2Reassignments.has(8080)).toBe(true);
|
|
150
|
+
globalExpect(web2Reassignments.get(8080)).toBeGreaterThan(8080);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
//#endregion
|
|
154
|
+
export {};
|
|
155
|
+
|
|
156
|
+
//# sourceMappingURL=port-scanner.test.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"port-scanner.test.mjs","names":[],"sources":["../src/port-scanner.test.ts"],"sourcesContent":["import { describe, expect, it, vi } from \"vitest\";\nimport { formatPortConflicts, scanPortConflicts } from \"./port-scanner.js\";\nimport type { ServiceDefinition } from \"./types.js\";\n\n/**\n * Tests for port scanner conflict detection and reassignment.\n *\n * We mock isPortAvailable via vi.mock to avoid actual port scanning\n * during tests. The focus is on the conflict detection logic.\n */\n\n// Mock the internal isPortAvailable to control port availability\nvi.mock(\"node:net\", () => {\n\tconst portsInUse = new Set([80, 443, 3000]);\n\n\treturn {\n\t\tdefault: {\n\t\t\tSocket: class MockSocket {\n\t\t\t\tconnect(port: number) {\n\t\t\t\t\t// Simulate: ports in portsInUse are \"in use\"\n\t\t\t\t\tsetTimeout(() => {\n\t\t\t\t\t\tif (portsInUse.has(port)) {\n\t\t\t\t\t\t\t(this as any).connectCb?.();\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t(this as any).errorCb?.();\n\t\t\t\t\t\t}\n\t\t\t\t\t}, 1);\n\t\t\t\t}\n\t\t\t\tonce(event: string, cb: () => void) {\n\t\t\t\t\tif (event === \"connect\") (this as any).connectCb = cb;\n\t\t\t\t\tif (event === \"error\") (this as any).errorCb = cb;\n\t\t\t\t\tif (event === \"timeout\") (this as any).timeoutCb = cb;\n\t\t\t\t}\n\t\t\t\tsetTimeout() {}\n\t\t\t\tremoveAllListeners() {}\n\t\t\t\tdestroy() {}\n\t\t\t},\n\t\t\tcreateServer() {\n\t\t\t\treturn {\n\t\t\t\t\tonce(event: string, cb: (err?: Error) => void) {\n\t\t\t\t\t\tif (event === \"error\") (this as any).errorCb = cb;\n\t\t\t\t\t},\n\t\t\t\t\tlisten(_port: number, _host: string, cb: () => void) {\n\t\t\t\t\t\tcb();\n\t\t\t\t\t},\n\t\t\t\t\tclose(cb: () => void) {\n\t\t\t\t\t\tcb();\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t},\n\t\t},\n\t};\n});\n\nfunction makeService(\n\tid: string,\n\tports: { host: number; container: number; exposed: boolean; description: string }[],\n): ServiceDefinition {\n\treturn {\n\t\tid,\n\t\tname: id,\n\t\tdescription: `${id} service`,\n\t\ticon: \"\",\n\t\tcategory: \"test\",\n\t\timage: `${id}:latest`,\n\t\tports,\n\t\tvolumes: [],\n\t\tenvironment: [],\n\t\tdependencies: [],\n\t\tconflicts: [],\n\t\tskills: [],\n\t\tmemoryMB: 256,\n\t\tdocsUrl: \"\",\n\t} as ServiceDefinition;\n}\n\ndescribe(\"formatPortConflicts\", () => {\n\tit(\"returns empty array when no reassignments\", () => {\n\t\tconst services = [\n\t\t\tmakeService(\"redis\", [{ host: 6379, container: 6379, exposed: true, description: \"Redis\" }]),\n\t\t];\n\t\tconst reassignments = new Map();\n\t\tconst conflicts = formatPortConflicts(services, reassignments);\n\t\texpect(conflicts).toEqual([]);\n\t});\n\n\tit(\"returns conflicts with suggested ports\", () => {\n\t\tconst services = [\n\t\t\tmakeService(\"redis\", [{ host: 6379, container: 6379, exposed: true, description: \"Redis\" }]),\n\t\t];\n\t\tconst reassignments = new Map([[\"redis\", new Map([[6379, 7379]])]]);\n\t\tconst conflicts = formatPortConflicts(services, reassignments);\n\t\texpect(conflicts).toHaveLength(1);\n\t\texpect(conflicts[0]).toEqual({\n\t\t\tport: 6379,\n\t\t\tserviceId: \"redis\",\n\t\t\tdescription: \"Redis\",\n\t\t\tsuggestedPort: 7379,\n\t\t});\n\t});\n\n\tit(\"handles multiple services with multiple port reassignments\", () => {\n\t\tconst services = [\n\t\t\tmakeService(\"web\", [\n\t\t\t\t{ host: 80, container: 80, exposed: true, description: \"HTTP\" },\n\t\t\t\t{ host: 443, container: 443, exposed: true, description: \"HTTPS\" },\n\t\t\t]),\n\t\t\tmakeService(\"api\", [{ host: 8080, container: 8080, exposed: true, description: \"API\" }]),\n\t\t];\n\t\tconst reassignments = new Map([\n\t\t\t[\n\t\t\t\t\"web\",\n\t\t\t\tnew Map([\n\t\t\t\t\t[80, 1080],\n\t\t\t\t\t[443, 1443],\n\t\t\t\t]),\n\t\t\t],\n\t\t]);\n\t\tconst conflicts = formatPortConflicts(services, reassignments);\n\t\texpect(conflicts).toHaveLength(2);\n\t\texpect(conflicts[0]!.port).toBe(80);\n\t\texpect(conflicts[0]!.suggestedPort).toBe(1080);\n\t\texpect(conflicts[1]!.port).toBe(443);\n\t\texpect(conflicts[1]!.suggestedPort).toBe(1443);\n\t});\n\n\tit(\"uses service name as fallback when port has no description\", () => {\n\t\tconst services = [\n\t\t\tmakeService(\"myapp\", [{ host: 9000, container: 9000, exposed: true, description: \"\" }]),\n\t\t];\n\t\tconst reassignments = new Map([[\"myapp\", new Map([[9000, 10000]])]]);\n\t\tconst conflicts = formatPortConflicts(services, reassignments);\n\t\texpect(conflicts[0]!.description).toBe(\"myapp port\");\n\t});\n});\n\ndescribe(\"scanPortConflicts\", () => {\n\tit(\"returns empty map for services with no ports\", async () => {\n\t\tconst services = [makeService(\"redis\", [])];\n\t\tconst result = await scanPortConflicts(services);\n\t\texpect(result.size).toBe(0);\n\t});\n\n\tit(\"returns empty map for services with no exposed ports\", async () => {\n\t\tconst services = [\n\t\t\tmakeService(\"redis\", [{ host: 6379, container: 6379, exposed: false, description: \"Redis\" }]),\n\t\t];\n\t\tconst result = await scanPortConflicts(services);\n\t\texpect(result.size).toBe(0);\n\t});\n\n\tit(\"detects inter-service port conflicts\", async () => {\n\t\t// Two services claiming the same host port\n\t\tconst services = [\n\t\t\tmakeService(\"web1\", [{ host: 8080, container: 80, exposed: true, description: \"HTTP\" }]),\n\t\t\tmakeService(\"web2\", [{ host: 8080, container: 80, exposed: true, description: \"HTTP\" }]),\n\t\t];\n\t\tconst result = await scanPortConflicts(services);\n\n\t\t// First service wins, second gets reassigned\n\t\texpect(result.has(\"web1\")).toBe(false); // web1 keeps its port\n\t\texpect(result.has(\"web2\")).toBe(true); // web2 gets reassigned\n\t\tconst web2Reassignments = result.get(\"web2\")!;\n\t\texpect(web2Reassignments.has(8080)).toBe(true);\n\t\texpect(web2Reassignments.get(8080)).toBeGreaterThan(8080);\n\t});\n});\n"],"mappings":";;;;;;;;;AAYA,GAAG,KAAK,kBAAkB;CACzB,MAAM,aAAa,IAAI,IAAI;EAAC;EAAI;EAAK;EAAK,CAAC;AAE3C,QAAO,EACN,SAAS;EACR,QAAQ,MAAM,WAAW;GACxB,QAAQ,MAAc;AAErB,qBAAiB;AAChB,SAAI,WAAW,IAAI,KAAK,CACtB,MAAa,aAAa;SAE1B,MAAa,WAAW;OAExB,EAAE;;GAEN,KAAK,OAAe,IAAgB;AACnC,QAAI,UAAU,UAAY,MAAa,YAAY;AACnD,QAAI,UAAU,QAAU,MAAa,UAAU;AAC/C,QAAI,UAAU,UAAY,MAAa,YAAY;;GAEpD,aAAa;GACb,qBAAqB;GACrB,UAAU;;EAEX,eAAe;AACd,UAAO;IACN,KAAK,OAAe,IAA2B;AAC9C,SAAI,UAAU,QAAU,MAAa,UAAU;;IAEhD,OAAO,OAAe,OAAe,IAAgB;AACpD,SAAI;;IAEL,MAAM,IAAgB;AACrB,SAAI;;IAEL;;EAEF,EACD;EACA;AAEF,SAAS,YACR,IACA,OACoB;AACpB,QAAO;EACN;EACA,MAAM;EACN,aAAa,GAAG,GAAG;EACnB,MAAM;EACN,UAAU;EACV,OAAO,GAAG,GAAG;EACb;EACA,SAAS,EAAE;EACX,aAAa,EAAE;EACf,cAAc,EAAE;EAChB,WAAW,EAAE;EACb,QAAQ,EAAE;EACV,UAAU;EACV,SAAS;EACT;;AAGF,SAAS,6BAA6B;AACrC,IAAG,mDAAmD;AAMrD,eADkB,oBAJD,CAChB,YAAY,SAAS,CAAC;GAAE,MAAM;GAAM,WAAW;GAAM,SAAS;GAAM,aAAa;GAAS,CAAC,CAAC,CAC5F,kBACqB,IAAI,KAAK,CAC+B,CAC7C,CAAC,QAAQ,EAAE,CAAC;GAC5B;AAEF,IAAG,gDAAgD;EAKlD,MAAM,YAAY,oBAJD,CAChB,YAAY,SAAS,CAAC;GAAE,MAAM;GAAM,WAAW;GAAM,SAAS;GAAM,aAAa;GAAS,CAAC,CAAC,CAC5F,EACqB,IAAI,IAAI,CAAC,CAAC,SAAS,IAAI,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CACL;AAC9D,eAAO,UAAU,CAAC,aAAa,EAAE;AACjC,eAAO,UAAU,GAAG,CAAC,QAAQ;GAC5B,MAAM;GACN,WAAW;GACX,aAAa;GACb,eAAe;GACf,CAAC;GACD;AAEF,IAAG,oEAAoE;EAiBtE,MAAM,YAAY,oBAhBD,CAChB,YAAY,OAAO,CAClB;GAAE,MAAM;GAAI,WAAW;GAAI,SAAS;GAAM,aAAa;GAAQ,EAC/D;GAAE,MAAM;GAAK,WAAW;GAAK,SAAS;GAAM,aAAa;GAAS,CAClE,CAAC,EACF,YAAY,OAAO,CAAC;GAAE,MAAM;GAAM,WAAW;GAAM,SAAS;GAAM,aAAa;GAAO,CAAC,CAAC,CACxF,EACqB,IAAI,IAAI,CAC7B,CACC,OACA,IAAI,IAAI,CACP,CAAC,IAAI,KAAK,EACV,CAAC,KAAK,KAAK,CACX,CAAC,CACF,CACD,CAAC,CAC4D;AAC9D,eAAO,UAAU,CAAC,aAAa,EAAE;AACjC,eAAO,UAAU,GAAI,KAAK,CAAC,KAAK,GAAG;AACnC,eAAO,UAAU,GAAI,cAAc,CAAC,KAAK,KAAK;AAC9C,eAAO,UAAU,GAAI,KAAK,CAAC,KAAK,IAAI;AACpC,eAAO,UAAU,GAAI,cAAc,CAAC,KAAK,KAAK;GAC7C;AAEF,IAAG,oEAAoE;AAMtE,eADkB,oBAJD,CAChB,YAAY,SAAS,CAAC;GAAE,MAAM;GAAM,WAAW;GAAM,SAAS;GAAM,aAAa;GAAI,CAAC,CAAC,CACvF,EACqB,IAAI,IAAI,CAAC,CAAC,SAAS,IAAI,IAAI,CAAC,CAAC,KAAM,IAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CACN,CAC7C,GAAI,YAAY,CAAC,KAAK,aAAa;GACnD;EACD;AAEF,SAAS,2BAA2B;AACnC,IAAG,gDAAgD,YAAY;AAG9D,gBADe,MAAM,kBADJ,CAAC,YAAY,SAAS,EAAE,CAAC,CAAC,CACK,EAClC,KAAK,CAAC,KAAK,EAAE;GAC1B;AAEF,IAAG,wDAAwD,YAAY;AAKtE,gBADe,MAAM,kBAHJ,CAChB,YAAY,SAAS,CAAC;GAAE,MAAM;GAAM,WAAW;GAAM,SAAS;GAAO,aAAa;GAAS,CAAC,CAAC,CAC7F,CAC+C,EAClC,KAAK,CAAC,KAAK,EAAE;GAC1B;AAEF,IAAG,wCAAwC,YAAY;EAMtD,MAAM,SAAS,MAAM,kBAJJ,CAChB,YAAY,QAAQ,CAAC;GAAE,MAAM;GAAM,WAAW;GAAI,SAAS;GAAM,aAAa;GAAQ,CAAC,CAAC,EACxF,YAAY,QAAQ,CAAC;GAAE,MAAM;GAAM,WAAW;GAAI,SAAS;GAAM,aAAa;GAAQ,CAAC,CAAC,CACxF,CAC+C;AAGhD,eAAO,OAAO,IAAI,OAAO,CAAC,CAAC,KAAK,MAAM;AACtC,eAAO,OAAO,IAAI,OAAO,CAAC,CAAC,KAAK,KAAK;EACrC,MAAM,oBAAoB,OAAO,IAAI,OAAO;AAC5C,eAAO,kBAAkB,IAAI,KAAK,CAAC,CAAC,KAAK,KAAK;AAC9C,eAAO,kBAAkB,IAAI,KAAK,CAAC,CAAC,gBAAgB,KAAK;GACxD;EACD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-openclaw/core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.31",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Core logic for better-openclaw: schemas, service registry, resolver, composer, validators and generators",
|
|
6
6
|
"author": "bidew.io <bachir@bidew.io>",
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tests for Coolify deployer utility functions and deploy orchestration.
|
|
5
|
+
*
|
|
6
|
+
* We mock `fetch` to simulate Coolify API responses and test the
|
|
7
|
+
* multi-step deployment flow without making real HTTP requests.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Inline the utility functions for direct testing
|
|
11
|
+
function hashString(str: string) {
|
|
12
|
+
let hash = 0;
|
|
13
|
+
for (let i = 0; i < str.length; i++) {
|
|
14
|
+
hash = (hash << 5) - hash + str.charCodeAt(i);
|
|
15
|
+
hash |= 0;
|
|
16
|
+
}
|
|
17
|
+
return hash;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseEnvContent(envContent?: string) {
|
|
21
|
+
if (!envContent) return [];
|
|
22
|
+
const result = [];
|
|
23
|
+
for (const line of envContent.split("\n")) {
|
|
24
|
+
const trimmed = line.trim();
|
|
25
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
26
|
+
const idx = trimmed.indexOf("=");
|
|
27
|
+
if (idx <= 0) continue;
|
|
28
|
+
const key = trimmed.slice(0, idx);
|
|
29
|
+
const value = trimmed.slice(idx + 1);
|
|
30
|
+
result.push({ key, value, is_preview: false, is_build_time: false, is_literal: true });
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("hashString", () => {
|
|
36
|
+
it("returns 0 for empty string", () => {
|
|
37
|
+
expect(hashString("")).toBe(0);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("returns same hash for same input", () => {
|
|
41
|
+
expect(hashString("hello")).toBe(hashString("hello"));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("returns different hash for different input", () => {
|
|
45
|
+
expect(hashString("hello")).not.toBe(hashString("world"));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("detects compose changes", () => {
|
|
49
|
+
const v1 = "version: '3'\nservices:\n redis:\n image: redis:7";
|
|
50
|
+
const v2 = "version: '3'\nservices:\n redis:\n image: redis:8";
|
|
51
|
+
expect(hashString(v1)).not.toBe(hashString(v2));
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("parseEnvContent", () => {
|
|
56
|
+
it("returns empty array for undefined input", () => {
|
|
57
|
+
expect(parseEnvContent(undefined)).toEqual([]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("returns empty array for empty string", () => {
|
|
61
|
+
expect(parseEnvContent("")).toEqual([]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("parses simple key=value pairs", () => {
|
|
65
|
+
const result = parseEnvContent("FOO=bar\nBAZ=qux");
|
|
66
|
+
expect(result).toHaveLength(2);
|
|
67
|
+
expect(result[0]).toEqual({
|
|
68
|
+
key: "FOO",
|
|
69
|
+
value: "bar",
|
|
70
|
+
is_preview: false,
|
|
71
|
+
is_build_time: false,
|
|
72
|
+
is_literal: true,
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("skips comments", () => {
|
|
77
|
+
const result = parseEnvContent("# This is a comment\nFOO=bar");
|
|
78
|
+
expect(result).toHaveLength(1);
|
|
79
|
+
expect(result[0]!.key).toBe("FOO");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("skips blank lines", () => {
|
|
83
|
+
const result = parseEnvContent("FOO=bar\n\n\nBAZ=qux");
|
|
84
|
+
expect(result).toHaveLength(2);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("handles values containing equals signs", () => {
|
|
88
|
+
const result = parseEnvContent(
|
|
89
|
+
"DATABASE_URL=postgres://user:pass@host:5432/db?sslmode=require",
|
|
90
|
+
);
|
|
91
|
+
expect(result).toHaveLength(1);
|
|
92
|
+
expect(result[0]!.key).toBe("DATABASE_URL");
|
|
93
|
+
expect(result[0]!.value).toBe("postgres://user:pass@host:5432/db?sslmode=require");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("handles empty values", () => {
|
|
97
|
+
const result = parseEnvContent("EMPTY_VAR=");
|
|
98
|
+
expect(result).toHaveLength(1);
|
|
99
|
+
expect(result[0]!.key).toBe("EMPTY_VAR");
|
|
100
|
+
expect(result[0]!.value).toBe("");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("skips lines without = sign", () => {
|
|
104
|
+
const result = parseEnvContent("INVALID_LINE\nVALID=yes");
|
|
105
|
+
expect(result).toHaveLength(1);
|
|
106
|
+
expect(result[0]!.key).toBe("VALID");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("handles quoted values", () => {
|
|
110
|
+
const result = parseEnvContent('PASSWORD="my secret"');
|
|
111
|
+
expect(result[0]!.key).toBe("PASSWORD");
|
|
112
|
+
expect(result[0]!.value).toBe('"my secret"');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("CoolifyDeployer", () => {
|
|
117
|
+
beforeEach(() => {
|
|
118
|
+
vi.restoreAllMocks();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("can be imported and instantiated", async () => {
|
|
122
|
+
const { CoolifyDeployer } = await import("./coolify.js");
|
|
123
|
+
const deployer = new CoolifyDeployer();
|
|
124
|
+
expect(deployer.name).toBe("Coolify");
|
|
125
|
+
expect(deployer.id).toBe("coolify");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("testConnection returns error on network failure", async () => {
|
|
129
|
+
vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("Connection refused")));
|
|
130
|
+
const { CoolifyDeployer } = await import("./coolify.js");
|
|
131
|
+
const deployer = new CoolifyDeployer();
|
|
132
|
+
const result = await deployer.testConnection({
|
|
133
|
+
instanceUrl: "https://coolify.example.com",
|
|
134
|
+
apiKey: "test-key",
|
|
135
|
+
});
|
|
136
|
+
expect(result.ok).toBe(false);
|
|
137
|
+
expect(result.error).toContain("Connection refused");
|
|
138
|
+
vi.unstubAllGlobals();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("testConnection returns ok on successful version response", async () => {
|
|
142
|
+
vi.stubGlobal(
|
|
143
|
+
"fetch",
|
|
144
|
+
vi.fn().mockResolvedValue({
|
|
145
|
+
ok: true,
|
|
146
|
+
text: () => Promise.resolve('"v4.0.0"'),
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
149
|
+
const { CoolifyDeployer } = await import("./coolify.js");
|
|
150
|
+
const deployer = new CoolifyDeployer();
|
|
151
|
+
const result = await deployer.testConnection({
|
|
152
|
+
instanceUrl: "https://coolify.example.com",
|
|
153
|
+
apiKey: "test-key",
|
|
154
|
+
});
|
|
155
|
+
expect(result.ok).toBe(true);
|
|
156
|
+
vi.unstubAllGlobals();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("deploy returns error when no servers available", async () => {
|
|
160
|
+
vi.stubGlobal(
|
|
161
|
+
"fetch",
|
|
162
|
+
vi.fn().mockResolvedValue({
|
|
163
|
+
ok: true,
|
|
164
|
+
text: () => Promise.resolve("[]"),
|
|
165
|
+
}),
|
|
166
|
+
);
|
|
167
|
+
const { CoolifyDeployer } = await import("./coolify.js");
|
|
168
|
+
const deployer = new CoolifyDeployer();
|
|
169
|
+
const result = await deployer.deploy({
|
|
170
|
+
target: { instanceUrl: "https://coolify.example.com", apiKey: "test-key" },
|
|
171
|
+
projectName: "test-project",
|
|
172
|
+
composeYaml: "version: '3'",
|
|
173
|
+
envContent: "",
|
|
174
|
+
});
|
|
175
|
+
expect(result.success).toBe(false);
|
|
176
|
+
expect(result.error).toContain("No Coolify servers");
|
|
177
|
+
expect(result.steps[0]!.status).toBe("error");
|
|
178
|
+
vi.unstubAllGlobals();
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
describe("DokployDeployer", () => {
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
vi.restoreAllMocks();
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
it("can be imported and instantiated", async () => {
|
|
9
|
+
const { DokployDeployer } = await import("./dokploy.js");
|
|
10
|
+
const deployer = new DokployDeployer();
|
|
11
|
+
expect(deployer.name).toBe("Dokploy");
|
|
12
|
+
expect(deployer.id).toBe("dokploy");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("testConnection returns error on network failure", async () => {
|
|
16
|
+
vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("ECONNREFUSED")));
|
|
17
|
+
const { DokployDeployer } = await import("./dokploy.js");
|
|
18
|
+
const deployer = new DokployDeployer();
|
|
19
|
+
const result = await deployer.testConnection({
|
|
20
|
+
instanceUrl: "https://dokploy.example.com",
|
|
21
|
+
apiKey: "test-key",
|
|
22
|
+
});
|
|
23
|
+
expect(result.ok).toBe(false);
|
|
24
|
+
expect(result.error).toContain("ECONNREFUSED");
|
|
25
|
+
vi.unstubAllGlobals();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("testConnection returns ok when project list succeeds", async () => {
|
|
29
|
+
vi.stubGlobal(
|
|
30
|
+
"fetch",
|
|
31
|
+
vi.fn().mockResolvedValue({
|
|
32
|
+
ok: true,
|
|
33
|
+
text: () => Promise.resolve("[]"),
|
|
34
|
+
}),
|
|
35
|
+
);
|
|
36
|
+
const { DokployDeployer } = await import("./dokploy.js");
|
|
37
|
+
const deployer = new DokployDeployer();
|
|
38
|
+
const result = await deployer.testConnection({
|
|
39
|
+
instanceUrl: "https://dokploy.example.com",
|
|
40
|
+
apiKey: "test-key",
|
|
41
|
+
});
|
|
42
|
+
expect(result.ok).toBe(true);
|
|
43
|
+
vi.unstubAllGlobals();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("listServers returns empty array on API failure", async () => {
|
|
47
|
+
vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("Not found")));
|
|
48
|
+
const { DokployDeployer } = await import("./dokploy.js");
|
|
49
|
+
const deployer = new DokployDeployer();
|
|
50
|
+
const servers = await deployer.listServers({
|
|
51
|
+
instanceUrl: "https://dokploy.example.com",
|
|
52
|
+
apiKey: "test-key",
|
|
53
|
+
});
|
|
54
|
+
expect(servers).toEqual([]);
|
|
55
|
+
vi.unstubAllGlobals();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("listServers maps server fields correctly", async () => {
|
|
59
|
+
vi.stubGlobal(
|
|
60
|
+
"fetch",
|
|
61
|
+
vi.fn().mockResolvedValue({
|
|
62
|
+
ok: true,
|
|
63
|
+
text: () =>
|
|
64
|
+
Promise.resolve(
|
|
65
|
+
JSON.stringify([
|
|
66
|
+
{ serverId: "s1", name: "Production", ipAddress: "1.2.3.4" },
|
|
67
|
+
{ serverId: "s2", name: "Staging", ipAddress: "5.6.7.8" },
|
|
68
|
+
]),
|
|
69
|
+
),
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
const { DokployDeployer } = await import("./dokploy.js");
|
|
73
|
+
const deployer = new DokployDeployer();
|
|
74
|
+
const servers = await deployer.listServers({
|
|
75
|
+
instanceUrl: "https://dokploy.example.com",
|
|
76
|
+
apiKey: "test-key",
|
|
77
|
+
});
|
|
78
|
+
expect(servers).toHaveLength(2);
|
|
79
|
+
expect(servers[0]).toEqual({ id: "s1", name: "Production", ip: "1.2.3.4" });
|
|
80
|
+
expect(servers[1]).toEqual({ id: "s2", name: "Staging", ip: "5.6.7.8" });
|
|
81
|
+
vi.unstubAllGlobals();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("deploy sets error on running step when an API call fails", async () => {
|
|
85
|
+
let callCount = 0;
|
|
86
|
+
vi.stubGlobal(
|
|
87
|
+
"fetch",
|
|
88
|
+
vi.fn().mockImplementation(() => {
|
|
89
|
+
callCount++;
|
|
90
|
+
if (callCount === 1) {
|
|
91
|
+
// project.all succeeds
|
|
92
|
+
return Promise.resolve({
|
|
93
|
+
ok: true,
|
|
94
|
+
text: () => Promise.resolve("[]"),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
// project.create fails
|
|
98
|
+
return Promise.resolve({
|
|
99
|
+
ok: false,
|
|
100
|
+
status: 500,
|
|
101
|
+
text: () => Promise.resolve('{"message":"Internal error"}'),
|
|
102
|
+
});
|
|
103
|
+
}),
|
|
104
|
+
);
|
|
105
|
+
const { DokployDeployer } = await import("./dokploy.js");
|
|
106
|
+
const deployer = new DokployDeployer();
|
|
107
|
+
const result = await deployer.deploy({
|
|
108
|
+
target: { instanceUrl: "https://dokploy.example.com", apiKey: "key" },
|
|
109
|
+
projectName: "test",
|
|
110
|
+
composeYaml: "version: '3'",
|
|
111
|
+
envContent: "",
|
|
112
|
+
});
|
|
113
|
+
expect(result.success).toBe(false);
|
|
114
|
+
expect(result.error).toBeDefined();
|
|
115
|
+
// At least one step should be in error state
|
|
116
|
+
const errorStep = result.steps.find((s) => s.status === "error");
|
|
117
|
+
expect(errorStep).toBeDefined();
|
|
118
|
+
vi.unstubAllGlobals();
|
|
119
|
+
});
|
|
120
|
+
});
|