day 1: Kin from zero to production - agents, GUI, autopilot, 352 tests
This commit is contained in:
parent
8d9facda4f
commit
8a6f280cbd
22 changed files with 1907 additions and 103 deletions
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue