mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
fix: refresh server plugin skills after enable toggles
The previous hot-reload fix covered the running CLI process, but empty desktop sessions build their slash skill list through the server-side /api/skills path. If the server had already cached disabled plugin settings, /api/plugins/reload could still reload plugin state from stale settings and /api/skills stayed empty until a real chat spawned a fresh CLI. Reset the server settings cache at the plugin reload boundary before clearing plugin caches. This keeps the desktop server, active CLI reload, and empty-session skills API aligned on enable-after-disable transitions. Constraint: EmptySession uses /api/skills, not the active CLI slash-command cache Rejected: Rely on starting a real CLI session to repopulate skills | delays visibility and makes empty-session slash commands stale Confidence: high Scope-risk: narrow Directive: Plugin reload paths must invalidate settings before resolving enabledPlugins Tested: bun test src/server/__tests__/skills.test.ts src/server/__tests__/plugins.test.ts Tested: bun run check:server Tested: cd desktop && bun run test --run src/pages/EmptySession.test.tsx src/stores/pluginStore.test.ts
This commit is contained in:
parent
70dc9ab111
commit
6ec8ae626b
@ -3,6 +3,10 @@ import * as fs from 'node:fs/promises'
|
||||
import * as os from 'node:os'
|
||||
import * as path from 'node:path'
|
||||
import { getCwdState, setCwdState } from '../../bootstrap/state.js'
|
||||
import { clearInstalledPluginsCache } from '../../utils/plugins/installedPluginsManager.js'
|
||||
import { clearPluginCache } from '../../utils/plugins/pluginLoader.js'
|
||||
import { resetSettingsCache } from '../../utils/settings/settingsCache.js'
|
||||
import { handlePluginsApi } from '../api/plugins.js'
|
||||
import { handleSkillsApi } from '../api/skills.js'
|
||||
|
||||
let tmpHome: string
|
||||
@ -21,6 +25,16 @@ function makeRequest(urlStr: string): { req: Request; url: URL; segments: string
|
||||
}
|
||||
}
|
||||
|
||||
function makePluginReloadRequest(): { req: Request; url: URL; segments: string[] } {
|
||||
const url = new URL('/api/plugins/reload', 'http://localhost:3456')
|
||||
const req = new Request(url.toString(), { method: 'POST' })
|
||||
return {
|
||||
req,
|
||||
url,
|
||||
segments: url.pathname.split('/').filter(Boolean),
|
||||
}
|
||||
}
|
||||
|
||||
async function writeSkill(root: string, skillName: string, content: string): Promise<void> {
|
||||
const skillDir = path.join(root, skillName)
|
||||
await fs.mkdir(skillDir, { recursive: true })
|
||||
@ -39,9 +53,15 @@ describe('Skills API', () => {
|
||||
process.env.USERPROFILE = tmpHome
|
||||
process.env.CLAUDE_CONFIG_DIR = path.join(tmpHome, '.claude')
|
||||
setCwdState(tmpHome)
|
||||
clearInstalledPluginsCache()
|
||||
clearPluginCache('skills-api-test-setup')
|
||||
resetSettingsCache()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
clearInstalledPluginsCache()
|
||||
clearPluginCache('skills-api-test-teardown')
|
||||
resetSettingsCache()
|
||||
if (originalHome === undefined) {
|
||||
delete process.env.HOME
|
||||
} else {
|
||||
@ -148,4 +168,115 @@ describe('Skills API', () => {
|
||||
expect.objectContaining({ path: 'SKILL.md', body: 'child body' }),
|
||||
)
|
||||
})
|
||||
|
||||
it('lists plugin skills after reload rereads an external enable toggle', async () => {
|
||||
const marketplaceRoot = path.join(tmpHome, 'marketplace-root')
|
||||
const pluginRoot = path.join(marketplaceRoot, 'plugins', 'draw')
|
||||
const pluginsDir = path.join(tmpHome, '.claude', '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, 'skills', 'render'), { 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, 'skills', 'render', 'SKILL.md'),
|
||||
[
|
||||
'---',
|
||||
'description: Render with the drawing plugin.',
|
||||
'---',
|
||||
'',
|
||||
'# Render',
|
||||
].join('\n'),
|
||||
'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(tmpHome, '.claude', 'settings.json')
|
||||
await fs.writeFile(
|
||||
settingsPath,
|
||||
JSON.stringify({
|
||||
enabledPlugins: {
|
||||
'draw@test-market': false,
|
||||
},
|
||||
}),
|
||||
'utf-8',
|
||||
)
|
||||
|
||||
const initial = makeRequest('/api/skills')
|
||||
const initialRes = await handleSkillsApi(initial.req, initial.url, initial.segments)
|
||||
const initialBody = await initialRes.json() as {
|
||||
skills: Array<{ name: string; source: string }>
|
||||
}
|
||||
expect(initialBody.skills).not.toContainEqual(
|
||||
expect.objectContaining({ name: 'draw:render', source: 'plugin' }),
|
||||
)
|
||||
|
||||
await fs.writeFile(
|
||||
settingsPath,
|
||||
JSON.stringify({
|
||||
enabledPlugins: {
|
||||
'draw@test-market': true,
|
||||
},
|
||||
}),
|
||||
'utf-8',
|
||||
)
|
||||
|
||||
const reload = makePluginReloadRequest()
|
||||
const reloadRes = await handlePluginsApi(reload.req, reload.url, reload.segments)
|
||||
expect(reloadRes.status).toBe(200)
|
||||
|
||||
const after = makeRequest('/api/skills')
|
||||
const afterRes = await handleSkillsApi(after.req, after.url, after.segments)
|
||||
const afterBody = await afterRes.json() as {
|
||||
skills: Array<{ name: string; source: string; description: string }>
|
||||
}
|
||||
|
||||
expect(afterBody.skills).toContainEqual(
|
||||
expect.objectContaining({
|
||||
name: 'draw:render',
|
||||
source: 'plugin',
|
||||
description: 'Render with the drawing plugin.',
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@ -28,6 +28,7 @@ import { getPluginCommands } from '../../utils/plugins/loadPluginCommands.js'
|
||||
import { clearPluginCacheExclusions } from '../../utils/plugins/orphanedPluginFilter.js'
|
||||
import { parseFrontmatter } from '../../utils/frontmatterParser.js'
|
||||
import { extractDescriptionFromMarkdown } from '../../utils/markdownConfigLoader.js'
|
||||
import { resetSettingsCache } from '../../utils/settings/settingsCache.js'
|
||||
import type {
|
||||
PluginInstallationEntry,
|
||||
PluginScope,
|
||||
@ -230,6 +231,7 @@ export class PluginService {
|
||||
}
|
||||
|
||||
async reloadPlugins(cwd?: string): Promise<ApiPluginReloadResponse> {
|
||||
resetSettingsCache()
|
||||
clearAllCaches()
|
||||
clearPluginCacheExclusions()
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user