From 79a0e524a7baaade6f4f6bf50a6320b7641ae992 Mon Sep 17 00:00:00 2001 From: Gros Frumos Date: Tue, 17 Mar 2026 16:50:44 +0200 Subject: [PATCH] kin: auto-commit after pipeline --- core/db.py | 1 + tests/test_db.py | 89 +++++++++++++++++++ .../src/__tests__/watchdog-toast.test.ts | 17 ++++ 3 files changed, 107 insertions(+) diff --git a/core/db.py b/core/db.py index a54790d..99ebfd9 100644 --- a/core/db.py +++ b/core/db.py @@ -143,6 +143,7 @@ CREATE TABLE IF NOT EXISTS pipelines ( total_duration_seconds INTEGER, parent_pipeline_id INTEGER REFERENCES pipelines(id), department TEXT, + pid INTEGER, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, completed_at DATETIME ); diff --git a/tests/test_db.py b/tests/test_db.py index 07ff8bd..0cbb472 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -392,3 +392,92 @@ def test_migrate_preserves_non_dept_sub_and_non_running_pipelines(): assert rows["dept_sub"] == "completed", "completed pipeline не должен меняться" assert rows["direct"] == "running", "non-dept_sub running pipeline не должен меняться" conn.close() + + +# --------------------------------------------------------------------------- +# Schema KIN-OBS-018: pid INTEGER присутствует в таблице pipelines +# --------------------------------------------------------------------------- + +class TestPipelinesSchemaKinObs018: + """PRAGMA table_info(pipelines) должен содержать kolонку pid INTEGER.""" + + def test_schema_has_pid_column(self, conn): + """Свежая инициализация: pipelines содержит колонку pid.""" + assert "pid" in _cols(conn, "pipelines") + + def test_pid_defaults_to_null(self, conn): + """Вставка pipeline без pid — значение NULL.""" + conn.execute( + "INSERT INTO projects (id, name) VALUES ('p_pid', 'P')" + ) + conn.execute( + "INSERT INTO tasks (id, project_id, title) VALUES ('t_pid', 'p_pid', 'T')" + ) + conn.execute( + "INSERT INTO pipelines (task_id, project_id, route_type, steps)" + " VALUES ('t_pid', 'p_pid', 'direct', '[]')" + ) + conn.commit() + row = conn.execute( + "SELECT pid FROM pipelines WHERE task_id='t_pid'" + ).fetchone() + assert row["pid"] is None + + def test_pid_can_store_integer_value(self, conn): + """Вставка pipeline с pid — значение сохраняется в БД.""" + conn.execute( + "INSERT INTO projects (id, name) VALUES ('p_pid2', 'P')" + ) + conn.execute( + "INSERT INTO tasks (id, project_id, title) VALUES ('t_pid2', 'p_pid2', 'T')" + ) + conn.execute( + "INSERT INTO pipelines (task_id, project_id, route_type, steps, pid)" + " VALUES ('t_pid2', 'p_pid2', 'direct', '[]', 12345)" + ) + conn.commit() + row = conn.execute( + "SELECT pid FROM pipelines WHERE task_id='t_pid2'" + ).fetchone() + assert row["pid"] == 12345 + + +# --------------------------------------------------------------------------- +# Migration KIN-OBS-018: _migrate добавляет pid в существующие БД без этой колонки +# Конвенция #384: три теста для _migrate guard +# --------------------------------------------------------------------------- + +def test_migrate_adds_pid_to_pipelines_without_column(): + """_migrate добавляет pid в таблицу pipelines если колонки нет.""" + conn = _old_schema_with_pipelines_conn() # схема без pid + _migrate(conn) + assert "pid" in _cols(conn, "pipelines") + conn.close() + + +def test_migrate_pid_is_nullable_after_migration(): + """После миграции pid nullable — существующие строки не ломаются (pid=NULL).""" + conn = _old_schema_with_pipelines_conn() + conn.execute("INSERT INTO projects (id, name) VALUES ('pm_pid', 'P')") + conn.execute("INSERT INTO tasks (id, project_id, title) VALUES ('tm_pid', 'pm_pid', 'T')") + conn.execute( + "INSERT INTO pipelines (task_id, project_id, route_type, status)" + " VALUES ('tm_pid', 'pm_pid', 'direct', 'completed')" + ) + conn.commit() + _migrate(conn) + row = conn.execute("SELECT pid FROM pipelines WHERE task_id='tm_pid'").fetchone() + assert row["pid"] is None + conn.close() + + +def test_migrate_pid_guard_is_idempotent(): + """Повторный вызов _migrate не ломает схему pipelines (guard idempotent).""" + conn = _old_schema_with_pipelines_conn() + _migrate(conn) + before = _cols(conn, "pipelines") + _migrate(conn) + after = _cols(conn, "pipelines") + assert before == after + assert "pid" in after + conn.close() diff --git a/web/frontend/src/__tests__/watchdog-toast.test.ts b/web/frontend/src/__tests__/watchdog-toast.test.ts index 8edd4d5..539be10 100644 --- a/web/frontend/src/__tests__/watchdog-toast.test.ts +++ b/web/frontend/src/__tests__/watchdog-toast.test.ts @@ -256,6 +256,23 @@ describe('KIN-099: onUnmounted очищает setTimeout таймеры watchdog await flushPromises() // Pass = no errors thrown after unmount }) + + it('KIN-OBS-016: vi.getTimerCount() === 0 после unmount — все таймеры очищены', async () => { + vi.mocked(api.notifications).mockResolvedValue([ + makeNotification('KIN-016', 'Process died unexpectedly (PID 99999)'), + ]) + + const wrapper = mount(EscalationBanner) + await flushPromises() + + // Toast с активным timerId должен быть виден + expect(wrapper.find('.border-red-700').exists()).toBe(true) + + // После unmount — оба таймера (setInterval pollTimer + setTimeout toast) очищены + wrapper.unmount() + + expect(vi.getTimerCount()).toBe(0) + }) }) // ─────────────────────────────────────────────────────────────