The project now has exactly one UI library. Every naive-ui component in
packages/client/src is replaced by its Arco equivalent across 24 views and
roughly 80 components, and naive-ui is dropped from package.json along with
its transitive vueuc / @css-render / vdirs / seemly dependencies.
The migration is rendering-layer only. No store logic, no API contract, and
no /chat-run payload was touched.
Three classes of leftover were invisible to `grep naive-ui` and are now
covered by tests/client/no-naive-ui.test.ts, which asserts all three:
- dead .n-* class names in style blocks
- dead --n-* custom properties
- naive-ui in package.json
Notable behavioural traps handled along the way, recorded in
docs/arco-migration-rules.md:
- a-input-password defaults to masked; passing default-visibility="false"
would have exposed API keys in clear text
- a-upload fires a real XHR unless auto-upload is false
- nested Trigger components fight over the first element child, so the
group-chat settings dropdown was flattened
- Arco renders tooltip default-slot text inline, which was silently
collapsing the chat and group-chat toolbars
126 lines
4.2 KiB
TypeScript
126 lines
4.2 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { flushPromises, mount } from '@vue/test-utils'
|
|
import ArcoVue from '@arco-design/web-vue'
|
|
|
|
const mockSettingsStore = vi.hoisted(() => ({
|
|
sessionReset: { mode: 'both', idle_minutes: 60, at_hour: 0 },
|
|
approvals: { mode: 'manual' },
|
|
memory: { write_approval: false },
|
|
skills: { write_approval: true },
|
|
updateLocal: vi.fn((section: string, values: Record<string, any>) => {
|
|
Object.assign((mockSettingsStore as any)[section], values)
|
|
}),
|
|
saveSection: vi.fn(() => Promise.resolve()),
|
|
}))
|
|
|
|
const mockPrefsStore = vi.hoisted(() => ({
|
|
humanOnly: true,
|
|
setHumanOnly: vi.fn((value: boolean) => {
|
|
mockPrefsStore.humanOnly = value
|
|
}),
|
|
}))
|
|
|
|
vi.mock('@/stores/hermes/settings', () => ({
|
|
useSettingsStore: () => mockSettingsStore,
|
|
}))
|
|
|
|
vi.mock('@/stores/hermes/session-browser-prefs', () => ({
|
|
useSessionBrowserPrefsStore: () => mockPrefsStore,
|
|
}))
|
|
|
|
vi.mock('vue-i18n', () => ({
|
|
useI18n: () => ({
|
|
t: (key: string) => key,
|
|
}),
|
|
}))
|
|
|
|
vi.mock('@/composables/useMessage', () => ({
|
|
useMessage: () => ({
|
|
success: vi.fn(),
|
|
error: vi.fn(),
|
|
}),
|
|
}))
|
|
|
|
import SessionSettings from '@/components/hermes/settings/SessionSettings.vue'
|
|
|
|
const SettingRowStub = {
|
|
props: ['label', 'hint'],
|
|
template: '<div class="setting-row"><div class="setting-row-label">{{ label }}</div><slot /></div>',
|
|
}
|
|
|
|
// `app.use(ArcoVue)` registers the components globally, so test-utils stubs only
|
|
// match Arco's internal component names, not the kebab-case template tags.
|
|
const arcoGlobal = {
|
|
plugins: [ArcoVue],
|
|
stubs: {
|
|
SettingRow: SettingRowStub,
|
|
Select: true,
|
|
InputNumber: true,
|
|
},
|
|
}
|
|
|
|
/** The Arco switch rendered inside the SettingRow carrying `label`. */
|
|
function switchByLabel(wrapper: any, label: string) {
|
|
const row = wrapper
|
|
.findAll('.setting-row')
|
|
.find((node: any) => node.get('.setting-row-label').text() === label)
|
|
if (!row) throw new Error(`no SettingRow labelled "${label}"`)
|
|
return row.get('.arco-switch')
|
|
}
|
|
|
|
describe('SessionSettings', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
mockPrefsStore.humanOnly = true
|
|
mockSettingsStore.memory.write_approval = false
|
|
mockSettingsStore.skills.write_approval = true
|
|
})
|
|
|
|
it('surfaces the human-only preference in the Session tab', async () => {
|
|
const wrapper = mount(SessionSettings, { global: arcoGlobal })
|
|
await flushPromises()
|
|
|
|
expect(wrapper.text()).toContain('settings.session.liveMonitorHumanOnly')
|
|
// requireAuth + memory + skills write approval + human-only
|
|
expect(wrapper.findAll('.arco-switch').length).toBe(4)
|
|
|
|
const humanOnlyToggle = switchByLabel(wrapper, 'settings.session.liveMonitorHumanOnly')
|
|
expect(humanOnlyToggle.attributes('aria-checked')).toBe('true')
|
|
|
|
await humanOnlyToggle.trigger('click')
|
|
await Promise.resolve()
|
|
|
|
expect(mockPrefsStore.setHumanOnly).toHaveBeenCalledWith(false)
|
|
})
|
|
|
|
it('saves write approval toggles to memory and skills config sections', async () => {
|
|
const wrapper = mount(SessionSettings, { global: arcoGlobal })
|
|
await flushPromises()
|
|
|
|
const memoryToggle = switchByLabel(wrapper, 'settings.session.memoryWriteApproval')
|
|
const skillsToggle = switchByLabel(wrapper, 'settings.session.skillsWriteApproval')
|
|
expect(memoryToggle.attributes('aria-checked')).toBe('false')
|
|
expect(skillsToggle.attributes('aria-checked')).toBe('true')
|
|
|
|
await memoryToggle.trigger('click')
|
|
await Promise.resolve()
|
|
await skillsToggle.trigger('click')
|
|
await Promise.resolve()
|
|
|
|
expect(mockSettingsStore.updateLocal).toHaveBeenCalledWith('memory', { write_approval: true })
|
|
expect(mockSettingsStore.saveSection).toHaveBeenCalledWith('memory', { write_approval: true })
|
|
expect(mockSettingsStore.updateLocal).toHaveBeenCalledWith('skills', { write_approval: false })
|
|
expect(mockSettingsStore.saveSection).toHaveBeenCalledWith('skills', { write_approval: false })
|
|
})
|
|
|
|
it('shows write approval toggles without probing Hermes Agent support', async () => {
|
|
const wrapper = mount(SessionSettings, { global: arcoGlobal })
|
|
|
|
await flushPromises()
|
|
|
|
expect(wrapper.text()).toContain('settings.session.memoryWriteApproval')
|
|
expect(wrapper.text()).toContain('settings.session.skillsWriteApproval')
|
|
})
|
|
})
|