Compare commits
3 commits
58a7cce6b2
...
461040cf33
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
461040cf33 | ||
|
|
e99e4ca803 | ||
|
|
35d448ba4f |
2 changed files with 154 additions and 3 deletions
151
web/frontend/src/__tests__/gate-blocked-review-buttons.test.ts
Normal file
151
web/frontend/src/__tests__/gate-blocked-review-buttons.test.ts
Normal file
|
|
@ -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<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 })
|
||||||
|
|
||||||
|
const Stub = { template: '<div />' }
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -571,17 +571,17 @@ async function saveEdit() {
|
||||||
<span class="inline-block w-2 h-2 bg-yellow-400 rounded-full animate-pulse"></span>
|
<span class="inline-block w-2 h-2 bg-yellow-400 rounded-full animate-pulse"></span>
|
||||||
{{ t('taskDetail.autopilot_active') }}
|
{{ t('taskDetail.autopilot_active') }}
|
||||||
</div>
|
</div>
|
||||||
<button v-if="task.status === 'review' && !autoMode"
|
<button v-if="task.status === 'blocked' || (task.status === 'review' && !autoMode)"
|
||||||
@click="showApprove = true"
|
@click="showApprove = true"
|
||||||
class="px-4 py-2 text-sm bg-green-900/50 text-green-400 border border-green-800 rounded hover:bg-green-900">
|
class="px-4 py-2 text-sm bg-green-900/50 text-green-400 border border-green-800 rounded hover:bg-green-900">
|
||||||
{{ t('taskDetail.approve_task') }}
|
{{ t('taskDetail.approve_task') }}
|
||||||
</button>
|
</button>
|
||||||
<button v-if="task.status === 'review' && !autoMode"
|
<button v-if="task.status === 'blocked' || (task.status === 'review' && !autoMode)"
|
||||||
@click="showRevise = true"
|
@click="showRevise = true"
|
||||||
class="px-4 py-2 text-sm bg-orange-900/50 text-orange-400 border border-orange-800 rounded hover:bg-orange-900">
|
class="px-4 py-2 text-sm bg-orange-900/50 text-orange-400 border border-orange-800 rounded hover:bg-orange-900">
|
||||||
{{ t('taskDetail.revise_task') }}
|
{{ t('taskDetail.revise_task') }}
|
||||||
</button>
|
</button>
|
||||||
<button v-if="(task.status === 'review' || task.status === 'in_progress') && !autoMode"
|
<button v-if="task.status === 'blocked' || ((task.status === 'review' || task.status === 'in_progress') && !autoMode)"
|
||||||
@click="showReject = true"
|
@click="showReject = true"
|
||||||
class="px-4 py-2 text-sm bg-red-900/50 text-red-400 border border-red-800 rounded hover:bg-red-900">
|
class="px-4 py-2 text-sm bg-red-900/50 text-red-400 border border-red-800 rounded hover:bg-red-900">
|
||||||
{{ t('taskDetail.reject_task') }}
|
{{ t('taskDetail.reject_task') }}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue