Files
ECC/tests/qoder-plugin.test.js
T

127 lines
4.5 KiB
JavaScript

/**
* Qoder native plugin contract tests.
*
* These tests intentionally execute the declared hook entry point instead of
* only inspecting JSON. Qoder launches hooks without a shell when `command`
* and `args` are separate, which keeps the same contract on Windows, macOS,
* and Linux.
*/
'use strict';
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
const repoRoot = path.resolve(__dirname, '..');
const manifestPath = path.join(repoRoot, '.qoder-plugin', 'plugin.json');
const hooksPath = path.join(repoRoot, 'hooks', 'qoder-hooks.json');
const packageJson = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
const releaseScript = fs.readFileSync(path.join(repoRoot, 'scripts', 'release.sh'), 'utf8');
let passed = 0;
let failed = 0;
function test(name, fn) {
try {
fn();
console.log(` ✓ ${name}`);
passed += 1;
} catch (error) {
console.log(` ✗ ${name}`);
console.log(` Error: ${error.message}`);
failed += 1;
}
}
console.log('\n=== Qoder native plugin ===\n');
test('manifest and hook projection exist', () => {
assert.ok(fs.existsSync(manifestPath), 'Expected .qoder-plugin/plugin.json');
assert.ok(fs.existsSync(hooksPath), 'Expected hooks/qoder-hooks.json');
});
if (fs.existsSync(manifestPath) && fs.existsSync(hooksPath)) {
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
const hooks = JSON.parse(fs.readFileSync(hooksPath, 'utf8'));
test('manifest stays versioned with the npm package', () => {
assert.strictEqual(manifest.name, 'ecc');
assert.strictEqual(manifest.version, packageJson.version);
});
test('release tooling updates and stages the Qoder manifest', () => {
assert.ok(releaseScript.includes('QODER_PLUGIN_JSON=".qoder-plugin/plugin.json"'));
assert.ok(releaseScript.includes('update_version "$QODER_PLUGIN_JSON"'));
assert.ok(releaseScript.includes('git add ') && releaseScript.includes('"$QODER_PLUGIN_JSON"'));
});
test('manifest reuses canonical plugin resources', () => {
assert.strictEqual(manifest.skills, './skills/');
assert.strictEqual(manifest.commands, './commands/');
assert.strictEqual(manifest.mcpServers, './.mcp.json');
assert.strictEqual(manifest.hooks, './hooks/qoder-hooks.json');
});
test('agent colors are accepted by Qoder', () => {
const allowedColors = new Set([
'red', 'blue', 'green', 'yellow', 'purple', 'orange', 'pink', 'cyan',
]);
for (const fileName of fs.readdirSync(path.join(repoRoot, 'agents'))) {
if (!fileName.endsWith('.md')) continue;
const source = fs.readFileSync(path.join(repoRoot, 'agents', fileName), 'utf8');
const match = source.match(/^color:\s*(\S+)$/m);
if (match) {
assert.ok(allowedColors.has(match[1]), `${fileName} uses unsupported Qoder color ${match[1]}`);
}
}
});
test('Qoder hook is a minimal SessionStart projection using exec form', () => {
assert.deepStrictEqual(Object.keys(hooks.hooks), ['SessionStart']);
const registrations = hooks.hooks.SessionStart;
assert.strictEqual(registrations.length, 1);
assert.strictEqual(registrations[0].matcher, 'startup|resume|clear|compact|new');
assert.strictEqual(registrations[0].hooks.length, 1);
const hook = registrations[0].hooks[0];
assert.strictEqual(hook.type, 'command');
assert.strictEqual(hook.command, 'node');
assert.ok(Array.isArray(hook.args));
assert.strictEqual(hook.args[0], '${QODER_PLUGIN_ROOT}/scripts/hooks/plugin-hook-bootstrap.js');
assert.deepStrictEqual(hook.args.slice(1), [
'node',
'scripts/hooks/session-start-bootstrap.js',
]);
});
test('declared Qoder SessionStart launcher executes from QODER_PLUGIN_ROOT', () => {
const hook = hooks.hooks.SessionStart[0].hooks[0];
const args = hook.args.map(value => value.replace('${QODER_PLUGIN_ROOT}', repoRoot));
const result = spawnSync(hook.command, args, {
cwd: repoRoot,
env: {
...process.env,
QODER_PLUGIN_ROOT: repoRoot,
ECC_HOOKS_ENABLED: 'false',
},
input: JSON.stringify({
hook_event_name: 'SessionStart',
source: 'startup',
cwd: repoRoot,
}),
encoding: 'utf8',
timeout: 10000,
windowsHide: true,
});
assert.ifError(result.error);
assert.strictEqual(result.signal, null);
assert.strictEqual(result.status, 0, result.stderr);
});
}
console.log(`\nPassed: ${passed}`);
console.log(`Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);