kin: auto-commit after pipeline

This commit is contained in:
Gros Frumos 2026-03-17 16:50:44 +02:00
parent 0731aad028
commit 79a0e524a7
3 changed files with 107 additions and 0 deletions

View file

@ -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
);

View file

@ -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()

View file

@ -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)
})
})
// ─────────────────────────────────────────────────────────────