/** * 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() 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 = {}) { 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) { 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 // Сериализуем как настоящий 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 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') }) })