mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
The desktop app could read plugin-produced skills and agents, but it had no plugin control plane of its own. This adds a dedicated Settings tab backed by server-side plugin APIs so installed plugins can be inspected, enabled, disabled, updated, reloaded, and uninstalled from the WebUI. The implementation also teaches browser-based desktop dev sessions to honor a custom backend URL, which made it possible to run isolated worktree ports for real UI automation. During verification, the long-lived desktop server kept a stale installed-plugin snapshot after external CLI mutations, so cache clearing now resets that session-level plugin installation state as well. Constraint: Desktop WebUI needed an isolated backend URL instead of the hard-coded 127.0.0.1:3456 fallback Constraint: Reuse existing plugin operations and loaders instead of rebuilding plugin lifecycle logic in the desktop layer Rejected: Fold plugin management into Skills or Adapters | mixed unrelated lifecycles and hid plugin-specific health/actions Rejected: Expose only read-only plugin status in desktop | did not satisfy enable-disable-reload-uninstall verification needs Confidence: high Scope-risk: moderate Reversibility: clean Directive: Keep desktop plugin actions routed through the shared plugin operation layer and clear installed-plugin session caches when plugin state changes externally Tested: cd desktop && bun run lint Tested: cd desktop && bun run test -- src/__tests__/pluginsSettings.test.tsx Tested: bun test src/server/__tests__/plugins.test.ts src/server/__tests__/skills.test.ts Tested: Browser automation against isolated ports 15120/38456 covering discord plugin list/detail/disable/apply/enable/update/uninstall flows Not-tested: Full desktop session runtime parity with CLI /reload-plugins AppState refresh beyond the new desktop API path
79 lines
2.4 KiB
TypeScript
79 lines
2.4 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
|
|
import * as fs from 'node:fs/promises'
|
|
import * as os from 'node:os'
|
|
import * as path from 'node:path'
|
|
import { handlePluginsApi } from '../api/plugins.js'
|
|
|
|
let tmpDir: string
|
|
let originalConfigDir: string | undefined
|
|
|
|
function makeRequest(
|
|
method: string,
|
|
urlStr: string,
|
|
body?: Record<string, unknown>,
|
|
): { req: Request; url: URL; segments: string[] } {
|
|
const url = new URL(urlStr, 'http://localhost:3456')
|
|
const init: RequestInit = { method }
|
|
if (body) {
|
|
init.headers = { 'Content-Type': 'application/json' }
|
|
init.body = JSON.stringify(body)
|
|
}
|
|
const req = new Request(url.toString(), init)
|
|
return {
|
|
req,
|
|
url,
|
|
segments: url.pathname.split('/').filter(Boolean),
|
|
}
|
|
}
|
|
|
|
describe('Plugins API', () => {
|
|
beforeEach(async () => {
|
|
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-plugins-api-'))
|
|
originalConfigDir = process.env.CLAUDE_CONFIG_DIR
|
|
process.env.CLAUDE_CONFIG_DIR = tmpDir
|
|
})
|
|
|
|
afterEach(async () => {
|
|
if (originalConfigDir === undefined) {
|
|
delete process.env.CLAUDE_CONFIG_DIR
|
|
} else {
|
|
process.env.CLAUDE_CONFIG_DIR = originalConfigDir
|
|
}
|
|
await fs.rm(tmpDir, { recursive: true, force: true })
|
|
})
|
|
|
|
it('GET /api/plugins returns an empty plugin list for a clean config', async () => {
|
|
const { req, url, segments } = makeRequest('GET', '/api/plugins')
|
|
const res = await handlePluginsApi(req, url, segments)
|
|
|
|
expect(res.status).toBe(200)
|
|
const body = await res.json() as {
|
|
plugins: unknown[]
|
|
marketplaces: unknown[]
|
|
summary: { total: number; enabled: number; errorCount: number }
|
|
}
|
|
|
|
expect(body.plugins).toEqual([])
|
|
expect(Array.isArray(body.marketplaces)).toBe(true)
|
|
expect(body.summary.total).toBe(0)
|
|
expect(body.summary.enabled).toBe(0)
|
|
expect(body.summary.errorCount).toBe(0)
|
|
})
|
|
|
|
it('POST /api/plugins/reload returns numeric counters', async () => {
|
|
const { req, url, segments } = makeRequest('POST', '/api/plugins/reload', {})
|
|
const res = await handlePluginsApi(req, url, segments)
|
|
|
|
expect(res.status).toBe(200)
|
|
const body = await res.json() as {
|
|
ok: boolean
|
|
summary: Record<string, number>
|
|
}
|
|
|
|
expect(body.ok).toBe(true)
|
|
expect(typeof body.summary.enabled).toBe('number')
|
|
expect(typeof body.summary.skills).toBe('number')
|
|
expect(typeof body.summary.errors).toBe('number')
|
|
})
|
|
})
|