/** * KIN-083: Тесты healthcheck Claude CLI auth — frontend баннеры * * Проверяет: * 1. TaskDetail.vue: при ошибке claude_auth_required от runTask — показывает баннер * 2. TaskDetail.vue: баннер закрывается кнопкой ✕ * 3. TaskDetail.vue: happy path — баннер не появляется при успешном runTask * 4. ProjectView.vue: при ошибке claude_auth_required от startPhase — показывает баннер * 5. ProjectView.vue: happy path — баннер не появляется при успешном startPhase */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { mount, flushPromises } from '@vue/test-utils' import { createRouter, createMemoryHistory } from 'vue-router' import TaskDetail from '../views/TaskDetail.vue' import ProjectView from '../views/ProjectView.vue' // importOriginal сохраняет реальный ApiError — нужен для instanceof-проверки в компоненте vi.mock('../api', async (importOriginal) => { const actual = await importOriginal() return { ...actual, api: { project: vi.fn(), taskFull: vi.fn(), runTask: vi.fn(), startPhase: vi.fn(), getPhases: vi.fn(), patchTask: vi.fn(), patchProject: vi.fn(), auditProject: vi.fn(), createTask: vi.fn(), deployProject: vi.fn(), notifications: vi.fn(), }, } }) import { api, ApiError } from '../api' const Stub = { template: '
' } const localStorageMock = (() => { let store: Record = {} return { getItem: (k: string) => store[k] ?? null, setItem: (k: string, v: string) => { store[k] = v }, removeItem: (k: string) => { delete store[k] }, clear: () => { store = {} }, } })() Object.defineProperty(globalThis, 'localStorage', { value: localStorageMock, configurable: true }) function makeRouter() { return createRouter({ history: createMemoryHistory(), routes: [ { path: '/', component: Stub }, { path: '/project/:id', component: ProjectView, props: true }, { path: '/task/:id', component: TaskDetail, props: true }, ], }) } const MOCK_TASK = { id: 'KIN-001', project_id: 'KIN', title: 'Тестовая задача', status: 'pending', priority: 5, assigned_role: null, parent_task_id: null, brief: null, spec: null, execution_mode: null, blocked_reason: null, dangerously_skipped: null, category: null, acceptance_criteria: null, created_at: '2024-01-01', updated_at: '2024-01-01', pipeline_steps: [], related_decisions: [], pending_actions: [], } const MOCK_PROJECT = { id: 'KIN', name: 'Kin', path: '/projects/kin', status: 'active', priority: 5, tech_stack: ['python', 'vue'], execution_mode: 'review', autocommit_enabled: 0, obsidian_vault_path: null, deploy_command: null, created_at: '2024-01-01', total_tasks: 1, done_tasks: 0, active_tasks: 1, blocked_tasks: 0, review_tasks: 0, project_type: 'development', ssh_host: null, ssh_user: null, ssh_key_path: null, ssh_proxy_jump: null, description: null, tasks: [], decisions: [], modules: [], } const MOCK_ACTIVE_PHASE = { id: 1, project_id: 'KIN', role: 'pm', phase_order: 1, status: 'active', task_id: 'KIN-R-001', revise_count: 0, revise_comment: null, created_at: '2024-01-01', updated_at: '2024-01-01', task: { id: 'KIN-R-001', status: 'pending', title: 'Research', priority: 5, assigned_role: 'pm', parent_task_id: null, brief: null, spec: null, execution_mode: null, blocked_reason: null, dangerously_skipped: null, category: null, acceptance_criteria: null, project_id: 'KIN', created_at: '2024-01-01', updated_at: '2024-01-01', }, } beforeEach(() => { localStorageMock.clear() vi.clearAllMocks() vi.mocked(api.project).mockResolvedValue(MOCK_PROJECT as any) vi.mocked(api.taskFull).mockResolvedValue(MOCK_TASK as any) vi.mocked(api.runTask).mockResolvedValue({ status: 'started' } as any) vi.mocked(api.startPhase).mockResolvedValue({ status: 'started', phase_id: 1, task_id: 'KIN-R-001' }) vi.mocked(api.getPhases).mockResolvedValue([]) vi.mocked(api.notifications).mockResolvedValue([]) }) afterEach(() => { vi.restoreAllMocks() }) // ───────────────────────────────────────────────────────────── // TaskDetail: баннер при claude_auth_required // ───────────────────────────────────────────────────────────── describe('KIN-083: TaskDetail — claude auth banner', () => { async function mountTaskDetail() { const router = makeRouter() await router.push('/task/KIN-001') const wrapper = mount(TaskDetail, { props: { id: 'KIN-001' }, global: { plugins: [router] }, }) await flushPromises() return wrapper } it('показывает баннер "Claude CLI requires login" при ошибке claude_auth_required от runTask', async () => { vi.mocked(api.runTask).mockRejectedValue( new ApiError('claude_auth_required', 'Claude CLI requires login. Run: claude login'), ) const wrapper = await mountTaskDetail() const runBtn = wrapper.findAll('button').find(b => b.text().includes('Run Pipeline')) expect(runBtn?.exists(), 'Кнопка Run Pipeline должна быть видна для pending задачи').toBe(true) await runBtn!.trigger('click') await flushPromises() expect(wrapper.text(), 'Баннер должен содержать текст ошибки аутентификации').toContain('Claude CLI requires login') }) it('баннер закрывается кнопкой ✕', async () => { vi.mocked(api.runTask).mockRejectedValue( new ApiError('claude_auth_required', 'Claude CLI requires login. Run: claude login'), ) const wrapper = await mountTaskDetail() const runBtn = wrapper.findAll('button').find(b => b.text().includes('Run Pipeline')) await runBtn!.trigger('click') await flushPromises() expect(wrapper.text()).toContain('Claude CLI requires login') const closeBtn = wrapper.findAll('button').find(b => b.text().trim() === '✕') expect(closeBtn?.exists(), 'Кнопка ✕ должна быть видна').toBe(true) await closeBtn!.trigger('click') await flushPromises() expect(wrapper.text(), 'После закрытия баннер не должен быть виден').not.toContain('Claude CLI requires login') }) it('не показывает баннер когда runTask успешен (happy path)', async () => { const wrapper = await mountTaskDetail() const runBtn = wrapper.findAll('button').find(b => b.text().includes('Run Pipeline')) if (runBtn?.exists()) { await runBtn.trigger('click') await flushPromises() } expect(wrapper.text(), 'Баннер не должен появляться при успешном запуске').not.toContain('Claude CLI requires login') }) }) // ───────────────────────────────────────────────────────────── // ProjectView: баннер при claude_auth_required // ───────────────────────────────────────────────────────────── describe('KIN-083: ProjectView — claude auth banner', () => { async function mountOnPhases() { vi.mocked(api.getPhases).mockResolvedValue([MOCK_ACTIVE_PHASE] as any) const router = makeRouter() await router.push('/project/KIN') const wrapper = mount(ProjectView, { props: { id: 'KIN' }, global: { plugins: [router] }, }) await flushPromises() const phasesTab = wrapper.findAll('button').find(b => b.text().includes('Phases')) await phasesTab!.trigger('click') await flushPromises() return wrapper } it('показывает баннер "Claude CLI requires login" при ошибке claude_auth_required от startPhase', async () => { vi.mocked(api.startPhase).mockRejectedValue( new ApiError('claude_auth_required', 'Claude CLI requires login. Run: claude login'), ) const wrapper = await mountOnPhases() const startBtn = wrapper.findAll('button').find(b => b.text().includes('Start Research')) expect(startBtn?.exists(), 'Кнопка Start Research должна быть видна').toBe(true) await startBtn!.trigger('click') await flushPromises() expect(wrapper.text(), 'Баннер должен содержать текст ошибки аутентификации').toContain('Claude CLI requires login') }) it('не показывает баннер когда startPhase успешен (happy path)', async () => { const wrapper = await mountOnPhases() const startBtn = wrapper.findAll('button').find(b => b.text().includes('Start Research')) if (startBtn?.exists()) { await startBtn.trigger('click') await flushPromises() } expect(wrapper.text(), 'Баннер не должен появляться при успешном запуске фазы').not.toContain('Claude CLI requires login') }) })