@openpalm/lib 0.11.0-beta.9 → 0.11.0-rc.18
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 +1 -1
- package/package.json +1 -1
- package/src/control-plane/akm-sources.test.ts +206 -0
- package/src/control-plane/akm-sources.ts +234 -0
- package/src/control-plane/akm-user-env.test.ts +142 -0
- package/src/control-plane/akm-user-env.ts +167 -0
- package/src/control-plane/backup.ts +14 -5
- package/src/control-plane/channels.ts +48 -29
- package/src/control-plane/cleanup-guardrails.test.ts +1 -1
- package/src/control-plane/compose-args.test.ts +69 -30
- package/src/control-plane/compose-args.ts +62 -8
- package/src/control-plane/config-persistence.ts +102 -136
- package/src/control-plane/core-assets.ts +45 -60
- package/src/control-plane/defaults.ts +16 -0
- package/src/control-plane/docker.ts +15 -14
- package/src/control-plane/env.test.ts +10 -10
- package/src/control-plane/env.ts +16 -1
- package/src/control-plane/extends-support.test.ts +8 -8
- package/src/control-plane/fs-atomic.ts +15 -0
- package/src/control-plane/home.ts +34 -46
- package/src/control-plane/host-akm-sharing.test.ts +145 -0
- package/src/control-plane/host-akm-sharing.ts +129 -0
- package/src/control-plane/host-opencode.test.ts +82 -10
- package/src/control-plane/host-opencode.ts +42 -13
- package/src/control-plane/install-edge-cases.test.ts +100 -136
- package/src/control-plane/install-lock.ts +7 -7
- package/src/control-plane/lifecycle.ts +45 -40
- package/src/control-plane/markdown-task.ts +30 -50
- package/src/control-plane/migrations.test.ts +272 -0
- package/src/control-plane/migrations.ts +423 -0
- package/src/control-plane/opencode-client.ts +1 -1
- package/src/control-plane/paths.ts +61 -46
- package/src/control-plane/profile-ids.ts +21 -0
- package/src/control-plane/provider-models.ts +3 -3
- package/src/control-plane/registry.test.ts +107 -90
- package/src/control-plane/registry.ts +301 -110
- package/src/control-plane/rollback.ts +8 -38
- package/src/control-plane/scheduler.ts +10 -7
- package/src/control-plane/secret-audit.test.ts +159 -0
- package/src/control-plane/secret-audit.ts +255 -0
- package/src/control-plane/secret-mappings.ts +2 -2
- package/src/control-plane/secrets-files.test.ts +99 -0
- package/src/control-plane/secrets-files.ts +113 -0
- package/src/control-plane/secrets.ts +113 -86
- package/src/control-plane/setup-config.schema.json +1 -1
- package/src/control-plane/setup-status.ts +6 -11
- package/src/control-plane/setup.test.ts +137 -61
- package/src/control-plane/setup.ts +82 -63
- package/src/control-plane/skeleton-guardrail.test.ts +66 -56
- package/src/control-plane/spec-to-env.test.ts +63 -26
- package/src/control-plane/spec-to-env.ts +51 -14
- package/src/control-plane/task-files.test.ts +45 -0
- package/src/control-plane/task-files.ts +51 -0
- package/src/control-plane/types.ts +2 -4
- package/src/control-plane/ui-assets.test.ts +333 -0
- package/src/control-plane/ui-assets.ts +290 -142
- package/src/control-plane/validate.ts +13 -15
- package/src/index.ts +96 -26
- package/src/control-plane/akm-vault.test.ts +0 -105
- package/src/control-plane/akm-vault.ts +0 -311
- package/src/control-plane/core-assets.test.ts +0 -104
- package/src/control-plane/migrate-0110.test.ts +0 -177
- package/src/control-plane/migrate-0110.ts +0 -99
- package/src/control-plane/registry-components.test.ts +0 -391
- package/src/control-plane/stack-spec.test.ts +0 -94
- package/src/control-plane/stack-spec.ts +0 -67
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Edge-case tests for the OpenPalm install and setup flow.
|
|
3
3
|
*
|
|
4
4
|
* Each test creates its own temp directory tree mimicking the single
|
|
5
|
-
* ~/.openpalm/ root layout (config,
|
|
5
|
+
* ~/.openpalm/ root layout (config, knowledge, data, logs), then runs the
|
|
6
6
|
* actual library functions against it. No mocks of code under test.
|
|
7
7
|
*/
|
|
8
8
|
import { describe, expect, it, beforeEach, afterEach } from "bun:test";
|
|
@@ -23,11 +23,10 @@ import {
|
|
|
23
23
|
performSetup,
|
|
24
24
|
buildSecretsFromSetup,
|
|
25
25
|
buildAuthJsonFromSetup,
|
|
26
|
-
buildSystemSecretsFromSetup,
|
|
27
26
|
} from "./setup.js";
|
|
28
27
|
import type { SetupSpec, SetupConnection } from "./setup.js";
|
|
29
28
|
import type { ControlPlaneState } from "./types.js";
|
|
30
|
-
import {
|
|
29
|
+
import { readSecret } from './secrets-files.js';
|
|
31
30
|
|
|
32
31
|
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
33
32
|
|
|
@@ -55,70 +54,69 @@ function makeValidSpec(overrides?: Partial<SetupSpec>): SetupSpec {
|
|
|
55
54
|
function seedRequiredAssets(homeDir: string): void {
|
|
56
55
|
mkdirSync(join(homeDir, "config", "stack"), { recursive: true });
|
|
57
56
|
writeFileSync(join(homeDir, "config", "stack", "core.compose.yml"), "services:\n assistant:\n image: assistant:latest\n");
|
|
58
|
-
mkdirSync(join(homeDir, "
|
|
59
|
-
writeFileSync(join(homeDir, "
|
|
60
|
-
writeFileSync(join(homeDir, "
|
|
61
|
-
mkdirSync(join(homeDir, "
|
|
62
|
-
// Automations live in
|
|
63
|
-
mkdirSync(join(homeDir, "
|
|
64
|
-
writeFileSync(join(homeDir, "
|
|
65
|
-
writeFileSync(join(homeDir, "
|
|
66
|
-
writeFileSync(join(homeDir, "
|
|
57
|
+
mkdirSync(join(homeDir, "data", "assistant"), { recursive: true });
|
|
58
|
+
writeFileSync(join(homeDir, "data", "assistant", "opencode.jsonc"), '{"$schema":"https://opencode.ai/config.json"}\n');
|
|
59
|
+
writeFileSync(join(homeDir, "data", "assistant", "AGENTS.md"), "# Agents\n");
|
|
60
|
+
mkdirSync(join(homeDir, "data"), { recursive: true });
|
|
61
|
+
// Automations live in knowledge/tasks as AKM-owned task files.
|
|
62
|
+
mkdirSync(join(homeDir, "knowledge", "tasks"), { recursive: true });
|
|
63
|
+
writeFileSync(join(homeDir, "knowledge", "tasks", "cleanup-logs.yml"), "schedule: \"0 4 * * 0\"\ndescription: cleanup logs\ncommand: [\"echo\",\"clean\"]\n");
|
|
64
|
+
writeFileSync(join(homeDir, "knowledge", "tasks", "cleanup-data.yml"), "schedule: \"0 5 * * 0\"\ndescription: cleanup data\ncommand: [\"echo\",\"clean\"]\n");
|
|
65
|
+
writeFileSync(join(homeDir, "knowledge", "tasks", "validate-config.yml"), "schedule: \"0 3 * * *\"\ndescription: validate config\ncommand: [\"echo\",\"clean\"]\n");
|
|
67
66
|
}
|
|
68
67
|
|
|
69
68
|
// ── Shared test fixture ──────────────────────────────────────────────────
|
|
70
69
|
|
|
71
70
|
let homeDir: string;
|
|
72
71
|
let configDir: string;
|
|
73
|
-
let
|
|
72
|
+
let dataDir: string;
|
|
74
73
|
let stackDir: string;
|
|
75
|
-
let cacheDir: string;
|
|
76
74
|
|
|
77
75
|
const savedEnv: Record<string, string | undefined> = {};
|
|
78
76
|
|
|
79
77
|
function saveAndSetEnv(): void {
|
|
80
78
|
savedEnv.OP_HOME = process.env.OP_HOME;
|
|
79
|
+
savedEnv.OP_UI_LOGIN_PASSWORD = process.env.OP_UI_LOGIN_PASSWORD;
|
|
80
|
+
savedEnv.OP_OPENCODE_PASSWORD = process.env.OP_OPENCODE_PASSWORD;
|
|
81
81
|
process.env.OP_HOME = homeDir;
|
|
82
|
+
delete process.env.OP_UI_LOGIN_PASSWORD;
|
|
83
|
+
delete process.env.OP_OPENCODE_PASSWORD;
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
function restoreEnv(): void {
|
|
85
|
-
|
|
87
|
+
if (savedEnv.OP_HOME === undefined) delete process.env.OP_HOME;
|
|
88
|
+
else process.env.OP_HOME = savedEnv.OP_HOME;
|
|
89
|
+
if (savedEnv.OP_UI_LOGIN_PASSWORD === undefined) delete process.env.OP_UI_LOGIN_PASSWORD;
|
|
90
|
+
else process.env.OP_UI_LOGIN_PASSWORD = savedEnv.OP_UI_LOGIN_PASSWORD;
|
|
91
|
+
if (savedEnv.OP_OPENCODE_PASSWORD === undefined) delete process.env.OP_OPENCODE_PASSWORD;
|
|
92
|
+
else process.env.OP_OPENCODE_PASSWORD = savedEnv.OP_OPENCODE_PASSWORD;
|
|
86
93
|
}
|
|
87
94
|
|
|
88
95
|
/** Create a full directory tree matching ensureHomeDirs() output. */
|
|
89
96
|
function createFullDirTree(): void {
|
|
90
97
|
homeDir = mkdtempSync(join(tmpdir(), "openpalm-edge-"));
|
|
91
98
|
configDir = join(homeDir, "config");
|
|
92
|
-
|
|
99
|
+
dataDir = join(homeDir, "data");
|
|
93
100
|
stackDir = join(configDir, "stack");
|
|
94
|
-
cacheDir = join(homeDir, "cache");
|
|
95
101
|
|
|
96
102
|
for (const dir of [
|
|
97
103
|
homeDir,
|
|
98
104
|
configDir,
|
|
99
|
-
join(homeDir, "state", "registry", "automations"),
|
|
100
105
|
join(configDir, "assistant"),
|
|
101
106
|
join(configDir, "akm"),
|
|
102
|
-
join(homeDir, "
|
|
107
|
+
join(homeDir, "knowledge"),
|
|
108
|
+
join(homeDir, "knowledge", "env"),
|
|
109
|
+
join(homeDir, "knowledge", "secrets"),
|
|
103
110
|
join(homeDir, "workspace"),
|
|
104
111
|
stackDir,
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
join(
|
|
108
|
-
join(
|
|
109
|
-
join(
|
|
110
|
-
join(
|
|
111
|
-
join(
|
|
112
|
-
join(
|
|
113
|
-
join(stateDir, "registry", "addons"),
|
|
114
|
-
join(stateDir, "backups"),
|
|
115
|
-
join(stateDir, "akm"),
|
|
116
|
-
join(stateDir, "akm", "data"),
|
|
117
|
-
join(stateDir, "akm", "state"),
|
|
118
|
-
cacheDir,
|
|
119
|
-
join(cacheDir, "akm"),
|
|
120
|
-
join(cacheDir, "guardian"),
|
|
121
|
-
join(cacheDir, "rollback"),
|
|
112
|
+
dataDir,
|
|
113
|
+
join(dataDir, "assistant"),
|
|
114
|
+
join(dataDir, "guardian"),
|
|
115
|
+
join(dataDir, "akm", "cache"),
|
|
116
|
+
join(dataDir, "akm", "data"),
|
|
117
|
+
join(dataDir, "logs"),
|
|
118
|
+
join(dataDir, "backups"),
|
|
119
|
+
join(dataDir, "rollback"),
|
|
122
120
|
]) {
|
|
123
121
|
mkdirSync(dir, { recursive: true });
|
|
124
122
|
}
|
|
@@ -132,16 +130,10 @@ function seedMinimalEnvFiles(): void {
|
|
|
132
130
|
mkdirSync(stackDir, { recursive: true });
|
|
133
131
|
|
|
134
132
|
writeFileSync(
|
|
135
|
-
join(
|
|
133
|
+
join(homeDir, "knowledge", "env", "stack.env"),
|
|
136
134
|
[
|
|
137
135
|
"# OpenPalm — Stack Configuration",
|
|
138
|
-
"OP_UI_LOGIN_PASSWORD=",
|
|
139
|
-
"OPENAI_API_KEY=",
|
|
140
136
|
"OPENAI_BASE_URL=",
|
|
141
|
-
"ANTHROPIC_API_KEY=",
|
|
142
|
-
"GROQ_API_KEY=",
|
|
143
|
-
"MISTRAL_API_KEY=",
|
|
144
|
-
"GOOGLE_API_KEY=",
|
|
145
137
|
"OP_OWNER_NAME=",
|
|
146
138
|
"OP_OWNER_EMAIL=",
|
|
147
139
|
"",
|
|
@@ -166,16 +158,15 @@ describe("Fresh Install", () => {
|
|
|
166
158
|
rmSync(homeDir, { recursive: true, force: true });
|
|
167
159
|
});
|
|
168
160
|
|
|
169
|
-
// Scenario 1: ensureSecrets does NOT seed user.env (see akm-
|
|
161
|
+
// Scenario 1: ensureSecrets does NOT seed user.env (see akm-user-env) but
|
|
170
162
|
// does create stack.env with required keys when files do not exist.
|
|
171
|
-
it("ensureSecrets creates
|
|
163
|
+
it("ensureSecrets creates stack.env with required keys on fresh install", () => {
|
|
172
164
|
const state: ControlPlaneState = {
|
|
173
165
|
homeDir,
|
|
174
166
|
configDir,
|
|
175
|
-
stashDir: join(homeDir, "
|
|
167
|
+
stashDir: join(homeDir, "knowledge"),
|
|
176
168
|
workspaceDir: join(homeDir, "workspace"),
|
|
177
|
-
|
|
178
|
-
stateDir,
|
|
169
|
+
dataDir,
|
|
179
170
|
stackDir,
|
|
180
171
|
services: {},
|
|
181
172
|
artifacts: { compose: "" },
|
|
@@ -184,17 +175,18 @@ describe("Fresh Install", () => {
|
|
|
184
175
|
|
|
185
176
|
ensureSecrets(state);
|
|
186
177
|
|
|
187
|
-
//
|
|
188
|
-
const stackContent = readFileSync(join(
|
|
189
|
-
expect(stackContent).toContain("OPENAI_API_KEY=");
|
|
190
|
-
expect(stackContent).toContain("
|
|
178
|
+
// stack.env only carries non-secret setup/config keys.
|
|
179
|
+
const stackContent = readFileSync(join(homeDir, "knowledge", "env", "stack.env"), "utf-8");
|
|
180
|
+
expect(stackContent).not.toContain("OPENAI_API_KEY=");
|
|
181
|
+
expect(stackContent).toContain("OP_SETUP_COMPLETE=false");
|
|
182
|
+
expect(readSecret(stackDir, 'op_ui_login_password')).toBeNull();
|
|
191
183
|
});
|
|
192
184
|
|
|
193
185
|
// Scenario 2: isSetupComplete returns false before setup
|
|
194
186
|
it("isSetupComplete returns false when stack.env has OP_SETUP_COMPLETE=false", () => {
|
|
195
|
-
mkdirSync(
|
|
187
|
+
mkdirSync(dataDir, { recursive: true });
|
|
196
188
|
writeFileSync(
|
|
197
|
-
join(
|
|
189
|
+
join(homeDir, "knowledge", "env", "stack.env"),
|
|
198
190
|
"OP_SETUP_COMPLETE=false\n"
|
|
199
191
|
);
|
|
200
192
|
|
|
@@ -223,7 +215,7 @@ describe("Fresh Install", () => {
|
|
|
223
215
|
|
|
224
216
|
await performSetup(makeValidSpec());
|
|
225
217
|
|
|
226
|
-
const stackEnv = readFileSync(join(
|
|
218
|
+
const stackEnv = readFileSync(join(homeDir, "knowledge", "env", "stack.env"), "utf-8");
|
|
227
219
|
const parsed = parseEnvContent(stackEnv);
|
|
228
220
|
// Either entirely absent, or still the seeded "false" — never "true".
|
|
229
221
|
expect(parsed.OP_SETUP_COMPLETE === undefined || parsed.OP_SETUP_COMPLETE === "false").toBe(true);
|
|
@@ -246,18 +238,17 @@ describe("Existing Install", () => {
|
|
|
246
238
|
rmSync(homeDir, { recursive: true, force: true });
|
|
247
239
|
});
|
|
248
240
|
|
|
249
|
-
// Scenario 5: ensureSecrets
|
|
250
|
-
it("ensureSecrets
|
|
251
|
-
mkdirSync(
|
|
252
|
-
writeFileSync(join(
|
|
241
|
+
// Scenario 5: ensureSecrets creates file-based secrets without stack.env tokens
|
|
242
|
+
it("ensureSecrets creates file-based system secrets", () => {
|
|
243
|
+
mkdirSync(dataDir, { recursive: true });
|
|
244
|
+
writeFileSync(join(homeDir, "knowledge", "env", "stack.env"), "OP_SETUP_COMPLETE=false\n");
|
|
253
245
|
|
|
254
246
|
const state: ControlPlaneState = {
|
|
255
247
|
homeDir,
|
|
256
248
|
configDir,
|
|
257
|
-
stashDir: join(homeDir, "
|
|
249
|
+
stashDir: join(homeDir, "knowledge"),
|
|
258
250
|
workspaceDir: join(homeDir, "workspace"),
|
|
259
|
-
|
|
260
|
-
stateDir,
|
|
251
|
+
dataDir,
|
|
261
252
|
stackDir,
|
|
262
253
|
services: {},
|
|
263
254
|
artifacts: { compose: "" },
|
|
@@ -266,25 +257,23 @@ describe("Existing Install", () => {
|
|
|
266
257
|
|
|
267
258
|
ensureSecrets(state);
|
|
268
259
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
expect(
|
|
260
|
+
const afterContent = readFileSync(join(homeDir, "knowledge", "env", "stack.env"), "utf-8");
|
|
261
|
+
expect(afterContent).not.toContain("OP_UI_LOGIN_PASSWORD=");
|
|
262
|
+
expect(readSecret(stackDir, 'op_ui_login_password')).toBeNull();
|
|
272
263
|
});
|
|
273
264
|
|
|
274
265
|
// Scenario 6: performSetup re-run rewrites OP_UI_LOGIN_PASSWORD when the
|
|
275
266
|
// operator supplies a new one in the spec. This is intentional — the
|
|
276
267
|
// wizard "rerun" path is how an operator rotates the password. The
|
|
277
268
|
// legacy OP_ASSISTANT_TOKEN preservation test was removed with the token.
|
|
278
|
-
it("performSetup re-run rewrites OP_UI_LOGIN_PASSWORD when spec changes", async () => {
|
|
269
|
+
it("performSetup re-run rewrites OP_UI_LOGIN_PASSWORD secret file when spec changes", async () => {
|
|
279
270
|
await performSetup(makeValidSpec({ security: { uiLoginPassword: "first-password-12345" } }));
|
|
280
271
|
|
|
281
|
-
|
|
282
|
-
expect(afterFirst).toContain("OP_UI_LOGIN_PASSWORD=first-password-12345");
|
|
272
|
+
expect(readSecret(stackDir, 'op_ui_login_password')).toBe("first-password-12345\n");
|
|
283
273
|
|
|
284
274
|
await performSetup(makeValidSpec({ security: { uiLoginPassword: "second-password-12345" } }));
|
|
285
275
|
|
|
286
|
-
|
|
287
|
-
expect(afterSecond).toContain("OP_UI_LOGIN_PASSWORD=second-password-12345");
|
|
276
|
+
expect(readSecret(stackDir, 'op_ui_login_password')).toBe("second-password-12345\n");
|
|
288
277
|
});
|
|
289
278
|
|
|
290
279
|
// Scenario 7: performSetup must NOT mark OP_SETUP_COMPLETE — see scenario
|
|
@@ -294,7 +283,7 @@ describe("Existing Install", () => {
|
|
|
294
283
|
await performSetup(makeValidSpec());
|
|
295
284
|
|
|
296
285
|
const stackEnv = readFileSync(
|
|
297
|
-
join(
|
|
286
|
+
join(homeDir, "knowledge", "env", "stack.env"),
|
|
298
287
|
"utf-8"
|
|
299
288
|
);
|
|
300
289
|
const parsed = parseEnvContent(stackEnv);
|
|
@@ -323,14 +312,8 @@ describe("Existing Install", () => {
|
|
|
323
312
|
})
|
|
324
313
|
);
|
|
325
314
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
expect(specAfterSecond).not.toBeNull();
|
|
329
|
-
expect(specAfterSecond!.version).toBe(2);
|
|
330
|
-
|
|
331
|
-
// stack.env should retain both keys
|
|
332
|
-
const secrets = readFileSync(join(stackDir, "stack.env"), "utf-8");
|
|
333
|
-
expect(secrets).toContain("GROQ_API_KEY");
|
|
315
|
+
const auth = JSON.parse(readFileSync(join(homeDir, "knowledge", "secrets", "auth.json"), "utf-8"));
|
|
316
|
+
expect(auth.groq.key).toBe("gsk-test-key-456");
|
|
334
317
|
});
|
|
335
318
|
});
|
|
336
319
|
|
|
@@ -351,16 +334,15 @@ describe("Broken/Corrupt State", () => {
|
|
|
351
334
|
|
|
352
335
|
// Scenario 9: ensureSecrets is idempotent on repeated calls
|
|
353
336
|
it("ensureSecrets is idempotent — second call does not overwrite existing stack.env", () => {
|
|
354
|
-
mkdirSync(
|
|
355
|
-
writeFileSync(join(
|
|
337
|
+
mkdirSync(dataDir, { recursive: true });
|
|
338
|
+
writeFileSync(join(homeDir, "knowledge", "env", "stack.env"), "OP_SETUP_COMPLETE=false\n");
|
|
356
339
|
|
|
357
340
|
const state: ControlPlaneState = {
|
|
358
341
|
homeDir,
|
|
359
342
|
configDir,
|
|
360
|
-
stashDir: join(homeDir, "
|
|
343
|
+
stashDir: join(homeDir, "knowledge"),
|
|
361
344
|
workspaceDir: join(homeDir, "workspace"),
|
|
362
|
-
|
|
363
|
-
stateDir,
|
|
345
|
+
dataDir,
|
|
364
346
|
stackDir,
|
|
365
347
|
services: {},
|
|
366
348
|
artifacts: { compose: "" },
|
|
@@ -369,9 +351,10 @@ describe("Broken/Corrupt State", () => {
|
|
|
369
351
|
|
|
370
352
|
ensureSecrets(state);
|
|
371
353
|
|
|
372
|
-
// Existing
|
|
373
|
-
const content = readFileSync(join(
|
|
374
|
-
expect(content).toContain("
|
|
354
|
+
// Existing non-secret stack config must be preserved.
|
|
355
|
+
const content = readFileSync(join(homeDir, "knowledge", "env", "stack.env"), "utf-8");
|
|
356
|
+
expect(content).toContain("OP_SETUP_COMPLETE=false");
|
|
357
|
+
expect(content).not.toContain("OP_UI_LOGIN_PASSWORD=");
|
|
375
358
|
});
|
|
376
359
|
|
|
377
360
|
// Scenario 10: env file with malformed lines
|
|
@@ -388,10 +371,10 @@ describe("Broken/Corrupt State", () => {
|
|
|
388
371
|
" # indented comment",
|
|
389
372
|
].join("\n");
|
|
390
373
|
|
|
391
|
-
mkdirSync(
|
|
392
|
-
writeFileSync(join(
|
|
374
|
+
mkdirSync(dataDir, { recursive: true });
|
|
375
|
+
writeFileSync(join(dataDir, "test.env"), malformedContent);
|
|
393
376
|
|
|
394
|
-
const parsed = parseEnvFile(join(
|
|
377
|
+
const parsed = parseEnvFile(join(dataDir, "test.env"));
|
|
395
378
|
expect(parsed.VALID_KEY).toBe("valid_value");
|
|
396
379
|
expect(parsed.EXPORTED_KEY).toBe("exported_value");
|
|
397
380
|
expect(parsed.ANOTHER_VALID).toBe("value");
|
|
@@ -400,23 +383,25 @@ describe("Broken/Corrupt State", () => {
|
|
|
400
383
|
// Scenario 11: stack.env missing OP_SETUP_COMPLETE
|
|
401
384
|
it("isSetupComplete falls back to token check when OP_SETUP_COMPLETE missing", () => {
|
|
402
385
|
// stack.env without OP_SETUP_COMPLETE
|
|
403
|
-
mkdirSync(
|
|
386
|
+
mkdirSync(dataDir, { recursive: true });
|
|
404
387
|
writeFileSync(
|
|
405
|
-
join(
|
|
388
|
+
join(homeDir, "knowledge", "env", "stack.env"),
|
|
406
389
|
"OP_IMAGE_TAG=latest\n"
|
|
407
390
|
);
|
|
408
391
|
|
|
409
392
|
expect(isSetupComplete(stackDir)).toBe(false);
|
|
410
393
|
});
|
|
411
394
|
|
|
412
|
-
it("isSetupComplete
|
|
413
|
-
mkdirSync(
|
|
395
|
+
it("isSetupComplete returns false when OP_UI_LOGIN_PASSWORD is set but OP_SETUP_COMPLETE is missing", () => {
|
|
396
|
+
mkdirSync(dataDir, { recursive: true });
|
|
414
397
|
writeFileSync(
|
|
415
|
-
join(
|
|
398
|
+
join(homeDir, "knowledge", "env", "stack.env"),
|
|
416
399
|
"OP_IMAGE_TAG=latest\nexport OP_UI_LOGIN_PASSWORD=my-real-password\n"
|
|
417
400
|
);
|
|
418
401
|
|
|
419
|
-
|
|
402
|
+
// Password alone is no longer a proxy for setup completion.
|
|
403
|
+
// Only OP_SETUP_COMPLETE=true counts.
|
|
404
|
+
expect(isSetupComplete(stackDir)).toBe(false);
|
|
420
405
|
});
|
|
421
406
|
|
|
422
407
|
// Scenario 12: API key with special characters round-trips
|
|
@@ -435,19 +420,13 @@ describe("Broken/Corrupt State", () => {
|
|
|
435
420
|
}
|
|
436
421
|
});
|
|
437
422
|
|
|
438
|
-
// Scenario
|
|
439
|
-
it("readStackSpec returns null when stack.yml missing", () => {
|
|
440
|
-
const spec = readStackSpec(stackDir);
|
|
441
|
-
expect(spec).toBeNull();
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
// Scenario 14: stash/tasks dir missing (performSetup should recreate it via ensureHomeDirs)
|
|
423
|
+
// Scenario 14: knowledge/tasks dir missing (performSetup should recreate it via ensureHomeDirs)
|
|
445
424
|
it("performSetup creates missing subdirectories", async () => {
|
|
446
425
|
// Seed the minimal env files first
|
|
447
426
|
seedMinimalEnvFiles();
|
|
448
427
|
|
|
449
|
-
// Remove
|
|
450
|
-
rmSync(join(homeDir, "
|
|
428
|
+
// Remove knowledge/tasks dir (performSetup should recreate it via ensureHomeDirs)
|
|
429
|
+
rmSync(join(homeDir, "knowledge", "tasks"), { recursive: true, force: true });
|
|
451
430
|
|
|
452
431
|
const result = await performSetup(
|
|
453
432
|
makeValidSpec()
|
|
@@ -458,20 +437,10 @@ describe("Broken/Corrupt State", () => {
|
|
|
458
437
|
expect(existsSync(join(homeDir, "config", "stack", "core.compose.yml"))).toBe(
|
|
459
438
|
true
|
|
460
439
|
);
|
|
461
|
-
//
|
|
462
|
-
expect(existsSync(join(homeDir, "
|
|
440
|
+
// knowledge/tasks dir should be recreated by ensureHomeDirs
|
|
441
|
+
expect(existsSync(join(homeDir, "knowledge", "tasks"))).toBe(true);
|
|
463
442
|
});
|
|
464
443
|
|
|
465
|
-
// Scenario 15: openpalm.yaml with old version
|
|
466
|
-
it("readStackSpec returns null for version 1 spec", () => {
|
|
467
|
-
writeFileSync(
|
|
468
|
-
join(stackDir, STACK_SPEC_FILENAME),
|
|
469
|
-
"version: 1\nconnections: []\n"
|
|
470
|
-
);
|
|
471
|
-
|
|
472
|
-
const spec = readStackSpec(stackDir);
|
|
473
|
-
expect(spec).toBeNull();
|
|
474
|
-
});
|
|
475
444
|
});
|
|
476
445
|
|
|
477
446
|
// =====================================================================
|
|
@@ -489,15 +458,15 @@ describe("Environment Edge Cases", () => {
|
|
|
489
458
|
rmSync(homeDir, { recursive: true, force: true });
|
|
490
459
|
});
|
|
491
460
|
|
|
492
|
-
// Scenario 16: isSetupComplete
|
|
493
|
-
it("isSetupComplete
|
|
494
|
-
mkdirSync(
|
|
461
|
+
// Scenario 16: isSetupComplete requires explicit OP_SETUP_COMPLETE=true
|
|
462
|
+
it("isSetupComplete returns false when only OP_UI_LOGIN_PASSWORD is set", () => {
|
|
463
|
+
mkdirSync(dataDir, { recursive: true });
|
|
495
464
|
writeFileSync(
|
|
496
|
-
join(
|
|
465
|
+
join(homeDir, "knowledge", "env", "stack.env"),
|
|
497
466
|
"SOME_OTHER_KEY=value\nexport OP_UI_LOGIN_PASSWORD=real-password-here\n"
|
|
498
467
|
);
|
|
499
468
|
|
|
500
|
-
expect(isSetupComplete(stackDir)).toBe(
|
|
469
|
+
expect(isSetupComplete(stackDir)).toBe(false);
|
|
501
470
|
});
|
|
502
471
|
|
|
503
472
|
// Scenario 17: export prefix on env vars
|
|
@@ -571,10 +540,6 @@ describe("Setup Input Variations", () => {
|
|
|
571
540
|
|
|
572
541
|
const result = await performSetup(input);
|
|
573
542
|
expect(result.ok).toBe(true);
|
|
574
|
-
|
|
575
|
-
const spec = readStackSpec(stackDir);
|
|
576
|
-
expect(spec).not.toBeNull();
|
|
577
|
-
expect(spec!.version).toBe(2);
|
|
578
543
|
});
|
|
579
544
|
|
|
580
545
|
// Scenario 21: Multiple providers map to correct env vars
|
|
@@ -635,12 +600,9 @@ describe("performSetup end-to-end artifacts", () => {
|
|
|
635
600
|
rmSync(homeDir, { recursive: true, force: true });
|
|
636
601
|
});
|
|
637
602
|
|
|
638
|
-
it("
|
|
603
|
+
it("does not create a stack.yml (addon state lives in stack.env)", async () => {
|
|
639
604
|
await performSetup(makeValidSpec());
|
|
640
|
-
|
|
641
|
-
const spec = readStackSpec(stackDir);
|
|
642
|
-
expect(spec).not.toBeNull();
|
|
643
|
-
expect(spec!.version).toBe(2);
|
|
605
|
+
expect(existsSync(join(stackDir, "stack.yml"))).toBe(false);
|
|
644
606
|
});
|
|
645
607
|
|
|
646
608
|
it("writes akm config with embedding dims from setup spec", async () => {
|
|
@@ -667,11 +629,10 @@ describe("performSetup end-to-end artifacts", () => {
|
|
|
667
629
|
).toBe(true);
|
|
668
630
|
});
|
|
669
631
|
|
|
670
|
-
it("writes the UI login password to
|
|
632
|
+
it("writes the UI login password to a secret file", async () => {
|
|
671
633
|
await performSetup(makeValidSpec());
|
|
672
634
|
|
|
673
|
-
|
|
674
|
-
expect(secrets.OP_UI_LOGIN_PASSWORD).toBe("test-admin-token-12345");
|
|
635
|
+
expect(readSecret(stackDir, 'op_ui_login_password')).toBe("test-admin-token-12345\n");
|
|
675
636
|
});
|
|
676
637
|
|
|
677
638
|
it("writes akm config with llm provider and model", async () => {
|
|
@@ -679,8 +640,11 @@ describe("performSetup end-to-end artifacts", () => {
|
|
|
679
640
|
|
|
680
641
|
const akmConfigPath = join(homeDir, "config", "akm", "config.json");
|
|
681
642
|
const config = JSON.parse(readFileSync(akmConfigPath, "utf-8"));
|
|
682
|
-
|
|
683
|
-
expect(config.llm
|
|
643
|
+
// Canonical akm 0.8.0 shape (I-3): profiles.llm.default + defaults.llm.
|
|
644
|
+
expect(config.llm).toBeUndefined();
|
|
645
|
+
expect(config.profiles.llm.default.provider).toBe("openai");
|
|
646
|
+
expect(config.profiles.llm.default.model).toBe("gpt-4o");
|
|
647
|
+
expect(config.defaults.llm).toBe("default");
|
|
684
648
|
expect(config.embedding.model).toBe("text-embedding-3-small");
|
|
685
649
|
});
|
|
686
650
|
});
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Both `performSetup` (config writes) and `startDeploy` (Docker work) need an
|
|
5
5
|
* exclusive lock against concurrent installs. The lock file lives at
|
|
6
|
-
* `<
|
|
6
|
+
* `<dataDir>/.install.lock` and contains `<pid>\n<timestamp>\n`.
|
|
7
7
|
*
|
|
8
8
|
* Self-healing rules:
|
|
9
9
|
* - On EEXIST, parse the holder PID. If the process is gone (`process.kill(pid, 0)`
|
|
@@ -89,23 +89,23 @@ function tryCreate(path: string): boolean {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
|
-
* Try to acquire the install lock under `
|
|
92
|
+
* Try to acquire the install lock under `dataDir`. Returns a handle on
|
|
93
93
|
* success or null if the lock is held by a live, recent install (or on any
|
|
94
94
|
* unexpected filesystem error — caller should surface "install_in_progress").
|
|
95
95
|
*
|
|
96
96
|
* Callers MUST call `releaseInstallLock()` in a finally block when done.
|
|
97
97
|
*/
|
|
98
|
-
export function acquireInstallLock(
|
|
98
|
+
export function acquireInstallLock(dataDir: string): InstallLockHandle | null {
|
|
99
99
|
try {
|
|
100
|
-
mkdirSync(
|
|
100
|
+
mkdirSync(dataDir, { recursive: true });
|
|
101
101
|
} catch (err) {
|
|
102
|
-
logger.warn("failed to ensure
|
|
103
|
-
|
|
102
|
+
logger.warn("failed to ensure data dir for install lock", {
|
|
103
|
+
dataDir,
|
|
104
104
|
error: err instanceof Error ? err.message : String(err),
|
|
105
105
|
});
|
|
106
106
|
return null;
|
|
107
107
|
}
|
|
108
|
-
const path = join(
|
|
108
|
+
const path = join(dataDir, ".install.lock");
|
|
109
109
|
|
|
110
110
|
try {
|
|
111
111
|
if (tryCreate(path)) return { path };
|