Merge branch 'KIN-134-debugger'

This commit is contained in:
Gros Frumos 2026-03-19 17:06:24 +02:00
commit e99e4ca803
2 changed files with 154 additions and 3 deletions

View 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)
})
})

View file

@ -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') }}