@orchagent/cli 0.3.29 → 0.3.31

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/dist/index.js CHANGED
@@ -61,6 +61,10 @@ Quick Reference:
61
61
  call Execute an agent on orchagent servers (requires login)
62
62
  info Show agent details and input/output schemas
63
63
 
64
+ Installation:
65
+ npm install -g @orchagent/cli Install globally (then use: orch)
66
+ npx orchagent <command> Run without installing
67
+
64
68
  Documentation: https://docs.orchagent.io
65
69
  orchagent docs Open docs in browser
66
70
  orchagent docs cli CLI command reference
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@orchagent/cli",
3
- "version": "0.3.29",
3
+ "version": "0.3.31",
4
4
  "description": "Command-line interface for the orchagent AI agent marketplace",
5
5
  "license": "MIT",
6
6
  "author": "orchagent <hello@orchagent.io>",
7
7
  "homepage": "https://orchagent.io",
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "https://github.com/orchagent/orchagent.git",
10
+ "url": "https://github.com/orchagent/orchagent-cli.git",
11
11
  "directory": "cli"
12
12
  },
13
13
  "bugs": {
@@ -40,7 +40,8 @@
40
40
  "test:coverage": "vitest run --coverage",
41
41
  "test:e2e": "vitest run --config vitest.e2e.config.ts",
42
42
  "test:all": "vitest run && vitest run --config vitest.e2e.config.ts",
43
- "prepublishOnly": "npm run build"
43
+ "prepublishOnly": "npm run build",
44
+ "postpublish": "cd ../cli-wrapper && npm version $npm_package_version --no-git-tag-version --allow-same-version && npm publish && echo '\n✓ Wrapper published at orchagent@'$npm_package_version"
44
45
  },
45
46
  "dependencies": {
46
47
  "@sentry/node": "^9.3.0",
@@ -1,40 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.registerLlmConfigCommand = registerLlmConfigCommand;
4
- const config_1 = require("../lib/config");
5
- const api_1 = require("../lib/api");
6
- const errors_1 = require("../lib/errors");
7
- function registerLlmConfigCommand(program) {
8
- program
9
- .command('llm-config')
10
- .description('Set default LLM configuration for prompt-based agents')
11
- .option('--endpoint <url>', 'LLM API base URL (e.g., https://api.openai.com/v1)')
12
- .option('--model <model>', 'Default model name (e.g., gpt-4.1-mini)')
13
- .option('--api-key <key>', 'LLM API key')
14
- .option('--clear', 'Clear the stored default LLM config')
15
- .action(async (options) => {
16
- const config = await (0, config_1.getResolvedConfig)();
17
- if (!config.apiKey) {
18
- throw new errors_1.CliError('Missing API key. Run `orchagent login` first.');
19
- }
20
- if (options.clear) {
21
- await (0, api_1.updateOrg)(config, { default_llm_config: null });
22
- process.stdout.write('Cleared default LLM config.\n');
23
- return;
24
- }
25
- const endpoint = options.endpoint?.trim();
26
- const model = options.model?.trim();
27
- const apiKey = options.apiKey?.trim();
28
- if (!endpoint || !model || !apiKey) {
29
- throw new errors_1.CliError('Missing required flags. Use --endpoint, --model, and --api-key (or --clear).');
30
- }
31
- await (0, api_1.updateOrg)(config, {
32
- default_llm_config: {
33
- endpoint,
34
- model,
35
- api_key: apiKey,
36
- },
37
- });
38
- process.stdout.write('Saved default LLM config.\n');
39
- });
40
- }
@@ -1,475 +0,0 @@
1
- "use strict";
2
- /**
3
- * Tests for the publish command.
4
- *
5
- * These tests cover publishing agents and skills from local files:
6
- * - Reading orchagent.json manifest
7
- * - Reading prompt.md for prompt agents
8
- * - Reading SKILL.md for skills
9
- * - Calling createAgent API with correct payload
10
- * - Error handling for missing files
11
- */
12
- var __importDefault = (this && this.__importDefault) || function (mod) {
13
- return (mod && mod.__esModule) ? mod : { "default": mod };
14
- };
15
- Object.defineProperty(exports, "__esModule", { value: true });
16
- const vitest_1 = require("vitest");
17
- const commander_1 = require("commander");
18
- // Mock modules before importing the command
19
- vitest_1.vi.mock('fs/promises');
20
- vitest_1.vi.mock('../lib/config');
21
- vitest_1.vi.mock('../lib/api');
22
- const promises_1 = __importDefault(require("fs/promises"));
23
- const publish_1 = require("./publish");
24
- const config_1 = require("../lib/config");
25
- const api_1 = require("../lib/api");
26
- const mockFs = vitest_1.vi.mocked(promises_1.default);
27
- const mockGetResolvedConfig = vitest_1.vi.mocked(config_1.getResolvedConfig);
28
- const mockCreateAgent = vitest_1.vi.mocked(api_1.createAgent);
29
- const mockGetOrg = vitest_1.vi.mocked(api_1.getOrg);
30
- (0, vitest_1.describe)('publish command', () => {
31
- let program;
32
- let stdoutSpy;
33
- let stderrSpy;
34
- let originalCwd;
35
- (0, vitest_1.beforeEach)(() => {
36
- vitest_1.vi.clearAllMocks();
37
- program = new commander_1.Command();
38
- program.exitOverride();
39
- (0, publish_1.registerPublishCommand)(program);
40
- // Mock stdout/stderr
41
- stdoutSpy = vitest_1.vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
42
- stderrSpy = vitest_1.vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
43
- // Mock config
44
- mockGetResolvedConfig.mockResolvedValue({
45
- apiKey: 'sk_test_123',
46
- apiUrl: 'https://api.test.com',
47
- });
48
- // Mock getOrg
49
- mockGetOrg.mockResolvedValue({
50
- id: 'org-123',
51
- slug: 'test-org',
52
- name: 'Test Org',
53
- });
54
- // Mock createAgent
55
- mockCreateAgent.mockResolvedValue({ id: 'agent-123' });
56
- // Store original cwd
57
- originalCwd = process.cwd;
58
- process.cwd = () => '/test/project';
59
- });
60
- (0, vitest_1.afterEach)(() => {
61
- stdoutSpy.mockRestore();
62
- stderrSpy.mockRestore();
63
- process.cwd = originalCwd;
64
- vitest_1.vi.restoreAllMocks();
65
- });
66
- (0, vitest_1.describe)('publishing from orchagent.json', () => {
67
- (0, vitest_1.it)('reads manifest and publishes prompt agent', async () => {
68
- const manifest = {
69
- name: 'my-agent',
70
- version: 'v1',
71
- type: 'prompt',
72
- description: 'Test agent',
73
- tags: ['test'],
74
- };
75
- mockFs.readFile.mockImplementation(async (filePath) => {
76
- const path = String(filePath);
77
- if (path.includes('SKILL.md')) {
78
- throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
79
- }
80
- if (path.includes('orchagent.json')) {
81
- return JSON.stringify(manifest);
82
- }
83
- if (path.includes('prompt.md')) {
84
- return 'You are a helpful assistant.\n\nAnalyze: {{input}}';
85
- }
86
- if (path.includes('schema.json')) {
87
- throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
88
- }
89
- throw new Error(`Unexpected file: ${path}`);
90
- });
91
- await program.parseAsync(['node', 'test', 'publish']);
92
- (0, vitest_1.expect)(mockCreateAgent).toHaveBeenCalledWith(vitest_1.expect.any(Object), vitest_1.expect.objectContaining({
93
- name: 'my-agent',
94
- version: 'v1',
95
- type: 'prompt',
96
- description: 'Test agent',
97
- prompt: 'You are a helpful assistant.\n\nAnalyze: {{input}}',
98
- tags: ['test'],
99
- is_public: true,
100
- supported_providers: ['any'],
101
- }));
102
- });
103
- (0, vitest_1.it)('reads schema.json when present', async () => {
104
- const manifest = {
105
- name: 'schema-agent',
106
- version: 'v1',
107
- type: 'prompt',
108
- description: 'Agent with schemas',
109
- };
110
- const schemas = {
111
- input: { type: 'object', properties: { text: { type: 'string' } } },
112
- output: { type: 'object', properties: { result: { type: 'string' } } },
113
- };
114
- mockFs.readFile.mockImplementation(async (filePath) => {
115
- const path = String(filePath);
116
- if (path.includes('SKILL.md')) {
117
- throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
118
- }
119
- if (path.includes('orchagent.json')) {
120
- return JSON.stringify(manifest);
121
- }
122
- if (path.includes('prompt.md')) {
123
- return 'Process: {{text}}';
124
- }
125
- if (path.includes('schema.json')) {
126
- return JSON.stringify(schemas);
127
- }
128
- throw new Error(`Unexpected file: ${path}`);
129
- });
130
- await program.parseAsync(['node', 'test', 'publish']);
131
- (0, vitest_1.expect)(mockCreateAgent).toHaveBeenCalledWith(vitest_1.expect.any(Object), vitest_1.expect.objectContaining({
132
- input_schema: schemas.input,
133
- output_schema: schemas.output,
134
- }));
135
- });
136
- (0, vitest_1.it)('throws error when orchagent.json is missing', async () => {
137
- mockFs.readFile.mockImplementation(async (filePath) => {
138
- const path = String(filePath);
139
- if (path.includes('SKILL.md')) {
140
- throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
141
- }
142
- if (path.includes('orchagent.json')) {
143
- throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
144
- }
145
- throw new Error(`Unexpected file: ${path}`);
146
- });
147
- await (0, vitest_1.expect)(program.parseAsync(['node', 'test', 'publish'])).rejects.toThrow('No orchagent.json found');
148
- });
149
- (0, vitest_1.it)('throws error when prompt.md is missing for prompt agent', async () => {
150
- const manifest = {
151
- name: 'prompt-agent',
152
- version: 'v1',
153
- type: 'prompt',
154
- };
155
- mockFs.readFile.mockImplementation(async (filePath) => {
156
- const path = String(filePath);
157
- if (path.includes('SKILL.md')) {
158
- throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
159
- }
160
- if (path.includes('orchagent.json')) {
161
- return JSON.stringify(manifest);
162
- }
163
- if (path.includes('prompt.md')) {
164
- throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
165
- }
166
- throw new Error(`Unexpected file: ${path}`);
167
- });
168
- await (0, vitest_1.expect)(program.parseAsync(['node', 'test', 'publish'])).rejects.toThrow('No prompt.md found');
169
- });
170
- (0, vitest_1.it)('throws error when manifest missing required fields', async () => {
171
- const manifest = {
172
- name: 'incomplete-agent',
173
- // missing version
174
- };
175
- mockFs.readFile.mockImplementation(async (filePath) => {
176
- const path = String(filePath);
177
- if (path.includes('SKILL.md')) {
178
- throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
179
- }
180
- if (path.includes('orchagent.json')) {
181
- return JSON.stringify(manifest);
182
- }
183
- throw new Error(`Unexpected file: ${path}`);
184
- });
185
- await (0, vitest_1.expect)(program.parseAsync(['node', 'test', 'publish'])).rejects.toThrow('must have name and version');
186
- });
187
- (0, vitest_1.it)('requires URL for code-based agents', async () => {
188
- const manifest = {
189
- name: 'code-agent',
190
- version: 'v1',
191
- type: 'code',
192
- };
193
- mockFs.readFile.mockImplementation(async (filePath) => {
194
- const path = String(filePath);
195
- if (path.includes('SKILL.md')) {
196
- throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
197
- }
198
- if (path.includes('orchagent.json')) {
199
- return JSON.stringify(manifest);
200
- }
201
- if (path.includes('schema.json')) {
202
- throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
203
- }
204
- throw new Error(`Unexpected file: ${path}`);
205
- });
206
- await (0, vitest_1.expect)(program.parseAsync(['node', 'test', 'publish'])).rejects.toThrow('URL is required for code-based agents');
207
- });
208
- (0, vitest_1.it)('publishes code agent with --url option', async () => {
209
- const manifest = {
210
- name: 'code-agent',
211
- version: 'v1',
212
- type: 'code',
213
- description: 'Code agent',
214
- };
215
- mockFs.readFile.mockImplementation(async (filePath) => {
216
- const path = String(filePath);
217
- if (path.includes('SKILL.md')) {
218
- throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
219
- }
220
- if (path.includes('orchagent.json')) {
221
- return JSON.stringify(manifest);
222
- }
223
- if (path.includes('schema.json')) {
224
- throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
225
- }
226
- throw new Error(`Unexpected file: ${path}`);
227
- });
228
- await program.parseAsync([
229
- 'node',
230
- 'test',
231
- 'publish',
232
- '--url',
233
- 'https://my-agent.run.app',
234
- ]);
235
- (0, vitest_1.expect)(mockCreateAgent).toHaveBeenCalledWith(vitest_1.expect.any(Object), vitest_1.expect.objectContaining({
236
- name: 'code-agent',
237
- type: 'code',
238
- url: 'https://my-agent.run.app',
239
- }));
240
- });
241
- (0, vitest_1.it)('respects --private flag', async () => {
242
- const manifest = {
243
- name: 'private-agent',
244
- version: 'v1',
245
- type: 'prompt',
246
- };
247
- mockFs.readFile.mockImplementation(async (filePath) => {
248
- const path = String(filePath);
249
- if (path.includes('SKILL.md')) {
250
- throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
251
- }
252
- if (path.includes('orchagent.json')) {
253
- return JSON.stringify(manifest);
254
- }
255
- if (path.includes('prompt.md')) {
256
- return 'Private prompt';
257
- }
258
- if (path.includes('schema.json')) {
259
- throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
260
- }
261
- throw new Error(`Unexpected file: ${path}`);
262
- });
263
- await program.parseAsync(['node', 'test', 'publish', '--private']);
264
- (0, vitest_1.expect)(mockCreateAgent).toHaveBeenCalledWith(vitest_1.expect.any(Object), vitest_1.expect.objectContaining({
265
- is_public: false,
266
- }));
267
- });
268
- (0, vitest_1.it)('uses manifest supported_providers', async () => {
269
- const manifest = {
270
- name: 'openai-agent',
271
- version: 'v1',
272
- type: 'prompt',
273
- supported_providers: ['openai', 'anthropic'],
274
- };
275
- mockFs.readFile.mockImplementation(async (filePath) => {
276
- const path = String(filePath);
277
- if (path.includes('SKILL.md')) {
278
- throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
279
- }
280
- if (path.includes('orchagent.json')) {
281
- return JSON.stringify(manifest);
282
- }
283
- if (path.includes('prompt.md')) {
284
- return 'Provider-specific prompt';
285
- }
286
- if (path.includes('schema.json')) {
287
- throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
288
- }
289
- throw new Error(`Unexpected file: ${path}`);
290
- });
291
- await program.parseAsync(['node', 'test', 'publish']);
292
- (0, vitest_1.expect)(mockCreateAgent).toHaveBeenCalledWith(vitest_1.expect.any(Object), vitest_1.expect.objectContaining({
293
- supported_providers: ['openai', 'anthropic'],
294
- }));
295
- });
296
- (0, vitest_1.it)('outputs service key when returned', async () => {
297
- const manifest = {
298
- name: 'service-agent',
299
- version: 'v1',
300
- type: 'prompt',
301
- };
302
- mockCreateAgent.mockResolvedValue({
303
- id: 'agent-123',
304
- service_key: 'sk_service_abc123',
305
- });
306
- mockFs.readFile.mockImplementation(async (filePath) => {
307
- const path = String(filePath);
308
- if (path.includes('SKILL.md')) {
309
- throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
310
- }
311
- if (path.includes('orchagent.json')) {
312
- return JSON.stringify(manifest);
313
- }
314
- if (path.includes('prompt.md')) {
315
- return 'Service prompt';
316
- }
317
- if (path.includes('schema.json')) {
318
- throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
319
- }
320
- throw new Error(`Unexpected file: ${path}`);
321
- });
322
- await program.parseAsync(['node', 'test', 'publish']);
323
- (0, vitest_1.expect)(stdoutSpy).toHaveBeenCalledWith(vitest_1.expect.stringContaining('sk_service_abc123'));
324
- });
325
- });
326
- (0, vitest_1.describe)('publishing from SKILL.md', () => {
327
- (0, vitest_1.it)('publishes skill from SKILL.md with frontmatter', async () => {
328
- const skillMd = `---
329
- name: my-skill
330
- description: A helpful skill
331
- metadata:
332
- version: v2
333
- ---
334
- You are a helpful skill that does specific things.
335
-
336
- Use this prompt to guide the agent.`;
337
- mockFs.readFile.mockImplementation(async (filePath) => {
338
- const path = String(filePath);
339
- if (path.includes('SKILL.md')) {
340
- return skillMd;
341
- }
342
- throw new Error(`Unexpected file: ${path}`);
343
- });
344
- await program.parseAsync(['node', 'test', 'publish']);
345
- (0, vitest_1.expect)(mockCreateAgent).toHaveBeenCalledWith(vitest_1.expect.any(Object), vitest_1.expect.objectContaining({
346
- name: 'my-skill',
347
- type: 'skill',
348
- description: 'A helpful skill',
349
- version: 'v2',
350
- prompt: vitest_1.expect.stringContaining('You are a helpful skill'),
351
- is_public: true,
352
- supported_providers: ['any'],
353
- }));
354
- });
355
- (0, vitest_1.it)('uses default version v1 when not specified', async () => {
356
- const skillMd = `---
357
- name: simple-skill
358
- description: Simple skill
359
- ---
360
- Skill content here.`;
361
- mockFs.readFile.mockImplementation(async (filePath) => {
362
- const path = String(filePath);
363
- if (path.includes('SKILL.md')) {
364
- return skillMd;
365
- }
366
- throw new Error(`Unexpected file: ${path}`);
367
- });
368
- await program.parseAsync(['node', 'test', 'publish']);
369
- (0, vitest_1.expect)(mockCreateAgent).toHaveBeenCalledWith(vitest_1.expect.any(Object), vitest_1.expect.objectContaining({
370
- version: 'v1',
371
- }));
372
- });
373
- (0, vitest_1.it)('respects --private flag for skills', async () => {
374
- const skillMd = `---
375
- name: private-skill
376
- description: Private skill
377
- ---
378
- Private skill content.`;
379
- mockFs.readFile.mockImplementation(async (filePath) => {
380
- const path = String(filePath);
381
- if (path.includes('SKILL.md')) {
382
- return skillMd;
383
- }
384
- throw new Error(`Unexpected file: ${path}`);
385
- });
386
- await program.parseAsync(['node', 'test', 'publish', '--private']);
387
- (0, vitest_1.expect)(mockCreateAgent).toHaveBeenCalledWith(vitest_1.expect.any(Object), vitest_1.expect.objectContaining({
388
- is_public: false,
389
- }));
390
- });
391
- (0, vitest_1.it)('falls back to manifest if SKILL.md has no frontmatter', async () => {
392
- const manifest = {
393
- name: 'fallback-agent',
394
- version: 'v1',
395
- type: 'prompt',
396
- };
397
- mockFs.readFile.mockImplementation(async (filePath) => {
398
- const path = String(filePath);
399
- if (path.includes('SKILL.md')) {
400
- // No frontmatter - just content
401
- return 'Just some content without frontmatter';
402
- }
403
- if (path.includes('orchagent.json')) {
404
- return JSON.stringify(manifest);
405
- }
406
- if (path.includes('prompt.md')) {
407
- return 'Fallback prompt';
408
- }
409
- if (path.includes('schema.json')) {
410
- throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
411
- }
412
- throw new Error(`Unexpected file: ${path}`);
413
- });
414
- await program.parseAsync(['node', 'test', 'publish']);
415
- // Should fall back to manifest
416
- (0, vitest_1.expect)(mockCreateAgent).toHaveBeenCalledWith(vitest_1.expect.any(Object), vitest_1.expect.objectContaining({
417
- name: 'fallback-agent',
418
- type: 'prompt',
419
- }));
420
- });
421
- (0, vitest_1.it)('falls back to manifest if SKILL.md missing required fields', async () => {
422
- const skillMd = `---
423
- name: incomplete-skill
424
- ---
425
- Missing description field.`;
426
- const manifest = {
427
- name: 'manifest-agent',
428
- version: 'v1',
429
- type: 'prompt',
430
- };
431
- mockFs.readFile.mockImplementation(async (filePath) => {
432
- const path = String(filePath);
433
- if (path.includes('SKILL.md')) {
434
- return skillMd;
435
- }
436
- if (path.includes('orchagent.json')) {
437
- return JSON.stringify(manifest);
438
- }
439
- if (path.includes('prompt.md')) {
440
- return 'Manifest prompt';
441
- }
442
- if (path.includes('schema.json')) {
443
- throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
444
- }
445
- throw new Error(`Unexpected file: ${path}`);
446
- });
447
- await program.parseAsync(['node', 'test', 'publish']);
448
- // Should fall back to manifest since SKILL.md is incomplete
449
- (0, vitest_1.expect)(mockCreateAgent).toHaveBeenCalledWith(vitest_1.expect.any(Object), vitest_1.expect.objectContaining({
450
- name: 'manifest-agent',
451
- type: 'prompt',
452
- }));
453
- });
454
- (0, vitest_1.it)('outputs correct skill publish message', async () => {
455
- const skillMd = `---
456
- name: output-skill
457
- description: Test output
458
- metadata:
459
- version: v3
460
- ---
461
- Skill prompt.`;
462
- mockFs.readFile.mockImplementation(async (filePath) => {
463
- const path = String(filePath);
464
- if (path.includes('SKILL.md')) {
465
- return skillMd;
466
- }
467
- throw new Error(`Unexpected file: ${path}`);
468
- });
469
- await program.parseAsync(['node', 'test', 'publish']);
470
- (0, vitest_1.expect)(stdoutSpy).toHaveBeenCalledWith(vitest_1.expect.stringContaining('Published skill'));
471
- (0, vitest_1.expect)(stdoutSpy).toHaveBeenCalledWith(vitest_1.expect.stringContaining('test-org/output-skill'));
472
- (0, vitest_1.expect)(stdoutSpy).toHaveBeenCalledWith(vitest_1.expect.stringContaining('Version: v3'));
473
- });
474
- });
475
- });
@@ -1,330 +0,0 @@
1
- "use strict";
2
- /**
3
- * Tests for the run command and LLM utilities.
4
- *
5
- * These tests cover downloading and running agents locally:
6
- * - parseAgentRef() parsing org/agent@version formats
7
- * - downloadAgent() fetching from API
8
- * - LLM key detection from environment
9
- * - Building prompts with variable substitution
10
- */
11
- var __importDefault = (this && this.__importDefault) || function (mod) {
12
- return (mod && mod.__esModule) ? mod : { "default": mod };
13
- };
14
- Object.defineProperty(exports, "__esModule", { value: true });
15
- const vitest_1 = require("vitest");
16
- const commander_1 = require("commander");
17
- // Mock modules before importing
18
- vitest_1.vi.mock('fs/promises');
19
- vitest_1.vi.mock('../lib/config');
20
- vitest_1.vi.mock('../lib/api');
21
- const promises_1 = __importDefault(require("fs/promises"));
22
- const run_1 = require("./run");
23
- const config_1 = require("../lib/config");
24
- const api_1 = require("../lib/api");
25
- const llm_1 = require("../lib/llm");
26
- const mockFs = vitest_1.vi.mocked(promises_1.default);
27
- const mockGetResolvedConfig = vitest_1.vi.mocked(config_1.getResolvedConfig);
28
- const mockPublicRequest = vitest_1.vi.mocked(api_1.publicRequest);
29
- const mockGetPublicAgent = vitest_1.vi.mocked(api_1.getPublicAgent);
30
- (0, vitest_1.describe)('run command - agent ref parsing', () => {
31
- let program;
32
- let stdoutSpy;
33
- let stderrSpy;
34
- (0, vitest_1.beforeEach)(() => {
35
- vitest_1.vi.clearAllMocks();
36
- program = new commander_1.Command();
37
- program.exitOverride();
38
- (0, run_1.registerRunCommand)(program);
39
- stdoutSpy = vitest_1.vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
40
- stderrSpy = vitest_1.vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
41
- mockGetResolvedConfig.mockResolvedValue({
42
- apiKey: 'sk_test_123',
43
- apiUrl: 'https://api.test.com',
44
- defaultOrg: 'default-org',
45
- });
46
- // Mock mkdir for agent saving
47
- mockFs.mkdir.mockResolvedValue(undefined);
48
- mockFs.writeFile.mockResolvedValue(undefined);
49
- });
50
- (0, vitest_1.afterEach)(() => {
51
- stdoutSpy.mockRestore();
52
- stderrSpy.mockRestore();
53
- vitest_1.vi.restoreAllMocks();
54
- });
55
- (0, vitest_1.it)('parses org/agent@version format', async () => {
56
- mockPublicRequest.mockResolvedValue({
57
- type: 'code',
58
- name: 'my-agent',
59
- version: 'v2',
60
- supported_providers: ['any'],
61
- });
62
- await program.parseAsync(['node', 'test', 'run', 'myorg/my-agent@v2']);
63
- (0, vitest_1.expect)(mockPublicRequest).toHaveBeenCalledWith(vitest_1.expect.any(Object), '/public/agents/myorg/my-agent/v2/download');
64
- });
65
- (0, vitest_1.it)('parses org/agent format with default version', async () => {
66
- mockPublicRequest.mockResolvedValue({
67
- type: 'code',
68
- name: 'my-agent',
69
- version: 'latest',
70
- supported_providers: ['any'],
71
- });
72
- await program.parseAsync(['node', 'test', 'run', 'myorg/my-agent']);
73
- (0, vitest_1.expect)(mockPublicRequest).toHaveBeenCalledWith(vitest_1.expect.any(Object), '/public/agents/myorg/my-agent/latest/download');
74
- });
75
- (0, vitest_1.it)('uses defaultOrg when no org specified', async () => {
76
- mockPublicRequest.mockResolvedValue({
77
- type: 'code',
78
- name: 'my-agent',
79
- version: 'latest',
80
- supported_providers: ['any'],
81
- });
82
- await program.parseAsync(['node', 'test', 'run', 'my-agent']);
83
- (0, vitest_1.expect)(mockPublicRequest).toHaveBeenCalledWith(vitest_1.expect.any(Object), '/public/agents/default-org/my-agent/latest/download');
84
- });
85
- (0, vitest_1.it)('parses agent@version format', async () => {
86
- mockPublicRequest.mockResolvedValue({
87
- type: 'code',
88
- name: 'my-agent',
89
- version: 'v3',
90
- supported_providers: ['any'],
91
- });
92
- await program.parseAsync(['node', 'test', 'run', 'my-agent@v3']);
93
- (0, vitest_1.expect)(mockPublicRequest).toHaveBeenCalledWith(vitest_1.expect.any(Object), '/public/agents/default-org/my-agent/v3/download');
94
- });
95
- (0, vitest_1.it)('throws error for invalid agent ref format', async () => {
96
- mockGetResolvedConfig.mockResolvedValue({
97
- apiUrl: 'https://api.test.com',
98
- // No defaultOrg
99
- });
100
- await (0, vitest_1.expect)(program.parseAsync(['node', 'test', 'run', 'just-agent'])).rejects.toThrow('Missing org');
101
- });
102
- (0, vitest_1.it)('throws error for too many segments', async () => {
103
- await (0, vitest_1.expect)(program.parseAsync(['node', 'test', 'run', 'a/b/c/d'])).rejects.toThrow('Invalid agent reference');
104
- });
105
- });
106
- (0, vitest_1.describe)('run command - download agent', () => {
107
- let program;
108
- let stdoutSpy;
109
- let stderrSpy;
110
- (0, vitest_1.beforeEach)(() => {
111
- vitest_1.vi.clearAllMocks();
112
- program = new commander_1.Command();
113
- program.exitOverride();
114
- (0, run_1.registerRunCommand)(program);
115
- stdoutSpy = vitest_1.vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
116
- stderrSpy = vitest_1.vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
117
- mockGetResolvedConfig.mockResolvedValue({
118
- apiKey: 'sk_test_123',
119
- apiUrl: 'https://api.test.com',
120
- defaultOrg: 'test-org',
121
- });
122
- mockFs.mkdir.mockResolvedValue(undefined);
123
- mockFs.writeFile.mockResolvedValue(undefined);
124
- });
125
- (0, vitest_1.afterEach)(() => {
126
- stdoutSpy.mockRestore();
127
- stderrSpy.mockRestore();
128
- vitest_1.vi.restoreAllMocks();
129
- });
130
- (0, vitest_1.it)('downloads agent via public API', async () => {
131
- mockPublicRequest.mockResolvedValue({
132
- type: 'prompt',
133
- name: 'test-agent',
134
- version: 'v1',
135
- description: 'A test agent',
136
- prompt: 'You are a test assistant.',
137
- supported_providers: ['openai'],
138
- });
139
- await program.parseAsync(['node', 'test', 'run', 'test-org/test-agent@v1']);
140
- (0, vitest_1.expect)(mockPublicRequest).toHaveBeenCalledWith({ apiKey: 'sk_test_123', apiUrl: 'https://api.test.com', defaultOrg: 'test-org' }, '/public/agents/test-org/test-agent/v1/download');
141
- });
142
- (0, vitest_1.it)('falls back to getPublicAgent if download fails', async () => {
143
- mockPublicRequest.mockRejectedValue(new Error('Not found'));
144
- mockGetPublicAgent.mockResolvedValue({
145
- id: 'agent-123',
146
- org_id: 'org-123',
147
- name: 'fallback-agent',
148
- version: 'v1',
149
- type: 'code',
150
- supported_providers: ['any'],
151
- is_public: true,
152
- });
153
- await program.parseAsync(['node', 'test', 'run', 'test-org/fallback-agent@v1']);
154
- (0, vitest_1.expect)(mockGetPublicAgent).toHaveBeenCalledWith(vitest_1.expect.any(Object), 'test-org', 'fallback-agent', 'v1');
155
- });
156
- (0, vitest_1.it)('saves agent metadata locally', async () => {
157
- mockPublicRequest.mockResolvedValue({
158
- type: 'prompt',
159
- name: 'saved-agent',
160
- version: 'v1',
161
- prompt: 'Test prompt',
162
- supported_providers: ['any'],
163
- });
164
- await program.parseAsync(['node', 'test', 'run', 'test-org/saved-agent@v1']);
165
- (0, vitest_1.expect)(mockFs.mkdir).toHaveBeenCalled();
166
- (0, vitest_1.expect)(mockFs.writeFile).toHaveBeenCalledWith(vitest_1.expect.stringContaining('agent.json'), vitest_1.expect.stringContaining('"name": "saved-agent"'));
167
- });
168
- (0, vitest_1.it)('saves prompt.md for prompt agents', async () => {
169
- mockPublicRequest.mockResolvedValue({
170
- type: 'prompt',
171
- name: 'prompt-agent',
172
- version: 'v1',
173
- prompt: 'You are a helpful assistant.',
174
- supported_providers: ['any'],
175
- });
176
- await program.parseAsync(['node', 'test', 'run', 'test-org/prompt-agent@v1']);
177
- (0, vitest_1.expect)(mockFs.writeFile).toHaveBeenCalledWith(vitest_1.expect.stringContaining('prompt.md'), 'You are a helpful assistant.');
178
- });
179
- (0, vitest_1.it)('handles --download-only flag', async () => {
180
- mockPublicRequest.mockResolvedValue({
181
- type: 'prompt',
182
- name: 'download-only-agent',
183
- version: 'v1',
184
- prompt: 'Test',
185
- supported_providers: ['any'],
186
- });
187
- await program.parseAsync([
188
- 'node',
189
- 'test',
190
- 'run',
191
- 'test-org/download-only-agent@v1',
192
- '--download-only',
193
- ]);
194
- (0, vitest_1.expect)(stdoutSpy).toHaveBeenCalledWith(vitest_1.expect.stringContaining('Agent downloaded'));
195
- });
196
- });
197
- (0, vitest_1.describe)('LLM utilities - detectLlmKeyFromEnv', () => {
198
- const originalEnv = process.env;
199
- (0, vitest_1.beforeEach)(() => {
200
- process.env = { ...originalEnv };
201
- delete process.env.OPENAI_API_KEY;
202
- delete process.env.ANTHROPIC_API_KEY;
203
- delete process.env.GEMINI_API_KEY;
204
- });
205
- (0, vitest_1.afterEach)(() => {
206
- process.env = originalEnv;
207
- });
208
- (0, vitest_1.it)('returns null when no keys found', () => {
209
- const result = (0, llm_1.detectLlmKeyFromEnv)(['openai']);
210
- (0, vitest_1.expect)(result).toBeNull();
211
- });
212
- (0, vitest_1.it)('detects OpenAI key', () => {
213
- process.env.OPENAI_API_KEY = 'sk-openai-test';
214
- const result = (0, llm_1.detectLlmKeyFromEnv)(['openai']);
215
- (0, vitest_1.expect)(result).toEqual({ provider: 'openai', key: 'sk-openai-test' });
216
- });
217
- (0, vitest_1.it)('detects Anthropic key', () => {
218
- process.env.ANTHROPIC_API_KEY = 'sk-ant-test';
219
- const result = (0, llm_1.detectLlmKeyFromEnv)(['anthropic']);
220
- (0, vitest_1.expect)(result).toEqual({ provider: 'anthropic', key: 'sk-ant-test' });
221
- });
222
- (0, vitest_1.it)('detects Gemini key', () => {
223
- process.env.GEMINI_API_KEY = 'AIza-gemini-test';
224
- const result = (0, llm_1.detectLlmKeyFromEnv)(['gemini']);
225
- (0, vitest_1.expect)(result).toEqual({ provider: 'gemini', key: 'AIza-gemini-test' });
226
- });
227
- (0, vitest_1.it)('returns first matching provider in order', () => {
228
- process.env.ANTHROPIC_API_KEY = 'sk-ant-test';
229
- process.env.OPENAI_API_KEY = 'sk-openai-test';
230
- const result = (0, llm_1.detectLlmKeyFromEnv)(['openai', 'anthropic']);
231
- (0, vitest_1.expect)(result).toEqual({ provider: 'openai', key: 'sk-openai-test' });
232
- });
233
- (0, vitest_1.it)('handles "any" provider - checks all in order', () => {
234
- process.env.GEMINI_API_KEY = 'AIza-gemini-test';
235
- const result = (0, llm_1.detectLlmKeyFromEnv)(['any']);
236
- // 'any' should find the first available key
237
- (0, vitest_1.expect)(result?.key).toBe('AIza-gemini-test');
238
- });
239
- (0, vitest_1.it)('handles "any" provider - returns first found', () => {
240
- process.env.OPENAI_API_KEY = 'sk-openai-test';
241
- process.env.ANTHROPIC_API_KEY = 'sk-ant-test';
242
- const result = (0, llm_1.detectLlmKeyFromEnv)(['any']);
243
- // OpenAI is checked first in the provider order
244
- (0, vitest_1.expect)(result).toEqual({ provider: 'openai', key: 'sk-openai-test' });
245
- });
246
- (0, vitest_1.it)('skips unavailable providers', () => {
247
- process.env.ANTHROPIC_API_KEY = 'sk-ant-test';
248
- // No OpenAI key
249
- const result = (0, llm_1.detectLlmKeyFromEnv)(['openai', 'anthropic']);
250
- (0, vitest_1.expect)(result).toEqual({ provider: 'anthropic', key: 'sk-ant-test' });
251
- });
252
- });
253
- (0, vitest_1.describe)('LLM utilities - getDefaultModel', () => {
254
- (0, vitest_1.it)('returns default model for OpenAI', () => {
255
- (0, vitest_1.expect)((0, llm_1.getDefaultModel)('openai')).toBe('gpt-4o');
256
- });
257
- (0, vitest_1.it)('returns default model for Anthropic', () => {
258
- (0, vitest_1.expect)((0, llm_1.getDefaultModel)('anthropic')).toBe('claude-sonnet-4-20250514');
259
- });
260
- (0, vitest_1.it)('returns default model for Gemini', () => {
261
- (0, vitest_1.expect)((0, llm_1.getDefaultModel)('gemini')).toBe('gemini-1.5-pro');
262
- });
263
- (0, vitest_1.it)('returns gpt-4o for unknown provider', () => {
264
- (0, vitest_1.expect)((0, llm_1.getDefaultModel)('unknown')).toBe('gpt-4o');
265
- });
266
- });
267
- (0, vitest_1.describe)('LLM utilities - buildPrompt', () => {
268
- (0, vitest_1.it)('substitutes single variable', () => {
269
- const template = 'Analyze this: {{input}}';
270
- const result = (0, llm_1.buildPrompt)(template, { input: 'Hello world' });
271
- (0, vitest_1.expect)(result).toContain('Analyze this: Hello world');
272
- });
273
- (0, vitest_1.it)('substitutes multiple variables', () => {
274
- const template = 'Name: {{name}}, Age: {{age}}';
275
- const result = (0, llm_1.buildPrompt)(template, { name: 'Alice', age: 30 });
276
- (0, vitest_1.expect)(result).toContain('Name: Alice, Age: 30');
277
- });
278
- (0, vitest_1.it)('appends JSON input block', () => {
279
- const template = 'Process this';
280
- const result = (0, llm_1.buildPrompt)(template, { key: 'value' });
281
- (0, vitest_1.expect)(result).toContain('Input:');
282
- (0, vitest_1.expect)(result).toContain('```json');
283
- (0, vitest_1.expect)(result).toContain('"key": "value"');
284
- });
285
- (0, vitest_1.it)('handles empty input data', () => {
286
- const template = 'No inputs needed';
287
- const result = (0, llm_1.buildPrompt)(template, {});
288
- (0, vitest_1.expect)(result).toBe('No inputs needed');
289
- });
290
- (0, vitest_1.it)('handles multiple occurrences of same variable', () => {
291
- const template = '{{name}} said hello. {{name}} waved goodbye.';
292
- const result = (0, llm_1.buildPrompt)(template, { name: 'Bob' });
293
- (0, vitest_1.expect)(result).toContain('Bob said hello. Bob waved goodbye.');
294
- });
295
- (0, vitest_1.it)('preserves unused placeholders', () => {
296
- const template = 'Value: {{exists}}, Missing: {{missing}}';
297
- const result = (0, llm_1.buildPrompt)(template, { exists: 'here' });
298
- (0, vitest_1.expect)(result).toContain('Value: here');
299
- (0, vitest_1.expect)(result).toContain('{{missing}}');
300
- });
301
- (0, vitest_1.it)('handles complex nested objects in JSON', () => {
302
- const template = 'Process data';
303
- const inputData = {
304
- nested: { deep: { value: 42 } },
305
- array: [1, 2, 3],
306
- };
307
- const result = (0, llm_1.buildPrompt)(template, inputData);
308
- (0, vitest_1.expect)(result).toContain('"nested"');
309
- (0, vitest_1.expect)(result).toContain('"deep"');
310
- (0, vitest_1.expect)(result).toContain('"array"');
311
- });
312
- (0, vitest_1.it)('converts non-string values to strings', () => {
313
- const template = 'Count: {{count}}, Active: {{active}}';
314
- const result = (0, llm_1.buildPrompt)(template, { count: 42, active: true });
315
- (0, vitest_1.expect)(result).toContain('Count: 42');
316
- (0, vitest_1.expect)(result).toContain('Active: true');
317
- });
318
- });
319
- (0, vitest_1.describe)('LLM utilities - constants', () => {
320
- (0, vitest_1.it)('has correct provider env vars', () => {
321
- (0, vitest_1.expect)(llm_1.PROVIDER_ENV_VARS.openai).toBe('OPENAI_API_KEY');
322
- (0, vitest_1.expect)(llm_1.PROVIDER_ENV_VARS.anthropic).toBe('ANTHROPIC_API_KEY');
323
- (0, vitest_1.expect)(llm_1.PROVIDER_ENV_VARS.gemini).toBe('GEMINI_API_KEY');
324
- });
325
- (0, vitest_1.it)('has correct default models', () => {
326
- (0, vitest_1.expect)(llm_1.DEFAULT_MODELS.openai).toBe('gpt-4o');
327
- (0, vitest_1.expect)(llm_1.DEFAULT_MODELS.anthropic).toBe('claude-sonnet-4-20250514');
328
- (0, vitest_1.expect)(llm_1.DEFAULT_MODELS.gemini).toBe('gemini-1.5-pro');
329
- });
330
- });
@@ -1,230 +0,0 @@
1
- "use strict";
2
- /**
3
- * Tests for API client functions.
4
- *
5
- * These tests cover the core API client that all CLI commands depend on:
6
- * - Request building and authentication
7
- * - Error parsing
8
- * - Public vs authenticated requests
9
- */
10
- Object.defineProperty(exports, "__esModule", { value: true });
11
- const vitest_1 = require("vitest");
12
- const api_1 = require("./api");
13
- // Mock fetch globally
14
- const mockFetch = vitest_1.vi.fn();
15
- global.fetch = mockFetch;
16
- (0, vitest_1.describe)('ApiError', () => {
17
- (0, vitest_1.it)('includes status code and message', () => {
18
- const error = new api_1.ApiError('Not found', 404);
19
- (0, vitest_1.expect)(error.message).toBe('Not found');
20
- (0, vitest_1.expect)(error.status).toBe(404);
21
- });
22
- (0, vitest_1.it)('includes optional payload', () => {
23
- const payload = { error: { code: 'NOT_FOUND' } };
24
- const error = new api_1.ApiError('Not found', 404, payload);
25
- (0, vitest_1.expect)(error.payload).toEqual(payload);
26
- });
27
- });
28
- (0, vitest_1.describe)('request', () => {
29
- const config = {
30
- apiKey: 'sk_test_123',
31
- apiUrl: 'https://api.test.com',
32
- };
33
- (0, vitest_1.beforeEach)(() => {
34
- mockFetch.mockReset();
35
- });
36
- (0, vitest_1.afterEach)(() => {
37
- vitest_1.vi.restoreAllMocks();
38
- });
39
- (0, vitest_1.it)('adds Authorization header with Bearer token', async () => {
40
- mockFetch.mockResolvedValueOnce({
41
- ok: true,
42
- json: () => Promise.resolve({ data: 'test' }),
43
- });
44
- await (0, api_1.request)(config, 'GET', '/test');
45
- (0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('https://api.test.com/test', vitest_1.expect.objectContaining({
46
- method: 'GET',
47
- headers: vitest_1.expect.objectContaining({
48
- Authorization: 'Bearer sk_test_123',
49
- }),
50
- }));
51
- });
52
- (0, vitest_1.it)('returns parsed JSON response', async () => {
53
- const responseData = { org: 'test', slug: 'test-org' };
54
- mockFetch.mockResolvedValueOnce({
55
- ok: true,
56
- json: () => Promise.resolve(responseData),
57
- });
58
- const result = await (0, api_1.request)(config, 'GET', '/org');
59
- (0, vitest_1.expect)(result).toEqual(responseData);
60
- });
61
- (0, vitest_1.it)('throws ApiError when response not ok', async () => {
62
- mockFetch.mockResolvedValueOnce({
63
- ok: false,
64
- status: 404,
65
- statusText: 'Not Found',
66
- text: () => Promise.resolve(JSON.stringify({
67
- error: { message: 'Agent not found' }
68
- })),
69
- });
70
- await (0, vitest_1.expect)((0, api_1.request)(config, 'GET', '/agents/missing'))
71
- .rejects.toThrow(api_1.ApiError);
72
- });
73
- (0, vitest_1.it)('throws ApiError with 401 when no API key', async () => {
74
- const noKeyConfig = {
75
- apiKey: undefined,
76
- apiUrl: 'https://api.test.com',
77
- };
78
- await (0, vitest_1.expect)((0, api_1.request)(noKeyConfig, 'GET', '/test'))
79
- .rejects.toThrow('Missing API key');
80
- });
81
- (0, vitest_1.it)('parses error message from JSON response', async () => {
82
- mockFetch.mockResolvedValueOnce({
83
- ok: false,
84
- status: 403,
85
- statusText: 'Forbidden',
86
- text: () => Promise.resolve(JSON.stringify({
87
- error: { message: 'Access denied to private agent' }
88
- })),
89
- });
90
- try {
91
- await (0, api_1.request)(config, 'GET', '/agents/private');
92
- vitest_1.expect.fail('Should have thrown');
93
- }
94
- catch (error) {
95
- (0, vitest_1.expect)(error).toBeInstanceOf(api_1.ApiError);
96
- (0, vitest_1.expect)(error.message).toBe('Access denied to private agent');
97
- (0, vitest_1.expect)(error.status).toBe(403);
98
- }
99
- });
100
- (0, vitest_1.it)('uses statusText when no JSON message', async () => {
101
- mockFetch.mockResolvedValueOnce({
102
- ok: false,
103
- status: 500,
104
- statusText: 'Internal Server Error',
105
- text: () => Promise.resolve('not json'),
106
- });
107
- try {
108
- await (0, api_1.request)(config, 'GET', '/broken');
109
- vitest_1.expect.fail('Should have thrown');
110
- }
111
- catch (error) {
112
- (0, vitest_1.expect)(error).toBeInstanceOf(api_1.ApiError);
113
- (0, vitest_1.expect)(error.message).toBe('Internal Server Error');
114
- }
115
- });
116
- (0, vitest_1.it)('includes custom headers', async () => {
117
- mockFetch.mockResolvedValueOnce({
118
- ok: true,
119
- json: () => Promise.resolve({}),
120
- });
121
- await (0, api_1.request)(config, 'POST', '/agents', {
122
- body: JSON.stringify({ name: 'test' }),
123
- headers: { 'Content-Type': 'application/json' },
124
- });
125
- (0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith(vitest_1.expect.any(String), vitest_1.expect.objectContaining({
126
- headers: vitest_1.expect.objectContaining({
127
- 'Content-Type': 'application/json',
128
- Authorization: 'Bearer sk_test_123',
129
- }),
130
- }));
131
- });
132
- (0, vitest_1.it)('strips trailing slash from API URL', async () => {
133
- const configWithSlash = {
134
- apiKey: 'sk_test_123',
135
- apiUrl: 'https://api.test.com/',
136
- };
137
- mockFetch.mockResolvedValueOnce({
138
- ok: true,
139
- json: () => Promise.resolve({}),
140
- });
141
- await (0, api_1.request)(configWithSlash, 'GET', '/test');
142
- (0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('https://api.test.com/test', vitest_1.expect.any(Object));
143
- });
144
- });
145
- (0, vitest_1.describe)('publicRequest', () => {
146
- const config = {
147
- apiUrl: 'https://api.test.com',
148
- };
149
- (0, vitest_1.beforeEach)(() => {
150
- mockFetch.mockReset();
151
- });
152
- (0, vitest_1.it)('makes unauthenticated request', async () => {
153
- mockFetch.mockResolvedValueOnce({
154
- ok: true,
155
- json: () => Promise.resolve([]),
156
- });
157
- await (0, api_1.publicRequest)(config, '/public/agents');
158
- (0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('https://api.test.com/public/agents');
159
- });
160
- (0, vitest_1.it)('returns parsed JSON', async () => {
161
- const agents = [{ name: 'agent1' }, { name: 'agent2' }];
162
- mockFetch.mockResolvedValueOnce({
163
- ok: true,
164
- json: () => Promise.resolve(agents),
165
- });
166
- const result = await (0, api_1.publicRequest)(config, '/public/agents');
167
- (0, vitest_1.expect)(result).toEqual(agents);
168
- });
169
- (0, vitest_1.it)('throws ApiError on failure', async () => {
170
- mockFetch.mockResolvedValueOnce({
171
- ok: false,
172
- status: 404,
173
- statusText: 'Not Found',
174
- text: () => Promise.resolve('{}'),
175
- });
176
- await (0, vitest_1.expect)((0, api_1.publicRequest)(config, '/public/agents/missing'))
177
- .rejects.toThrow(api_1.ApiError);
178
- });
179
- });
180
- (0, vitest_1.describe)('getOrg', () => {
181
- const config = {
182
- apiKey: 'sk_test_123',
183
- apiUrl: 'https://api.test.com',
184
- };
185
- (0, vitest_1.beforeEach)(() => {
186
- mockFetch.mockReset();
187
- });
188
- (0, vitest_1.it)('calls GET /org endpoint', async () => {
189
- const orgData = { id: '123', slug: 'test-org', name: 'Test Org' };
190
- mockFetch.mockResolvedValueOnce({
191
- ok: true,
192
- json: () => Promise.resolve(orgData),
193
- });
194
- const result = await (0, api_1.getOrg)(config);
195
- (0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('https://api.test.com/org', vitest_1.expect.objectContaining({ method: 'GET' }));
196
- (0, vitest_1.expect)(result).toEqual(orgData);
197
- });
198
- });
199
- (0, vitest_1.describe)('searchAgents', () => {
200
- const config = {
201
- apiUrl: 'https://api.test.com',
202
- };
203
- (0, vitest_1.beforeEach)(() => {
204
- mockFetch.mockReset();
205
- });
206
- (0, vitest_1.it)('builds search query params', async () => {
207
- mockFetch.mockResolvedValueOnce({
208
- ok: true,
209
- json: () => Promise.resolve([]),
210
- });
211
- await (0, api_1.searchAgents)(config, 'pdf analyzer', { sort: 'stars' });
212
- (0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('https://api.test.com/public/agents?search=pdf+analyzer&sort=stars');
213
- });
214
- (0, vitest_1.it)('handles empty query', async () => {
215
- mockFetch.mockResolvedValueOnce({
216
- ok: true,
217
- json: () => Promise.resolve([]),
218
- });
219
- await (0, api_1.searchAgents)(config, '');
220
- (0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('https://api.test.com/public/agents');
221
- });
222
- (0, vitest_1.it)('includes tags in query', async () => {
223
- mockFetch.mockResolvedValueOnce({
224
- ok: true,
225
- json: () => Promise.resolve([]),
226
- });
227
- await (0, api_1.searchAgents)(config, 'test', { tags: ['ai', 'document'] });
228
- (0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith(vitest_1.expect.stringContaining('tags=ai%2Cdocument'));
229
- });
230
- });
@@ -1,144 +0,0 @@
1
- "use strict";
2
- /**
3
- * Tests for config loading and saving.
4
- *
5
- * These tests cover the security-critical config management:
6
- * - Loading config from file
7
- * - Saving config with proper permissions
8
- * - Config resolution priority (overrides > env > file)
9
- */
10
- var __importDefault = (this && this.__importDefault) || function (mod) {
11
- return (mod && mod.__esModule) ? mod : { "default": mod };
12
- };
13
- Object.defineProperty(exports, "__esModule", { value: true });
14
- const vitest_1 = require("vitest");
15
- const path_1 = __importDefault(require("path"));
16
- const os_1 = __importDefault(require("os"));
17
- // Mock fs/promises
18
- vitest_1.vi.mock('fs/promises', () => ({
19
- default: {
20
- readFile: vitest_1.vi.fn(),
21
- writeFile: vitest_1.vi.fn(),
22
- mkdir: vitest_1.vi.fn(),
23
- chmod: vitest_1.vi.fn(),
24
- },
25
- }));
26
- const promises_1 = __importDefault(require("fs/promises"));
27
- const config_1 = require("./config");
28
- (0, vitest_1.describe)('loadConfig', () => {
29
- (0, vitest_1.beforeEach)(() => {
30
- vitest_1.vi.resetAllMocks();
31
- });
32
- (0, vitest_1.it)('returns empty object when config file missing', async () => {
33
- const error = new Error('ENOENT');
34
- error.code = 'ENOENT';
35
- vitest_1.vi.mocked(promises_1.default.readFile).mockRejectedValueOnce(error);
36
- const config = await (0, config_1.loadConfig)();
37
- (0, vitest_1.expect)(config).toEqual({});
38
- });
39
- (0, vitest_1.it)('parses JSON config file', async () => {
40
- const configData = {
41
- api_key: 'sk_test_123',
42
- api_url: 'https://custom.api.com',
43
- default_org: 'my-org',
44
- };
45
- vitest_1.vi.mocked(promises_1.default.readFile).mockResolvedValueOnce(JSON.stringify(configData));
46
- const config = await (0, config_1.loadConfig)();
47
- (0, vitest_1.expect)(config).toEqual(configData);
48
- });
49
- (0, vitest_1.it)('throws on non-ENOENT errors', async () => {
50
- const error = new Error('Permission denied');
51
- error.code = 'EACCES';
52
- vitest_1.vi.mocked(promises_1.default.readFile).mockRejectedValueOnce(error);
53
- await (0, vitest_1.expect)((0, config_1.loadConfig)()).rejects.toThrow('Permission denied');
54
- });
55
- (0, vitest_1.it)('reads from ~/.orchagent/config.json', async () => {
56
- vitest_1.vi.mocked(promises_1.default.readFile).mockResolvedValueOnce('{}');
57
- await (0, config_1.loadConfig)();
58
- (0, vitest_1.expect)(promises_1.default.readFile).toHaveBeenCalledWith(path_1.default.join(os_1.default.homedir(), '.orchagent', 'config.json'), 'utf-8');
59
- });
60
- });
61
- (0, vitest_1.describe)('saveConfig', () => {
62
- (0, vitest_1.beforeEach)(() => {
63
- vitest_1.vi.resetAllMocks();
64
- vitest_1.vi.mocked(promises_1.default.mkdir).mockResolvedValue(undefined);
65
- vitest_1.vi.mocked(promises_1.default.writeFile).mockResolvedValue(undefined);
66
- vitest_1.vi.mocked(promises_1.default.chmod).mockResolvedValue(undefined);
67
- });
68
- (0, vitest_1.it)('creates config directory if missing', async () => {
69
- await (0, config_1.saveConfig)({ api_key: 'sk_test' });
70
- (0, vitest_1.expect)(promises_1.default.mkdir).toHaveBeenCalledWith(path_1.default.join(os_1.default.homedir(), '.orchagent'), { recursive: true });
71
- });
72
- (0, vitest_1.it)('writes JSON with pretty formatting', async () => {
73
- const config = { api_key: 'sk_test_123' };
74
- await (0, config_1.saveConfig)(config);
75
- (0, vitest_1.expect)(promises_1.default.writeFile).toHaveBeenCalledWith(path_1.default.join(os_1.default.homedir(), '.orchagent', 'config.json'), vitest_1.expect.stringContaining('"api_key": "sk_test_123"'), { mode: 0o600 });
76
- });
77
- (0, vitest_1.it)('sets restrictive file permissions (0600)', async () => {
78
- await (0, config_1.saveConfig)({ api_key: 'sk_test' });
79
- // First via writeFile options
80
- (0, vitest_1.expect)(promises_1.default.writeFile).toHaveBeenCalledWith(vitest_1.expect.any(String), vitest_1.expect.any(String), { mode: 0o600 });
81
- // Then explicitly with chmod
82
- (0, vitest_1.expect)(promises_1.default.chmod).toHaveBeenCalledWith(path_1.default.join(os_1.default.homedir(), '.orchagent', 'config.json'), 0o600);
83
- });
84
- (0, vitest_1.it)('adds trailing newline to file', async () => {
85
- await (0, config_1.saveConfig)({ api_key: 'test' });
86
- const writeCall = vitest_1.vi.mocked(promises_1.default.writeFile).mock.calls[0];
87
- const content = writeCall[1];
88
- (0, vitest_1.expect)(content.endsWith('\n')).toBe(true);
89
- });
90
- });
91
- (0, vitest_1.describe)('getResolvedConfig', () => {
92
- const originalEnv = process.env;
93
- (0, vitest_1.beforeEach)(() => {
94
- vitest_1.vi.resetAllMocks();
95
- process.env = { ...originalEnv };
96
- // Default: no config file
97
- const error = new Error('ENOENT');
98
- error.code = 'ENOENT';
99
- vitest_1.vi.mocked(promises_1.default.readFile).mockRejectedValue(error);
100
- });
101
- (0, vitest_1.afterEach)(() => {
102
- process.env = originalEnv;
103
- });
104
- (0, vitest_1.it)('uses default API URL when not configured', async () => {
105
- const config = await (0, config_1.getResolvedConfig)();
106
- (0, vitest_1.expect)(config.apiUrl).toBe('https://api.orchagent.com');
107
- });
108
- (0, vitest_1.it)('reads API key from file config', async () => {
109
- vitest_1.vi.mocked(promises_1.default.readFile).mockResolvedValueOnce(JSON.stringify({ api_key: 'sk_from_file' }));
110
- const config = await (0, config_1.getResolvedConfig)();
111
- (0, vitest_1.expect)(config.apiKey).toBe('sk_from_file');
112
- });
113
- (0, vitest_1.it)('prioritizes env vars over file config', async () => {
114
- process.env.ORCHAGENT_API_KEY = 'sk_from_env';
115
- vitest_1.vi.mocked(promises_1.default.readFile).mockResolvedValueOnce(JSON.stringify({ api_key: 'sk_from_file' }));
116
- const config = await (0, config_1.getResolvedConfig)();
117
- (0, vitest_1.expect)(config.apiKey).toBe('sk_from_env');
118
- });
119
- (0, vitest_1.it)('prioritizes overrides over env vars', async () => {
120
- process.env.ORCHAGENT_API_KEY = 'sk_from_env';
121
- const config = await (0, config_1.getResolvedConfig)({ api_key: 'sk_override' });
122
- (0, vitest_1.expect)(config.apiKey).toBe('sk_override');
123
- });
124
- (0, vitest_1.it)('resolves API URL from env var', async () => {
125
- process.env.ORCHAGENT_API_URL = 'https://custom.api.com';
126
- const config = await (0, config_1.getResolvedConfig)();
127
- (0, vitest_1.expect)(config.apiUrl).toBe('https://custom.api.com');
128
- });
129
- (0, vitest_1.it)('resolves default org from env var', async () => {
130
- process.env.ORCHAGENT_DEFAULT_ORG = 'my-org';
131
- const config = await (0, config_1.getResolvedConfig)();
132
- (0, vitest_1.expect)(config.defaultOrg).toBe('my-org');
133
- });
134
- (0, vitest_1.it)('returns undefined apiKey when not set anywhere', async () => {
135
- const config = await (0, config_1.getResolvedConfig)();
136
- (0, vitest_1.expect)(config.apiKey).toBeUndefined();
137
- });
138
- });
139
- (0, vitest_1.describe)('getConfigPath', () => {
140
- (0, vitest_1.it)('returns path to config file', () => {
141
- const configPath = (0, config_1.getConfigPath)();
142
- (0, vitest_1.expect)(configPath).toBe(path_1.default.join(os_1.default.homedir(), '.orchagent', 'config.json'));
143
- });
144
- });