mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-26 15:03:34 +08:00
fix: Prevent plugin detail hook mismatch during reload
Plugin actions refresh the selected plugin detail after mutating enabled state. The detail component can enter its loading branch while a plugin is still selected, so every hook must run before that early return. Constraint: React hook order must stay identical across the detail and loading render paths. Rejected: Clear the selected plugin before reload | would hide the current context and add UI churn during short refreshes. Confidence: high Scope-risk: narrow Directive: Keep PluginDetail hooks before loading and empty-state returns when adding detail-derived memoization. Tested: cd desktop && bun run test src/__tests__/pluginsSettings.test.tsx Tested: cd desktop && bun run lint Tested: cd desktop && bun run build Not-tested: Full desktop Vitest suite has pre-existing locale assertion failures unrelated to this plugin fix.
This commit is contained in:
parent
6d40b2befa
commit
77d2cbfab8
@ -1,5 +1,5 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { act, fireEvent, render, screen } from '@testing-library/react'
|
||||||
import '@testing-library/jest-dom'
|
import '@testing-library/jest-dom'
|
||||||
|
|
||||||
import { Settings } from '../pages/Settings'
|
import { Settings } from '../pages/Settings'
|
||||||
@ -338,6 +338,59 @@ describe('Settings > Plugins tab', () => {
|
|||||||
expect(screen.getByText('Uninstall')).toBeInTheDocument()
|
expect(screen.getByText('Uninstall')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('keeps plugin detail hook order stable while the selected plugin reloads', () => {
|
||||||
|
usePluginStore.setState({
|
||||||
|
selectedPlugin: {
|
||||||
|
id: 'github@claude-plugins-official',
|
||||||
|
name: 'github',
|
||||||
|
marketplace: 'claude-plugins-official',
|
||||||
|
scope: 'user',
|
||||||
|
enabled: false,
|
||||||
|
hasErrors: false,
|
||||||
|
isBuiltin: false,
|
||||||
|
description: 'GitHub integration',
|
||||||
|
componentCounts: {
|
||||||
|
commands: 1,
|
||||||
|
agents: 0,
|
||||||
|
skills: 0,
|
||||||
|
hooks: 0,
|
||||||
|
mcpServers: 0,
|
||||||
|
lspServers: 0,
|
||||||
|
},
|
||||||
|
capabilities: {
|
||||||
|
commands: ['review-pr'],
|
||||||
|
agents: [],
|
||||||
|
skills: [],
|
||||||
|
hooks: [],
|
||||||
|
mcpServers: [],
|
||||||
|
lspServers: [],
|
||||||
|
},
|
||||||
|
commandEntries: [
|
||||||
|
{
|
||||||
|
name: 'review-pr',
|
||||||
|
description: 'Review the current pull request.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
agentEntries: [],
|
||||||
|
hookEntries: [],
|
||||||
|
skillEntries: [],
|
||||||
|
mcpServerEntries: [],
|
||||||
|
errors: [],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { container } = render(<Settings />)
|
||||||
|
switchToPluginsTab()
|
||||||
|
|
||||||
|
expect(screen.getByText('GitHub integration')).toBeInTheDocument()
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
usePluginStore.setState({ isDetailLoading: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(container.querySelector('.animate-spin')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
it('navigates plugin skills into the shared Skills page flow', () => {
|
it('navigates plugin skills into the shared Skills page flow', () => {
|
||||||
usePluginStore.setState({
|
usePluginStore.setState({
|
||||||
selectedPlugin: {
|
selectedPlugin: {
|
||||||
|
|||||||
@ -42,6 +42,15 @@ export function PluginDetail() {
|
|||||||
const activeSession = sessions.find((session) => session.id === activeSessionId)
|
const activeSession = sessions.find((session) => session.id === activeSessionId)
|
||||||
const currentWorkDir = activeSession?.workDir || undefined
|
const currentWorkDir = activeSession?.workDir || undefined
|
||||||
|
|
||||||
|
const otherCapabilityItems = useMemo(
|
||||||
|
() =>
|
||||||
|
CAPABILITY_ORDER.map((key) => ({
|
||||||
|
key,
|
||||||
|
items: selectedPlugin?.capabilities[key] ?? [],
|
||||||
|
})),
|
||||||
|
[selectedPlugin],
|
||||||
|
)
|
||||||
|
|
||||||
if (isDetailLoading) {
|
if (isDetailLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center py-12">
|
<div className="flex justify-center py-12">
|
||||||
@ -54,14 +63,6 @@ export function PluginDetail() {
|
|||||||
|
|
||||||
const canMutate = selectedPlugin.scope !== 'managed' && selectedPlugin.scope !== 'builtin'
|
const canMutate = selectedPlugin.scope !== 'managed' && selectedPlugin.scope !== 'builtin'
|
||||||
const canNavigateSharedCapabilities = selectedPlugin.enabled
|
const canNavigateSharedCapabilities = selectedPlugin.enabled
|
||||||
const otherCapabilityItems = useMemo(
|
|
||||||
() =>
|
|
||||||
CAPABILITY_ORDER.map((key) => ({
|
|
||||||
key,
|
|
||||||
items: selectedPlugin.capabilities[key],
|
|
||||||
})),
|
|
||||||
[selectedPlugin],
|
|
||||||
)
|
|
||||||
|
|
||||||
const runAction = async (key: string, fn: () => Promise<string>) => {
|
const runAction = async (key: string, fn: () => Promise<string>) => {
|
||||||
setActionKey(key)
|
setActionKey(key)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user