/**
* 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)
})
})