openpalm 0.9.1 → 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,23 +36,30 @@ 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;
42
43
  const originalWorkDir = process.env.OPENPALM_WORK_DIR;
44
+ const originalAdminToken = process.env.ADMIN_TOKEN;
45
+ const originalOpenPalmAdminToken = process.env.OPENPALM_ADMIN_TOKEN;
43
46
 
44
47
  afterEach(() => {
45
48
  globalThis.fetch = originalFetch;
46
49
  console.log = originalLog;
50
+ console.warn = originalWarn;
47
51
  restoreDockerCli();
48
52
  process.env.OPENPALM_CONFIG_HOME = originalConfigHome;
49
53
  process.env.OPENPALM_DATA_HOME = originalDataHome;
50
54
  process.env.OPENPALM_STATE_HOME = originalStateHome;
51
55
  process.env.OPENPALM_WORK_DIR = originalWorkDir;
56
+ process.env.ADMIN_TOKEN = originalAdminToken;
57
+ process.env.OPENPALM_ADMIN_TOKEN = originalOpenPalmAdminToken;
52
58
  });
53
59
 
54
60
  it('calls containers pull for update', async () => {
55
61
  const calls: string[] = [];
62
+ process.env.OPENPALM_ADMIN_TOKEN = 'test-token';
56
63
  globalThis.fetch = mock(async (input: string | URL) => {
57
64
  calls.push(String(input));
58
65
  return new Response('{"ok":true}', { status: 200 });
@@ -64,8 +71,54 @@ describe('cli main', () => {
64
71
  expect(calls).toEqual(['http://localhost:8100/admin/containers/pull']);
65
72
  });
66
73
 
67
- it('calls admin install when stack is already running', async () => {
74
+ it('uses ADMIN_TOKEN from the environment for admin requests', async () => {
75
+ const adminTokens: string[] = [];
76
+ process.env.ADMIN_TOKEN = 'env-admin-token';
77
+ delete process.env.OPENPALM_ADMIN_TOKEN;
78
+
79
+ globalThis.fetch = mock(async (_input: string | URL, init?: RequestInit) => {
80
+ const headers = new Headers(init?.headers);
81
+ adminTokens.push(headers.get('X-Admin-Token') ?? '');
82
+ return new Response('{"ok":true}', { status: 200 });
83
+ }) as typeof fetch;
84
+ console.log = mock(() => {}) as typeof console.log;
85
+
86
+ await main(['update']);
87
+
88
+ expect(adminTokens).toEqual(['env-admin-token']);
89
+ });
90
+
91
+ it('falls back to the legacy parent secrets.env for admin requests', async () => {
92
+ const base = mkdtempSync(join(tmpdir(), 'openpalm-config-'));
93
+ const configHome = join(base, 'openpalm');
94
+ const adminTokens: string[] = [];
95
+
96
+ mkdirSync(configHome, { recursive: true });
97
+ writeFileSync(join(configHome, 'secrets.env'), 'OPENPALM_ADMIN_TOKEN=\nADMIN_TOKEN=\n');
98
+ writeFileSync(join(base, 'secrets.env'), 'export ADMIN_TOKEN="legacy-admin-token"\n');
99
+
100
+ process.env.OPENPALM_CONFIG_HOME = configHome;
101
+ delete process.env.ADMIN_TOKEN;
102
+ delete process.env.OPENPALM_ADMIN_TOKEN;
103
+
104
+ globalThis.fetch = mock(async (_input: string | URL, init?: RequestInit) => {
105
+ const headers = new Headers(init?.headers);
106
+ adminTokens.push(headers.get('X-Admin-Token') ?? '');
107
+ return new Response('{"ok":true}', { status: 200 });
108
+ }) as typeof fetch;
109
+ console.log = mock(() => {}) as typeof console.log;
110
+
111
+ try {
112
+ await main(['update']);
113
+ expect(adminTokens).toEqual(['legacy-admin-token']);
114
+ } finally {
115
+ rmSync(base, { recursive: true, force: true });
116
+ }
117
+ });
118
+
119
+ it('calls admin install when stack is already running and token exists', async () => {
68
120
  const calls: string[] = [];
121
+ process.env.OPENPALM_ADMIN_TOKEN = 'test-token';
69
122
  globalThis.fetch = mock(async (input: string | URL) => {
70
123
  const url = String(input);
71
124
  calls.push(url);
@@ -79,13 +132,59 @@ describe('cli main', () => {
79
132
  await main(['install']);
80
133
 
81
134
  expect(calls).toEqual([
82
- 'http://127.0.0.1:8100/health',
135
+ 'http://localhost:8100/health',
83
136
  'http://localhost:8100/admin/install',
84
137
  ]);
85
138
  });
86
139
 
87
- it('throws for unknown command', async () => {
88
- 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
+ }
89
188
  });
90
189
 
91
190
  it('creates the admin data directory during bootstrap install', async () => {
@@ -134,18 +233,13 @@ describe('cli main', () => {
134
233
  });
135
234
 
136
235
  describe('validate command', () => {
137
- it('throws "Unknown command" is no longer thrown for validate', async () => {
138
- // validate is now a known command, so it won't throw Unknown command.
139
- // It will fail because varlock exits non-zero on a missing env/schema, but that's a different error.
140
- // We set up a temp state dir with a fake varlock binary that exits immediately to avoid a network download.
141
-
236
+ it('is a recognized command (does not throw Unknown command)', async () => {
142
237
  const tempStateHome = mkdtempSync(join(tmpdir(), 'openpalm-test-'));
143
238
  const binDir = join(tempStateHome, 'bin');
144
239
  const artifactsDir = join(tempStateHome, 'artifacts');
145
240
  mkdirSync(binDir, { recursive: true });
146
241
  mkdirSync(artifactsDir, { recursive: true });
147
242
 
148
- // Create a fake varlock script that exits 1 immediately
149
243
  const fakeVarlock = join(binDir, 'varlock');
150
244
  writeFileSync(fakeVarlock, '#!/bin/sh\nexit 1\n');
151
245
  chmodSync(fakeVarlock, 0o755);
@@ -153,32 +247,22 @@ describe('validate command', () => {
153
247
  const originalStateHome = process.env.OPENPALM_STATE_HOME;
154
248
  const originalExit = process.exit;
155
249
  process.env.OPENPALM_STATE_HOME = tempStateHome;
156
- // Prevent process.exit from terminating the test runner
157
250
  process.exit = mock((_code?: number) => { throw new Error(`process.exit(${_code})`); }) as typeof process.exit;
158
251
 
159
252
  try {
160
253
  const err = await main(['validate']).catch((e: unknown) => e);
161
254
  const message = err instanceof Error ? err.message : String(err);
162
- expect(message).not.toContain('Unknown command: validate');
255
+ expect(message).not.toContain('Unknown command');
163
256
  } finally {
164
257
  process.exit = originalExit;
165
258
  process.env.OPENPALM_STATE_HOME = originalStateHome;
166
259
  rmSync(tempStateHome, { recursive: true, force: true });
167
260
  }
168
261
  });
169
-
170
262
  });
171
263
 
172
264
  describe('scan command', () => {
173
265
  it('is a recognized command (does not throw Unknown command)', async () => {
174
- // Verifies 'scan' is in the COMMANDS list and dispatches correctly.
175
- // Does NOT test full scan behavior (which requires a real varlock binary,
176
- // secrets.env, and secrets.env.schema). A more complete test would:
177
- // - Stage a secrets.env.schema + secrets.env in temp dirs
178
- // - Provide a fake varlock binary that echoes its args
179
- // - Assert varlock is invoked with 'scan --path <tmpDir>/'
180
- // - Verify the temp dir is cleaned up after execution
181
-
182
266
  const tempStateHome = mkdtempSync(join(tmpdir(), 'openpalm-test-'));
183
267
  const tempConfigHome = mkdtempSync(join(tmpdir(), 'openpalm-test-'));
184
268
  const binDir = join(tempStateHome, 'bin');
@@ -186,12 +270,10 @@ describe('scan command', () => {
186
270
  mkdirSync(binDir, { recursive: true });
187
271
  mkdirSync(artifactsDir, { recursive: true });
188
272
 
189
- // Create a fake varlock script that exits 0 immediately
190
273
  const fakeVarlock = join(binDir, 'varlock');
191
274
  writeFileSync(fakeVarlock, '#!/bin/sh\nexit 0\n');
192
275
  chmodSync(fakeVarlock, 0o755);
193
276
 
194
- // Create required files so the command reaches the varlock invocation
195
277
  writeFileSync(join(artifactsDir, 'secrets.env.schema'), 'ADMIN_TOKEN\n');
196
278
  writeFileSync(join(tempConfigHome, 'secrets.env'), 'ADMIN_TOKEN=testtoken\n');
197
279
 
@@ -205,9 +287,7 @@ describe('scan command', () => {
205
287
  try {
206
288
  const err = await main(['scan']).catch((e: unknown) => e);
207
289
  const message = err instanceof Error ? err.message : String(err);
208
- // Should not be an unknown command error
209
- expect(message).not.toContain('Unknown command: scan');
210
- // The fake varlock exits 0, so process.exit(0) should be called
290
+ expect(message).not.toContain('Unknown command');
211
291
  expect(message).toBe('process.exit(0)');
212
292
  } finally {
213
293
  process.exit = originalExit;
@@ -224,7 +304,6 @@ describe('scan command', () => {
224
304
  const artifactsDir = join(tempStateHome, 'artifacts');
225
305
  mkdirSync(artifactsDir, { recursive: true });
226
306
 
227
- // secrets.env exists but secrets.env.schema does NOT
228
307
  writeFileSync(join(tempConfigHome, 'secrets.env'), 'ADMIN_TOKEN=testtoken\n');
229
308
 
230
309
  const originalStateHome = process.env.OPENPALM_STATE_HOME;
@@ -349,4 +428,33 @@ describe('install image tag pinning', () => {
349
428
  'KEY.WITH-HYPHEN=new\n',
350
429
  );
351
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
+ });
352
460
  });