import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; import { useState } from 'react'; import { MemoryRouter, useLocation, useNavigate } from 'react-router-dom'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { Plugins, PluginsView } from '@/pages/Plugins'; import { useAuthStore } from '@/stores/auth'; import { codingPluginsStore } from '@/stores/coding-plugins'; import { codingWorkspaceStore } from '@/stores/coding-workspace'; import { devicePackageStore } from '@/stores/device-packages'; import { pluginMarketplaceStore } from '@/stores/plugin-marketplace'; import type { PluginWorkspaceItem, PluginWorkspaceProjection, } from '@/pages/Plugins/plugin-workspace-model'; const initialAuthState = useAuthStore.getInitialState(); const initialCodingPluginsState = codingPluginsStore.getInitialState(); const initialCodingWorkspaceState = codingWorkspaceStore.getInitialState(); const initialDevicePackageState = devicePackageStore.getInitialState(); const initialMarketplaceState = pluginMarketplaceStore.getInitialState(); afterEach(() => { vi.restoreAllMocks(); act(() => { useAuthStore.setState(initialAuthState, true); codingPluginsStore.setState(initialCodingPluginsState, true); codingWorkspaceStore.setState(initialCodingWorkspaceState, true); devicePackageStore.setState(initialDevicePackageState, true); pluginMarketplaceStore.setState(initialMarketplaceState, true); }); }); const projectPlugin = { id: 'makelore.data-service', version: '1.0.0', contractVersion: 1, requiresBackend: true, displayName: '开发数据服务', description: '为当前项目提供 JSON 数据。', enabled: true, state: 'configuration_required' as const, backend: { status: 'unconfigured' as const }, skills: [{ id: 'data-service', assignedAgentIds: ['agent-a'] }], capabilities: [{ id: 'data-service.documents', operations: [{ id: 'put_document', billing: { mode: 'included' as const, availability: 'available' as const, notice: 'Fixed quotas apply' }, tool: { name: 'data_service_put_document', label: '写入文档', description: '写入项目数据', mutation: 'write' as const, permissions: ['project.data.write'], }, }], }], settingsSurface: 'data-service', }; const officialItem: PluginWorkspaceItem = { key: 'official:makelore.data-service', source: 'official', pluginId: 'makelore.data-service', packageId: null, title: '开发数据服务', summary: '为当前项目提供 JSON 数据。', category: '系统', tags: ['data'], publisher: 'MakeLore', version: '1.0.0', delivery: 'system_included', projectState: 'enabled', projectPolicyStatus: 'current', localEnabled: null, assignedAgentIds: ['agent-a'], assignedAgentNames: ['小明'], billing: 'included', updateAvailable: false, unavailable: false, stale: false, suspended: false, retired: false, deviceReason: null, commands: [ { kind: 'disable_project', projectId: 'project-a', pluginId: 'makelore.data-service' }, { kind: 'open_settings', projectId: 'project-a', pluginId: 'makelore.data-service' }, ], official: { catalog: null, library: null, installation: null, project: projectPlugin, detail: { pluginId: 'makelore.data-service', title: '开发数据服务', summary: '为当前项目提供 JSON 数据。', category: '系统', tags: ['data'], providerDisplayName: 'MakeLore', runtimeKind: 'bundled_typed', runtimeStatus: 'enabled', acquisition: 'system_included', usageBilling: 'included', includedOperationCount: 1, meteredOperationCount: 0, descriptionMarkdown: '详细的数据服务介绍。', permissions: ['读取项目名称'], operations: [], stableVersion: '1.0.0', betaVersion: null, stableRelease: { releaseId: 'data-1', version: '1.0.0' }, betaRelease: null, etag: 'data-1', pricingVersionId: null, stale: false, fetchedAt: 1, }, }, local: null, }; const localItem: PluginWorkspaceItem = { key: 'local:pi-web-search', source: 'local', pluginId: 'pi-web-search', packageId: 'pi-web-search', title: 'Pi Web Search', summary: 'npm:pi-web-search', category: null, tags: ['search'], publisher: null, version: '1.2.3', delivery: 'local_installed', projectState: 'not_applicable', projectPolicyStatus: null, localEnabled: true, assignedAgentIds: [], assignedAgentNames: [], billing: 'none', updateAvailable: false, unavailable: false, stale: false, suspended: false, retired: false, deviceReason: null, commands: [ { kind: 'disable_local', packageId: 'pi-web-search' }, { kind: 'remove_local', packageId: 'pi-web-search' }, ], official: null, local: { schemaVersion: 1, packageId: 'pi-web-search', displayName: 'Pi Web Search', resolvedVersion: '1.2.3', source: { kind: 'npm', requested: 'npm:pi-web-search', resolved: 'npm:pi-web-search@1.2.3' }, kind: 'mixed', skillEntries: [{ id: 'search', entryPath: 'skills/search/SKILL.md' }], extensionEntries: ['extensions/search.ts'], enabled: true, confirmedExecutableCode: true, installedAt: '2026-09-03T00:00:00Z', }, }; function projection(selected: PluginWorkspaceItem | null = null): PluginWorkspaceProjection { return { items: [officialItem, localItem], selected, counts: { all: 2, available: 0, mine: 2, enabled: 2, update: 0, unavailable: 0 }, notices: [], }; } function props(selected: PluginWorkspaceItem | null = null) { return { projection: projection(selected), activeProjectId: 'project-a', activeProjectName: 'Project A', sourceErrors: [], sourceLoading: [], dataService: null, dataServicePending: {}, activeTab: 'mine' as const, onTabChange: vi.fn(), onSelect: vi.fn(), onCommand: vi.fn(), isCommandPending: vi.fn().mockReturnValue(false), onConfigureDataService: vi.fn(), onResetDataService: vi.fn(), onRemoveDataServiceCollection: vi.fn(), onRemoveDataServiceProject: vi.fn(), }; } function PluginsRouteHarness() { const location = useLocation(); const navigate = useNavigate(); return ( <>
{location.pathname}{location.search}
); } function preparePluginsRouteState() { useAuthStore.setState({ user: null }); codingWorkspaceStore.setState({ activeProjectId: null, activeProject: null, config: null, loadState: 'ready', }); codingPluginsStore.setState({ projection: null, loadState: 'ready', error: null }); devicePackageStore.setState({ index: { schemaVersion: 1, generation: 1, packages: [] }, state: 'ready', error: null, }); pluginMarketplaceStore.setState({ accountKey: null, catalog: null, catalogState: 'ready', catalogError: null, library: null, libraryState: 'idle', libraryError: null, installations: {}, details: {}, detailState: {}, }); vi.spyOn(pluginMarketplaceStore.getState(), 'loadCatalog').mockResolvedValue(undefined); vi.spyOn(devicePackageStore.getState(), 'load').mockResolvedValue(undefined); vi.spyOn(codingPluginsStore.getState(), 'load').mockResolvedValue(undefined); } describe('PluginsView', () => { it('renders a direct Skill-like list in the project drawer without filters or refresh', () => { const view = props(); render(); expect(screen.getByTestId('plugins-page')).toBeVisible(); expect(screen.queryByRole('heading', { name: '插件', level: 1 })).not.toBeInTheDocument(); expect(screen.getByRole('tab', { name: /可选/ })).toHaveAttribute('aria-selected', 'false'); expect(screen.getByRole('tab', { name: /已安装/ })).toHaveAttribute('aria-selected', 'true'); fireEvent.mouseDown(screen.getByRole('tab', { name: /可选/ }), { button: 0, ctrlKey: false }); expect(view.onTabChange).toHaveBeenCalledWith('available'); expect(screen.getByTestId('plugin-list')).toHaveClass('space-y-3'); expect(screen.queryByRole('button', { name: /刷新/ })).not.toBeInTheDocument(); expect(screen.queryByRole('searchbox')).not.toBeInTheDocument(); expect(screen.queryByRole('combobox')).not.toBeInTheDocument(); expect(screen.getByRole('button', { name: '查看开发数据服务详情' })).toBeVisible(); expect(screen.getByRole('button', { name: '开发数据服务:已添加到项目' })).toBeDisabled(); expect(screen.getByRole('button', { name: 'Pi Web Search:已安装' })).toBeDisabled(); expect(screen.getByText('Skill + 插件')).toBeVisible(); }); it('keeps the production URL and fallback notice aligned without exposing query controls', async () => { preparePluginsRouteState(); render( , ); await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent( '/project-config/plugins?scope=all&source=all&state=available', )); expect(screen.queryByLabelText('范围')).not.toBeInTheDocument(); expect(screen.getByText('当前没有打开的项目,已显示全部插件。')).toBeVisible(); const activeProject = { id: 'project-a', name: 'Project A', createdAt: '2026-09-03T00:00:00Z', updatedAt: '2026-09-03T00:00:00Z', lastOpenedAt: '2026-09-03T00:00:00Z', }; act(() => codingWorkspaceStore.setState({ activeProjectId: activeProject.id, activeProject })); fireEvent.click(screen.getByRole('button', { name: '打开裸插件地址' })); await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent( '/project-config/plugins?scope=all&source=all&state=available', )); expect(screen.queryByLabelText('范围')).not.toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '打开非法插件地址' })); await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent( '/project-config/plugins?scope=all&source=all&state=available', )); expect(screen.queryByLabelText('范围')).not.toBeInTheDocument(); act(() => codingWorkspaceStore.setState({ activeProjectId: null, activeProject: null })); await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent( '/project-config/plugins?scope=all&source=all&state=available', )); expect(screen.queryByLabelText('范围')).not.toBeInTheDocument(); expect(screen.queryByText('当前没有打开的项目,已显示全部插件。')).not.toBeInTheDocument(); }); it('keeps installed rows visible when individual sources fail to load', async () => { const activeProject = { id: 'project-a', name: 'Project A', createdAt: '2026-09-03T00:00:00Z', updatedAt: '2026-09-03T00:00:00Z', lastOpenedAt: '2026-09-03T00:00:00Z', }; useAuthStore.setState({ user: null }); codingWorkspaceStore.setState({ activeProjectId: activeProject.id, activeProject, config: null, loadState: 'ready', }); codingPluginsStore.setState({ projectId: activeProject.id, projection: { schemaVersion: 1, project: { localProjectId: activeProject.id, durableProjectId: 'durable-a' }, policyStatus: 'current', unknownPluginIds: ['makelore.retained'], items: [projectPlugin], }, loadState: 'error', error: 'project refresh failed', }); devicePackageStore.setState({ index: { schemaVersion: 1, generation: 8, packages: [localItem.local!] }, state: 'error', error: 'device refresh failed', }); pluginMarketplaceStore.setState({ accountKey: null, catalog: null, catalogState: 'ready', catalogError: null, library: null, libraryState: 'idle', libraryError: null, installations: {}, details: {}, detailState: {}, }); vi.spyOn(pluginMarketplaceStore.getState(), 'loadCatalog').mockResolvedValue(undefined); vi.spyOn(devicePackageStore.getState(), 'load').mockResolvedValue(undefined); vi.spyOn(codingPluginsStore.getState(), 'load').mockResolvedValue(undefined); render(); for (const name of ['开发数据服务', 'Pi Web Search', 'makelore.retained']) { expect(await screen.findByRole('heading', { name })).toBeVisible(); } expect(screen.getAllByRole('alert')).toHaveLength(2); expect(screen.queryByText('缓存')).not.toBeInTheDocument(); }); it('clears legacy source filters when switching to installed so local Skills remain visible', async () => { preparePluginsRouteState(); pluginMarketplaceStore.setState({ catalog: { items: [{ pluginId: 'makelore.notes', title: 'Notes', summary: 'Write project notes', category: 'productivity', tags: ['notes'], providerDisplayName: 'MakeLore', runtimeKind: 'skill_only', runtimeStatus: 'enabled', acquisition: 'free', usageBilling: 'included', includedOperationCount: 1, meteredOperationCount: 0, stableVersion: '1.0.0', betaVersion: null, }], nextCursor: null, total: 1, catalogGeneration: 1, etag: 'catalog-1', pricingVersionId: null, stale: false, fetchedAt: 1, }, }); devicePackageStore.setState({ index: { schemaVersion: 1, generation: 2, packages: [{ ...localItem.local!, packageId: 'local-writing', displayName: 'Local Writing', source: { kind: 'file', requested: '/Users/example/.codex/skills/local-writing/SKILL.md', resolved: '/Users/example/.codex/skills/local-writing/SKILL.md', }, kind: 'skill-only', skillEntries: [{ id: 'local-writing', entryPath: 'SKILL.md' }], extensionEntries: [], confirmedExecutableCode: false, }], }, }); render( , ); expect(await screen.findByRole('heading', { name: 'Notes' })).toBeVisible(); fireEvent.mouseDown(screen.getByRole('tab', { name: /已安装/ }), { button: 0, ctrlKey: false }); await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent( '/project-config/plugins?scope=all&source=all&state=mine', )); expect(await screen.findByRole('heading', { name: 'Local Writing' })).toBeVisible(); expect(screen.getByText('Skill')).toBeVisible(); }); it('attempts a rejected official detail load once per open and retries only after close and reopen', async () => { preparePluginsRouteState(); pluginMarketplaceStore.setState({ catalog: { items: [{ pluginId: 'makelore.notes', title: 'Notes', summary: 'Write project notes', category: 'productivity', tags: ['notes'], providerDisplayName: 'MakeLore', runtimeKind: 'skill_only', runtimeStatus: 'enabled', acquisition: 'free', usageBilling: 'token_point', includedOperationCount: 0, meteredOperationCount: 1, stableVersion: '2.0.0', betaVersion: null, }], nextCursor: null, total: 1, catalogGeneration: 1, etag: 'catalog-1', pricingVersionId: 'pricing-1', stale: false, fetchedAt: 1, }, }); let rejectFirstAttempt: (reason?: unknown) => void = () => undefined; const firstAttempt = new Promise((_resolve, reject) => { rejectFirstAttempt = reject; }); const loadDetail = vi.spyOn(pluginMarketplaceStore.getState(), 'loadDetail') .mockImplementationOnce(async () => await firstAttempt) .mockResolvedValue(undefined); render( , ); await waitFor(() => expect(loadDetail).toHaveBeenCalledTimes(1)); await act(async () => { pluginMarketplaceStore.setState((state) => ({ detailState: { ...state.detailState, 'makelore.notes': 'error' }, catalogError: 'detail offline', })); rejectFirstAttempt(new Error('detail offline')); await Promise.resolve(); }); await act(async () => { await Promise.resolve(); await Promise.resolve(); }); expect(loadDetail).toHaveBeenCalledTimes(1); expect(screen.getByRole('alert', { hidden: true })).toHaveTextContent('插件详情加载失败。 detail offline'); fireEvent.click(screen.getByRole('button', { name: '关闭插件详情' })); await waitFor(() => expect(screen.queryByRole('dialog', { name: 'Notes' })).not.toBeInTheDocument()); fireEvent.click(screen.getByRole('button', { name: '查看Notes详情' })); await waitFor(() => expect(loadDetail).toHaveBeenCalledTimes(2)); }); it('renders the capability-first list with details and contextual actions', () => { const view = props(); view.sourceErrors = [ { source: 'catalog', message: 'catalog offline' }, { source: 'project', message: 'project unavailable' }, ]; render(); expect(screen.getByRole('heading', { name: '插件' })).toBeVisible(); expect(screen.getByText('插件与 Skill 统一管理;云端能力在这里下发,本机安装的 Skill 也会集中显示。')).toBeVisible(); expect(screen.getByText('开发数据服务')).toBeVisible(); expect(screen.getByText('Pi Web Search')).toBeVisible(); expect(screen.getByRole('button', { name: '查看开发数据服务详情' })).toBeVisible(); expect(screen.getByRole('button', { name: '开发数据服务:已添加到项目' })).toBeDisabled(); expect(screen.getAllByRole('alert')).toHaveLength(2); expect(screen.queryByRole('button', { name: /刷新/ })).not.toBeInTheDocument(); expect(screen.queryByRole('searchbox')).not.toBeInTheDocument(); expect(screen.queryByRole('combobox')).not.toBeInTheDocument(); }); it('labels an unacquired code-owned bundled plugin as supplied by MakeLore instead of not downloaded', () => { const scaffoldItem: PluginWorkspaceItem = { ...officialItem, key: 'official:makelore.project-scaffold', pluginId: 'makelore.project-scaffold', title: 'MakeLore 项目脚手架', delivery: 'not_acquired', projectState: 'disabled', assignedAgentIds: [], assignedAgentNames: [], commands: [], official: { catalog: null, detail: null, library: null, installation: null, project: { ...projectPlugin, id: 'makelore.project-scaffold', displayName: 'MakeLore 项目脚手架', enabled: false, state: 'disabled', }, }, }; const view = props(scaffoldItem); view.projection = { items: [scaffoldItem], selected: scaffoldItem, counts: { all: 1, available: 1, mine: 0, enabled: 0, update: 0, unavailable: 0 }, notices: [], }; render(); expect(screen.getByRole('button', { name: 'MakeLore 项目脚手架:随应用提供', hidden: true })).toBeDisabled(); const dialog = screen.getByRole('dialog', { name: 'MakeLore 项目脚手架' }); expect(dialog).toHaveTextContent('此插件随应用提供,添加到项目后即可使用。'); expect(dialog).not.toHaveTextContent('未下载官方包'); expect(dialog).not.toHaveTextContent('生效范围'); }); it('shows one dialog detail surface, confirms destructive commands, and embeds Data Service settings', () => { const view = props(officialItem); render(); const dialog = screen.getByRole('dialog', { name: '开发数据服务' }); expect(dialog).toHaveTextContent('详细的数据服务介绍。'); expect(dialog).toHaveTextContent('写入文档'); expect(dialog).toHaveTextContent('写入项目数据'); expect(dialog).not.toHaveTextContent('读取项目名称'); expect(dialog).not.toHaveTextContent('data-service.documents'); expect(dialog).not.toHaveTextContent('按平台包含'); expect(dialog).not.toHaveTextContent('版本 1.0.0'); expect(dialog).toHaveTextContent('使用设置'); expect(dialog).toHaveTextContent('创建开发数据空间'); fireEvent.click(screen.getByRole('button', { name: '从项目移除开发数据服务' })); expect(view.onCommand).not.toHaveBeenCalled(); expect(screen.getByRole('alertdialog')).toHaveTextContent('Project A'); fireEvent.click(screen.getByRole('button', { name: '确认从项目移除' })); expect(view.onCommand).toHaveBeenCalledWith({ kind: 'disable_project', projectId: 'project-a', pluginId: 'makelore.data-service', }); }); it('describes local plugins by capability without exposing implementation details', () => { const scriptSkill: PluginWorkspaceItem = { ...localItem, key: 'local:makelore-project-scaffold', pluginId: 'makelore-project-scaffold', packageId: 'makelore-project-scaffold', title: 'MakeLore 项目初始化', local: { ...localItem.local!, packageId: 'makelore-project-scaffold', displayName: 'MakeLore 项目初始化', kind: 'skill-only', skillEntries: [{ id: 'makelore-project-scaffold', entryPath: 'skills/makelore-project-scaffold/SKILL.md', }], extensionEntries: [], }, }; render(); const dialog = screen.getByRole('dialog', { name: 'MakeLore 项目初始化' }); expect(dialog).toHaveTextContent('这是用户自行安装在本机的 Skill,已统一收纳在插件模块中。'); expect(dialog).not.toHaveTextContent('Skill 脚本'); expect(dialog).not.toHaveTextContent('Pi extension'); expect(dialog).not.toHaveTextContent('完整桌面权限'); }); it('shows a friendly unavailable message and keeps the applicable sign-in action', () => { const unavailableItem: PluginWorkspaceItem = { ...officialItem, title: 'Notes', delivery: 'account_unknown', projectState: 'not_applicable', version: '1.0.0', stale: true, unavailable: true, suspended: true, retired: true, deviceReason: '当前客户端与新版本不兼容', commands: [{ kind: 'sign_in' }], official: { ...officialItem.official!, installation: { status: 'unavailable', pluginId: 'makelore.notes', version: '1.0.0', channel: 'stable', reason: '当前客户端与新版本不兼容', }, project: null, }, }; const view = props(unavailableItem); render(); const dialog = screen.getByRole('dialog', { name: 'Notes' }); expect(dialog).toHaveTextContent('这项能力目前暂不可用'); expect(dialog).not.toHaveTextContent('缓存'); expect(dialog).not.toHaveTextContent('已暂停'); expect(dialog).not.toHaveTextContent('已退役'); expect(dialog).not.toHaveTextContent('1.0.0'); expect(dialog).not.toHaveTextContent('当前客户端与新版本不兼容'); fireEvent.click(screen.getByRole('button', { name: '登录后添加Notes' })); expect(view.onCommand).toHaveBeenCalledWith({ kind: 'sign_in' }); }); it('keeps Marketplace details capability-focused without exposing operation or pricing fields', () => { const detailItem: PluginWorkspaceItem = { ...officialItem, title: 'Notes', summary: '记录、整理并回顾项目笔记。', projectState: 'not_applicable', assignedAgentIds: [], assignedAgentNames: [], official: { ...officialItem.official!, project: null, detail: { ...officialItem.official!.detail!, pluginId: 'makelore.notes', title: 'Notes', operations: [{ capabilityId: 'notes', operation: 'write', executionMode: 'job', billing: { mode: 'platform_metered', status: null, notice: '每次由服务端结算', entitlementScope: 'account', unitName: 'request', unitSize: 2, ratePoints: '3', minimumChargePoints: '1', roundingMode: 'ceil', pricingVersion: 'pricing-7', }, enabled: true, }], }, }, }; const view = { ...props(detailItem), activeProjectId: null, activeProjectName: null, }; render(); const dialog = screen.getByRole('dialog', { name: 'Notes' }); expect(dialog).toHaveTextContent('记录、整理并回顾项目笔记。'); expect(dialog).not.toHaveTextContent('notes'); expect(dialog).not.toHaveTextContent('write'); expect(dialog).not.toHaveTextContent('每次由服务端结算'); expect(dialog).not.toHaveTextContent('request'); expect(dialog).not.toHaveTextContent('Token Point'); expect(dialog).not.toHaveTextContent('pricing-7'); }); it.each([ ['stale'], ['unavailable'], ] as const)('does not expose the %s policy implementation state in details', (projectPolicyStatus) => { const policyItem: PluginWorkspaceItem = { ...officialItem, projectPolicyStatus }; render(); const dialog = screen.getByRole('dialog', { name: '开发数据服务' }); expect(dialog).not.toHaveTextContent('当前项目策略使用缓存'); expect(dialog).not.toHaveTextContent('当前项目策略不可用'); }); it('deduplicates human-facing capability descriptions and hides their technical identity', () => { const capabilityTool = { name: 'notes_write', label: '整理笔记', description: '整理并保存项目笔记。', mutation: 'write' as const, permissions: ['project.notes.write'], }; const projectPolicy = { ...projectPlugin, capabilities: [{ id: 'notes.internal', operations: [ { id: 'write_internal', billing: { mode: 'platform_metered' as const, availability: 'available' as const, notice: '当前项目策略价格', ratePoints: '7', }, tool: capabilityTool, }, { id: 'write_duplicate', billing: { mode: 'included' as const, availability: 'available' as const, notice: '目录包含读取', }, tool: capabilityTool, }, ], }], }; const detailItem: PluginWorkspaceItem = { ...officialItem, title: 'Notes', official: { ...officialItem.official!, project: projectPolicy, detail: { ...officialItem.official!.detail!, pluginId: 'makelore.notes', title: 'Notes', operations: [ { capabilityId: 'notes', operation: 'write', executionMode: 'synchronous', billing: { mode: 'platform_metered', status: null, notice: '目录价格', entitlementScope: null, unitName: 'catalog-request', unitSize: 1, ratePoints: '3', minimumChargePoints: null, roundingMode: 'ceil', pricingVersion: 'pricing-3', }, enabled: true, }, { capabilityId: 'notes', operation: 'read', executionMode: 'synchronous', billing: { mode: 'included', status: null, notice: '目录包含读取', entitlementScope: null, unitName: null, unitSize: null, ratePoints: null, minimumChargePoints: null, roundingMode: null, pricingVersion: 'pricing-3', }, enabled: true, }, ], }, }, }; render(); const dialog = screen.getByRole('dialog', { name: 'Notes' }); expect(screen.getAllByText('整理笔记')).toHaveLength(1); expect(screen.getAllByText('整理并保存项目笔记。')).toHaveLength(1); expect(dialog).not.toHaveTextContent('notes.internal'); expect(dialog).not.toHaveTextContent('write_internal'); expect(dialog).not.toHaveTextContent('project.notes.write'); expect(dialog).not.toHaveTextContent('当前项目策略价格'); expect(dialog).not.toHaveTextContent('目录价格'); expect(dialog).not.toHaveTextContent('Token Point'); }); it('dispatches non-destructive detail commands directly and closes from the dialog control', () => { const view = props(officialItem); render(); fireEvent.click(screen.getByRole('button', { name: '打开开发数据服务设置' })); expect(view.onCommand).toHaveBeenCalledWith({ kind: 'open_settings', projectId: 'project-a', pluginId: 'makelore.data-service', }); fireEvent.click(screen.getByRole('button', { name: '关闭插件详情' })); expect(view.onSelect).toHaveBeenCalledWith(null); }); it('clears a destructive confirmation when the active project changes', () => { const viewA = props(officialItem); const { rerender } = render(); fireEvent.click(screen.getByRole('button', { name: '从项目移除开发数据服务' })); expect(screen.getByRole('alertdialog')).toHaveTextContent('Project A'); const projectBItem: PluginWorkspaceItem = { ...officialItem, commands: [ { kind: 'disable_project', projectId: 'project-b', pluginId: 'makelore.data-service' }, ], }; const viewB = { ...props(projectBItem), activeProjectId: 'project-b', activeProjectName: 'Project B', }; rerender(); expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument(); expect(viewA.onCommand).not.toHaveBeenCalled(); expect(viewB.onCommand).not.toHaveBeenCalled(); }); it('closes the detail with Escape and returns focus to the originating card', async () => { function Harness() { const [selected, setSelected] = useState(null); const view = props(selected); view.onSelect = vi.fn((key: PluginWorkspaceItem['key'] | null) => { setSelected(key === officialItem.key ? officialItem : null); }); return ; } render(); const trigger = screen.getByRole('button', { name: '查看开发数据服务详情' }); fireEvent.click(trigger); const dialog = screen.getByRole('dialog', { name: '开发数据服务' }); expect(dialog).toBeVisible(); await waitFor(() => expect(dialog.contains(document.activeElement)).toBe(true)); fireEvent.keyDown(dialog, { key: 'Escape' }); await waitFor(() => expect(screen.queryByRole('dialog', { name: '开发数据服务' })).not.toBeInTheDocument()); await waitFor(() => expect(trigger).toHaveFocus()); }); });