openpalm 0.9.8 → 0.9.10

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
@@ -1,7 +1,8 @@
1
1
  import { afterEach, describe, expect, it, mock } from 'bun:test';
2
- import { existsSync, mkdirSync, writeFileSync, chmodSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
2
+ import { existsSync, mkdirSync, writeFileSync, chmodSync, mkdtempSync, readFileSync, readdirSync, rmSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
5
6
  import { detectHostInfo, main, reconcileStackEnvImageTag, resolveRequestedImageTag, upsertEnvValue } from './main.ts';
6
7
 
7
8
  // Helpers to mock Bun.spawn and Bun.which for tests that would otherwise
@@ -57,92 +58,57 @@ describe('cli main', () => {
57
58
  process.env.OPENPALM_ADMIN_TOKEN = originalOpenPalmAdminToken;
58
59
  });
59
60
 
60
- it('calls containers pull for update (via admin when reachable)', async () => {
61
- const calls: string[] = [];
62
- process.env.OPENPALM_ADMIN_TOKEN = 'test-token';
63
- globalThis.fetch = mock(async (input: string | URL) => {
64
- calls.push(String(input));
65
- return new Response('{"ok":true}', { status: 200 });
66
- }) as typeof fetch;
67
- console.log = mock(() => {}) as typeof console.log;
68
-
69
- await main(['update']);
70
-
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[] = [];
61
+ it('runs bootstrap install directly without admin delegation', async () => {
62
+ const base = mkdtempSync(join(tmpdir(), 'openpalm-install-'));
63
+ const configHome = join(base, 'config');
64
+ const dataHome = join(base, 'data');
65
+ const stateHome = join(base, 'state');
66
+ const workDir = join(base, 'work');
67
+ const binDir = join(stateHome, 'bin');
99
68
 
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');
69
+ mkdirSync(binDir, { recursive: true });
70
+ writeFileSync(join(binDir, 'varlock'), '#!/bin/sh\nexit 0\n');
71
+ chmodSync(join(binDir, 'varlock'), 0o755);
103
72
 
104
73
  process.env.OPENPALM_CONFIG_HOME = configHome;
74
+ process.env.OPENPALM_DATA_HOME = dataHome;
75
+ process.env.OPENPALM_STATE_HOME = stateHome;
76
+ process.env.OPENPALM_WORK_DIR = workDir;
105
77
  delete process.env.ADMIN_TOKEN;
106
78
  delete process.env.OPENPALM_ADMIN_TOKEN;
107
79
 
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
- }
122
- });
123
-
124
- it('calls admin install when stack is already running and token exists', async () => {
125
- const calls: string[] = [];
126
- process.env.OPENPALM_ADMIN_TOKEN = 'test-token';
80
+ mockDockerCli();
81
+ const fetchedUrls: string[] = [];
127
82
  globalThis.fetch = mock(async (input: string | URL) => {
128
83
  const url = String(input);
129
- calls.push(url);
84
+ fetchedUrls.push(url);
130
85
  if (url.endsWith('/health')) {
131
86
  return new Response('ok', { status: 200 });
132
87
  }
133
- return new Response('{\"ok\":true}', { status: 200 });
88
+ if (url.includes('/docker-compose.yml')) {
89
+ return new Response('services: {}\n', { status: 200 });
90
+ }
91
+ if (url.includes('/Caddyfile')) {
92
+ return new Response(':80 {\n}\n', { status: 200 });
93
+ }
94
+ if (url.includes('/secrets.env.schema') || url.includes('/stack.env.schema')) {
95
+ return new Response('KEY=string\n', { status: 200 });
96
+ }
97
+ return new Response('', { status: 503 });
134
98
  }) as typeof fetch;
135
99
  console.log = mock(() => {}) as typeof console.log;
100
+ console.warn = mock(() => {}) as typeof console.warn;
136
101
 
137
- await main(['install']);
138
-
139
- expect(calls).toEqual([
140
- 'http://localhost:8100/health',
141
- 'http://localhost:8100/admin/install',
142
- ]);
102
+ try {
103
+ await main(['install', '--no-start', '--force', '--no-open']);
104
+ // Bootstrap runs directly, creating directories
105
+ expect(existsSync(join(dataHome, 'admin'))).toBe(true);
106
+ } finally {
107
+ rmSync(base, { recursive: true, force: true });
108
+ }
143
109
  });
144
110
 
145
- it('falls back to bootstrap when stack is running but no token exists', async () => {
111
+ it('creates the admin data directory during bootstrap install', async () => {
146
112
  const base = mkdtempSync(join(tmpdir(), 'openpalm-install-'));
147
113
  const configHome = join(base, 'config');
148
114
  const dataHome = join(base, 'data');
@@ -158,16 +124,12 @@ describe('cli main', () => {
158
124
  process.env.OPENPALM_DATA_HOME = dataHome;
159
125
  process.env.OPENPALM_STATE_HOME = stateHome;
160
126
  process.env.OPENPALM_WORK_DIR = workDir;
161
- delete process.env.ADMIN_TOKEN;
162
- delete process.env.OPENPALM_ADMIN_TOKEN;
163
127
 
164
128
  mockDockerCli();
165
- const fetchedUrls: string[] = [];
166
129
  globalThis.fetch = mock(async (input: string | URL) => {
167
130
  const url = String(input);
168
- fetchedUrls.push(url);
169
131
  if (url.endsWith('/health')) {
170
- return new Response('ok', { status: 200 });
132
+ throw new TypeError('fetch failed');
171
133
  }
172
134
  if (url.includes('/docker-compose.yml')) {
173
135
  return new Response('services: {}\n', { status: 200 });
@@ -181,24 +143,23 @@ describe('cli main', () => {
181
143
  return new Response('', { status: 503 });
182
144
  }) as typeof fetch;
183
145
  console.log = mock(() => {}) as typeof console.log;
184
- console.warn = mock(() => {}) as typeof console.warn;
185
146
 
186
147
  try {
187
148
  await main(['install', '--no-start', '--force', '--no-open']);
188
- // Should have fallen through to bootstrap, creating directories
189
149
  expect(existsSync(join(dataHome, 'admin'))).toBe(true);
190
150
  } finally {
191
151
  rmSync(base, { recursive: true, force: true });
192
152
  }
193
153
  });
194
154
 
195
- it('creates the admin data directory during bootstrap install', async () => {
155
+ it('resolves version-pinned install ref (falls back to CLI package version)', async () => {
196
156
  const base = mkdtempSync(join(tmpdir(), 'openpalm-install-'));
197
157
  const configHome = join(base, 'config');
198
158
  const dataHome = join(base, 'data');
199
159
  const stateHome = join(base, 'state');
200
160
  const workDir = join(base, 'work');
201
161
  const binDir = join(stateHome, 'bin');
162
+ const fetchedUrls: string[] = [];
202
163
 
203
164
  mkdirSync(binDir, { recursive: true });
204
165
  writeFileSync(join(binDir, 'varlock'), '#!/bin/sh\nexit 0\n');
@@ -209,12 +170,20 @@ describe('cli main', () => {
209
170
  process.env.OPENPALM_STATE_HOME = stateHome;
210
171
  process.env.OPENPALM_WORK_DIR = workDir;
211
172
 
173
+ // Read the CLI package version to verify pinning behaviour
174
+ const cliPkg = JSON.parse(
175
+ readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
176
+ ) as { version: string };
177
+ const expectedRef = `v${cliPkg.version}`;
178
+
212
179
  mockDockerCli();
213
180
  globalThis.fetch = mock(async (input: string | URL) => {
214
181
  const url = String(input);
182
+ fetchedUrls.push(url);
215
183
  if (url.endsWith('/health')) {
216
184
  throw new TypeError('fetch failed');
217
185
  }
186
+ // Respond to version-pinned asset URLs
218
187
  if (url.includes('/docker-compose.yml')) {
219
188
  return new Response('services: {}\n', { status: 200 });
220
189
  }
@@ -227,10 +196,20 @@ describe('cli main', () => {
227
196
  return new Response('', { status: 503 });
228
197
  }) as typeof fetch;
229
198
  console.log = mock(() => {}) as typeof console.log;
199
+ console.warn = mock(() => {}) as typeof console.warn;
230
200
 
231
201
  try {
232
202
  await main(['install', '--no-start', '--force', '--no-open']);
233
- expect(existsSync(join(dataHome, 'admin'))).toBe(true);
203
+
204
+ // Verify that assets were fetched using the version-pinned ref, not 'main'
205
+ const composeUrl = fetchedUrls.find((u) => u.includes('/docker-compose.yml'));
206
+ expect(composeUrl).toBeDefined();
207
+ expect(composeUrl).toContain(expectedRef);
208
+ expect(composeUrl).not.toContain('/main/');
209
+
210
+ const caddyUrl = fetchedUrls.find((u) => u.includes('/Caddyfile'));
211
+ expect(caddyUrl).toBeDefined();
212
+ expect(caddyUrl).toContain(expectedRef);
234
213
  } finally {
235
214
  rmSync(base, { recursive: true, force: true });
236
215
  }
@@ -251,6 +230,65 @@ describe('npm bin launcher', () => {
251
230
 
252
231
  expect(launcher.startsWith('#!/usr/bin/env bun\n')).toBe(true);
253
232
  });
233
+
234
+ it('packs a real semver range for @openpalm/lib so published installs can resolve the latest compatible lib', {
235
+ timeout: 15000,
236
+ }, () => {
237
+ const cliPkg = JSON.parse(
238
+ readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
239
+ ) as {
240
+ dependencies?: Record<string, string>;
241
+ };
242
+ const libPkg = JSON.parse(
243
+ readFileSync(new URL('../../lib/package.json', import.meta.url), 'utf8'),
244
+ ) as {
245
+ version: string;
246
+ };
247
+ const versionMatch = libPkg.version.match(/^(\d+)\.\d+\.\d+(?:-.+)?$/);
248
+ if (!versionMatch) throw new Error(`Unexpected lib version format: ${libPkg.version}`);
249
+ const libMajor = Number.parseInt(versionMatch[1], 10);
250
+
251
+ const expectedRange = `>=${libPkg.version} <${libMajor + 1}.0.0`;
252
+
253
+ expect(cliPkg.dependencies?.['@openpalm/lib']).toBe(expectedRange);
254
+
255
+ const packageDir = fileURLToPath(new URL('../', import.meta.url));
256
+ const packDir = mkdtempSync(join(tmpdir(), 'openpalm-cli-pack-'));
257
+
258
+ try {
259
+ const pack = Bun.spawnSync(
260
+ [process.execPath, 'pm', 'pack', '--destination', packDir, '--quiet'],
261
+ {
262
+ cwd: packageDir,
263
+ stdout: 'pipe',
264
+ stderr: 'pipe',
265
+ },
266
+ );
267
+
268
+ expect(pack.exitCode).toBe(0);
269
+
270
+ const tarball = readdirSync(packDir).find((name) => name.endsWith('.tgz'));
271
+ if (!tarball) throw new Error('Expected bun pm pack to produce a tarball');
272
+
273
+ const extract = Bun.spawnSync(
274
+ ['tar', '-xOf', join(packDir, tarball), 'package/package.json'],
275
+ {
276
+ stdout: 'pipe',
277
+ stderr: 'pipe',
278
+ },
279
+ );
280
+
281
+ expect(extract.exitCode).toBe(0);
282
+
283
+ const packedPkg = JSON.parse(new TextDecoder().decode(extract.stdout)) as {
284
+ dependencies?: Record<string, string>;
285
+ };
286
+
287
+ expect(packedPkg.dependencies?.['@openpalm/lib']).toBe(expectedRange);
288
+ } finally {
289
+ rmSync(packDir, { recursive: true, force: true });
290
+ }
291
+ });
254
292
  });
255
293
 
256
294
  describe('validate command', () => {