day 1: Kin from zero to production - agents, GUI, autopilot, 352 tests

This commit is contained in:
Gros Frumos 2026-03-15 23:22:49 +02:00
parent 8d9facda4f
commit 8a6f280cbd
22 changed files with 1907 additions and 103 deletions

View file

@ -1,14 +1,15 @@
/**
* KIN-011: Тесты сохранения фильтра статусов при навигации
* KIN-011/KIN-014: Тесты фильтра статусов при навигации
*
* Проверяет:
* 1. Выбор фильтра обновляет URL (?status=...)
* 2. Прямая ссылка с query param инициализирует фильтр
* 1. Клик по кнопке статуса обновляет URL (?status=...)
* 2. Прямая ссылка с query param активирует нужную кнопку
* 3. Фильтр показывает только задачи с нужным статусом
* 4. Сброс фильтра удаляет param из URL
* 5. goBack() вызывает router.back() при наличии истории
* 6. goBack() делает push на /project/:id без истории
* 7. После router.back() URL проекта восстанавливается с фильтром
* 4. Сброс фильтра () удаляет param из URL
* 5. Без фильтра отображаются все задачи
* 6. goBack() вызывает router.back() при наличии истории
* 7. goBack() делает push на /project/:id без истории
* 8. После router.back() URL проекта восстанавливается с фильтром
*/
import { describe, it, expect, vi, beforeEach } from 'vitest'
@ -117,8 +118,8 @@ beforeEach(() => {
// ProjectView: фильтр ↔ URL
// ─────────────────────────────────────────────────────────────
describe('KIN-011: ProjectView — фильтр и URL', () => {
it('1. При выборе фильтра URL обновляется query param ?status', async () => {
describe('KIN-011/KIN-014: ProjectView — фильтр и URL', () => {
it('1. Клик по кнопке статуса обновляет URL (?status=...)', async () => {
const router = makeRouter()
await router.push('/project/KIN')
@ -131,16 +132,16 @@ describe('KIN-011: ProjectView — фильтр и URL', () => {
// Изначально status нет в URL
expect(router.currentRoute.value.query.status).toBeUndefined()
// Меняем фильтр через select (первый select — фильтр статусов)
const select = wrapper.find('select')
await select.setValue('in_progress')
// Кликаем по кнопке in_progress
const btn = wrapper.find('[data-status="in_progress"]')
await btn.trigger('click')
await flushPromises()
// URL должен содержать ?status=in_progress
expect(router.currentRoute.value.query.status).toBe('in_progress')
})
it('2. Прямая ссылка ?status=in_progress инициализирует фильтр в select', async () => {
it('2. Прямая ссылка ?status=in_progress активирует нужную кнопку', async () => {
const router = makeRouter()
await router.push('/project/KIN?status=in_progress')
@ -150,9 +151,13 @@ describe('KIN-011: ProjectView — фильтр и URL', () => {
})
await flushPromises()
// select должен показывать in_progress
const select = wrapper.find('select')
expect((select.element as HTMLSelectElement).value).toBe('in_progress')
// Кнопка in_progress должна быть активна (иметь класс text-blue-300)
const btn = wrapper.find('[data-status="in_progress"]')
expect(btn.classes()).toContain('text-blue-300')
// Другие кнопки не активны
const pendingBtn = wrapper.find('[data-status="pending"]')
expect(pendingBtn.classes()).not.toContain('text-blue-300')
})
it('3. Прямая ссылка ?status=in_progress показывает только задачи с этим статусом', async () => {
@ -171,7 +176,7 @@ describe('KIN-011: ProjectView — фильтр и URL', () => {
expect(links[0].text()).toContain('KIN-002')
})
it('4. Сброс фильтра (пустое значение) удаляет status из URL', async () => {
it('4. Сброс фильтра (кнопка ✕) удаляет status из URL', async () => {
const router = makeRouter()
await router.push('/project/KIN?status=done')
@ -181,9 +186,9 @@ describe('KIN-011: ProjectView — фильтр и URL', () => {
})
await flushPromises()
// Сброс фильтра
const select = wrapper.find('select')
await select.setValue('')
// Кликаем кнопку сброса
const clearBtn = wrapper.find('[data-action="clear-status"]')
await clearBtn.trigger('click')
await flushPromises()
// status должен исчезнуть из URL
@ -203,6 +208,89 @@ describe('KIN-011: ProjectView — фильтр и URL', () => {
const links = wrapper.findAll('a[href^="/task/"]')
expect(links).toHaveLength(3)
})
it('KIN-014: Выбор нескольких статусов — URL содержит оба через запятую', async () => {
const router = makeRouter()
await router.push('/project/KIN')
const wrapper = mount(ProjectView, {
props: { id: 'KIN' },
global: { plugins: [router] },
})
await flushPromises()
await wrapper.find('[data-status="pending"]').trigger('click')
await wrapper.find('[data-status="in_progress"]').trigger('click')
await flushPromises()
const status = router.currentRoute.value.query.status as string
expect(status.split(',').sort()).toEqual(['in_progress', 'pending'])
})
it('KIN-014: Фильтр сохраняется в localStorage', async () => {
const router = makeRouter()
await router.push('/project/KIN')
const wrapper = mount(ProjectView, {
props: { id: 'KIN' },
global: { plugins: [router] },
})
await flushPromises()
await wrapper.find('[data-status="pending"]').trigger('click')
await flushPromises()
const stored = JSON.parse(localStorageMock.getItem('kin-task-statuses-KIN') ?? '[]')
expect(stored).toContain('pending')
})
})
// ─────────────────────────────────────────────────────────────
// KIN-046: кнопки фильтра и сигнатура runTask
// ─────────────────────────────────────────────────────────────
describe('KIN-046: ProjectView — фильтр статусов и runTask', () => {
it('Все 7 кнопок фильтра статусов отображаются в DOM', async () => {
const router = makeRouter()
await router.push('/project/KIN')
const wrapper = mount(ProjectView, {
props: { id: 'KIN' },
global: { plugins: [router] },
})
await flushPromises()
const ALL_TASK_STATUSES = ['pending', 'in_progress', 'review', 'blocked', 'decomposed', 'done', 'cancelled']
for (const s of ALL_TASK_STATUSES) {
expect(wrapper.find(`[data-status="${s}"]`).exists(), `кнопка "${s}" должна быть в DOM`).toBe(true)
}
})
it('api.runTask вызывается только с taskId — без второго аргумента', async () => {
vi.mocked(api.runTask).mockResolvedValue({ status: 'ok' } as any)
vi.spyOn(window, 'confirm').mockReturnValue(true)
const router = makeRouter()
await router.push('/project/KIN')
const wrapper = mount(ProjectView, {
props: { id: 'KIN' },
global: { plugins: [router] },
})
await flushPromises()
// KIN-001 имеет статус pending — кнопка "Run pipeline" должна быть видна
const runBtn = wrapper.find('button[title="Run pipeline"]')
expect(runBtn.exists()).toBe(true)
await runBtn.trigger('click')
await flushPromises()
expect(api.runTask).toHaveBeenCalledTimes(1)
// Проверяем: вызван только с taskId, второй аргумент (autoMode) отсутствует
const callArgs = vi.mocked(api.runTask).mock.calls[0]
expect(callArgs).toHaveLength(1)
expect(callArgs[0]).toBe('KIN-001')
})
})
// ─────────────────────────────────────────────────────────────
@ -210,7 +298,7 @@ describe('KIN-011: ProjectView — фильтр и URL', () => {
// ─────────────────────────────────────────────────────────────
describe('KIN-011: TaskDetail — возврат с сохранением URL', () => {
it('6. goBack() вызывает router.back() когда window.history.length > 1', async () => {
it('6 (KIN-011). goBack() вызывает router.back() когда window.history.length > 1', async () => {
const router = makeRouter()
await router.push('/project/KIN?status=in_progress')
await router.push('/task/KIN-002')
@ -278,3 +366,146 @@ describe('KIN-011: TaskDetail — возврат с сохранением URL',
expect(router.currentRoute.value.query.status).toBe('in_progress')
})
})
// ─────────────────────────────────────────────────────────────
// KIN-047: TaskDetail — кнопки Approve/Reject в статусе review
// ─────────────────────────────────────────────────────────────
describe('KIN-047: TaskDetail — Approve/Reject в статусе review', () => {
function makeTaskWith(status: string, executionMode: 'auto' | 'review' | null = null) {
return {
id: 'KIN-047',
project_id: 'KIN',
title: 'Review Task',
status,
priority: 3,
assigned_role: null,
parent_task_id: null,
brief: null,
spec: null,
execution_mode: executionMode,
created_at: '2024-01-01',
updated_at: '2024-01-01',
pipeline_steps: [],
related_decisions: [],
}
}
it('Approve и Reject видны при статусе review и ручном режиме', async () => {
vi.mocked(api.taskFull).mockResolvedValue(makeTaskWith('review', 'review') as any)
const router = makeRouter()
await router.push('/task/KIN-047')
const wrapper = mount(TaskDetail, {
props: { id: 'KIN-047' },
global: { plugins: [router] },
})
await flushPromises()
const buttons = wrapper.findAll('button')
const approveExists = buttons.some(b => b.text().includes('Approve'))
const rejectExists = buttons.some(b => b.text().includes('Reject'))
expect(approveExists, 'Approve должна быть видна в review + ручной режим').toBe(true)
expect(rejectExists, 'Reject должна быть видна в review + ручной режим').toBe(true)
})
it('Approve и Reject скрыты при autoMode в статусе review', async () => {
vi.mocked(api.taskFull).mockResolvedValue(makeTaskWith('review', 'auto') as any)
const router = makeRouter()
await router.push('/task/KIN-047')
const wrapper = mount(TaskDetail, {
props: { id: 'KIN-047' },
global: { plugins: [router] },
})
await flushPromises()
const buttons = wrapper.findAll('button')
const approveExists = buttons.some(b => b.text().includes('Approve'))
const rejectExists = buttons.some(b => b.text().includes('Reject'))
expect(approveExists, 'Approve должна быть скрыта в autoMode').toBe(false)
expect(rejectExists, 'Reject должна быть скрыта в autoMode').toBe(false)
})
it('Тоггл Auto/Review виден в статусе review при autoMode (позволяет выйти из автопилота)', async () => {
vi.mocked(api.taskFull).mockResolvedValue(makeTaskWith('review', 'auto') as any)
const router = makeRouter()
await router.push('/task/KIN-047')
const wrapper = mount(TaskDetail, {
props: { id: 'KIN-047' },
global: { plugins: [router] },
})
await flushPromises()
const buttons = wrapper.findAll('button')
const toggleExists = buttons.some(b => b.text().includes('Auto') || b.text().includes('Review'))
expect(toggleExists, 'Тоггл Auto/Review должен быть виден в статусе review').toBe(true)
})
it('После клика тоггла в review+autoMode появляются Approve и Reject', async () => {
const task = makeTaskWith('review', 'auto')
vi.mocked(api.taskFull).mockResolvedValue(task as any)
vi.mocked(api.patchTask).mockResolvedValue({ execution_mode: 'review' } as any)
const router = makeRouter()
await router.push('/task/KIN-047')
const wrapper = mount(TaskDetail, {
props: { id: 'KIN-047' },
global: { plugins: [router] },
})
await flushPromises()
// Находим тоггл-кнопку (текст "Auto" когда autoMode=true)
const toggleBtn = wrapper.findAll('button').find(b => b.text().includes('Auto'))
expect(toggleBtn?.exists(), 'Тоггл должен быть виден').toBe(true)
await toggleBtn!.trigger('click')
await flushPromises()
// После переключения autoMode=false → Approve и Reject должны появиться
const buttons = wrapper.findAll('button')
const approveExists = buttons.some(b => b.text().includes('Approve'))
const rejectExists = buttons.some(b => b.text().includes('Reject'))
expect(approveExists, 'Approve должна появиться после отключения autoMode').toBe(true)
expect(rejectExists, 'Reject должна появиться после отключения autoMode').toBe(true)
})
it('KIN-051: Approve и Reject видны при статусе review и execution_mode=null (фикс баги)', async () => {
// Воспроизводит баг: задача в review без явного execution_mode зависала
// без кнопок, потому что localStorage мог содержать 'auto'
localStorageMock.setItem('kin-mode-KIN', 'auto') // имитируем "плохой" localStorage
vi.mocked(api.taskFull).mockResolvedValue(makeTaskWith('review', null) as any)
const router = makeRouter()
await router.push('/task/KIN-047')
const wrapper = mount(TaskDetail, {
props: { id: 'KIN-047' },
global: { plugins: [router] },
})
await flushPromises()
const buttons = wrapper.findAll('button')
const approveExists = buttons.some(b => b.text().includes('Approve'))
const rejectExists = buttons.some(b => b.text().includes('Reject'))
expect(approveExists, 'Approve должна быть видна: review+null mode игнорирует localStorage').toBe(true)
expect(rejectExists, 'Reject должна быть видна: review+null mode игнорирует localStorage').toBe(true)
})
it('Approve скрыта для статусов pending и done', async () => {
for (const status of ['pending', 'done']) {
vi.mocked(api.taskFull).mockResolvedValue(makeTaskWith(status, 'review') as any)
const router = makeRouter()
await router.push('/task/KIN-047')
const wrapper = mount(TaskDetail, {
props: { id: 'KIN-047' },
global: { plugins: [router] },
})
await flushPromises()
const approveExists = wrapper.findAll('button').some(b => b.text().includes('Approve'))
expect(approveExists, `Approve не должна быть видна для статуса "${status}"`).toBe(false)
}
})
})