mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-21 14:03:35 +08:00
fix: refresh plugin settings before slash reload
Desktop plugin enable and disable actions are written by the server process, while an already-running CLI owns its own settings cache. Reloading only the plugin caches left enabledPlugins stale after a disable-then-enable cycle, so the slash command list stayed empty until a new conversation spawned a fresh CLI. Reset the settings cache at the active plugin refresh boundary before resolving enabled plugins. The regression test simulates an external desktop enable toggle after a cached disabled read and verifies the plugin slash command is restored in the same refresh. Constraint: Desktop server and active CLI are separate processes with independent in-memory settings caches Rejected: Restart the CLI on every plugin toggle | heavier user-visible lifecycle change and unnecessary once the reload handoff invalidates settings Confidence: high Scope-risk: narrow Directive: Keep reload_plugins as the handoff that rereads settings before plugin command discovery Tested: bun test src/server/__tests__/plugins.test.ts Tested: bun run check:server
This commit is contained in:
parent
5871af11d4
commit
70dc9ab111
@ -2,9 +2,11 @@ import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
|
|||||||
import * as fs from 'node:fs/promises'
|
import * as fs from 'node:fs/promises'
|
||||||
import * as os from 'node:os'
|
import * as os from 'node:os'
|
||||||
import * as path from 'node:path'
|
import * as path from 'node:path'
|
||||||
|
import type { AppState } from '../../state/AppStateStore.js'
|
||||||
import { isEnabledPluginSettingValue } from '../../utils/plugins/dependencyResolver.js'
|
import { isEnabledPluginSettingValue } from '../../utils/plugins/dependencyResolver.js'
|
||||||
import { clearInstalledPluginsCache } from '../../utils/plugins/installedPluginsManager.js'
|
import { clearInstalledPluginsCache } from '../../utils/plugins/installedPluginsManager.js'
|
||||||
import { clearPluginCache, loadAllPluginsCacheOnly } from '../../utils/plugins/pluginLoader.js'
|
import { clearPluginCache, loadAllPluginsCacheOnly } from '../../utils/plugins/pluginLoader.js'
|
||||||
|
import { refreshActivePlugins } from '../../utils/plugins/refresh.js'
|
||||||
import { resetSettingsCache } from '../../utils/settings/settingsCache.js'
|
import { resetSettingsCache } from '../../utils/settings/settingsCache.js'
|
||||||
import { handlePluginsApi } from '../api/plugins.js'
|
import { handlePluginsApi } from '../api/plugins.js'
|
||||||
import { conversationService } from '../services/conversationService.js'
|
import { conversationService } from '../services/conversationService.js'
|
||||||
@ -241,4 +243,116 @@ describe('Plugins API', () => {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('refreshActivePlugins rereads settings after an external enable toggle', async () => {
|
||||||
|
const marketplaceRoot = path.join(tmpDir, 'marketplace-root')
|
||||||
|
const pluginRoot = path.join(marketplaceRoot, 'plugins', 'draw')
|
||||||
|
const pluginsDir = path.join(tmpDir, 'plugins')
|
||||||
|
const marketplaceFile = path.join(
|
||||||
|
marketplaceRoot,
|
||||||
|
'.claude-plugin',
|
||||||
|
'marketplace.json',
|
||||||
|
)
|
||||||
|
|
||||||
|
await fs.mkdir(path.join(pluginRoot, '.claude-plugin'), { recursive: true })
|
||||||
|
await fs.mkdir(path.join(pluginRoot, 'commands'), { recursive: true })
|
||||||
|
await fs.mkdir(path.dirname(marketplaceFile), { recursive: true })
|
||||||
|
await fs.mkdir(pluginsDir, { recursive: true })
|
||||||
|
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(pluginRoot, '.claude-plugin', 'plugin.json'),
|
||||||
|
JSON.stringify({
|
||||||
|
name: 'draw',
|
||||||
|
version: '1.0.0',
|
||||||
|
description: 'Drawing plugin',
|
||||||
|
}),
|
||||||
|
'utf-8',
|
||||||
|
)
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(pluginRoot, 'commands', 'render.md'),
|
||||||
|
'---\ndescription: Render a drawing.\n---\nRender this drawing.',
|
||||||
|
'utf-8',
|
||||||
|
)
|
||||||
|
await fs.writeFile(
|
||||||
|
marketplaceFile,
|
||||||
|
JSON.stringify({
|
||||||
|
name: 'test-market',
|
||||||
|
owner: { name: 'Test' },
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
name: 'draw',
|
||||||
|
source: './plugins/draw',
|
||||||
|
version: '1.0.0',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
'utf-8',
|
||||||
|
)
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(pluginsDir, 'known_marketplaces.json'),
|
||||||
|
JSON.stringify({
|
||||||
|
'test-market': {
|
||||||
|
source: { source: 'directory', path: marketplaceRoot },
|
||||||
|
installLocation: marketplaceRoot,
|
||||||
|
lastUpdated: new Date(0).toISOString(),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
'utf-8',
|
||||||
|
)
|
||||||
|
|
||||||
|
const settingsPath = path.join(tmpDir, 'settings.json')
|
||||||
|
await fs.writeFile(
|
||||||
|
settingsPath,
|
||||||
|
JSON.stringify({
|
||||||
|
enabledPlugins: {
|
||||||
|
'draw@test-market': false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
'utf-8',
|
||||||
|
)
|
||||||
|
|
||||||
|
const disabledResult = await loadAllPluginsCacheOnly()
|
||||||
|
expect(disabledResult.enabled).toEqual([])
|
||||||
|
expect(disabledResult.disabled).toContainEqual(
|
||||||
|
expect.objectContaining({ source: 'draw@test-market', enabled: false }),
|
||||||
|
)
|
||||||
|
|
||||||
|
await fs.writeFile(
|
||||||
|
settingsPath,
|
||||||
|
JSON.stringify({
|
||||||
|
enabledPlugins: {
|
||||||
|
'draw@test-market': true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
'utf-8',
|
||||||
|
)
|
||||||
|
|
||||||
|
let appState = {
|
||||||
|
plugins: {
|
||||||
|
enabled: [],
|
||||||
|
disabled: disabledResult.disabled,
|
||||||
|
commands: [],
|
||||||
|
errors: [],
|
||||||
|
needsRefresh: true,
|
||||||
|
},
|
||||||
|
mcp: { pluginReconnectKey: 0 },
|
||||||
|
agentDefinitions: { allAgents: [], errors: [] },
|
||||||
|
} as unknown as AppState
|
||||||
|
|
||||||
|
const result = await refreshActivePlugins(updater => {
|
||||||
|
appState = updater(appState)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.enabled_count).toBe(1)
|
||||||
|
expect(result.command_count).toBe(1)
|
||||||
|
expect(result.pluginCommands).toContainEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
name: 'draw:render',
|
||||||
|
description: 'Render a drawing.',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
expect(appState.plugins.commands).toContainEqual(
|
||||||
|
expect.objectContaining({ name: 'draw:render' }),
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import type { PluginError } from '../../types/plugin.js'
|
|||||||
import { logForDebugging } from '../debug.js'
|
import { logForDebugging } from '../debug.js'
|
||||||
import { errorMessage } from '../errors.js'
|
import { errorMessage } from '../errors.js'
|
||||||
import { logError } from '../log.js'
|
import { logError } from '../log.js'
|
||||||
|
import { resetSettingsCache } from '../settings/settingsCache.js'
|
||||||
import { clearAllCaches } from './cacheUtils.js'
|
import { clearAllCaches } from './cacheUtils.js'
|
||||||
import { getPluginCommands } from './loadPluginCommands.js'
|
import { getPluginCommands } from './loadPluginCommands.js'
|
||||||
import { loadPluginHooks } from './loadPluginHooks.js'
|
import { loadPluginHooks } from './loadPluginHooks.js'
|
||||||
@ -72,7 +73,11 @@ export type RefreshActivePluginsResult = {
|
|||||||
export async function refreshActivePlugins(
|
export async function refreshActivePlugins(
|
||||||
setAppState: SetAppState,
|
setAppState: SetAppState,
|
||||||
): Promise<RefreshActivePluginsResult> {
|
): Promise<RefreshActivePluginsResult> {
|
||||||
logForDebugging('refreshActivePlugins: clearing all plugin caches')
|
logForDebugging('refreshActivePlugins: clearing settings and plugin caches')
|
||||||
|
// Desktop plugin toggles are written by the server process while the active
|
||||||
|
// CLI keeps its own settings cache. /reload-plugins is the explicit handoff
|
||||||
|
// point, so re-read settings before resolving enabledPlugins.
|
||||||
|
resetSettingsCache()
|
||||||
clearAllCaches()
|
clearAllCaches()
|
||||||
// Orphan exclusions are session-frozen by default, but /reload-plugins is
|
// Orphan exclusions are session-frozen by default, but /reload-plugins is
|
||||||
// an explicit "disk changed, re-read it" signal — recompute them too.
|
// an explicit "disk changed, re-read it" signal — recompute them too.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user