Files

317 lines
11 KiB
JavaScript

import assert from 'node:assert/strict';
import { lstat, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
import path from 'node:path';
import test from 'node:test';
import { stringify as stringifyToml } from '@decimalturn/toml-patch';
import {
assertManagedHook,
cliPath,
createFixture,
installConfig,
legacyCommand,
managedHook,
readKimiConfig,
registryModuleUrl,
runNode,
runPlugin,
} from '../test-support/kimi-test-helpers.mjs';
const userStableConfig = [
'# stable user config',
'default_model = "kimi-code/k3"',
'',
'[[hooks]]',
'event = "SessionStart"',
'command = "existing-stable"',
'timeout = 30 # keep stable hook',
'',
].join('\n');
const userLegacyConfig = [
'# legacy user config',
'telemetry = false',
'',
'[[hooks]]',
'event = "SessionEnd"',
'command = "existing-legacy"',
'',
].join('\n');
async function assertMissing(filePath) {
await assert.rejects(readFile(filePath), { code: 'ENOENT' });
}
function assertManagedTriple(config) {
assertManagedHook(managedHook(config, 'PreToolUse'), 'PreToolUse');
assertManagedHook(managedHook(config, 'PostToolUse'), 'PostToolUse');
assertManagedHook(managedHook(config, 'PostToolUseFailure'), 'PostToolUseFailure');
assert.equal(
managedHook(config, 'PostToolUse').command,
managedHook(config, 'PostToolUseFailure').command,
);
}
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 fixture = await createFixture({ legacyDetected: false });
try {
const result = await runNode(['--no-warnings', cliPath, 'status'], {
HOME: fixture.homeDir,
USERPROFILE: fixture.homeDir,
KIMI_CODE_HOME: fixture.kimiHome,
}, fixture.projectDir);
assert.equal(result.code, 0, result.stderr);
assert.match(result.stdout, /Kimi Code \(kimi\)/);
} finally {
await fixture.close();
}
});
test('Kimi installs an exact managed triple in both configs and is idempotent', async () => {
const fixture = await createFixture({
stableConfig: userStableConfig,
legacyConfig: userLegacyConfig,
});
try {
const first = await fixture.install();
assert.equal(first.code, 0, first.stderr);
assert.match(first.stdout, /Kimi Code and kimi-cli/i);
const firstSources = [];
for (const [configPath, comment, userCommand] of [
[fixture.stablePath, '# stable user config', 'existing-stable'],
[fixture.legacyPath, '# legacy user config', 'existing-legacy'],
]) {
const { raw, config } = await readKimiConfig(configPath);
firstSources.push(raw);
assert.match(raw, new RegExp(comment));
assert(config.hooks.some((hook) => hook.command === userCommand));
assert.equal(config.hooks.length, 4);
assertManagedTriple(config);
}
const second = await fixture.install();
assert.equal(second.code, 0, second.stderr);
assert.equal(await readFile(fixture.stablePath, 'utf-8'), firstSources[0]);
assert.equal(await readFile(fixture.legacyPath, 'utf-8'), firstSources[1]);
await assertMissing(path.join(fixture.homeDir, '.kimi-code', 'config.toml'));
} finally {
await fixture.close();
}
});
test('fresh stable Kimi installation leaves the legacy migration source absent', async () => {
const fixture = await createFixture({ legacyDetected: false });
try {
const result = await fixture.install();
assert.equal(result.code, 0, result.stderr);
const { config } = await readKimiConfig(fixture.stablePath);
assert.equal(config.hooks.length, 3);
assertManagedTriple(config);
await assertMissing(fixture.legacyPath);
} finally {
await fixture.close();
}
});
test('empty KIMI_CODE_HOME resolves to the documented default', async () => {
const fixture = await createFixture({
explicitKimiHome: false,
legacyDetected: false,
});
try {
assert.equal((await fixture.install()).code, 0);
fixture.kimiHomeOverride = '';
const status = await runPlugin(fixture, 'status', null);
assert.equal(status.code, 0, status.stderr);
assert.equal(JSON.parse(status.stdout).installed, true);
assertManagedTriple((await readKimiConfig(fixture.stablePath)).config);
} finally {
await fixture.close();
}
});
test('legacy-only installation leaves the stable migration target absent', async () => {
const fixture = await createFixture({
explicitKimiHome: false,
stableDetected: false,
});
try {
const result = await fixture.install();
assert.equal(result.code, 0, result.stderr);
assertManagedTriple((await readKimiConfig(fixture.legacyPath)).config);
await assertMissing(fixture.stablePath);
} finally {
await fixture.close();
}
});
test('Kimi parses every selected config before creating runtime files', async () => {
const fixture = await createFixture({
stableConfig: userStableConfig,
legacyConfig: '[malformed',
});
try {
const result = await fixture.install();
assert.equal(result.code, 1);
assert.match(result.stderr, /parse kimi-cli legacy hooks config/i);
assert.equal(await readFile(fixture.stablePath, 'utf-8'), userStableConfig);
assert.equal(await readFile(fixture.legacyPath, 'utf-8'), '[malformed');
await assert.rejects(lstat(fixture.agentDir), { code: 'ENOENT' });
} finally {
await fixture.close();
}
});
test('Kimi rejects fields and events outside each official contract', async (t) => {
for (const [name, stableConfig, legacyConfig, pattern] of [
[
'unsupported field',
'[[hooks]]\nevent = "PreToolUse"\ncommand = "existing"\ncwd = "/tmp"\n',
undefined,
/unsupported field "cwd"/i,
],
[
'legacy stable-only event',
undefined,
'[[hooks]]\nevent = "Interrupt"\ncommand = "existing"\n',
/unsupported event "Interrupt"/i,
],
[
'timeout bound',
'[[hooks]]\nevent = "PreToolUse"\ncommand = "existing"\ntimeout = 601\n',
undefined,
/integer from 1 to 600/i,
],
]) {
await t.test(name, async () => {
const fixture = await createFixture({ stableConfig, legacyConfig });
try {
const result = await fixture.install();
assert.equal(result.code, 1);
assert.match(result.stderr, pattern);
await assert.rejects(lstat(fixture.agentDir), { code: 'ENOENT' });
} finally {
await fixture.close();
}
});
}
});
test('Kimi migrates exact legacy commands and preserves command lookalikes', async () => {
const fixture = await createFixture({ legacyDetected: false });
const lookalike = `${legacyCommand(fixture.guardScriptPath)} --inspect`;
const source = [
'[[hooks]]',
'event = "PreToolUse"',
`command = ${JSON.stringify(legacyCommand(fixture.guardScriptPath))}`,
'timeout = 10',
'',
'[[hooks]]',
'event = "PostToolUse"',
`command = ${JSON.stringify(legacyCommand(fixture.hookScriptPath))}`,
'timeout = 10',
'',
'[[hooks]]',
'event = "PreToolUse"',
`command = ${JSON.stringify(lookalike)}`,
'timeout = 10',
'',
].join('\n');
try {
await mkdir(path.dirname(fixture.stablePath), { recursive: true });
await writeFile(fixture.stablePath, source, { encoding: 'utf-8', mode: 0o600 });
const install = await fixture.install();
assert.equal(install.code, 0, install.stderr);
let config = (await readKimiConfig(fixture.stablePath)).config;
assert.equal(config.hooks.length, 4);
assert(config.hooks.some((hook) => hook.command === lookalike));
assertManagedTriple(config);
const uninstall = await runPlugin(fixture, 'uninstall', 'agent-1');
assert.equal(uninstall.code, 0, uninstall.stderr);
config = (await readKimiConfig(fixture.stablePath)).config;
assert.deepEqual(config.hooks.map((hook) => ({ ...hook })), [{
event: 'PreToolUse',
command: lookalike,
timeout: 10,
}]);
} finally {
await fixture.close();
}
});
test('Kimi uninstall preserves user config and removes fully managed configs', async () => {
const userFixture = await createFixture({
stableConfig: userStableConfig,
legacyConfig: userLegacyConfig,
});
try {
assert.equal((await userFixture.install()).code, 0);
const result = await runPlugin(userFixture, 'uninstall', 'agent-1');
assert.equal(result.code, 0, result.stderr);
const stable = await readKimiConfig(userFixture.stablePath);
const legacy = await readKimiConfig(userFixture.legacyPath);
assert.match(stable.raw, /# keep stable hook/);
assert.match(legacy.raw, /# legacy user config/);
assert.deepEqual(stable.config.hooks.map((hook) => hook.command), ['existing-stable']);
assert.deepEqual(legacy.config.hooks.map((hook) => hook.command), ['existing-legacy']);
} finally {
await userFixture.close();
}
const managedFixture = await createFixture();
try {
assert.equal((await managedFixture.install()).code, 0);
const result = await runPlugin(managedFixture, 'uninstall', 'agent-1');
assert.equal(result.code, 0, result.stderr);
await assertMissing(managedFixture.stablePath);
await assertMissing(managedFixture.legacyPath);
} finally {
await managedFixture.close();
}
});
test('Kimi status requires the complete triple, runtime identity, and private key', async () => {
const fixture = await createFixture({ legacyDetected: false });
try {
assert.equal((await fixture.install()).code, 0);
let status = await runPlugin(fixture, 'status', null);
assert.equal(status.code, 0, status.stderr);
assert.equal(JSON.parse(status.stdout).installed, true);
const { config } = await readKimiConfig(fixture.stablePath);
config.hooks = config.hooks.filter((hook) => hook.event !== 'PostToolUseFailure');
await writeFile(fixture.stablePath, stringifyToml(config), 'utf-8');
status = await runPlugin(fixture, 'status', null);
assert.equal(JSON.parse(status.stdout).installed, false);
assert.equal((await fixture.install()).code, 0);
await writeFile(path.join(fixture.agentDir, 'private.key'), 'invalid');
status = await runPlugin(fixture, 'status', null);
assert.equal(status.code, 1);
assert.match(status.stderr, /private key is invalid/i);
} finally {
await fixture.close();
}
});
test('Kimi installation leaves no transaction files', async () => {
const fixture = await createFixture();
try {
const result = await fixture.install();
assert.equal(result.code, 0, result.stderr);
for (const directory of [fixture.agentDir, fixture.kimiHome, fixture.legacyHome]) {
const names = await readdir(directory);
assert.equal(names.some((name) => /\.(tmp|rollback)$/.test(name)), false, names.join(', '));
}
} finally {
await fixture.close();
}
});