diff --git a/tests/test_kin_126_regression.py b/tests/test_kin_126_regression.py new file mode 100644 index 0000000..5c1a56d --- /dev/null +++ b/tests/test_kin_126_regression.py @@ -0,0 +1,108 @@ +"""Regression tests for KIN-126 — фильтр по дате выполнения задач. + +Проверяет backend-поведение: +1. Колонка completed_at добавляется в таблицу tasks при инициализации БД +2. Новая задача имеет completed_at=None по умолчанию +3. update_task(status='done') устанавливает completed_at как ISO-строку +4. update_task(status='done') — completed_at является валидной ISO-строкой +5. update_task(status='in_progress') не устанавливает completed_at +6. update_task(status='cancelled') не устанавливает completed_at +7. Обновление полей без смены статуса на done не трогает completed_at +8. update_task(status='blocked') не устанавливает completed_at +""" + +import pytest +from datetime import datetime +from core.db import init_db +from core import models + + +@pytest.fixture +def conn(): + """Fresh in-memory DB for each test.""" + c = init_db(db_path=":memory:") + yield c + c.close() + + +# --------------------------------------------------------------------------- +# Schema +# --------------------------------------------------------------------------- + +def test_schema_tasks_has_completed_at_column(conn): + """KIN-126: таблица tasks содержит колонку completed_at после инициализации БД.""" + cols = {r[1] for r in conn.execute("PRAGMA table_info(tasks)").fetchall()} + assert "completed_at" in cols, "KIN-126: колонка completed_at должна быть в таблице tasks" + + +# --------------------------------------------------------------------------- +# Default value +# --------------------------------------------------------------------------- + +def test_new_task_completed_at_is_null(conn): + """KIN-126: новая задача имеет completed_at=None по умолчанию.""" + models.create_project(conn, "p1", "P1", "/p1") + t = models.create_task(conn, "P1-001", "p1", "Task") + assert t["completed_at"] is None + + +# --------------------------------------------------------------------------- +# Setting completed_at on status='done' +# --------------------------------------------------------------------------- + +def test_update_task_to_done_sets_completed_at(conn): + """KIN-126: update_task(status='done') устанавливает completed_at.""" + models.create_project(conn, "p1", "P1", "/p1") + models.create_task(conn, "P1-001", "p1", "Task") + updated = models.update_task(conn, "P1-001", status="done") + assert updated["completed_at"] is not None + + +def test_update_task_to_done_completed_at_is_valid_iso(conn): + """KIN-126: completed_at при status='done' — валидная ISO-строка datetime.""" + models.create_project(conn, "p1", "P1", "/p1") + models.create_task(conn, "P1-001", "p1", "Task") + updated = models.update_task(conn, "P1-001", status="done") + # Must parse without exception + parsed = datetime.fromisoformat(updated["completed_at"]) + assert parsed is not None + + +# --------------------------------------------------------------------------- +# Non-done statuses do NOT set completed_at +# --------------------------------------------------------------------------- + +def test_update_task_to_in_progress_does_not_set_completed_at(conn): + """KIN-126: update_task(status='in_progress') не устанавливает completed_at.""" + models.create_project(conn, "p1", "P1", "/p1") + models.create_task(conn, "P1-001", "p1", "Task") + updated = models.update_task(conn, "P1-001", status="in_progress") + assert updated["completed_at"] is None + + +def test_update_task_to_cancelled_does_not_set_completed_at(conn): + """KIN-126: update_task(status='cancelled') не устанавливает completed_at.""" + models.create_project(conn, "p1", "P1", "/p1") + models.create_task(conn, "P1-001", "p1", "Task") + updated = models.update_task(conn, "P1-001", status="cancelled") + assert updated["completed_at"] is None + + +def test_update_task_to_blocked_does_not_set_completed_at(conn): + """KIN-126: update_task(status='blocked') не устанавливает completed_at.""" + models.create_project(conn, "p1", "P1", "/p1") + models.create_task(conn, "P1-001", "p1", "Task") + updated = models.update_task(conn, "P1-001", status="blocked") + assert updated["completed_at"] is None + + +# --------------------------------------------------------------------------- +# Non-status updates do NOT touch completed_at +# --------------------------------------------------------------------------- + +def test_update_task_priority_only_does_not_set_completed_at(conn): + """KIN-126: update_task с изменением только priority не трогает completed_at.""" + models.create_project(conn, "p1", "P1", "/p1") + models.create_task(conn, "P1-001", "p1", "Task") + updated = models.update_task(conn, "P1-001", priority=1) + assert updated["completed_at"] is None diff --git a/web/frontend/src/__tests__/date-filter.test.ts b/web/frontend/src/__tests__/date-filter.test.ts new file mode 100644 index 0000000..869308a --- /dev/null +++ b/web/frontend/src/__tests__/date-filter.test.ts @@ -0,0 +1,283 @@ +/** + * KIN-126: Тесты фильтра по дате выполнения задач (done tasks date range filter) + * + * AC: фильтр сделан и работает + * + * Проверяет: + * 1. Date filter inputs отображаются только когда выбран статус 'done' + * 2. Date filter inputs скрыты на других статусах + * 3. Date filter inputs скрыты без фильтра статуса + * 4. Фильтрация по dateFrom — задачи до этой даты исключаются + * 5. Фильтрация по dateTo — задачи после этой даты исключаются + * 6. Пустой результат при несовпадении дат + * 7. Сброс фильтра (кнопка ✕) показывает все done-задачи снова + * 8. Fallback на updated_at когда completed_at=null + */ + +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { mount, flushPromises } from '@vue/test-utils' +import { createRouter, createMemoryHistory } from 'vue-router' +import ProjectView from '../views/ProjectView.vue' + +vi.mock('../api', () => ({ + api: { + project: vi.fn(), + taskFull: vi.fn(), + runTask: vi.fn(), + auditProject: vi.fn(), + createTask: vi.fn(), + patchTask: vi.fn(), + patchProject: vi.fn(), + deployProject: vi.fn(), + }, +})) + +import { api } from '../api' + +const Stub = { template: '
' } + +function makeTask( + id: string, + status: string, + completedAt: string | null, + updatedAt = '2024-01-10T00:00:00', +) { + return { + id, + project_id: 'KIN', + title: `Task ${id}`, + status, + priority: 5, + assigned_role: null, + parent_task_id: null, + brief: null, + spec: null, + created_at: '2024-01-01T00:00:00', + updated_at: updatedAt, + completed_at: completedAt, + } +} + +const MOCK_PROJECT_DATE_FILTER = { + id: 'KIN', + name: 'Kin', + path: '/projects/kin', + status: 'active', + priority: 5, + tech_stack: ['python', 'vue'], + created_at: '2024-01-01', + total_tasks: 4, + done_tasks: 3, + active_tasks: 1, + blocked_tasks: 0, + review_tasks: 0, + tasks: [ + makeTask('KIN-001', 'done', '2024-01-05T12:00:00'), // before range + makeTask('KIN-002', 'done', '2024-01-15T12:00:00'), // in middle + makeTask('KIN-003', 'done', '2024-01-25T12:00:00'), // after range + makeTask('KIN-004', 'pending', null), + ], + decisions: [], + modules: [], +} + +function makeRouter() { + return createRouter({ + history: createMemoryHistory(), + routes: [ + { path: '/', component: Stub }, + { path: '/project/:id', component: ProjectView, props: true }, + ], + }) +} + +const localStorageMock = (() => { + let store: Record