import { mkdir, mkdtemp, readdir, readFile, realpath, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { KIMI_CODE_PROVIDER_NAME } from '@moonshot-ai/kimi-code-oauth'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { ScopeActivation, _clearScopedRegistryForTests, registerScopedService, } from '#/_base/di/scope'; import { createScopedTestHost, stubPair, type ScopedTestHost } from '#/_base/di/test'; import { IBootstrapService } from '#/app/bootstrap/bootstrap'; import { IPluginService } from '#/app/plugin/plugin'; import { PluginService } from '#/app/plugin/pluginService'; import * as pluginStore from '#/app/plugin/store'; import type { InstalledFile } from '#/app/plugin/store'; import type { PluginMutationSummary, ReloadSummary } from '#/app/plugin/types'; import { LifecycleScope } from '#/app/scopes'; import { ISkillDiscovery } from '#/features/skill/catalog/skillDiscovery'; import { IProviderService, type ProviderConfig } from '#/llm-adapter/provider/provider'; import { stubBootstrap } from '../bootstrap/stubs'; import { stubProviderService } from '../provider/stubs'; vi.mock('#/app/plugin/store', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, readInstalled: vi.fn(actual.readInstalled), writeInstalled: vi.fn(actual.writeInstalled), }; }); const readInstalled = vi.mocked(pluginStore.readInstalled); const writeInstalled = vi.mocked(pluginStore.writeInstalled); function makeHost( homeDir: string, providers = stubProviderService(), env: NodeJS.ProcessEnv = {}, ): ScopedTestHost { return createScopedTestHost([ stubPair(IBootstrapService, stubBootstrap(homeDir, env)), stubPair(IProviderService, providers), stubPair(ISkillDiscovery, { _serviceBrand: undefined, discover: async () => ({ skills: [], skipped: [], scannedRoots: [], scannedDirectories: [], }), } satisfies ISkillDiscovery), ]); } async function writeInstalledFile(homeDir: string, contents: string): Promise { await mkdir(path.join(homeDir, 'plugins'), { recursive: true }); await writeFile(path.join(homeDir, 'plugins', 'installed.json'), contents, 'utf8'); } async function writeValidInstalledFile(homeDir: string): Promise { await writeInstalledFile(homeDir, JSON.stringify({ version: 1, plugins: [] })); } function installedFile(id: string, root: string, enabled = true): InstalledFile { return { version: 1, plugins: [ { id, root, source: 'local-path', enabled, installedAt: '2026-01-01T00:00:00.000Z', updatedAt: '2026-01-01T00:00:00.000Z', originalSource: root, }, ], }; } function githubInstalledFile(id: string, root: string): InstalledFile { const local = installedFile(id, root).plugins[0]!; return { version: 1, plugins: [ { ...local, source: 'github', originalSource: `https://github.com/example/${id}`, github: { owner: 'example', repo: id, ref: { kind: 'tag', value: 'v1.0.0' }, }, }, ], }; } async function persistedPluginIds(homeDir: string): Promise { const contents = await readFile(path.join(homeDir, 'plugins', 'installed.json'), 'utf8'); return (JSON.parse(contents) as InstalledFile).plugins.map((plugin) => plugin.id); } function deferred(): { readonly promise: Promise; readonly resolve: (value: T | PromiseLike) => void; } { let resolve!: (value: T | PromiseLike) => void; const promise = new Promise((resolvePromise) => { resolve = resolvePromise; }); return { promise, resolve }; } async function makePluginDir(name: string, manifest: Record): Promise { const root = await mkdtemp(path.join(tmpdir(), `plugin-${name}-`)); await writeFile( path.join(root, 'kimi.plugin.json'), JSON.stringify({ name, ...manifest }), 'utf8', ); return realpath(root); } describe('PluginService (plugin boundary)', () => { const createdDirs: string[] = []; async function makeHome(): Promise { const home = await mkdtemp(path.join(tmpdir(), 'kimi-home-')); createdDirs.push(home); return home; } beforeEach(() => { _clearScopedRegistryForTests(); registerScopedService( LifecycleScope.App, IPluginService, PluginService, ScopeActivation.OnDemand, 'plugin', ); readInstalled.mockClear(); writeInstalled.mockClear(); }); afterEach(async () => { vi.unstubAllGlobals(); vi.restoreAllMocks(); while (createdDirs.length > 0) { const dir = createdDirs.pop(); if (dir !== undefined) await rm(dir, { recursive: true, force: true }); } }); it('degrades consumption-plane reads to empty when installed.json is corrupt', async () => { const home = await makeHome(); await writeInstalledFile(home, '{ not json'); const host = makeHost(home); try { const svc = host.app.accessor.get(IPluginService); await expect(svc.pluginSkillRoots()).resolves.toEqual([]); await expect(svc.enabledSessionStarts()).resolves.toEqual([]); await expect(svc.enabledSystemPrompts()).resolves.toEqual([]); await expect(svc.enabledHooks()).resolves.toEqual([]); } finally { host.dispose(); } }); it('resolves empty plugin MCP servers instead of failing when installed.json is corrupt', async () => { const home = await makeHome(); await writeInstalledFile(home, '{ not json'); const host = makeHost(home); try { const svc = host.app.accessor.get(IPluginService); await expect(svc.enabledMcpServers()).resolves.toEqual({}); const failure = await svc.mcpServerEntries().catch((error: unknown) => error); expect(failure).toMatchObject({ code: 'plugin.load_failed' }); } finally { host.dispose(); } }); it('throws plugin.load_failed with a repair hint on management-plane calls when installed.json is corrupt', async () => { const home = await makeHome(); await writeInstalledFile(home, '{ not json'); const host = makeHost(home); try { const svc = host.app.accessor.get(IPluginService); const failure = await svc.listPlugins().catch((error: unknown) => error); expect(failure).toMatchObject({ code: 'plugin.load_failed' }); expect((failure as Error).message).toContain('installed.json'); expect((failure as Error).message).toContain('/plugins reload'); } finally { host.dispose(); } }); it('keeps the first load failure latched after the file is fixed', async () => { const home = await makeHome(); await writeInstalledFile(home, '{ not json'); const host = makeHost(home); try { const svc = host.app.accessor.get(IPluginService); await expect(svc.pluginSkillRoots()).resolves.toEqual([]); const pluginRoot = await makePluginDir('recovery-demo', {}); createdDirs.push(pluginRoot); await writeInstalledFile(home, JSON.stringify(installedFile('recovery-demo', pluginRoot))); await expect(svc.listPlugins()).rejects.toMatchObject({ code: 'plugin.load_failed' }); await expect(svc.pluginSkillRoots()).resolves.toEqual([]); } finally { host.dispose(); } }); it('recovers the management plane through an explicit reload after the file is fixed', async () => { const home = await makeHome(); await writeInstalledFile(home, '{ not json'); const host = makeHost(home); try { const svc = host.app.accessor.get(IPluginService); await expect(svc.listPlugins()).rejects.toMatchObject({ code: 'plugin.load_failed' }); const pluginRoot = await makePluginDir('recovery-demo', {}); createdDirs.push(pluginRoot); await writeInstalledFile(home, JSON.stringify(installedFile('recovery-demo', pluginRoot))); const reloads: ReloadSummary[] = []; svc.onDidReload(({ added, removed, errors }) => reloads.push({ added, removed, errors })); await expect(svc.reloadPlugins()).resolves.toEqual({ added: ['recovery-demo'], removed: [], errors: [], }); await expect(svc.listPlugins()).resolves.toEqual([ expect.objectContaining({ id: 'recovery-demo' }), ]); expect(reloads).toEqual([{ added: ['recovery-demo'], removed: [], errors: [] }]); } finally { host.dispose(); } }); it('fires onDidReload after install / enable / disable / remove so workspace consumers refresh immediately', async () => { const home = await makeHome(); await writeValidInstalledFile(home); const pluginRoot = await makePluginDir('notify-demo', { description: 'demo plugin' }); createdDirs.push(pluginRoot); const host = makeHost(home); try { const svc = host.app.accessor.get(IPluginService); const reloads: ReloadSummary[] = []; svc.onDidReload((summary) => reloads.push(summary)); await svc.installPlugin({ source: pluginRoot }); await svc.setPluginEnabled({ id: 'notify-demo', enabled: false }); await svc.setPluginEnabled({ id: 'notify-demo', enabled: true }); await svc.removePlugin({ id: 'notify-demo' }); expect(reloads).toHaveLength(4); await expect(svc.listPlugins()).resolves.toEqual([]); } finally { host.dispose(); } }); it('fires onDidMutate after install / enable / disable / remove but not on an explicit reloadPlugins', async () => { const home = await makeHome(); await writeValidInstalledFile(home); const pluginRoot = await makePluginDir('mutate-demo', { description: 'demo plugin' }); createdDirs.push(pluginRoot); const host = makeHost(home); try { const svc = host.app.accessor.get(IPluginService); const mutations: PluginMutationSummary[] = []; svc.onDidMutate((summary) => mutations.push(summary)); await svc.installPlugin({ source: pluginRoot }); await svc.setPluginEnabled({ id: 'mutate-demo', enabled: false }); await svc.setPluginEnabled({ id: 'mutate-demo', enabled: true }); await svc.removePlugin({ id: 'mutate-demo' }); expect(mutations).toHaveLength(4); expect(mutations.map((summary) => summary.mutation)).toEqual([ { kind: 'install', id: 'mutate-demo' }, { kind: 'disable', id: 'mutate-demo' }, { kind: 'enable', id: 'mutate-demo' }, { kind: 'remove', id: 'mutate-demo' }, ]); await svc.reloadPlugins(); expect(mutations).toHaveLength(4); } finally { host.dispose(); } }); it('resolves a mutation only after reload listeners settle their waitUntil work', async () => { const home = await makeHome(); const pluginRoot = await makePluginDir('barrier-demo', {}); createdDirs.push(pluginRoot); await writeInstalledFile(home, JSON.stringify(installedFile('barrier-demo', pluginRoot))); const host = makeHost(home); try { const svc = host.app.accessor.get(IPluginService); await expect(svc.listPlugins()).resolves.toHaveLength(1); const listenerCalled = deferred(); const reconcileGate = deferred(); let reconciled = false; svc.onDidReload((event) => { listenerCalled.resolve(undefined); event.waitUntil( reconcileGate.promise.then(() => { reconciled = true; }), ); }); const mutation = svc.setPluginEnabled({ id: 'barrier-demo', enabled: false }); let mutationSettled = false; void mutation.then(() => { mutationSettled = true; }); await listenerCalled.promise; for (let i = 0; i < 5; i++) await Promise.resolve(); expect(mutationSettled).toBe(false); reconcileGate.resolve(undefined); await mutation; expect(reconciled).toBe(true); await expect(svc.listPlugins()).resolves.toEqual([ expect.objectContaining({ id: 'barrier-demo', enabled: false }), ]); } finally { host.dispose(); } }); it('does not reject a mutation when a reload listener waitUntil promise rejects', async () => { const home = await makeHome(); const pluginRoot = await makePluginDir('tolerant-demo', {}); createdDirs.push(pluginRoot); await writeInstalledFile(home, JSON.stringify(installedFile('tolerant-demo', pluginRoot))); const host = makeHost(home); try { const svc = host.app.accessor.get(IPluginService); await expect(svc.listPlugins()).resolves.toHaveLength(1); vi.spyOn(console, 'error').mockImplementation(() => {}); svc.onDidReload((event) => { event.waitUntil(Promise.reject(new Error('workspace reconcile failed'))); }); await expect( svc.setPluginEnabled({ id: 'tolerant-demo', enabled: false }), ).resolves.toBeUndefined(); await expect(svc.listPlugins()).resolves.toEqual([ expect.objectContaining({ id: 'tolerant-demo', enabled: false }), ]); } finally { host.dispose(); } }); it('serves enabled plugin system-prompt sections on the consumption plane', async () => { const home = await makeHome(); const pluginRoot = await makePluginDir('prompt-demo', { systemPrompt: 'Always cite sources.' }); createdDirs.push(pluginRoot); await writeInstalledFile(home, JSON.stringify(installedFile('prompt-demo', pluginRoot))); const host = makeHost(home); try { const svc = host.app.accessor.get(IPluginService); await expect(svc.enabledSystemPrompts()).resolves.toEqual([ { pluginId: 'prompt-demo', content: 'Always cite sources.' }, ]); } finally { host.dispose(); } }); it('keeps the last valid consumption snapshot after a reload failure', async () => { const home = await makeHome(); const pluginRoot = await makePluginDir('stable-demo', { skills: './skills/' }); createdDirs.push(pluginRoot); await mkdir(path.join(pluginRoot, 'skills')); await writeInstalledFile(home, JSON.stringify(installedFile('stable-demo', pluginRoot))); const host = makeHost(home); try { const svc = host.app.accessor.get(IPluginService); await expect(svc.pluginSkillRoots()).resolves.toEqual([ expect.objectContaining({ plugin: expect.objectContaining({ id: 'stable-demo' }) }), ]); await writeInstalledFile(home, '{ not json'); await expect(svc.reloadPlugins()).rejects.toMatchObject({ code: 'plugin.load_failed' }); await expect(svc.listPlugins()).rejects.toMatchObject({ code: 'plugin.load_failed' }); await expect(svc.pluginSkillRoots()).resolves.toEqual([ expect.objectContaining({ plugin: expect.objectContaining({ id: 'stable-demo' }) }), ]); } finally { host.dispose(); } }); it('uses one initial plugin snapshot for concurrent readers', async () => { const home = await makeHome(); await writeValidInstalledFile(home); const pluginRoot = await makePluginDir('snapshot-demo', { skills: './skills/' }); createdDirs.push(pluginRoot); await mkdir(path.join(pluginRoot, 'skills')); readInstalled.mockImplementationOnce(async () => installedFile('snapshot-demo', pluginRoot)); const host = makeHost(home); try { const svc = host.app.accessor.get(IPluginService); const [plugins, roots] = await Promise.all([svc.listPlugins(), svc.pluginSkillRoots()]); expect(plugins).toEqual([expect.objectContaining({ id: 'snapshot-demo' })]); expect(roots).toEqual([ expect.objectContaining({ plugin: expect.objectContaining({ id: 'snapshot-demo' }), }), ]); } finally { host.dispose(); } }); it('waits for a pending install before reading managed plugin files', async () => { const home = await makeHome(); await writeValidInstalledFile(home); const downloadStarted = deferred(); const downloadResponse = deferred(); vi.stubGlobal( 'fetch', vi.fn(() => { downloadStarted.resolve(undefined); return downloadResponse.promise; }) as typeof fetch, ); const host = makeHost(home); try { const svc = host.app.accessor.get(IPluginService); await expect(svc.listPlugins()).resolves.toEqual([]); const install = svc.installPlugin({ source: 'https://downloads.example.test/plugin.zip' }); await downloadStarted.promise; const roots = svc.pluginSkillRoots(); let rootsSettled = false; void roots.then(() => { rootsSettled = true; }); await Promise.resolve(); expect(rootsSettled).toBe(false); downloadResponse.resolve(new Response('not a zip archive', { status: 200 })); await expect(install).rejects.toThrow(); await expect(roots).resolves.toEqual([]); } finally { host.dispose(); } }); it('restores the previous managed copy when reinstall persistence fails', async () => { const home = await makeHome(); await writeValidInstalledFile(home); const previousSource = await makePluginDir('demo', { version: '1.0.0' }); const nextSource = await makePluginDir('demo', { version: '2.0.0' }); createdDirs.push(previousSource, nextSource); const host = makeHost(home); try { const svc = host.app.accessor.get(IPluginService); await svc.installPlugin({ source: previousSource }); const previous = await svc.getPluginInfo({ id: 'demo' }); writeInstalled.mockRejectedValueOnce(new Error('persist failed')); await expect(svc.installPlugin({ source: nextSource })).rejects.toThrow('persist failed'); await expect(svc.getPluginInfo({ id: 'demo' })).resolves.toEqual( expect.objectContaining({ root: previous.root, version: '1.0.0' }), ); await expect( readFile(path.join(previous.root, 'kimi.plugin.json'), 'utf8'), ).resolves.toContain('"version":"1.0.0"'); await expect(readdir(path.join(home, 'plugins', 'managed'))).resolves.toEqual(['demo']); } finally { host.dispose(); } }); it('does not block consumption reads while an update check is pending', async () => { const home = await makeHome(); const pluginRoot = await makePluginDir('github-demo', { skills: './skills/' }); createdDirs.push(pluginRoot); await mkdir(path.join(pluginRoot, 'skills')); await writeInstalledFile(home, JSON.stringify(githubInstalledFile('github-demo', pluginRoot))); const lookupStarted = deferred(); const lookupResponse = deferred(); vi.stubGlobal( 'fetch', vi.fn(() => { lookupStarted.resolve(undefined); return lookupResponse.promise; }) as typeof fetch, ); const host = makeHost(home); try { const svc = host.app.accessor.get(IPluginService); await expect(svc.listPlugins()).resolves.toEqual([ expect.objectContaining({ id: 'github-demo' }), ]); const updates = svc.checkUpdates(); await lookupStarted.promise; await expect(svc.pluginSkillRoots()).resolves.toEqual([ expect.objectContaining({ plugin: expect.objectContaining({ id: 'github-demo' }) }), ]); lookupResponse.resolve( new Response(null, { status: 302, headers: { location: 'https://github.com/example/github-demo/releases/tag/v2.0.0', }, }), ); await expect(updates).resolves.toEqual([ expect.objectContaining({ id: 'github-demo', updateAvailable: true }), ]); } finally { host.dispose(); } }); it('keeps an explicit reload result when the first load is still in flight', async () => { const home = await makeHome(); await writeValidInstalledFile(home); const pluginRoot = await makePluginDir('old-demo', {}); createdDirs.push(pluginRoot); const firstRead = deferred(); const firstReadStarted = deferred(); readInstalled.mockImplementationOnce(async () => { firstReadStarted.resolve(undefined); return firstRead.promise; }); const host = makeHost(home); try { const svc = host.app.accessor.get(IPluginService); const reloads: ReloadSummary[] = []; svc.onDidReload(({ added, removed, errors }) => reloads.push({ added, removed, errors })); const firstList = svc.listPlugins(); await firstReadStarted.promise; const reload = svc.reloadPlugins(); firstRead.resolve(installedFile('old-demo', pluginRoot)); await expect(firstList).resolves.toEqual([expect.objectContaining({ id: 'old-demo' })]); await expect(reload).resolves.toEqual({ added: [], removed: ['old-demo'], errors: [] }); await expect(svc.listPlugins()).resolves.toEqual([]); await expect(persistedPluginIds(home)).resolves.toEqual([]); expect(reloads).toEqual([{ added: [], removed: ['old-demo'], errors: [] }]); } finally { host.dispose(); } }); it('keeps a queued removal when reload is already reading the installed file', async () => { const home = await makeHome(); const pluginRoot = await makePluginDir('demo', {}); createdDirs.push(pluginRoot); await writeInstalledFile(home, JSON.stringify(installedFile('demo', pluginRoot))); const host = makeHost(home); try { const svc = host.app.accessor.get(IPluginService); await expect(svc.listPlugins()).resolves.toEqual([ expect.objectContaining({ id: 'demo', enabled: true }), ]); const reloadRead = deferred(); const reloadReadStarted = deferred(); readInstalled.mockImplementationOnce(async () => { reloadReadStarted.resolve(undefined); return reloadRead.promise; }); const reload = svc.reloadPlugins(); await reloadReadStarted.promise; const remove = svc.removePlugin({ id: 'demo' }); await Promise.resolve(); reloadRead.resolve(installedFile('demo', pluginRoot)); await expect(reload).resolves.toEqual({ added: [], removed: [], errors: [] }); await expect(remove).resolves.toBeUndefined(); await expect(svc.listPlugins()).resolves.toEqual([]); await expect(persistedPluginIds(home)).resolves.toEqual([]); } finally { host.dispose(); } }); it('throws plugin.not_found from getPluginInfo for an unknown plugin', async () => { const home = await makeHome(); await writeValidInstalledFile(home); const host = makeHost(home); try { const svc = host.app.accessor.get(IPluginService); await expect(svc.getPluginInfo({ id: 'nope' })).rejects.toMatchObject({ code: 'plugin.not_found', }); } finally { host.dispose(); } }); it('injects the managed Kimi endpoint env into stdio plugin MCP servers only', async () => { const home = await makeHome(); await writeValidInstalledFile(home); const host = makeHost( home, stubProviderService({ [KIMI_CODE_PROVIDER_NAME]: { baseUrl: 'https://api.example.test/', oauth: { storage: 'file', key: 'kimi', oauthHost: 'https://auth.example.test' }, }, }), ); try { const svc = host.app.accessor.get(IPluginService); const pluginRoot = await makePluginDir('demo', { mcpServers: { finance: { command: 'finance-mcp', env: { CUSTOM: '1' } }, docs: { url: 'https://example.test/mcp' }, }, }); createdDirs.push(pluginRoot); await svc.installPlugin({ source: pluginRoot }); const servers = await svc.enabledMcpServers(); const managedRoot = path.join(home, 'plugins', 'managed', 'demo'); expect(servers['plugin-demo:finance']).toEqual( expect.objectContaining({ env: expect.objectContaining({ KIMI_CODE_BASE_URL: 'https://api.example.test/', KIMI_CODE_OAUTH_HOST: 'https://auth.example.test', CUSTOM: '1', KIMI_CODE_HOME: home, KIMI_PLUGIN_ROOT: await realpath(managedRoot), }), }), ); expect(JSON.stringify(servers['plugin-demo:docs'])).not.toContain('KIMI_CODE_BASE_URL'); } finally { host.dispose(); } }); it('merges the managed Kimi endpoint env into stdio MCP server entries with provenance', async () => { const home = await makeHome(); await writeValidInstalledFile(home); const host = makeHost( home, stubProviderService({ [KIMI_CODE_PROVIDER_NAME]: { baseUrl: 'https://api.example.test/', oauth: { storage: 'file', key: 'kimi', oauthHost: 'https://auth.example.test' }, }, }), ); try { const svc = host.app.accessor.get(IPluginService); const pluginRoot = await makePluginDir('demo', { mcpServers: { finance: { command: 'finance-mcp', env: { CUSTOM: '1' } }, docs: { url: 'https://example.test/mcp' }, }, }); createdDirs.push(pluginRoot); await svc.installPlugin({ source: pluginRoot }); await svc.setPluginMcpServerEnabled({ id: 'demo', server: 'finance', enabled: false }); const entries = await svc.mcpServerEntries(); const managedRoot = await realpath(path.join(home, 'plugins', 'managed', 'demo')); const finance = entries.find((entry) => entry.name === 'plugin-demo:finance'); expect(finance).toEqual(expect.objectContaining({ pluginId: 'demo', serverName: 'finance' })); expect(finance?.config).toEqual( expect.objectContaining({ enabled: false, env: expect.objectContaining({ KIMI_CODE_BASE_URL: 'https://api.example.test/', KIMI_CODE_OAUTH_HOST: 'https://auth.example.test', CUSTOM: '1', KIMI_CODE_HOME: home, KIMI_PLUGIN_ROOT: managedRoot, }), }), ); const docs = entries.find((entry) => entry.name === 'plugin-demo:docs'); expect(docs).toEqual(expect.objectContaining({ pluginId: 'demo', serverName: 'docs' })); expect(docs?.config.enabled).toBe(true); expect(JSON.stringify(docs?.config)).not.toContain('KIMI_CODE_BASE_URL'); } finally { host.dispose(); } }); it('waits for provider config before injecting persisted managed endpoints', async () => { const home = await makeHome(); await writeValidInstalledFile(home); const providerConfigs: Record = {}; const readyAccessed = deferred(); const readyGate = deferred(); const providers = stubProviderService(providerConfigs, readyGate.promise); Object.defineProperty(providers, 'ready', { get: () => { readyAccessed.resolve(undefined); return readyGate.promise; }, }); const host = makeHost(home, providers); try { const svc = host.app.accessor.get(IPluginService); const pluginRoot = await makePluginDir('ready-demo', { mcpServers: { finance: { command: 'finance-mcp' } }, }); createdDirs.push(pluginRoot); await svc.installPlugin({ source: pluginRoot }); const servers = svc.enabledMcpServers(); await readyAccessed.promise; providerConfigs[KIMI_CODE_PROVIDER_NAME] = { baseUrl: 'https://ready.example.test/', oauth: { storage: 'file', key: 'kimi', oauthHost: 'https://auth.ready.example.test' }, }; readyGate.resolve(undefined); await expect(servers).resolves.toMatchObject({ 'plugin-ready-demo:finance': { env: { KIMI_CODE_BASE_URL: 'https://ready.example.test/', KIMI_CODE_OAUTH_HOST: 'https://auth.ready.example.test', }, }, }); } finally { host.dispose(); } }); it('prefers explicit KIMI_CODE_BASE_URL / KIMI_OAUTH_HOST env over the persisted provider', async () => { const home = await makeHome(); await writeValidInstalledFile(home); const host = makeHost( home, stubProviderService({ [KIMI_CODE_PROVIDER_NAME]: { baseUrl: 'https://api.example.test', oauth: { storage: 'file', key: 'kimi', oauthHost: 'https://auth.example.test' }, }, }), { KIMI_CODE_BASE_URL: 'https://env.example.test/', KIMI_OAUTH_HOST: 'https://legacy.example.test', }, ); try { const svc = host.app.accessor.get(IPluginService); const pluginRoot = await makePluginDir('demo', { mcpServers: { finance: { command: 'finance-mcp' } }, }); createdDirs.push(pluginRoot); await svc.installPlugin({ source: pluginRoot }); const servers = await svc.enabledMcpServers(); expect(servers['plugin-demo:finance']).toEqual( expect.objectContaining({ env: expect.objectContaining({ KIMI_CODE_BASE_URL: 'https://env.example.test', KIMI_CODE_OAUTH_HOST: 'https://legacy.example.test', }), }), ); } finally { host.dispose(); } }); it('does not inject managed env when neither env nor the kimi provider supplies it', async () => { const home = await makeHome(); await writeValidInstalledFile(home); const host = makeHost(home); try { const svc = host.app.accessor.get(IPluginService); const pluginRoot = await makePluginDir('demo', { mcpServers: { finance: { command: 'finance-mcp', env: { CUSTOM: '1' } } }, }); createdDirs.push(pluginRoot); await svc.installPlugin({ source: pluginRoot }); const servers = await svc.enabledMcpServers(); const env = (servers['plugin-demo:finance'] as { env?: Record }).env ?? {}; expect(env['CUSTOM']).toBe('1'); expect(env).not.toHaveProperty('KIMI_CODE_BASE_URL'); expect(env).not.toHaveProperty('KIMI_CODE_OAUTH_HOST'); } finally { host.dispose(); } }); });