From b75269fa6c53aac72e9c9a62d60335eb25bc86b5 Mon Sep 17 00:00:00 2001 From: Gros Frumos Date: Tue, 17 Mar 2026 18:54:02 +0200 Subject: [PATCH] kin: auto-commit after pipeline --- .../src/__tests__/deploy-standardized.test.ts | 81 +++++++++++++++++++ .../src/__tests__/filter-persistence.test.ts | 22 +++++ web/frontend/src/api.ts | 2 + web/frontend/src/views/SettingsView.vue | 6 +- 4 files changed, 108 insertions(+), 3 deletions(-) diff --git a/web/frontend/src/__tests__/deploy-standardized.test.ts b/web/frontend/src/__tests__/deploy-standardized.test.ts index 9593a5e..7a90e83 100644 --- a/web/frontend/src/__tests__/deploy-standardized.test.ts +++ b/web/frontend/src/__tests__/deploy-standardized.test.ts @@ -235,6 +235,61 @@ describe('SettingsView — Deploy Config', () => { }) }) +// ───────────────────────────────────────────────────────────── +// 1b. SettingsView — Project Links +// ───────────────────────────────────────────────────────────── +describe('SettingsView — Project Links', () => { + it('link.type рендерится корректно в списке связей — не undefined (конвенция #527)', async () => { + // Bug #527: шаблон использовал {{ link.link_type }} вместо {{ link.type }} → runtime undefined + vi.mocked(api.projectLinks).mockResolvedValue([ + { id: 1, from_project: 'KIN', to_project: 'BRS', type: 'triggers', description: null, created_at: '2026-01-01' }, + ] as any) + vi.mocked(api.projects).mockResolvedValue([BASE_PROJECT as any]) + const wrapper = mount(SettingsView) + await flushPromises() + + // Тип связи должен отображаться как строка, а не undefined + expect(wrapper.text()).toContain('triggers') + expect(wrapper.text()).not.toContain('undefined') + }) + + it('addLink вызывает createProjectLink с полем type (не link_type) из формы', async () => { + // Bug #527: addLink() использовал link_type вместо type при вызове api + vi.mocked(api.projects).mockResolvedValue([ + { ...BASE_PROJECT, id: 'BRS', name: 'Barsik' } as any, + BASE_PROJECT as any, + ]) + vi.mocked(api.projectLinks).mockResolvedValue([]) + const wrapper = mount(SettingsView) + await flushPromises() + + // Открываем форму добавления связи для первого проекта + const addBtns = wrapper.findAll('button').filter(b => b.text().includes('+ Add Link')) + if (addBtns.length > 0) { + await addBtns[0].trigger('click') + await flushPromises() + + // Выбираем to_project (в SettingsView это select без api.projects — используем allProjectList) + const selects = wrapper.findAll('select') + const toProjectSelect = selects.find(s => s.findAll('option').some(o => o.element.value !== '' && o.element.value !== 'depends_on' && o.element.value !== 'triggers' && o.element.value !== 'related_to')) + if (toProjectSelect) { + const opts = toProjectSelect.findAll('option').filter(o => o.element.value !== '') + if (opts.length > 0) await toProjectSelect.setValue(opts[0].element.value) + } + + const form = wrapper.find('form') + await form.trigger('submit') + await flushPromises() + + if (vi.mocked(api.createProjectLink).mock.calls.length > 0) { + const callArg = vi.mocked(api.createProjectLink).mock.calls[0][0] + expect(callArg).toHaveProperty('type') + expect(callArg).not.toHaveProperty('link_type') + } + } + }) +}) + // ───────────────────────────────────────────────────────────── // 2. ProjectView — Deploy кнопка // ───────────────────────────────────────────────────────────── @@ -603,6 +658,32 @@ describe('ProjectView — Links таб', () => { })) }) + it('createProjectLink передаёт поле type (не link_type) — конвенция #527', async () => { + // Bug #527: addLink() передавал link_type вместо type → поле type отсутствовало в запросе + vi.mocked(api.projects).mockResolvedValue([ + { ...BASE_PROJECT, id: 'BRS', name: 'Barsik' } as any, + ]) + const wrapper = await mountProjectView() + await switchToLinksTab(wrapper) + + const plusBtn = wrapper.findAll('button').find(b => b.text().includes('+') && b.text().includes('Link')) + await plusBtn!.trigger('click') + await flushPromises() + + const selects = wrapper.findAll('select') + const toProjectSelect = selects.find(s => s.findAll('option').some(o => o.text().includes('BRS'))) + if (toProjectSelect) await toProjectSelect.setValue('BRS') + + const form = wrapper.find('form') + await form.trigger('submit') + await flushPromises() + + const callArg = vi.mocked(api.createProjectLink).mock.calls[0]?.[0] + expect(callArg).toBeDefined() + expect(callArg).toHaveProperty('type') + expect(callArg).not.toHaveProperty('link_type') + }) + it('Delete вызывает api.deleteProjectLink с id связи', async () => { const links = [ { id: 7, from_project: 'KIN', to_project: 'BRS', type: 'depends_on', description: null, created_at: '2026-01-01' }, diff --git a/web/frontend/src/__tests__/filter-persistence.test.ts b/web/frontend/src/__tests__/filter-persistence.test.ts index 7a784ac..b313950 100644 --- a/web/frontend/src/__tests__/filter-persistence.test.ts +++ b/web/frontend/src/__tests__/filter-persistence.test.ts @@ -905,6 +905,28 @@ describe('KIN-049: TaskDetail — кнопка Deploy', () => { expect(hasDeployBtn, 'Deploy не должна быть видна при статусе review').toBe(false) }) + it('Кнопка Deploy видна при status=done с обоими полями одновременно (сосуществование #520)', async () => { + // Конвенция #520: legacy deploy_command и новый deploy_runtime могут быть заданы одновременно + vi.mocked(api.taskFull).mockResolvedValue(makeDeployTask('done', 'echo deploy.sh', 'node') as any) + const router = makeRouter() + await router.push('/task/KIN-049') + + const wrapper = mount(TaskDetail, { + props: { id: 'KIN-049' }, + global: { plugins: [router] }, + }) + await flushPromises() + + const deployBtn = wrapper.findAll('button').find(b => b.text().includes('Deploy')) + expect(deployBtn?.exists(), 'Deploy должна быть видна при одновременном наличии deploy_command и deploy_runtime').toBe(true) + }) + + it('makeDeployTask factory использует null по умолчанию для deployRuntime (конвенция #521)', () => { + // Конвенция #521: project_deploy_runtime: null — явное значение по умолчанию + const task = makeDeployTask('done', 'echo deploy.sh') + expect(task.project_deploy_runtime).toBe(null) + }) + it('Клик по Deploy вызывает api.deployProject с project_id задачи', async () => { vi.mocked(api.taskFull).mockResolvedValue(makeDeployTask('done', 'echo ok') as any) vi.mocked(api.deployProject).mockResolvedValue({ diff --git a/web/frontend/src/api.ts b/web/frontend/src/api.ts index 1002848..8c2e8a0 100644 --- a/web/frontend/src/api.ts +++ b/web/frontend/src/api.ts @@ -190,6 +190,8 @@ export interface ProjectLink { id: number from_project: string to_project: string + // WARNING (decision #527): поле называется `type` — так отдаёт бэкенд. + // НЕ переименовывать в link_type — это вызовет runtime undefined во всех компонентах. type: string description: string | null created_at: string diff --git a/web/frontend/src/views/SettingsView.vue b/web/frontend/src/views/SettingsView.vue index 9cd847e..88b3984 100644 --- a/web/frontend/src/views/SettingsView.vue +++ b/web/frontend/src/views/SettingsView.vue @@ -65,7 +65,7 @@ async function saveDeployConfig(projectId: string) { deploy_path: deployPaths.value[projectId], deploy_runtime: deployRuntimes.value[projectId], deploy_restart_cmd: deployRestartCmds.value[projectId], - deploy_command: deployCommands.value[projectId] || undefined, + deploy_command: deployCommands.value[projectId], }) saveDeployConfigStatus.value[projectId] = 'Saved' } catch (e: unknown) { @@ -150,7 +150,7 @@ async function addLink(projectId: string) { await api.createProjectLink({ from_project: projectId, to_project: form.to_project, - link_type: form.link_type, + type: form.link_type, description: form.description || undefined, }) showAddLinkForm.value[projectId] = false @@ -306,7 +306,7 @@ async function deleteLink(projectId: string, linkId: number) { {{ link.from_project }} {{ link.to_project }} - {{ link.link_type }} + {{ link.type }} {{ link.description }}