- scripts/rebuild-frontend.sh: builds Vue 3 frontend and restarts uvicorn API - cli/main.py: hook group with add/list/remove/logs/setup commands; `hook setup` idempotently registers rebuild-frontend for a project - agents/runner.py: call run_hooks(event="pipeline_completed") after successful pipeline; wrap in try/except so hook errors never block results - tests: 3 tests for hook_setup CLI + 3 tests for pipeline→hooks integration Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
112 lines
5 KiB
Python
112 lines
5 KiB
Python
"""Regression test — KIN-009.
|
||
|
||
Проверяет, что в рабочей директории проекта НЕ создаются файлы с именами,
|
||
содержащими 'sqlite3.Connection'. Такие артефакты появляются, если путь к БД
|
||
формируется передачей объекта sqlite3.Connection вместо строки/Path в
|
||
sqlite3.connect().
|
||
"""
|
||
|
||
import os
|
||
import sqlite3
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
# Корень проекта — три уровня вверх от этого файла (tests/ → kin/)
|
||
PROJECT_ROOT = Path(__file__).parent.parent
|
||
|
||
|
||
def _find_connection_artifacts(search_dir: Path) -> list[Path]:
|
||
"""Рекурсивно ищет файлы, чьё имя содержит 'sqlite3.Connection'."""
|
||
found = []
|
||
try:
|
||
for entry in search_dir.rglob("*"):
|
||
if entry.is_file() and "sqlite3.Connection" in entry.name:
|
||
found.append(entry)
|
||
except PermissionError:
|
||
pass
|
||
return found
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Тест 1: статическая проверка — артефактов нет прямо сейчас
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def test_no_connection_artifact_files_in_project():
|
||
"""В рабочей директории проекта не должно быть файлов с 'sqlite3.Connection' в имени."""
|
||
artifacts = _find_connection_artifacts(PROJECT_ROOT)
|
||
assert artifacts == [], (
|
||
f"Найдены файлы-артефакты sqlite3.Connection:\n"
|
||
+ "\n".join(f" {p}" for p in artifacts)
|
||
)
|
||
|
||
|
||
def test_no_connection_artifact_files_in_kin_home():
|
||
"""В ~/.kin/ тоже не должно быть таких файлов."""
|
||
kin_home = Path.home() / ".kin"
|
||
if not kin_home.exists():
|
||
pytest.skip("~/.kin не существует")
|
||
artifacts = _find_connection_artifacts(kin_home)
|
||
assert artifacts == [], (
|
||
f"Найдены файлы-артефакты sqlite3.Connection в ~/.kin:\n"
|
||
+ "\n".join(f" {p}" for p in artifacts)
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Тест 2: динамическая проверка — init_db не создаёт артефактов в tmp_path
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def test_init_db_does_not_create_connection_artifact(tmp_path):
|
||
"""init_db() должен создавать файл с нормальным именем, а не 'sqlite3.Connection ...'."""
|
||
from core.db import init_db
|
||
|
||
db_file = tmp_path / "test.db"
|
||
conn = init_db(db_file)
|
||
conn.close()
|
||
|
||
artifacts = _find_connection_artifacts(tmp_path)
|
||
assert artifacts == [], (
|
||
"init_db() создал файл с именем, содержащим 'sqlite3.Connection':\n"
|
||
+ "\n".join(f" {p}" for p in artifacts)
|
||
)
|
||
# Убедимся, что файл БД реально создан с правильным именем
|
||
assert db_file.exists(), "Файл БД должен существовать"
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Тест 3: воспроизводит сценарий утечки — connect(conn) вместо connect(path)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def test_init_db_passes_string_to_sqlite_connect(tmp_path, monkeypatch):
|
||
"""core/db.init_db() должен вызывать sqlite3.connect() со строкой пути, а НЕ с объектом Connection.
|
||
|
||
Баг-сценарий: если где-то в коде путь к БД перепутан с объектом conn,
|
||
sqlite3.connect(str(conn)) создаст файл с именем '<sqlite3.Connection object at 0x...>'.
|
||
Этот тест перехватывает вызов и проверяет тип аргумента напрямую.
|
||
"""
|
||
import core.db as db_module
|
||
|
||
connect_calls: list = []
|
||
real_connect = sqlite3.connect
|
||
|
||
def mock_connect(database, *args, **kwargs):
|
||
connect_calls.append(database)
|
||
return real_connect(database, *args, **kwargs)
|
||
|
||
monkeypatch.setattr(db_module.sqlite3, "connect", mock_connect)
|
||
|
||
db_file = tmp_path / "test.db"
|
||
conn = db_module.init_db(db_file)
|
||
conn.close()
|
||
|
||
assert connect_calls, "sqlite3.connect() должен быть вызван хотя бы один раз"
|
||
|
||
for call_arg in connect_calls:
|
||
assert isinstance(call_arg, str), (
|
||
f"sqlite3.connect() получил не строку: {type(call_arg).__name__!r} = {call_arg!r}"
|
||
)
|
||
assert "sqlite3.Connection" not in call_arg, (
|
||
f"sqlite3.connect() получил строку объекта Connection: {call_arg!r}\n"
|
||
"Баг: str(conn) передаётся вместо пути к файлу — это создаёт файл-артефакт!"
|
||
)
|