kin/web/frontend/src/__tests__/blocked-reason-list.test.ts
2026-03-17 16:21:33 +02:00

225 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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: '<div />' }
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<typeof makeTask>[]) {
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<string, string> = {}
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<typeof makeTask>[]) {
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)
})
})