kin: auto-commit after pipeline
This commit is contained in:
parent
c614bf6bce
commit
f0a69ed1d3
3 changed files with 436 additions and 1 deletions
|
|
@ -219,7 +219,8 @@ CREATE TABLE IF NOT EXISTS project_links (
|
||||||
to_project TEXT NOT NULL REFERENCES projects(id),
|
to_project TEXT NOT NULL REFERENCES projects(id),
|
||||||
type TEXT NOT NULL,
|
type TEXT NOT NULL,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE(from_project, to_project, type)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_project_links_to ON project_links(to_project);
|
CREATE INDEX IF NOT EXISTS idx_project_links_to ON project_links(to_project);
|
||||||
|
|
|
||||||
244
web/frontend/src/__tests__/agent-output-parsing.test.ts
Normal file
244
web/frontend/src/__tests__/agent-output-parsing.test.ts
Normal file
|
|
@ -0,0 +1,244 @@
|
||||||
|
/**
|
||||||
|
* KIN-100: parseAgentOutput — человекочитаемый вывод агентов
|
||||||
|
*
|
||||||
|
* Acceptance Criteria (проверяем через рендеринг TaskDetail.vue):
|
||||||
|
* 1. Вывод с ## Verdict → verdict текст показывается читаемым текстом
|
||||||
|
* 2. Вывод с ## Details → JSON скрыт за <details> с "подробнее"
|
||||||
|
* 3. Старый формат (без ## Verdict) → fallback на <pre> без <details>
|
||||||
|
* 4. Null output → нет ни verdict, ни details элементов
|
||||||
|
* 5. ## Verdict без ## Details → verdict показывается, <details> нет
|
||||||
|
* 6. Невалидный JSON в ## Details → сырой текст в <details>
|
||||||
|
*/
|
||||||
|
|
||||||
|
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(),
|
||||||
|
getAttachments: vi.fn(),
|
||||||
|
approveTask: vi.fn(),
|
||||||
|
rejectTask: vi.fn(),
|
||||||
|
reviseTask: vi.fn(),
|
||||||
|
deployProject: vi.fn(),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
import { api } from '../api'
|
||||||
|
|
||||||
|
const Stub = { template: '<div />' }
|
||||||
|
|
||||||
|
function makeRouter() {
|
||||||
|
return createRouter({
|
||||||
|
history: createMemoryHistory(),
|
||||||
|
routes: [
|
||||||
|
{ path: '/', component: Stub },
|
||||||
|
{ path: '/task/:id', component: TaskDetail, props: true },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вывод в новом формате: ## Verdict + ## Details с JSON
|
||||||
|
const NEW_FORMAT_OUTPUT = `## Verdict
|
||||||
|
Написано 4 теста, все прошли успешно. Покрытие полное. Можно закрывать.
|
||||||
|
|
||||||
|
## Details
|
||||||
|
\`\`\`json
|
||||||
|
{"status": "passed", "tests_run": 10, "tests_passed": 10, "tests_failed": 0}
|
||||||
|
\`\`\``
|
||||||
|
|
||||||
|
// Вывод только с Verdict, без Details
|
||||||
|
const VERDICT_ONLY_OUTPUT = `## Verdict
|
||||||
|
Реализация корректна, безопасность соблюдена. Задачу можно закрывать.`
|
||||||
|
|
||||||
|
// Вывод в старом формате (plain JSON, без ## Verdict)
|
||||||
|
const OLD_FORMAT_OUTPUT = JSON.stringify({
|
||||||
|
status: 'done',
|
||||||
|
changes: [{ file: 'core/models.py', description: 'Fixed the bug' }],
|
||||||
|
})
|
||||||
|
|
||||||
|
// Вывод с невалидным JSON в Details
|
||||||
|
const INVALID_JSON_OUTPUT = `## Verdict
|
||||||
|
Тесты прошли, всё ок.
|
||||||
|
|
||||||
|
## Details
|
||||||
|
\`\`\`json
|
||||||
|
{not valid json here
|
||||||
|
\`\`\``
|
||||||
|
|
||||||
|
function makeTask(outputSummary: string | null) {
|
||||||
|
return {
|
||||||
|
id: 'KIN-001',
|
||||||
|
project_id: 'KIN',
|
||||||
|
title: 'Test Task',
|
||||||
|
status: 'done',
|
||||||
|
priority: 5,
|
||||||
|
assigned_role: null,
|
||||||
|
parent_task_id: null,
|
||||||
|
brief: null,
|
||||||
|
spec: null,
|
||||||
|
execution_mode: null,
|
||||||
|
blocked_reason: null,
|
||||||
|
pipeline_id: null,
|
||||||
|
dangerously_skipped: false,
|
||||||
|
category: null,
|
||||||
|
acceptance_criteria: null,
|
||||||
|
created_at: '2024-01-01',
|
||||||
|
updated_at: '2024-01-01',
|
||||||
|
pipeline_steps: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
agent_role: 'tester',
|
||||||
|
action: 'test',
|
||||||
|
output_summary: outputSummary,
|
||||||
|
success: true,
|
||||||
|
duration_seconds: 30,
|
||||||
|
tokens_used: 1000,
|
||||||
|
cost_usd: 0.01,
|
||||||
|
model: null,
|
||||||
|
created_at: '2024-01-01',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
related_decisions: [],
|
||||||
|
project_deploy_command: null,
|
||||||
|
project_deploy_runtime: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
vi.mocked(api.getAttachments).mockResolvedValue([] as any)
|
||||||
|
vi.mocked(api.patchTask).mockResolvedValue({} as any)
|
||||||
|
})
|
||||||
|
|
||||||
|
async function mountAndSelectStep(outputSummary: string | null) {
|
||||||
|
vi.mocked(api.taskFull).mockResolvedValue(makeTask(outputSummary) as any)
|
||||||
|
|
||||||
|
const router = makeRouter()
|
||||||
|
await router.push('/task/KIN-001')
|
||||||
|
|
||||||
|
const wrapper = mount(TaskDetail, {
|
||||||
|
props: { id: 'KIN-001' },
|
||||||
|
global: { plugins: [router] },
|
||||||
|
})
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
// Находим и кликаем кнопку pipeline шага (border + rounded-lg)
|
||||||
|
const stepBtn = wrapper.find('button.rounded-lg')
|
||||||
|
expect(stepBtn.exists(), 'кнопка pipeline шага должна быть в DOM').toBe(true)
|
||||||
|
await stepBtn.trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('KIN-100: parseAgentOutput — новый формат (Verdict + Details)', () => {
|
||||||
|
it('показывает текст Verdict читаемым текстом (не JSON)', async () => {
|
||||||
|
const wrapper = await mountAndSelectStep(NEW_FORMAT_OUTPUT)
|
||||||
|
|
||||||
|
const text = wrapper.text()
|
||||||
|
expect(text).toContain('Написано 4 теста, все прошли успешно. Покрытие полное. Можно закрывать.')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('скрывает JSON Details за <details> элементом', async () => {
|
||||||
|
const wrapper = await mountAndSelectStep(NEW_FORMAT_OUTPUT)
|
||||||
|
|
||||||
|
const details = wrapper.find('details')
|
||||||
|
expect(details.exists(), '<details> должен присутствовать для скрытия JSON').toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('summary в <details> содержит текст "подробнее"', async () => {
|
||||||
|
const wrapper = await mountAndSelectStep(NEW_FORMAT_OUTPUT)
|
||||||
|
|
||||||
|
const summary = wrapper.find('details summary')
|
||||||
|
expect(summary.exists()).toBe(true)
|
||||||
|
expect(summary.text()).toContain('подробнее')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('JSON из Details виден внутри <details><pre>', async () => {
|
||||||
|
const wrapper = await mountAndSelectStep(NEW_FORMAT_OUTPUT)
|
||||||
|
|
||||||
|
const pre = wrapper.find('details pre')
|
||||||
|
expect(pre.exists(), '<pre> с JSON должен быть внутри <details>').toBe(true)
|
||||||
|
expect(pre.text()).toContain('"status"')
|
||||||
|
expect(pre.text()).toContain('"passed"')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Verdict рендерится как параграф, не как <pre>', async () => {
|
||||||
|
const wrapper = await mountAndSelectStep(NEW_FORMAT_OUTPUT)
|
||||||
|
|
||||||
|
// Verdict должен быть в <p>, не в <pre>
|
||||||
|
const p = wrapper.findAll('p').find(el =>
|
||||||
|
el.text().includes('Написано 4 теста')
|
||||||
|
)
|
||||||
|
expect(p, 'verdict должен быть в <p> элементе').toBeDefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('KIN-100: parseAgentOutput — только Verdict без Details', () => {
|
||||||
|
it('показывает Verdict текст', async () => {
|
||||||
|
const wrapper = await mountAndSelectStep(VERDICT_ONLY_OUTPUT)
|
||||||
|
|
||||||
|
expect(wrapper.text()).toContain('Реализация корректна, безопасность соблюдена.')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('не показывает <details> когда ## Details отсутствует', async () => {
|
||||||
|
const wrapper = await mountAndSelectStep(VERDICT_ONLY_OUTPUT)
|
||||||
|
|
||||||
|
const details = wrapper.find('details')
|
||||||
|
expect(details.exists(), '<details> не должен быть без ## Details секции').toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('KIN-100: parseAgentOutput — fallback для старого формата', () => {
|
||||||
|
it('не показывает <details> для старого JSON-формата', async () => {
|
||||||
|
const wrapper = await mountAndSelectStep(OLD_FORMAT_OUTPUT)
|
||||||
|
|
||||||
|
const details = wrapper.find('details')
|
||||||
|
expect(details.exists(), 'старый формат не должен создавать <details>').toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('показывает содержимое через formatOutput (pre с JSON)', async () => {
|
||||||
|
const wrapper = await mountAndSelectStep(OLD_FORMAT_OUTPUT)
|
||||||
|
|
||||||
|
// В fallback режиме содержимое JSON должно быть видно
|
||||||
|
expect(wrapper.text()).toContain('core/models.py')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('KIN-100: parseAgentOutput — null/пустой вывод', () => {
|
||||||
|
it('не падает при null output_summary', async () => {
|
||||||
|
// Не должно быть exception, компонент рендерится нормально
|
||||||
|
const wrapper = await mountAndSelectStep(null)
|
||||||
|
expect(wrapper.exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('не создаёт <details> для null вывода', async () => {
|
||||||
|
const wrapper = await mountAndSelectStep(null)
|
||||||
|
|
||||||
|
const details = wrapper.find('details')
|
||||||
|
expect(details.exists()).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('KIN-100: parseAgentOutput — невалидный JSON в Details', () => {
|
||||||
|
it('показывает raw текст в <details> когда JSON невалидный', async () => {
|
||||||
|
const wrapper = await mountAndSelectStep(INVALID_JSON_OUTPUT)
|
||||||
|
|
||||||
|
// Verdict должен показываться
|
||||||
|
expect(wrapper.text()).toContain('Тесты прошли, всё ок.')
|
||||||
|
|
||||||
|
// <details> должен присутствовать (так как ## Details секция есть)
|
||||||
|
const details = wrapper.find('details')
|
||||||
|
expect(details.exists(), '<details> должен быть даже при невалидном JSON').toBe(true)
|
||||||
|
|
||||||
|
// Raw text должен быть виден в <pre>
|
||||||
|
const pre = details.find('pre')
|
||||||
|
expect(pre.exists()).toBe(true)
|
||||||
|
expect(pre.text()).toContain('not valid json here')
|
||||||
|
})
|
||||||
|
})
|
||||||
190
web/frontend/src/__tests__/deploy-config-clear-fields.test.ts
Normal file
190
web/frontend/src/__tests__/deploy-config-clear-fields.test.ts
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
/**
|
||||||
|
* 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')
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Add table
Add a link
Reference in a new issue