@orchagent/cli 0.3.23 → 0.3.25
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/commands/test.js +79 -51
- package/package.json +3 -1
package/dist/commands/test.js
CHANGED
|
@@ -9,9 +9,32 @@ const path_1 = __importDefault(require("path"));
|
|
|
9
9
|
const child_process_1 = require("child_process");
|
|
10
10
|
const chalk_1 = __importDefault(require("chalk"));
|
|
11
11
|
const yaml_1 = __importDefault(require("yaml"));
|
|
12
|
+
const fast_deep_equal_1 = __importDefault(require("fast-deep-equal"));
|
|
13
|
+
const chokidar_1 = __importDefault(require("chokidar"));
|
|
12
14
|
const errors_1 = require("../lib/errors");
|
|
13
15
|
const config_1 = require("../lib/config");
|
|
14
16
|
const llm_1 = require("../lib/llm");
|
|
17
|
+
/**
|
|
18
|
+
* Validate a fixture and return helpful errors
|
|
19
|
+
*/
|
|
20
|
+
function validateFixture(data, fixturePath) {
|
|
21
|
+
const fileName = path_1.default.basename(fixturePath);
|
|
22
|
+
if (typeof data !== 'object' || data === null) {
|
|
23
|
+
throw new errors_1.CliError(`Invalid fixture ${fileName}: must be a JSON object`);
|
|
24
|
+
}
|
|
25
|
+
const obj = data;
|
|
26
|
+
if (!obj.input || typeof obj.input !== 'object') {
|
|
27
|
+
throw new errors_1.CliError(`Invalid fixture ${fileName}: missing required "input" field.\n` +
|
|
28
|
+
` Expected format: { "input": {...}, "expected_output": {...} }`);
|
|
29
|
+
}
|
|
30
|
+
if (!obj.expected_output && !obj.expected_contains) {
|
|
31
|
+
throw new errors_1.CliError(`Invalid fixture ${fileName}: must have "expected_output" or "expected_contains".\n` +
|
|
32
|
+
` Add one of:\n` +
|
|
33
|
+
` "expected_output": {"key": "exact value to match"}\n` +
|
|
34
|
+
` "expected_contains": ["substring to find"]`);
|
|
35
|
+
}
|
|
36
|
+
return data;
|
|
37
|
+
}
|
|
15
38
|
/**
|
|
16
39
|
* Parse SKILL.md frontmatter
|
|
17
40
|
*/
|
|
@@ -68,8 +91,10 @@ function runCommand(command, args, cwd, verbose) {
|
|
|
68
91
|
* Check if a command exists
|
|
69
92
|
*/
|
|
70
93
|
async function commandExists(command) {
|
|
94
|
+
const isWindows = process.platform === 'win32';
|
|
95
|
+
const checker = isWindows ? 'where' : 'which';
|
|
71
96
|
try {
|
|
72
|
-
const proc = (0, child_process_1.spawn)(
|
|
97
|
+
const proc = (0, child_process_1.spawn)(checker, [command], { shell: true, stdio: 'ignore' });
|
|
73
98
|
return new Promise((resolve) => {
|
|
74
99
|
proc.on('close', (code) => resolve(code === 0));
|
|
75
100
|
proc.on('error', () => resolve(false));
|
|
@@ -202,22 +227,28 @@ async function discoverTests(agentDir) {
|
|
|
202
227
|
*/
|
|
203
228
|
async function runPythonTests(agentDir, verbose) {
|
|
204
229
|
process.stderr.write(chalk_1.default.blue('\nRunning Python tests...\n\n'));
|
|
205
|
-
// Check if pytest is available
|
|
230
|
+
// Check if pytest is available directly
|
|
206
231
|
const hasPytest = await commandExists('pytest');
|
|
207
|
-
let command;
|
|
208
|
-
let args;
|
|
209
232
|
if (hasPytest) {
|
|
210
|
-
|
|
211
|
-
|
|
233
|
+
const args = verbose ? ['-v'] : [];
|
|
234
|
+
const { code } = await runCommand('pytest', args, agentDir, verbose);
|
|
235
|
+
return code;
|
|
212
236
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
237
|
+
// Try Python commands in order of preference
|
|
238
|
+
const pythonCommands = process.platform === 'win32'
|
|
239
|
+
? ['python', 'py', 'python3']
|
|
240
|
+
: ['python3', 'python'];
|
|
241
|
+
for (const pythonCmd of pythonCommands) {
|
|
242
|
+
if (await commandExists(pythonCmd)) {
|
|
243
|
+
const args = ['-m', 'pytest'];
|
|
244
|
+
if (verbose)
|
|
245
|
+
args.push('-v');
|
|
246
|
+
const { code } = await runCommand(pythonCmd, args, agentDir, verbose);
|
|
247
|
+
return code;
|
|
248
|
+
}
|
|
218
249
|
}
|
|
219
|
-
|
|
220
|
-
return
|
|
250
|
+
process.stderr.write(chalk_1.default.red('No Python interpreter found. Install Python and pytest.\n'));
|
|
251
|
+
return 1;
|
|
221
252
|
}
|
|
222
253
|
/**
|
|
223
254
|
* Run JavaScript/TypeScript tests
|
|
@@ -290,8 +321,8 @@ async function runFixtureTests(agentDir, fixtures, verbose, config) {
|
|
|
290
321
|
throw new errors_1.CliError('No LLM key found for fixture tests.\n' +
|
|
291
322
|
'Set an environment variable (e.g., OPENAI_API_KEY) or run `orchagent keys add <provider>`');
|
|
292
323
|
}
|
|
293
|
-
const { provider, key } = detected;
|
|
294
|
-
const model = (0, llm_1.getDefaultModel)(provider);
|
|
324
|
+
const { provider, key, model: serverModel } = detected;
|
|
325
|
+
const model = serverModel ?? (0, llm_1.getDefaultModel)(provider);
|
|
295
326
|
let passed = 0;
|
|
296
327
|
let failed = 0;
|
|
297
328
|
for (const fixturePath of fixtures) {
|
|
@@ -299,7 +330,14 @@ async function runFixtureTests(agentDir, fixtures, verbose, config) {
|
|
|
299
330
|
process.stderr.write(` ${fixtureName}: `);
|
|
300
331
|
try {
|
|
301
332
|
const raw = await promises_1.default.readFile(fixturePath, 'utf-8');
|
|
302
|
-
|
|
333
|
+
let parsed;
|
|
334
|
+
try {
|
|
335
|
+
parsed = JSON.parse(raw);
|
|
336
|
+
}
|
|
337
|
+
catch (e) {
|
|
338
|
+
throw new errors_1.CliError(`Invalid JSON in ${path_1.default.basename(fixturePath)}: ${e.message}`);
|
|
339
|
+
}
|
|
340
|
+
const fixture = validateFixture(parsed, fixturePath);
|
|
303
341
|
// Build and call LLM
|
|
304
342
|
const fullPrompt = (0, llm_1.buildPrompt)(prompt, fixture.input);
|
|
305
343
|
const result = await (0, llm_1.callLlm)(provider, key, model, fullPrompt, outputSchema);
|
|
@@ -307,12 +345,9 @@ async function runFixtureTests(agentDir, fixtures, verbose, config) {
|
|
|
307
345
|
let testPassed = true;
|
|
308
346
|
const failures = [];
|
|
309
347
|
if (fixture.expected_output) {
|
|
310
|
-
|
|
311
|
-
const resultStr = JSON.stringify(result, Object.keys(result).sort());
|
|
312
|
-
const expectedStr = JSON.stringify(fixture.expected_output, Object.keys(fixture.expected_output).sort());
|
|
313
|
-
if (resultStr !== expectedStr) {
|
|
348
|
+
if (!(0, fast_deep_equal_1.default)(result, fixture.expected_output)) {
|
|
314
349
|
testPassed = false;
|
|
315
|
-
failures.push(`Expected: ${
|
|
350
|
+
failures.push(`Expected: ${JSON.stringify(fixture.expected_output, null, 2)}\nGot: ${JSON.stringify(result, null, 2)}`);
|
|
316
351
|
}
|
|
317
352
|
}
|
|
318
353
|
if (fixture.expected_contains) {
|
|
@@ -355,46 +390,39 @@ async function runFixtureTests(agentDir, fixtures, verbose, config) {
|
|
|
355
390
|
/**
|
|
356
391
|
* Watch mode: re-run tests on file changes
|
|
357
392
|
*/
|
|
358
|
-
async function watchTests(agentDir, agentType,
|
|
393
|
+
async function watchTests(agentDir, agentType, verbose, config) {
|
|
359
394
|
process.stderr.write(chalk_1.default.cyan('\nWatching for file changes... (press Ctrl+C to exit)\n\n'));
|
|
360
|
-
// Collect all files to watch
|
|
361
|
-
const watchPaths = [agentDir];
|
|
362
395
|
const runTests = async () => {
|
|
363
396
|
process.stderr.write(chalk_1.default.dim(`\n[${new Date().toLocaleTimeString()}] Running tests...\n`));
|
|
397
|
+
// Re-discover tests each time to pick up new files
|
|
398
|
+
const testFiles = await discoverTests(agentDir);
|
|
364
399
|
await executeTests(agentDir, agentType, testFiles, verbose, config);
|
|
365
400
|
};
|
|
366
401
|
// Initial run
|
|
367
402
|
await runTests();
|
|
368
|
-
// Set up
|
|
369
|
-
const watchers = [];
|
|
403
|
+
// Set up chokidar watcher
|
|
370
404
|
let debounceTimer = null;
|
|
371
|
-
const onChange = () => {
|
|
405
|
+
const onChange = (filePath) => {
|
|
372
406
|
if (debounceTimer)
|
|
373
407
|
clearTimeout(debounceTimer);
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
for (const watchPath of watchPaths) {
|
|
377
|
-
try {
|
|
378
|
-
// Use recursive watch on the directory
|
|
379
|
-
const ac = new AbortController();
|
|
380
|
-
(async () => {
|
|
381
|
-
try {
|
|
382
|
-
const watcher = promises_1.default.watch(watchPath, { recursive: true, signal: ac.signal });
|
|
383
|
-
for await (const _event of watcher) {
|
|
384
|
-
onChange();
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
catch (err) {
|
|
388
|
-
if (err.name !== 'AbortError') {
|
|
389
|
-
// Watcher error, ignore
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
})();
|
|
393
|
-
}
|
|
394
|
-
catch {
|
|
395
|
-
// Watch not supported, fall back to polling
|
|
408
|
+
if (verbose) {
|
|
409
|
+
process.stderr.write(chalk_1.default.dim(` Changed: ${path_1.default.relative(agentDir, filePath)}\n`));
|
|
396
410
|
}
|
|
397
|
-
|
|
411
|
+
debounceTimer = setTimeout(runTests, 300);
|
|
412
|
+
};
|
|
413
|
+
const watcher = chokidar_1.default.watch(agentDir, {
|
|
414
|
+
ignored: /(node_modules|__pycache__|\.git|dist|build|\.venv|venv)/,
|
|
415
|
+
persistent: true,
|
|
416
|
+
ignoreInitial: true,
|
|
417
|
+
});
|
|
418
|
+
watcher
|
|
419
|
+
.on('change', onChange)
|
|
420
|
+
.on('add', onChange)
|
|
421
|
+
.on('unlink', onChange)
|
|
422
|
+
.on('error', (error) => {
|
|
423
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
424
|
+
process.stderr.write(chalk_1.default.red(`Watcher error: ${message}\n`));
|
|
425
|
+
});
|
|
398
426
|
// Keep process alive
|
|
399
427
|
await new Promise(() => { });
|
|
400
428
|
}
|
|
@@ -520,7 +548,7 @@ Fixture Format (tests/fixture-1.json):
|
|
|
520
548
|
}
|
|
521
549
|
// Watch mode
|
|
522
550
|
if (options.watch) {
|
|
523
|
-
await watchTests(agentDir, agentType,
|
|
551
|
+
await watchTests(agentDir, agentType, !!options.verbose, config);
|
|
524
552
|
return;
|
|
525
553
|
}
|
|
526
554
|
// Run tests
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orchagent/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.25",
|
|
4
4
|
"description": "Command-line interface for the orchagent AI agent marketplace",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "orchagent <hello@orchagent.io>",
|
|
@@ -46,8 +46,10 @@
|
|
|
46
46
|
"@sentry/node": "^9.3.0",
|
|
47
47
|
"archiver": "^7.0.0",
|
|
48
48
|
"chalk": "^4.1.2",
|
|
49
|
+
"chokidar": "^4.0.0",
|
|
49
50
|
"cli-table3": "^0.6.3",
|
|
50
51
|
"commander": "^11.1.0",
|
|
52
|
+
"fast-deep-equal": "^3.1.3",
|
|
51
53
|
"open": "^8.4.2",
|
|
52
54
|
"ora": "^9.1.0",
|
|
53
55
|
"posthog-node": "^4.0.0",
|