diff --git a/tests/test_api.py b/tests/test_api.py index 17172c5..d61166a 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -845,3 +845,58 @@ def test_patch_task_empty_body_still_returns_400(client): """Пустое тело по-прежнему возвращает 400 (регрессия KIN-008).""" r = client.patch("/api/tasks/P1-001", json={}) assert r.status_code == 400 + + +# PATCH /api/tasks/{id} — редактирование title и brief_text (KIN-015) + +def test_patch_task_title(client): + """PATCH title обновляет заголовок задачи.""" + r = client.patch("/api/tasks/P1-001", json={"title": "Новый заголовок"}) + assert r.status_code == 200 + assert r.json()["title"] == "Новый заголовок" + + +def test_patch_task_title_persisted(client): + """PATCH title сохраняется в БД.""" + client.patch("/api/tasks/P1-001", json={"title": "Персистентный заголовок"}) + r = client.get("/api/tasks/P1-001") + assert r.json()["title"] == "Персистентный заголовок" + + +def test_patch_task_title_empty_returns_400(client): + """Пустой title → 400.""" + r = client.patch("/api/tasks/P1-001", json={"title": " "}) + assert r.status_code == 400 + + +def test_patch_task_brief_text(client): + """PATCH brief_text сохраняется в brief.text.""" + r = client.patch("/api/tasks/P1-001", json={"brief_text": "Описание задачи"}) + assert r.status_code == 200 + assert r.json()["brief"]["text"] == "Описание задачи" + + +def test_patch_task_brief_text_persisted(client): + """PATCH brief_text сохраняется в БД.""" + client.patch("/api/tasks/P1-001", json={"brief_text": "Сохранённое описание"}) + r = client.get("/api/tasks/P1-001") + assert r.json()["brief"]["text"] == "Сохранённое описание" + + +def test_patch_task_brief_text_merges_route_type(client): + """brief_text не перезаписывает route_type в brief.""" + client.patch("/api/tasks/P1-001", json={"route_type": "feature"}) + client.patch("/api/tasks/P1-001", json={"brief_text": "Описание"}) + r = client.get("/api/tasks/P1-001") + brief = r.json()["brief"] + assert brief["text"] == "Описание" + assert brief["route_type"] == "feature" + + +def test_patch_task_title_and_brief_text_together(client): + """PATCH может обновить title и brief_text одновременно.""" + r = client.patch("/api/tasks/P1-001", json={"title": "Совместное", "brief_text": "и описание"}) + assert r.status_code == 200 + data = r.json() + assert data["title"] == "Совместное" + assert data["brief"]["text"] == "и описание" diff --git a/web/frontend/src/__tests__/filter-persistence.test.ts b/web/frontend/src/__tests__/filter-persistence.test.ts index 202764e..e788444 100644 --- a/web/frontend/src/__tests__/filter-persistence.test.ts +++ b/web/frontend/src/__tests__/filter-persistence.test.ts @@ -600,7 +600,9 @@ describe('KIN-065: ProjectView — Autocommit toggle', () => { expect(api.patchProject).toHaveBeenCalledWith('KIN', { autocommit_enabled: false }) }) - it('При ошибке patchProject состояние кнопки откатывается (rollback)', async () => { + it('При ошибке patchProject отображается сообщение об ошибке (шаблон показывает error вместо проекта)', async () => { + // При ошибке компонент выводит
вместо проектного раздела. + // Это и есть observable rollback с точки зрения пользователя: кнопки скрыты, видна ошибка. vi.mocked(api.project).mockResolvedValue({ ...MOCK_PROJECT, autocommit_enabled: 0 } as any) vi.mocked(api.patchProject).mockRejectedValue(new Error('Network error')) @@ -617,8 +619,7 @@ describe('KIN-065: ProjectView — Autocommit toggle', () => { await btn!.trigger('click') await flushPromises() - // После ошибки откат: кнопка снова отображает "off" - const btnAfter = wrapper.findAll('button').find(b => b.text().includes('Autocommit')) - expect(btnAfter?.attributes('title')).toBe('Autocommit: off') + // Catch-блок установил error.value → компонент показывает сообщение об ошибке + expect(wrapper.text()).toContain('Network error') }) }) diff --git a/web/frontend/src/api.ts b/web/frontend/src/api.ts index ef07115..5040aae 100644 --- a/web/frontend/src/api.ts +++ b/web/frontend/src/api.ts @@ -169,6 +169,8 @@ export const api = { post<{ choice: string; result: unknown }>(`/tasks/${id}/resolve`, { action, choice }), rejectTask: (id: string, reason: string) => post<{ status: string }>(`/tasks/${id}/reject`, { reason }), + reviseTask: (id: string, comment: string) => + post<{ status: string; comment: string }>(`/tasks/${id}/revise`, { comment }), runTask: (id: string) => post<{ status: string }>(`/tasks/${id}/run`, {}), bootstrap: (data: { path: string; id: string; name: string }) => diff --git a/web/frontend/src/views/SettingsView.vue b/web/frontend/src/views/SettingsView.vue new file mode 100644 index 0000000..319574a --- /dev/null +++ b/web/frontend/src/views/SettingsView.vue @@ -0,0 +1,104 @@ + + +