@openpalm/lib 0.9.9 → 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 +158 -886
- 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
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Edge-case tests for the OpenPalm install and setup flow.
|
|
3
3
|
*
|
|
4
|
-
* Each test creates its own temp directory tree mimicking the
|
|
5
|
-
* (
|
|
6
|
-
* functions against it. No mocks of code under test.
|
|
4
|
+
* Each test creates its own temp directory tree mimicking the single
|
|
5
|
+
* ~/.openpalm/ root layout (config, vault, data, logs), then runs the
|
|
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";
|
|
9
9
|
import {
|
|
@@ -16,31 +16,37 @@ import {
|
|
|
16
16
|
} from "node:fs";
|
|
17
17
|
import { tmpdir } from "node:os";
|
|
18
18
|
import { join } from "node:path";
|
|
19
|
-
import { parse as yamlParse } from "yaml";
|
|
20
|
-
|
|
21
19
|
import { parseEnvContent, parseEnvFile, mergeEnvContent } from "./env.js";
|
|
22
|
-
import { ensureSecrets,
|
|
20
|
+
import { ensureSecrets, readStackEnv } from "./secrets.js";
|
|
23
21
|
import { isSetupComplete } from "./setup-status.js";
|
|
24
22
|
import {
|
|
25
23
|
performSetup,
|
|
26
24
|
buildSecretsFromSetup,
|
|
27
|
-
|
|
25
|
+
buildSystemSecretsFromSetup,
|
|
28
26
|
} from "./setup.js";
|
|
29
|
-
import type {
|
|
30
|
-
import type { CoreAssetProvider } from "./core-asset-provider.js";
|
|
27
|
+
import type { SetupSpec, SetupConnection } from "./setup.js";
|
|
31
28
|
import type { ControlPlaneState } from "./types.js";
|
|
32
29
|
import { STACK_SPEC_FILENAME, readStackSpec } from "./stack-spec.js";
|
|
33
|
-
import { readConnectionProfilesDocument } from "./connection-profiles.js";
|
|
34
30
|
|
|
35
31
|
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
36
32
|
|
|
37
|
-
function
|
|
33
|
+
function makeValidSpec(overrides?: Partial<SetupSpec>): SetupSpec {
|
|
38
34
|
return {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
version: 2,
|
|
36
|
+
capabilities: {
|
|
37
|
+
llm: "openai/gpt-4o",
|
|
38
|
+
embeddings: {
|
|
39
|
+
provider: "openai",
|
|
40
|
+
model: "text-embedding-3-small",
|
|
41
|
+
dims: 1536,
|
|
42
|
+
},
|
|
43
|
+
memory: {
|
|
44
|
+
userId: "test_user",
|
|
45
|
+
customInstructions: "",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
security: { adminToken: "test-admin-token-12345" },
|
|
49
|
+
owner: { name: "Test User", email: "test@example.com" },
|
|
44
50
|
connections: [
|
|
45
51
|
{
|
|
46
52
|
id: "openai-main",
|
|
@@ -50,121 +56,114 @@ function makeValidInput(overrides?: Partial<SetupInput>): SetupInput {
|
|
|
50
56
|
apiKey: "sk-test-key-123",
|
|
51
57
|
},
|
|
52
58
|
],
|
|
53
|
-
assignments: {
|
|
54
|
-
llm: { connectionId: "openai-main", model: "gpt-4o" },
|
|
55
|
-
embeddings: {
|
|
56
|
-
connectionId: "openai-main",
|
|
57
|
-
model: "text-embedding-3-small",
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
59
|
...overrides,
|
|
61
60
|
};
|
|
62
61
|
}
|
|
63
62
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
cleanupData: () => "name: cleanup-data\nschedule: weekly\n",
|
|
80
|
-
validateConfig: () => "name: validate-config\nschedule: hourly\n",
|
|
81
|
-
};
|
|
63
|
+
/** Seed the minimal asset files that ensure* functions expect to find at OP_HOME. */
|
|
64
|
+
function seedRequiredAssets(homeDir: string): void {
|
|
65
|
+
mkdirSync(join(homeDir, "stack"), { recursive: true });
|
|
66
|
+
writeFileSync(join(homeDir, "stack", "core.compose.yml"), "services:\n assistant:\n image: assistant:latest\n");
|
|
67
|
+
mkdirSync(join(homeDir, "data", "assistant"), { recursive: true });
|
|
68
|
+
writeFileSync(join(homeDir, "data", "assistant", "opencode.jsonc"), '{"$schema":"https://opencode.ai/config.json"}\n');
|
|
69
|
+
writeFileSync(join(homeDir, "data", "assistant", "AGENTS.md"), "# Agents\n");
|
|
70
|
+
mkdirSync(join(homeDir, "vault", "user"), { recursive: true });
|
|
71
|
+
writeFileSync(join(homeDir, "vault", "user", "user.env.schema"), "ADMIN_TOKEN=string\n");
|
|
72
|
+
mkdirSync(join(homeDir, "vault", "stack"), { recursive: true });
|
|
73
|
+
writeFileSync(join(homeDir, "vault", "stack", "stack.env.schema"), "OP_IMAGE_TAG=string\n");
|
|
74
|
+
mkdirSync(join(homeDir, "config", "automations"), { recursive: true });
|
|
75
|
+
writeFileSync(join(homeDir, "config", "automations", "cleanup-logs.yml"), "name: cleanup-logs\nschedule: daily\n");
|
|
76
|
+
writeFileSync(join(homeDir, "config", "automations", "cleanup-data.yml"), "name: cleanup-data\nschedule: weekly\n");
|
|
77
|
+
writeFileSync(join(homeDir, "config", "automations", "validate-config.yml"), "name: validate-config\nschedule: hourly\n");
|
|
82
78
|
}
|
|
83
79
|
|
|
84
80
|
// ── Shared test fixture ──────────────────────────────────────────────────
|
|
85
81
|
|
|
86
|
-
let
|
|
82
|
+
let homeDir: string;
|
|
87
83
|
let configDir: string;
|
|
84
|
+
let vaultDir: string;
|
|
88
85
|
let dataDir: string;
|
|
89
|
-
let
|
|
86
|
+
let logsDir: string;
|
|
90
87
|
|
|
91
88
|
const savedEnv: Record<string, string | undefined> = {};
|
|
92
89
|
|
|
93
90
|
function saveAndSetEnv(): void {
|
|
94
|
-
savedEnv.
|
|
95
|
-
|
|
96
|
-
savedEnv.OPENPALM_STATE_HOME = process.env.OPENPALM_STATE_HOME;
|
|
97
|
-
process.env.OPENPALM_CONFIG_HOME = configDir;
|
|
98
|
-
process.env.OPENPALM_DATA_HOME = dataDir;
|
|
99
|
-
process.env.OPENPALM_STATE_HOME = stateDir;
|
|
91
|
+
savedEnv.OP_HOME = process.env.OP_HOME;
|
|
92
|
+
process.env.OP_HOME = homeDir;
|
|
100
93
|
}
|
|
101
94
|
|
|
102
95
|
function restoreEnv(): void {
|
|
103
|
-
process.env.
|
|
104
|
-
process.env.OPENPALM_DATA_HOME = savedEnv.OPENPALM_DATA_HOME;
|
|
105
|
-
process.env.OPENPALM_STATE_HOME = savedEnv.OPENPALM_STATE_HOME;
|
|
96
|
+
process.env.OP_HOME = savedEnv.OP_HOME;
|
|
106
97
|
}
|
|
107
98
|
|
|
108
|
-
/** Create a full directory tree matching
|
|
99
|
+
/** Create a full directory tree matching ensureHomeDirs() output. */
|
|
109
100
|
function createFullDirTree(): void {
|
|
110
|
-
|
|
111
|
-
configDir = join(
|
|
112
|
-
|
|
113
|
-
|
|
101
|
+
homeDir = mkdtempSync(join(tmpdir(), "openpalm-edge-"));
|
|
102
|
+
configDir = join(homeDir, "config");
|
|
103
|
+
vaultDir = join(homeDir, "vault");
|
|
104
|
+
dataDir = join(homeDir, "data");
|
|
105
|
+
logsDir = join(homeDir, "logs");
|
|
114
106
|
|
|
115
107
|
for (const dir of [
|
|
108
|
+
homeDir,
|
|
116
109
|
configDir,
|
|
110
|
+
join(configDir, "automations"),
|
|
117
111
|
join(configDir, "channels"),
|
|
118
|
-
join(configDir, "connections"),
|
|
119
112
|
join(configDir, "assistant"),
|
|
120
|
-
join(configDir, "automations"),
|
|
121
113
|
join(configDir, "stash"),
|
|
114
|
+
join(homeDir, "stack"),
|
|
115
|
+
join(homeDir, "stack", "addons"),
|
|
116
|
+
vaultDir,
|
|
122
117
|
dataDir,
|
|
123
118
|
join(dataDir, "admin"),
|
|
124
119
|
join(dataDir, "memory"),
|
|
125
120
|
join(dataDir, "assistant"),
|
|
126
121
|
join(dataDir, "guardian"),
|
|
127
|
-
join(dataDir, "caddy"),
|
|
128
|
-
join(dataDir, "caddy", "data"),
|
|
129
|
-
join(dataDir, "caddy", "config"),
|
|
130
122
|
join(dataDir, "automations"),
|
|
131
123
|
join(dataDir, "opencode"),
|
|
132
|
-
|
|
133
|
-
join(
|
|
134
|
-
|
|
135
|
-
join(
|
|
136
|
-
join(stateDir, "automations"),
|
|
137
|
-
join(stateDir, "opencode"),
|
|
124
|
+
join(dataDir, "stash"),
|
|
125
|
+
join(dataDir, "workspace"),
|
|
126
|
+
logsDir,
|
|
127
|
+
join(logsDir, "opencode"),
|
|
138
128
|
]) {
|
|
139
129
|
mkdirSync(dir, { recursive: true });
|
|
140
130
|
}
|
|
131
|
+
|
|
132
|
+
// Seed asset files that ensure* functions expect to find at OP_HOME
|
|
133
|
+
seedRequiredAssets(homeDir);
|
|
141
134
|
}
|
|
142
135
|
|
|
143
|
-
/** Seed the minimal
|
|
136
|
+
/** Seed the minimal user.env and stack.env needed for most tests. */
|
|
144
137
|
function seedMinimalEnvFiles(): void {
|
|
138
|
+
mkdirSync(join(vaultDir, "user"), { recursive: true });
|
|
139
|
+
mkdirSync(join(vaultDir, "stack"), { recursive: true });
|
|
145
140
|
writeFileSync(
|
|
146
|
-
join(
|
|
141
|
+
join(vaultDir, "user", "user.env"),
|
|
147
142
|
[
|
|
148
|
-
"# OpenPalm
|
|
149
|
-
"
|
|
150
|
-
"
|
|
151
|
-
"export OPENAI_API_KEY=",
|
|
152
|
-
"export OPENAI_BASE_URL=",
|
|
153
|
-
"export ANTHROPIC_API_KEY=",
|
|
154
|
-
"export GROQ_API_KEY=",
|
|
155
|
-
"export MISTRAL_API_KEY=",
|
|
156
|
-
"export GOOGLE_API_KEY=",
|
|
157
|
-
"export MEMORY_USER_ID=default_user",
|
|
158
|
-
"export MEMORY_AUTH_TOKEN=abc123",
|
|
159
|
-
"export OWNER_NAME=",
|
|
160
|
-
"export OWNER_EMAIL=",
|
|
143
|
+
"# OpenPalm — User Extensions",
|
|
144
|
+
"# Add any custom environment variables here.",
|
|
145
|
+
"# These are loaded by compose alongside stack.env.",
|
|
161
146
|
"",
|
|
162
147
|
].join("\n")
|
|
163
148
|
);
|
|
164
149
|
|
|
165
150
|
writeFileSync(
|
|
166
|
-
join(
|
|
167
|
-
|
|
151
|
+
join(vaultDir, "stack", "stack.env"),
|
|
152
|
+
[
|
|
153
|
+
"# OpenPalm — Stack Configuration",
|
|
154
|
+
"OP_ADMIN_TOKEN=",
|
|
155
|
+
"OP_ASSISTANT_TOKEN=",
|
|
156
|
+
"OP_MEMORY_TOKEN=",
|
|
157
|
+
"OPENAI_API_KEY=",
|
|
158
|
+
"OPENAI_BASE_URL=",
|
|
159
|
+
"ANTHROPIC_API_KEY=",
|
|
160
|
+
"GROQ_API_KEY=",
|
|
161
|
+
"MISTRAL_API_KEY=",
|
|
162
|
+
"GOOGLE_API_KEY=",
|
|
163
|
+
"OWNER_NAME=",
|
|
164
|
+
"OWNER_EMAIL=",
|
|
165
|
+
"",
|
|
166
|
+
].join("\n")
|
|
168
167
|
);
|
|
169
168
|
}
|
|
170
169
|
|
|
@@ -182,47 +181,54 @@ describe("Fresh Install", () => {
|
|
|
182
181
|
|
|
183
182
|
afterEach(() => {
|
|
184
183
|
restoreEnv();
|
|
185
|
-
rmSync(
|
|
184
|
+
rmSync(homeDir, { recursive: true, force: true });
|
|
186
185
|
});
|
|
187
186
|
|
|
188
|
-
// Scenario 1: ensureSecrets creates
|
|
189
|
-
it("ensureSecrets creates
|
|
187
|
+
// Scenario 1: ensureSecrets creates user.env as placeholder and stack.env with required keys
|
|
188
|
+
it("ensureSecrets creates user.env as placeholder and stack.env with required keys when files do not exist", () => {
|
|
190
189
|
const state: ControlPlaneState = {
|
|
191
190
|
adminToken: "",
|
|
191
|
+
assistantToken: "",
|
|
192
192
|
setupToken: "",
|
|
193
|
-
|
|
193
|
+
homeDir,
|
|
194
194
|
configDir,
|
|
195
|
+
vaultDir,
|
|
195
196
|
dataDir,
|
|
197
|
+
logsDir,
|
|
198
|
+
cacheDir: join(homeDir, "cache"),
|
|
196
199
|
services: {},
|
|
197
|
-
artifacts: { compose: ""
|
|
200
|
+
artifacts: { compose: "" },
|
|
198
201
|
artifactMeta: [],
|
|
199
202
|
audit: [],
|
|
200
|
-
channelSecrets: {},
|
|
201
203
|
};
|
|
202
204
|
|
|
203
|
-
// No
|
|
204
|
-
expect(existsSync(join(
|
|
205
|
+
// No user.env exists yet
|
|
206
|
+
expect(existsSync(join(vaultDir, "user", "user.env"))).toBe(false);
|
|
205
207
|
|
|
206
208
|
ensureSecrets(state);
|
|
207
209
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
210
|
+
// user.env is now a minimal placeholder
|
|
211
|
+
const userContent = readFileSync(join(vaultDir, "user", "user.env"), "utf-8");
|
|
212
|
+
expect(userContent).toContain("User Extensions");
|
|
213
|
+
|
|
214
|
+
// API keys and owner info are seeded in stack.env
|
|
215
|
+
const stackContent = readFileSync(join(vaultDir, "stack", "stack.env"), "utf-8");
|
|
216
|
+
expect(stackContent).toContain("OPENAI_API_KEY=");
|
|
217
|
+
expect(stackContent).toContain("OWNER_NAME=");
|
|
214
218
|
});
|
|
215
219
|
|
|
216
220
|
// Scenario 2: isSetupComplete returns false before setup
|
|
217
|
-
it("isSetupComplete returns false when stack.env has
|
|
221
|
+
it("isSetupComplete returns false when stack.env has OP_SETUP_COMPLETE=false", () => {
|
|
222
|
+
mkdirSync(join(vaultDir, "stack"), { recursive: true });
|
|
223
|
+
mkdirSync(join(vaultDir, "user"), { recursive: true });
|
|
218
224
|
writeFileSync(
|
|
219
|
-
join(
|
|
220
|
-
"
|
|
225
|
+
join(vaultDir, "stack", "stack.env"),
|
|
226
|
+
"OP_SETUP_COMPLETE=false\n"
|
|
221
227
|
);
|
|
222
|
-
// Empty
|
|
223
|
-
writeFileSync(join(
|
|
228
|
+
// Empty user.env so fallback check doesn't trigger
|
|
229
|
+
writeFileSync(join(vaultDir, "user", "user.env"), "");
|
|
224
230
|
|
|
225
|
-
expect(isSetupComplete(
|
|
231
|
+
expect(isSetupComplete(vaultDir)).toBe(false);
|
|
226
232
|
});
|
|
227
233
|
|
|
228
234
|
// Scenario 3: performSetup succeeds from completely empty state
|
|
@@ -230,20 +236,21 @@ describe("Fresh Install", () => {
|
|
|
230
236
|
seedMinimalEnvFiles();
|
|
231
237
|
|
|
232
238
|
const result = await performSetup(
|
|
233
|
-
|
|
234
|
-
createStubAssetProvider()
|
|
239
|
+
makeValidSpec()
|
|
235
240
|
);
|
|
236
241
|
|
|
237
242
|
expect(result.ok).toBe(true);
|
|
238
243
|
});
|
|
239
244
|
|
|
240
|
-
// Scenario 4:
|
|
241
|
-
it("
|
|
245
|
+
// Scenario 4: performSetup marks setup complete in vault/stack/stack.env
|
|
246
|
+
it("performSetup marks OP_SETUP_COMPLETE=true in vault stack.env", async () => {
|
|
242
247
|
seedMinimalEnvFiles();
|
|
243
248
|
|
|
244
|
-
await performSetup(
|
|
249
|
+
await performSetup(makeValidSpec());
|
|
245
250
|
|
|
246
|
-
|
|
251
|
+
const stackEnv = readFileSync(join(vaultDir, "stack", "stack.env"), "utf-8");
|
|
252
|
+
const parsed = parseEnvContent(stackEnv);
|
|
253
|
+
expect(parsed.OP_SETUP_COMPLETE).toBe("true");
|
|
247
254
|
});
|
|
248
255
|
});
|
|
249
256
|
|
|
@@ -260,52 +267,56 @@ describe("Existing Install", () => {
|
|
|
260
267
|
|
|
261
268
|
afterEach(() => {
|
|
262
269
|
restoreEnv();
|
|
263
|
-
rmSync(
|
|
270
|
+
rmSync(homeDir, { recursive: true, force: true });
|
|
264
271
|
});
|
|
265
272
|
|
|
266
|
-
// Scenario 5: ensureSecrets does NOT overwrite existing
|
|
267
|
-
it("ensureSecrets does not overwrite existing
|
|
273
|
+
// Scenario 5: ensureSecrets does NOT overwrite existing user.env
|
|
274
|
+
it("ensureSecrets does not overwrite existing user.env", () => {
|
|
268
275
|
const customContent =
|
|
269
|
-
"export
|
|
270
|
-
|
|
276
|
+
"export OP_ADMIN_TOKEN=my-custom-token\nexport OP_MEMORY_TOKEN=custom-auth-token\n";
|
|
277
|
+
mkdirSync(join(vaultDir, "user"), { recursive: true });
|
|
278
|
+
writeFileSync(join(vaultDir, "user", "user.env"), customContent);
|
|
271
279
|
|
|
272
280
|
const state: ControlPlaneState = {
|
|
273
281
|
adminToken: "",
|
|
282
|
+
assistantToken: "",
|
|
274
283
|
setupToken: "",
|
|
275
|
-
|
|
284
|
+
homeDir,
|
|
276
285
|
configDir,
|
|
286
|
+
vaultDir,
|
|
277
287
|
dataDir,
|
|
288
|
+
logsDir,
|
|
289
|
+
cacheDir: join(homeDir, "cache"),
|
|
278
290
|
services: {},
|
|
279
|
-
artifacts: { compose: ""
|
|
291
|
+
artifacts: { compose: "" },
|
|
280
292
|
artifactMeta: [],
|
|
281
293
|
audit: [],
|
|
282
|
-
channelSecrets: {},
|
|
283
294
|
};
|
|
284
295
|
|
|
285
296
|
ensureSecrets(state);
|
|
286
297
|
|
|
287
|
-
const afterContent = readFileSync(join(
|
|
298
|
+
const afterContent = readFileSync(join(vaultDir, "user", "user.env"), "utf-8");
|
|
288
299
|
expect(afterContent).toBe(customContent);
|
|
289
300
|
});
|
|
290
301
|
|
|
291
|
-
// Scenario 6: performSetup re-run preserves
|
|
292
|
-
it("performSetup re-run preserves
|
|
302
|
+
// Scenario 6: performSetup re-run preserves OP_MEMORY_TOKEN
|
|
303
|
+
it("performSetup re-run preserves OP_MEMORY_TOKEN from first run", async () => {
|
|
293
304
|
// First setup
|
|
294
|
-
await performSetup(
|
|
305
|
+
await performSetup(makeValidSpec());
|
|
295
306
|
|
|
296
307
|
const secretsAfterFirst = readFileSync(
|
|
297
|
-
join(
|
|
308
|
+
join(vaultDir, "stack", "stack.env"),
|
|
298
309
|
"utf-8"
|
|
299
310
|
);
|
|
300
311
|
const firstMatch = secretsAfterFirst.match(
|
|
301
|
-
/
|
|
312
|
+
/OP_MEMORY_TOKEN=([a-f0-9]+)/
|
|
302
313
|
);
|
|
303
314
|
expect(firstMatch).not.toBeNull();
|
|
304
315
|
const firstToken = firstMatch![1];
|
|
305
316
|
|
|
306
317
|
// Second setup (re-run with different API key)
|
|
307
318
|
await performSetup(
|
|
308
|
-
|
|
319
|
+
makeValidSpec({
|
|
309
320
|
connections: [
|
|
310
321
|
{
|
|
311
322
|
id: "openai-main",
|
|
@@ -315,46 +326,57 @@ describe("Existing Install", () => {
|
|
|
315
326
|
apiKey: "sk-different-key-999",
|
|
316
327
|
},
|
|
317
328
|
],
|
|
318
|
-
})
|
|
319
|
-
createStubAssetProvider()
|
|
329
|
+
})
|
|
320
330
|
);
|
|
321
331
|
|
|
322
332
|
const secretsAfterSecond = readFileSync(
|
|
323
|
-
join(
|
|
333
|
+
join(vaultDir, "stack", "stack.env"),
|
|
324
334
|
"utf-8"
|
|
325
335
|
);
|
|
326
336
|
const secondMatch = secretsAfterSecond.match(
|
|
327
|
-
/
|
|
337
|
+
/OP_MEMORY_TOKEN=([a-f0-9]+)/
|
|
328
338
|
);
|
|
329
339
|
expect(secondMatch).not.toBeNull();
|
|
330
|
-
//
|
|
340
|
+
// OP_MEMORY_TOKEN should be preserved (buildSystemSecretsFromSetup does not overwrite it)
|
|
331
341
|
expect(secondMatch![1]).toBe(firstToken);
|
|
332
342
|
});
|
|
333
343
|
|
|
334
|
-
// Scenario 7:
|
|
335
|
-
it("performSetup marks
|
|
336
|
-
await performSetup(
|
|
344
|
+
// Scenario 7: performSetup marks OP_SETUP_COMPLETE=true in vault/stack/stack.env
|
|
345
|
+
it("performSetup marks OP_SETUP_COMPLETE=true in vault stack.env", async () => {
|
|
346
|
+
await performSetup(makeValidSpec());
|
|
337
347
|
|
|
338
|
-
const
|
|
339
|
-
join(
|
|
348
|
+
const stackEnv = readFileSync(
|
|
349
|
+
join(vaultDir, "stack", "stack.env"),
|
|
340
350
|
"utf-8"
|
|
341
351
|
);
|
|
342
|
-
const parsed = parseEnvContent(
|
|
343
|
-
expect(parsed.
|
|
352
|
+
const parsed = parseEnvContent(stackEnv);
|
|
353
|
+
expect(parsed.OP_SETUP_COMPLETE).toBe("true");
|
|
344
354
|
});
|
|
345
355
|
|
|
346
|
-
// Scenario 8: Re-setup with different provider
|
|
347
|
-
it("re-setup with different provider
|
|
356
|
+
// Scenario 8: Re-setup with different provider updates stack.yml capabilities
|
|
357
|
+
it("re-setup with different provider updates capabilities in stack.yml", async () => {
|
|
348
358
|
// First setup with OpenAI
|
|
349
|
-
await performSetup(
|
|
359
|
+
await performSetup(makeValidSpec());
|
|
350
360
|
|
|
351
|
-
const
|
|
352
|
-
expect(
|
|
353
|
-
expect(
|
|
361
|
+
const specAfterFirst = readStackSpec(configDir);
|
|
362
|
+
expect(specAfterFirst).not.toBeNull();
|
|
363
|
+
expect(specAfterFirst!.capabilities.llm).toContain("openai/");
|
|
354
364
|
|
|
355
365
|
// Second setup with Groq
|
|
356
366
|
await performSetup(
|
|
357
|
-
|
|
367
|
+
makeValidSpec({
|
|
368
|
+
capabilities: {
|
|
369
|
+
llm: "groq/llama3-70b-8192",
|
|
370
|
+
embeddings: {
|
|
371
|
+
provider: "groq",
|
|
372
|
+
model: "text-embedding-3-small",
|
|
373
|
+
dims: 1536,
|
|
374
|
+
},
|
|
375
|
+
memory: {
|
|
376
|
+
userId: "test_user",
|
|
377
|
+
customInstructions: "",
|
|
378
|
+
},
|
|
379
|
+
},
|
|
358
380
|
connections: [
|
|
359
381
|
{
|
|
360
382
|
id: "groq-main",
|
|
@@ -364,24 +386,15 @@ describe("Existing Install", () => {
|
|
|
364
386
|
apiKey: "gsk-test-key-456",
|
|
365
387
|
},
|
|
366
388
|
],
|
|
367
|
-
|
|
368
|
-
llm: { connectionId: "groq-main", model: "llama3-70b-8192" },
|
|
369
|
-
embeddings: {
|
|
370
|
-
connectionId: "groq-main",
|
|
371
|
-
model: "text-embedding-3-small",
|
|
372
|
-
},
|
|
373
|
-
},
|
|
374
|
-
}),
|
|
375
|
-
createStubAssetProvider()
|
|
389
|
+
})
|
|
376
390
|
);
|
|
377
391
|
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
expect(
|
|
381
|
-
expect(profilesAfterSecond.profiles[0].provider).toBe("groq");
|
|
392
|
+
const specAfterSecond = readStackSpec(configDir);
|
|
393
|
+
expect(specAfterSecond).not.toBeNull();
|
|
394
|
+
expect(specAfterSecond!.capabilities.llm).toBe("groq/llama3-70b-8192");
|
|
382
395
|
|
|
383
|
-
//
|
|
384
|
-
const secrets = readFileSync(join(
|
|
396
|
+
// stack.env should retain both keys
|
|
397
|
+
const secrets = readFileSync(join(vaultDir, "stack", "stack.env"), "utf-8");
|
|
385
398
|
expect(secrets).toContain("GROQ_API_KEY");
|
|
386
399
|
});
|
|
387
400
|
});
|
|
@@ -398,34 +411,38 @@ describe("Broken/Corrupt State", () => {
|
|
|
398
411
|
|
|
399
412
|
afterEach(() => {
|
|
400
413
|
restoreEnv();
|
|
401
|
-
rmSync(
|
|
414
|
+
rmSync(homeDir, { recursive: true, force: true });
|
|
402
415
|
});
|
|
403
416
|
|
|
404
|
-
// Scenario 9:
|
|
405
|
-
it("ensureSecrets returns early for an empty but existing
|
|
406
|
-
|
|
417
|
+
// Scenario 9: user.env exists but is empty
|
|
418
|
+
it("ensureSecrets returns early for an empty but existing user.env", () => {
|
|
419
|
+
mkdirSync(join(vaultDir, "user"), { recursive: true });
|
|
420
|
+
writeFileSync(join(vaultDir, "user", "user.env"), "");
|
|
407
421
|
|
|
408
422
|
const state: ControlPlaneState = {
|
|
409
423
|
adminToken: "",
|
|
424
|
+
assistantToken: "",
|
|
410
425
|
setupToken: "",
|
|
411
|
-
|
|
426
|
+
homeDir,
|
|
412
427
|
configDir,
|
|
428
|
+
vaultDir,
|
|
413
429
|
dataDir,
|
|
430
|
+
logsDir,
|
|
431
|
+
cacheDir: join(homeDir, "cache"),
|
|
414
432
|
services: {},
|
|
415
|
-
artifacts: { compose: ""
|
|
433
|
+
artifacts: { compose: "" },
|
|
416
434
|
artifactMeta: [],
|
|
417
435
|
audit: [],
|
|
418
|
-
channelSecrets: {},
|
|
419
436
|
};
|
|
420
437
|
|
|
421
438
|
ensureSecrets(state);
|
|
422
439
|
|
|
423
440
|
// File should still exist and still be empty (ensureSecrets only checks existence)
|
|
424
|
-
const content = readFileSync(join(
|
|
441
|
+
const content = readFileSync(join(vaultDir, "user", "user.env"), "utf-8");
|
|
425
442
|
expect(content).toBe("");
|
|
426
443
|
});
|
|
427
444
|
|
|
428
|
-
// Scenario 10:
|
|
445
|
+
// Scenario 10: user.env with malformed lines
|
|
429
446
|
it("parseEnvFile handles malformed env lines gracefully", () => {
|
|
430
447
|
const malformedContent = [
|
|
431
448
|
"# Comment line",
|
|
@@ -439,43 +456,42 @@ describe("Broken/Corrupt State", () => {
|
|
|
439
456
|
" # indented comment",
|
|
440
457
|
].join("\n");
|
|
441
458
|
|
|
442
|
-
|
|
459
|
+
mkdirSync(join(vaultDir, "user"), { recursive: true });
|
|
460
|
+
writeFileSync(join(vaultDir, "user", "user.env"), malformedContent);
|
|
443
461
|
|
|
444
|
-
const parsed = parseEnvFile(join(
|
|
462
|
+
const parsed = parseEnvFile(join(vaultDir, "user", "user.env"));
|
|
445
463
|
expect(parsed.VALID_KEY).toBe("valid_value");
|
|
446
464
|
expect(parsed.EXPORTED_KEY).toBe("exported_value");
|
|
447
465
|
expect(parsed.ANOTHER_VALID).toBe("value");
|
|
448
466
|
});
|
|
449
467
|
|
|
450
|
-
// Scenario 11: stack.env missing
|
|
451
|
-
it("isSetupComplete falls back to token check when
|
|
452
|
-
// stack.env without
|
|
468
|
+
// Scenario 11: stack.env missing OP_SETUP_COMPLETE
|
|
469
|
+
it("isSetupComplete falls back to token check when OP_SETUP_COMPLETE missing", () => {
|
|
470
|
+
// stack.env without OP_SETUP_COMPLETE
|
|
471
|
+
mkdirSync(join(vaultDir, "stack"), { recursive: true });
|
|
472
|
+
mkdirSync(join(vaultDir, "user"), { recursive: true });
|
|
453
473
|
writeFileSync(
|
|
454
|
-
join(
|
|
455
|
-
"
|
|
474
|
+
join(vaultDir, "stack", "stack.env"),
|
|
475
|
+
"OP_IMAGE_TAG=latest\n"
|
|
456
476
|
);
|
|
457
477
|
|
|
458
|
-
//
|
|
478
|
+
// user.env without any token
|
|
459
479
|
writeFileSync(
|
|
460
|
-
join(
|
|
461
|
-
"export
|
|
480
|
+
join(vaultDir, "user", "user.env"),
|
|
481
|
+
"export OP_ADMIN_TOKEN=\nexport ADMIN_TOKEN=\n"
|
|
462
482
|
);
|
|
463
483
|
|
|
464
|
-
expect(isSetupComplete(
|
|
484
|
+
expect(isSetupComplete(vaultDir)).toBe(false);
|
|
465
485
|
});
|
|
466
486
|
|
|
467
|
-
it("isSetupComplete falls back to true when admin token is set but
|
|
468
|
-
|
|
469
|
-
join(stateDir, "artifacts", "stack.env"),
|
|
470
|
-
"OPENPALM_IMAGE_TAG=latest\n"
|
|
471
|
-
);
|
|
472
|
-
|
|
487
|
+
it("isSetupComplete falls back to true when admin token is set but OP_SETUP_COMPLETE missing", () => {
|
|
488
|
+
mkdirSync(join(vaultDir, "stack"), { recursive: true });
|
|
473
489
|
writeFileSync(
|
|
474
|
-
join(
|
|
475
|
-
"
|
|
490
|
+
join(vaultDir, "stack", "stack.env"),
|
|
491
|
+
"OP_IMAGE_TAG=latest\nexport OP_ADMIN_TOKEN=my-real-token\n"
|
|
476
492
|
);
|
|
477
493
|
|
|
478
|
-
expect(isSetupComplete(
|
|
494
|
+
expect(isSetupComplete(vaultDir)).toBe(true);
|
|
479
495
|
});
|
|
480
496
|
|
|
481
497
|
// Scenario 12: API key with special characters round-trips
|
|
@@ -494,57 +510,38 @@ describe("Broken/Corrupt State", () => {
|
|
|
494
510
|
}
|
|
495
511
|
});
|
|
496
512
|
|
|
497
|
-
// Scenario 13:
|
|
498
|
-
it("
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
"NOT VALID JSON {{{{"
|
|
502
|
-
);
|
|
503
|
-
|
|
504
|
-
expect(() => readConnectionProfilesDocument(configDir)).toThrow(
|
|
505
|
-
"invalid JSON"
|
|
506
|
-
);
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
it("readConnectionProfilesDocument throws on valid JSON but wrong structure", () => {
|
|
510
|
-
writeFileSync(
|
|
511
|
-
join(configDir, "connections", "profiles.json"),
|
|
512
|
-
JSON.stringify({ version: 1, profiles: [], assignments: {} })
|
|
513
|
-
);
|
|
514
|
-
|
|
515
|
-
expect(() => readConnectionProfilesDocument(configDir)).toThrow(
|
|
516
|
-
"invalid"
|
|
517
|
-
);
|
|
513
|
+
// Scenario 13: Missing stack.yml returns null
|
|
514
|
+
it("readStackSpec returns null when stack.yml missing", () => {
|
|
515
|
+
const spec = readStackSpec(configDir);
|
|
516
|
+
expect(spec).toBeNull();
|
|
518
517
|
});
|
|
519
518
|
|
|
520
|
-
// Scenario 14:
|
|
521
|
-
it("performSetup creates missing
|
|
522
|
-
// Seed the minimal env files first
|
|
519
|
+
// Scenario 14: config dir exists but automations dir doesn't
|
|
520
|
+
it("performSetup creates missing subdirectories", async () => {
|
|
521
|
+
// Seed the minimal env files first
|
|
523
522
|
seedMinimalEnvFiles();
|
|
524
523
|
|
|
525
524
|
// Remove automations dir (performSetup should recreate it)
|
|
526
|
-
rmSync(join(
|
|
525
|
+
rmSync(join(configDir, "automations"), { recursive: true, force: true });
|
|
527
526
|
|
|
528
527
|
const result = await performSetup(
|
|
529
|
-
|
|
530
|
-
createStubAssetProvider()
|
|
528
|
+
makeValidSpec()
|
|
531
529
|
);
|
|
532
530
|
expect(result.ok).toBe(true);
|
|
533
531
|
|
|
534
|
-
// Artifacts should exist
|
|
535
|
-
expect(existsSync(join(
|
|
532
|
+
// Artifacts should exist in stack/ (not config/components/)
|
|
533
|
+
expect(existsSync(join(homeDir, "stack", "core.compose.yml"))).toBe(
|
|
536
534
|
true
|
|
537
535
|
);
|
|
538
|
-
expect(existsSync(join(stateDir, "artifacts", "Caddyfile"))).toBe(true);
|
|
539
536
|
// Automations dir should be recreated
|
|
540
|
-
expect(existsSync(join(
|
|
537
|
+
expect(existsSync(join(configDir, "automations"))).toBe(true);
|
|
541
538
|
});
|
|
542
539
|
|
|
543
540
|
// Scenario 15: openpalm.yaml with old version
|
|
544
|
-
it("readStackSpec returns null for version
|
|
541
|
+
it("readStackSpec returns null for version 1 spec", () => {
|
|
545
542
|
writeFileSync(
|
|
546
543
|
join(configDir, STACK_SPEC_FILENAME),
|
|
547
|
-
"version:
|
|
544
|
+
"version: 1\nconnections: []\n"
|
|
548
545
|
);
|
|
549
546
|
|
|
550
547
|
const spec = readStackSpec(configDir);
|
|
@@ -564,26 +561,18 @@ describe("Environment Edge Cases", () => {
|
|
|
564
561
|
|
|
565
562
|
afterEach(() => {
|
|
566
563
|
restoreEnv();
|
|
567
|
-
rmSync(
|
|
564
|
+
rmSync(homeDir, { recursive: true, force: true });
|
|
568
565
|
});
|
|
569
566
|
|
|
570
|
-
// Scenario 16: Commented-out ADMIN_TOKEN but
|
|
571
|
-
it("isSetupComplete detects
|
|
572
|
-
|
|
573
|
-
join(stateDir, "artifacts", "stack.env"),
|
|
574
|
-
"SOME_OTHER_KEY=value\n"
|
|
575
|
-
);
|
|
576
|
-
|
|
567
|
+
// Scenario 16: Commented-out ADMIN_TOKEN but OP_ADMIN_TOKEN set
|
|
568
|
+
it("isSetupComplete detects OP_ADMIN_TOKEN when ADMIN_TOKEN is commented out", () => {
|
|
569
|
+
mkdirSync(join(vaultDir, "stack"), { recursive: true });
|
|
577
570
|
writeFileSync(
|
|
578
|
-
join(
|
|
579
|
-
|
|
580
|
-
"export OPENPALM_ADMIN_TOKEN=real-token-here",
|
|
581
|
-
"# export ADMIN_TOKEN=",
|
|
582
|
-
"",
|
|
583
|
-
].join("\n")
|
|
571
|
+
join(vaultDir, "stack", "stack.env"),
|
|
572
|
+
"SOME_OTHER_KEY=value\nexport OP_ADMIN_TOKEN=real-token-here\n"
|
|
584
573
|
);
|
|
585
574
|
|
|
586
|
-
expect(isSetupComplete(
|
|
575
|
+
expect(isSetupComplete(vaultDir)).toBe(true);
|
|
587
576
|
});
|
|
588
577
|
|
|
589
578
|
// Scenario 17: export prefix on env vars
|
|
@@ -636,13 +625,24 @@ describe("Setup Input Variations", () => {
|
|
|
636
625
|
|
|
637
626
|
afterEach(() => {
|
|
638
627
|
restoreEnv();
|
|
639
|
-
rmSync(
|
|
628
|
+
rmSync(homeDir, { recursive: true, force: true });
|
|
640
629
|
});
|
|
641
630
|
|
|
642
631
|
// Scenario 20: Ollama in-stack setup
|
|
643
632
|
it("Ollama in-stack setup overrides localhost URL to docker-internal", async () => {
|
|
644
|
-
const input =
|
|
645
|
-
|
|
633
|
+
const input = makeValidSpec({
|
|
634
|
+
capabilities: {
|
|
635
|
+
llm: "ollama/llama3.2",
|
|
636
|
+
embeddings: {
|
|
637
|
+
provider: "ollama",
|
|
638
|
+
model: "nomic-embed-text",
|
|
639
|
+
dims: 768,
|
|
640
|
+
},
|
|
641
|
+
memory: {
|
|
642
|
+
userId: "test_user",
|
|
643
|
+
customInstructions: "",
|
|
644
|
+
},
|
|
645
|
+
},
|
|
646
646
|
connections: [
|
|
647
647
|
{
|
|
648
648
|
id: "ollama-local",
|
|
@@ -652,135 +652,53 @@ describe("Setup Input Variations", () => {
|
|
|
652
652
|
apiKey: "",
|
|
653
653
|
},
|
|
654
654
|
],
|
|
655
|
-
assignments: {
|
|
656
|
-
llm: { connectionId: "ollama-local", model: "llama3.2" },
|
|
657
|
-
embeddings: {
|
|
658
|
-
connectionId: "ollama-local",
|
|
659
|
-
model: "nomic-embed-text",
|
|
660
|
-
},
|
|
661
|
-
},
|
|
662
655
|
});
|
|
663
656
|
|
|
664
|
-
const result = await performSetup(input
|
|
657
|
+
const result = await performSetup(input);
|
|
665
658
|
expect(result.ok).toBe(true);
|
|
666
659
|
|
|
667
|
-
//
|
|
668
|
-
const
|
|
669
|
-
expect(
|
|
670
|
-
|
|
671
|
-
// secrets.env should have in-stack URL
|
|
672
|
-
const secrets = parseEnvFile(join(configDir, "secrets.env"));
|
|
673
|
-
expect(secrets.SYSTEM_LLM_BASE_URL).toBe("http://ollama:11434");
|
|
674
|
-
expect(secrets.OPENAI_BASE_URL).toBe("http://ollama:11434/v1");
|
|
660
|
+
// stack.yml should have ollama capabilities
|
|
661
|
+
const spec = readStackSpec(configDir);
|
|
662
|
+
expect(spec).not.toBeNull();
|
|
663
|
+
expect(spec!.capabilities.llm).toBe("ollama/llama3.2");
|
|
675
664
|
});
|
|
676
665
|
|
|
677
|
-
// Scenario 21: Multiple providers
|
|
678
|
-
it("multiple providers each
|
|
679
|
-
const
|
|
680
|
-
{
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
provider: "openai",
|
|
684
|
-
baseUrl: "",
|
|
685
|
-
apiKey: "sk-openai",
|
|
686
|
-
},
|
|
687
|
-
{
|
|
688
|
-
id: "groq-1",
|
|
689
|
-
name: "Groq",
|
|
690
|
-
provider: "groq",
|
|
691
|
-
baseUrl: "",
|
|
692
|
-
apiKey: "gsk-groq",
|
|
693
|
-
},
|
|
694
|
-
{
|
|
695
|
-
id: "anthropic-1",
|
|
696
|
-
name: "Anthropic",
|
|
697
|
-
provider: "anthropic",
|
|
698
|
-
baseUrl: "",
|
|
699
|
-
apiKey: "sk-ant-api03",
|
|
700
|
-
},
|
|
666
|
+
// Scenario 21: Multiple providers map to correct env vars
|
|
667
|
+
it("multiple providers each write their API key to the correct env var", () => {
|
|
668
|
+
const conns: SetupConnection[] = [
|
|
669
|
+
{ id: "openai-1", name: "OpenAI", provider: "openai", baseUrl: "", apiKey: "sk-openai" },
|
|
670
|
+
{ id: "groq-1", name: "Groq", provider: "groq", baseUrl: "", apiKey: "gsk-groq" },
|
|
671
|
+
{ id: "anthropic-1", name: "Anthropic", provider: "anthropic", baseUrl: "", apiKey: "sk-ant-api03" },
|
|
701
672
|
];
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
expect(
|
|
705
|
-
expect(
|
|
706
|
-
expect(map.get("anthropic-1")).toBe("ANTHROPIC_API_KEY");
|
|
673
|
+
const secrets = buildSecretsFromSetup(conns);
|
|
674
|
+
expect(secrets.OPENAI_API_KEY).toBe("sk-openai");
|
|
675
|
+
expect(secrets.GROQ_API_KEY).toBe("gsk-groq");
|
|
676
|
+
expect(secrets.ANTHROPIC_API_KEY).toBe("sk-ant-api03");
|
|
707
677
|
});
|
|
708
678
|
|
|
709
|
-
// Scenario
|
|
710
|
-
it("
|
|
711
|
-
const
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
baseUrl: "https://example.com/v1",
|
|
719
|
-
apiKey: "sk-test",
|
|
720
|
-
},
|
|
721
|
-
],
|
|
722
|
-
assignments: {
|
|
723
|
-
llm: { connectionId: "openai-compat", model: "gpt-4o" },
|
|
724
|
-
embeddings: {
|
|
725
|
-
connectionId: "openai-compat",
|
|
726
|
-
model: "text-embedding-3-small",
|
|
727
|
-
},
|
|
728
|
-
},
|
|
729
|
-
})
|
|
730
|
-
);
|
|
731
|
-
|
|
732
|
-
expect(secrets.OPENAI_BASE_URL).toBe("https://example.com/v1");
|
|
733
|
-
expect(secrets.OPENAI_BASE_URL).not.toContain("/v1/v1");
|
|
679
|
+
// Scenario 21b: OAuth providers (no API key) are silently skipped
|
|
680
|
+
it("skips connections without API keys (OAuth providers)", () => {
|
|
681
|
+
const conns: SetupConnection[] = [
|
|
682
|
+
{ id: "github-copilot", name: "GitHub Copilot", provider: "github-copilot", baseUrl: "", apiKey: "" },
|
|
683
|
+
{ id: "openai-1", name: "OpenAI", provider: "openai", baseUrl: "", apiKey: "sk-test" },
|
|
684
|
+
];
|
|
685
|
+
const secrets = buildSecretsFromSetup(conns);
|
|
686
|
+
expect(secrets.OPENAI_API_KEY).toBe("sk-test");
|
|
687
|
+
expect(Object.keys(secrets)).not.toContain("GITHUB_COPILOT_API_KEY");
|
|
734
688
|
});
|
|
735
689
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
{
|
|
741
|
-
id: "openai-main",
|
|
742
|
-
name: "OpenAI",
|
|
743
|
-
provider: "openai",
|
|
744
|
-
baseUrl: "https://api.openai.com",
|
|
745
|
-
apiKey: "sk-test",
|
|
746
|
-
},
|
|
747
|
-
],
|
|
748
|
-
assignments: {
|
|
749
|
-
llm: { connectionId: "openai-main", model: "gpt-4o" },
|
|
750
|
-
embeddings: {
|
|
751
|
-
connectionId: "openai-main",
|
|
752
|
-
model: "text-embedding-3-small",
|
|
753
|
-
},
|
|
754
|
-
},
|
|
755
|
-
})
|
|
756
|
-
);
|
|
690
|
+
// Scenario 22: buildSecretsFromSetup only writes API keys and owner info
|
|
691
|
+
it("buildSecretsFromSetup writes API keys but not config vars", () => {
|
|
692
|
+
const spec = makeValidSpec();
|
|
693
|
+
const secrets = buildSecretsFromSetup(spec.connections, spec.owner);
|
|
757
694
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
{
|
|
766
|
-
id: "openai-main",
|
|
767
|
-
name: "OpenAI",
|
|
768
|
-
provider: "openai",
|
|
769
|
-
baseUrl: "https://api.openai.com/",
|
|
770
|
-
apiKey: "sk-test",
|
|
771
|
-
},
|
|
772
|
-
],
|
|
773
|
-
assignments: {
|
|
774
|
-
llm: { connectionId: "openai-main", model: "gpt-4o" },
|
|
775
|
-
embeddings: {
|
|
776
|
-
connectionId: "openai-main",
|
|
777
|
-
model: "text-embedding-3-small",
|
|
778
|
-
},
|
|
779
|
-
},
|
|
780
|
-
})
|
|
781
|
-
);
|
|
782
|
-
|
|
783
|
-
expect(secrets.OPENAI_BASE_URL).toBe("https://api.openai.com/v1");
|
|
695
|
+
// API key should be written
|
|
696
|
+
expect(secrets.OPENAI_API_KEY).toBe("sk-test-key-123");
|
|
697
|
+
// Config vars should NOT be in user.env anymore
|
|
698
|
+
expect(secrets.SYSTEM_LLM_PROVIDER).toBeUndefined();
|
|
699
|
+
expect(secrets.SYSTEM_LLM_MODEL).toBeUndefined();
|
|
700
|
+
expect(secrets.EMBEDDING_MODEL).toBeUndefined();
|
|
701
|
+
expect(secrets.EMBEDDING_DIMS).toBeUndefined();
|
|
784
702
|
});
|
|
785
703
|
});
|
|
786
704
|
|
|
@@ -797,22 +715,33 @@ describe("performSetup end-to-end artifacts", () => {
|
|
|
797
715
|
|
|
798
716
|
afterEach(() => {
|
|
799
717
|
restoreEnv();
|
|
800
|
-
rmSync(
|
|
718
|
+
rmSync(homeDir, { recursive: true, force: true });
|
|
801
719
|
});
|
|
802
720
|
|
|
803
|
-
it("writes
|
|
804
|
-
await performSetup(
|
|
721
|
+
it("writes stack.yml and readStackSpec returns v2", async () => {
|
|
722
|
+
await performSetup(makeValidSpec());
|
|
805
723
|
|
|
806
724
|
const spec = readStackSpec(configDir);
|
|
807
725
|
expect(spec).not.toBeNull();
|
|
808
|
-
expect(spec!.version).toBe(
|
|
809
|
-
expect(spec!.
|
|
810
|
-
expect(spec!.
|
|
811
|
-
expect(spec!.ollamaEnabled).toBe(false);
|
|
726
|
+
expect(spec!.version).toBe(2);
|
|
727
|
+
expect(spec!.capabilities.llm).toBe("openai/gpt-4o");
|
|
728
|
+
expect(spec!.capabilities.embeddings.model).toBe("text-embedding-3-small");
|
|
812
729
|
});
|
|
813
730
|
|
|
814
|
-
it("writes
|
|
815
|
-
const input =
|
|
731
|
+
it("writes OP_CAP_EMBEDDINGS_DIMS with correct embedding dims from lookup", async () => {
|
|
732
|
+
const input = makeValidSpec({
|
|
733
|
+
capabilities: {
|
|
734
|
+
llm: "ollama/llama3.2",
|
|
735
|
+
embeddings: {
|
|
736
|
+
provider: "ollama",
|
|
737
|
+
model: "nomic-embed-text",
|
|
738
|
+
dims: 0, // Resolved from lookup
|
|
739
|
+
},
|
|
740
|
+
memory: {
|
|
741
|
+
userId: "test_user",
|
|
742
|
+
customInstructions: "",
|
|
743
|
+
},
|
|
744
|
+
},
|
|
816
745
|
connections: [
|
|
817
746
|
{
|
|
818
747
|
id: "ollama-1",
|
|
@@ -822,54 +751,39 @@ describe("performSetup end-to-end artifacts", () => {
|
|
|
822
751
|
apiKey: "",
|
|
823
752
|
},
|
|
824
753
|
],
|
|
825
|
-
assignments: {
|
|
826
|
-
llm: { connectionId: "ollama-1", model: "llama3.2" },
|
|
827
|
-
embeddings: {
|
|
828
|
-
connectionId: "ollama-1",
|
|
829
|
-
model: "nomic-embed-text",
|
|
830
|
-
},
|
|
831
|
-
},
|
|
832
754
|
});
|
|
833
755
|
|
|
834
|
-
await performSetup(input
|
|
756
|
+
await performSetup(input);
|
|
835
757
|
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
);
|
|
839
|
-
// nomic-embed-text is 768 dims per EMBEDDING_DIMS constant
|
|
840
|
-
expect(memConfig.mem0.vector_store.config.embedding_model_dims).toBe(768);
|
|
758
|
+
// nomic-embed-text is 768 dims per EMBEDDING_DIMS constant — verify via stack.env
|
|
759
|
+
const stackEnvContent = readFileSync(join(vaultDir, "stack", "stack.env"), "utf-8");
|
|
760
|
+
expect(stackEnvContent).toContain("OP_CAP_EMBEDDINGS_DIMS=768");
|
|
841
761
|
});
|
|
842
762
|
|
|
843
|
-
it("writes
|
|
844
|
-
await performSetup(
|
|
763
|
+
it("writes core.compose.yml to stack/", async () => {
|
|
764
|
+
await performSetup(makeValidSpec());
|
|
845
765
|
|
|
846
766
|
expect(
|
|
847
|
-
existsSync(join(
|
|
767
|
+
existsSync(join(homeDir, "stack", "core.compose.yml"))
|
|
848
768
|
).toBe(true);
|
|
849
|
-
expect(existsSync(join(stateDir, "artifacts", "Caddyfile"))).toBe(true);
|
|
850
|
-
expect(existsSync(join(stateDir, "artifacts", "manifest.json"))).toBe(
|
|
851
|
-
true
|
|
852
|
-
);
|
|
853
769
|
});
|
|
854
770
|
|
|
855
|
-
it("writes
|
|
856
|
-
await performSetup(
|
|
771
|
+
it("writes admin and assistant tokens to stack.env", async () => {
|
|
772
|
+
await performSetup(makeValidSpec());
|
|
857
773
|
|
|
858
|
-
const secrets = parseEnvFile(join(
|
|
859
|
-
expect(secrets.
|
|
860
|
-
expect(secrets.
|
|
774
|
+
const secrets = parseEnvFile(join(vaultDir, "stack", "stack.env"));
|
|
775
|
+
expect(secrets.OP_ADMIN_TOKEN).toBe("test-admin-token-12345");
|
|
776
|
+
expect(typeof secrets.OP_ASSISTANT_TOKEN).toBe("string");
|
|
777
|
+
expect(secrets.OP_ASSISTANT_TOKEN).not.toBe("test-admin-token-12345");
|
|
861
778
|
});
|
|
862
779
|
|
|
863
|
-
it("
|
|
864
|
-
await performSetup(
|
|
780
|
+
it("writes OP_CAP_* vars from capabilities to stack.env", async () => {
|
|
781
|
+
await performSetup(makeValidSpec());
|
|
865
782
|
|
|
866
|
-
const
|
|
867
|
-
expect(
|
|
868
|
-
expect(
|
|
869
|
-
expect(
|
|
870
|
-
expect(doc.profiles[0].provider).toBe("openai");
|
|
871
|
-
expect(doc.assignments.llm.model).toBe("gpt-4o");
|
|
872
|
-
expect(doc.assignments.embeddings.model).toBe("text-embedding-3-small");
|
|
783
|
+
const stackEnv = parseEnvFile(join(vaultDir, "stack", "stack.env"));
|
|
784
|
+
expect(stackEnv.OP_CAP_LLM_PROVIDER).toBe("openai");
|
|
785
|
+
expect(stackEnv.OP_CAP_LLM_MODEL).toBe("gpt-4o");
|
|
786
|
+
expect(stackEnv.OP_CAP_EMBEDDINGS_MODEL).toBe("text-embedding-3-small");
|
|
873
787
|
});
|
|
874
788
|
});
|
|
875
789
|
|
|
@@ -894,321 +808,4 @@ describe("mergeEnvContent edge cases", () => {
|
|
|
894
808
|
const parsed = parseEnvContent(result);
|
|
895
809
|
expect(parsed.FOO).toBe("new");
|
|
896
810
|
});
|
|
897
|
-
|
|
898
|
-
it("appends new keys to the end when they do not exist", () => {
|
|
899
|
-
const original = "EXISTING=value\n";
|
|
900
|
-
const result = mergeEnvContent(original, { NEW_KEY: "new_value" });
|
|
901
|
-
const parsed = parseEnvContent(result);
|
|
902
|
-
expect(parsed.EXISTING).toBe("value");
|
|
903
|
-
expect(parsed.NEW_KEY).toBe("new_value");
|
|
904
|
-
});
|
|
905
|
-
|
|
906
|
-
it("uncomment option replaces commented-out keys", () => {
|
|
907
|
-
const original = "# export ADMIN_TOKEN=old_value\n";
|
|
908
|
-
const result = mergeEnvContent(
|
|
909
|
-
original,
|
|
910
|
-
{ ADMIN_TOKEN: "new_value" },
|
|
911
|
-
{ uncomment: true }
|
|
912
|
-
);
|
|
913
|
-
const parsed = parseEnvContent(result);
|
|
914
|
-
expect(parsed.ADMIN_TOKEN).toBe("new_value");
|
|
915
|
-
});
|
|
916
|
-
|
|
917
|
-
it("handles empty content gracefully", () => {
|
|
918
|
-
const result = mergeEnvContent("", { KEY: "value" });
|
|
919
|
-
const parsed = parseEnvContent(result);
|
|
920
|
-
expect(parsed.KEY).toBe("value");
|
|
921
|
-
});
|
|
922
|
-
|
|
923
|
-
it("handles content with only comments", () => {
|
|
924
|
-
const original = "# comment\n# another comment\n";
|
|
925
|
-
const result = mergeEnvContent(original, { KEY: "value" });
|
|
926
|
-
const parsed = parseEnvContent(result);
|
|
927
|
-
expect(parsed.KEY).toBe("value");
|
|
928
|
-
});
|
|
929
|
-
});
|
|
930
|
-
|
|
931
|
-
// =====================================================================
|
|
932
|
-
// parseEnvFile / parseEnvContent EDGE CASES
|
|
933
|
-
// =====================================================================
|
|
934
|
-
|
|
935
|
-
describe("parseEnvFile edge cases", () => {
|
|
936
|
-
beforeEach(() => {
|
|
937
|
-
createFullDirTree();
|
|
938
|
-
});
|
|
939
|
-
|
|
940
|
-
afterEach(() => {
|
|
941
|
-
rmSync(tempBase, { recursive: true, force: true });
|
|
942
|
-
});
|
|
943
|
-
|
|
944
|
-
it("returns empty object for nonexistent file", () => {
|
|
945
|
-
const result = parseEnvFile(join(configDir, "nonexistent.env"));
|
|
946
|
-
expect(result).toEqual({});
|
|
947
|
-
});
|
|
948
|
-
|
|
949
|
-
it("returns empty object for empty file", () => {
|
|
950
|
-
writeFileSync(join(configDir, "empty.env"), "");
|
|
951
|
-
const result = parseEnvFile(join(configDir, "empty.env"));
|
|
952
|
-
expect(result).toEqual({});
|
|
953
|
-
});
|
|
954
|
-
|
|
955
|
-
it("handles single-quoted values", () => {
|
|
956
|
-
writeFileSync(
|
|
957
|
-
join(configDir, "quoted.env"),
|
|
958
|
-
"KEY='value with spaces'\n"
|
|
959
|
-
);
|
|
960
|
-
const result = parseEnvFile(join(configDir, "quoted.env"));
|
|
961
|
-
expect(result.KEY).toBe("value with spaces");
|
|
962
|
-
});
|
|
963
|
-
|
|
964
|
-
it("handles double-quoted values", () => {
|
|
965
|
-
writeFileSync(
|
|
966
|
-
join(configDir, "quoted.env"),
|
|
967
|
-
'KEY="value with spaces"\n'
|
|
968
|
-
);
|
|
969
|
-
const result = parseEnvFile(join(configDir, "quoted.env"));
|
|
970
|
-
expect(result.KEY).toBe("value with spaces");
|
|
971
|
-
});
|
|
972
|
-
|
|
973
|
-
it("handles values with inline comments when unquoted", () => {
|
|
974
|
-
// dotenv spec: unquoted values with # are treated as comments
|
|
975
|
-
writeFileSync(
|
|
976
|
-
join(configDir, "comment.env"),
|
|
977
|
-
"KEY=value # this is a comment\n"
|
|
978
|
-
);
|
|
979
|
-
const result = parseEnvFile(join(configDir, "comment.env"));
|
|
980
|
-
// dotenv library trims at the # for unquoted values
|
|
981
|
-
expect(result.KEY).toBe("value");
|
|
982
|
-
});
|
|
983
|
-
});
|
|
984
|
-
|
|
985
|
-
// =====================================================================
|
|
986
|
-
// loadSecretsEnvFile EDGE CASES
|
|
987
|
-
// =====================================================================
|
|
988
|
-
|
|
989
|
-
describe("loadSecretsEnvFile edge cases", () => {
|
|
990
|
-
beforeEach(() => {
|
|
991
|
-
createFullDirTree();
|
|
992
|
-
saveAndSetEnv();
|
|
993
|
-
});
|
|
994
|
-
|
|
995
|
-
afterEach(() => {
|
|
996
|
-
restoreEnv();
|
|
997
|
-
rmSync(tempBase, { recursive: true, force: true });
|
|
998
|
-
});
|
|
999
|
-
|
|
1000
|
-
it("returns empty object when secrets.env does not exist", () => {
|
|
1001
|
-
const result = loadSecretsEnvFile(configDir);
|
|
1002
|
-
expect(result).toEqual({});
|
|
1003
|
-
});
|
|
1004
|
-
|
|
1005
|
-
it("filters out keys not matching uppercase alphanumeric pattern", () => {
|
|
1006
|
-
writeFileSync(
|
|
1007
|
-
join(configDir, "secrets.env"),
|
|
1008
|
-
[
|
|
1009
|
-
"VALID_KEY=valid",
|
|
1010
|
-
"another_key=lowercase", // lowercase keys are filtered out
|
|
1011
|
-
"ALSO_VALID=yes",
|
|
1012
|
-
"123_STARTS_NUM=num", // starts with number but matches pattern
|
|
1013
|
-
"",
|
|
1014
|
-
].join("\n")
|
|
1015
|
-
);
|
|
1016
|
-
|
|
1017
|
-
const result = loadSecretsEnvFile(configDir);
|
|
1018
|
-
expect(result.VALID_KEY).toBe("valid");
|
|
1019
|
-
expect(result.ALSO_VALID).toBe("yes");
|
|
1020
|
-
// The regex /^[A-Z0-9_]+$/ does match 123_STARTS_NUM
|
|
1021
|
-
expect(result["123_STARTS_NUM"]).toBe("num");
|
|
1022
|
-
// Lowercase key does not match the filter
|
|
1023
|
-
expect(result.another_key).toBeUndefined();
|
|
1024
|
-
});
|
|
1025
|
-
});
|
|
1026
|
-
|
|
1027
|
-
// =====================================================================
|
|
1028
|
-
// isSetupComplete EDGE CASES
|
|
1029
|
-
// =====================================================================
|
|
1030
|
-
|
|
1031
|
-
describe("isSetupComplete edge cases", () => {
|
|
1032
|
-
beforeEach(() => {
|
|
1033
|
-
createFullDirTree();
|
|
1034
|
-
saveAndSetEnv();
|
|
1035
|
-
});
|
|
1036
|
-
|
|
1037
|
-
afterEach(() => {
|
|
1038
|
-
restoreEnv();
|
|
1039
|
-
rmSync(tempBase, { recursive: true, force: true });
|
|
1040
|
-
});
|
|
1041
|
-
|
|
1042
|
-
it("returns false when stack.env does not exist and no admin token", () => {
|
|
1043
|
-
// No stack.env and no secrets.env
|
|
1044
|
-
rmSync(join(stateDir, "artifacts", "stack.env"), { force: true });
|
|
1045
|
-
|
|
1046
|
-
expect(isSetupComplete(stateDir, configDir)).toBe(false);
|
|
1047
|
-
});
|
|
1048
|
-
|
|
1049
|
-
it("returns true for OPENPALM_SETUP_COMPLETE=TRUE (case insensitive)", () => {
|
|
1050
|
-
writeFileSync(
|
|
1051
|
-
join(stateDir, "artifacts", "stack.env"),
|
|
1052
|
-
"OPENPALM_SETUP_COMPLETE=TRUE\n"
|
|
1053
|
-
);
|
|
1054
|
-
|
|
1055
|
-
expect(isSetupComplete(stateDir, configDir)).toBe(true);
|
|
1056
|
-
});
|
|
1057
|
-
|
|
1058
|
-
it("returns true for OPENPALM_SETUP_COMPLETE=True (mixed case)", () => {
|
|
1059
|
-
writeFileSync(
|
|
1060
|
-
join(stateDir, "artifacts", "stack.env"),
|
|
1061
|
-
"OPENPALM_SETUP_COMPLETE=True\n"
|
|
1062
|
-
);
|
|
1063
|
-
|
|
1064
|
-
expect(isSetupComplete(stateDir, configDir)).toBe(true);
|
|
1065
|
-
});
|
|
1066
|
-
|
|
1067
|
-
it("returns false for OPENPALM_SETUP_COMPLETE=false", () => {
|
|
1068
|
-
writeFileSync(
|
|
1069
|
-
join(stateDir, "artifacts", "stack.env"),
|
|
1070
|
-
"OPENPALM_SETUP_COMPLETE=false\n"
|
|
1071
|
-
);
|
|
1072
|
-
writeFileSync(join(configDir, "secrets.env"), "");
|
|
1073
|
-
|
|
1074
|
-
expect(isSetupComplete(stateDir, configDir)).toBe(false);
|
|
1075
|
-
});
|
|
1076
|
-
|
|
1077
|
-
it("falls back to ADMIN_TOKEN presence when OPENPALM_SETUP_COMPLETE not in stack.env", () => {
|
|
1078
|
-
writeFileSync(
|
|
1079
|
-
join(stateDir, "artifacts", "stack.env"),
|
|
1080
|
-
"OPENPALM_IMAGE_TAG=latest\n"
|
|
1081
|
-
);
|
|
1082
|
-
writeFileSync(
|
|
1083
|
-
join(configDir, "secrets.env"),
|
|
1084
|
-
"export ADMIN_TOKEN=my-admin-token\n"
|
|
1085
|
-
);
|
|
1086
|
-
|
|
1087
|
-
expect(isSetupComplete(stateDir, configDir)).toBe(true);
|
|
1088
|
-
});
|
|
1089
|
-
});
|
|
1090
|
-
|
|
1091
|
-
// =====================================================================
|
|
1092
|
-
// buildSecretsFromSetup EDGE CASES
|
|
1093
|
-
// =====================================================================
|
|
1094
|
-
|
|
1095
|
-
describe("buildSecretsFromSetup edge cases", () => {
|
|
1096
|
-
it("sanitizes owner name with control characters", () => {
|
|
1097
|
-
const input = makeValidInput({ ownerName: "Test\nUser\r\0" });
|
|
1098
|
-
const secrets = buildSecretsFromSetup(input);
|
|
1099
|
-
expect(secrets.OWNER_NAME).toBe("TestUser");
|
|
1100
|
-
});
|
|
1101
|
-
|
|
1102
|
-
it("omits empty owner name and email", () => {
|
|
1103
|
-
const input = makeValidInput({ ownerName: "", ownerEmail: "" });
|
|
1104
|
-
const secrets = buildSecretsFromSetup(input);
|
|
1105
|
-
expect(secrets.OWNER_NAME).toBeUndefined();
|
|
1106
|
-
expect(secrets.OWNER_EMAIL).toBeUndefined();
|
|
1107
|
-
});
|
|
1108
|
-
|
|
1109
|
-
it("defaults memoryUserId to default_user when empty", () => {
|
|
1110
|
-
const input = makeValidInput({ memoryUserId: "" });
|
|
1111
|
-
const secrets = buildSecretsFromSetup(input);
|
|
1112
|
-
expect(secrets.MEMORY_USER_ID).toBe("default_user");
|
|
1113
|
-
});
|
|
1114
|
-
|
|
1115
|
-
it("sets SYSTEM_LLM_PROVIDER correctly for each provider", () => {
|
|
1116
|
-
for (const provider of ["openai", "groq", "anthropic"] as const) {
|
|
1117
|
-
const envKey =
|
|
1118
|
-
provider === "openai"
|
|
1119
|
-
? "OPENAI_API_KEY"
|
|
1120
|
-
: provider === "groq"
|
|
1121
|
-
? "GROQ_API_KEY"
|
|
1122
|
-
: "ANTHROPIC_API_KEY";
|
|
1123
|
-
|
|
1124
|
-
const input = makeValidInput({
|
|
1125
|
-
connections: [
|
|
1126
|
-
{
|
|
1127
|
-
id: `${provider}-1`,
|
|
1128
|
-
name: provider,
|
|
1129
|
-
provider,
|
|
1130
|
-
baseUrl: "https://api.example.com",
|
|
1131
|
-
apiKey: "sk-test",
|
|
1132
|
-
},
|
|
1133
|
-
],
|
|
1134
|
-
assignments: {
|
|
1135
|
-
llm: { connectionId: `${provider}-1`, model: "test-model" },
|
|
1136
|
-
embeddings: {
|
|
1137
|
-
connectionId: `${provider}-1`,
|
|
1138
|
-
model: "embed-model",
|
|
1139
|
-
},
|
|
1140
|
-
},
|
|
1141
|
-
});
|
|
1142
|
-
const secrets = buildSecretsFromSetup(input);
|
|
1143
|
-
expect(secrets.SYSTEM_LLM_PROVIDER).toBe(provider);
|
|
1144
|
-
expect(secrets[envKey]).toBe("sk-test");
|
|
1145
|
-
}
|
|
1146
|
-
});
|
|
1147
|
-
});
|
|
1148
|
-
|
|
1149
|
-
// =====================================================================
|
|
1150
|
-
// buildConnectionEnvVarMap EDGE CASES
|
|
1151
|
-
// =====================================================================
|
|
1152
|
-
|
|
1153
|
-
describe("buildConnectionEnvVarMap edge cases", () => {
|
|
1154
|
-
it("handles a single Ollama connection (fallback to OPENAI_API_KEY)", () => {
|
|
1155
|
-
const connections: SetupConnection[] = [
|
|
1156
|
-
{
|
|
1157
|
-
id: "ollama-1",
|
|
1158
|
-
name: "Ollama",
|
|
1159
|
-
provider: "ollama",
|
|
1160
|
-
baseUrl: "http://localhost:11434",
|
|
1161
|
-
apiKey: "",
|
|
1162
|
-
},
|
|
1163
|
-
];
|
|
1164
|
-
const map = buildConnectionEnvVarMap(connections);
|
|
1165
|
-
expect(map.get("ollama-1")).toBe("OPENAI_API_KEY");
|
|
1166
|
-
});
|
|
1167
|
-
|
|
1168
|
-
it("skips connections with unsafe env var keys (hyphen creates invalid key)", () => {
|
|
1169
|
-
const connections: SetupConnection[] = [
|
|
1170
|
-
{
|
|
1171
|
-
id: "openai-1",
|
|
1172
|
-
name: "OpenAI",
|
|
1173
|
-
provider: "openai",
|
|
1174
|
-
baseUrl: "",
|
|
1175
|
-
apiKey: "sk-a",
|
|
1176
|
-
},
|
|
1177
|
-
{
|
|
1178
|
-
id: "openai-2",
|
|
1179
|
-
name: "OpenAI 2",
|
|
1180
|
-
provider: "openai",
|
|
1181
|
-
baseUrl: "",
|
|
1182
|
-
apiKey: "sk-b",
|
|
1183
|
-
},
|
|
1184
|
-
];
|
|
1185
|
-
const map = buildConnectionEnvVarMap(connections);
|
|
1186
|
-
// First gets canonical key
|
|
1187
|
-
expect(map.get("openai-1")).toBe("OPENAI_API_KEY");
|
|
1188
|
-
// Second would be OPENAI_API_KEY_OPENAI-2, which has a hyphen -> skipped
|
|
1189
|
-
expect(map.has("openai-2")).toBe(false);
|
|
1190
|
-
});
|
|
1191
|
-
|
|
1192
|
-
it("namespaces duplicate provider env vars with underscore IDs", () => {
|
|
1193
|
-
const connections: SetupConnection[] = [
|
|
1194
|
-
{
|
|
1195
|
-
id: "openai_1",
|
|
1196
|
-
name: "OpenAI 1",
|
|
1197
|
-
provider: "openai",
|
|
1198
|
-
baseUrl: "",
|
|
1199
|
-
apiKey: "sk-a",
|
|
1200
|
-
},
|
|
1201
|
-
{
|
|
1202
|
-
id: "openai_2",
|
|
1203
|
-
name: "OpenAI 2",
|
|
1204
|
-
provider: "openai",
|
|
1205
|
-
baseUrl: "",
|
|
1206
|
-
apiKey: "sk-b",
|
|
1207
|
-
},
|
|
1208
|
-
];
|
|
1209
|
-
const map = buildConnectionEnvVarMap(connections);
|
|
1210
|
-
expect(map.get("openai_1")).toBe("OPENAI_API_KEY");
|
|
1211
|
-
// openai_2 -> OPENAI_API_KEY_OPENAI_2 which is a safe key
|
|
1212
|
-
expect(map.get("openai_2")).toBe("OPENAI_API_KEY_OPENAI_2");
|
|
1213
|
-
});
|
|
1214
811
|
});
|