|
|
@@ -4,7 +4,7 @@ import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
|
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
|
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-test-runtime'
|
|
|
import { createSnapshotStore } from '@deepseek-ai/dsh-client-store'
|
|
|
-import { SubagentLimitsCard, type SubagentLimitsCardProps } from '../src/client/SubagentLimitsCard.tsx'
|
|
|
+import { SubagentCard, type SubagentCardProps } from '../src/client/SubagentCard.tsx'
|
|
|
import type { SubagentLimitsCardState } from '../src/client/subagent-limits-card-controller.ts'
|
|
|
import { AgentLoopCard } from '../src/client/AgentLoopCard.tsx'
|
|
|
import type { AgentLoopCardProps } from '../src/client/AgentLoopCard.tsx'
|
|
|
@@ -14,8 +14,6 @@ import { ConfigurablePluginsTab } from '../src/client/ConfigurablePluginsTab.tsx
|
|
|
import type { ConfigurablePluginsTabProps } from '../src/client/ConfigurablePluginsTab.tsx'
|
|
|
import { PluginsSettingsSection } from '../src/client/PluginsSettingsSection.tsx'
|
|
|
import type { PluginsSettingsSectionProps, PluginsSettingsTabEntry } from '../src/client/PluginsSettingsSection.tsx'
|
|
|
-import { SubagentModelSelectionCard } from '../src/client/SubagentModelSelectionCard.tsx'
|
|
|
-import type { SubagentModelSelectionCardProps } from '../src/client/SubagentModelSelectionCard.tsx'
|
|
|
import { WebSearchCard } from '../src/client/WebSearchCard.tsx'
|
|
|
import type { WebSearchCardProps } from '../src/client/WebSearchCard.tsx'
|
|
|
import type { AgentLoopCardState } from '../src/client/agent-loop-card-controller.ts'
|
|
|
@@ -88,17 +86,29 @@ function renderBash(state: Partial<BashCardState> = {}) {
|
|
|
return renderBashCard(state).actions
|
|
|
}
|
|
|
|
|
|
-function renderSubagentModelSelection(state: Partial<SubagentModelSelectionCardState> = {}) {
|
|
|
- const store = createSnapshotStore<SubagentModelSelectionCardState>({
|
|
|
+function renderSubagent(
|
|
|
+ limitState: Partial<SubagentLimitsCardState> = {},
|
|
|
+ modelState: Partial<SubagentModelSelectionCardState> = {},
|
|
|
+ fallback = false,
|
|
|
+) {
|
|
|
+ const limits = createSnapshotStore<SubagentLimitsCardState>({
|
|
|
+ ...settled,
|
|
|
+ maxDepth: field('3'),
|
|
|
+ maxActiveSubagents: field('8'),
|
|
|
+ ...limitState,
|
|
|
+ })
|
|
|
+ const models = createSnapshotStore<SubagentModelSelectionCardState>({
|
|
|
...settled,
|
|
|
enabled: false,
|
|
|
candidates: [],
|
|
|
catalogStatus: 'idle',
|
|
|
catalogPartial: false,
|
|
|
conflicted: false,
|
|
|
- ...state,
|
|
|
+ ...modelState,
|
|
|
})
|
|
|
const actions = {
|
|
|
+ editLimit: vi.fn(),
|
|
|
+ resetLimit: vi.fn(),
|
|
|
toggleEnabled: vi.fn(),
|
|
|
toggleModel: vi.fn(),
|
|
|
retryCatalog: vi.fn(),
|
|
|
@@ -108,10 +118,16 @@ function renderSubagentModelSelection(state: Partial<SubagentModelSelectionCardS
|
|
|
const props = {
|
|
|
...actions,
|
|
|
t,
|
|
|
- useSubagentModelSelectionCard: bindSnapshotSelector(store),
|
|
|
- } as unknown as SubagentModelSelectionCardProps
|
|
|
- render(<SubagentModelSelectionCard {...props} />)
|
|
|
- return actions
|
|
|
+ fallback,
|
|
|
+ useSubagentLimitsCard: bindSnapshotSelector(limits),
|
|
|
+ useSubagentModelSelectionCard: bindSnapshotSelector(models),
|
|
|
+ } as unknown as SubagentCardProps
|
|
|
+ render(<SubagentCard {...props} />)
|
|
|
+ return { actions, limits, models }
|
|
|
+}
|
|
|
+
|
|
|
+function renderSubagentModelSelection(state: Partial<SubagentModelSelectionCardState> = {}) {
|
|
|
+ return renderSubagent({ available: false }, state, true).actions
|
|
|
}
|
|
|
|
|
|
describe('PluginsSettingsSection', () => {
|
|
|
@@ -353,10 +369,10 @@ describe('BashCard', () => {
|
|
|
})
|
|
|
})
|
|
|
|
|
|
-describe('SubagentModelSelectionCard', () => {
|
|
|
+describe('Subagent model selection fields', () => {
|
|
|
it('renders the default-off preference in its staged plugin card', () => {
|
|
|
const actions = renderSubagentModelSelection()
|
|
|
- fireEvent.click(screen.getByText(en.subagentModelSelectionTitle))
|
|
|
+ fireEvent.click(screen.getByText(en.subagentTitle))
|
|
|
|
|
|
const control = screen.getByRole('switch', { name: en.subagentModelSelectionToggle })
|
|
|
expect(control.getAttribute('aria-checked')).toBe('false')
|
|
|
@@ -390,7 +406,7 @@ describe('SubagentModelSelectionCard', () => {
|
|
|
],
|
|
|
catalogStatus: 'ready',
|
|
|
})
|
|
|
- fireEvent.click(screen.getByText(en.subagentModelSelectionTitle))
|
|
|
+ fireEvent.click(screen.getByText(en.subagentTitle))
|
|
|
|
|
|
expect(screen.getByRole('switch').getAttribute('aria-checked')).toBe('true')
|
|
|
expect(screen.getByText('Alpha API', { exact: true })).toBeTruthy()
|
|
|
@@ -402,13 +418,13 @@ describe('SubagentModelSelectionCard', () => {
|
|
|
|
|
|
it('renders directory progress, failures, unavailable routes, and validation', () => {
|
|
|
renderSubagentModelSelection({ enabled: true, catalogStatus: 'loading', invalid: true })
|
|
|
- fireEvent.click(screen.getByText(en.subagentModelSelectionTitle))
|
|
|
+ fireEvent.click(screen.getByText(en.subagentTitle))
|
|
|
expect(screen.getByText(en.subagentModelSelectionLoading)).toBeTruthy()
|
|
|
expect(screen.getByText(en.subagentModelSelectionRequired)).toBeTruthy()
|
|
|
|
|
|
cleanup()
|
|
|
const errorActions = renderSubagentModelSelection({ enabled: true, catalogStatus: 'error' })
|
|
|
- fireEvent.click(screen.getByText(en.subagentModelSelectionTitle))
|
|
|
+ fireEvent.click(screen.getByText(en.subagentTitle))
|
|
|
fireEvent.click(screen.getByRole('button', { name: en.subagentModelSelectionRetry }))
|
|
|
expect(errorActions.retryCatalog).toHaveBeenCalledOnce()
|
|
|
|
|
|
@@ -427,20 +443,20 @@ describe('SubagentModelSelectionCard', () => {
|
|
|
selected: true,
|
|
|
}],
|
|
|
})
|
|
|
- fireEvent.click(screen.getByText(en.subagentModelSelectionTitle))
|
|
|
+ fireEvent.click(screen.getByText(en.subagentTitle))
|
|
|
expect(screen.getByText(en.subagentModelSelectionPartial)).toBeTruthy()
|
|
|
expect(screen.getByText(en.subagentModelSelectionUnavailable)).toBeTruthy()
|
|
|
expect(screen.getByText(en.subagentModelSelectionUnavailableGroup)).toBeTruthy()
|
|
|
|
|
|
cleanup()
|
|
|
renderSubagentModelSelection({ enabled: true, catalogStatus: 'ready' })
|
|
|
- fireEvent.click(screen.getByText(en.subagentModelSelectionTitle))
|
|
|
+ fireEvent.click(screen.getByText(en.subagentTitle))
|
|
|
expect(screen.getByText(en.subagentModelSelectionEmpty)).toBeTruthy()
|
|
|
})
|
|
|
|
|
|
it('distinguishes a stale draft from a rejected save', () => {
|
|
|
renderSubagentModelSelection({ dirty: true, conflicted: true })
|
|
|
- fireEvent.click(screen.getByText(en.subagentModelSelectionTitle))
|
|
|
+ fireEvent.click(screen.getByText(en.subagentTitle))
|
|
|
|
|
|
expect(screen.getByText(en.subagentModelSelectionConflict)).toBeTruthy()
|
|
|
expect(screen.queryByText(en.saveFailed)).toBeNull()
|
|
|
@@ -448,11 +464,11 @@ describe('SubagentModelSelectionCard', () => {
|
|
|
|
|
|
it('stays hidden when unavailable and disables writes when read-only', () => {
|
|
|
renderSubagentModelSelection({ available: false })
|
|
|
- expect(screen.queryByText(en.subagentModelSelectionTitle)).toBeNull()
|
|
|
+ expect(screen.queryByText(en.subagentTitle)).toBeNull()
|
|
|
|
|
|
cleanup()
|
|
|
const actions = renderSubagentModelSelection({ writable: false })
|
|
|
- fireEvent.click(screen.getByText(en.subagentModelSelectionTitle))
|
|
|
+ fireEvent.click(screen.getByText(en.subagentTitle))
|
|
|
const control = screen.getByRole('switch') as HTMLButtonElement
|
|
|
expect(control.disabled).toBe(true)
|
|
|
fireEvent.click(control)
|
|
|
@@ -573,21 +589,114 @@ describe('WebSearchCard', () => {
|
|
|
})
|
|
|
|
|
|
|
|
|
-describe('SubagentLimitsCard', () => {
|
|
|
- it('edits, resets and disables each limit while saving', () => {
|
|
|
- const store = createSnapshotStore<SubagentLimitsCardState>({ ...settled, dirty: true, maxDepth: { ...field('3'), overridden: true }, maxActiveSubagents: { ...field('8'), overridden: true } })
|
|
|
- const actions = cardActions()
|
|
|
- const props = { ...actions, t, useSubagentLimitsCard: bindSnapshotSelector(store) } as unknown as SubagentLimitsCardProps
|
|
|
- render(<SubagentLimitsCard {...props} />)
|
|
|
- fireEvent.click(screen.getByText(en.subagentLimitsTitle))
|
|
|
+describe('SubagentCard', () => {
|
|
|
+ it('shows both sections under one disclosure and save footer', () => {
|
|
|
+ renderSubagent({ dirty: true })
|
|
|
+ fireEvent.click(screen.getByText(en.subagentTitle))
|
|
|
+
|
|
|
+ expect(screen.getByRole('heading', { name: en.subagentLimitsTitle })).toBeTruthy()
|
|
|
+ expect(screen.getByRole('heading', { name: en.subagentModelSelectionTitle })).toBeTruthy()
|
|
|
+ expect(screen.getAllByRole('button', { name: en.save })).toHaveLength(1)
|
|
|
+ expect(screen.getAllByRole('button', { name: en.discard })).toHaveLength(1)
|
|
|
+ })
|
|
|
+
|
|
|
+ it('reveals field rules on demand without changing staged values', () => {
|
|
|
+ renderSubagent({ dirty: true, maxDepth: field('2') })
|
|
|
+ fireEvent.click(screen.getByText(en.subagentTitle))
|
|
|
+ const depthHelp = screen.getByRole('button', { name: en.subagentDepthHelpLabel })
|
|
|
+ const capacityHelp = screen.getByRole('button', { name: en.subagentCapacityHelpLabel })
|
|
|
+ expect(depthHelp.getAttribute('aria-expanded')).toBe('false')
|
|
|
+ expect(screen.queryByText(en.subagentDepthHelp)).toBeNull()
|
|
|
+ expect(screen.queryByText(en.subagentCapacityHelp)).toBeNull()
|
|
|
+
|
|
|
+ fireEvent.click(depthHelp)
|
|
|
+ expect(depthHelp.getAttribute('aria-expanded')).toBe('true')
|
|
|
+ expect(screen.getByRole('region', { name: en.subagentDepthHelpLabel })).toBeTruthy()
|
|
|
+ expect(screen.getByText(en.subagentDepthHelp)).toBeTruthy()
|
|
|
+ expect(screen.getByRole('table', { name: en.subagentDepthHelpLabel })).toBeTruthy()
|
|
|
+ expect(screen.getByRole('row', { name: `0 ${en.subagentDepthZero}` })).toBeTruthy()
|
|
|
+ expect(screen.getByRole('row', { name: `1 ${en.subagentDepthOne}` })).toBeTruthy()
|
|
|
+ expect(screen.getByText(en.subagentDepthOverride)).toBeTruthy()
|
|
|
+ fireEvent.click(capacityHelp)
|
|
|
+ expect(screen.getByText(en.subagentCapacityHelp)).toBeTruthy()
|
|
|
+ fireEvent.click(depthHelp)
|
|
|
+ expect(screen.queryByText(en.subagentDepthHelp)).toBeNull()
|
|
|
+ expect(screen.getByLabelText(en.subagentMaxDepth)).toHaveProperty('value', '2')
|
|
|
+ expect(screen.getByRole('button', { name: en.save })).toHaveProperty('disabled', false)
|
|
|
+ })
|
|
|
+
|
|
|
+ it('keeps validation visible when the rules are collapsed and links it to the input', () => {
|
|
|
+ renderSubagent({ dirty: true, invalid: true, maxDepth: field('1.5', { invalid: true }) })
|
|
|
+ fireEvent.click(screen.getByText(en.subagentTitle))
|
|
|
+ const depth = screen.getByLabelText(en.subagentMaxDepth)
|
|
|
+ const messageId = depth.getAttribute('aria-describedby')!
|
|
|
+ expect(document.getElementById(messageId)?.textContent).toBe(en.subagentDepthInvalid)
|
|
|
+ expect(screen.queryByRole('region', { name: en.subagentDepthHelpLabel })).toBeNull()
|
|
|
+ })
|
|
|
+
|
|
|
+ it('edits and resets limits through the shared card', () => {
|
|
|
+ const { actions, limits } = renderSubagent({
|
|
|
+ dirty: true,
|
|
|
+ maxDepth: field('3', { overridden: true }),
|
|
|
+ maxActiveSubagents: field('8', { overridden: true }),
|
|
|
+ })
|
|
|
+ fireEvent.click(screen.getByText(en.subagentTitle))
|
|
|
fireEvent.change(screen.getByLabelText(en.subagentMaxDepth), { target: { value: '2' } })
|
|
|
fireEvent.change(screen.getByLabelText(en.subagentMaxActive), { target: { value: '12' } })
|
|
|
- expect(actions.edit.mock.calls).toEqual([['maxDepth', '2'], ['maxActiveSubagents', '12']])
|
|
|
+ expect(actions.editLimit.mock.calls).toEqual([['maxDepth', '2'], ['maxActiveSubagents', '12']])
|
|
|
for (const button of screen.getAllByRole('button', { name: en.reset })) fireEvent.click(button)
|
|
|
- expect(actions.resetField.mock.calls).toEqual([['maxDepth'], ['maxActiveSubagents']])
|
|
|
- act(() => { store.set({ ...store.getSnapshot(), saving: true }) })
|
|
|
- expect(screen.getByLabelText(en.subagentMaxDepth)).toHaveProperty('disabled', true)
|
|
|
- act(() => { store.set({ ...store.getSnapshot(), saving: false, writable: false }) })
|
|
|
+ expect(actions.resetLimit.mock.calls).toEqual([['maxDepth'], ['maxActiveSubagents']])
|
|
|
+ act(() => { limits.set({ ...limits.getSnapshot(), writable: false }) })
|
|
|
expect(screen.getByLabelText(en.subagentMaxActive)).toHaveProperty('disabled', true)
|
|
|
})
|
|
|
+
|
|
|
+ it('blocks saving both sections when a model selection is invalid or conflicted', () => {
|
|
|
+ const { models } = renderSubagent({ dirty: true }, { dirty: true, invalid: true })
|
|
|
+ fireEvent.click(screen.getByText(en.subagentTitle))
|
|
|
+ expect(screen.getByRole('button', { name: en.save })).toHaveProperty('disabled', true)
|
|
|
+ act(() => { models.set({ ...models.getSnapshot(), invalid: false, conflicted: true }) })
|
|
|
+ expect(screen.getByRole('button', { name: en.save })).toHaveProperty('disabled', true)
|
|
|
+ })
|
|
|
+
|
|
|
+ it('locks both sections while either is saving and collapses after both settle', () => {
|
|
|
+ const { limits, models } = renderSubagent({ dirty: true }, { dirty: true })
|
|
|
+ fireEvent.click(screen.getByText(en.subagentTitle))
|
|
|
+ act(() => {
|
|
|
+ limits.set({ ...limits.getSnapshot(), saving: true })
|
|
|
+ models.set({ ...models.getSnapshot(), saving: true })
|
|
|
+ })
|
|
|
+ expect(screen.getByLabelText(en.subagentMaxDepth)).toHaveProperty('disabled', true)
|
|
|
+ expect(screen.getByRole('switch')).toHaveProperty('disabled', true)
|
|
|
+ expect(screen.getByRole('button', { name: en.discard })).toHaveProperty('disabled', true)
|
|
|
+ act(() => { limits.set({ ...limits.getSnapshot(), saving: false, dirty: false }) })
|
|
|
+ expect(screen.getByRole('switch')).toHaveProperty('disabled', true)
|
|
|
+ act(() => { models.set({ ...models.getSnapshot(), saving: false, dirty: false }) })
|
|
|
+ expect(screen.queryByRole('switch')).toBeNull()
|
|
|
+ })
|
|
|
+
|
|
|
+ it('keeps a rejected section open after the other section saves', () => {
|
|
|
+ const { limits, models } = renderSubagent({ dirty: true }, { dirty: true })
|
|
|
+ fireEvent.click(screen.getByText(en.subagentTitle))
|
|
|
+ act(() => {
|
|
|
+ limits.set({ ...limits.getSnapshot(), saving: true })
|
|
|
+ models.set({ ...models.getSnapshot(), saving: true })
|
|
|
+ })
|
|
|
+ act(() => {
|
|
|
+ limits.set({ ...limits.getSnapshot(), saving: false, dirty: false })
|
|
|
+ models.set({ ...models.getSnapshot(), saving: false, failed: true })
|
|
|
+ })
|
|
|
+ expect(screen.getByRole('switch')).toBeTruthy()
|
|
|
+ expect(screen.getByText(en.saveFailed)).toBeTruthy()
|
|
|
+ expect(screen.getByRole('button', { name: en.save })).toHaveProperty('disabled', false)
|
|
|
+ })
|
|
|
+
|
|
|
+ it('renders the model-only fallback only when the limits namespace is absent', () => {
|
|
|
+ renderSubagent({}, {}, true)
|
|
|
+ expect(screen.queryByText(en.subagentTitle)).toBeNull()
|
|
|
+ cleanup()
|
|
|
+ renderSubagent({ available: false }, {}, true)
|
|
|
+ fireEvent.click(screen.getByText(en.subagentTitle))
|
|
|
+ expect(screen.getByRole('switch')).toBeTruthy()
|
|
|
+ expect(screen.queryByLabelText(en.subagentMaxDepth)).toBeNull()
|
|
|
+ })
|
|
|
})
|