mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-19 05:35:34 +00:00
Move src/{domain}/* to src/{scope}/{domain}/* (app/session/agent) so the
directory layout mirrors the DI LifecycleScope tree. Split the three
multi-scope domains so no domain spans two scopes:
- log -> app/log + session/sessionLog
- question -> session/question + agent/questionTools
- skill -> app/globalSkillCatalog + session/sessionSkillCatalog + agent/skill
Rewrite every #/ and relative import, module augmentation, vi.mock, and
source-path read; update the package barrel, registerScopedService domain
labels, the domain-layer checker, the dep-graph script, file headers, and
the di-scope-domains map.
200 lines
6.9 KiB
TypeScript
200 lines
6.9 KiB
TypeScript
import { execFileSync } from 'node:child_process';
|
|
import { createServer } from 'node:http';
|
|
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { PluginManager } from '#/app/plugin/manager';
|
|
|
|
describe('PluginManager', () => {
|
|
let home: string;
|
|
let root: string;
|
|
|
|
beforeEach(async () => {
|
|
home = await mkdtemp(join(tmpdir(), 'plugin-manager-home-'));
|
|
root = await mkdtemp(join(tmpdir(), 'plugin-manager-root-'));
|
|
await mkdir(join(home, 'plugins'), { recursive: true });
|
|
await mkdir(join(root, 'commands'), { recursive: true });
|
|
await writeFile(join(root, 'commands', 'deploy.md'), '---\ndescription: Deploy\n---\n\nBody', 'utf8');
|
|
await writeFile(
|
|
join(root, 'kimi.plugin.json'),
|
|
JSON.stringify({
|
|
name: 'demo',
|
|
commands: ['./commands'],
|
|
hooks: [{ event: 'Stop', command: 'echo stop' }],
|
|
}),
|
|
'utf8',
|
|
);
|
|
await writeFile(
|
|
join(home, 'plugins', 'installed.json'),
|
|
JSON.stringify({
|
|
version: 1,
|
|
plugins: [
|
|
{
|
|
id: 'demo',
|
|
root,
|
|
source: 'local-path',
|
|
enabled: true,
|
|
installedAt: '2026-01-01T00:00:00.000Z',
|
|
},
|
|
],
|
|
}),
|
|
'utf8',
|
|
);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
vi.unstubAllGlobals();
|
|
await rm(home, { recursive: true, force: true });
|
|
await rm(root, { recursive: true, force: true });
|
|
});
|
|
|
|
it('loads installed plugins and exposes summaries, hooks, and commands', async () => {
|
|
const manager = new PluginManager({ kimiHomeDir: home });
|
|
await manager.load();
|
|
|
|
expect(manager.summaries()).toEqual([
|
|
expect.objectContaining({
|
|
id: 'demo',
|
|
state: 'ok',
|
|
commandCount: 1,
|
|
hookCount: 1,
|
|
}),
|
|
]);
|
|
expect(manager.enabledHooks()).toEqual([
|
|
{
|
|
event: 'Stop',
|
|
command: 'echo stop',
|
|
cwd: root,
|
|
env: { KIMI_CODE_HOME: home, KIMI_PLUGIN_ROOT: root },
|
|
},
|
|
]);
|
|
await expect(manager.enabledCommands()).resolves.toEqual([
|
|
expect.objectContaining({ pluginId: 'demo', name: 'deploy', description: 'Deploy' }),
|
|
]);
|
|
});
|
|
|
|
it('installs a local-path plugin into the managed root', async () => {
|
|
const sourceRoot = await mkdtemp(join(tmpdir(), 'plugin-install-source-'));
|
|
try {
|
|
await writeFile(join(sourceRoot, 'kimi.plugin.json'), JSON.stringify({ name: 'other' }), 'utf8');
|
|
const manager = new PluginManager({ kimiHomeDir: home });
|
|
|
|
const record = await manager.install(sourceRoot);
|
|
|
|
expect(record.id).toBe('other');
|
|
expect(record.root).toContain(join(home, 'plugins', 'managed', 'other'));
|
|
expect(manager.get('other')?.manifest?.name).toBe('other');
|
|
} finally {
|
|
await rm(sourceRoot, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('installs a zip-url plugin', async () => {
|
|
const sourceRoot = await mkdtemp(join(tmpdir(), 'plugin-zip-source-'));
|
|
const zipPath = join(tmpdir(), `plugin-${Date.now()}.zip`);
|
|
const server = createServer((_req, res) => {
|
|
void readFile(zipPath).then((data) => res.end(data));
|
|
});
|
|
try {
|
|
await writeFile(join(sourceRoot, 'kimi.plugin.json'), JSON.stringify({ name: 'zip-plugin' }), 'utf8');
|
|
execFileSync('zip', ['-qr', zipPath, '.'], { cwd: sourceRoot });
|
|
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
const address = server.address();
|
|
if (address === null || typeof address === 'string') throw new Error('bad server address');
|
|
const manager = new PluginManager({ kimiHomeDir: home });
|
|
|
|
const record = await manager.install(`http://127.0.0.1:${address.port}/plugin.zip`);
|
|
|
|
expect(record.id).toBe('zip-plugin');
|
|
expect(record.source).toBe('zip-url');
|
|
expect(manager.get('zip-plugin')?.manifest?.name).toBe('zip-plugin');
|
|
} finally {
|
|
await new Promise<void>((resolve, reject) => server.close((err) => (err === undefined ? resolve() : reject(err))));
|
|
await rm(sourceRoot, { recursive: true, force: true });
|
|
await rm(zipPath, { force: true });
|
|
}
|
|
});
|
|
|
|
it('installs a github plugin through codeload', async () => {
|
|
const sourceRoot = await mkdtemp(join(tmpdir(), 'plugin-github-source-'));
|
|
const zipPath = join(tmpdir(), `plugin-github-${Date.now()}.zip`);
|
|
try {
|
|
await writeFile(join(sourceRoot, 'kimi.plugin.json'), JSON.stringify({ name: 'github-plugin' }), 'utf8');
|
|
execFileSync('zip', ['-qr', zipPath, '.'], { cwd: sourceRoot });
|
|
const zip = await readFile(zipPath);
|
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(zip)));
|
|
const manager = new PluginManager({ kimiHomeDir: home });
|
|
|
|
const record = await manager.install('https://github.com/owner/repo/tree/v1');
|
|
|
|
expect(record.id).toBe('github-plugin');
|
|
expect(record.source).toBe('github');
|
|
expect(record.github).toEqual({
|
|
owner: 'owner',
|
|
repo: 'repo',
|
|
ref: { kind: 'branch', value: 'v1' },
|
|
});
|
|
expect(manager.get('github-plugin')?.manifest?.name).toBe('github-plugin');
|
|
} finally {
|
|
await rm(sourceRoot, { recursive: true, force: true });
|
|
await rm(zipPath, { force: true });
|
|
}
|
|
});
|
|
|
|
it('checks github plugin updates against latest release', async () => {
|
|
await writeFile(
|
|
join(home, 'plugins', 'installed.json'),
|
|
JSON.stringify({
|
|
version: 1,
|
|
plugins: [
|
|
{
|
|
id: 'demo',
|
|
root,
|
|
source: 'github',
|
|
enabled: true,
|
|
installedAt: '2026-01-01T00:00:00.000Z',
|
|
github: { owner: 'owner', repo: 'repo', ref: { kind: 'branch', value: 'v1' } },
|
|
},
|
|
],
|
|
}),
|
|
'utf8',
|
|
);
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
status: 302,
|
|
ok: false,
|
|
headers: new Headers({ location: 'https://github.com/owner/repo/releases/tag/v2' }),
|
|
}),
|
|
);
|
|
const manager = new PluginManager({ kimiHomeDir: home });
|
|
await manager.load();
|
|
|
|
await expect(manager.checkUpdates()).resolves.toEqual([
|
|
{
|
|
id: 'demo',
|
|
source: 'github',
|
|
current: { kind: 'branch', value: 'v1' },
|
|
latest: { kind: 'tag', value: 'v2' },
|
|
displayVersion: 'v2',
|
|
updateAvailable: true,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('persists enabled state changes', async () => {
|
|
const manager = new PluginManager({ kimiHomeDir: home });
|
|
await manager.load();
|
|
|
|
await manager.setEnabled('demo', false);
|
|
|
|
expect(manager.get('demo')?.enabled).toBe(false);
|
|
const stored = JSON.parse(await readFile(join(home, 'plugins', 'installed.json'), 'utf8')) as {
|
|
plugins: Array<{ id: string; enabled: boolean }>;
|
|
};
|
|
expect(stored.plugins).toEqual([expect.objectContaining({ id: 'demo', enabled: false })]);
|
|
});
|
|
});
|