mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Three fixes to the settings surface. The default never reached settings.yaml. The api-proxy keeps an explicit allowlist of settings namespaces it exposes to configuration clients, and `agent-presets` was never added — so both pickers moved and then silently forgot, which is worse than refusing the control. The host seam was fine all along; only the wire boundary refused. The regression test fails with the namespace removed. The nav row showed the fallback gear and read `Agent preset` in Chinese, matching neither sibling (通用设置 / 模型). It is now 智能体 with the think glyph, the only unused icon in the set whose semantics point at the agent rather than at tuning sliders (Personalization is already the workspace browser's filter control).
243 lines
8.8 KiB
TypeScript
243 lines
8.8 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import { useEffect, useState } from 'react'
|
|
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
|
|
import type { SettingsRootComponentProps } from '../src/client/contract/slots.ts'
|
|
import { SettingsRoot } from '../src/client/SettingsRoot.tsx'
|
|
|
|
afterEach(cleanup)
|
|
|
|
type Row = { id: string; order: number; label: string }
|
|
type Step = { id: string; order: number }
|
|
|
|
/** Slot-content stand-ins: the shell renders whatever the seats contribute. */
|
|
const SEAT_CONTENT: Record<string, string> = {
|
|
'settings.trigger': 'Settings',
|
|
'settings.header': 'Settings Title',
|
|
'settings.action': 'Open configuration file',
|
|
'settings.close': 'Close',
|
|
}
|
|
|
|
function mount({
|
|
wide = true,
|
|
onboardingActive = true,
|
|
rows = [
|
|
{ id: 'general', order: 0, label: 'General' },
|
|
{ id: 'models', order: 10, label: 'Models' },
|
|
{ id: 'agent-presets', order: 20, label: 'Agent presets' },
|
|
],
|
|
steps = [
|
|
{ id: 'welcome', order: -100 },
|
|
{ id: 'credential', order: 0 },
|
|
],
|
|
}: { wide?: boolean; onboardingActive?: boolean; rows?: Row[]; steps?: Step[] } = {}) {
|
|
// Mutable row source standing in for the bound useSections hook; bump()
|
|
// plays a ledger change through the same observable contract.
|
|
let current = rows
|
|
const listeners = new Set<() => void>()
|
|
const renderSlot = vi.fn(
|
|
((key: string, _owner: unknown, opts?: { only?: string }) => {
|
|
if (key === 'settings.section') return <div data-testid={`section-${opts?.only ?? 'all'}`} />
|
|
return SEAT_CONTENT[key]
|
|
}) as SettingsRootComponentProps['renderSlot'],
|
|
)
|
|
const useSessions = ((select: (state: unknown) => unknown) => select(onboardingActive
|
|
? { phase: 'ready', current: undefined, byId: {} }
|
|
: {
|
|
phase: 'ready',
|
|
current: 'active-session',
|
|
byId: { 'active-session': { blank: false } },
|
|
})) as never
|
|
const unusedHook = (() => { throw new Error('unused by SettingsRoot') }) as never
|
|
const props: SettingsRootComponentProps = {
|
|
useSessions,
|
|
useWorkspaces: unusedHook,
|
|
wide,
|
|
useOnboardingSteps: select => select(steps),
|
|
useSections: (select) => {
|
|
const [, force] = useState(0)
|
|
useEffect(() => {
|
|
const listener = () => { force(n => n + 1) }
|
|
listeners.add(listener)
|
|
return () => { listeners.delete(listener) }
|
|
}, [])
|
|
return select(current)
|
|
},
|
|
renderSlot,
|
|
}
|
|
const view = render(<SettingsRoot {...props} />)
|
|
const bump = (next: Row[]) => {
|
|
act(() => {
|
|
current = next
|
|
for (const fn of [...listeners]) fn()
|
|
})
|
|
}
|
|
return { view, renderSlot, bump, listeners }
|
|
}
|
|
|
|
function openPanel() {
|
|
fireEvent.click(screen.getByRole('button', { name: 'Settings' }))
|
|
}
|
|
|
|
describe('SettingsRoot trigger', () => {
|
|
it('renders the trigger seat content as the accessible name (no aria-label of its own)', () => {
|
|
const { renderSlot } = mount()
|
|
const trigger = screen.getByRole('button', { name: 'Settings' })
|
|
expect(trigger.hasAttribute('aria-label')).toBe(false)
|
|
expect(renderSlot).toHaveBeenCalledWith('settings.trigger', { wide: true })
|
|
expect(trigger.getAttribute('aria-expanded')).toBe('false')
|
|
fireEvent.click(trigger)
|
|
expect(screen.getByRole('dialog')).toBeTruthy()
|
|
expect(screen.getByRole('button', { name: 'Settings', expanded: true })).toBeTruthy()
|
|
})
|
|
|
|
it('hands the rail state to the trigger seat', () => {
|
|
const { renderSlot } = mount({ wide: false })
|
|
expect(renderSlot).toHaveBeenCalledWith('settings.trigger', { wide: false })
|
|
})
|
|
})
|
|
|
|
describe('SettingsPanel chrome seats', () => {
|
|
it('names the dialog via aria-labelledby pointing at the header seat node', () => {
|
|
mount()
|
|
openPanel()
|
|
const dialog = screen.getByRole('dialog')
|
|
const titleId = dialog.getAttribute('aria-labelledby')!
|
|
expect(titleId).toBeTruthy()
|
|
const title = document.getElementById(titleId)!
|
|
expect(title.textContent).toBe('Settings Title')
|
|
expect(screen.getByRole('dialog', { name: 'Settings Title' })).toBeTruthy()
|
|
})
|
|
|
|
it('names the close button through the visually-hidden close seat text', () => {
|
|
mount()
|
|
openPanel()
|
|
const close = screen.getByRole('button', { name: 'Close' })
|
|
expect(close.hasAttribute('aria-label')).toBe(false)
|
|
expect(close.textContent).toContain('Close')
|
|
})
|
|
|
|
it('renders header actions before the shell-owned close control', () => {
|
|
const { renderSlot } = mount()
|
|
openPanel()
|
|
expect(screen.getByText('Open configuration file')).toBeTruthy()
|
|
expect(renderSlot).toHaveBeenCalledWith('settings.action', {})
|
|
})
|
|
})
|
|
|
|
describe('SettingsPanel close paths', () => {
|
|
it('closes via the header button', () => {
|
|
mount()
|
|
openPanel()
|
|
fireEvent.click(screen.getByRole('button', { name: 'Close' }))
|
|
expect(screen.queryByRole('dialog')).toBeNull()
|
|
})
|
|
|
|
it('closes via a mask click', () => {
|
|
mount()
|
|
openPanel()
|
|
const dialog = screen.getByRole('dialog')
|
|
fireEvent.click(dialog.parentElement!.firstElementChild!)
|
|
expect(screen.queryByRole('dialog')).toBeNull()
|
|
})
|
|
|
|
it('closes via document-level Escape and unhooks the listener with the panel', () => {
|
|
mount()
|
|
openPanel()
|
|
fireEvent.keyDown(document, { key: 'Escape' })
|
|
expect(screen.queryByRole('dialog')).toBeNull()
|
|
// Ignored while closed (listener removed with the panel) and non-Escape
|
|
// keys are ignored while open.
|
|
fireEvent.keyDown(document, { key: 'Escape' })
|
|
openPanel()
|
|
fireEvent.keyDown(document, { key: 'Enter' })
|
|
expect(screen.getByRole('dialog')).toBeTruthy()
|
|
})
|
|
|
|
it('lands focus on the close button when the dialog opens', () => {
|
|
mount()
|
|
openPanel()
|
|
expect(document.activeElement).toBe(screen.getByRole('button', { name: 'Close' }))
|
|
})
|
|
})
|
|
|
|
describe('SettingsPanel navigation', () => {
|
|
it('projects rows, marks the first active, and renders only that section', () => {
|
|
mount()
|
|
openPanel()
|
|
expect(screen.getByRole('button', { name: 'General' }).getAttribute('aria-current')).toBe('true')
|
|
expect(screen.getByRole('button', { name: 'Models' }).getAttribute('aria-current')).toBeNull()
|
|
expect(screen.getByTestId('section-general')).toBeTruthy()
|
|
})
|
|
|
|
it('switches the rendered section on nav click', () => {
|
|
mount()
|
|
openPanel()
|
|
fireEvent.click(screen.getByRole('button', { name: 'Models' }))
|
|
expect(screen.getByRole('button', { name: 'Models' }).getAttribute('aria-current')).toBe('true')
|
|
expect(screen.getByTestId('section-models')).toBeTruthy()
|
|
expect(screen.queryByTestId('section-general')).toBeNull()
|
|
})
|
|
|
|
it('mounts onboarding steps in order and transfers ownership only on completion', () => {
|
|
const { renderSlot } = mount()
|
|
const first = renderSlot.mock.calls.find(call => call[0] === 'settings.onboarding')
|
|
expect(first?.[1]).toMatchObject({ stepId: 'welcome' })
|
|
expect(first?.[2]).toEqual({ only: 'welcome' })
|
|
act(() => {
|
|
(first?.[1] as { complete: () => void }).complete()
|
|
;(first?.[1] as { complete: () => void }).complete()
|
|
})
|
|
const onboardingCalls = renderSlot.mock.calls.filter(call => call[0] === 'settings.onboarding')
|
|
const second = onboardingCalls.at(-1)
|
|
expect(second?.[1]).toMatchObject({ stepId: 'credential' })
|
|
expect(second?.[2]).toEqual({ only: 'credential' })
|
|
|
|
act(() => {
|
|
(second?.[1] as { openSection: (id: string) => void }).openSection('models')
|
|
})
|
|
expect(screen.getByRole('dialog')).toBeTruthy()
|
|
expect(screen.getByTestId('section-models')).toBeTruthy()
|
|
|
|
cleanup()
|
|
const inactive = mount({ onboardingActive: false }).renderSlot.mock.calls
|
|
.filter(call => call[0] === 'settings.onboarding')
|
|
expect(inactive).toHaveLength(0)
|
|
})
|
|
|
|
it('makes the underlying application inert while onboarding owns the viewport', () => {
|
|
const appRoot = document.createElement('div')
|
|
appRoot.id = 'root'
|
|
document.body.append(appRoot)
|
|
const { view } = mount()
|
|
expect(appRoot.inert).toBe(true)
|
|
view.unmount()
|
|
expect(appRoot.inert).toBe(false)
|
|
appRoot.remove()
|
|
})
|
|
|
|
it('falls back to the first row when the active entry unregisters', () => {
|
|
const { bump } = mount()
|
|
openPanel()
|
|
fireEvent.click(screen.getByRole('button', { name: 'Models' }))
|
|
bump([{ id: 'general', order: 0, label: 'General' }])
|
|
expect(screen.queryByRole('button', { name: 'Models' })).toBeNull()
|
|
expect(screen.getByTestId('section-general')).toBeTruthy()
|
|
})
|
|
|
|
it('renders an empty content column when the ledger is empty', () => {
|
|
const { renderSlot } = mount({ rows: [] })
|
|
openPanel()
|
|
expect(screen.getByRole('dialog')).toBeTruthy()
|
|
const sectionCalls = renderSlot.mock.calls.filter(c => c[0] === 'settings.section')
|
|
expect(sectionCalls).toHaveLength(0)
|
|
})
|
|
|
|
it('drops the ledger subscription on unmount', () => {
|
|
const { view, listeners } = mount()
|
|
expect(listeners.size).toBe(1)
|
|
view.unmount()
|
|
expect(listeners.size).toBe(0)
|
|
})
|
|
})
|