@openpalm/lib 0.9.8 → 0.10.1
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 +31 -71
- package/package.json +1 -1
- package/src/control-plane/audit.ts +4 -4
- package/src/control-plane/backup.ts +31 -0
- package/src/control-plane/channels.ts +88 -156
- package/src/control-plane/cleanup-guardrails.test.ts +289 -0
- package/src/control-plane/compose-args.test.ts +170 -0
- package/src/control-plane/compose-args.ts +57 -0
- package/src/control-plane/config-persistence.ts +270 -0
- package/src/control-plane/core-assets.ts +58 -234
- package/src/control-plane/crypto.ts +14 -0
- package/src/control-plane/docker.ts +94 -204
- package/src/control-plane/env-schema-validation.test.ts +118 -0
- package/src/control-plane/extends-support.test.ts +105 -0
- package/src/control-plane/home.ts +133 -0
- package/src/control-plane/install-edge-cases.test.ts +314 -717
- package/src/control-plane/lifecycle.ts +215 -233
- package/src/control-plane/lock.test.ts +194 -0
- package/src/control-plane/lock.ts +176 -0
- package/src/control-plane/memory-config.ts +34 -160
- package/src/control-plane/opencode-client.test.ts +154 -0
- package/src/control-plane/opencode-client.ts +113 -0
- package/src/control-plane/provider-config.ts +34 -0
- package/src/control-plane/redact-schema.ts +50 -0
- package/src/control-plane/registry-components.test.ts +313 -0
- package/src/control-plane/registry.test.ts +414 -0
- package/src/control-plane/registry.ts +418 -0
- package/src/control-plane/rollback.ts +128 -0
- package/src/control-plane/scheduler.ts +18 -190
- package/src/control-plane/secret-backend.test.ts +359 -0
- package/src/control-plane/secret-backend.ts +322 -0
- package/src/control-plane/secret-mappings.ts +185 -0
- package/src/control-plane/secrets.ts +186 -112
- package/src/control-plane/setup-config.schema.json +306 -0
- package/src/control-plane/setup-status.ts +15 -8
- package/src/control-plane/setup-validation.ts +90 -0
- package/src/control-plane/setup.test.ts +336 -929
- package/src/control-plane/setup.ts +159 -849
- package/src/control-plane/spec-to-env.test.ts +100 -0
- package/src/control-plane/spec-to-env.ts +195 -0
- package/src/control-plane/spec-validator.ts +159 -0
- package/src/control-plane/stack-spec.test.ts +150 -0
- package/src/control-plane/stack-spec.ts +101 -22
- package/src/control-plane/types.ts +6 -99
- package/src/control-plane/validate.ts +107 -0
- package/src/index.ts +101 -159
- package/src/provider-constants.ts +2 -31
- package/src/control-plane/connection-mapping.ts +0 -191
- package/src/control-plane/connection-migration-flags.ts +0 -40
- package/src/control-plane/connection-profiles.ts +0 -317
- package/src/control-plane/core-asset-provider.ts +0 -21
- package/src/control-plane/fs-asset-provider.ts +0 -65
- package/src/control-plane/fs-registry-provider.ts +0 -46
- package/src/control-plane/paths.ts +0 -77
- package/src/control-plane/registry-provider.ts +0 -19
- package/src/control-plane/staging.ts +0 -399
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for registry sync functions.
|
|
3
|
+
*
|
|
4
|
+
* Tests validation, discovery, and materialization.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, it, beforeEach, afterEach } from "bun:test";
|
|
7
|
+
import { existsSync, mkdirSync, writeFileSync, rmSync, readFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { backupOpenPalmHome } from "./backup.js";
|
|
11
|
+
import {
|
|
12
|
+
validateBranch,
|
|
13
|
+
validateRegistryUrl,
|
|
14
|
+
isValidComponentName,
|
|
15
|
+
getRegistryConfig,
|
|
16
|
+
materializeRegistryCatalog,
|
|
17
|
+
verifyRegistryCatalog,
|
|
18
|
+
discoverRegistryComponents,
|
|
19
|
+
discoverRegistryAutomations,
|
|
20
|
+
getRegistryAutomation,
|
|
21
|
+
getRegistryAddonConfig,
|
|
22
|
+
listAvailableAddonIds,
|
|
23
|
+
getAddonServiceNames,
|
|
24
|
+
enableAddon,
|
|
25
|
+
disableAddonByName,
|
|
26
|
+
setAddonEnabled,
|
|
27
|
+
installAutomationFromRegistry,
|
|
28
|
+
uninstallAutomation,
|
|
29
|
+
} from "./registry.js";
|
|
30
|
+
|
|
31
|
+
// ── Validation Tests ─────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
describe("validateBranch", () => {
|
|
34
|
+
it("accepts 'main'", () => {
|
|
35
|
+
expect(validateBranch("main")).toBe("main");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("accepts 'feat/my-branch'", () => {
|
|
39
|
+
expect(validateBranch("feat/my-branch")).toBe("feat/my-branch");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("accepts branch with dots and underscores", () => {
|
|
43
|
+
expect(validateBranch("release_1.0.0")).toBe("release_1.0.0");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("rejects branch with '..'", () => {
|
|
47
|
+
expect(() => validateBranch("main/../hack")).toThrow("contains '..'");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("rejects branch with spaces", () => {
|
|
51
|
+
expect(() => validateBranch("my branch")).toThrow("Invalid registry branch name");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("rejects empty string", () => {
|
|
55
|
+
expect(() => validateBranch("")).toThrow("Invalid registry branch name");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("rejects branch with shell metacharacters", () => {
|
|
59
|
+
expect(() => validateBranch("main;rm -rf /")).toThrow("Invalid registry branch name");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("rejects branch with backticks", () => {
|
|
63
|
+
expect(() => validateBranch("`whoami`")).toThrow("Invalid registry branch name");
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("validateRegistryUrl", () => {
|
|
68
|
+
it("accepts https:// URLs", () => {
|
|
69
|
+
expect(validateRegistryUrl("https://github.com/org/repo.git")).toBe(
|
|
70
|
+
"https://github.com/org/repo.git"
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("accepts git@ URLs", () => {
|
|
75
|
+
expect(validateRegistryUrl("git@github.com:org/repo.git")).toBe(
|
|
76
|
+
"git@github.com:org/repo.git"
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("accepts absolute local paths", () => {
|
|
81
|
+
expect(validateRegistryUrl("/tmp/openpalm-registry")).toBe("/tmp/openpalm-registry");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("rejects http:// URLs", () => {
|
|
85
|
+
expect(() => validateRegistryUrl("http://github.com/repo.git")).toThrow(
|
|
86
|
+
"Invalid registry URL"
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("rejects file:// URLs", () => {
|
|
91
|
+
expect(() => validateRegistryUrl("file:///etc/passwd")).toThrow("Invalid registry URL");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("rejects empty string", () => {
|
|
95
|
+
expect(() => validateRegistryUrl("")).toThrow("Invalid registry URL");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("rejects arbitrary strings", () => {
|
|
99
|
+
expect(() => validateRegistryUrl("not-a-url")).toThrow("Invalid registry URL");
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("isValidComponentName", () => {
|
|
104
|
+
it("accepts lowercase alpha names", () => {
|
|
105
|
+
expect(isValidComponentName("chat")).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("accepts names with hyphens", () => {
|
|
109
|
+
expect(isValidComponentName("my-channel")).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("accepts names with digits", () => {
|
|
113
|
+
expect(isValidComponentName("channel2")).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("rejects uppercase", () => {
|
|
117
|
+
expect(isValidComponentName("MyChannel")).toBe(false);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("rejects names starting with hyphen", () => {
|
|
121
|
+
expect(isValidComponentName("-bad")).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("rejects empty string", () => {
|
|
125
|
+
expect(isValidComponentName("")).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("rejects names with dots", () => {
|
|
129
|
+
expect(isValidComponentName("my.channel")).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("rejects names longer than 63 chars", () => {
|
|
133
|
+
expect(isValidComponentName("a".repeat(64))).toBe(false);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("accepts names exactly 63 chars", () => {
|
|
137
|
+
expect(isValidComponentName("a".repeat(63))).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe("getRegistryConfig", () => {
|
|
142
|
+
const origUrl = process.env.OP_REGISTRY_URL;
|
|
143
|
+
const origBranch = process.env.OP_REGISTRY_BRANCH;
|
|
144
|
+
|
|
145
|
+
afterEach(() => {
|
|
146
|
+
if (origUrl === undefined) delete process.env.OP_REGISTRY_URL;
|
|
147
|
+
else process.env.OP_REGISTRY_URL = origUrl;
|
|
148
|
+
if (origBranch === undefined) delete process.env.OP_REGISTRY_BRANCH;
|
|
149
|
+
else process.env.OP_REGISTRY_BRANCH = origBranch;
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("returns defaults when env vars are unset", () => {
|
|
153
|
+
delete process.env.OP_REGISTRY_URL;
|
|
154
|
+
delete process.env.OP_REGISTRY_BRANCH;
|
|
155
|
+
const config = getRegistryConfig();
|
|
156
|
+
expect(config.repoUrl).toContain("github.com");
|
|
157
|
+
expect(config.branch).toBe("main");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("respects OP_REGISTRY_URL", () => {
|
|
161
|
+
process.env.OP_REGISTRY_URL = "https://github.com/custom/repo.git";
|
|
162
|
+
const config = getRegistryConfig();
|
|
163
|
+
expect(config.repoUrl).toBe("https://github.com/custom/repo.git");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("respects OP_REGISTRY_BRANCH", () => {
|
|
167
|
+
process.env.OP_REGISTRY_BRANCH = "develop";
|
|
168
|
+
const config = getRegistryConfig();
|
|
169
|
+
expect(config.branch).toBe("develop");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("throws on invalid branch in env", () => {
|
|
173
|
+
process.env.OP_REGISTRY_BRANCH = "main;exploit";
|
|
174
|
+
expect(() => getRegistryConfig()).toThrow("Invalid registry branch name");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("throws on invalid URL in env", () => {
|
|
178
|
+
process.env.OP_REGISTRY_URL = "ftp://bad.com/repo";
|
|
179
|
+
expect(() => getRegistryConfig()).toThrow("Invalid registry URL");
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// ── Materialized Catalog Tests ───────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
describe("materialized registry catalog", () => {
|
|
186
|
+
let tmpDir: string;
|
|
187
|
+
let originalHome: string | undefined;
|
|
188
|
+
|
|
189
|
+
beforeEach(() => {
|
|
190
|
+
tmpDir = join(tmpdir(), `registry-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
191
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
192
|
+
originalHome = process.env.OP_HOME;
|
|
193
|
+
process.env.OP_HOME = join(tmpDir, 'home');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
afterEach(() => {
|
|
197
|
+
if (originalHome === undefined) delete process.env.OP_HOME;
|
|
198
|
+
else process.env.OP_HOME = originalHome;
|
|
199
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("materializes addons and automations into OP_HOME/registry", () => {
|
|
203
|
+
const sourceRoot = join(tmpDir, 'repo');
|
|
204
|
+
const addonDir = join(sourceRoot, '.openpalm', 'registry', 'addons', 'chat');
|
|
205
|
+
const automationsDir = join(sourceRoot, '.openpalm', 'registry', 'automations');
|
|
206
|
+
|
|
207
|
+
mkdirSync(addonDir, { recursive: true });
|
|
208
|
+
mkdirSync(automationsDir, { recursive: true });
|
|
209
|
+
writeFileSync(join(addonDir, 'compose.yml'), 'services: {}\n');
|
|
210
|
+
writeFileSync(join(addonDir, '.env.schema'), 'CHANNEL_CHAT_SECRET=\n');
|
|
211
|
+
writeFileSync(join(automationsDir, 'cleanup.yml'), 'description: Cleanup\nschedule: daily\n');
|
|
212
|
+
|
|
213
|
+
const root = materializeRegistryCatalog(sourceRoot);
|
|
214
|
+
|
|
215
|
+
expect(root).toBe(join(process.env.OP_HOME!, 'registry'));
|
|
216
|
+
expect(existsSync(join(root, 'addons', 'chat', 'compose.yml'))).toBe(true);
|
|
217
|
+
expect(existsSync(join(root, 'addons', 'chat', '.env.schema'))).toBe(true);
|
|
218
|
+
expect(readFileSync(join(root, 'automations', 'cleanup.yml'), 'utf-8')).toContain('Cleanup');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("discovers materialized registry entries", () => {
|
|
222
|
+
const sourceRoot = join(tmpDir, 'repo');
|
|
223
|
+
const addonDir = join(sourceRoot, '.openpalm', 'registry', 'addons', 'chat');
|
|
224
|
+
const automationsDir = join(sourceRoot, '.openpalm', 'registry', 'automations');
|
|
225
|
+
|
|
226
|
+
mkdirSync(addonDir, { recursive: true });
|
|
227
|
+
mkdirSync(automationsDir, { recursive: true });
|
|
228
|
+
writeFileSync(join(addonDir, 'compose.yml'), 'services: {}\n');
|
|
229
|
+
writeFileSync(join(addonDir, '.env.schema'), 'CHANNEL_CHAT_SECRET=\n');
|
|
230
|
+
writeFileSync(join(automationsDir, 'cleanup.yml'), 'description: Cleanup\nschedule: daily\n');
|
|
231
|
+
|
|
232
|
+
materializeRegistryCatalog(sourceRoot);
|
|
233
|
+
|
|
234
|
+
const components = discoverRegistryComponents();
|
|
235
|
+
const automations = discoverRegistryAutomations();
|
|
236
|
+
|
|
237
|
+
expect(Object.keys(components)).toEqual(['chat']);
|
|
238
|
+
expect(components.chat?.schema).toContain('CHANNEL_CHAT_SECRET');
|
|
239
|
+
expect(automations.map((entry) => entry.name)).toEqual(['cleanup']);
|
|
240
|
+
expect(getRegistryAutomation('cleanup')).toContain('schedule: daily');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("returns addon config metadata from the materialized registry", () => {
|
|
244
|
+
const sourceRoot = join(tmpDir, 'repo');
|
|
245
|
+
const addonDir = join(sourceRoot, '.openpalm', 'registry', 'addons', 'chat');
|
|
246
|
+
const automationsDir = join(sourceRoot, '.openpalm', 'registry', 'automations');
|
|
247
|
+
|
|
248
|
+
mkdirSync(addonDir, { recursive: true });
|
|
249
|
+
mkdirSync(automationsDir, { recursive: true });
|
|
250
|
+
writeFileSync(join(addonDir, 'compose.yml'), 'services: {}\n');
|
|
251
|
+
writeFileSync(join(addonDir, '.env.schema'), 'CHANNEL_CHAT_SECRET=\n');
|
|
252
|
+
writeFileSync(join(automationsDir, 'cleanup.yml'), 'description: Cleanup\nschedule: daily\n');
|
|
253
|
+
|
|
254
|
+
materializeRegistryCatalog(sourceRoot);
|
|
255
|
+
|
|
256
|
+
expect(getRegistryAddonConfig(process.env.OP_HOME!, 'chat')).toEqual({
|
|
257
|
+
schemaPath: 'registry/addons/chat/.env.schema',
|
|
258
|
+
userEnvPath: 'vault/user/user.env',
|
|
259
|
+
envSchema: 'CHANNEL_CHAT_SECRET=\n',
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("verifies the materialized registry and returns counts", () => {
|
|
264
|
+
const sourceRoot = join(tmpDir, 'repo');
|
|
265
|
+
const addonDir = join(sourceRoot, '.openpalm', 'registry', 'addons', 'chat');
|
|
266
|
+
const automationsDir = join(sourceRoot, '.openpalm', 'registry', 'automations');
|
|
267
|
+
|
|
268
|
+
mkdirSync(addonDir, { recursive: true });
|
|
269
|
+
mkdirSync(automationsDir, { recursive: true });
|
|
270
|
+
writeFileSync(join(addonDir, 'compose.yml'), 'services: {}\n');
|
|
271
|
+
writeFileSync(join(addonDir, '.env.schema'), 'CHANNEL_CHAT_SECRET=\n');
|
|
272
|
+
writeFileSync(join(automationsDir, 'cleanup.yml'), 'description: Cleanup\nschedule: daily\n');
|
|
273
|
+
|
|
274
|
+
const root = materializeRegistryCatalog(sourceRoot);
|
|
275
|
+
|
|
276
|
+
expect(verifyRegistryCatalog(root)).toEqual({
|
|
277
|
+
root,
|
|
278
|
+
addonCount: 1,
|
|
279
|
+
automationCount: 1,
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("returns no available addons when the registry addons directory is missing", () => {
|
|
284
|
+
expect(listAvailableAddonIds()).toEqual([]);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("fails when source catalog is incomplete", () => {
|
|
288
|
+
const sourceRoot = join(tmpDir, 'repo');
|
|
289
|
+
mkdirSync(join(sourceRoot, '.openpalm', 'registry', 'addons'), { recursive: true });
|
|
290
|
+
mkdirSync(join(sourceRoot, '.openpalm', 'registry', 'automations'), { recursive: true });
|
|
291
|
+
|
|
292
|
+
expect(() => materializeRegistryCatalog(sourceRoot)).toThrow('Registry catalog is incomplete');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("enables and disables addons through the runtime stack directory", () => {
|
|
296
|
+
const sourceRoot = join(tmpDir, 'repo');
|
|
297
|
+
const addonDir = join(sourceRoot, '.openpalm', 'registry', 'addons', 'chat');
|
|
298
|
+
const automationsDir = join(sourceRoot, '.openpalm', 'registry', 'automations');
|
|
299
|
+
|
|
300
|
+
mkdirSync(addonDir, { recursive: true });
|
|
301
|
+
mkdirSync(automationsDir, { recursive: true });
|
|
302
|
+
writeFileSync(join(addonDir, 'compose.yml'), 'services: {}\n');
|
|
303
|
+
writeFileSync(join(addonDir, '.env.schema'), 'CHANNEL_CHAT_SECRET=\n');
|
|
304
|
+
writeFileSync(join(automationsDir, 'cleanup.yml'), 'description: Cleanup\nschedule: daily\n');
|
|
305
|
+
|
|
306
|
+
materializeRegistryCatalog(sourceRoot);
|
|
307
|
+
|
|
308
|
+
expect(enableAddon(process.env.OP_HOME!, 'chat')).toEqual({ ok: true });
|
|
309
|
+
expect(existsSync(join(process.env.OP_HOME!, 'stack', 'addons', 'chat', 'compose.yml'))).toBe(true);
|
|
310
|
+
|
|
311
|
+
expect(disableAddonByName(process.env.OP_HOME!, 'chat')).toEqual({ ok: true });
|
|
312
|
+
expect(existsSync(join(process.env.OP_HOME!, 'stack', 'addons', 'chat'))).toBe(false);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("returns addon service names from stack or registry compose files", () => {
|
|
316
|
+
const sourceRoot = join(tmpDir, 'repo');
|
|
317
|
+
const addonDir = join(sourceRoot, '.openpalm', 'registry', 'addons', 'admin');
|
|
318
|
+
const automationsDir = join(sourceRoot, '.openpalm', 'registry', 'automations');
|
|
319
|
+
|
|
320
|
+
mkdirSync(addonDir, { recursive: true });
|
|
321
|
+
mkdirSync(automationsDir, { recursive: true });
|
|
322
|
+
writeFileSync(join(addonDir, 'compose.yml'), 'services:\n docker-socket-proxy:\n image: proxy\n admin:\n image: admin\n');
|
|
323
|
+
writeFileSync(join(addonDir, '.env.schema'), 'OP_ADMIN_TOKEN=\n');
|
|
324
|
+
writeFileSync(join(automationsDir, 'cleanup.yml'), 'description: Cleanup\nschedule: daily\n');
|
|
325
|
+
|
|
326
|
+
materializeRegistryCatalog(sourceRoot);
|
|
327
|
+
|
|
328
|
+
expect(getAddonServiceNames(process.env.OP_HOME!, 'admin')).toEqual(['docker-socket-proxy', 'admin']);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it("toggles addons and generates channel secrets when enabling channel addons", () => {
|
|
332
|
+
const sourceRoot = join(tmpDir, 'repo');
|
|
333
|
+
const addonDir = join(sourceRoot, '.openpalm', 'registry', 'addons', 'chat');
|
|
334
|
+
const automationsDir = join(sourceRoot, '.openpalm', 'registry', 'automations');
|
|
335
|
+
|
|
336
|
+
mkdirSync(addonDir, { recursive: true });
|
|
337
|
+
mkdirSync(automationsDir, { recursive: true });
|
|
338
|
+
writeFileSync(join(addonDir, 'compose.yml'), 'services:\n chat:\n image: test\n environment:\n CHANNEL_NAME: "Chat"\n CHANNEL_ID: "chat"\n');
|
|
339
|
+
writeFileSync(join(addonDir, '.env.schema'), 'CHANNEL_CHAT_SECRET=\n');
|
|
340
|
+
writeFileSync(join(automationsDir, 'cleanup.yml'), 'description: Cleanup\nschedule: daily\n');
|
|
341
|
+
|
|
342
|
+
materializeRegistryCatalog(sourceRoot);
|
|
343
|
+
|
|
344
|
+
expect(setAddonEnabled(process.env.OP_HOME!, join(process.env.OP_HOME!, 'vault'), 'chat', true)).toEqual({
|
|
345
|
+
ok: true,
|
|
346
|
+
enabled: true,
|
|
347
|
+
changed: true,
|
|
348
|
+
services: ['chat'],
|
|
349
|
+
});
|
|
350
|
+
expect(existsSync(join(process.env.OP_HOME!, 'stack', 'addons', 'chat', 'compose.yml'))).toBe(true);
|
|
351
|
+
expect(readFileSync(join(process.env.OP_HOME!, 'vault', 'stack', 'guardian.env'), 'utf-8')).toMatch(/CHANNEL_CHAT_SECRET=/);
|
|
352
|
+
|
|
353
|
+
expect(setAddonEnabled(process.env.OP_HOME!, join(process.env.OP_HOME!, 'vault'), 'chat', false)).toEqual({
|
|
354
|
+
ok: true,
|
|
355
|
+
enabled: false,
|
|
356
|
+
changed: true,
|
|
357
|
+
services: ['chat'],
|
|
358
|
+
});
|
|
359
|
+
expect(existsSync(join(process.env.OP_HOME!, 'stack', 'addons', 'chat'))).toBe(false);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("backs up OP_HOME without recursively copying backups", () => {
|
|
363
|
+
mkdirSync(join(process.env.OP_HOME!, 'config'), { recursive: true });
|
|
364
|
+
mkdirSync(join(process.env.OP_HOME!, 'backups', 'old-backup'), { recursive: true });
|
|
365
|
+
writeFileSync(join(process.env.OP_HOME!, 'config', 'stack.yml'), 'llm: test\n');
|
|
366
|
+
writeFileSync(join(process.env.OP_HOME!, 'backups', 'old-backup', 'marker.txt'), 'old\n');
|
|
367
|
+
|
|
368
|
+
const backupDir = backupOpenPalmHome(process.env.OP_HOME!);
|
|
369
|
+
|
|
370
|
+
expect(backupDir).not.toBeNull();
|
|
371
|
+
expect(existsSync(join(backupDir!, 'config', 'stack.yml'))).toBe(true);
|
|
372
|
+
expect(existsSync(join(backupDir!, 'backups'))).toBe(false);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("writes backups under the provided homeDir even when OP_HOME points elsewhere", () => {
|
|
376
|
+
const actualHome = join(tmpDir, 'actual-home');
|
|
377
|
+
const otherHome = join(tmpDir, 'other-home');
|
|
378
|
+
|
|
379
|
+
mkdirSync(join(actualHome, 'config'), { recursive: true });
|
|
380
|
+
mkdirSync(join(otherHome, 'backups'), { recursive: true });
|
|
381
|
+
writeFileSync(join(actualHome, 'config', 'stack.yml'), 'llm: local\n');
|
|
382
|
+
|
|
383
|
+
process.env.OP_HOME = otherHome;
|
|
384
|
+
|
|
385
|
+
const backupDir = backupOpenPalmHome(actualHome);
|
|
386
|
+
|
|
387
|
+
expect(backupDir).not.toBeNull();
|
|
388
|
+
expect(backupDir!.startsWith(join(actualHome, 'backups'))).toBe(true);
|
|
389
|
+
expect(existsSync(join(backupDir!, 'config', 'stack.yml'))).toBe(true);
|
|
390
|
+
expect(existsSync(join(otherHome, 'backups', 'config', 'stack.yml'))).toBe(false);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it("installs and uninstalls automations through config/automations", () => {
|
|
394
|
+
const sourceRoot = join(tmpDir, 'repo');
|
|
395
|
+
const addonDir = join(sourceRoot, '.openpalm', 'registry', 'addons', 'chat');
|
|
396
|
+
const automationsDir = join(sourceRoot, '.openpalm', 'registry', 'automations');
|
|
397
|
+
const configDir = join(process.env.OP_HOME!, 'config');
|
|
398
|
+
|
|
399
|
+
mkdirSync(addonDir, { recursive: true });
|
|
400
|
+
mkdirSync(automationsDir, { recursive: true });
|
|
401
|
+
mkdirSync(configDir, { recursive: true });
|
|
402
|
+
writeFileSync(join(addonDir, 'compose.yml'), 'services: {}\n');
|
|
403
|
+
writeFileSync(join(addonDir, '.env.schema'), 'CHANNEL_CHAT_SECRET=\n');
|
|
404
|
+
writeFileSync(join(automationsDir, 'cleanup.yml'), 'description: Cleanup\nschedule: daily\n');
|
|
405
|
+
|
|
406
|
+
materializeRegistryCatalog(sourceRoot);
|
|
407
|
+
|
|
408
|
+
expect(installAutomationFromRegistry('cleanup', configDir)).toEqual({ ok: true });
|
|
409
|
+
expect(readFileSync(join(configDir, 'automations', 'cleanup.yml'), 'utf-8')).toContain('Cleanup');
|
|
410
|
+
|
|
411
|
+
expect(uninstallAutomation('cleanup', configDir)).toEqual({ ok: true });
|
|
412
|
+
expect(existsSync(join(configDir, 'automations', 'cleanup.yml'))).toBe(false);
|
|
413
|
+
});
|
|
414
|
+
});
|