kin: auto-commit after pipeline
This commit is contained in:
parent
05bcb14b99
commit
02b53e82ca
8 changed files with 555 additions and 68 deletions
|
|
@ -172,9 +172,7 @@ describe('api.deployProject', () => {
|
|||
{ step: 'git pull', stdout: 'ok', stderr: '', exit_code: 0 },
|
||||
{ step: 'docker compose up -d', stdout: 'started', stderr: '', exit_code: 0 },
|
||||
],
|
||||
dependents_deployed: [
|
||||
{ project_id: 'FE', project_name: 'Frontend', success: true, results: [] },
|
||||
],
|
||||
dependents_deployed: ['FE'],
|
||||
overall_success: true,
|
||||
}
|
||||
mockFetch(structured)
|
||||
|
|
|
|||
292
web/frontend/src/__tests__/deploy-dependents-string.test.ts
Normal file
292
web/frontend/src/__tests__/deploy-dependents-string.test.ts
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
/**
|
||||
* KIN-INFRA-011: Регрессионный тест — dependents_deployed как string[]
|
||||
*
|
||||
* Проверяет:
|
||||
* 1. ProjectView: dependents_deployed рендерит строки напрямую (не dep.project_name)
|
||||
* 2. ProjectView: статичный бейдж "ok" отображается для каждого dependent
|
||||
* 3. ProjectView: пустой dependents_deployed не показывает секцию
|
||||
* 4. TaskDetail: dependents_deployed рендерит строки напрямую
|
||||
* 5. TaskDetail: статичный бейдж "ok" отображается для каждого dependent
|
||||
* 6. TaskDetail: секция "Зависимые проекты" показывается при непустом dependents_deployed
|
||||
*
|
||||
* Decision #546: бэкенд возвращает list[str] (project_id), не объекты DependentDeploy.
|
||||
* DependentDeploy интерфейс удалён. Шаблон использует dep как строку.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { mount, flushPromises } from '@vue/test-utils'
|
||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||
import ProjectView from '../views/ProjectView.vue'
|
||||
import TaskDetail from '../views/TaskDetail.vue'
|
||||
|
||||
vi.mock('../api', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../api')>()
|
||||
return {
|
||||
...actual,
|
||||
api: {
|
||||
projects: vi.fn(),
|
||||
project: vi.fn(),
|
||||
getPhases: vi.fn(),
|
||||
environments: vi.fn(),
|
||||
projectLinks: vi.fn(),
|
||||
patchProject: vi.fn(),
|
||||
deployProject: vi.fn(),
|
||||
createProjectLink: vi.fn(),
|
||||
deleteProjectLink: vi.fn(),
|
||||
syncObsidian: vi.fn(),
|
||||
taskFull: vi.fn(),
|
||||
patchTask: vi.fn(),
|
||||
runTask: vi.fn(),
|
||||
notifications: vi.fn(),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
import { api } from '../api'
|
||||
|
||||
const Stub = { template: '<div />' }
|
||||
|
||||
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 })
|
||||
|
||||
function makeRouter() {
|
||||
return createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{ path: '/', component: Stub },
|
||||
{ path: '/project/:id', component: ProjectView, props: true },
|
||||
{ path: '/task/:id', component: TaskDetail, props: true },
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
const BASE_PROJECT = {
|
||||
id: 'KIN',
|
||||
name: 'Kin',
|
||||
path: '/projects/kin',
|
||||
status: 'active',
|
||||
priority: 5,
|
||||
tech_stack: ['python', 'vue'],
|
||||
execution_mode: null,
|
||||
autocommit_enabled: null,
|
||||
auto_test_enabled: null,
|
||||
worktrees_enabled: null,
|
||||
obsidian_vault_path: null,
|
||||
deploy_command: null,
|
||||
test_command: null,
|
||||
deploy_host: 'srv',
|
||||
deploy_path: '/app',
|
||||
deploy_runtime: 'docker',
|
||||
deploy_restart_cmd: null,
|
||||
created_at: '2024-01-01',
|
||||
total_tasks: 0,
|
||||
done_tasks: 0,
|
||||
active_tasks: 0,
|
||||
blocked_tasks: 0,
|
||||
review_tasks: 0,
|
||||
project_type: null,
|
||||
ssh_host: null,
|
||||
ssh_user: null,
|
||||
ssh_key_path: null,
|
||||
ssh_proxy_jump: null,
|
||||
description: null,
|
||||
tasks: [],
|
||||
decisions: [],
|
||||
modules: [],
|
||||
}
|
||||
|
||||
// TaskFull с deploy_runtime — кнопка Deploy видна только при status=done
|
||||
const DONE_TASK = {
|
||||
id: 'KIN-001',
|
||||
project_id: 'KIN',
|
||||
title: 'Тестовая задача',
|
||||
status: 'done',
|
||||
priority: 5,
|
||||
assigned_role: null,
|
||||
parent_task_id: null,
|
||||
brief: null,
|
||||
spec: null,
|
||||
execution_mode: null,
|
||||
blocked_reason: null,
|
||||
dangerously_skipped: null,
|
||||
category: null,
|
||||
acceptance_criteria: null,
|
||||
created_at: '2024-01-01',
|
||||
updated_at: '2024-01-01',
|
||||
pipeline_steps: [],
|
||||
related_decisions: [],
|
||||
pending_actions: [],
|
||||
project_deploy_command: null,
|
||||
project_deploy_host: 'srv',
|
||||
project_deploy_path: '/app',
|
||||
project_deploy_runtime: 'docker',
|
||||
pipeline_id: null,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
localStorageMock.clear()
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(api.project).mockResolvedValue(BASE_PROJECT as any)
|
||||
vi.mocked(api.getPhases).mockResolvedValue([])
|
||||
vi.mocked(api.environments).mockResolvedValue([])
|
||||
vi.mocked(api.projectLinks).mockResolvedValue([])
|
||||
vi.mocked(api.projects).mockResolvedValue([])
|
||||
vi.mocked(api.patchProject).mockResolvedValue(BASE_PROJECT as any)
|
||||
vi.mocked(api.deployProject).mockResolvedValue({
|
||||
success: true, exit_code: 0, stdout: '', stderr: '', duration_seconds: 1,
|
||||
} as any)
|
||||
vi.mocked(api.taskFull).mockResolvedValue(DONE_TASK as any)
|
||||
vi.mocked(api.notifications).mockResolvedValue([])
|
||||
})
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Helper: смонтировать ProjectView и запустить деплой
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
async function mountProjectViewAndDeploy(deployResult: any) {
|
||||
vi.mocked(api.deployProject).mockResolvedValue(deployResult)
|
||||
const router = makeRouter()
|
||||
await router.push('/project/KIN')
|
||||
const wrapper = mount(ProjectView, {
|
||||
props: { id: 'KIN' },
|
||||
global: { plugins: [router] },
|
||||
})
|
||||
await flushPromises()
|
||||
const deployBtn = wrapper.findAll('button').find(b => b.text().includes('Deploy'))
|
||||
await deployBtn!.trigger('click')
|
||||
await flushPromises()
|
||||
return wrapper
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Helper: смонтировать TaskDetail и запустить деплой
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
async function mountTaskDetailAndDeploy(deployResult: any) {
|
||||
vi.mocked(api.deployProject).mockResolvedValue(deployResult)
|
||||
const router = makeRouter()
|
||||
await router.push('/task/KIN-001')
|
||||
const wrapper = mount(TaskDetail, {
|
||||
props: { id: 'KIN-001' },
|
||||
global: { plugins: [router] },
|
||||
})
|
||||
await flushPromises()
|
||||
const deployBtn = wrapper.findAll('button').find(b => b.text().includes('Deploy'))
|
||||
await deployBtn!.trigger('click')
|
||||
await flushPromises()
|
||||
return wrapper
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// ProjectView — dependents_deployed как string[]
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
describe('ProjectView — dependents_deployed как string[] (KIN-INFRA-011)', () => {
|
||||
it('dep рендерится напрямую как строка — project_id виден в тексте', async () => {
|
||||
const wrapper = await mountProjectViewAndDeploy({
|
||||
success: true, exit_code: 0, stdout: '', stderr: '', duration_seconds: 2,
|
||||
dependents_deployed: ['BARSIK'],
|
||||
overall_success: true,
|
||||
})
|
||||
expect(wrapper.text()).toContain('BARSIK')
|
||||
})
|
||||
|
||||
it('несколько dep-строк — все project_id видны', async () => {
|
||||
const wrapper = await mountProjectViewAndDeploy({
|
||||
success: true, exit_code: 0, stdout: '', stderr: '', duration_seconds: 2,
|
||||
dependents_deployed: ['PROJ-A', 'PROJ-B', 'PROJ-C'],
|
||||
overall_success: true,
|
||||
})
|
||||
expect(wrapper.text()).toContain('PROJ-A')
|
||||
expect(wrapper.text()).toContain('PROJ-B')
|
||||
expect(wrapper.text()).toContain('PROJ-C')
|
||||
})
|
||||
|
||||
it('статичный бейдж "ok" отображается для каждого dependent', async () => {
|
||||
const wrapper = await mountProjectViewAndDeploy({
|
||||
success: true, exit_code: 0, stdout: '', stderr: '', duration_seconds: 2,
|
||||
dependents_deployed: ['FE', 'BE'],
|
||||
overall_success: true,
|
||||
})
|
||||
// Зависимые проекты секция видна
|
||||
expect(wrapper.text()).toContain('Зависимые проекты')
|
||||
// Бейджи "ok" — по одному на каждый dep (два .text-teal-400 span с текстом ok)
|
||||
const okBadges = wrapper.findAll('span.text-teal-400').filter(el => el.text().trim() === 'ok')
|
||||
expect(okBadges.length).toBeGreaterThanOrEqual(2)
|
||||
})
|
||||
|
||||
it('пустой dependents_deployed — секция "Зависимые проекты" не отображается', async () => {
|
||||
const wrapper = await mountProjectViewAndDeploy({
|
||||
success: true, exit_code: 0, stdout: '', stderr: '', duration_seconds: 2,
|
||||
dependents_deployed: [],
|
||||
overall_success: true,
|
||||
})
|
||||
expect(wrapper.text()).not.toContain('Зависимые проекты')
|
||||
})
|
||||
|
||||
it('отсутствующий dependents_deployed — секция не отображается', async () => {
|
||||
const wrapper = await mountProjectViewAndDeploy({
|
||||
success: true, exit_code: 0, stdout: '', stderr: '', duration_seconds: 2,
|
||||
})
|
||||
expect(wrapper.text()).not.toContain('Зависимые проекты')
|
||||
})
|
||||
})
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// TaskDetail — dependents_deployed как string[]
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
describe('TaskDetail — dependents_deployed как string[] (KIN-INFRA-011)', () => {
|
||||
it('dep рендерится напрямую как строка — project_id виден в тексте', async () => {
|
||||
const wrapper = await mountTaskDetailAndDeploy({
|
||||
success: true, exit_code: 0, stdout: '', stderr: '', duration_seconds: 2,
|
||||
dependents_deployed: ['BARSIK'],
|
||||
overall_success: true,
|
||||
})
|
||||
expect(wrapper.text()).toContain('BARSIK')
|
||||
})
|
||||
|
||||
it('несколько dep-строк — все project_id видны', async () => {
|
||||
const wrapper = await mountTaskDetailAndDeploy({
|
||||
success: true, exit_code: 0, stdout: '', stderr: '', duration_seconds: 2,
|
||||
dependents_deployed: ['PROJ-X', 'PROJ-Y'],
|
||||
overall_success: true,
|
||||
})
|
||||
expect(wrapper.text()).toContain('PROJ-X')
|
||||
expect(wrapper.text()).toContain('PROJ-Y')
|
||||
})
|
||||
|
||||
it('статичный бейдж "ok" отображается для каждого dependent', async () => {
|
||||
const wrapper = await mountTaskDetailAndDeploy({
|
||||
success: true, exit_code: 0, stdout: '', stderr: '', duration_seconds: 2,
|
||||
dependents_deployed: ['SVC-1', 'SVC-2'],
|
||||
overall_success: true,
|
||||
})
|
||||
expect(wrapper.text()).toContain('Зависимые проекты')
|
||||
const okBadges = wrapper.findAll('span.text-teal-400').filter(el => el.text().trim() === 'ok')
|
||||
expect(okBadges.length).toBeGreaterThanOrEqual(2)
|
||||
})
|
||||
|
||||
it('пустой dependents_deployed — секция "Зависимые проекты" не отображается', async () => {
|
||||
const wrapper = await mountTaskDetailAndDeploy({
|
||||
success: true, exit_code: 0, stdout: '', stderr: '', duration_seconds: 2,
|
||||
dependents_deployed: [],
|
||||
overall_success: true,
|
||||
})
|
||||
expect(wrapper.text()).not.toContain('Зависимые проекты')
|
||||
})
|
||||
|
||||
it('секция зависимых видна когда dependents_deployed непустой', async () => {
|
||||
const wrapper = await mountTaskDetailAndDeploy({
|
||||
success: true, exit_code: 0, stdout: '', stderr: '', duration_seconds: 2,
|
||||
dependents_deployed: ['SOME-DEP'],
|
||||
overall_success: true,
|
||||
})
|
||||
expect(wrapper.text()).toContain('Зависимые проекты:')
|
||||
expect(wrapper.text()).toContain('SOME-DEP')
|
||||
})
|
||||
})
|
||||
|
|
@ -479,14 +479,12 @@ describe('ProjectView — Deploy результат', () => {
|
|||
stderr: '',
|
||||
duration_seconds: 5,
|
||||
results: [{ step: 'git pull', stdout: 'ok', stderr: '', exit_code: 0 }],
|
||||
dependents_deployed: [
|
||||
{ project_id: 'FE', project_name: 'Frontend App', success: true, results: [] },
|
||||
],
|
||||
dependents_deployed: ['FRONTEND-APP'],
|
||||
overall_success: true,
|
||||
}
|
||||
const wrapper = await clickDeploy(result)
|
||||
expect(wrapper.text()).toContain('Зависимые проекты')
|
||||
expect(wrapper.text()).toContain('Frontend App')
|
||||
expect(wrapper.text()).toContain('FRONTEND-APP')
|
||||
})
|
||||
|
||||
it('overall_success=false → Deploy failed даже если success=true', async () => {
|
||||
|
|
@ -497,16 +495,14 @@ describe('ProjectView — Deploy результат', () => {
|
|||
stderr: '',
|
||||
duration_seconds: 5,
|
||||
results: [{ step: 'git pull', stdout: 'ok', stderr: '', exit_code: 0 }],
|
||||
dependents_deployed: [
|
||||
{ project_id: 'FE', project_name: 'Frontend', success: false, results: [] },
|
||||
],
|
||||
dependents_deployed: ['FE'],
|
||||
overall_success: false,
|
||||
}
|
||||
const wrapper = await clickDeploy(result)
|
||||
expect(wrapper.text()).toContain('Deploy failed')
|
||||
})
|
||||
|
||||
it('dependents показывают статус "ok" или "fail" для каждого проекта', async () => {
|
||||
it('dependents_deployed рендерит каждый project_id строкой с бейджем ok', async () => {
|
||||
const result = {
|
||||
success: true,
|
||||
exit_code: 0,
|
||||
|
|
@ -514,15 +510,12 @@ describe('ProjectView — Deploy результат', () => {
|
|||
stderr: '',
|
||||
duration_seconds: 5,
|
||||
results: [],
|
||||
dependents_deployed: [
|
||||
{ project_id: 'FE', project_name: 'FrontendOK', success: true, results: [] },
|
||||
{ project_id: 'BE2', project_name: 'ServiceFail', success: false, results: [] },
|
||||
],
|
||||
overall_success: false,
|
||||
dependents_deployed: ['FE', 'BE2'],
|
||||
overall_success: true,
|
||||
}
|
||||
const wrapper = await clickDeploy(result)
|
||||
expect(wrapper.text()).toContain('FrontendOK')
|
||||
expect(wrapper.text()).toContain('ServiceFail')
|
||||
expect(wrapper.text()).toContain('FE')
|
||||
expect(wrapper.text()).toContain('BE2')
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -734,7 +727,7 @@ describe('Граничные кейсы', () => {
|
|||
expect(wrapper.find('[class*="text-red"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('overall_success=false с одним failed dependent → "Deploy failed" и dependent виден', async () => {
|
||||
it('overall_success=false с dependents → "Deploy failed" и project_id виден', async () => {
|
||||
const project = { ...BASE_PROJECT, deploy_host: 'srv', deploy_path: '/app', deploy_runtime: 'docker' }
|
||||
vi.mocked(api.deployProject).mockResolvedValue({
|
||||
success: true,
|
||||
|
|
@ -743,9 +736,7 @@ describe('Граничные кейсы', () => {
|
|||
stderr: '',
|
||||
duration_seconds: 5,
|
||||
results: [{ step: 'git pull', stdout: 'ok', stderr: '', exit_code: 0 }],
|
||||
dependents_deployed: [
|
||||
{ project_id: 'FE', project_name: 'FrontendFailed', success: false, results: [] },
|
||||
],
|
||||
dependents_deployed: ['FE'],
|
||||
overall_success: false,
|
||||
} as any)
|
||||
const wrapper = await mountProjectView(project)
|
||||
|
|
@ -753,7 +744,7 @@ describe('Граничные кейсы', () => {
|
|||
await deployBtn!.trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('Deploy failed')
|
||||
expect(wrapper.text()).toContain('FrontendFailed')
|
||||
expect(wrapper.text()).toContain('FE')
|
||||
})
|
||||
|
||||
it('только deploy_host без deploy_path/runtime — кнопка Deploy disabled', async () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue