@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.
Files changed (96) hide show
  1. package/README.md +2 -1
  2. package/dist/deployers/coolify.test.cjs +156 -0
  3. package/dist/deployers/coolify.test.cjs.map +1 -0
  4. package/dist/deployers/coolify.test.d.cts +1 -0
  5. package/dist/deployers/coolify.test.d.mts +1 -0
  6. package/dist/deployers/coolify.test.mjs +157 -0
  7. package/dist/deployers/coolify.test.mjs.map +1 -0
  8. package/dist/deployers/dokploy.test.cjs +108 -0
  9. package/dist/deployers/dokploy.test.cjs.map +1 -0
  10. package/dist/deployers/dokploy.test.d.cts +1 -0
  11. package/dist/deployers/dokploy.test.d.mts +1 -0
  12. package/dist/deployers/dokploy.test.mjs +109 -0
  13. package/dist/deployers/dokploy.test.mjs.map +1 -0
  14. package/dist/frameworks/frameworks.test.cjs +94 -0
  15. package/dist/frameworks/frameworks.test.cjs.map +1 -0
  16. package/dist/frameworks/frameworks.test.d.cts +1 -0
  17. package/dist/frameworks/frameworks.test.d.mts +1 -0
  18. package/dist/frameworks/frameworks.test.mjs +94 -0
  19. package/dist/frameworks/frameworks.test.mjs.map +1 -0
  20. package/dist/generators/cloud-init.test.cjs +58 -0
  21. package/dist/generators/cloud-init.test.cjs.map +1 -0
  22. package/dist/generators/cloud-init.test.d.cts +1 -0
  23. package/dist/generators/cloud-init.test.d.mts +1 -0
  24. package/dist/generators/cloud-init.test.mjs +59 -0
  25. package/dist/generators/cloud-init.test.mjs.map +1 -0
  26. package/dist/generators/get-shit-done.test.cjs +48 -0
  27. package/dist/generators/get-shit-done.test.cjs.map +1 -0
  28. package/dist/generators/get-shit-done.test.d.cts +1 -0
  29. package/dist/generators/get-shit-done.test.d.mts +1 -0
  30. package/dist/generators/get-shit-done.test.mjs +49 -0
  31. package/dist/generators/get-shit-done.test.mjs.map +1 -0
  32. package/dist/generators/grafana.test.cjs +74 -0
  33. package/dist/generators/grafana.test.cjs.map +1 -0
  34. package/dist/generators/grafana.test.d.cts +1 -0
  35. package/dist/generators/grafana.test.d.mts +1 -0
  36. package/dist/generators/grafana.test.mjs +74 -0
  37. package/dist/generators/grafana.test.mjs.map +1 -0
  38. package/dist/generators/n8n-workflows.test.cjs +75 -0
  39. package/dist/generators/n8n-workflows.test.cjs.map +1 -0
  40. package/dist/generators/n8n-workflows.test.d.cts +1 -0
  41. package/dist/generators/n8n-workflows.test.d.mts +1 -0
  42. package/dist/generators/n8n-workflows.test.mjs +76 -0
  43. package/dist/generators/n8n-workflows.test.mjs.map +1 -0
  44. package/dist/generators/openclaw-install-script.test.cjs +35 -0
  45. package/dist/generators/openclaw-install-script.test.cjs.map +1 -0
  46. package/dist/generators/openclaw-install-script.test.d.cts +1 -0
  47. package/dist/generators/openclaw-install-script.test.d.mts +1 -0
  48. package/dist/generators/openclaw-install-script.test.mjs +36 -0
  49. package/dist/generators/openclaw-install-script.test.mjs.map +1 -0
  50. package/dist/generators/postgres-init.test.cjs +111 -0
  51. package/dist/generators/postgres-init.test.cjs.map +1 -0
  52. package/dist/generators/postgres-init.test.d.cts +1 -0
  53. package/dist/generators/postgres-init.test.d.mts +1 -0
  54. package/dist/generators/postgres-init.test.mjs +112 -0
  55. package/dist/generators/postgres-init.test.mjs.map +1 -0
  56. package/dist/generators/prometheus.test.cjs +99 -0
  57. package/dist/generators/prometheus.test.cjs.map +1 -0
  58. package/dist/generators/prometheus.test.d.cts +1 -0
  59. package/dist/generators/prometheus.test.d.mts +1 -0
  60. package/dist/generators/prometheus.test.mjs +99 -0
  61. package/dist/generators/prometheus.test.mjs.map +1 -0
  62. package/dist/generators/stack-manifest.test.cjs +97 -0
  63. package/dist/generators/stack-manifest.test.cjs.map +1 -0
  64. package/dist/generators/stack-manifest.test.d.cts +1 -0
  65. package/dist/generators/stack-manifest.test.d.mts +1 -0
  66. package/dist/generators/stack-manifest.test.mjs +98 -0
  67. package/dist/generators/stack-manifest.test.mjs.map +1 -0
  68. package/dist/index.cjs +0 -2
  69. package/dist/index.d.cts +1 -2
  70. package/dist/index.d.mts +1 -2
  71. package/dist/index.mjs +1 -2
  72. package/dist/logger/index.cjs +0 -2
  73. package/dist/logger/index.d.cts +1 -2
  74. package/dist/logger/index.d.mts +1 -2
  75. package/dist/logger/index.mjs +1 -2
  76. package/dist/port-scanner.test.cjs +155 -0
  77. package/dist/port-scanner.test.cjs.map +1 -0
  78. package/dist/port-scanner.test.d.cts +1 -0
  79. package/dist/port-scanner.test.d.mts +1 -0
  80. package/dist/port-scanner.test.mjs +156 -0
  81. package/dist/port-scanner.test.mjs.map +1 -0
  82. package/package.json +1 -1
  83. package/src/deployers/coolify.test.ts +180 -0
  84. package/src/deployers/dokploy.test.ts +120 -0
  85. package/src/frameworks/frameworks.test.ts +119 -0
  86. package/src/generators/cloud-init.test.ts +70 -0
  87. package/src/generators/get-shit-done.test.ts +54 -0
  88. package/src/generators/grafana.test.ts +90 -0
  89. package/src/generators/n8n-workflows.test.ts +80 -0
  90. package/src/generators/openclaw-install-script.test.ts +42 -0
  91. package/src/generators/postgres-init.test.ts +116 -0
  92. package/src/generators/prometheus.test.ts +108 -0
  93. package/src/generators/stack-manifest.test.ts +104 -0
  94. package/src/index.ts +3 -2
  95. package/src/logger/index.ts +2 -1
  96. package/src/port-scanner.test.ts +167 -0
@@ -0,0 +1,119 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { parse } from "yaml";
3
+ import { generate } from "../generate.js";
4
+ import { getAllFrameworks, getFrameworkById } from "./registry.js";
5
+
6
+ /**
7
+ * Smoke tests for all registered framework definitions.
8
+ *
9
+ * Each framework's buildGatewayService, generateConfig, and getBaseEnvVars
10
+ * should produce valid output when invoked through the generate() pipeline.
11
+ */
12
+
13
+ describe("framework smoke tests via generate()", () => {
14
+ const primaryFrameworks = getAllFrameworks().filter((fw) => fw.canBePrimary);
15
+
16
+ for (const fw of primaryFrameworks) {
17
+ describe(`${fw.name} (${fw.id})`, () => {
18
+ it("generates a valid stack with this framework as primary", () => {
19
+ const result = generate({
20
+ projectName: `${fw.id}-test`,
21
+ services: ["redis"],
22
+ skillPacks: [],
23
+ proxy: "none",
24
+ gpu: false,
25
+ platform: "linux/amd64",
26
+ deployment: "local",
27
+ generateSecrets: true,
28
+ openclawVersion: "latest",
29
+ primaryFramework: fw.id,
30
+ });
31
+
32
+ expect(result.files).toHaveProperty("docker-compose.yml");
33
+ const composed = parse(result.files["docker-compose.yml"]!);
34
+ expect(composed.services).toBeDefined();
35
+
36
+ // Gateway service should exist with the framework's naming pattern
37
+ const gatewayKey = `${fw.id}-gateway`;
38
+ expect(composed.services).toHaveProperty(gatewayKey);
39
+ });
40
+
41
+ it("produces a docker-compose with the framework's network", () => {
42
+ const result = generate({
43
+ projectName: `${fw.id}-net-test`,
44
+ services: ["redis"],
45
+ skillPacks: [],
46
+ proxy: "none",
47
+ gpu: false,
48
+ platform: "linux/amd64",
49
+ deployment: "local",
50
+ generateSecrets: true,
51
+ openclawVersion: "latest",
52
+ primaryFramework: fw.id,
53
+ });
54
+
55
+ const composed = parse(result.files["docker-compose.yml"]!);
56
+ if (fw.networkName) {
57
+ expect(composed.networks).toHaveProperty(fw.networkName);
58
+ }
59
+ });
60
+
61
+ it("getBaseEnvVars returns an array", () => {
62
+ const envVars = fw.getBaseEnvVars({
63
+ generateSecrets: true,
64
+ domain: "example.com",
65
+ });
66
+ expect(envVars).toBeInstanceOf(Array);
67
+ });
68
+
69
+ it("getMandatoryServices returns an array", () => {
70
+ const mandatory = fw.getMandatoryServices();
71
+ expect(mandatory).toBeInstanceOf(Array);
72
+ });
73
+
74
+ it("getRecommendedServices returns an array", () => {
75
+ const recommended = fw.getRecommendedServices();
76
+ expect(recommended).toBeInstanceOf(Array);
77
+ });
78
+
79
+ it("getEnvSectionName returns a non-empty string", () => {
80
+ const section = fw.getEnvSectionName();
81
+ expect(typeof section).toBe("string");
82
+ expect(section.length).toBeGreaterThan(0);
83
+ });
84
+
85
+ it("getProviderEnvKeys returns an array", () => {
86
+ const keys = fw.getProviderEnvKeys();
87
+ expect(keys).toBeInstanceOf(Array);
88
+ });
89
+ });
90
+ }
91
+ });
92
+
93
+ describe("companion frameworks via generate()", () => {
94
+ const companionFrameworks = getAllFrameworks().filter((fw) => fw.canBeCompanion);
95
+
96
+ for (const fw of companionFrameworks) {
97
+ it(`${fw.name} can be added as companion`, () => {
98
+ const result = generate({
99
+ projectName: `companion-${fw.id}-test`,
100
+ services: ["redis"],
101
+ skillPacks: [],
102
+ proxy: "none",
103
+ gpu: false,
104
+ platform: "linux/amd64",
105
+ deployment: "local",
106
+ generateSecrets: true,
107
+ openclawVersion: "latest",
108
+ companionFrameworks: [fw.id],
109
+ });
110
+
111
+ const composed = parse(result.files["docker-compose.yml"]!);
112
+ expect(composed.services).toBeDefined();
113
+
114
+ // Companion should appear in compose
115
+ const companionKey = `${fw.id}-companion`;
116
+ expect(composed.services).toHaveProperty(companionKey);
117
+ });
118
+ }
119
+ });
@@ -0,0 +1,70 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { generateCloudInit } from "./cloud-init.js";
3
+
4
+ describe("generateCloudInit", () => {
5
+ const baseOptions = {
6
+ composeYaml: "version: '3'\nservices:\n redis:\n image: redis:7",
7
+ envContent: "REDIS_PASSWORD=secret123",
8
+ projectName: "my-stack",
9
+ };
10
+
11
+ it("generates valid cloud-init YAML starting with #cloud-config", () => {
12
+ const result = generateCloudInit(baseOptions);
13
+ expect(result).toMatch(/^#cloud-config/);
14
+ });
15
+
16
+ it("embeds the compose YAML content", () => {
17
+ const result = generateCloudInit(baseOptions);
18
+ expect(result).toContain("redis:7");
19
+ });
20
+
21
+ it("embeds the env content with restrictive permissions", () => {
22
+ const result = generateCloudInit(baseOptions);
23
+ expect(result).toContain("REDIS_PASSWORD=secret123");
24
+ expect(result).toContain('"0600"');
25
+ });
26
+
27
+ it("includes project name in the output", () => {
28
+ const result = generateCloudInit(baseOptions);
29
+ expect(result).toContain("my-stack");
30
+ });
31
+
32
+ it("uses default gateway port 18789 when not specified", () => {
33
+ const result = generateCloudInit(baseOptions);
34
+ expect(result).toContain("18789");
35
+ });
36
+
37
+ it("uses custom gateway port when specified", () => {
38
+ const result = generateCloudInit({ ...baseOptions, gatewayPort: 9999 });
39
+ expect(result).toContain("9999");
40
+ expect(result).not.toContain("18789");
41
+ });
42
+
43
+ it("includes Docker installation via convenience script", () => {
44
+ const result = generateCloudInit(baseOptions);
45
+ expect(result).toContain("get.docker.com");
46
+ });
47
+
48
+ it("includes health check polling logic", () => {
49
+ const result = generateCloudInit(baseOptions);
50
+ expect(result).toContain("RETRIES=");
51
+ expect(result).toContain("curl");
52
+ });
53
+
54
+ it("includes runcmd section", () => {
55
+ const result = generateCloudInit(baseOptions);
56
+ expect(result).toContain("runcmd:");
57
+ });
58
+
59
+ it("includes final_message", () => {
60
+ const result = generateCloudInit(baseOptions);
61
+ expect(result).toContain("final_message:");
62
+ });
63
+
64
+ it("includes required packages", () => {
65
+ const result = generateCloudInit(baseOptions);
66
+ expect(result).toContain("packages:");
67
+ expect(result).toContain("curl");
68
+ expect(result).toContain("git");
69
+ });
70
+ });
@@ -0,0 +1,54 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { generateGsdScripts } from "./get-shit-done.js";
3
+
4
+ describe("generateGsdScripts", () => {
5
+ it("returns undefined when runtimes is undefined", () => {
6
+ expect(generateGsdScripts(undefined)).toBeUndefined();
7
+ });
8
+
9
+ it("returns undefined when runtimes is empty", () => {
10
+ expect(generateGsdScripts([])).toBeUndefined();
11
+ });
12
+
13
+ it("generates both sh and ps1 scripts", () => {
14
+ const result = generateGsdScripts(["claude" as any]);
15
+ expect(result).toBeDefined();
16
+ expect(result!.sh).toBeDefined();
17
+ expect(result!.ps1).toBeDefined();
18
+ });
19
+
20
+ it("sh script starts with shebang", () => {
21
+ const result = generateGsdScripts(["claude" as any]);
22
+ expect(result!.sh).toMatch(/^#!/);
23
+ });
24
+
25
+ it("includes runtime flags in sh script", () => {
26
+ const result = generateGsdScripts(["claude", "codex"] as any[]);
27
+ expect(result!.sh).toContain("--claude");
28
+ expect(result!.sh).toContain("--codex");
29
+ });
30
+
31
+ it("includes runtime flags in ps1 script", () => {
32
+ const result = generateGsdScripts(["claude", "codex"] as any[]);
33
+ expect(result!.ps1).toContain("--claude");
34
+ expect(result!.ps1).toContain("--codex");
35
+ });
36
+
37
+ it("uses npx to run get-shit-done", () => {
38
+ const result = generateGsdScripts(["claude"] as any[]);
39
+ expect(result!.sh).toContain("npx get-shit-done-cc@latest");
40
+ expect(result!.ps1).toContain("npx get-shit-done-cc@latest");
41
+ });
42
+
43
+ it("includes npx availability check", () => {
44
+ const result = generateGsdScripts(["claude"] as any[]);
45
+ expect(result!.sh).toContain("command -v npx");
46
+ expect(result!.ps1).toContain("Get-Command npx");
47
+ });
48
+
49
+ it("lists runtimes in output message", () => {
50
+ const result = generateGsdScripts(["claude", "codex"] as any[]);
51
+ expect(result!.sh).toContain("claude, codex");
52
+ expect(result!.ps1).toContain("claude, codex");
53
+ });
54
+ });
@@ -0,0 +1,90 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { parse } from "yaml";
3
+ import { generateGrafanaConfig, generateGrafanaDashboard } from "./grafana.js";
4
+
5
+ describe("generateGrafanaConfig", () => {
6
+ it("returns multiple config files", () => {
7
+ const files = generateGrafanaConfig();
8
+ expect(Object.keys(files).length).toBeGreaterThanOrEqual(3);
9
+ });
10
+
11
+ it("generates datasource provisioning YAML", () => {
12
+ const files = generateGrafanaConfig();
13
+ const dsFile = files["config/grafana/provisioning/datasources/prometheus.yml"];
14
+ expect(dsFile).toBeDefined();
15
+ const parsed = parse(dsFile!);
16
+ expect(parsed.datasources).toBeInstanceOf(Array);
17
+ expect(parsed.datasources[0].type).toBe("prometheus");
18
+ expect(parsed.datasources[0].isDefault).toBe(true);
19
+ });
20
+
21
+ it("generates dashboard provisioning YAML", () => {
22
+ const files = generateGrafanaConfig();
23
+ const dashFile = files["config/grafana/provisioning/dashboards/default.yml"];
24
+ expect(dashFile).toBeDefined();
25
+ const parsed = parse(dashFile!);
26
+ expect(parsed.providers).toBeInstanceOf(Array);
27
+ expect(parsed.providers[0].name).toContain("OpenClaw");
28
+ });
29
+
30
+ it("generates grafana.ini configuration", () => {
31
+ const files = generateGrafanaConfig();
32
+ const iniFile = files["config/grafana/grafana.ini"];
33
+ expect(iniFile).toBeDefined();
34
+ expect(iniFile).toContain("[server]");
35
+ expect(iniFile).toContain("[security]");
36
+ expect(iniFile).toContain("[alerting]");
37
+ });
38
+
39
+ it("disables anonymous auth", () => {
40
+ const files = generateGrafanaConfig();
41
+ const iniFile = files["config/grafana/grafana.ini"]!;
42
+ expect(iniFile).toContain("[auth.anonymous]");
43
+ expect(iniFile).toContain("enabled = false");
44
+ });
45
+ });
46
+
47
+ describe("generateGrafanaDashboard", () => {
48
+ it("returns valid JSON", () => {
49
+ const json = generateGrafanaDashboard();
50
+ const dashboard = JSON.parse(json);
51
+ expect(dashboard).toBeDefined();
52
+ });
53
+
54
+ it("includes expected panels", () => {
55
+ const dashboard = JSON.parse(generateGrafanaDashboard());
56
+ expect(dashboard.panels).toBeInstanceOf(Array);
57
+ expect(dashboard.panels.length).toBe(3);
58
+
59
+ const titles = dashboard.panels.map((p: { title: string }) => p.title);
60
+ expect(titles).toContain("Service Health");
61
+ expect(titles).toContain("Memory Usage");
62
+ expect(titles).toContain("Request Rate");
63
+ });
64
+
65
+ it("uses prometheus as datasource", () => {
66
+ const dashboard = JSON.parse(generateGrafanaDashboard());
67
+ for (const panel of dashboard.panels) {
68
+ expect(panel.datasource.type).toBe("prometheus");
69
+ }
70
+ });
71
+
72
+ it("has openclaw tag", () => {
73
+ const dashboard = JSON.parse(generateGrafanaDashboard());
74
+ expect(dashboard.tags).toContain("openclaw");
75
+ });
76
+
77
+ it("has correct schema version", () => {
78
+ const dashboard = JSON.parse(generateGrafanaDashboard());
79
+ expect(dashboard.schemaVersion).toBe(39);
80
+ });
81
+
82
+ it("panels have proper grid positions", () => {
83
+ const dashboard = JSON.parse(generateGrafanaDashboard());
84
+ for (const panel of dashboard.panels) {
85
+ expect(panel.gridPos).toBeDefined();
86
+ expect(panel.gridPos.h).toBeGreaterThan(0);
87
+ expect(panel.gridPos.w).toBeGreaterThan(0);
88
+ }
89
+ });
90
+ });
@@ -0,0 +1,80 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { ResolverOutput } from "../types.js";
3
+ import { generateN8nWorkflows } from "./n8n-workflows.js";
4
+
5
+ function makeResolved(serviceIds: string[]): ResolverOutput {
6
+ return {
7
+ services: serviceIds.map((id) => ({
8
+ definition: {
9
+ id,
10
+ name: id.charAt(0).toUpperCase() + id.slice(1),
11
+ description: "",
12
+ icon: "",
13
+ category: "test",
14
+ image: `${id}:latest`,
15
+ ports: [],
16
+ volumes: [],
17
+ environment: [],
18
+ dependencies: [],
19
+ conflicts: [],
20
+ skills: [],
21
+ memoryMB: 256,
22
+ docsUrl: "",
23
+ },
24
+ addedBy: "user" as const,
25
+ })),
26
+ addedDependencies: [],
27
+ estimatedMemoryMB: 512,
28
+ } as unknown as ResolverOutput;
29
+ }
30
+
31
+ describe("generateN8nWorkflows", () => {
32
+ it("returns empty object when n8n is not in the stack", () => {
33
+ const result = generateN8nWorkflows(makeResolved(["redis", "postgresql"]));
34
+ expect(Object.keys(result)).toHaveLength(0);
35
+ });
36
+
37
+ it("generates workflow file when n8n is present", () => {
38
+ const result = generateN8nWorkflows(makeResolved(["n8n", "redis"]));
39
+ expect(result).toHaveProperty("n8n/workflows/openclaw-webhook-handler.json");
40
+ });
41
+
42
+ it("generates valid JSON workflow", () => {
43
+ const result = generateN8nWorkflows(makeResolved(["n8n"]));
44
+ const workflow = JSON.parse(result["n8n/workflows/openclaw-webhook-handler.json"]!);
45
+ expect(workflow).toBeDefined();
46
+ expect(workflow.name).toBe("OpenClaw Webhook Handler");
47
+ });
48
+
49
+ it("workflow has three nodes (webhook, process, respond)", () => {
50
+ const result = generateN8nWorkflows(makeResolved(["n8n", "redis"]));
51
+ const workflow = JSON.parse(result["n8n/workflows/openclaw-webhook-handler.json"]!);
52
+ expect(workflow.nodes).toHaveLength(3);
53
+
54
+ const types = workflow.nodes.map((n: { type: string }) => n.type);
55
+ expect(types).toContain("n8n-nodes-base.webhook");
56
+ expect(types).toContain("n8n-nodes-base.code");
57
+ expect(types).toContain("n8n-nodes-base.respondToWebhook");
58
+ });
59
+
60
+ it("workflow has correct connections", () => {
61
+ const result = generateN8nWorkflows(makeResolved(["n8n"]));
62
+ const workflow = JSON.parse(result["n8n/workflows/openclaw-webhook-handler.json"]!);
63
+ expect(workflow.connections).toHaveProperty("Webhook");
64
+ expect(workflow.connections).toHaveProperty("Process Payload");
65
+ });
66
+
67
+ it("workflow is not active by default", () => {
68
+ const result = generateN8nWorkflows(makeResolved(["n8n"]));
69
+ const workflow = JSON.parse(result["n8n/workflows/openclaw-webhook-handler.json"]!);
70
+ expect(workflow.active).toBe(false);
71
+ });
72
+
73
+ it("includes service names in the code node comment", () => {
74
+ const result = generateN8nWorkflows(makeResolved(["n8n", "redis", "postgresql"]));
75
+ const workflow = JSON.parse(result["n8n/workflows/openclaw-webhook-handler.json"]!);
76
+ const codeNode = workflow.nodes.find((n: { type: string }) => n.type === "n8n-nodes-base.code");
77
+ expect(codeNode.parameters.jsCode).toContain("Redis");
78
+ expect(codeNode.parameters.jsCode).toContain("Postgresql");
79
+ });
80
+ });
@@ -0,0 +1,42 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { generateOpenclawInstallScript } from "./openclaw-install-script.js";
3
+
4
+ describe("generateOpenclawInstallScript", () => {
5
+ it("generates both sh and ps1 scripts", () => {
6
+ const files = generateOpenclawInstallScript({ projectName: "test-project" });
7
+ expect(files).toHaveProperty("scripts/install-openclaw.sh");
8
+ expect(files).toHaveProperty("scripts/install-openclaw.ps1");
9
+ });
10
+
11
+ it("sh script starts with shebang", () => {
12
+ const files = generateOpenclawInstallScript({ projectName: "test" });
13
+ expect(files["scripts/install-openclaw.sh"]).toMatch(/^#!/);
14
+ });
15
+
16
+ it("sh script uses set -euo pipefail", () => {
17
+ const files = generateOpenclawInstallScript({ projectName: "test" });
18
+ expect(files["scripts/install-openclaw.sh"]).toContain("set -euo pipefail");
19
+ });
20
+
21
+ it("includes project name in scripts", () => {
22
+ const files = generateOpenclawInstallScript({ projectName: "my-project" });
23
+ expect(files["scripts/install-openclaw.sh"]).toContain("my-project");
24
+ expect(files["scripts/install-openclaw.ps1"]).toContain("my-project");
25
+ });
26
+
27
+ it("uses official installer URL", () => {
28
+ const files = generateOpenclawInstallScript({ projectName: "test" });
29
+ expect(files["scripts/install-openclaw.sh"]).toContain("openclaw.ai/install.sh");
30
+ });
31
+
32
+ it("ps1 script checks for WSL", () => {
33
+ const files = generateOpenclawInstallScript({ projectName: "test" });
34
+ expect(files["scripts/install-openclaw.ps1"]).toContain("wsl");
35
+ });
36
+
37
+ it("includes next steps in output", () => {
38
+ const files = generateOpenclawInstallScript({ projectName: "test" });
39
+ expect(files["scripts/install-openclaw.sh"]).toContain("Next steps");
40
+ expect(files["scripts/install-openclaw.ps1"]).toContain("Next steps");
41
+ });
42
+ });
@@ -0,0 +1,116 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { ResolverOutput } from "../types.js";
3
+ import { generatePostgresInit, getDbRequirements } from "./postgres-init.js";
4
+
5
+ function makeResolved(serviceIds: string[]): ResolverOutput {
6
+ return {
7
+ services: serviceIds.map((id) => ({
8
+ definition: {
9
+ id,
10
+ name: id.charAt(0).toUpperCase() + id.slice(1),
11
+ description: "",
12
+ icon: "",
13
+ category: "test",
14
+ image: `${id}:latest`,
15
+ ports: [],
16
+ volumes: [],
17
+ environment: [],
18
+ dependencies: [],
19
+ conflicts: [],
20
+ skills: [],
21
+ memoryMB: 256,
22
+ docsUrl: "",
23
+ },
24
+ addedBy: "user" as const,
25
+ })),
26
+ addedDependencies: [],
27
+ estimatedMemoryMB: 512,
28
+ } as unknown as ResolverOutput;
29
+ }
30
+
31
+ describe("getDbRequirements", () => {
32
+ it("returns empty array when no services need a DB", () => {
33
+ const resolved = makeResolved(["redis", "minio"]);
34
+ expect(getDbRequirements(resolved)).toEqual([]);
35
+ });
36
+
37
+ it("returns requirements for n8n", () => {
38
+ const resolved = makeResolved(["n8n", "redis"]);
39
+ const reqs = getDbRequirements(resolved);
40
+ expect(reqs).toHaveLength(1);
41
+ expect(reqs[0]).toEqual({
42
+ serviceId: "n8n",
43
+ serviceName: "N8n",
44
+ dbName: "n8n",
45
+ dbUser: "n8n",
46
+ passwordEnvVar: "N8N_DB_PASSWORD",
47
+ });
48
+ });
49
+
50
+ it("returns requirements for multiple services", () => {
51
+ const resolved = makeResolved(["n8n", "outline", "postiz", "redis"]);
52
+ const reqs = getDbRequirements(resolved);
53
+ expect(reqs).toHaveLength(3);
54
+ const ids = reqs.map((r) => r.serviceId);
55
+ expect(ids).toContain("n8n");
56
+ expect(ids).toContain("outline");
57
+ expect(ids).toContain("postiz");
58
+ });
59
+ });
60
+
61
+ describe("generatePostgresInit", () => {
62
+ it("returns null when PostgreSQL is not in the stack", () => {
63
+ const resolved = makeResolved(["n8n", "redis"]);
64
+ expect(generatePostgresInit(resolved)).toBeNull();
65
+ });
66
+
67
+ it("returns null when PostgreSQL is present but no services need DBs", () => {
68
+ const resolved = makeResolved(["postgresql", "redis", "minio"]);
69
+ expect(generatePostgresInit(resolved)).toBeNull();
70
+ });
71
+
72
+ it("generates init script when PostgreSQL and DB-needing services are present", () => {
73
+ const resolved = makeResolved(["postgresql", "n8n"]);
74
+ const script = generatePostgresInit(resolved);
75
+ expect(script).not.toBeNull();
76
+ expect(script).toContain("#!/bin/bash");
77
+ expect(script).toContain("create_db_and_user");
78
+ expect(script).toContain('"n8n"');
79
+ expect(script).toContain("N8N_DB_PASSWORD");
80
+ });
81
+
82
+ it("includes all services that need DBs", () => {
83
+ const resolved = makeResolved(["postgresql", "n8n", "outline", "dify"]);
84
+ const script = generatePostgresInit(resolved)!;
85
+ expect(script).toContain('"n8n"');
86
+ expect(script).toContain('"outline"');
87
+ expect(script).toContain('"dify"');
88
+ });
89
+
90
+ it("uses environment variable substitution for passwords (not literals)", () => {
91
+ const resolved = makeResolved(["postgresql", "n8n"]);
92
+ const script = generatePostgresInit(resolved)!;
93
+ // Passwords should be referenced as env vars, not hardcoded
94
+ expect(script).toContain("${N8N_DB_PASSWORD:-$POSTGRES_PASSWORD}");
95
+ });
96
+
97
+ it("script uses psql with ON_ERROR_STOP", () => {
98
+ const resolved = makeResolved(["postgresql", "n8n"]);
99
+ const script = generatePostgresInit(resolved)!;
100
+ expect(script).toContain("ON_ERROR_STOP=1");
101
+ });
102
+
103
+ it("script creates user with IF NOT EXISTS check", () => {
104
+ const resolved = makeResolved(["postgresql", "n8n"]);
105
+ const script = generatePostgresInit(resolved)!;
106
+ expect(script).toContain("IF NOT EXISTS");
107
+ expect(script).toContain("CREATE ROLE");
108
+ });
109
+
110
+ it("includes service names in summary line", () => {
111
+ const resolved = makeResolved(["postgresql", "n8n", "outline"]);
112
+ const script = generatePostgresInit(resolved)!;
113
+ expect(script).toContain("N8n");
114
+ expect(script).toContain("Outline");
115
+ });
116
+ });
@@ -0,0 +1,108 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { parse } from "yaml";
3
+ import { generate } from "../generate.js";
4
+ import type { ResolverOutput } from "../types.js";
5
+ import { generatePrometheusConfig } from "./prometheus.js";
6
+
7
+ function makeResolved(serviceIds: string[]): ResolverOutput {
8
+ return {
9
+ services: serviceIds.map((id) => ({
10
+ definition: {
11
+ id,
12
+ name: id.charAt(0).toUpperCase() + id.slice(1),
13
+ description: "",
14
+ icon: "📦",
15
+ category: "test",
16
+ image: `${id}:latest`,
17
+ ports: [],
18
+ volumes: [],
19
+ environment: [],
20
+ dependencies: [],
21
+ conflicts: [],
22
+ skills: [],
23
+ memoryMB: 256,
24
+ docsUrl: "",
25
+ },
26
+ addedBy: "user" as const,
27
+ })),
28
+ addedDependencies: [],
29
+ estimatedMemoryMB: 512,
30
+ } as unknown as ResolverOutput;
31
+ }
32
+
33
+ describe("generatePrometheusConfig", () => {
34
+ it("always includes prometheus self-monitoring", () => {
35
+ const config = generatePrometheusConfig(makeResolved(["redis"]));
36
+ expect(config).toContain('job_name: "prometheus"');
37
+ expect(config).toContain("localhost:9090");
38
+ });
39
+
40
+ it("includes scrape config for redis", () => {
41
+ const config = generatePrometheusConfig(makeResolved(["redis"]));
42
+ expect(config).toContain('job_name: "redis"');
43
+ expect(config).toContain("redis:9121");
44
+ });
45
+
46
+ it("includes scrape config for postgresql", () => {
47
+ const config = generatePrometheusConfig(makeResolved(["postgresql"]));
48
+ expect(config).toContain('job_name: "postgresql"');
49
+ expect(config).toContain("postgresql:9187");
50
+ });
51
+
52
+ it("includes scrape configs for multiple services", () => {
53
+ const config = generatePrometheusConfig(makeResolved(["redis", "n8n", "minio"]));
54
+ expect(config).toContain('job_name: "redis"');
55
+ expect(config).toContain('job_name: "n8n"');
56
+ expect(config).toContain('job_name: "minio"');
57
+ });
58
+
59
+ it("skips services without known metrics endpoints", () => {
60
+ const config = generatePrometheusConfig(makeResolved(["ffmpeg"]));
61
+ expect(config).not.toContain('job_name: "ffmpeg"');
62
+ });
63
+
64
+ it("does not duplicate prometheus job", () => {
65
+ const config = generatePrometheusConfig(makeResolved(["prometheus", "redis"]));
66
+ const matches = config.match(/job_name: "prometheus"/g);
67
+ expect(matches).toHaveLength(1);
68
+ });
69
+
70
+ it("always includes openclaw gateway metrics", () => {
71
+ const config = generatePrometheusConfig(makeResolved(["redis"]));
72
+ expect(config).toContain('job_name: "openclaw-gateway"');
73
+ expect(config).toContain("openclaw:18789");
74
+ });
75
+
76
+ it("is valid YAML", () => {
77
+ const config = generatePrometheusConfig(makeResolved(["redis", "postgresql"]));
78
+ const parsed = parse(config);
79
+ expect(parsed).toBeDefined();
80
+ expect(parsed.global).toBeDefined();
81
+ expect(parsed.scrape_configs).toBeInstanceOf(Array);
82
+ });
83
+
84
+ it("sets correct metrics paths per service", () => {
85
+ const config = generatePrometheusConfig(makeResolved(["minio"]));
86
+ expect(config).toContain("/minio/v2/metrics/cluster");
87
+ });
88
+ });
89
+
90
+ describe("prometheus via generate()", () => {
91
+ it("generates prometheus config when monitoring is enabled", () => {
92
+ const result = generate({
93
+ projectName: "prom-test",
94
+ services: ["redis"],
95
+ skillPacks: [],
96
+ proxy: "none",
97
+ gpu: false,
98
+ platform: "linux/amd64",
99
+ deployment: "local",
100
+ generateSecrets: true,
101
+ openclawVersion: "latest",
102
+ monitoring: true,
103
+ });
104
+ expect(result.files).toHaveProperty("prometheus/prometheus.yml");
105
+ const promConfig = parse(result.files["prometheus/prometheus.yml"]!);
106
+ expect(promConfig.scrape_configs.length).toBeGreaterThan(0);
107
+ });
108
+ });