cognitive-modules-cli 2.2.1 → 2.2.7
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/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +35 -29
- package/dist/cli.js +519 -23
- package/dist/commands/add.d.ts +33 -14
- package/dist/commands/add.js +383 -16
- package/dist/commands/compose.js +60 -23
- package/dist/commands/index.d.ts +4 -0
- package/dist/commands/index.js +4 -0
- package/dist/commands/init.js +23 -1
- package/dist/commands/migrate.d.ts +30 -0
- package/dist/commands/migrate.js +650 -0
- package/dist/commands/pipe.d.ts +1 -0
- package/dist/commands/pipe.js +31 -11
- package/dist/commands/remove.js +33 -2
- package/dist/commands/run.d.ts +2 -0
- package/dist/commands/run.js +61 -28
- package/dist/commands/search.d.ts +28 -0
- package/dist/commands/search.js +143 -0
- package/dist/commands/test.d.ts +65 -0
- package/dist/commands/test.js +454 -0
- package/dist/commands/update.d.ts +1 -0
- package/dist/commands/update.js +106 -14
- package/dist/commands/validate.d.ts +36 -0
- package/dist/commands/validate.js +97 -0
- package/dist/errors/index.d.ts +225 -0
- package/dist/errors/index.js +420 -0
- package/dist/mcp/server.js +84 -79
- package/dist/modules/composition.js +97 -32
- package/dist/modules/loader.js +4 -2
- package/dist/modules/runner.d.ts +72 -5
- package/dist/modules/runner.js +306 -59
- package/dist/modules/subagent.d.ts +6 -1
- package/dist/modules/subagent.js +18 -13
- package/dist/modules/validator.js +14 -6
- package/dist/providers/anthropic.d.ts +15 -0
- package/dist/providers/anthropic.js +147 -5
- package/dist/providers/base.d.ts +11 -0
- package/dist/providers/base.js +18 -0
- package/dist/providers/gemini.d.ts +15 -0
- package/dist/providers/gemini.js +122 -5
- package/dist/providers/ollama.d.ts +15 -0
- package/dist/providers/ollama.js +111 -3
- package/dist/providers/openai.d.ts +11 -0
- package/dist/providers/openai.js +133 -0
- package/dist/registry/client.d.ts +212 -0
- package/dist/registry/client.js +359 -0
- package/dist/registry/index.d.ts +4 -0
- package/dist/registry/index.js +4 -0
- package/dist/registry/tar.d.ts +8 -0
- package/dist/registry/tar.js +353 -0
- package/dist/server/http.js +301 -45
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +1 -0
- package/dist/server/sse.d.ts +13 -0
- package/dist/server/sse.js +22 -0
- package/dist/types.d.ts +32 -1
- package/dist/types.js +4 -1
- package/dist/version.d.ts +1 -0
- package/dist/version.js +4 -0
- package/package.json +31 -7
- package/dist/modules/composition.test.d.ts +0 -11
- package/dist/modules/composition.test.js +0 -450
- package/dist/modules/policy.test.d.ts +0 -10
- package/dist/modules/policy.test.js +0 -369
- package/src/cli.ts +0 -471
- package/src/commands/add.ts +0 -315
- package/src/commands/compose.ts +0 -185
- package/src/commands/index.ts +0 -13
- package/src/commands/init.ts +0 -94
- package/src/commands/list.ts +0 -33
- package/src/commands/pipe.ts +0 -76
- package/src/commands/remove.ts +0 -57
- package/src/commands/run.ts +0 -80
- package/src/commands/update.ts +0 -130
- package/src/commands/versions.ts +0 -79
- package/src/index.ts +0 -90
- package/src/mcp/index.ts +0 -5
- package/src/mcp/server.ts +0 -403
- package/src/modules/composition.test.ts +0 -558
- package/src/modules/composition.ts +0 -1674
- package/src/modules/index.ts +0 -9
- package/src/modules/loader.ts +0 -508
- package/src/modules/policy.test.ts +0 -455
- package/src/modules/runner.ts +0 -1983
- package/src/modules/subagent.ts +0 -277
- package/src/modules/validator.ts +0 -700
- package/src/providers/anthropic.ts +0 -89
- package/src/providers/base.ts +0 -29
- package/src/providers/deepseek.ts +0 -83
- package/src/providers/gemini.ts +0 -117
- package/src/providers/index.ts +0 -78
- package/src/providers/minimax.ts +0 -81
- package/src/providers/moonshot.ts +0 -82
- package/src/providers/ollama.ts +0 -83
- package/src/providers/openai.ts +0 -84
- package/src/providers/qwen.ts +0 -82
- package/src/server/http.ts +0 -316
- package/src/server/index.ts +0 -6
- package/src/types.ts +0 -599
- package/tsconfig.json +0 -17
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test Command - Run golden tests for Cognitive Modules
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* cog test <module> - Run tests for a specific module
|
|
6
|
+
* cog test --all - Run tests for all modules
|
|
7
|
+
* cog test <module> --update - Update expected outputs
|
|
8
|
+
*
|
|
9
|
+
* Tests are defined in module.yaml:
|
|
10
|
+
* tests:
|
|
11
|
+
* - tests/case1.input.json -> tests/case1.expected.json
|
|
12
|
+
*/
|
|
13
|
+
import * as fs from 'node:fs';
|
|
14
|
+
import * as path from 'node:path';
|
|
15
|
+
import { findModule, listModules, getDefaultSearchPaths } from '../modules/loader.js';
|
|
16
|
+
import { runModule } from '../modules/runner.js';
|
|
17
|
+
// =============================================================================
|
|
18
|
+
// Test Case Parsing
|
|
19
|
+
// =============================================================================
|
|
20
|
+
/**
|
|
21
|
+
* Parse test definitions from module.yaml tests field
|
|
22
|
+
*
|
|
23
|
+
* Format: "tests/case1.input.json -> tests/case1.expected.json"
|
|
24
|
+
*/
|
|
25
|
+
export function parseTestDefinitions(module, modulePath) {
|
|
26
|
+
const tests = [];
|
|
27
|
+
const testsConfig = module.tests;
|
|
28
|
+
if (!testsConfig || !Array.isArray(testsConfig)) {
|
|
29
|
+
return tests;
|
|
30
|
+
}
|
|
31
|
+
for (const testDef of testsConfig) {
|
|
32
|
+
// Parse "input.json -> expected.json" format
|
|
33
|
+
const match = testDef.match(/^\s*(.+?)\s*->\s*(.+?)\s*$/);
|
|
34
|
+
if (!match) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const inputRelPath = match[1];
|
|
38
|
+
const expectedRelPath = match[2];
|
|
39
|
+
// Get the module directory
|
|
40
|
+
const moduleDir = path.dirname(modulePath);
|
|
41
|
+
const inputPath = path.resolve(moduleDir, inputRelPath);
|
|
42
|
+
const expectedPath = path.resolve(moduleDir, expectedRelPath);
|
|
43
|
+
// Extract test name from input file
|
|
44
|
+
const testName = path.basename(inputRelPath, '.input.json');
|
|
45
|
+
tests.push({
|
|
46
|
+
name: testName,
|
|
47
|
+
inputPath,
|
|
48
|
+
expectedPath,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return tests;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Auto-discover test cases in tests/ directory
|
|
55
|
+
*/
|
|
56
|
+
export function discoverTestCases(modulePath) {
|
|
57
|
+
const tests = [];
|
|
58
|
+
const moduleDir = path.dirname(modulePath);
|
|
59
|
+
const testsDir = path.join(moduleDir, 'tests');
|
|
60
|
+
if (!fs.existsSync(testsDir)) {
|
|
61
|
+
return tests;
|
|
62
|
+
}
|
|
63
|
+
const files = fs.readdirSync(testsDir);
|
|
64
|
+
const inputFiles = files.filter(f => f.endsWith('.input.json'));
|
|
65
|
+
for (const inputFile of inputFiles) {
|
|
66
|
+
const testName = inputFile.replace('.input.json', '');
|
|
67
|
+
const expectedFile = `${testName}.expected.json`;
|
|
68
|
+
if (files.includes(expectedFile)) {
|
|
69
|
+
tests.push({
|
|
70
|
+
name: testName,
|
|
71
|
+
inputPath: path.join(testsDir, inputFile),
|
|
72
|
+
expectedPath: path.join(testsDir, expectedFile),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return tests;
|
|
77
|
+
}
|
|
78
|
+
// =============================================================================
|
|
79
|
+
// Test Execution
|
|
80
|
+
// =============================================================================
|
|
81
|
+
/**
|
|
82
|
+
* Check if expected file uses schema validation format ($validate)
|
|
83
|
+
*/
|
|
84
|
+
function isSchemaValidationFormat(expected) {
|
|
85
|
+
return (typeof expected === 'object' &&
|
|
86
|
+
expected !== null &&
|
|
87
|
+
'$validate' in expected);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Validate actual result against schema validation format
|
|
91
|
+
* Returns array of validation errors
|
|
92
|
+
*/
|
|
93
|
+
function validateAgainstSchema(schema, actual, path = '') {
|
|
94
|
+
const errors = [];
|
|
95
|
+
if (actual === null || actual === undefined) {
|
|
96
|
+
errors.push({ field: path || 'root', expected: 'value', actual });
|
|
97
|
+
return errors;
|
|
98
|
+
}
|
|
99
|
+
// Handle const constraint
|
|
100
|
+
if ('const' in schema) {
|
|
101
|
+
if (actual !== schema.const) {
|
|
102
|
+
errors.push({ field: path || 'root', expected: `const: ${schema.const}`, actual });
|
|
103
|
+
}
|
|
104
|
+
return errors;
|
|
105
|
+
}
|
|
106
|
+
// Handle type constraint
|
|
107
|
+
if ('type' in schema) {
|
|
108
|
+
const schemaType = schema.type;
|
|
109
|
+
const actualType = Array.isArray(actual) ? 'array' : typeof actual;
|
|
110
|
+
if (schemaType === 'array' && !Array.isArray(actual)) {
|
|
111
|
+
errors.push({ field: path || 'root', expected: 'array', actual: actualType });
|
|
112
|
+
return errors;
|
|
113
|
+
}
|
|
114
|
+
if (schemaType === 'object' && (typeof actual !== 'object' || Array.isArray(actual))) {
|
|
115
|
+
errors.push({ field: path || 'root', expected: 'object', actual: actualType });
|
|
116
|
+
return errors;
|
|
117
|
+
}
|
|
118
|
+
if (schemaType === 'number' && typeof actual !== 'number') {
|
|
119
|
+
errors.push({ field: path || 'root', expected: 'number', actual: actualType });
|
|
120
|
+
return errors;
|
|
121
|
+
}
|
|
122
|
+
if (schemaType === 'string' && typeof actual !== 'string') {
|
|
123
|
+
errors.push({ field: path || 'root', expected: 'string', actual: actualType });
|
|
124
|
+
return errors;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Handle number constraints
|
|
128
|
+
if (typeof actual === 'number') {
|
|
129
|
+
if ('minimum' in schema && actual < schema.minimum) {
|
|
130
|
+
errors.push({ field: path, expected: `>= ${schema.minimum}`, actual });
|
|
131
|
+
}
|
|
132
|
+
if ('maximum' in schema && actual > schema.maximum) {
|
|
133
|
+
errors.push({ field: path, expected: `<= ${schema.maximum}`, actual });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Handle string constraints
|
|
137
|
+
if (typeof actual === 'string') {
|
|
138
|
+
if ('minLength' in schema && actual.length < schema.minLength) {
|
|
139
|
+
errors.push({ field: path, expected: `minLength: ${schema.minLength}`, actual: `length: ${actual.length}` });
|
|
140
|
+
}
|
|
141
|
+
if ('maxLength' in schema && actual.length > schema.maxLength) {
|
|
142
|
+
errors.push({ field: path, expected: `maxLength: ${schema.maxLength}`, actual: `length: ${actual.length}` });
|
|
143
|
+
}
|
|
144
|
+
if ('enum' in schema && !(schema.enum.includes(actual))) {
|
|
145
|
+
errors.push({ field: path, expected: `enum: ${JSON.stringify(schema.enum)}`, actual });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Handle array constraints
|
|
149
|
+
if (Array.isArray(actual)) {
|
|
150
|
+
if ('minItems' in schema && actual.length < schema.minItems) {
|
|
151
|
+
errors.push({ field: path, expected: `minItems: ${schema.minItems}`, actual: `length: ${actual.length}` });
|
|
152
|
+
}
|
|
153
|
+
if ('items' in schema) {
|
|
154
|
+
const itemSchema = schema.items;
|
|
155
|
+
for (let i = 0; i < actual.length; i++) {
|
|
156
|
+
errors.push(...validateAgainstSchema(itemSchema, actual[i], `${path}[${i}]`));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Handle object constraints
|
|
161
|
+
if (typeof actual === 'object' && !Array.isArray(actual) && actual !== null) {
|
|
162
|
+
const actualObj = actual;
|
|
163
|
+
// Check required fields
|
|
164
|
+
if ('required' in schema) {
|
|
165
|
+
const required = schema.required;
|
|
166
|
+
for (const field of required) {
|
|
167
|
+
if (!(field in actualObj)) {
|
|
168
|
+
errors.push({ field: `${path}.${field}`, expected: 'required field', actual: 'missing' });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Validate properties
|
|
173
|
+
if ('properties' in schema) {
|
|
174
|
+
const properties = schema.properties;
|
|
175
|
+
for (const [key, propSchema] of Object.entries(properties)) {
|
|
176
|
+
if (key in actualObj) {
|
|
177
|
+
errors.push(...validateAgainstSchema(propSchema, actualObj[key], path ? `${path}.${key}` : key));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return errors;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Compare two values and return differences
|
|
186
|
+
*/
|
|
187
|
+
function deepCompare(expected, actual, path = '') {
|
|
188
|
+
const diffs = [];
|
|
189
|
+
// Handle null/undefined
|
|
190
|
+
if (expected === null || expected === undefined) {
|
|
191
|
+
if (actual !== expected) {
|
|
192
|
+
diffs.push({ field: path || 'root', expected, actual });
|
|
193
|
+
}
|
|
194
|
+
return diffs;
|
|
195
|
+
}
|
|
196
|
+
// Type mismatch
|
|
197
|
+
if (typeof expected !== typeof actual) {
|
|
198
|
+
diffs.push({ field: path || 'root', expected, actual });
|
|
199
|
+
return diffs;
|
|
200
|
+
}
|
|
201
|
+
// Arrays
|
|
202
|
+
if (Array.isArray(expected)) {
|
|
203
|
+
if (!Array.isArray(actual)) {
|
|
204
|
+
diffs.push({ field: path || 'root', expected, actual });
|
|
205
|
+
return diffs;
|
|
206
|
+
}
|
|
207
|
+
if (expected.length !== actual.length) {
|
|
208
|
+
diffs.push({ field: `${path}.length`, expected: expected.length, actual: actual.length });
|
|
209
|
+
}
|
|
210
|
+
const maxLen = Math.max(expected.length, actual.length);
|
|
211
|
+
for (let i = 0; i < maxLen; i++) {
|
|
212
|
+
diffs.push(...deepCompare(expected[i], actual[i], `${path}[${i}]`));
|
|
213
|
+
}
|
|
214
|
+
return diffs;
|
|
215
|
+
}
|
|
216
|
+
// Objects
|
|
217
|
+
if (typeof expected === 'object') {
|
|
218
|
+
if (typeof actual !== 'object' || actual === null) {
|
|
219
|
+
diffs.push({ field: path || 'root', expected, actual });
|
|
220
|
+
return diffs;
|
|
221
|
+
}
|
|
222
|
+
const expectedObj = expected;
|
|
223
|
+
const actualObj = actual;
|
|
224
|
+
const allKeys = new Set([...Object.keys(expectedObj), ...Object.keys(actualObj)]);
|
|
225
|
+
for (const key of allKeys) {
|
|
226
|
+
// Skip dynamic fields that change between runs
|
|
227
|
+
if (['trace_id', 'latency_ms', 'model', 'timestamp', 'version'].includes(key)) {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const fieldPath = path ? `${path}.${key}` : key;
|
|
231
|
+
if (!(key in expectedObj)) {
|
|
232
|
+
// Extra field in actual - only report if it's significant
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (!(key in actualObj)) {
|
|
236
|
+
diffs.push({ field: fieldPath, expected: expectedObj[key], actual: undefined });
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
diffs.push(...deepCompare(expectedObj[key], actualObj[key], fieldPath));
|
|
240
|
+
}
|
|
241
|
+
return diffs;
|
|
242
|
+
}
|
|
243
|
+
// Primitives
|
|
244
|
+
if (expected !== actual) {
|
|
245
|
+
// For confidence, allow small variations
|
|
246
|
+
if (path.endsWith('.confidence') && typeof expected === 'number' && typeof actual === 'number') {
|
|
247
|
+
if (Math.abs(expected - actual) <= 0.1) {
|
|
248
|
+
return diffs; // Close enough
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
diffs.push({ field: path || 'root', expected, actual });
|
|
252
|
+
}
|
|
253
|
+
return diffs;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Run a single test case
|
|
257
|
+
*/
|
|
258
|
+
async function runTestCase(testCase, module, ctx, options = {}) {
|
|
259
|
+
const startTime = Date.now();
|
|
260
|
+
const { timeout = 60000 } = options;
|
|
261
|
+
try {
|
|
262
|
+
// Load input
|
|
263
|
+
if (!fs.existsSync(testCase.inputPath)) {
|
|
264
|
+
return {
|
|
265
|
+
name: testCase.name,
|
|
266
|
+
passed: false,
|
|
267
|
+
duration_ms: Date.now() - startTime,
|
|
268
|
+
error: `Input file not found: ${testCase.inputPath}`,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
const inputContent = fs.readFileSync(testCase.inputPath, 'utf-8');
|
|
272
|
+
const input = JSON.parse(inputContent);
|
|
273
|
+
// Load expected (if exists)
|
|
274
|
+
let expected = null;
|
|
275
|
+
if (fs.existsSync(testCase.expectedPath)) {
|
|
276
|
+
const expectedContent = fs.readFileSync(testCase.expectedPath, 'utf-8');
|
|
277
|
+
expected = JSON.parse(expectedContent);
|
|
278
|
+
}
|
|
279
|
+
// Run module with timeout
|
|
280
|
+
const runPromise = runModule(module, ctx.provider, {
|
|
281
|
+
input,
|
|
282
|
+
validateInput: true,
|
|
283
|
+
validateOutput: true,
|
|
284
|
+
useV22: true,
|
|
285
|
+
});
|
|
286
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
287
|
+
setTimeout(() => reject(new Error(`Test timeout after ${timeout}ms`)), timeout);
|
|
288
|
+
});
|
|
289
|
+
const result = await Promise.race([runPromise, timeoutPromise]);
|
|
290
|
+
// Update mode - write actual output as expected
|
|
291
|
+
if (options.update) {
|
|
292
|
+
fs.writeFileSync(testCase.expectedPath, JSON.stringify(result, null, 2));
|
|
293
|
+
return {
|
|
294
|
+
name: testCase.name,
|
|
295
|
+
passed: true,
|
|
296
|
+
duration_ms: Date.now() - startTime,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
// No expected file - skip comparison
|
|
300
|
+
if (expected === null) {
|
|
301
|
+
return {
|
|
302
|
+
name: testCase.name,
|
|
303
|
+
passed: false,
|
|
304
|
+
duration_ms: Date.now() - startTime,
|
|
305
|
+
error: `Expected file not found: ${testCase.expectedPath}`,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
// Compare results - use schema validation or direct comparison
|
|
309
|
+
let diffs;
|
|
310
|
+
if (isSchemaValidationFormat(expected)) {
|
|
311
|
+
// Schema validation mode
|
|
312
|
+
diffs = validateAgainstSchema(expected.$validate, result);
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
// Direct comparison mode
|
|
316
|
+
diffs = deepCompare(expected, result);
|
|
317
|
+
}
|
|
318
|
+
if (diffs.length === 0) {
|
|
319
|
+
return {
|
|
320
|
+
name: testCase.name,
|
|
321
|
+
passed: true,
|
|
322
|
+
duration_ms: Date.now() - startTime,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
return {
|
|
327
|
+
name: testCase.name,
|
|
328
|
+
passed: false,
|
|
329
|
+
duration_ms: Date.now() - startTime,
|
|
330
|
+
diff: diffs.slice(0, 10), // Limit to first 10 diffs
|
|
331
|
+
error: `${diffs.length} difference(s) found`,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
return {
|
|
337
|
+
name: testCase.name,
|
|
338
|
+
passed: false,
|
|
339
|
+
duration_ms: Date.now() - startTime,
|
|
340
|
+
error: error instanceof Error ? error.message : String(error),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
// =============================================================================
|
|
345
|
+
// Commands
|
|
346
|
+
// =============================================================================
|
|
347
|
+
/**
|
|
348
|
+
* Run tests for a single module
|
|
349
|
+
*/
|
|
350
|
+
export async function test(moduleName, ctx, options = {}) {
|
|
351
|
+
const startTime = Date.now();
|
|
352
|
+
const searchPaths = getDefaultSearchPaths(ctx.cwd);
|
|
353
|
+
// Find module
|
|
354
|
+
const module = await findModule(moduleName, searchPaths);
|
|
355
|
+
if (!module) {
|
|
356
|
+
return {
|
|
357
|
+
success: false,
|
|
358
|
+
error: `Module '${moduleName}' not found`,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
// Get test cases from module.yaml or auto-discover
|
|
362
|
+
let testCases = parseTestDefinitions(module, module.location);
|
|
363
|
+
if (testCases.length === 0) {
|
|
364
|
+
testCases = discoverTestCases(module.location);
|
|
365
|
+
}
|
|
366
|
+
if (testCases.length === 0) {
|
|
367
|
+
return {
|
|
368
|
+
success: true,
|
|
369
|
+
data: {
|
|
370
|
+
moduleName: module.name,
|
|
371
|
+
modulePath: module.location,
|
|
372
|
+
total: 0,
|
|
373
|
+
passed: 0,
|
|
374
|
+
failed: 0,
|
|
375
|
+
skipped: 0,
|
|
376
|
+
duration_ms: Date.now() - startTime,
|
|
377
|
+
results: [],
|
|
378
|
+
},
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
// Filter tests if specified
|
|
382
|
+
if (options.filter) {
|
|
383
|
+
testCases = testCases.filter(tc => tc.name.includes(options.filter));
|
|
384
|
+
}
|
|
385
|
+
// Run tests
|
|
386
|
+
const results = [];
|
|
387
|
+
let passed = 0;
|
|
388
|
+
let failed = 0;
|
|
389
|
+
for (const testCase of testCases) {
|
|
390
|
+
if (options.verbose) {
|
|
391
|
+
console.error(` Running: ${testCase.name}...`);
|
|
392
|
+
}
|
|
393
|
+
const result = await runTestCase(testCase, module, ctx, options);
|
|
394
|
+
results.push(result);
|
|
395
|
+
if (result.passed) {
|
|
396
|
+
passed++;
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
failed++;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const testResult = {
|
|
403
|
+
moduleName: module.name,
|
|
404
|
+
modulePath: module.location,
|
|
405
|
+
total: testCases.length,
|
|
406
|
+
passed,
|
|
407
|
+
failed,
|
|
408
|
+
skipped: 0,
|
|
409
|
+
duration_ms: Date.now() - startTime,
|
|
410
|
+
results,
|
|
411
|
+
};
|
|
412
|
+
return {
|
|
413
|
+
success: failed === 0,
|
|
414
|
+
data: testResult,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Run tests for all modules
|
|
419
|
+
*/
|
|
420
|
+
export async function testAll(ctx, options = {}) {
|
|
421
|
+
const startTime = Date.now();
|
|
422
|
+
const searchPaths = getDefaultSearchPaths(ctx.cwd);
|
|
423
|
+
// List all modules
|
|
424
|
+
const modules = await listModules(searchPaths);
|
|
425
|
+
const results = [];
|
|
426
|
+
let totalTests = 0;
|
|
427
|
+
let totalPassed = 0;
|
|
428
|
+
let totalFailed = 0;
|
|
429
|
+
let modulesWithTests = 0;
|
|
430
|
+
for (const moduleInfo of modules) {
|
|
431
|
+
const result = await test(moduleInfo.name, ctx, options);
|
|
432
|
+
if (result.success && result.data) {
|
|
433
|
+
const moduleResult = result.data;
|
|
434
|
+
results.push(moduleResult);
|
|
435
|
+
if (moduleResult.total > 0) {
|
|
436
|
+
modulesWithTests++;
|
|
437
|
+
totalTests += moduleResult.total;
|
|
438
|
+
totalPassed += moduleResult.passed;
|
|
439
|
+
totalFailed += moduleResult.failed;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return {
|
|
444
|
+
success: totalFailed === 0,
|
|
445
|
+
data: {
|
|
446
|
+
total: totalTests,
|
|
447
|
+
passed: totalPassed,
|
|
448
|
+
failed: totalFailed,
|
|
449
|
+
skipped: modules.length - modulesWithTests,
|
|
450
|
+
duration_ms: Date.now() - startTime,
|
|
451
|
+
modules: results,
|
|
452
|
+
},
|
|
453
|
+
};
|
|
454
|
+
}
|
package/dist/commands/update.js
CHANGED
|
@@ -6,15 +6,38 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { existsSync } from 'node:fs';
|
|
8
8
|
import { readFile } from 'node:fs/promises';
|
|
9
|
-
import { join } from 'node:path';
|
|
9
|
+
import { join, resolve, sep, isAbsolute } from 'node:path';
|
|
10
10
|
import { homedir } from 'node:os';
|
|
11
|
-
import { add, getInstallInfo } from './add.js';
|
|
11
|
+
import { add, addFromRegistry, getInstallInfo } from './add.js';
|
|
12
|
+
import { RegistryClient } from '../registry/client.js';
|
|
12
13
|
const USER_MODULES_DIR = join(homedir(), '.cognitive', 'modules');
|
|
14
|
+
function assertSafeModuleName(name) {
|
|
15
|
+
const trimmed = name.trim();
|
|
16
|
+
if (!trimmed) {
|
|
17
|
+
throw new Error('Invalid module name: empty');
|
|
18
|
+
}
|
|
19
|
+
if (trimmed.includes('..') || trimmed.includes('/') || trimmed.includes('\\')) {
|
|
20
|
+
throw new Error(`Invalid module name: ${name}`);
|
|
21
|
+
}
|
|
22
|
+
if (isAbsolute(trimmed)) {
|
|
23
|
+
throw new Error(`Invalid module name (absolute path not allowed): ${name}`);
|
|
24
|
+
}
|
|
25
|
+
return trimmed;
|
|
26
|
+
}
|
|
27
|
+
function resolveModuleTarget(moduleName) {
|
|
28
|
+
const safeName = assertSafeModuleName(moduleName);
|
|
29
|
+
const targetPath = resolve(USER_MODULES_DIR, safeName);
|
|
30
|
+
const root = resolve(USER_MODULES_DIR) + sep;
|
|
31
|
+
if (!targetPath.startsWith(root)) {
|
|
32
|
+
throw new Error(`Invalid module name (path traversal): ${moduleName}`);
|
|
33
|
+
}
|
|
34
|
+
return targetPath;
|
|
35
|
+
}
|
|
13
36
|
/**
|
|
14
37
|
* Get module version from installed module
|
|
15
38
|
*/
|
|
16
39
|
async function getInstalledVersion(moduleName) {
|
|
17
|
-
const modulePath =
|
|
40
|
+
const modulePath = resolveModuleTarget(moduleName);
|
|
18
41
|
if (!existsSync(modulePath)) {
|
|
19
42
|
return undefined;
|
|
20
43
|
}
|
|
@@ -46,29 +69,97 @@ async function getInstalledVersion(moduleName) {
|
|
|
46
69
|
* Update an installed module
|
|
47
70
|
*/
|
|
48
71
|
export async function update(moduleName, ctx, options = {}) {
|
|
72
|
+
let safeName;
|
|
73
|
+
try {
|
|
74
|
+
safeName = assertSafeModuleName(moduleName);
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
return {
|
|
78
|
+
success: false,
|
|
79
|
+
error: e instanceof Error ? e.message : String(e),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
49
82
|
// Get installation info
|
|
50
|
-
const info = await getInstallInfo(
|
|
83
|
+
const info = await getInstallInfo(safeName);
|
|
51
84
|
if (!info) {
|
|
52
85
|
return {
|
|
53
86
|
success: false,
|
|
54
|
-
error: `Module not found or not installed
|
|
87
|
+
error: `Module not found or not installed with 'cog add': ${safeName}. Only modules installed with 'cog add' can be updated.`,
|
|
55
88
|
};
|
|
56
89
|
}
|
|
90
|
+
// Get current version
|
|
91
|
+
const oldVersion = await getInstalledVersion(safeName);
|
|
92
|
+
// Check if module was installed from registry
|
|
93
|
+
if (info.registryModule) {
|
|
94
|
+
const registryModule = info.registryModule;
|
|
95
|
+
// Use undefined instead of empty string for default registry URL
|
|
96
|
+
const registryUrl = info.registryUrl || undefined;
|
|
97
|
+
// Check registry for latest version
|
|
98
|
+
const client = new RegistryClient(registryUrl);
|
|
99
|
+
const registryInfo = await client.getModule(registryModule);
|
|
100
|
+
if (!registryInfo) {
|
|
101
|
+
return {
|
|
102
|
+
success: false,
|
|
103
|
+
error: `Module '${registryModule}' is no longer available in the registry.`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
// Warn if deprecated
|
|
107
|
+
if (registryInfo.deprecated) {
|
|
108
|
+
console.warn(`Warning: Module '${registryModule}' is deprecated.`);
|
|
109
|
+
}
|
|
110
|
+
const targetVersion = options.tag || registryInfo.version;
|
|
111
|
+
// Re-install from registry with optional version
|
|
112
|
+
const moduleSpec = options.tag ? `${registryModule}@${options.tag}` : registryModule;
|
|
113
|
+
const result = await addFromRegistry(moduleSpec, ctx, {
|
|
114
|
+
name: safeName,
|
|
115
|
+
registry: registryUrl,
|
|
116
|
+
});
|
|
117
|
+
if (!result.success) {
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
const data = result.data;
|
|
121
|
+
const newVersion = data.version || targetVersion;
|
|
122
|
+
// Determine message
|
|
123
|
+
let message;
|
|
124
|
+
if (oldVersion && newVersion) {
|
|
125
|
+
if (oldVersion === newVersion) {
|
|
126
|
+
message = `Already up to date: ${safeName} v${newVersion}`;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
message = `Updated: ${safeName} v${oldVersion} → v${newVersion}`;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else if (newVersion) {
|
|
133
|
+
message = `Updated: ${safeName} to v${newVersion}`;
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
message = `Updated: ${safeName}`;
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
success: true,
|
|
140
|
+
data: {
|
|
141
|
+
message,
|
|
142
|
+
name: safeName,
|
|
143
|
+
oldVersion,
|
|
144
|
+
newVersion,
|
|
145
|
+
source: 'registry',
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
// GitHub-based update
|
|
57
150
|
if (!info.githubUrl) {
|
|
58
151
|
return {
|
|
59
152
|
success: false,
|
|
60
|
-
error: `Module was not installed from GitHub: ${moduleName}`,
|
|
153
|
+
error: `Module was not installed from GitHub or registry: ${moduleName}`,
|
|
61
154
|
};
|
|
62
155
|
}
|
|
63
|
-
// Get current version
|
|
64
|
-
const oldVersion = await getInstalledVersion(moduleName);
|
|
65
156
|
// Determine what ref to use
|
|
66
157
|
const tag = options.tag || info.tag;
|
|
67
158
|
const branch = info.branch || 'main';
|
|
68
159
|
// Re-install from source
|
|
69
160
|
const result = await add(info.githubUrl, ctx, {
|
|
70
161
|
module: info.modulePath,
|
|
71
|
-
name:
|
|
162
|
+
name: safeName,
|
|
72
163
|
tag,
|
|
73
164
|
branch: tag ? undefined : branch,
|
|
74
165
|
});
|
|
@@ -81,25 +172,26 @@ export async function update(moduleName, ctx, options = {}) {
|
|
|
81
172
|
let message;
|
|
82
173
|
if (oldVersion && newVersion) {
|
|
83
174
|
if (oldVersion === newVersion) {
|
|
84
|
-
message = `Already up to date: ${
|
|
175
|
+
message = `Already up to date: ${safeName} v${newVersion}`;
|
|
85
176
|
}
|
|
86
177
|
else {
|
|
87
|
-
message = `Updated: ${
|
|
178
|
+
message = `Updated: ${safeName} v${oldVersion} → v${newVersion}`;
|
|
88
179
|
}
|
|
89
180
|
}
|
|
90
181
|
else if (newVersion) {
|
|
91
|
-
message = `Updated: ${
|
|
182
|
+
message = `Updated: ${safeName} to v${newVersion}`;
|
|
92
183
|
}
|
|
93
184
|
else {
|
|
94
|
-
message = `Updated: ${
|
|
185
|
+
message = `Updated: ${safeName}`;
|
|
95
186
|
}
|
|
96
187
|
return {
|
|
97
188
|
success: true,
|
|
98
189
|
data: {
|
|
99
190
|
message,
|
|
100
|
-
name:
|
|
191
|
+
name: safeName,
|
|
101
192
|
oldVersion,
|
|
102
193
|
newVersion,
|
|
194
|
+
source: 'github',
|
|
103
195
|
},
|
|
104
196
|
};
|
|
105
197
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cog validate - Validate a Cognitive Module's structure and examples
|
|
3
|
+
*
|
|
4
|
+
* Aligns with Python CLI's `cog validate` command.
|
|
5
|
+
*/
|
|
6
|
+
import type { CommandContext, CommandResult } from '../types.js';
|
|
7
|
+
export interface ValidateOptions {
|
|
8
|
+
/** Enable strict v2.2 validation */
|
|
9
|
+
v22?: boolean;
|
|
10
|
+
/** Output format: 'text' or 'json' */
|
|
11
|
+
format?: 'text' | 'json';
|
|
12
|
+
}
|
|
13
|
+
export interface ValidateResult {
|
|
14
|
+
valid: boolean;
|
|
15
|
+
modulePath: string;
|
|
16
|
+
moduleName?: string;
|
|
17
|
+
errors: string[];
|
|
18
|
+
warnings: string[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Validate a cognitive module's structure and examples.
|
|
22
|
+
*
|
|
23
|
+
* @param nameOrPath Module name or path
|
|
24
|
+
* @param ctx Command context
|
|
25
|
+
* @param options Validation options
|
|
26
|
+
* @returns Validation result
|
|
27
|
+
*/
|
|
28
|
+
export declare function validate(nameOrPath: string, ctx: CommandContext, options?: ValidateOptions): Promise<CommandResult>;
|
|
29
|
+
/**
|
|
30
|
+
* Validate all modules in the search paths.
|
|
31
|
+
*
|
|
32
|
+
* @param ctx Command context
|
|
33
|
+
* @param options Validation options
|
|
34
|
+
* @returns Batch validation results
|
|
35
|
+
*/
|
|
36
|
+
export declare function validateAll(ctx: CommandContext, options?: ValidateOptions): Promise<CommandResult>;
|