kin/web/frontend/src/__tests__/deploy-config-clear-fields.test.ts
2026-03-17 18:31:33 +02:00

190 lines
8.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* KIN-INFRA-012: Регрессионный тест — saveDeployConfig не теряет пустые строки
*
* Проверяет, что пустая строка в deploy-полях:
* 1. Передаётся в patchProject как "" (не undefined)
* 2. Переживает JSON.stringify — т.е. бэкенд получает ключ со значением ""
* 3. Непустые значения тоже передаются корректно
*
* До фикса: `deploy_host: deployHosts.value[id] || undefined` → пустая строка
* превращалась в undefined и JSON.stringify отбрасывал поле. Бэкенд не получал
* сигнал очистки. После фикса: пустая строка передаётся как-есть.
*/
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import SettingsView from '../views/SettingsView.vue'
vi.mock('../api', async (importOriginal) => {
const actual = await importOriginal<typeof import('../api')>()
return {
...actual,
api: {
projects: vi.fn(),
patchProject: vi.fn(),
syncObsidian: vi.fn(),
},
}
})
import { api } from '../api'
const BASE_PROJECT = {
id: 'KIN',
name: 'Kin',
path: '/projects/kin',
status: 'active',
priority: 5,
tech_stack: ['python'],
execution_mode: null,
autocommit_enabled: null,
auto_test_enabled: null,
obsidian_vault_path: null,
deploy_command: null,
test_command: null,
deploy_host: null,
deploy_path: null,
deploy_runtime: null,
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,
}
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(api.patchProject).mockResolvedValue(BASE_PROJECT as any)
})
async function mountSettingsWithProject(overrides: Partial<typeof BASE_PROJECT> = {}) {
const project = { ...BASE_PROJECT, ...overrides }
vi.mocked(api.projects).mockResolvedValue([project as any])
const wrapper = mount(SettingsView)
await flushPromises()
return wrapper
}
async function clickSaveDeployConfig(wrapper: ReturnType<typeof mount>) {
const saveBtn = wrapper.findAll('button').find(b => b.text().includes('Save Deploy Config'))
expect(saveBtn).toBeDefined()
await saveBtn!.trigger('click')
await flushPromises()
}
// ─────────────────────────────────────────────────────────────
// 1. Пустая строка передаётся как "" — не теряется как undefined
// ─────────────────────────────────────────────────────────────
describe('saveDeployConfig — пустые строки сохраняются в payload', () => {
it('deploy_host="" передаётся как "" когда поле пустое (не undefined)', async () => {
// null в проекте → инициализируется как '' в компоненте → должно передаться как ""
const wrapper = await mountSettingsWithProject({ deploy_host: null })
await clickSaveDeployConfig(wrapper)
const callArgs = vi.mocked(api.patchProject).mock.calls[0]
expect(callArgs[1]).toHaveProperty('deploy_host')
expect((callArgs[1] as any).deploy_host).toBe('')
})
it('deploy_path="" передаётся как "" когда поле пустое (не undefined)', async () => {
const wrapper = await mountSettingsWithProject({ deploy_path: null })
await clickSaveDeployConfig(wrapper)
const callArgs = vi.mocked(api.patchProject).mock.calls[0]
expect(callArgs[1]).toHaveProperty('deploy_path')
expect((callArgs[1] as any).deploy_path).toBe('')
})
it('deploy_runtime="" передаётся как "" когда поле пустое (не undefined)', async () => {
const wrapper = await mountSettingsWithProject({ deploy_runtime: null })
await clickSaveDeployConfig(wrapper)
const callArgs = vi.mocked(api.patchProject).mock.calls[0]
expect(callArgs[1]).toHaveProperty('deploy_runtime')
expect((callArgs[1] as any).deploy_runtime).toBe('')
})
it('deploy_restart_cmd="" передаётся как "" когда поле пустое (не undefined)', async () => {
const wrapper = await mountSettingsWithProject({ deploy_restart_cmd: null })
await clickSaveDeployConfig(wrapper)
const callArgs = vi.mocked(api.patchProject).mock.calls[0]
expect(callArgs[1]).toHaveProperty('deploy_restart_cmd')
expect((callArgs[1] as any).deploy_restart_cmd).toBe('')
})
})
// ─────────────────────────────────────────────────────────────
// 2. JSON.stringify не выбрасывает пустые строки — бэкенд их получит
// ─────────────────────────────────────────────────────────────
describe('saveDeployConfig — пустые строки выживают JSON.stringify', () => {
it('все 4 поля присутствуют в JSON когда все значения пустые', async () => {
// Проект с null во всех deploy-полях → инициализируются как ''
const wrapper = await mountSettingsWithProject()
await clickSaveDeployConfig(wrapper)
const callArgs = vi.mocked(api.patchProject).mock.calls[0]
const payload = callArgs[1] as Record<string, unknown>
// Сериализуем как настоящий PATCH-запрос
const jsonBody = JSON.parse(JSON.stringify(payload))
expect(jsonBody).toHaveProperty('deploy_host', '')
expect(jsonBody).toHaveProperty('deploy_path', '')
expect(jsonBody).toHaveProperty('deploy_runtime', '')
expect(jsonBody).toHaveProperty('deploy_restart_cmd', '')
})
it('пустая строка выживает JSON.stringify (регрессия: undefined исчезает)', async () => {
const wrapper = await mountSettingsWithProject({ deploy_host: null })
await clickSaveDeployConfig(wrapper)
const callArgs = vi.mocked(api.patchProject).mock.calls[0]
const payload = callArgs[1] as Record<string, unknown>
const jsonBody = JSON.parse(JSON.stringify(payload))
// "" должна присутствовать в JSON как пустая строка
// undefined здесь отсутствовало бы — это был бы баг
expect(Object.keys(jsonBody)).toContain('deploy_host')
expect(jsonBody.deploy_host).toBe('')
expect(jsonBody.deploy_host).not.toBeUndefined()
})
})
// ─────────────────────────────────────────────────────────────
// 3. Непустые значения передаются корректно
// ─────────────────────────────────────────────────────────────
describe('saveDeployConfig — непустые значения передаются корректно', () => {
it('deploy_host с непустым значением передаётся без изменений', async () => {
const wrapper = await mountSettingsWithProject({ deploy_host: 'prod.example.com' })
await clickSaveDeployConfig(wrapper)
const callArgs = vi.mocked(api.patchProject).mock.calls[0]
expect((callArgs[1] as any).deploy_host).toBe('prod.example.com')
})
it('все 4 поля с непустыми значениями передаются корректно', async () => {
const wrapper = await mountSettingsWithProject({
deploy_host: 'prod.example.com',
deploy_path: '/opt/kin',
deploy_runtime: 'docker',
deploy_restart_cmd: 'docker compose restart',
})
await clickSaveDeployConfig(wrapper)
const callArgs = vi.mocked(api.patchProject).mock.calls[0]
const payload = callArgs[1] as any
expect(payload.deploy_host).toBe('prod.example.com')
expect(payload.deploy_path).toBe('/opt/kin')
expect(payload.deploy_runtime).toBe('docker')
expect(payload.deploy_restart_cmd).toBe('docker compose restart')
})
})