/** * KIN-099: blocked_reason в списке задач (ProjectView) * * Проверяет: * 1. Задача со статусом 'blocked' и blocked_reason — reason отображается в карточке * 2. Задача со статусом 'blocked' без blocked_reason — дополнительный текст НЕ отображается * 3. Задача с другим статусом — blocked_reason НЕ отображается */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { mount, flushPromises } from '@vue/test-utils' import { createRouter, createMemoryHistory } from 'vue-router' import ProjectView from '../views/ProjectView.vue' vi.mock('../api', () => ({ api: { project: vi.fn(), taskFull: vi.fn(), runTask: vi.fn(), auditProject: vi.fn(), createTask: vi.fn(), patchTask: vi.fn(), patchProject: vi.fn(), deployProject: vi.fn(), getPhases: vi.fn(), uploadAttachment: vi.fn(), environments: vi.fn(), reviseTask: vi.fn(), }, })) import { api } from '../api' const Stub = { template: '
' } function makeTask(id: string, status: string, blocked_reason: string | null = null) { return { id, project_id: 'KIN', title: `Task ${id}`, status, priority: 5, assigned_role: null, parent_task_id: null, brief: null, spec: null, execution_mode: null, blocked_reason, dangerously_skipped: null, category: null, acceptance_criteria: null, created_at: '2024-01-01', updated_at: '2024-01-01', } } function makeMockProject(tasks: ReturnType[]) { return { id: 'KIN', name: 'Kin', path: '/projects/kin', status: 'active', priority: 5, tech_stack: ['python', 'vue'], execution_mode: 'review', autocommit_enabled: 0, auto_test_enabled: 0, test_command: null, obsidian_vault_path: null, deploy_command: null, created_at: '2024-01-01', total_tasks: tasks.length, done_tasks: 0, active_tasks: 0, 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 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: Stub }, ], }) } async function mountProjectView(tasks: ReturnType[]) { vi.mocked(api.project).mockResolvedValue(makeMockProject(tasks) as any) const router = makeRouter() await router.push('/project/KIN') const wrapper = mount(ProjectView, { props: { id: 'KIN' }, global: { plugins: [router] }, }) await flushPromises() return wrapper } beforeEach(() => { localStorageMock.clear() vi.clearAllMocks() vi.mocked(api.getPhases).mockResolvedValue([]) vi.mocked(api.environments).mockResolvedValue([]) }) afterEach(() => { vi.restoreAllMocks() }) // ───────────────────────────────────────────────────────────── // Критерий 1: blocked + blocked_reason → reason отображается // ───────────────────────────────────────────────────────────── describe('KIN-099: blocked_reason отображается для задач со статусом blocked', () => { it('Задача со статусом blocked и blocked_reason — reason виден в тексте карточки', async () => { const wrapper = await mountProjectView([ makeTask('KIN-001', 'blocked', 'Process died unexpectedly (PID 12345)'), ]) expect(wrapper.text()).toContain('Process died unexpectedly (PID 12345)') }) it('blocked_reason отображается в div с классами text-red-400 truncate', async () => { const wrapper = await mountProjectView([ makeTask('KIN-002', 'blocked', 'Agent needs human decision'), ]) // Ищем div с blocked_reason — он имеет класс text-xs text-red-400 truncate const reasonDiv = wrapper.find('.text-red-400.truncate') expect(reasonDiv.exists()).toBe(true) expect(reasonDiv.text()).toContain('Agent needs human decision') }) it('Сам task_id и title также присутствуют вместе с reason', async () => { const wrapper = await mountProjectView([ makeTask('KIN-003', 'blocked', 'Process died unexpectedly (PID 99)'), ]) const text = wrapper.text() expect(text).toContain('KIN-003') expect(text).toContain('Task KIN-003') expect(text).toContain('Process died unexpectedly (PID 99)') }) }) // ───────────────────────────────────────────────────────────── // Критерий 2: blocked без blocked_reason → div с reason НЕ рендерится // ───────────────────────────────────────────────────────────── describe('KIN-099: blocked_reason НЕ отображается если поле пустое (null)', () => { it('Задача со статусом blocked без blocked_reason — div text-red-400.truncate отсутствует', async () => { const wrapper = await mountProjectView([ makeTask('KIN-004', 'blocked', null), ]) // Задача отображается expect(wrapper.text()).toContain('KIN-004') // Но div для blocked_reason отсутствует expect(wrapper.find('.text-red-400.truncate').exists()).toBe(false) }) }) // ───────────────────────────────────────────────────────────── // Критерий 3: другой статус → blocked_reason НЕ отображается // ───────────────────────────────────────────────────────────── describe('KIN-099: blocked_reason НЕ отображается для не-blocked статусов', () => { it('Задача со статусом pending и blocked_reason — reason НЕ отображается', async () => { const wrapper = await mountProjectView([ makeTask('KIN-005', 'pending', 'Should not be visible'), ]) expect(wrapper.text()).toContain('KIN-005') expect(wrapper.text()).not.toContain('Should not be visible') }) it('Задача со статусом in_progress и blocked_reason — reason НЕ отображается', async () => { const wrapper = await mountProjectView([ makeTask('KIN-006', 'in_progress', 'Hidden in progress reason'), ]) expect(wrapper.text()).not.toContain('Hidden in progress reason') }) it('Задача со статусом done и blocked_reason — reason НЕ отображается', async () => { const wrapper = await mountProjectView([ makeTask('KIN-007', 'done', 'Past blocked reason'), ]) expect(wrapper.text()).not.toContain('Past blocked reason') }) it('div text-red-400.truncate отсутствует для non-blocked задачи с blocked_reason', async () => { const wrapper = await mountProjectView([ makeTask('KIN-008', 'review', 'Hidden review reason'), ]) expect(wrapper.find('.text-red-400.truncate').exists()).toBe(false) }) })