openpalm 0.9.2 → 0.9.3

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/src/main.test.ts CHANGED
@@ -36,6 +36,7 @@ function restoreDockerCli(): void {
36
36
  describe('cli main', () => {
37
37
  const originalFetch = globalThis.fetch;
38
38
  const originalLog = console.log;
39
+ const originalWarn = console.warn;
39
40
  const originalConfigHome = process.env.OPENPALM_CONFIG_HOME;
40
41
  const originalDataHome = process.env.OPENPALM_DATA_HOME;
41
42
  const originalStateHome = process.env.OPENPALM_STATE_HOME;
@@ -46,6 +47,7 @@ describe('cli main', () => {
46
47
  afterEach(() => {
47
48
  globalThis.fetch = originalFetch;
48
49
  console.log = originalLog;
50
+ console.warn = originalWarn;
49
51
  restoreDockerCli();
50
52
  process.env.OPENPALM_CONFIG_HOME = originalConfigHome;
51
53
  process.env.OPENPALM_DATA_HOME = originalDataHome;
@@ -57,6 +59,7 @@ describe('cli main', () => {
57
59
 
58
60
  it('calls containers pull for update', async () => {
59
61
  const calls: string[] = [];
62
+ process.env.OPENPALM_ADMIN_TOKEN = 'test-token';
60
63
  globalThis.fetch = mock(async (input: string | URL) => {
61
64
  calls.push(String(input));
62
65
  return new Response('{"ok":true}', { status: 200 });
@@ -91,7 +94,7 @@ describe('cli main', () => {
91
94
  const adminTokens: string[] = [];
92
95
 
93
96
  mkdirSync(configHome, { recursive: true });
94
- writeFileSync(join(configHome, 'secrets.env'), 'ADMIN_TOKEN=\n');
97
+ writeFileSync(join(configHome, 'secrets.env'), 'OPENPALM_ADMIN_TOKEN=\nADMIN_TOKEN=\n');
95
98
  writeFileSync(join(base, 'secrets.env'), 'export ADMIN_TOKEN="legacy-admin-token"\n');
96
99
 
97
100
  process.env.OPENPALM_CONFIG_HOME = configHome;
@@ -113,8 +116,9 @@ describe('cli main', () => {
113
116
  }
114
117
  });
115
118
 
116
- it('calls admin install when stack is already running', async () => {
119
+ it('calls admin install when stack is already running and token exists', async () => {
117
120
  const calls: string[] = [];
121
+ process.env.OPENPALM_ADMIN_TOKEN = 'test-token';
118
122
  globalThis.fetch = mock(async (input: string | URL) => {
119
123
  const url = String(input);
120
124
  calls.push(url);
@@ -128,13 +132,59 @@ describe('cli main', () => {
128
132
  await main(['install']);
129
133
 
130
134
  expect(calls).toEqual([
131
- 'http://127.0.0.1:8100/health',
135
+ 'http://localhost:8100/health',
132
136
  'http://localhost:8100/admin/install',
133
137
  ]);
134
138
  });
135
139
 
136
- it('throws for unknown command', async () => {
137
- await expect(main(['nope'])).rejects.toThrow('Unknown command: nope');
140
+ it('falls back to bootstrap when stack is running but no token exists', async () => {
141
+ const base = mkdtempSync(join(tmpdir(), 'openpalm-install-'));
142
+ const configHome = join(base, 'config');
143
+ const dataHome = join(base, 'data');
144
+ const stateHome = join(base, 'state');
145
+ const workDir = join(base, 'work');
146
+ const binDir = join(stateHome, 'bin');
147
+
148
+ mkdirSync(binDir, { recursive: true });
149
+ writeFileSync(join(binDir, 'varlock'), '#!/bin/sh\nexit 0\n');
150
+ chmodSync(join(binDir, 'varlock'), 0o755);
151
+
152
+ process.env.OPENPALM_CONFIG_HOME = configHome;
153
+ process.env.OPENPALM_DATA_HOME = dataHome;
154
+ process.env.OPENPALM_STATE_HOME = stateHome;
155
+ process.env.OPENPALM_WORK_DIR = workDir;
156
+ delete process.env.ADMIN_TOKEN;
157
+ delete process.env.OPENPALM_ADMIN_TOKEN;
158
+
159
+ mockDockerCli();
160
+ const fetchedUrls: string[] = [];
161
+ globalThis.fetch = mock(async (input: string | URL) => {
162
+ const url = String(input);
163
+ fetchedUrls.push(url);
164
+ if (url.endsWith('/health')) {
165
+ return new Response('ok', { status: 200 });
166
+ }
167
+ if (url.includes('/docker-compose.yml')) {
168
+ return new Response('services: {}\n', { status: 200 });
169
+ }
170
+ if (url.includes('/Caddyfile')) {
171
+ return new Response(':80 {\n}\n', { status: 200 });
172
+ }
173
+ if (url.includes('/secrets.env.schema') || url.includes('/stack.env.schema')) {
174
+ return new Response('KEY=string\n', { status: 200 });
175
+ }
176
+ return new Response('', { status: 503 });
177
+ }) as typeof fetch;
178
+ console.log = mock(() => {}) as typeof console.log;
179
+ console.warn = mock(() => {}) as typeof console.warn;
180
+
181
+ try {
182
+ await main(['install', '--no-start', '--force', '--no-open']);
183
+ // Should have fallen through to bootstrap, creating directories
184
+ expect(existsSync(join(dataHome, 'admin'))).toBe(true);
185
+ } finally {
186
+ rmSync(base, { recursive: true, force: true });
187
+ }
138
188
  });
139
189
 
140
190
  it('creates the admin data directory during bootstrap install', async () => {
@@ -183,18 +233,13 @@ describe('cli main', () => {
183
233
  });
184
234
 
185
235
  describe('validate command', () => {
186
- it('throws "Unknown command" is no longer thrown for validate', async () => {
187
- // validate is now a known command, so it won't throw Unknown command.
188
- // It will fail because varlock exits non-zero on a missing env/schema, but that's a different error.
189
- // We set up a temp state dir with a fake varlock binary that exits immediately to avoid a network download.
190
-
236
+ it('is a recognized command (does not throw Unknown command)', async () => {
191
237
  const tempStateHome = mkdtempSync(join(tmpdir(), 'openpalm-test-'));
192
238
  const binDir = join(tempStateHome, 'bin');
193
239
  const artifactsDir = join(tempStateHome, 'artifacts');
194
240
  mkdirSync(binDir, { recursive: true });
195
241
  mkdirSync(artifactsDir, { recursive: true });
196
242
 
197
- // Create a fake varlock script that exits 1 immediately
198
243
  const fakeVarlock = join(binDir, 'varlock');
199
244
  writeFileSync(fakeVarlock, '#!/bin/sh\nexit 1\n');
200
245
  chmodSync(fakeVarlock, 0o755);
@@ -202,32 +247,22 @@ describe('validate command', () => {
202
247
  const originalStateHome = process.env.OPENPALM_STATE_HOME;
203
248
  const originalExit = process.exit;
204
249
  process.env.OPENPALM_STATE_HOME = tempStateHome;
205
- // Prevent process.exit from terminating the test runner
206
250
  process.exit = mock((_code?: number) => { throw new Error(`process.exit(${_code})`); }) as typeof process.exit;
207
251
 
208
252
  try {
209
253
  const err = await main(['validate']).catch((e: unknown) => e);
210
254
  const message = err instanceof Error ? err.message : String(err);
211
- expect(message).not.toContain('Unknown command: validate');
255
+ expect(message).not.toContain('Unknown command');
212
256
  } finally {
213
257
  process.exit = originalExit;
214
258
  process.env.OPENPALM_STATE_HOME = originalStateHome;
215
259
  rmSync(tempStateHome, { recursive: true, force: true });
216
260
  }
217
261
  });
218
-
219
262
  });
220
263
 
221
264
  describe('scan command', () => {
222
265
  it('is a recognized command (does not throw Unknown command)', async () => {
223
- // Verifies 'scan' is in the COMMANDS list and dispatches correctly.
224
- // Does NOT test full scan behavior (which requires a real varlock binary,
225
- // secrets.env, and secrets.env.schema). A more complete test would:
226
- // - Stage a secrets.env.schema + secrets.env in temp dirs
227
- // - Provide a fake varlock binary that echoes its args
228
- // - Assert varlock is invoked with 'scan --path <tmpDir>/'
229
- // - Verify the temp dir is cleaned up after execution
230
-
231
266
  const tempStateHome = mkdtempSync(join(tmpdir(), 'openpalm-test-'));
232
267
  const tempConfigHome = mkdtempSync(join(tmpdir(), 'openpalm-test-'));
233
268
  const binDir = join(tempStateHome, 'bin');
@@ -235,12 +270,10 @@ describe('scan command', () => {
235
270
  mkdirSync(binDir, { recursive: true });
236
271
  mkdirSync(artifactsDir, { recursive: true });
237
272
 
238
- // Create a fake varlock script that exits 0 immediately
239
273
  const fakeVarlock = join(binDir, 'varlock');
240
274
  writeFileSync(fakeVarlock, '#!/bin/sh\nexit 0\n');
241
275
  chmodSync(fakeVarlock, 0o755);
242
276
 
243
- // Create required files so the command reaches the varlock invocation
244
277
  writeFileSync(join(artifactsDir, 'secrets.env.schema'), 'ADMIN_TOKEN\n');
245
278
  writeFileSync(join(tempConfigHome, 'secrets.env'), 'ADMIN_TOKEN=testtoken\n');
246
279
 
@@ -254,9 +287,7 @@ describe('scan command', () => {
254
287
  try {
255
288
  const err = await main(['scan']).catch((e: unknown) => e);
256
289
  const message = err instanceof Error ? err.message : String(err);
257
- // Should not be an unknown command error
258
- expect(message).not.toContain('Unknown command: scan');
259
- // The fake varlock exits 0, so process.exit(0) should be called
290
+ expect(message).not.toContain('Unknown command');
260
291
  expect(message).toBe('process.exit(0)');
261
292
  } finally {
262
293
  process.exit = originalExit;
@@ -273,7 +304,6 @@ describe('scan command', () => {
273
304
  const artifactsDir = join(tempStateHome, 'artifacts');
274
305
  mkdirSync(artifactsDir, { recursive: true });
275
306
 
276
- // secrets.env exists but secrets.env.schema does NOT
277
307
  writeFileSync(join(tempConfigHome, 'secrets.env'), 'ADMIN_TOKEN=testtoken\n');
278
308
 
279
309
  const originalStateHome = process.env.OPENPALM_STATE_HOME;
@@ -398,4 +428,33 @@ describe('install image tag pinning', () => {
398
428
  'KEY.WITH-HYPHEN=new\n',
399
429
  );
400
430
  });
431
+
432
+ it('preserves export prefix when upserting a key', () => {
433
+ expect(upsertEnvValue('export OPENPALM_ADMIN_TOKEN=old\n', 'OPENPALM_ADMIN_TOKEN', 'new')).toBe(
434
+ 'export OPENPALM_ADMIN_TOKEN=new\n',
435
+ );
436
+ });
437
+
438
+ it('upserts without export prefix when original has none', () => {
439
+ expect(upsertEnvValue('OPENPALM_IMAGE_TAG=latest\n', 'OPENPALM_IMAGE_TAG', 'v1.0.0')).toBe(
440
+ 'OPENPALM_IMAGE_TAG=v1.0.0\n',
441
+ );
442
+ });
443
+ });
444
+
445
+ describe('secrets.env generation', () => {
446
+ it('generates secrets.env with export prefix and OPENPALM_ADMIN_TOKEN', async () => {
447
+ const { ensureSecrets } = await import('./lib/env.ts');
448
+ const tempDir = mkdtempSync(join(tmpdir(), 'openpalm-secrets-'));
449
+
450
+ try {
451
+ await ensureSecrets(tempDir);
452
+ const content = await Bun.file(join(tempDir, 'secrets.env')).text();
453
+ expect(content).toContain('export OPENPALM_ADMIN_TOKEN=');
454
+ expect(content).toContain('export OPENAI_API_KEY=');
455
+ expect(content).toContain('export MEMORY_USER_ID=');
456
+ } finally {
457
+ rmSync(tempDir, { recursive: true, force: true });
458
+ }
459
+ });
401
460
  });