strray-ai 1.22.18 → 1.22.20

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/CHANGELOG.md CHANGED
@@ -4,6 +4,24 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Conventional Commits](https://www.conventionalcommits.org/).
6
6
 
7
+ ## [1.22.19] - 2026-04-27
8
+
9
+ ### 🔄 Changes
10
+
11
+ - Version bump
12
+
13
+ ---
14
+
15
+ ## [1.22.18] - 2026-04-27
16
+
17
+ ### 🔄 Changes
18
+
19
+ ### 🐛 Bug Fixes
20
+ - fix: add validate script and hooks to dist/scripts, include mcps registry in build (fc059458e)
21
+ - fix: add mcps directory to build, fix MCP registry path resolution (c17ca67e9)
22
+
23
+ ---
24
+
7
25
  ## [1.22.18] - 2026-04-27
8
26
 
9
27
  ### 🔄 Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strray-ai",
3
- "version": "1.22.18",
3
+ "version": "1.22.20",
4
4
  "description": "⚡ 0xRay: Self-Healing AI Governance OS - Enterprise AI orchestration for OpenCode and Hermes",
5
5
  "readme": "README.md",
6
6
  "license": "MIT",
@@ -0,0 +1,413 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * StringRay CJS/MJS Scripts Loadability Tests
5
+ *
6
+ * Tests that all .cjs and .mjs scripts that ship with the package
7
+ * can be loaded without errors, and checks for expected exports.
8
+ *
9
+ * CJS scripts that are self-invoking are tested via subprocess to
10
+ * avoid side effects. Library-style CJS scripts are tested with
11
+ * require() directly.
12
+ *
13
+ * Usage: node scripts/test/test-cjs-mjs-scripts.cjs
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const { spawn } = require('child_process');
19
+
20
+ const ROOT = path.join(__dirname, '..', '..');
21
+
22
+ let passed = 0;
23
+ let failed = 0;
24
+ let skipped = 0;
25
+
26
+ function pass(name) {
27
+ passed++;
28
+ console.log(` PASS: ${name}`);
29
+ }
30
+
31
+ function fail(name, reason) {
32
+ failed++;
33
+ console.log(` FAIL: ${name} — ${reason}`);
34
+ }
35
+
36
+ function skip(name, reason) {
37
+ skipped++;
38
+ console.log(` SKIP: ${name} — ${reason}`);
39
+ }
40
+
41
+ function checkSyntax(filePath) {
42
+ return new Promise((resolve) => {
43
+ const child = spawn('node', ['--check', filePath], { stdio: 'pipe' });
44
+ let stderr = '';
45
+ child.stderr.on('data', (data) => { stderr += data.toString(); });
46
+ child.on('close', (code) => {
47
+ resolve({ valid: code === 0, error: stderr });
48
+ });
49
+ child.on('error', (err) => {
50
+ resolve({ valid: false, error: err.message });
51
+ });
52
+ });
53
+ }
54
+
55
+ function runSubprocess(filePath, timeout = 15000) {
56
+ return new Promise((resolve) => {
57
+ const child = spawn('node', [filePath], {
58
+ stdio: 'pipe',
59
+ cwd: ROOT,
60
+ timeout,
61
+ });
62
+
63
+ let stderr = '';
64
+ child.stderr.on('data', (data) => { stderr += data.toString(); });
65
+
66
+ const timer = setTimeout(() => {
67
+ child.kill('SIGTERM');
68
+ }, timeout);
69
+
70
+ child.on('close', (code) => {
71
+ clearTimeout(timer);
72
+ resolve({ code, stderr });
73
+ });
74
+
75
+ child.on('error', (err) => {
76
+ clearTimeout(timer);
77
+ resolve({ code: -1, stderr: err.message });
78
+ });
79
+ });
80
+ }
81
+
82
+ const cjsScripts = [
83
+ {
84
+ path: path.join(ROOT, 'scripts', 'node', 'postinstall.cjs'),
85
+ name: 'scripts/node/postinstall.cjs',
86
+ expectedExports: [],
87
+ description: 'Post-installation setup script',
88
+ selfInvoking: true,
89
+ },
90
+ {
91
+ path: path.join(ROOT, 'scripts', 'node', 'setup-dev.cjs'),
92
+ name: 'scripts/node/setup-dev.cjs',
93
+ expectedExports: [],
94
+ description: 'Development environment setup script',
95
+ selfInvoking: true,
96
+ },
97
+ {
98
+ path: path.join(ROOT, 'scripts', 'node', 'prepare-consumer.cjs'),
99
+ name: 'scripts/node/prepare-consumer.cjs',
100
+ expectedExports: [],
101
+ description: 'Consumer preparation script',
102
+ selfInvoking: true,
103
+ },
104
+ {
105
+ path: path.join(ROOT, 'scripts', 'node', 'reflection-processor.cjs'),
106
+ name: 'scripts/node/reflection-processor.cjs',
107
+ expectedExports: [],
108
+ description: 'Reflection post-processor',
109
+ selfInvoking: true,
110
+ },
111
+ {
112
+ path: path.join(ROOT, 'scripts', 'node', 'basic-security-audit.cjs'),
113
+ name: 'scripts/node/basic-security-audit.cjs',
114
+ expectedExports: [],
115
+ description: 'Basic security audit script',
116
+ selfInvoking: true,
117
+ },
118
+ {
119
+ path: path.join(ROOT, 'scripts', 'node', 'ci-cd-auto-fix.cjs'),
120
+ name: 'scripts/node/ci-cd-auto-fix.cjs',
121
+ expectedExports: [],
122
+ description: 'CI/CD auto-fix script',
123
+ selfInvoking: true,
124
+ },
125
+ {
126
+ path: path.join(ROOT, 'scripts', 'helpers', 'resolve-config-path.cjs'),
127
+ name: 'scripts/helpers/resolve-config-path.cjs',
128
+ expectedExports: ['getConfigDir', 'resolveConfigPath'],
129
+ description: 'Config path resolver (CJS)',
130
+ selfInvoking: false,
131
+ },
132
+ ];
133
+
134
+ const mjsScripts = [
135
+ {
136
+ path: path.join(ROOT, 'scripts', 'node', 'enforce-agents-md.mjs'),
137
+ name: 'scripts/node/enforce-agents-md.mjs',
138
+ description: 'AGENTS.md enforcement script',
139
+ selfInvoking: false,
140
+ },
141
+ {
142
+ path: path.join(ROOT, 'scripts', 'node', 'version-manager.mjs'),
143
+ name: 'scripts/node/version-manager.mjs',
144
+ description: 'Version manager script',
145
+ selfInvoking: true,
146
+ },
147
+ {
148
+ path: path.join(ROOT, 'scripts', 'node', 'release.mjs'),
149
+ name: 'scripts/node/release.mjs',
150
+ description: 'Release script',
151
+ selfInvoking: true,
152
+ },
153
+ {
154
+ path: path.join(ROOT, 'scripts', 'node', 'auto-reflection-generator.mjs'),
155
+ name: 'scripts/node/auto-reflection-generator.mjs',
156
+ description: 'Auto-reflection generator',
157
+ selfInvoking: true,
158
+ },
159
+ {
160
+ path: path.join(ROOT, 'scripts', 'node', 'ci-report-generator.mjs'),
161
+ name: 'scripts/node/ci-report-generator.mjs',
162
+ description: 'CI report generator',
163
+ selfInvoking: true,
164
+ },
165
+ {
166
+ path: path.join(ROOT, 'scripts', 'mjs', 'validate-postinstall-config.mjs'),
167
+ name: 'scripts/mjs/validate-postinstall-config.mjs',
168
+ description: 'Post-install config validator',
169
+ selfInvoking: true,
170
+ },
171
+ ];
172
+
173
+ async function runTests() {
174
+ console.log('=== CJS/MJS Scripts Loadability Tests ===\n');
175
+
176
+ // ── CJS Scripts ──────────────────────────────────────────────
177
+ console.log('--- CJS Scripts ---\n');
178
+
179
+ for (const script of cjsScripts) {
180
+ console.log(`Testing: ${script.name} (${script.description})`);
181
+
182
+ if (!fs.existsSync(script.path)) {
183
+ fail(`${script.name} exists`, `File not found: ${script.path}`);
184
+ console.log('');
185
+ continue;
186
+ }
187
+ pass(`${script.name} exists`);
188
+
189
+ // Syntax check
190
+ const syntaxResult = await checkSyntax(script.path);
191
+ if (syntaxResult.valid) {
192
+ pass(`${script.name} has valid JS syntax`);
193
+ } else {
194
+ fail(`${script.name} has valid JS syntax`, syntaxResult.error.trim());
195
+ console.log('');
196
+ continue;
197
+ }
198
+
199
+ if (script.selfInvoking) {
200
+ // Self-invoking scripts: run in subprocess with timeout
201
+ // Exit code 0 = ran without crash, anything else = error
202
+ const runResult = await runSubprocess(script.path, 10000);
203
+ if (runResult.code === 0 || runResult.code === null) {
204
+ // code 0 = success; null can happen if timeout killed it (still loadable)
205
+ pass(`${script.name} runs without fatal errors`);
206
+ } else {
207
+ // Non-zero exit might be expected (e.g., security audit returns 1 on findings)
208
+ // Only fail if there's an uncaught exception or module load error
209
+ const hasLoadError = runResult.stderr && (
210
+ runResult.stderr.includes('Cannot find module') ||
211
+ runResult.stderr.includes('SyntaxError') ||
212
+ runResult.stderr.includes('MODULE_NOT_FOUND')
213
+ );
214
+ if (hasLoadError) {
215
+ fail(`${script.name} runs without fatal errors`, `Load error detected in stderr`);
216
+ } else {
217
+ pass(`${script.name} runs without fatal module errors (exit: ${runResult.code})`);
218
+ }
219
+ }
220
+ } else {
221
+ // Library-style CJS: require() and check exports
222
+ try {
223
+ const mod = require(script.path);
224
+ pass(`${script.name} can be required without errors`);
225
+
226
+ if (script.expectedExports.length > 0) {
227
+ const exportedKeys = Object.keys(mod || {});
228
+ for (const expectedExport of script.expectedExports) {
229
+ if (mod && mod[expectedExport]) {
230
+ pass(`${script.name} exports "${expectedExport}"`);
231
+ } else {
232
+ fail(`${script.name} exports "${expectedExport}"`, `Export not found. Available: [${exportedKeys.join(', ')}]`);
233
+ }
234
+ }
235
+ }
236
+ } catch (err) {
237
+ if (err.code === 'ERR_REQUIRE_ESM' || (err.message && err.message.includes('ES Module'))) {
238
+ skip(`${script.name} require (ESM module)`, err.message);
239
+ } else if (err.message && (err.message.includes('Cannot find module') || err.code === 'MODULE_NOT_FOUND')) {
240
+ const missingMatch = err.message.match(/Cannot find module '([^']+)'/);
241
+ const missingModule = missingMatch ? missingMatch[1] : 'unknown';
242
+ skip(`${script.name} require`, `Missing dependency: ${missingModule}`);
243
+ } else {
244
+ fail(`${script.name} can be required without errors`, err.message);
245
+ }
246
+ }
247
+ }
248
+
249
+ console.log('');
250
+ }
251
+
252
+ // ── MJS Scripts ──────────────────────────────────────────────
253
+ console.log('--- MJS Scripts ---\n');
254
+
255
+ for (const script of mjsScripts) {
256
+ console.log(`Testing: ${script.name} (${script.description})`);
257
+
258
+ if (!fs.existsSync(script.path)) {
259
+ fail(`${script.name} exists`, `File not found: ${script.path}`);
260
+ console.log('');
261
+ continue;
262
+ }
263
+ pass(`${script.name} exists`);
264
+
265
+ // Syntax check (works for .mjs files too)
266
+ const syntaxResult = await checkSyntax(script.path);
267
+ if (syntaxResult.valid) {
268
+ pass(`${script.name} has valid JS syntax`);
269
+ } else {
270
+ fail(`${script.name} has valid JS syntax`, syntaxResult.error.trim());
271
+ console.log('');
272
+ continue;
273
+ }
274
+
275
+ // Attempt dynamic import or subprocess test
276
+ if (script.selfInvoking) {
277
+ // Self-invoking MJS scripts: run in subprocess with timeout
278
+ const runResult = await runSubprocess(script.path, 10000);
279
+ if (runResult.code === 0 || runResult.code === null) {
280
+ pass(`${script.name} runs without fatal errors`);
281
+ } else {
282
+ const hasLoadError = runResult.stderr && (
283
+ runResult.stderr.includes('Cannot find module') ||
284
+ runResult.stderr.includes('SyntaxError') ||
285
+ runResult.stderr.includes('MODULE_NOT_FOUND')
286
+ );
287
+ if (hasLoadError) {
288
+ fail(`${script.name} runs without fatal errors`, 'Load error detected in stderr');
289
+ } else {
290
+ // Self-invoking scripts often exit with non-zero for expected reasons
291
+ pass(`${script.name} runs without fatal module errors (exit: ${runResult.code})`);
292
+ }
293
+ }
294
+ } else {
295
+ // Library-style MJS: test with dynamic import
296
+ const importResult = await new Promise((resolve) => {
297
+ const importCheckScript = `
298
+ import('file://${script.path.replace(/\\/g, '/')}')
299
+ .then(mod => {
300
+ const exports = Object.keys(mod);
301
+ console.log(JSON.stringify({ success: true, exports: exports }));
302
+ process.exit(0);
303
+ })
304
+ .catch(err => {
305
+ const safeErr = { message: err.message, code: err.code || null };
306
+ console.log(JSON.stringify({ success: false, error: safeErr }));
307
+ process.exit(0);
308
+ });
309
+ `;
310
+ const child = spawn('node', ['--input-type=module', '-e', importCheckScript], {
311
+ stdio: 'pipe',
312
+ cwd: ROOT,
313
+ });
314
+
315
+ let stdout = '';
316
+ let stderr = '';
317
+ child.stdout.on('data', (data) => { stdout += data.toString(); });
318
+ child.stderr.on('data', (data) => { stderr += data.toString(); });
319
+
320
+ const timer = setTimeout(() => {
321
+ child.kill('SIGTERM');
322
+ }, 15000);
323
+
324
+ child.on('close', (code) => {
325
+ clearTimeout(timer);
326
+ try {
327
+ const lines = stdout.trim().split('\n');
328
+ const jsonLine = lines.find(l => {
329
+ try { JSON.parse(l); return true; } catch { return false; }
330
+ });
331
+ if (jsonLine) {
332
+ resolve(JSON.parse(jsonLine));
333
+ } else {
334
+ resolve({
335
+ success: false,
336
+ error: { message: `No JSON output found. stdout: ${stdout.slice(0, 300)}, stderr: ${stderr.slice(0, 200)}` },
337
+ });
338
+ }
339
+ } catch {
340
+ resolve({
341
+ success: false,
342
+ error: { message: `Parse error. stdout: ${stdout.slice(0, 200)}, stderr: ${stderr.slice(0, 200)}` },
343
+ });
344
+ }
345
+ });
346
+
347
+ child.on('error', (err) => {
348
+ clearTimeout(timer);
349
+ resolve({ success: false, error: { message: err.message } });
350
+ });
351
+ });
352
+
353
+ if (importResult.success) {
354
+ pass(`${script.name} can be imported without errors`);
355
+ if (importResult.exports && importResult.exports.length > 0) {
356
+ pass(`${script.name} has exports: [${importResult.exports.slice(0, 5).join(', ')}${importResult.exports.length > 5 ? '...' : ''}]`);
357
+ } else {
358
+ pass(`${script.name} loaded (no named exports — likely side-effect script)`);
359
+ }
360
+ } else {
361
+ const errMsg = importResult.error?.message || 'Unknown error';
362
+ const errCode = importResult.error?.code;
363
+
364
+ if (errCode === 'ERR_MODULE_NOT_FOUND' || errMsg.includes('Cannot find module') || errMsg.includes('MODULE_NOT_FOUND')) {
365
+ const missingMatch = errMsg.match(/Cannot find module '([^']+)'/) || errMsg.match(/Cannot find package '([^']+)'/);
366
+ const missingModule = missingMatch ? missingMatch[1] : 'unknown';
367
+ skip(`${script.name} dynamic import`, `Missing dependency: ${missingModule}`);
368
+ } else if (errMsg.includes('ECONNREFUSED') || errMsg.includes('fetch failed') || errMsg.includes('network')) {
369
+ skip(`${script.name} dynamic import`, 'Network dependency unavailable');
370
+ } else {
371
+ fail(`${script.name} can be imported without errors`, errMsg);
372
+ }
373
+ }
374
+ }
375
+
376
+ console.log('');
377
+ }
378
+
379
+ // ── Helper script: resolve-config-path.mjs ────────────────────
380
+ console.log('--- Helper Scripts (additional) ---\n');
381
+
382
+ const helperMjs = path.join(ROOT, 'scripts', 'helpers', 'resolve-config-path.mjs');
383
+ console.log('Testing: scripts/helpers/resolve-config-path.mjs');
384
+
385
+ if (!fs.existsSync(helperMjs)) {
386
+ skip('scripts/helpers/resolve-config-path.mjs exists', 'File not found');
387
+ } else {
388
+ pass('scripts/helpers/resolve-config-path.mjs exists');
389
+
390
+ const syntaxResult = await checkSyntax(helperMjs);
391
+ if (syntaxResult.valid) {
392
+ pass('scripts/helpers/resolve-config-path.mjs has valid JS syntax');
393
+ } else {
394
+ fail('scripts/helpers/resolve-config-path.mjs has valid JS syntax', syntaxResult.error.trim());
395
+ }
396
+ }
397
+
398
+ console.log('');
399
+
400
+ // ── Summary ──────────────────────────────────────────────────
401
+ console.log('=== Summary ===');
402
+ console.log(` Passed: ${passed}`);
403
+ console.log(` Failed: ${failed}`);
404
+ console.log(` Skipped: ${skipped}`);
405
+ console.log(` Total: ${passed + failed + skipped}`);
406
+
407
+ process.exit(failed > 0 ? 1 : 0);
408
+ }
409
+
410
+ runTests().catch((err) => {
411
+ console.error('Test runner failed:', err);
412
+ process.exit(2);
413
+ });
@@ -0,0 +1,229 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * StringRay Hermes Agent MCP Plugin Integration Tests
5
+ *
6
+ * Tests that the Hermes integration can be loaded, instantiated,
7
+ * and exports the correct MCP tools and bridge configuration.
8
+ *
9
+ * Usage: node scripts/test/test-hermes-mcp-integration.cjs
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const { spawn } = require('child_process');
15
+
16
+ const ROOT = path.join(__dirname, '..', '..');
17
+ const DIST_HERMES = path.join(ROOT, 'dist', 'integrations', 'hermes-agent');
18
+
19
+ let passed = 0;
20
+ let failed = 0;
21
+ let skipped = 0;
22
+
23
+ function pass(name) {
24
+ passed++;
25
+ console.log(` PASS: ${name}`);
26
+ }
27
+
28
+ function fail(name, reason) {
29
+ failed++;
30
+ console.log(` FAIL: ${name} — ${reason}`);
31
+ }
32
+
33
+ function skip(name, reason) {
34
+ skipped++;
35
+ console.log(` SKIP: ${name} — ${reason}`);
36
+ }
37
+
38
+ async function checkSyntax(filePath) {
39
+ return new Promise((resolve) => {
40
+ const child = spawn('node', ['--check', filePath], { stdio: 'pipe' });
41
+ let stderr = '';
42
+ child.stderr.on('data', (data) => { stderr += data.toString(); });
43
+ child.on('close', (code) => {
44
+ resolve({ valid: code === 0, error: stderr });
45
+ });
46
+ child.on('error', (err) => {
47
+ resolve({ valid: false, error: err.message });
48
+ });
49
+ });
50
+ }
51
+
52
+ async function runTests() {
53
+ console.log('=== Hermes Agent MCP Plugin Integration Tests ===\n');
54
+
55
+ // Test 1: bridge.mjs can be loaded (syntax check)
56
+ console.log('Test 1: bridge.mjs can be loaded');
57
+ const bridgePath = path.join(DIST_HERMES, 'bridge.mjs');
58
+ if (!fs.existsSync(bridgePath)) {
59
+ fail('bridge.mjs exists', `File not found: ${bridgePath}`);
60
+ } else {
61
+ const result = await checkSyntax(bridgePath);
62
+ if (result.valid) {
63
+ pass('bridge.mjs has valid JS syntax');
64
+ } else {
65
+ fail('bridge.mjs has valid JS syntax', result.error.trim());
66
+ }
67
+ }
68
+
69
+ // Test 2: plugin.yaml has valid structure
70
+ console.log('\nTest 2: plugin.yaml has valid structure');
71
+ const pluginYamlPath = path.join(DIST_HERMES, 'plugin.yaml');
72
+ if (!fs.existsSync(pluginYamlPath)) {
73
+ fail('plugin.yaml exists', `File not found: ${pluginYamlPath}`);
74
+ } else {
75
+ try {
76
+ const content = fs.readFileSync(pluginYamlPath, 'utf-8');
77
+
78
+ const requiredFields = ['name', 'version', 'description'];
79
+ for (const field of requiredFields) {
80
+ if (content.includes(`${field}:`)) {
81
+ pass(`plugin.yaml has "${field}" field`);
82
+ } else {
83
+ fail(`plugin.yaml has "${field}" field`, `Missing "${field}:" key`);
84
+ }
85
+ }
86
+
87
+ if (content.includes('provides_tools:')) {
88
+ pass('plugin.yaml has "provides_tools" field');
89
+ } else {
90
+ fail('plugin.yaml has "provides_tools" field', 'Missing "provides_tools:" key');
91
+ }
92
+
93
+ if (content.includes('provides_hooks:')) {
94
+ pass('plugin.yaml has "provides_hooks" field');
95
+ } else {
96
+ fail('plugin.yaml has "provides_hooks" field', 'Missing "provides_hooks:" key');
97
+ }
98
+ } catch (err) {
99
+ fail('plugin.yaml is readable', err.message);
100
+ }
101
+ }
102
+
103
+ // Test 3: plugin.yaml lists correct MCP tool names
104
+ console.log('\nTest 3: plugin.yaml lists correct MCP tool names');
105
+ if (fs.existsSync(pluginYamlPath)) {
106
+ try {
107
+ const content = fs.readFileSync(pluginYamlPath, 'utf-8');
108
+ const expectedTools = ['strray_validate', 'strray_codex_check', 'strray_health', 'strray_hooks'];
109
+ for (const tool of expectedTools) {
110
+ if (content.includes(tool)) {
111
+ pass(`plugin.yaml lists tool "${tool}"`);
112
+ } else {
113
+ fail(`plugin.yaml lists tool "${tool}"`, `Tool not found in plugin.yaml`);
114
+ }
115
+ }
116
+ } catch (err) {
117
+ fail('plugin.yaml tools check', err.message);
118
+ }
119
+ }
120
+
121
+ // Test 4: hermes-agent-integration.js can be instantiated (if it exists in dist)
122
+ console.log('\nTest 4: HermesAgentIntegration class can be instantiated');
123
+ const integrationJsPath = path.join(DIST_HERMES, 'hermes-agent-integration.js');
124
+ const integrationSrcPath = path.join(ROOT, 'src', 'integrations', 'hermes-agent', 'hermes-agent-integration.ts');
125
+
126
+ if (fs.existsSync(integrationJsPath)) {
127
+ try {
128
+ const mod = require(integrationJsPath);
129
+ if (mod.HermesAgentIntegration) {
130
+ const instance = new mod.HermesAgentIntegration();
131
+ if (instance) {
132
+ pass('HermesAgentIntegration can be instantiated');
133
+ } else {
134
+ fail('HermesAgentIntegration can be instantiated', 'Constructor returned null/undefined');
135
+ }
136
+ } else {
137
+ fail('HermesAgentIntegration is exported', 'Module does not export HermesAgentIntegration');
138
+ }
139
+ } catch (err) {
140
+ if (err.code === 'ERR_REQUIRE_ESM' || err.message?.includes('ES Module')) {
141
+ skip('HermesAgentIntegration instantiation (ESM module - needs dynamic import)', err.message);
142
+ } else {
143
+ fail('HermesAgentIntegration can be instantiated', err.message);
144
+ }
145
+ }
146
+ } else if (fs.existsSync(integrationSrcPath)) {
147
+ skip('HermesAgentIntegration class instantiation', 'Compiled JS not in dist yet; source TS exists');
148
+ } else {
149
+ skip('HermesAgentIntegration class instantiation', 'Integration class not built to dist (Python/MJS bridge)');
150
+ }
151
+
152
+ // Test 5: Python modules exist and are valid (syntax check via python3)
153
+ console.log('\nTest 5: Python modules exist and are valid');
154
+ const pythonFiles = ['__init__.py', 'tools.py', 'schemas.py', 'conftest.py'];
155
+ for (const pyFile of pythonFiles) {
156
+ const pyPath = path.join(DIST_HERMES, pyFile);
157
+ if (!fs.existsSync(pyPath)) {
158
+ skip(`Python module ${pyFile} syntax check`, 'File not found');
159
+ continue;
160
+ }
161
+
162
+ const hasPython = await new Promise((resolve) => {
163
+ const child = spawn('python3', ['--version'], { stdio: 'pipe' });
164
+ child.on('close', (code) => resolve(code === 0));
165
+ child.on('error', () => resolve(false));
166
+ });
167
+
168
+ if (!hasPython) {
169
+ skip(`Python module ${pyFile} syntax check`, 'python3 not available');
170
+ break;
171
+ }
172
+
173
+ const result = await new Promise((resolve) => {
174
+ const child = spawn('python3', ['-m', 'py_compile', pyPath], { stdio: 'pipe' });
175
+ let stderr = '';
176
+ child.stderr.on('data', (data) => { stderr += data.toString(); });
177
+ child.on('close', (code) => resolve({ valid: code === 0, error: stderr }));
178
+ child.on('error', (err) => resolve({ valid: false, error: err.message }));
179
+ });
180
+
181
+ if (result.valid) {
182
+ pass(`Python module ${pyFile} has valid syntax`);
183
+ } else {
184
+ fail(`Python module ${pyFile} has valid syntax`, result.error.trim());
185
+ }
186
+ }
187
+
188
+ // Test 6: after-install.md exists and is non-empty
189
+ console.log('\nTest 6: after-install.md exists and is non-empty');
190
+ const afterInstallPath = path.join(DIST_HERMES, 'after-install.md');
191
+ if (!fs.existsSync(afterInstallPath)) {
192
+ fail('after-install.md exists', `File not found: ${afterInstallPath}`);
193
+ } else {
194
+ const content = fs.readFileSync(afterInstallPath, 'utf-8');
195
+ if (content.trim().length > 0) {
196
+ pass('after-install.md is non-empty');
197
+ } else {
198
+ fail('after-install.md is non-empty', 'File is empty');
199
+ }
200
+ }
201
+
202
+ // Test 7: bridge.mjs exports expected command handlers
203
+ console.log('\nTest 7: bridge.mjs contains expected command handlers');
204
+ if (fs.existsSync(bridgePath)) {
205
+ const bridgeContent = fs.readFileSync(bridgePath, 'utf-8');
206
+ const expectedCommands = ['health', 'pre-process', 'post-process', 'validate', 'codex-check', 'hooks'];
207
+ for (const cmd of expectedCommands) {
208
+ if (bridgeContent.includes(`"${cmd}"`) || bridgeContent.includes(`'${cmd}'`)) {
209
+ pass(`bridge.mjs handles "${cmd}" command`);
210
+ } else {
211
+ fail(`bridge.mjs handles "${cmd}" command`, `Command not found in bridge.mjs`);
212
+ }
213
+ }
214
+ }
215
+
216
+ // Summary
217
+ console.log('\n=== Summary ===');
218
+ console.log(` Passed: ${passed}`);
219
+ console.log(` Failed: ${failed}`);
220
+ console.log(` Skipped: ${skipped}`);
221
+ console.log(` Total: ${passed + failed + skipped}`);
222
+
223
+ process.exit(failed > 0 ? 1 : 0);
224
+ }
225
+
226
+ runTests().catch((err) => {
227
+ console.error('Test runner failed:', err);
228
+ process.exit(2);
229
+ });
@@ -0,0 +1,240 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * StringRay OpenClaw Integration Tests
5
+ *
6
+ * Tests that the OpenClaw integration can be loaded, instantiated,
7
+ * and exports the expected types, client, and hooks.
8
+ *
9
+ * Usage: node scripts/test/test-openclaw-integration.cjs
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const { spawn } = require('child_process');
15
+
16
+ const ROOT = path.join(__dirname, '..', '..');
17
+ const DIST_OPENCLAW = path.join(ROOT, 'dist', 'integrations', 'openclaw');
18
+ const SRC_OPENCLAW = path.join(ROOT, 'src', 'integrations', 'openclaw');
19
+
20
+ let passed = 0;
21
+ let failed = 0;
22
+ let skipped = 0;
23
+
24
+ function pass(name) {
25
+ passed++;
26
+ console.log(` PASS: ${name}`);
27
+ }
28
+
29
+ function fail(name, reason) {
30
+ failed++;
31
+ console.log(` FAIL: ${name} — ${reason}`);
32
+ }
33
+
34
+ function skip(name, reason) {
35
+ skipped++;
36
+ console.log(` SKIP: ${name} — ${reason}`);
37
+ }
38
+
39
+ async function checkSyntax(filePath) {
40
+ return new Promise((resolve) => {
41
+ const child = spawn('node', ['--check', filePath], { stdio: 'pipe' });
42
+ let stderr = '';
43
+ child.stderr.on('data', (data) => { stderr += data.toString(); });
44
+ child.on('close', (code) => {
45
+ resolve({ valid: code === 0, error: stderr });
46
+ });
47
+ child.on('error', (err) => {
48
+ resolve({ valid: false, error: err.message });
49
+ });
50
+ });
51
+ }
52
+
53
+ async function runTests() {
54
+ console.log('=== OpenClaw Integration Tests ===\n');
55
+
56
+ // Test 1: OpenClaw integration source exists
57
+ console.log('Test 1: OpenClaw integration source exists');
58
+ const srcIndex = path.join(SRC_OPENCLAW, 'index.ts');
59
+ if (fs.existsSync(srcIndex)) {
60
+ pass('OpenClaw source index.ts exists');
61
+ } else {
62
+ fail('OpenClaw source index.ts exists', `Not found: ${srcIndex}`);
63
+ }
64
+
65
+ // Test 2: OpenClawIntegration class can be instantiated (via dist or source check)
66
+ console.log('\nTest 2: OpenClawIntegration class can be instantiated');
67
+ const distIndex = path.join(DIST_OPENCLAW, 'index.js');
68
+
69
+ if (fs.existsSync(distIndex)) {
70
+ try {
71
+ const mod = require(distIndex);
72
+ if (mod.OpenClawIntegration) {
73
+ const instance = new mod.OpenClawIntegration();
74
+ if (instance) {
75
+ pass('OpenClawIntegration can be instantiated from dist');
76
+ } else {
77
+ fail('OpenClawIntegration can be instantiated from dist', 'Constructor returned null/undefined');
78
+ }
79
+ } else {
80
+ // Check if module loaded but doesn't export the class directly
81
+ const exportKeys = Object.keys(mod);
82
+ if (exportKeys.length > 0) {
83
+ pass(`OpenClawIntegration dist module loaded (exports: ${exportKeys.slice(0, 5).join(', ')})`);
84
+ } else {
85
+ fail('OpenClawIntegration is exported from dist', 'Module has no exports');
86
+ }
87
+ }
88
+ } catch (err) {
89
+ if (err.code === 'ERR_REQUIRE_ESM' || err.message?.includes('ES Module')) {
90
+ skip('OpenClawIntegration instantiation from dist', 'ESM module requires dynamic import');
91
+ } else {
92
+ fail('OpenClawIntegration instantiation from dist', err.message);
93
+ }
94
+ }
95
+ } else {
96
+ // Check source file exists with the class definition
97
+ if (fs.existsSync(srcIndex)) {
98
+ const content = fs.readFileSync(srcIndex, 'utf-8');
99
+ if (content.includes('class OpenClawIntegration')) {
100
+ skip('OpenClawIntegration class instantiation', 'Compiled dist not found; class exists in source');
101
+ } else {
102
+ fail('OpenClawIntegration class exists in source', 'Class definition not found in index.ts');
103
+ }
104
+ } else {
105
+ fail('OpenClawIntegration class', 'Neither dist nor source found');
106
+ }
107
+ }
108
+
109
+ // Test 3: Types module exports expected interfaces
110
+ console.log('\nTest 3: Types module exports expected interfaces');
111
+ const srcTypes = path.join(SRC_OPENCLAW, 'types.ts');
112
+ if (fs.existsSync(srcTypes)) {
113
+ const content = fs.readFileSync(srcTypes, 'utf-8');
114
+ const expectedTypes = ['OpenClawIntegrationConfig', 'IntegrationStatistics', 'ClientStatistics'];
115
+
116
+ for (const typeName of expectedTypes) {
117
+ if (content.includes(typeName)) {
118
+ pass(`Types module exports "${typeName}"`);
119
+ } else {
120
+ fail(`Types module exports "${typeName}"`, `Interface not found in types.ts`);
121
+ }
122
+ }
123
+ } else {
124
+ // Check dist
125
+ const distTypes = path.join(DIST_OPENCLAW, 'types.js');
126
+ if (fs.existsSync(distTypes)) {
127
+ skip('Types module check', 'Only dist JS available; TypeScript interfaces are type-only');
128
+ } else {
129
+ fail('Types module exists', 'Neither source nor dist types found');
130
+ }
131
+ }
132
+
133
+ // Test 4: Client module exists
134
+ console.log('\nTest 4: Client module exists');
135
+ const srcClient = path.join(SRC_OPENCLAW, 'client.ts');
136
+ const distClient = path.join(DIST_OPENCLAW, 'client.js');
137
+
138
+ if (fs.existsSync(srcClient)) {
139
+ const content = fs.readFileSync(srcClient, 'utf-8');
140
+ if (content.includes('class OpenClawClient') || content.includes('OpenClawClient')) {
141
+ pass('Client module (source) defines OpenClawClient');
142
+ } else {
143
+ fail('Client module defines OpenClawClient', 'Class not found in client.ts');
144
+ }
145
+ } else if (fs.existsSync(distClient)) {
146
+ const result = await checkSyntax(distClient);
147
+ if (result.valid) {
148
+ pass('Client module (dist) has valid JS syntax');
149
+ } else {
150
+ fail('Client module (dist) has valid JS syntax', result.error.trim());
151
+ }
152
+ } else {
153
+ fail('Client module exists', 'Neither source nor dist client found');
154
+ }
155
+
156
+ // Test 5: API server module exists
157
+ console.log('\nTest 5: API server module exists');
158
+ const srcApiServer = path.join(SRC_OPENCLAW, 'api-server.ts');
159
+ const distApiServer = path.join(DIST_OPENCLAW, 'api-server.js');
160
+
161
+ if (fs.existsSync(srcApiServer)) {
162
+ const content = fs.readFileSync(srcApiServer, 'utf-8');
163
+ if (content.includes('StringRayAPIServer') || content.includes('class APIServer')) {
164
+ pass('API server module (source) defines StringRayAPIServer');
165
+ } else {
166
+ fail('API server module defines StringRayAPIServer', 'Class not found in api-server.ts');
167
+ }
168
+ } else if (fs.existsSync(distApiServer)) {
169
+ const result = await checkSyntax(distApiServer);
170
+ if (result.valid) {
171
+ pass('API server module (dist) has valid JS syntax');
172
+ } else {
173
+ fail('API server module (dist) has valid JS syntax', result.error.trim());
174
+ }
175
+ } else {
176
+ // Check if it's referenced in the README
177
+ const readmePath = path.join(DIST_OPENCLAW, 'README.md');
178
+ if (fs.existsSync(readmePath)) {
179
+ const readme = fs.readFileSync(readmePath, 'utf-8');
180
+ if (readme.includes('api-server') || readme.includes('API Server')) {
181
+ skip('API server module', 'Source not compiled to dist; documented in README');
182
+ } else {
183
+ fail('API server module exists', 'Neither source nor dist found');
184
+ }
185
+ } else {
186
+ fail('API server module exists', 'No source, dist, or documentation found');
187
+ }
188
+ }
189
+
190
+ // Test 6: Hooks (strray-hooks) can be loaded
191
+ console.log('\nTest 6: Hooks (strray-hooks) can be loaded');
192
+ const srcHooks = path.join(SRC_OPENCLAW, 'hooks', 'strray-hooks.ts');
193
+ const distHooks = path.join(DIST_OPENCLAW, 'hooks', 'strray-hooks.js');
194
+
195
+ if (fs.existsSync(srcHooks)) {
196
+ const content = fs.readFileSync(srcHooks, 'utf-8');
197
+ if (content.includes('OpenClawHooksManager')) {
198
+ pass('Hooks module (source) defines OpenClawHooksManager');
199
+ } else {
200
+ fail('Hooks module defines OpenClawHooksManager', 'Class not found in strray-hooks.ts');
201
+ }
202
+ } else if (fs.existsSync(distHooks)) {
203
+ const result = await checkSyntax(distHooks);
204
+ if (result.valid) {
205
+ pass('Hooks module (dist) has valid JS syntax');
206
+ } else {
207
+ fail('Hooks module (dist) has valid JS syntax', result.error.trim());
208
+ }
209
+ } else {
210
+ skip('Hooks module (strray-hooks)', 'Not compiled to dist; source module not found');
211
+ }
212
+
213
+ // Test 7: Config module exists
214
+ console.log('\nTest 7: Config module exists');
215
+ const srcConfig = path.join(SRC_OPENCLAW, 'config.ts');
216
+ if (fs.existsSync(srcConfig)) {
217
+ const content = fs.readFileSync(srcConfig, 'utf-8');
218
+ if (content.includes('OpenClawConfigLoader')) {
219
+ pass('Config module (source) defines OpenClawConfigLoader');
220
+ } else {
221
+ fail('Config module defines OpenClawConfigLoader', 'Class not found in config.ts');
222
+ }
223
+ } else {
224
+ skip('Config module', 'Source not found; may not be compiled');
225
+ }
226
+
227
+ // Summary
228
+ console.log('\n=== Summary ===');
229
+ console.log(` Passed: ${passed}`);
230
+ console.log(` Failed: ${failed}`);
231
+ console.log(` Skipped: ${skipped}`);
232
+ console.log(` Total: ${passed + failed + skipped}`);
233
+
234
+ process.exit(failed > 0 ? 1 : 0);
235
+ }
236
+
237
+ runTests().catch((err) => {
238
+ console.error('Test runner failed:', err);
239
+ process.exit(2);
240
+ });