diff --git a/web/frontend/src/__tests__/gate-blocked-review-buttons.test.ts b/web/frontend/src/__tests__/gate-blocked-review-buttons.test.ts new file mode 100644 index 0000000..8c66803 --- /dev/null +++ b/web/frontend/src/__tests__/gate-blocked-review-buttons.test.ts @@ -0,0 +1,151 @@ +/** + * KIN-134: Кнопки Approve/Revise/Reject видны для задач со статусом 'blocked' + * + * Когда gate-агент блокирует задачу в auto_complete-режиме: + * - task.status → 'blocked' + * - task.execution_mode остаётся 'auto_complete' (pipeline не сбрасывает его) + * Раньше кнопки проверяли только status === 'review' && !autoMode, + * поэтому для blocked-задач кнопки не рендерились вообще. + */ + +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { mount, flushPromises } from '@vue/test-utils' +import { createRouter, createMemoryHistory } from 'vue-router' +import TaskDetail from '../views/TaskDetail.vue' + +vi.mock('../api', () => ({ + api: { + taskFull: vi.fn(), + patchTask: vi.fn(), + runTask: vi.fn(), + approveTask: vi.fn(), + rejectTask: vi.fn(), + reviseTask: vi.fn(), + followupTask: vi.fn(), + deployProject: vi.fn(), + getAttachments: vi.fn(), + resolveAction: vi.fn(), + }, +})) + +import { api } from '../api' + +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 }) + +const Stub = { template: '
' } + +function makeRouter() { + return createRouter({ + history: createMemoryHistory(), + routes: [ + { path: '/', component: Stub }, + { path: '/task/:id', component: TaskDetail, props: true }, + ], + }) +} + +function makeBlockedTask(execution_mode: string | null = 'auto_complete') { + return { + id: 'KIN-134', + project_id: 'KIN', + title: 'Gate-blocked task', + status: 'blocked', + priority: 5, + assigned_role: 'reviewer', + parent_task_id: null, + brief: null, + spec: null, + execution_mode, + blocked_reason: 'Reviewer: задача не соответствует критериям', + dangerously_skipped: null, + category: null, + acceptance_criteria: null, + created_at: '2026-01-01', + updated_at: '2026-01-01', + pipeline_steps: [], + related_decisions: [], + pending_actions: [], + pipeline_id: null, + project_deploy_command: null, + project_deploy_runtime: null, + } +} + +beforeEach(() => { + localStorageMock.clear() + vi.clearAllMocks() + vi.mocked(api.getAttachments).mockResolvedValue([]) +}) + +describe('KIN-134: кнопки Approve/Revise/Reject для status=blocked', () => { + it('Approve-кнопка видна когда статус blocked и execution_mode=auto_complete', async () => { + vi.mocked(api.taskFull).mockResolvedValue(makeBlockedTask('auto_complete') as any) + const router = makeRouter() + await router.push('/task/KIN-134') + const wrapper = mount(TaskDetail, { + props: { id: 'KIN-134' }, + global: { plugins: [router] }, + }) + await flushPromises() + + const buttons = wrapper.findAll('button') + const approveBtn = buttons.find(b => b.text().includes('Approve')) + expect(approveBtn?.exists(), 'Кнопка Approve должна быть видна для blocked-задачи').toBe(true) + }) + + it('Revise-кнопка видна когда статус blocked и execution_mode=auto_complete', async () => { + vi.mocked(api.taskFull).mockResolvedValue(makeBlockedTask('auto_complete') as any) + const router = makeRouter() + await router.push('/task/KIN-134') + const wrapper = mount(TaskDetail, { + props: { id: 'KIN-134' }, + global: { plugins: [router] }, + }) + await flushPromises() + + const buttons = wrapper.findAll('button') + const reviseBtn = buttons.find(b => b.text().includes('Revise')) + expect(reviseBtn?.exists(), 'Кнопка Revise должна быть видна для blocked-задачи').toBe(true) + }) + + it('Reject-кнопка видна когда статус blocked и execution_mode=auto_complete', async () => { + vi.mocked(api.taskFull).mockResolvedValue(makeBlockedTask('auto_complete') as any) + const router = makeRouter() + await router.push('/task/KIN-134') + const wrapper = mount(TaskDetail, { + props: { id: 'KIN-134' }, + global: { plugins: [router] }, + }) + await flushPromises() + + const buttons = wrapper.findAll('button') + const rejectBtn = buttons.find(b => b.text().includes('Reject')) + expect(rejectBtn?.exists(), 'Кнопка Reject должна быть видна для blocked-задачи').toBe(true) + }) + + it('Все три кнопки видны для blocked-задачи без execution_mode', async () => { + vi.mocked(api.taskFull).mockResolvedValue(makeBlockedTask(null) as any) + const router = makeRouter() + await router.push('/task/KIN-134') + const wrapper = mount(TaskDetail, { + props: { id: 'KIN-134' }, + global: { plugins: [router] }, + }) + await flushPromises() + + const buttons = wrapper.findAll('button') + const texts = buttons.map(b => b.text()) + expect(texts.some(t => t.includes('Approve')), 'Approve должен быть').toBe(true) + expect(texts.some(t => t.includes('Revise')), 'Revise должен быть').toBe(true) + expect(texts.some(t => t.includes('Reject')), 'Reject должен быть').toBe(true) + }) +}) diff --git a/web/frontend/src/views/TaskDetail.vue b/web/frontend/src/views/TaskDetail.vue index 59025aa..359d9ad 100644 --- a/web/frontend/src/views/TaskDetail.vue +++ b/web/frontend/src/views/TaskDetail.vue @@ -571,17 +571,17 @@ async function saveEdit() { {{ t('taskDetail.autopilot_active') }}
- - -