Files
Elydora-Node-SDK/test/kimi-plugin.test.mjs
T

460 lines
16 KiB
JavaScript

import assert from 'node:assert/strict';
import { spawn } from 'node:child_process';
import { chmod, mkdtemp, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { pathToFileURL } from 'node:url';
import { parse as parseToml } from '@decimalturn/toml-patch';
const pluginModuleUrl = pathToFileURL(path.resolve('dist/plugins/kimi.js')).href;
const registryModuleUrl = pathToFileURL(path.resolve('dist/plugins/registry.js')).href;
const cliPath = path.resolve('dist/cli.js');
function runNode(args, env, input = '', unset = []) {
return new Promise((resolve, reject) => {
const childEnv = { ...process.env, ...env };
for (const key of unset) delete childEnv[key];
const child = spawn(process.execPath, args, {
env: childEnv,
stdio: ['pipe', 'pipe', 'pipe'],
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (chunk) => { stdout += chunk; });
child.stderr.on('data', (chunk) => { stderr += chunk; });
child.once('error', reject);
child.once('close', (code) => resolve({ code, stdout, stderr }));
child.stdin.end(input);
});
}
function runCommand(command, input = '') {
return new Promise((resolve, reject) => {
const child = spawn(command, {
shell: true,
stdio: ['pipe', 'pipe', 'pipe'],
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (chunk) => { stdout += chunk; });
child.stderr.on('data', (chunk) => { stderr += chunk; });
child.once('error', reject);
child.once('close', (code) => resolve({ code, stdout, stderr }));
child.stdin.end(input);
});
}
async function runPlugin(fixture, method, argument) {
const script = `
import { kimiPlugin } from ${JSON.stringify(pluginModuleUrl)};
const argument = JSON.parse(process.env.ELYDORA_TEST_ARGUMENT);
const result = await kimiPlugin[process.env.ELYDORA_TEST_METHOD](argument);
if (result !== undefined) console.log(JSON.stringify(result));
`;
const env = {
HOME: fixture.homeDir,
USERPROFILE: fixture.homeDir,
PATH: fixture.binDir,
ELYDORA_TEST_ARGUMENT: JSON.stringify(argument),
ELYDORA_TEST_METHOD: method,
};
if (fixture.explicitKimiHome) env.KIMI_CODE_HOME = fixture.kimiHome;
return runNode(
['--input-type=module', '--eval', script],
env,
'',
fixture.explicitKimiHome ? [] : ['KIMI_CODE_HOME'],
);
}
async function writeOptional(filePath, contents) {
if (contents === undefined) return;
await mkdir(path.dirname(filePath), { recursive: true });
await writeFile(filePath, contents, 'utf-8');
}
async function createFixture({
guardSource,
hookSource,
modernConfig,
legacyConfig,
legacyInstalled = true,
explicitKimiHome = true,
} = {}) {
const homeDir = await mkdtemp(path.join(os.tmpdir(), 'elydora-kimi-'));
const binDir = path.join(homeDir, 'bin');
const kimiHome = explicitKimiHome
? path.join(homeDir, 'custom-kimi-code')
: path.join(homeDir, '.kimi-code');
const modernPath = path.join(kimiHome, 'config.toml');
const legacyPath = path.join(homeDir, '.kimi', 'config.toml');
const agentDir = path.join(homeDir, '.elydora', 'agent-1');
const guardScriptPath = path.join(agentDir, 'guard.js');
const hookScriptPath = path.join(agentDir, 'hook.js');
await mkdir(agentDir, { recursive: true });
await mkdir(binDir, { recursive: true });
if (legacyInstalled) {
const executable = path.join(binDir, process.platform === 'win32' ? 'kimi-cli.cmd' : 'kimi-cli');
await writeFile(executable, '');
if (process.platform !== 'win32') await chmod(executable, 0o700);
}
await writeFile(
guardScriptPath,
guardSource ?? "process.stderr.write('Agent is frozen by Elydora.'); process.exit(2);\n",
);
await writeFile(hookScriptPath, hookSource ?? 'process.exit(0);\n');
await writeFile(path.join(agentDir, 'config.json'), JSON.stringify({
agent_id: 'agent-1',
agent_name: 'kimi',
}));
await writeOptional(modernPath, modernConfig);
await writeOptional(legacyPath, legacyConfig);
const fixture = {
agentDir,
binDir,
guardScriptPath,
homeDir,
hookScriptPath,
kimiHome,
explicitKimiHome,
legacyPath,
modernPath,
async close() {
await rm(homeDir, { recursive: true, force: true });
},
};
fixture.installResult = await runPlugin(fixture, 'install', {
agentName: 'kimi',
agentId: 'agent-1',
baseUrl: 'https://api.elydora.com',
guardScriptPath,
hookScriptPath,
});
return fixture;
}
function managedHook(config, event, scriptName) {
return config.hooks.find(
(hook) => hook.event === event && hook.command.includes(scriptName),
);
}
function assertStrictHook(hook) {
assert.deepEqual(Object.keys(hook).sort(), ['command', 'event', 'timeout']);
assert.equal(hook.timeout, 10);
}
test('Kimi is registered in the SDK and CLI', async () => {
const { SUPPORTED_AGENTS } = await import(registryModuleUrl);
assert.deepEqual(SUPPORTED_AGENTS.get('kimi'), {
name: 'Kimi Code',
configDir: '~/.kimi-code',
configFile: 'config.toml',
});
const homeDir = await mkdtemp(path.join(os.tmpdir(), 'elydora-kimi-cli-'));
try {
const result = await runNode(['--no-warnings', cliPath, 'status'], {
HOME: homeDir,
USERPROFILE: homeDir,
KIMI_CODE_HOME: path.join(homeDir, '.kimi-code'),
});
assert.equal(result.code, 0, result.stderr);
assert.match(result.stdout, /Kimi Code \(kimi\)/);
} finally {
await rm(homeDir, { recursive: true, force: true });
}
});
test('Kimi install preserves both user configs and is idempotent', async () => {
const modernConfig = [
'# modern user config',
'default_model = "kimi-code/k3"',
'',
'[[hooks]]',
'event = "SessionStart"',
'command = "existing-modern"',
'timeout = 30 # keep modern hook',
'',
].join('\n');
const legacyConfig = [
'# legacy user config',
'telemetry = false',
'',
'[[hooks]]',
'event = "SessionEnd"',
'command = "existing-legacy"',
'',
].join('\n');
const fixture = await createFixture({ modernConfig, legacyConfig });
try {
assert.equal(fixture.installResult.code, 0, fixture.installResult.stderr);
assert.match(fixture.installResult.stdout, /Kimi Code and kimi-cli/i);
const secondInstall = await runPlugin(fixture, 'install', {
agentName: 'kimi',
agentId: 'agent-1',
baseUrl: 'https://api.elydora.com',
guardScriptPath: fixture.guardScriptPath,
hookScriptPath: fixture.hookScriptPath,
});
assert.equal(secondInstall.code, 0, secondInstall.stderr);
for (const [configPath, comment, existingCommand] of [
[fixture.modernPath, '# modern user config', 'existing-modern'],
[fixture.legacyPath, '# legacy user config', 'existing-legacy'],
]) {
const raw = await readFile(configPath, 'utf-8');
const config = parseToml(raw);
assert.match(raw, new RegExp(comment));
assert(config.hooks.some((hook) => hook.command === existingCommand));
assert.equal(config.hooks.length, 3);
assertStrictHook(managedHook(config, 'PreToolUse', 'guard.js'));
assertStrictHook(managedHook(config, 'PostToolUse', 'hook.js'));
}
await assert.rejects(
readFile(path.join(fixture.homeDir, '.kimi-code', 'config.toml')),
{ code: 'ENOENT' },
);
} finally {
await fixture.close();
}
});
test('Kimi Code install does not create a false legacy migration source', async () => {
const fixture = await createFixture({ legacyInstalled: false });
try {
assert.equal(fixture.installResult.code, 0, fixture.installResult.stderr);
assert.equal(parseToml(await readFile(fixture.modernPath, 'utf-8')).hooks.length, 2);
await assert.rejects(readFile(fixture.legacyPath), { code: 'ENOENT' });
} finally {
await fixture.close();
}
});
test('Kimi Code treats an empty home override as the official default', async () => {
const fixture = await createFixture({ explicitKimiHome: false, legacyInstalled: false });
try {
assert.equal(fixture.installResult.code, 0, fixture.installResult.stderr);
fixture.explicitKimiHome = true;
fixture.kimiHome = '';
const status = await runPlugin(fixture, 'status', null);
assert.equal(status.code, 0, status.stderr);
assert.equal(JSON.parse(status.stdout).installed, true);
} finally {
await fixture.close();
}
});
test('Legacy kimi-cli install does not create a premature migration target', async () => {
const fixture = await createFixture({ explicitKimiHome: false });
try {
assert.equal(fixture.installResult.code, 0, fixture.installResult.stderr);
assert.equal(parseToml(await readFile(fixture.legacyPath, 'utf-8')).hooks.length, 2);
await assert.rejects(readFile(fixture.modernPath), { code: 'ENOENT' });
await rm(fixture.binDir, { recursive: true });
const status = await runPlugin(fixture, 'status', null);
assert.equal(status.code, 0, status.stderr);
assert.equal(JSON.parse(status.stdout).installed, true);
} finally {
await fixture.close();
}
});
test('Kimi commands block freezes and forward the official payload unchanged', async () => {
const capturePath = path.join(os.tmpdir(), `elydora-kimi-event-${process.pid}-${Date.now()}.json`);
const hookSource = `
const fs = require('node:fs');
const chunks = [];
process.stdin.on('data', (chunk) => chunks.push(chunk));
process.stdin.on('end', () => fs.writeFileSync(${JSON.stringify(capturePath)}, Buffer.concat(chunks)));
`;
const fixture = await createFixture({ hookSource });
try {
assert.equal(fixture.installResult.code, 0, fixture.installResult.stderr);
const config = parseToml(await readFile(fixture.modernPath, 'utf-8'));
const guard = managedHook(config, 'PreToolUse', 'guard.js');
const audit = managedHook(config, 'PostToolUse', 'hook.js');
const prePayload = {
hook_event_name: 'PreToolUse',
session_id: 'session-1',
cwd: fixture.homeDir,
tool_name: 'Bash',
tool_input: { command: 'echo test' },
tool_call_id: 'call-1',
};
const guardResult = await runCommand(guard.command, JSON.stringify(prePayload));
assert.equal(guardResult.code, 2);
assert.match(guardResult.stderr, /Agent is frozen by Elydora/);
const postPayload = {
...prePayload,
hook_event_name: 'PostToolUse',
tool_output: { output: 'test' },
};
const auditResult = await runCommand(audit.command, JSON.stringify(postPayload));
assert.equal(auditResult.code, 0, auditResult.stderr);
assert.deepEqual(JSON.parse(await readFile(capturePath, 'utf-8')), postPayload);
} finally {
await fixture.close();
await rm(capturePath, { force: true });
}
});
test('Kimi status accepts either runtime contract and requires both scripts', async () => {
const fixture = await createFixture();
try {
assert.equal(fixture.installResult.code, 0, fixture.installResult.stderr);
await rm(fixture.modernPath);
let status = await runPlugin(fixture, 'status', null);
assert.equal(status.code, 0, status.stderr);
assert.equal(JSON.parse(status.stdout).installed, true);
const reinstall = await runPlugin(fixture, 'install', {
agentName: 'kimi',
agentId: 'agent-1',
baseUrl: 'https://api.elydora.com',
guardScriptPath: fixture.guardScriptPath,
hookScriptPath: fixture.hookScriptPath,
});
assert.equal(reinstall.code, 0, reinstall.stderr);
await rm(fixture.legacyPath);
status = await runPlugin(fixture, 'status', null);
assert.equal(JSON.parse(status.stdout).installed, true);
await rm(fixture.guardScriptPath);
status = await runPlugin(fixture, 'status', null);
assert.equal(JSON.parse(status.stdout).installed, false);
} finally {
await fixture.close();
}
});
test('Kimi uninstall preserves user hooks in both contracts', async () => {
const userHook = [
'# user hook',
'[[hooks]]',
'event = "SessionStart"',
'command = "existing-command"',
'timeout = 30 # keep timeout',
'',
].join('\n');
const fixture = await createFixture({ modernConfig: userHook, legacyConfig: userHook });
try {
assert.equal(fixture.installResult.code, 0, fixture.installResult.stderr);
const result = await runPlugin(fixture, 'uninstall', 'agent-1');
assert.equal(result.code, 0, result.stderr);
for (const configPath of [fixture.modernPath, fixture.legacyPath]) {
const raw = await readFile(configPath, 'utf-8');
const config = parseToml(raw);
assert.match(raw, /# user hook/);
assert.match(raw, /# keep timeout/);
assert.deepEqual(config.hooks.map((hook) => ({ ...hook })), [{
event: 'SessionStart',
command: 'existing-command',
timeout: 30,
}]);
}
} finally {
await fixture.close();
}
});
test('Kimi uninstall removes configs created entirely by Elydora', async () => {
const fixture = await createFixture();
try {
assert.equal(fixture.installResult.code, 0, fixture.installResult.stderr);
const result = await runPlugin(fixture, 'uninstall', 'agent-1');
assert.equal(result.code, 0, result.stderr);
await assert.rejects(readFile(fixture.modernPath), { code: 'ENOENT' });
await assert.rejects(readFile(fixture.legacyPath), { code: 'ENOENT' });
} finally {
await fixture.close();
}
});
test('Kimi parses every config before writing either contract', async () => {
const modernConfig = '# untouched modern\ndefault_model = "kimi-code/k3"\n';
const legacyConfig = '[malformed';
const fixture = await createFixture({ modernConfig, legacyConfig });
try {
assert.equal(fixture.installResult.code, 1);
assert.match(fixture.installResult.stderr, /parse kimi-cli legacy hooks config/i);
assert.equal(await readFile(fixture.modernPath, 'utf-8'), modernConfig);
assert.equal(await readFile(fixture.legacyPath, 'utf-8'), legacyConfig);
} finally {
await fixture.close();
}
});
test('Kimi rejects invalid hook shapes without rewriting user config', async () => {
const modernConfig = [
'[[hooks]]',
'event = "PreToolUse"',
'command = "existing-command"',
'cwd = "/tmp"',
'',
].join('\n');
const fixture = await createFixture({ modernConfig });
try {
assert.equal(fixture.installResult.code, 1);
assert.match(fixture.installResult.stderr, /unsupported field "cwd"/i);
assert.equal(await readFile(fixture.modernPath, 'utf-8'), modernConfig);
await assert.rejects(readFile(fixture.legacyPath), { code: 'ENOENT' });
} finally {
await fixture.close();
}
});
test('Kimi install rejects missing runtimes before creating configs', async () => {
const homeDir = await mkdtemp(path.join(os.tmpdir(), 'elydora-kimi-missing-runtime-'));
const fixture = {
binDir: path.join(homeDir, 'bin'),
explicitKimiHome: true,
homeDir,
kimiHome: path.join(homeDir, 'custom-kimi-code'),
modernPath: path.join(homeDir, 'custom-kimi-code', 'config.toml'),
legacyPath: path.join(homeDir, '.kimi', 'config.toml'),
};
try {
const result = await runPlugin(fixture, 'install', {
agentName: 'kimi',
agentId: 'agent-1',
guardScriptPath: path.join(homeDir, '.elydora', 'agent-1', 'guard.js'),
hookScriptPath: path.join(homeDir, '.elydora', 'agent-1', 'hook.js'),
});
assert.equal(result.code, 1);
assert.match(result.stderr, /runtime is missing/i);
await assert.rejects(readFile(fixture.modernPath), { code: 'ENOENT' });
await assert.rejects(readFile(fixture.legacyPath), { code: 'ENOENT' });
} finally {
await rm(homeDir, { recursive: true, force: true });
}
});
test('Kimi status surfaces malformed referenced runtime metadata', async () => {
const fixture = await createFixture();
try {
assert.equal(fixture.installResult.code, 0, fixture.installResult.stderr);
await writeFile(path.join(fixture.agentDir, 'config.json'), '{ malformed');
const status = await runPlugin(fixture, 'status', null);
assert.equal(status.code, 1);
assert.match(status.stderr, /parse Elydora runtime config/i);
} finally {
await fixture.close();
}
});
test('Kimi atomic writes leave no temporary files behind', async () => {
const fixture = await createFixture();
try {
assert.equal(fixture.installResult.code, 0, fixture.installResult.stderr);
for (const directory of [path.dirname(fixture.modernPath), path.dirname(fixture.legacyPath)]) {
assert.equal((await readdir(directory)).some((name) => name.endsWith('.tmp')), false);
}
} finally {
await fixture.close();
}
});