@openpalm/lib 0.10.1 → 0.11.0-beta.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 +2 -2
- package/package.json +7 -3
- package/src/control-plane/admin-token.ts +73 -0
- package/src/control-plane/akm-vault.test.ts +108 -0
- package/src/control-plane/akm-vault.ts +307 -0
- package/src/control-plane/audit.ts +3 -2
- package/src/control-plane/channels.ts +3 -3
- package/src/control-plane/cleanup-guardrails.test.ts +8 -9
- package/src/control-plane/compose-args.test.ts +25 -21
- package/src/control-plane/config-persistence.ts +103 -64
- package/src/control-plane/core-assets.test.ts +104 -0
- package/src/control-plane/core-assets.ts +54 -57
- package/src/control-plane/docker.ts +55 -21
- package/src/control-plane/env.test.ts +25 -1
- package/src/control-plane/env.ts +80 -0
- package/src/control-plane/home.ts +66 -69
- package/src/control-plane/host-opencode.test.ts +263 -0
- package/src/control-plane/host-opencode.ts +229 -0
- package/src/control-plane/install-edge-cases.test.ts +182 -244
- package/src/control-plane/install-lock.ts +157 -0
- package/src/control-plane/lifecycle.ts +57 -56
- package/src/control-plane/markdown-task.ts +200 -0
- package/src/control-plane/paths.ts +75 -0
- package/src/control-plane/provider-config.ts +2 -2
- package/src/control-plane/provider-models.ts +154 -0
- package/src/control-plane/registry-components.test.ts +102 -25
- package/src/control-plane/registry.test.ts +49 -47
- package/src/control-plane/registry.ts +71 -50
- package/src/control-plane/rollback.ts +17 -16
- package/src/control-plane/scheduler.ts +75 -262
- package/src/control-plane/secret-backend.test.ts +98 -108
- package/src/control-plane/secret-backend.ts +221 -181
- package/src/control-plane/secret-mappings.ts +3 -6
- package/src/control-plane/secrets.ts +83 -47
- package/src/control-plane/setup-config.schema.json +2 -14
- package/src/control-plane/setup-status.ts +4 -29
- package/src/control-plane/setup-validation.ts +21 -21
- package/src/control-plane/setup.test.ts +122 -227
- package/src/control-plane/setup.ts +224 -125
- package/src/control-plane/skeleton-guardrail.test.ts +151 -0
- package/src/control-plane/spec-to-env.test.ts +59 -58
- package/src/control-plane/spec-to-env.ts +39 -140
- package/src/control-plane/spec-validator.ts +2 -99
- package/src/control-plane/stack-spec.test.ts +21 -77
- package/src/control-plane/stack-spec.ts +7 -83
- package/src/control-plane/types.ts +17 -15
- package/src/control-plane/ui-assets.ts +349 -0
- package/src/control-plane/validate.ts +44 -79
- package/src/index.ts +77 -44
- package/src/logger.test.ts +228 -0
- package/src/logger.ts +71 -1
- package/src/provider-constants.ts +22 -1
- package/src/control-plane/env-schema-validation.test.ts +0 -118
- package/src/control-plane/memory-config.ts +0 -298
- package/src/control-plane/redact-schema.ts +0 -50
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
2
|
-
import { lstatSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { appendFileSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import {
|
|
@@ -8,35 +8,34 @@ import {
|
|
|
8
8
|
ensureSecrets,
|
|
9
9
|
validatePassEntryName,
|
|
10
10
|
} from '../index.js';
|
|
11
|
-
import { PlaintextBackend, PassBackend } from './secret-backend.js';
|
|
12
|
-
import { generateRedactSchema } from './redact-schema.js';
|
|
13
|
-
import { getCoreSecretMappings } from './secret-mappings.js';
|
|
14
11
|
import { writeSecretProviderConfig } from './provider-config.js';
|
|
12
|
+
import { akmUserVaultPathSync } from './akm-vault.js';
|
|
13
|
+
import { dirname } from 'node:path';
|
|
15
14
|
|
|
16
15
|
let rootDir = '';
|
|
17
16
|
|
|
18
17
|
function createState(): ControlPlaneState {
|
|
19
|
-
const
|
|
20
|
-
const dataDir = join(rootDir, 'data');
|
|
18
|
+
const stateDir = join(rootDir, 'state');
|
|
21
19
|
const configDir = join(rootDir, 'config');
|
|
22
|
-
const
|
|
20
|
+
const stackDir = join(configDir, 'stack');
|
|
23
21
|
const cacheDir = join(rootDir, 'cache');
|
|
24
|
-
mkdirSync(
|
|
25
|
-
mkdirSync(
|
|
22
|
+
mkdirSync(stateDir, { recursive: true });
|
|
23
|
+
mkdirSync(stackDir, { recursive: true });
|
|
26
24
|
mkdirSync(configDir, { recursive: true });
|
|
27
|
-
mkdirSync(
|
|
25
|
+
mkdirSync(join(rootDir, 'stash'), { recursive: true });
|
|
26
|
+
mkdirSync(join(rootDir, 'workspace'), { recursive: true });
|
|
28
27
|
mkdirSync(cacheDir, { recursive: true });
|
|
29
28
|
|
|
30
29
|
return {
|
|
31
30
|
adminToken: 'admin-token',
|
|
32
31
|
assistantToken: '',
|
|
33
|
-
setupToken: 'setup-token',
|
|
34
32
|
homeDir: rootDir,
|
|
35
33
|
configDir,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
logsDir,
|
|
34
|
+
stashDir: join(rootDir, 'stash'),
|
|
35
|
+
workspaceDir: join(rootDir, 'workspace'),
|
|
39
36
|
cacheDir,
|
|
37
|
+
stateDir,
|
|
38
|
+
stackDir,
|
|
40
39
|
services: {},
|
|
41
40
|
artifacts: { compose: '' },
|
|
42
41
|
artifactMeta: [],
|
|
@@ -55,11 +54,11 @@ afterEach(() => {
|
|
|
55
54
|
describe('secret backend', () => {
|
|
56
55
|
test('ensureSecrets repairs auth.json when Docker created it as a directory', () => {
|
|
57
56
|
const state = createState();
|
|
58
|
-
mkdirSync(join(state.
|
|
57
|
+
mkdirSync(join(state.configDir, "auth.json"), { recursive: true });
|
|
59
58
|
|
|
60
59
|
ensureSecrets(state);
|
|
61
60
|
|
|
62
|
-
const authJsonPath = join(state.
|
|
61
|
+
const authJsonPath = join(state.configDir, "auth.json");
|
|
63
62
|
expect(lstatSync(authJsonPath).isFile()).toBe(true);
|
|
64
63
|
expect(readFileSync(authJsonPath, 'utf-8')).toBe('{}\n');
|
|
65
64
|
});
|
|
@@ -70,6 +69,9 @@ describe('secret backend', () => {
|
|
|
70
69
|
const backend = detectSecretBackend(state);
|
|
71
70
|
|
|
72
71
|
expect(backend.provider).toBe('plaintext');
|
|
72
|
+
expect(backend.capabilities.generate).toBe(true);
|
|
73
|
+
expect(backend.capabilities.remove).toBe(true);
|
|
74
|
+
expect(backend.capabilities.rename).toBe(false);
|
|
73
75
|
|
|
74
76
|
const entry = await backend.write('openpalm/custom/example', 'very-secret');
|
|
75
77
|
expect(entry.provider).toBe('plaintext');
|
|
@@ -77,7 +79,7 @@ describe('secret backend', () => {
|
|
|
77
79
|
expect(await backend.exists('openpalm/custom/example')).toBe(true);
|
|
78
80
|
|
|
79
81
|
// Custom secrets are now written to stack.env (all secrets consolidated there)
|
|
80
|
-
const stackEnv = readFileSync(join(state.
|
|
82
|
+
const stackEnv = readFileSync(join(state.stackDir, "stack.env"), 'utf-8');
|
|
81
83
|
expect(stackEnv).toContain('very-secret');
|
|
82
84
|
});
|
|
83
85
|
|
|
@@ -109,11 +111,11 @@ describe('secret backend', () => {
|
|
|
109
111
|
});
|
|
110
112
|
});
|
|
111
113
|
|
|
112
|
-
describe('
|
|
114
|
+
describe('plaintext backend (via detectSecretBackend)', () => {
|
|
113
115
|
test('remove clears value for non-core secrets', async () => {
|
|
114
116
|
const state = createState();
|
|
115
117
|
ensureSecrets(state);
|
|
116
|
-
const backend =
|
|
118
|
+
const backend = detectSecretBackend(state);
|
|
117
119
|
|
|
118
120
|
await backend.write('openpalm/custom/temp', 'temp-value');
|
|
119
121
|
expect(await backend.exists('openpalm/custom/temp')).toBe(true);
|
|
@@ -132,7 +134,7 @@ describe('PlaintextBackend', () => {
|
|
|
132
134
|
test('remove clears value but keeps index for core secrets', async () => {
|
|
133
135
|
const state = createState();
|
|
134
136
|
ensureSecrets(state);
|
|
135
|
-
const backend =
|
|
137
|
+
const backend = detectSecretBackend(state);
|
|
136
138
|
|
|
137
139
|
// Write a core secret
|
|
138
140
|
await backend.write('openpalm/admin-token', 'my-token');
|
|
@@ -150,7 +152,7 @@ describe('PlaintextBackend', () => {
|
|
|
150
152
|
test('list includes both core and indexed entries', async () => {
|
|
151
153
|
const state = createState();
|
|
152
154
|
ensureSecrets(state);
|
|
153
|
-
const backend =
|
|
155
|
+
const backend = detectSecretBackend(state);
|
|
154
156
|
|
|
155
157
|
await backend.write('openpalm/custom/my-key', 'value');
|
|
156
158
|
|
|
@@ -166,16 +168,71 @@ describe('PlaintextBackend', () => {
|
|
|
166
168
|
test('generate creates a secret with random value', async () => {
|
|
167
169
|
const state = createState();
|
|
168
170
|
ensureSecrets(state);
|
|
169
|
-
const backend =
|
|
171
|
+
const backend = detectSecretBackend(state);
|
|
170
172
|
|
|
171
173
|
const entry = await backend.generate('openpalm/custom/generated', 64);
|
|
172
174
|
expect(entry.present).toBe(true);
|
|
173
175
|
expect(await backend.exists('openpalm/custom/generated')).toBe(true);
|
|
174
176
|
});
|
|
177
|
+
|
|
178
|
+
test('user-scope reads from akm vault, system-scope reads from stack.env', async () => {
|
|
179
|
+
// Regression test: user scope must consult the akm vault file, system scope
|
|
180
|
+
// must consult state/stack.env. When both files define the same key with
|
|
181
|
+
// different values, the two scopes must return their own file's value.
|
|
182
|
+
const state = createState();
|
|
183
|
+
ensureSecrets(state);
|
|
184
|
+
const backend = detectSecretBackend(state);
|
|
185
|
+
|
|
186
|
+
// Seed the akm vault file with a user-scope value.
|
|
187
|
+
const akmPath = akmUserVaultPathSync(state);
|
|
188
|
+
mkdirSync(dirname(akmPath), { recursive: true });
|
|
189
|
+
writeFileSync(akmPath, 'OPENAI_API_KEY=akm-vault-openai\n');
|
|
190
|
+
|
|
191
|
+
// Stack.env already exists from ensureSecrets — seed a system token.
|
|
192
|
+
const stackEnvPath = join(state.stackDir, "stack.env");
|
|
193
|
+
const stackContent = readFileSync(stackEnvPath, 'utf-8')
|
|
194
|
+
.replace(/^OP_UI_TOKEN=.*$/m, 'OP_UI_TOKEN=stack-admin-token');
|
|
195
|
+
writeFileSync(stackEnvPath, stackContent);
|
|
196
|
+
|
|
197
|
+
// System scope reads stack.env exclusively.
|
|
198
|
+
expect(await backend.exists('openpalm/admin-token')).toBe(true);
|
|
199
|
+
const systemEntries = await backend.list('openpalm/admin-token');
|
|
200
|
+
expect(systemEntries.find((e) => e.key === 'openpalm/admin-token')?.present).toBe(true);
|
|
201
|
+
|
|
202
|
+
// User scope reads akm vault file.
|
|
203
|
+
const userEntries = await backend.list('openpalm/openai/');
|
|
204
|
+
const openai = userEntries.find((e) => e.key === 'openpalm/openai/api-key');
|
|
205
|
+
expect(openai).toBeDefined();
|
|
206
|
+
expect(openai?.scope).toBe('user');
|
|
207
|
+
expect(openai?.present).toBe(true);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('list/exists resolve user-scope secrets from akm vault', async () => {
|
|
211
|
+
// The backend MUST resolve user-managed secrets through the akm vault file
|
|
212
|
+
// (stash/vaults/user.env), not from any legacy path.
|
|
213
|
+
const state = createState();
|
|
214
|
+
ensureSecrets(state);
|
|
215
|
+
const backend = detectSecretBackend(state);
|
|
216
|
+
|
|
217
|
+
// Place the secret in the akm vault file.
|
|
218
|
+
const akmPath = akmUserVaultPathSync(state);
|
|
219
|
+
mkdirSync(dirname(akmPath), { recursive: true });
|
|
220
|
+
writeFileSync(akmPath, 'OPENAI_API_KEY=migrated-akm-value\n');
|
|
221
|
+
|
|
222
|
+
// exists() must report the user-scope secret as present.
|
|
223
|
+
expect(await backend.exists('openpalm/openai/api-key')).toBe(true);
|
|
224
|
+
|
|
225
|
+
// list() must enumerate it with present: true.
|
|
226
|
+
const entries = await backend.list('openpalm/openai/');
|
|
227
|
+
const openai = entries.find((e) => e.key === 'openpalm/openai/api-key');
|
|
228
|
+
expect(openai).toBeDefined();
|
|
229
|
+
expect(openai?.scope).toBe('user');
|
|
230
|
+
expect(openai?.present).toBe(true);
|
|
231
|
+
});
|
|
175
232
|
});
|
|
176
233
|
|
|
177
|
-
describe('
|
|
178
|
-
test('
|
|
234
|
+
describe('pass backend (via detectSecretBackend)', () => {
|
|
235
|
+
test('reports pass provider when configured', () => {
|
|
179
236
|
const state = createState();
|
|
180
237
|
writeSecretProviderConfig(state, {
|
|
181
238
|
provider: 'pass',
|
|
@@ -183,40 +240,42 @@ describe('PassBackend', () => {
|
|
|
183
240
|
passPrefix: 'myprefix',
|
|
184
241
|
});
|
|
185
242
|
|
|
186
|
-
const backend =
|
|
243
|
+
const backend = detectSecretBackend(state);
|
|
187
244
|
expect(backend.provider).toBe('pass');
|
|
188
|
-
// Verify it doesn't throw with valid config
|
|
189
245
|
expect(backend.capabilities.generate).toBe(true);
|
|
190
246
|
});
|
|
191
247
|
|
|
192
|
-
test('
|
|
248
|
+
test('uses default store dir when no config', () => {
|
|
193
249
|
const state = createState();
|
|
194
|
-
|
|
250
|
+
writeSecretProviderConfig(state, { provider: 'pass' });
|
|
251
|
+
const backend = detectSecretBackend(state);
|
|
195
252
|
expect(backend.provider).toBe('pass');
|
|
196
253
|
});
|
|
197
254
|
|
|
198
255
|
test('exists returns false for non-existent entries', async () => {
|
|
199
256
|
const state = createState();
|
|
200
|
-
const storeDir = join(rootDir, '
|
|
257
|
+
const storeDir = join(rootDir, 'state', 'secrets', 'pass-store');
|
|
201
258
|
mkdirSync(storeDir, { recursive: true });
|
|
259
|
+
writeSecretProviderConfig(state, { provider: 'pass', passwordStoreDir: storeDir });
|
|
202
260
|
|
|
203
|
-
const backend =
|
|
261
|
+
const backend = detectSecretBackend(state);
|
|
204
262
|
expect(await backend.exists('openpalm/nonexistent')).toBe(false);
|
|
205
263
|
});
|
|
206
264
|
|
|
207
265
|
test('list returns empty array for empty store', async () => {
|
|
208
266
|
const state = createState();
|
|
209
|
-
const storeDir = join(rootDir, '
|
|
267
|
+
const storeDir = join(rootDir, 'state', 'secrets', 'pass-store');
|
|
210
268
|
mkdirSync(storeDir, { recursive: true });
|
|
269
|
+
writeSecretProviderConfig(state, { provider: 'pass', passwordStoreDir: storeDir });
|
|
211
270
|
|
|
212
|
-
const backend =
|
|
271
|
+
const backend = detectSecretBackend(state);
|
|
213
272
|
const entries = await backend.list();
|
|
214
273
|
expect(entries).toEqual([]);
|
|
215
274
|
});
|
|
216
275
|
|
|
217
276
|
test('list scopes to passPrefix subdirectory', async () => {
|
|
218
277
|
const state = createState();
|
|
219
|
-
const storeDir = join(rootDir, '
|
|
278
|
+
const storeDir = join(rootDir, 'state', 'secrets', 'pass-store');
|
|
220
279
|
|
|
221
280
|
// Create fake .gpg files under the prefix subdirectory
|
|
222
281
|
const prefixDir = join(storeDir, 'myprefix', 'openpalm');
|
|
@@ -234,7 +293,7 @@ describe('PassBackend', () => {
|
|
|
234
293
|
passPrefix: 'myprefix',
|
|
235
294
|
});
|
|
236
295
|
|
|
237
|
-
const backend =
|
|
296
|
+
const backend = detectSecretBackend(state);
|
|
238
297
|
const entries = await backend.list();
|
|
239
298
|
|
|
240
299
|
expect(entries).toHaveLength(2);
|
|
@@ -245,7 +304,7 @@ describe('PassBackend', () => {
|
|
|
245
304
|
|
|
246
305
|
test('exists checks prefixed path in store', async () => {
|
|
247
306
|
const state = createState();
|
|
248
|
-
const storeDir = join(rootDir, '
|
|
307
|
+
const storeDir = join(rootDir, 'state', 'secrets', 'pass-store');
|
|
249
308
|
const prefixDir = join(storeDir, 'myprefix');
|
|
250
309
|
mkdirSync(join(prefixDir, 'openpalm'), { recursive: true });
|
|
251
310
|
writeFileSync(join(prefixDir, 'openpalm', 'admin-token.gpg'), 'fake');
|
|
@@ -256,21 +315,20 @@ describe('PassBackend', () => {
|
|
|
256
315
|
passPrefix: 'myprefix',
|
|
257
316
|
});
|
|
258
317
|
|
|
259
|
-
const backend =
|
|
318
|
+
const backend = detectSecretBackend(state);
|
|
260
319
|
expect(await backend.exists('openpalm/admin-token')).toBe(true);
|
|
261
320
|
expect(await backend.exists('openpalm/nonexistent')).toBe(false);
|
|
262
321
|
});
|
|
263
322
|
});
|
|
264
323
|
|
|
265
324
|
describe('detectSecretBackend', () => {
|
|
266
|
-
test('returns
|
|
325
|
+
test('returns plaintext provider by default', () => {
|
|
267
326
|
const state = createState();
|
|
268
327
|
const backend = detectSecretBackend(state);
|
|
269
328
|
expect(backend.provider).toBe('plaintext');
|
|
270
|
-
expect(backend).toBeInstanceOf(PlaintextBackend);
|
|
271
329
|
});
|
|
272
330
|
|
|
273
|
-
test('returns
|
|
331
|
+
test('returns pass provider when provider.json has provider: pass', () => {
|
|
274
332
|
const state = createState();
|
|
275
333
|
writeSecretProviderConfig(state, {
|
|
276
334
|
provider: 'pass',
|
|
@@ -279,81 +337,13 @@ describe('detectSecretBackend', () => {
|
|
|
279
337
|
|
|
280
338
|
const backend = detectSecretBackend(state);
|
|
281
339
|
expect(backend.provider).toBe('pass');
|
|
282
|
-
expect(backend).toBeInstanceOf(PassBackend);
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
test('returns PassBackend when schema contains @varlock/pass-plugin', () => {
|
|
286
|
-
const state = createState();
|
|
287
|
-
mkdirSync(join(state.vaultDir, 'user'), { recursive: true });
|
|
288
|
-
writeFileSync(
|
|
289
|
-
join(state.vaultDir, 'user', 'user.env.schema'),
|
|
290
|
-
'# @plugin(@varlock/pass-plugin)\nOPENAI_API_KEY=pass("openpalm/openai/api-key")\n',
|
|
291
|
-
);
|
|
292
|
-
|
|
293
|
-
const backend = detectSecretBackend(state);
|
|
294
|
-
expect(backend.provider).toBe('pass');
|
|
295
|
-
expect(backend).toBeInstanceOf(PassBackend);
|
|
296
340
|
});
|
|
297
341
|
|
|
298
|
-
test('returns
|
|
342
|
+
test('returns plaintext provider when provider.json has provider: plaintext', () => {
|
|
299
343
|
const state = createState();
|
|
300
344
|
writeSecretProviderConfig(state, { provider: 'plaintext' });
|
|
301
345
|
|
|
302
346
|
const backend = detectSecretBackend(state);
|
|
303
347
|
expect(backend.provider).toBe('plaintext');
|
|
304
|
-
expect(backend).toBeInstanceOf(PlaintextBackend);
|
|
305
348
|
});
|
|
306
349
|
});
|
|
307
|
-
|
|
308
|
-
describe('generateRedactSchema', () => {
|
|
309
|
-
test('output includes all mapped env keys', () => {
|
|
310
|
-
const systemEnv: Record<string, string> = {};
|
|
311
|
-
const schema = generateRedactSchema(systemEnv);
|
|
312
|
-
|
|
313
|
-
// All static core mappings should be present
|
|
314
|
-
expect(schema).toContain('OP_ADMIN_TOKEN=');
|
|
315
|
-
expect(schema).toContain('OP_ASSISTANT_TOKEN=');
|
|
316
|
-
expect(schema).toContain('OP_MEMORY_TOKEN=');
|
|
317
|
-
expect(schema).toContain('OPENAI_API_KEY=');
|
|
318
|
-
expect(schema).toContain('ANTHROPIC_API_KEY=');
|
|
319
|
-
expect(schema).toContain('GROQ_API_KEY=');
|
|
320
|
-
expect(schema).toContain('MISTRAL_API_KEY=');
|
|
321
|
-
expect(schema).toContain('GOOGLE_API_KEY=');
|
|
322
|
-
expect(schema).toContain('MCP_API_KEY=');
|
|
323
|
-
expect(schema).toContain('EMBEDDING_API_KEY=');
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
test('includes legacy aliases', () => {
|
|
327
|
-
const schema = generateRedactSchema({});
|
|
328
|
-
expect(schema).toContain('ADMIN_TOKEN=');
|
|
329
|
-
expect(schema).toContain('OP_OPENCODE_PASSWORD=');
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
test('includes dynamic channel secrets', () => {
|
|
333
|
-
const systemEnv = {
|
|
334
|
-
CHANNEL_DISCORD_SECRET: 'abc123',
|
|
335
|
-
CHANNEL_SLACK_SECRET: 'def456',
|
|
336
|
-
};
|
|
337
|
-
const schema = generateRedactSchema(systemEnv);
|
|
338
|
-
expect(schema).toContain('CHANNEL_DISCORD_SECRET=');
|
|
339
|
-
expect(schema).toContain('CHANNEL_SLACK_SECRET=');
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
test('has correct header format', () => {
|
|
343
|
-
const schema = generateRedactSchema({});
|
|
344
|
-
expect(schema).toContain('@defaultSensitive=true');
|
|
345
|
-
expect(schema).toContain('@defaultRequired=false');
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
test('entries are sorted', () => {
|
|
349
|
-
const schema = generateRedactSchema({});
|
|
350
|
-
const lines = schema
|
|
351
|
-
.split('\n')
|
|
352
|
-
.filter((l) => l.match(/^[A-Z]/))
|
|
353
|
-
.map((l) => l.replace(/=.*$/, ''));
|
|
354
|
-
|
|
355
|
-
const sorted = [...lines].sort();
|
|
356
|
-
expect(lines).toEqual(sorted);
|
|
357
|
-
});
|
|
358
|
-
});
|
|
359
|
-
|