Import the client, server, skills, tests, and build scripts as the starting point for the rebuild, then trim the surfaces this project does not ship: Electron desktop packaging, the marketing website, and the ESP32 firmware tree. CI keeps the web build, e2e, lockfile, release, and Docker workflows. Rename the package and CLI entry points to hermes-ui, drop the prepare hook so npm ci no longer triggers a build, and drop the removed tsconfig and nodemon references. Rewrite AGENTS.md, ARCHITECTURE.md, DEVELOPMENT.md, and the READMEs so the documented package map and hard rules match this repository. reference_code/ is ignored: it is a read-only snapshot, not an input to the build.
134 lines
4.0 KiB
TypeScript
134 lines
4.0 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { mount, flushPromises } from '@vue/test-utils'
|
|
|
|
const mockApi = vi.hoisted(() => ({
|
|
startCopilotLogin: vi.fn(),
|
|
pollCopilotLogin: vi.fn(),
|
|
}))
|
|
|
|
const mockMessage = vi.hoisted(() => ({
|
|
success: vi.fn(),
|
|
warning: vi.fn(),
|
|
error: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('@/api/hermes/copilot-auth', () => mockApi)
|
|
vi.mock('@/utils/clipboard', () => ({ copyToClipboard: vi.fn(async () => true) }))
|
|
vi.mock('vue-i18n', () => ({
|
|
useI18n: () => ({ t: (k: string) => k }),
|
|
}))
|
|
vi.mock('naive-ui', () => ({
|
|
NModal: { template: '<div><slot /><slot name="footer" /></div>' },
|
|
NButton: { template: '<button @click="$emit(\'click\')"><slot /></button>' },
|
|
NSpin: { template: '<span class="spin" />' },
|
|
useMessage: () => mockMessage,
|
|
}))
|
|
|
|
import CopilotLoginModal from '@/components/hermes/models/CopilotLoginModal.vue'
|
|
|
|
function mountModal() {
|
|
return mount(CopilotLoginModal)
|
|
}
|
|
|
|
describe('CopilotLoginModal device-flow state machine', () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers()
|
|
mockApi.startCopilotLogin.mockReset()
|
|
mockApi.pollCopilotLogin.mockReset()
|
|
mockMessage.success.mockReset()
|
|
mockMessage.warning.mockReset()
|
|
mockMessage.error.mockReset()
|
|
})
|
|
|
|
it('启动后进入 waiting 并显示 user_code', async () => {
|
|
mockApi.startCopilotLogin.mockResolvedValue({
|
|
session_id: 'sess-1',
|
|
user_code: 'ABCD-1234',
|
|
verification_url: 'https://github.com/login/device',
|
|
expires_in: 900,
|
|
interval: 5,
|
|
})
|
|
mockApi.pollCopilotLogin.mockResolvedValue({ status: 'pending', error: null })
|
|
|
|
const wrapper = mountModal()
|
|
await flushPromises()
|
|
|
|
expect(wrapper.text()).toContain('ABCD-1234')
|
|
expect(mockApi.startCopilotLogin).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('approved 时 emit success 且消息为 copilotApproved', async () => {
|
|
mockApi.startCopilotLogin.mockResolvedValue({
|
|
session_id: 'sess-2',
|
|
user_code: 'WXYZ-9999',
|
|
verification_url: 'https://github.com/login/device',
|
|
expires_in: 900,
|
|
interval: 5,
|
|
})
|
|
mockApi.pollCopilotLogin.mockResolvedValue({ status: 'approved', error: null })
|
|
|
|
const wrapper = mountModal()
|
|
await flushPromises()
|
|
|
|
// 推动一次 poll timer
|
|
await vi.advanceTimersByTimeAsync(3000)
|
|
await flushPromises()
|
|
|
|
expect(mockMessage.success).toHaveBeenCalledWith('models.copilotApproved')
|
|
|
|
// approved 后 1s 自动关闭
|
|
await vi.advanceTimersByTimeAsync(1500)
|
|
await flushPromises()
|
|
expect(wrapper.emitted('success')).toBeTruthy()
|
|
})
|
|
|
|
it('expired 时进入 expired 状态并显示重试按钮', async () => {
|
|
mockApi.startCopilotLogin.mockResolvedValue({
|
|
session_id: 'sess-3',
|
|
user_code: 'EXPI-RED!',
|
|
verification_url: 'https://github.com/login/device',
|
|
expires_in: 900,
|
|
interval: 5,
|
|
})
|
|
mockApi.pollCopilotLogin.mockResolvedValue({ status: 'expired', error: null })
|
|
|
|
const wrapper = mountModal()
|
|
await flushPromises()
|
|
await vi.advanceTimersByTimeAsync(3000)
|
|
await flushPromises()
|
|
|
|
expect(wrapper.text()).toContain('models.copilotExpired')
|
|
expect(wrapper.emitted('success')).toBeFalsy()
|
|
})
|
|
|
|
it('startCopilotLogin 抛错时显示 error 且不 emit success', async () => {
|
|
mockApi.startCopilotLogin.mockRejectedValue(new Error('boom'))
|
|
|
|
const wrapper = mountModal()
|
|
await flushPromises()
|
|
|
|
expect(mockMessage.error).toHaveBeenCalled()
|
|
expect(wrapper.emitted('success')).toBeFalsy()
|
|
})
|
|
|
|
it('denied 时进入 error 状态', async () => {
|
|
mockApi.startCopilotLogin.mockResolvedValue({
|
|
session_id: 'sess-4',
|
|
user_code: 'NOPE',
|
|
verification_url: 'https://github.com/login/device',
|
|
expires_in: 900,
|
|
interval: 5,
|
|
})
|
|
mockApi.pollCopilotLogin.mockResolvedValue({ status: 'denied', error: null })
|
|
|
|
const wrapper = mountModal()
|
|
await flushPromises()
|
|
await vi.advanceTimersByTimeAsync(3000)
|
|
await flushPromises()
|
|
|
|
expect(wrapper.text()).toContain('models.copilotDenied')
|
|
expect(wrapper.emitted('success')).toBeFalsy()
|
|
})
|
|
})
|