openpalm 0.9.1 → 0.9.2-rc2

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
- it('calls containers pull for update', async () => {
60
+ it('calls containers pull for update (via admin when reachable)', 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 });
@@ -61,11 +68,62 @@ describe('cli main', () => {
61
68
 
62
69
  await main(['update']);
63
70
 
64
- expect(calls).toEqual(['http://localhost:8100/admin/containers/pull']);
71
+ // tryAdminRequest attempts directly — no separate health check
72
+ expect(calls).toEqual([
73
+ 'http://localhost:8100/admin/containers/pull',
74
+ ]);
75
+ });
76
+
77
+ it('uses ADMIN_TOKEN from the environment for admin requests', async () => {
78
+ const adminTokens: string[] = [];
79
+ process.env.ADMIN_TOKEN = 'env-admin-token';
80
+ delete process.env.OPENPALM_ADMIN_TOKEN;
81
+
82
+ globalThis.fetch = mock(async (_input: string | URL, init?: RequestInit) => {
83
+ const headers = new Headers(init?.headers);
84
+ adminTokens.push(headers.get('X-Admin-Token') ?? '');
85
+ return new Response('{"ok":true}', { status: 200 });
86
+ }) as typeof fetch;
87
+ console.log = mock(() => {}) as typeof console.log;
88
+
89
+ await main(['update']);
90
+
91
+ // Direct request — single call with token header
92
+ expect(adminTokens).toEqual(['env-admin-token']);
93
+ });
94
+
95
+ it('falls back to the legacy parent secrets.env for admin requests', async () => {
96
+ const base = mkdtempSync(join(tmpdir(), 'openpalm-config-'));
97
+ const configHome = join(base, 'openpalm');
98
+ const adminTokens: string[] = [];
99
+
100
+ mkdirSync(configHome, { recursive: true });
101
+ writeFileSync(join(configHome, 'secrets.env'), 'OPENPALM_ADMIN_TOKEN=\nADMIN_TOKEN=\n');
102
+ writeFileSync(join(base, 'secrets.env'), 'export ADMIN_TOKEN="legacy-admin-token"\n');
103
+
104
+ process.env.OPENPALM_CONFIG_HOME = configHome;
105
+ delete process.env.ADMIN_TOKEN;
106
+ delete process.env.OPENPALM_ADMIN_TOKEN;
107
+
108
+ globalThis.fetch = mock(async (_input: string | URL, init?: RequestInit) => {
109
+ const headers = new Headers(init?.headers);
110
+ adminTokens.push(headers.get('X-Admin-Token') ?? '');
111
+ return new Response('{"ok":true}', { status: 200 });
112
+ }) as typeof fetch;
113
+ console.log = mock(() => {}) as typeof console.log;
114
+
115
+ try {
116
+ await main(['update']);
117
+ // Direct request — single call with legacy token
118
+ expect(adminTokens).toEqual(['legacy-admin-token']);
119
+ } finally {
120
+ rmSync(base, { recursive: true, force: true });
121
+ }
65
122
  });
66
123
 
67
- it('calls admin install when stack is already running', async () => {
124
+ it('calls admin install when stack is already running and token exists', async () => {
68
125
  const calls: string[] = [];
126
+ process.env.OPENPALM_ADMIN_TOKEN = 'test-token';
69
127
  globalThis.fetch = mock(async (input: string | URL) => {
70
128
  const url = String(input);
71
129
  calls.push(url);
@@ -79,13 +137,59 @@ describe('cli main', () => {
79
137
  await main(['install']);
80
138
 
81
139
  expect(calls).toEqual([
82
- 'http://127.0.0.1:8100/health',
140
+ 'http://localhost:8100/health',
83
141
  'http://localhost:8100/admin/install',
84
142
  ]);
85
143
  });
86
144
 
87
- it('throws for unknown command', async () => {
88
- await expect(main(['nope'])).rejects.toThrow('Unknown command: nope');
145
+ it('falls back to bootstrap when stack is running but no token exists', async () => {
146
+ const base = mkdtempSync(join(tmpdir(), 'openpalm-install-'));
147
+ const configHome = join(base, 'config');
148
+ const dataHome = join(base, 'data');
149
+ const stateHome = join(base, 'state');
150
+ const workDir = join(base, 'work');
151
+ const binDir = join(stateHome, 'bin');
152
+
153
+ mkdirSync(binDir, { recursive: true });
154
+ writeFileSync(join(binDir, 'varlock'), '#!/bin/sh\nexit 0\n');
155
+ chmodSync(join(binDir, 'varlock'), 0o755);
156
+
157
+ process.env.OPENPALM_CONFIG_HOME = configHome;
158
+ process.env.OPENPALM_DATA_HOME = dataHome;
159
+ process.env.OPENPALM_STATE_HOME = stateHome;
160
+ process.env.OPENPALM_WORK_DIR = workDir;
161
+ delete process.env.ADMIN_TOKEN;
162
+ delete process.env.OPENPALM_ADMIN_TOKEN;
163
+
164
+ mockDockerCli();
165
+ const fetchedUrls: string[] = [];
166
+ globalThis.fetch = mock(async (input: string | URL) => {
167
+ const url = String(input);
168
+ fetchedUrls.push(url);
169
+ if (url.endsWith('/health')) {
170
+ return new Response('ok', { status: 200 });
171
+ }
172
+ if (url.includes('/docker-compose.yml')) {
173
+ return new Response('services: {}\n', { status: 200 });
174
+ }
175
+ if (url.includes('/Caddyfile')) {
176
+ return new Response(':80 {\n}\n', { status: 200 });
177
+ }
178
+ if (url.includes('/secrets.env.schema') || url.includes('/stack.env.schema')) {
179
+ return new Response('KEY=string\n', { status: 200 });
180
+ }
181
+ return new Response('', { status: 503 });
182
+ }) as typeof fetch;
183
+ console.log = mock(() => {}) as typeof console.log;
184
+ console.warn = mock(() => {}) as typeof console.warn;
185
+
186
+ try {
187
+ await main(['install', '--no-start', '--force', '--no-open']);
188
+ // Should have fallen through to bootstrap, creating directories
189
+ expect(existsSync(join(dataHome, 'admin'))).toBe(true);
190
+ } finally {
191
+ rmSync(base, { recursive: true, force: true });
192
+ }
89
193
  });
90
194
 
91
195
  it('creates the admin data directory during bootstrap install', async () => {
@@ -134,18 +238,13 @@ describe('cli main', () => {
134
238
  });
135
239
 
136
240
  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
-
241
+ it('is a recognized command (does not throw Unknown command)', async () => {
142
242
  const tempStateHome = mkdtempSync(join(tmpdir(), 'openpalm-test-'));
143
243
  const binDir = join(tempStateHome, 'bin');
144
244
  const artifactsDir = join(tempStateHome, 'artifacts');
145
245
  mkdirSync(binDir, { recursive: true });
146
246
  mkdirSync(artifactsDir, { recursive: true });
147
247
 
148
- // Create a fake varlock script that exits 1 immediately
149
248
  const fakeVarlock = join(binDir, 'varlock');
150
249
  writeFileSync(fakeVarlock, '#!/bin/sh\nexit 1\n');
151
250
  chmodSync(fakeVarlock, 0o755);
@@ -153,32 +252,22 @@ describe('validate command', () => {
153
252
  const originalStateHome = process.env.OPENPALM_STATE_HOME;
154
253
  const originalExit = process.exit;
155
254
  process.env.OPENPALM_STATE_HOME = tempStateHome;
156
- // Prevent process.exit from terminating the test runner
157
255
  process.exit = mock((_code?: number) => { throw new Error(`process.exit(${_code})`); }) as typeof process.exit;
158
256
 
159
257
  try {
160
258
  const err = await main(['validate']).catch((e: unknown) => e);
161
259
  const message = err instanceof Error ? err.message : String(err);
162
- expect(message).not.toContain('Unknown command: validate');
260
+ expect(message).not.toContain('Unknown command');
163
261
  } finally {
164
262
  process.exit = originalExit;
165
263
  process.env.OPENPALM_STATE_HOME = originalStateHome;
166
264
  rmSync(tempStateHome, { recursive: true, force: true });
167
265
  }
168
266
  });
169
-
170
267
  });
171
268
 
172
269
  describe('scan command', () => {
173
270
  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
271
  const tempStateHome = mkdtempSync(join(tmpdir(), 'openpalm-test-'));
183
272
  const tempConfigHome = mkdtempSync(join(tmpdir(), 'openpalm-test-'));
184
273
  const binDir = join(tempStateHome, 'bin');
@@ -186,12 +275,10 @@ describe('scan command', () => {
186
275
  mkdirSync(binDir, { recursive: true });
187
276
  mkdirSync(artifactsDir, { recursive: true });
188
277
 
189
- // Create a fake varlock script that exits 0 immediately
190
278
  const fakeVarlock = join(binDir, 'varlock');
191
279
  writeFileSync(fakeVarlock, '#!/bin/sh\nexit 0\n');
192
280
  chmodSync(fakeVarlock, 0o755);
193
281
 
194
- // Create required files so the command reaches the varlock invocation
195
282
  writeFileSync(join(artifactsDir, 'secrets.env.schema'), 'ADMIN_TOKEN\n');
196
283
  writeFileSync(join(tempConfigHome, 'secrets.env'), 'ADMIN_TOKEN=testtoken\n');
197
284
 
@@ -205,9 +292,7 @@ describe('scan command', () => {
205
292
  try {
206
293
  const err = await main(['scan']).catch((e: unknown) => e);
207
294
  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
295
+ expect(message).not.toContain('Unknown command');
211
296
  expect(message).toBe('process.exit(0)');
212
297
  } finally {
213
298
  process.exit = originalExit;
@@ -224,7 +309,6 @@ describe('scan command', () => {
224
309
  const artifactsDir = join(tempStateHome, 'artifacts');
225
310
  mkdirSync(artifactsDir, { recursive: true });
226
311
 
227
- // secrets.env exists but secrets.env.schema does NOT
228
312
  writeFileSync(join(tempConfigHome, 'secrets.env'), 'ADMIN_TOKEN=testtoken\n');
229
313
 
230
314
  const originalStateHome = process.env.OPENPALM_STATE_HOME;
@@ -349,4 +433,33 @@ describe('install image tag pinning', () => {
349
433
  'KEY.WITH-HYPHEN=new\n',
350
434
  );
351
435
  });
436
+
437
+ it('preserves export prefix when upserting a key', () => {
438
+ expect(upsertEnvValue('export OPENPALM_ADMIN_TOKEN=old\n', 'OPENPALM_ADMIN_TOKEN', 'new')).toBe(
439
+ 'export OPENPALM_ADMIN_TOKEN=new\n',
440
+ );
441
+ });
442
+
443
+ it('upserts without export prefix when original has none', () => {
444
+ expect(upsertEnvValue('OPENPALM_IMAGE_TAG=latest\n', 'OPENPALM_IMAGE_TAG', 'v1.0.0')).toBe(
445
+ 'OPENPALM_IMAGE_TAG=v1.0.0\n',
446
+ );
447
+ });
448
+ });
449
+
450
+ describe('secrets.env generation', () => {
451
+ it('generates secrets.env with export prefix and OPENPALM_ADMIN_TOKEN', async () => {
452
+ const { ensureSecrets } = await import('./lib/env.ts');
453
+ const tempDir = mkdtempSync(join(tmpdir(), 'openpalm-secrets-'));
454
+
455
+ try {
456
+ await ensureSecrets(tempDir);
457
+ const content = await Bun.file(join(tempDir, 'secrets.env')).text();
458
+ expect(content).toContain('export OPENPALM_ADMIN_TOKEN=');
459
+ expect(content).toContain('export OPENAI_API_KEY=');
460
+ expect(content).toContain('export MEMORY_USER_ID=');
461
+ } finally {
462
+ rmSync(tempDir, { recursive: true, force: true });
463
+ }
464
+ });
352
465
  });