cli4ai 0.9.2 → 1.0.0
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/LICENSE +109 -0
- package/README.md +95 -177
- package/package.json +2 -2
- package/src/commands/init.ts +1 -1
- package/src/commands/init.test.ts +0 -211
- package/src/core/config.test.ts +0 -188
- package/src/core/link.test.ts +0 -238
- package/src/core/lockfile.test.ts +0 -337
- package/src/core/manifest.test.ts +0 -327
- package/src/core/routine-engine.test.ts +0 -139
- package/src/core/scheduler.test.ts +0 -291
- package/src/core/secrets.test.ts +0 -79
- package/src/mcp/adapter.test.ts +0 -132
- package/src/mcp/config-gen.test.ts +0 -214
|
@@ -1,337 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for lockfile.ts
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, test, expect, beforeEach, afterEach } from 'vitest';
|
|
6
|
-
import { mkdtempSync, rmSync, readFileSync, writeFileSync } from 'fs';
|
|
7
|
-
import { join } from 'path';
|
|
8
|
-
import { tmpdir } from 'os';
|
|
9
|
-
import {
|
|
10
|
-
getLockfilePath,
|
|
11
|
-
lockfileExists,
|
|
12
|
-
loadLockfile,
|
|
13
|
-
saveLockfile,
|
|
14
|
-
lockPackage,
|
|
15
|
-
unlockPackage,
|
|
16
|
-
getLockedPackage,
|
|
17
|
-
getLockedPackages,
|
|
18
|
-
isPackageLocked,
|
|
19
|
-
clearLockfile,
|
|
20
|
-
formatLockfile,
|
|
21
|
-
LOCKFILE_NAME,
|
|
22
|
-
LOCKFILE_VERSION,
|
|
23
|
-
type Lockfile,
|
|
24
|
-
type LockedPackage
|
|
25
|
-
} from './lockfile.js';
|
|
26
|
-
|
|
27
|
-
describe('lockfile', () => {
|
|
28
|
-
let tempDir: string;
|
|
29
|
-
|
|
30
|
-
beforeEach(() => {
|
|
31
|
-
tempDir = mkdtempSync(join(tmpdir(), 'cli4ai-lockfile-test-'));
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
afterEach(() => {
|
|
35
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
describe('getLockfilePath', () => {
|
|
39
|
-
test('returns correct path', () => {
|
|
40
|
-
const path = getLockfilePath(tempDir);
|
|
41
|
-
expect(path).toBe(join(tempDir, LOCKFILE_NAME));
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
describe('lockfileExists', () => {
|
|
46
|
-
test('returns false when no lockfile', () => {
|
|
47
|
-
expect(lockfileExists(tempDir)).toBe(false);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test('returns true when lockfile exists', () => {
|
|
51
|
-
writeFileSync(join(tempDir, LOCKFILE_NAME), '{}');
|
|
52
|
-
expect(lockfileExists(tempDir)).toBe(true);
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
describe('loadLockfile', () => {
|
|
57
|
-
test('returns empty lockfile when none exists', () => {
|
|
58
|
-
const lockfile = loadLockfile(tempDir);
|
|
59
|
-
|
|
60
|
-
expect(lockfile.lockfileVersion).toBe(LOCKFILE_VERSION);
|
|
61
|
-
expect(lockfile.packages).toEqual({});
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test('loads existing lockfile', () => {
|
|
65
|
-
const existing: Lockfile = {
|
|
66
|
-
lockfileVersion: 1,
|
|
67
|
-
packages: {
|
|
68
|
-
github: {
|
|
69
|
-
name: 'github',
|
|
70
|
-
version: '1.0.0',
|
|
71
|
-
resolved: '/path/to/github'
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
writeFileSync(
|
|
77
|
-
join(tempDir, LOCKFILE_NAME),
|
|
78
|
-
JSON.stringify(existing)
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
const lockfile = loadLockfile(tempDir);
|
|
82
|
-
expect(lockfile.packages.github.name).toBe('github');
|
|
83
|
-
expect(lockfile.packages.github.version).toBe('1.0.0');
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test('handles invalid JSON gracefully', () => {
|
|
87
|
-
writeFileSync(join(tempDir, LOCKFILE_NAME), 'invalid json');
|
|
88
|
-
|
|
89
|
-
const lockfile = loadLockfile(tempDir);
|
|
90
|
-
expect(lockfile.lockfileVersion).toBe(LOCKFILE_VERSION);
|
|
91
|
-
expect(lockfile.packages).toEqual({});
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
describe('saveLockfile', () => {
|
|
96
|
-
test('saves lockfile to disk', () => {
|
|
97
|
-
const lockfile: Lockfile = {
|
|
98
|
-
lockfileVersion: 1,
|
|
99
|
-
packages: {
|
|
100
|
-
tool: {
|
|
101
|
-
name: 'tool',
|
|
102
|
-
version: '2.0.0',
|
|
103
|
-
resolved: '/some/path'
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
saveLockfile(tempDir, lockfile);
|
|
109
|
-
|
|
110
|
-
const content = readFileSync(join(tempDir, LOCKFILE_NAME), 'utf-8');
|
|
111
|
-
const parsed = JSON.parse(content);
|
|
112
|
-
|
|
113
|
-
expect(parsed.lockfileVersion).toBe(1);
|
|
114
|
-
expect(parsed.packages.tool.version).toBe('2.0.0');
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
test('formats JSON with indentation', () => {
|
|
118
|
-
const lockfile: Lockfile = {
|
|
119
|
-
lockfileVersion: 1,
|
|
120
|
-
packages: {}
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
saveLockfile(tempDir, lockfile);
|
|
124
|
-
|
|
125
|
-
const content = readFileSync(join(tempDir, LOCKFILE_NAME), 'utf-8');
|
|
126
|
-
expect(content).toContain(' '); // Has indentation
|
|
127
|
-
expect(content.endsWith('\n')).toBe(true); // Trailing newline
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
describe('lockPackage', () => {
|
|
132
|
-
test('adds new package to lockfile', () => {
|
|
133
|
-
const pkg: LockedPackage = {
|
|
134
|
-
name: 'new-tool',
|
|
135
|
-
version: '1.0.0',
|
|
136
|
-
resolved: '/path/to/tool'
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
lockPackage(tempDir, pkg);
|
|
140
|
-
|
|
141
|
-
const lockfile = loadLockfile(tempDir);
|
|
142
|
-
expect(lockfile.packages['new-tool']).toEqual(pkg);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
test('updates existing package', () => {
|
|
146
|
-
lockPackage(tempDir, {
|
|
147
|
-
name: 'tool',
|
|
148
|
-
version: '1.0.0',
|
|
149
|
-
resolved: '/old/path'
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
lockPackage(tempDir, {
|
|
153
|
-
name: 'tool',
|
|
154
|
-
version: '2.0.0',
|
|
155
|
-
resolved: '/new/path'
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
const lockfile = loadLockfile(tempDir);
|
|
159
|
-
expect(lockfile.packages.tool.version).toBe('2.0.0');
|
|
160
|
-
expect(lockfile.packages.tool.resolved).toBe('/new/path');
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
test('preserves other packages when adding', () => {
|
|
164
|
-
lockPackage(tempDir, { name: 'a', version: '1.0.0', resolved: '/a' });
|
|
165
|
-
lockPackage(tempDir, { name: 'b', version: '1.0.0', resolved: '/b' });
|
|
166
|
-
lockPackage(tempDir, { name: 'c', version: '1.0.0', resolved: '/c' });
|
|
167
|
-
|
|
168
|
-
const lockfile = loadLockfile(tempDir);
|
|
169
|
-
expect(Object.keys(lockfile.packages)).toEqual(['a', 'b', 'c']);
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
describe('unlockPackage', () => {
|
|
174
|
-
test('removes package from lockfile', () => {
|
|
175
|
-
lockPackage(tempDir, { name: 'tool', version: '1.0.0', resolved: '/path' });
|
|
176
|
-
unlockPackage(tempDir, 'tool');
|
|
177
|
-
|
|
178
|
-
const lockfile = loadLockfile(tempDir);
|
|
179
|
-
expect(lockfile.packages.tool).toBeUndefined();
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
test('handles non-existent package gracefully', () => {
|
|
183
|
-
// Should not throw
|
|
184
|
-
unlockPackage(tempDir, 'nonexistent');
|
|
185
|
-
|
|
186
|
-
const lockfile = loadLockfile(tempDir);
|
|
187
|
-
expect(lockfile.packages.nonexistent).toBeUndefined();
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
test('preserves other packages when removing', () => {
|
|
191
|
-
lockPackage(tempDir, { name: 'a', version: '1.0.0', resolved: '/a' });
|
|
192
|
-
lockPackage(tempDir, { name: 'b', version: '1.0.0', resolved: '/b' });
|
|
193
|
-
lockPackage(tempDir, { name: 'c', version: '1.0.0', resolved: '/c' });
|
|
194
|
-
|
|
195
|
-
unlockPackage(tempDir, 'b');
|
|
196
|
-
|
|
197
|
-
const lockfile = loadLockfile(tempDir);
|
|
198
|
-
expect(Object.keys(lockfile.packages)).toEqual(['a', 'c']);
|
|
199
|
-
});
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
describe('getLockedPackage', () => {
|
|
203
|
-
test('returns package when found', () => {
|
|
204
|
-
lockPackage(tempDir, {
|
|
205
|
-
name: 'github',
|
|
206
|
-
version: '1.0.0',
|
|
207
|
-
resolved: '/path'
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
const pkg = getLockedPackage(tempDir, 'github');
|
|
211
|
-
expect(pkg).not.toBeNull();
|
|
212
|
-
expect(pkg?.name).toBe('github');
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
test('returns null when not found', () => {
|
|
216
|
-
const pkg = getLockedPackage(tempDir, 'nonexistent');
|
|
217
|
-
expect(pkg).toBeNull();
|
|
218
|
-
});
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
describe('getLockedPackages', () => {
|
|
222
|
-
test('returns empty array when no packages', () => {
|
|
223
|
-
const packages = getLockedPackages(tempDir);
|
|
224
|
-
expect(packages).toEqual([]);
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
test('returns all packages', () => {
|
|
228
|
-
lockPackage(tempDir, { name: 'a', version: '1.0.0', resolved: '/a' });
|
|
229
|
-
lockPackage(tempDir, { name: 'b', version: '2.0.0', resolved: '/b' });
|
|
230
|
-
|
|
231
|
-
const packages = getLockedPackages(tempDir);
|
|
232
|
-
expect(packages).toHaveLength(2);
|
|
233
|
-
expect(packages.map(p => p.name).sort()).toEqual(['a', 'b']);
|
|
234
|
-
});
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
describe('isPackageLocked', () => {
|
|
238
|
-
test('returns true when package is locked', () => {
|
|
239
|
-
lockPackage(tempDir, { name: 'tool', version: '1.0.0', resolved: '/path' });
|
|
240
|
-
expect(isPackageLocked(tempDir, 'tool')).toBe(true);
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
test('returns false when package not locked', () => {
|
|
244
|
-
expect(isPackageLocked(tempDir, 'nonexistent')).toBe(false);
|
|
245
|
-
});
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
describe('clearLockfile', () => {
|
|
249
|
-
test('removes all packages', () => {
|
|
250
|
-
lockPackage(tempDir, { name: 'a', version: '1.0.0', resolved: '/a' });
|
|
251
|
-
lockPackage(tempDir, { name: 'b', version: '1.0.0', resolved: '/b' });
|
|
252
|
-
|
|
253
|
-
clearLockfile(tempDir);
|
|
254
|
-
|
|
255
|
-
const lockfile = loadLockfile(tempDir);
|
|
256
|
-
expect(lockfile.packages).toEqual({});
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
test('preserves lockfile version', () => {
|
|
260
|
-
clearLockfile(tempDir);
|
|
261
|
-
|
|
262
|
-
const lockfile = loadLockfile(tempDir);
|
|
263
|
-
expect(lockfile.lockfileVersion).toBe(LOCKFILE_VERSION);
|
|
264
|
-
});
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
describe('formatLockfile', () => {
|
|
268
|
-
test('formats empty lockfile', () => {
|
|
269
|
-
const lockfile: Lockfile = {
|
|
270
|
-
lockfileVersion: 1,
|
|
271
|
-
packages: {}
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
const output = formatLockfile(lockfile);
|
|
275
|
-
expect(output).toContain('# cli4ai.lock');
|
|
276
|
-
expect(output).toContain('lockfileVersion: 1');
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
test('formats packages alphabetically', () => {
|
|
280
|
-
const lockfile: Lockfile = {
|
|
281
|
-
lockfileVersion: 1,
|
|
282
|
-
packages: {
|
|
283
|
-
zebra: { name: 'zebra', version: '1.0.0', resolved: '/z' },
|
|
284
|
-
apple: { name: 'apple', version: '2.0.0', resolved: '/a' },
|
|
285
|
-
monkey: { name: 'monkey', version: '3.0.0', resolved: '/m' }
|
|
286
|
-
}
|
|
287
|
-
};
|
|
288
|
-
|
|
289
|
-
const output = formatLockfile(lockfile);
|
|
290
|
-
const appleIndex = output.indexOf('apple@');
|
|
291
|
-
const monkeyIndex = output.indexOf('monkey@');
|
|
292
|
-
const zebraIndex = output.indexOf('zebra@');
|
|
293
|
-
|
|
294
|
-
expect(appleIndex).toBeLessThan(monkeyIndex);
|
|
295
|
-
expect(monkeyIndex).toBeLessThan(zebraIndex);
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
test('includes integrity when present', () => {
|
|
299
|
-
const lockfile: Lockfile = {
|
|
300
|
-
lockfileVersion: 1,
|
|
301
|
-
packages: {
|
|
302
|
-
tool: {
|
|
303
|
-
name: 'tool',
|
|
304
|
-
version: '1.0.0',
|
|
305
|
-
resolved: '/path',
|
|
306
|
-
integrity: 'sha256-abc123'
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
};
|
|
310
|
-
|
|
311
|
-
const output = formatLockfile(lockfile);
|
|
312
|
-
expect(output).toContain('integrity: sha256-abc123');
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
test('includes dependencies when present', () => {
|
|
316
|
-
const lockfile: Lockfile = {
|
|
317
|
-
lockfileVersion: 1,
|
|
318
|
-
packages: {
|
|
319
|
-
tool: {
|
|
320
|
-
name: 'tool',
|
|
321
|
-
version: '1.0.0',
|
|
322
|
-
resolved: '/path',
|
|
323
|
-
dependencies: {
|
|
324
|
-
'dep-a': '1.0.0',
|
|
325
|
-
'dep-b': '2.0.0'
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
};
|
|
330
|
-
|
|
331
|
-
const output = formatLockfile(lockfile);
|
|
332
|
-
expect(output).toContain('dependencies:');
|
|
333
|
-
expect(output).toContain('dep-a: "1.0.0"');
|
|
334
|
-
expect(output).toContain('dep-b: "2.0.0"');
|
|
335
|
-
});
|
|
336
|
-
});
|
|
337
|
-
});
|
|
@@ -1,327 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for manifest.ts
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, test, expect, beforeEach, afterEach } from 'vitest';
|
|
6
|
-
import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'fs';
|
|
7
|
-
import { join } from 'path';
|
|
8
|
-
import { tmpdir } from 'os';
|
|
9
|
-
import {
|
|
10
|
-
validateManifest,
|
|
11
|
-
loadManifestOrThrow,
|
|
12
|
-
tryLoadManifest,
|
|
13
|
-
findManifest,
|
|
14
|
-
createManifest,
|
|
15
|
-
MANIFEST_FILENAME,
|
|
16
|
-
ManifestValidationError,
|
|
17
|
-
ManifestNotFoundError,
|
|
18
|
-
ManifestParseError
|
|
19
|
-
} from './manifest.js';
|
|
20
|
-
|
|
21
|
-
describe('manifest', () => {
|
|
22
|
-
let tempDir: string;
|
|
23
|
-
|
|
24
|
-
beforeEach(() => {
|
|
25
|
-
tempDir = mkdtempSync(join(tmpdir(), 'cli4ai-manifest-test-'));
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
afterEach(() => {
|
|
29
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
describe('validateManifest', () => {
|
|
33
|
-
test('accepts valid minimal manifest', () => {
|
|
34
|
-
const manifest = validateManifest({
|
|
35
|
-
name: 'my-tool',
|
|
36
|
-
version: '1.0.0',
|
|
37
|
-
entry: 'run.ts'
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
expect(manifest.name).toBe('my-tool');
|
|
41
|
-
expect(manifest.version).toBe('1.0.0');
|
|
42
|
-
expect(manifest.entry).toBe('run.ts');
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test('accepts valid full manifest', () => {
|
|
46
|
-
const manifest = validateManifest({
|
|
47
|
-
name: 'github',
|
|
48
|
-
version: '2.1.0',
|
|
49
|
-
entry: 'run.ts',
|
|
50
|
-
description: 'GitHub CLI wrapper',
|
|
51
|
-
author: 'cliforai',
|
|
52
|
-
license: 'MIT',
|
|
53
|
-
runtime: 'bun',
|
|
54
|
-
commands: {
|
|
55
|
-
notifs: { description: 'Your notifications' },
|
|
56
|
-
repos: {
|
|
57
|
-
description: 'List repos',
|
|
58
|
-
args: [{ name: 'user', required: false }]
|
|
59
|
-
}
|
|
60
|
-
},
|
|
61
|
-
env: {
|
|
62
|
-
GITHUB_TOKEN: { required: false, description: 'Token' }
|
|
63
|
-
},
|
|
64
|
-
mcp: { enabled: true }
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
expect(manifest.name).toBe('github');
|
|
68
|
-
expect(manifest.commands?.notifs.description).toBe('Your notifications');
|
|
69
|
-
expect(manifest.mcp?.enabled).toBe(true);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test('accepts prerelease versions', () => {
|
|
73
|
-
const manifest = validateManifest({
|
|
74
|
-
name: 'tool',
|
|
75
|
-
version: '1.0.0-beta.1',
|
|
76
|
-
entry: 'run.ts'
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
expect(manifest.version).toBe('1.0.0-beta.1');
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test('accepts all valid runtimes', () => {
|
|
83
|
-
for (const runtime of ['bun', 'node'] as const) {
|
|
84
|
-
const manifest = validateManifest({
|
|
85
|
-
name: 'tool',
|
|
86
|
-
version: '1.0.0',
|
|
87
|
-
entry: 'run.ts',
|
|
88
|
-
runtime
|
|
89
|
-
});
|
|
90
|
-
expect(manifest.runtime).toBe(runtime);
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
test('rejects null manifest', () => {
|
|
95
|
-
expect(() => validateManifest(null)).toThrow();
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test('rejects non-object manifest', () => {
|
|
99
|
-
expect(() => validateManifest('string')).toThrow();
|
|
100
|
-
expect(() => validateManifest(123)).toThrow();
|
|
101
|
-
expect(() => validateManifest([])).toThrow();
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test('rejects missing name', () => {
|
|
105
|
-
expect(() => validateManifest({
|
|
106
|
-
version: '1.0.0',
|
|
107
|
-
entry: 'run.ts'
|
|
108
|
-
})).toThrow();
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
test('rejects invalid name format', () => {
|
|
112
|
-
expect(() => validateManifest({
|
|
113
|
-
name: 'Invalid-Name',
|
|
114
|
-
version: '1.0.0',
|
|
115
|
-
entry: 'run.ts'
|
|
116
|
-
})).toThrow();
|
|
117
|
-
|
|
118
|
-
expect(() => validateManifest({
|
|
119
|
-
name: '123invalid',
|
|
120
|
-
version: '1.0.0',
|
|
121
|
-
entry: 'run.ts'
|
|
122
|
-
})).toThrow();
|
|
123
|
-
|
|
124
|
-
expect(() => validateManifest({
|
|
125
|
-
name: 'has_underscore',
|
|
126
|
-
version: '1.0.0',
|
|
127
|
-
entry: 'run.ts'
|
|
128
|
-
})).toThrow();
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
test('rejects missing version', () => {
|
|
132
|
-
expect(() => validateManifest({
|
|
133
|
-
name: 'tool',
|
|
134
|
-
entry: 'run.ts'
|
|
135
|
-
})).toThrow();
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
test('rejects invalid version format', () => {
|
|
139
|
-
expect(() => validateManifest({
|
|
140
|
-
name: 'tool',
|
|
141
|
-
version: 'invalid',
|
|
142
|
-
entry: 'run.ts'
|
|
143
|
-
})).toThrow();
|
|
144
|
-
|
|
145
|
-
expect(() => validateManifest({
|
|
146
|
-
name: 'tool',
|
|
147
|
-
version: '1.0',
|
|
148
|
-
entry: 'run.ts'
|
|
149
|
-
})).toThrow();
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
test('rejects missing entry', () => {
|
|
153
|
-
expect(() => validateManifest({
|
|
154
|
-
name: 'tool',
|
|
155
|
-
version: '1.0.0'
|
|
156
|
-
})).toThrow();
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
test('rejects empty entry', () => {
|
|
160
|
-
expect(() => validateManifest({
|
|
161
|
-
name: 'tool',
|
|
162
|
-
version: '1.0.0',
|
|
163
|
-
entry: ''
|
|
164
|
-
})).toThrow();
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
test('rejects invalid runtime', () => {
|
|
168
|
-
expect(() => validateManifest({
|
|
169
|
-
name: 'tool',
|
|
170
|
-
version: '1.0.0',
|
|
171
|
-
entry: 'run.ts',
|
|
172
|
-
runtime: 'python'
|
|
173
|
-
})).toThrow();
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
test('rejects commands without description', () => {
|
|
177
|
-
expect(() => validateManifest({
|
|
178
|
-
name: 'tool',
|
|
179
|
-
version: '1.0.0',
|
|
180
|
-
entry: 'run.ts',
|
|
181
|
-
commands: {
|
|
182
|
-
mycommand: {}
|
|
183
|
-
}
|
|
184
|
-
})).toThrow();
|
|
185
|
-
});
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
describe('loadManifestOrThrow', () => {
|
|
189
|
-
test('loads valid manifest from directory', () => {
|
|
190
|
-
writeFileSync(
|
|
191
|
-
join(tempDir, MANIFEST_FILENAME),
|
|
192
|
-
JSON.stringify({
|
|
193
|
-
name: 'test-tool',
|
|
194
|
-
version: '1.0.0',
|
|
195
|
-
entry: 'run.ts'
|
|
196
|
-
})
|
|
197
|
-
);
|
|
198
|
-
|
|
199
|
-
const manifest = loadManifestOrThrow(tempDir);
|
|
200
|
-
expect(manifest.name).toBe('test-tool');
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
test('throws ManifestNotFoundError when manifest not found', () => {
|
|
204
|
-
expect(() => loadManifestOrThrow(tempDir)).toThrow(ManifestNotFoundError);
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
test('throws ManifestParseError on invalid JSON', () => {
|
|
208
|
-
writeFileSync(join(tempDir, MANIFEST_FILENAME), 'not json');
|
|
209
|
-
expect(() => loadManifestOrThrow(tempDir)).toThrow(ManifestParseError);
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
test('throws ManifestValidationError on invalid manifest content', () => {
|
|
213
|
-
writeFileSync(
|
|
214
|
-
join(tempDir, MANIFEST_FILENAME),
|
|
215
|
-
JSON.stringify({ name: 'Invalid' }) // Missing version, entry
|
|
216
|
-
);
|
|
217
|
-
expect(() => loadManifestOrThrow(tempDir)).toThrow(ManifestValidationError);
|
|
218
|
-
});
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
describe('tryLoadManifest', () => {
|
|
222
|
-
test('returns manifest when valid', () => {
|
|
223
|
-
writeFileSync(
|
|
224
|
-
join(tempDir, MANIFEST_FILENAME),
|
|
225
|
-
JSON.stringify({
|
|
226
|
-
name: 'test-tool',
|
|
227
|
-
version: '1.0.0',
|
|
228
|
-
entry: 'run.ts'
|
|
229
|
-
})
|
|
230
|
-
);
|
|
231
|
-
|
|
232
|
-
const manifest = tryLoadManifest(tempDir);
|
|
233
|
-
expect(manifest).not.toBeNull();
|
|
234
|
-
expect(manifest?.name).toBe('test-tool');
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
test('returns null when not found', () => {
|
|
238
|
-
const manifest = tryLoadManifest(tempDir);
|
|
239
|
-
expect(manifest).toBeNull();
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
test('returns null on invalid JSON', () => {
|
|
243
|
-
writeFileSync(join(tempDir, MANIFEST_FILENAME), 'not json');
|
|
244
|
-
const manifest = tryLoadManifest(tempDir);
|
|
245
|
-
expect(manifest).toBeNull();
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
test('returns null on invalid manifest', () => {
|
|
249
|
-
writeFileSync(
|
|
250
|
-
join(tempDir, MANIFEST_FILENAME),
|
|
251
|
-
JSON.stringify({ name: 'Invalid' })
|
|
252
|
-
);
|
|
253
|
-
const manifest = tryLoadManifest(tempDir);
|
|
254
|
-
expect(manifest).toBeNull();
|
|
255
|
-
});
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
describe('findManifest', () => {
|
|
259
|
-
test('finds manifest in current directory', () => {
|
|
260
|
-
writeFileSync(
|
|
261
|
-
join(tempDir, MANIFEST_FILENAME),
|
|
262
|
-
JSON.stringify({
|
|
263
|
-
name: 'found-tool',
|
|
264
|
-
version: '1.0.0',
|
|
265
|
-
entry: 'run.ts'
|
|
266
|
-
})
|
|
267
|
-
);
|
|
268
|
-
|
|
269
|
-
const result = findManifest(tempDir);
|
|
270
|
-
expect(result).not.toBeNull();
|
|
271
|
-
expect(result?.manifest.name).toBe('found-tool');
|
|
272
|
-
expect(result?.dir).toBe(tempDir);
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
test('finds manifest in parent directory', () => {
|
|
276
|
-
const childDir = join(tempDir, 'child');
|
|
277
|
-
mkdirSync(childDir);
|
|
278
|
-
|
|
279
|
-
writeFileSync(
|
|
280
|
-
join(tempDir, MANIFEST_FILENAME),
|
|
281
|
-
JSON.stringify({
|
|
282
|
-
name: 'parent-tool',
|
|
283
|
-
version: '1.0.0',
|
|
284
|
-
entry: 'run.ts'
|
|
285
|
-
})
|
|
286
|
-
);
|
|
287
|
-
|
|
288
|
-
const result = findManifest(childDir);
|
|
289
|
-
expect(result).not.toBeNull();
|
|
290
|
-
expect(result?.manifest.name).toBe('parent-tool');
|
|
291
|
-
expect(result?.dir).toBe(tempDir);
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
test('returns null when no manifest found', () => {
|
|
295
|
-
const result = findManifest(tempDir);
|
|
296
|
-
expect(result).toBeNull();
|
|
297
|
-
});
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
describe('createManifest', () => {
|
|
301
|
-
test('creates minimal manifest', () => {
|
|
302
|
-
const manifest = createManifest('my-tool');
|
|
303
|
-
|
|
304
|
-
expect(manifest.name).toBe('my-tool');
|
|
305
|
-
expect(manifest.version).toBe('1.0.0');
|
|
306
|
-
expect(manifest.entry).toBe('run.ts');
|
|
307
|
-
expect(manifest.runtime).toBe('node');
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
test('normalizes name', () => {
|
|
311
|
-
const manifest = createManifest('My Tool Name');
|
|
312
|
-
expect(manifest.name).toBe('my-tool-name');
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
test('accepts options', () => {
|
|
316
|
-
const manifest = createManifest('tool', {
|
|
317
|
-
version: '2.0.0',
|
|
318
|
-
description: 'Custom description',
|
|
319
|
-
runtime: 'node'
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
expect(manifest.version).toBe('2.0.0');
|
|
323
|
-
expect(manifest.description).toBe('Custom description');
|
|
324
|
-
expect(manifest.runtime).toBe('node');
|
|
325
|
-
});
|
|
326
|
-
});
|
|
327
|
-
});
|