From b58da600d4d0d8ebfbf3f2deeb3561b9be2195eb Mon Sep 17 00:00:00 2001 From: Gros Frumos Date: Tue, 17 Mar 2026 18:33:42 +0200 Subject: [PATCH] =?UTF-8?q?kin:=20KIN-OBS-027=20=D0=98=D1=81=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=B8=D1=82=D1=8C=20catch(e:=20any)=20=D0=B8=20set?= =?UTF-8?q?Timeout=20cleanup=20=D0=B2=20LiveConsole.vue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_deploy.py | 109 +++++++++++++++++++++++++++++++++++++++++++ tests/test_runner.py | 4 +- 2 files changed, 111 insertions(+), 2 deletions(-) diff --git a/tests/test_deploy.py b/tests/test_deploy.py index f4b19ee..e673edc 100644 --- a/tests/test_deploy.py +++ b/tests/test_deploy.py @@ -740,3 +740,112 @@ class TestProjectLinksIndexMigration: assert "idx_project_links_from" in after assert before == after conn.close() + + +# --------------------------------------------------------------------------- +# 12. Migration: UNIQUE(from_project, to_project, type) (KIN-INFRA-013) +# Convention #433: set-assert unique constraint after fresh init +# Convention #434: negative test — ALTER TABLE cannot add UNIQUE in SQLite +# --------------------------------------------------------------------------- + +class TestProjectLinksUniqueMigration: + """KIN-INFRA-013: UNIQUE(from_project, to_project, type) на project_links.""" + + # --- fresh schema --- + + def test_fresh_schema_project_links_has_unique_constraint(self): + """Свежая схема должна иметь UNIQUE-ограничение на (from_project, to_project, type).""" + conn = init_db(db_path=":memory:") + unique_indexes = [ + r for r in conn.execute("PRAGMA index_list(project_links)").fetchall() + if r[2] == 1 # unique == 1 + ] + assert len(unique_indexes) >= 1 + conn.close() + + # --- модельный уровень --- + + def test_create_duplicate_link_raises_integrity_error(self, conn): + """Дублирующая вставка должна вызывать IntegrityError.""" + import sqlite3 as _sqlite3 + models.create_project(conn, "dup_a", "A", "/a") + models.create_project(conn, "dup_b", "B", "/b") + models.create_project_link(conn, "dup_a", "dup_b", "depends_on") + with pytest.raises(_sqlite3.IntegrityError): + models.create_project_link(conn, "dup_a", "dup_b", "depends_on") + + # --- migration guard: 3 кейса (Convention #384) --- + + # Кейс 1: без таблицы — guard не падает + def test_migrate_without_project_links_table_no_error_unique(self): + conn = _old_schema_no_deploy() # project_links отсутствует + _migrate(conn) # не должно упасть + conn.close() + + # Кейс 2: таблица без UNIQUE → _migrate() добавляет ограничение + def test_migrate_adds_unique_constraint_to_old_schema(self): + conn = _schema_with_project_links_no_indexes() # без UNIQUE + _migrate(conn) + pl_sql = conn.execute( + "SELECT sql FROM sqlite_master WHERE type='table' AND name='project_links'" + ).fetchone() + assert "UNIQUE" in (pl_sql[0] or "").upper() + conn.close() + + # Кейс 3: таблица уже с UNIQUE → _migrate() идемпотентен + def test_migrate_unique_constraint_is_idempotent(self): + conn = init_db(db_path=":memory:") + before_sql = conn.execute( + "SELECT sql FROM sqlite_master WHERE type='table' AND name='project_links'" + ).fetchone()[0] + _migrate(conn) + after_sql = conn.execute( + "SELECT sql FROM sqlite_master WHERE type='table' AND name='project_links'" + ).fetchone()[0] + assert before_sql == after_sql + conn.close() + + # Convention #434: документируем, почему ALTER TABLE нельзя использовать + def test_alter_table_cannot_add_unique_constraint(self): + """SQLite не поддерживает ALTER TABLE ADD CONSTRAINT. + + Именно поэтому _migrate() пересоздаёт таблицу вместо ALTER TABLE. + """ + import sqlite3 as _sqlite3 + _conn = _sqlite3.connect(":memory:") + _conn.execute("CREATE TABLE t (a TEXT, b TEXT)") + with pytest.raises(_sqlite3.OperationalError): + _conn.execute("ALTER TABLE t ADD CONSTRAINT uq UNIQUE (a, b)") + _conn.close() + + +# --------------------------------------------------------------------------- +# 13. API: POST /api/project-links возвращает 409 при дублировании +# --------------------------------------------------------------------------- + +class TestProjectLinksDuplicateAPI: + def _create_projects(self, client): + client.post("/api/projects", json={"id": "dup_p2", "name": "P2", "path": "/p2"}) + + def test_create_duplicate_link_returns_409(self, client): + self._create_projects(client) + client.post("/api/project-links", json={ + "from_project": "p1", "to_project": "dup_p2", "type": "depends_on" + }) + r = client.post("/api/project-links", json={ + "from_project": "p1", "to_project": "dup_p2", "type": "depends_on" + }) + assert r.status_code == 409 + assert "already exists" in r.json()["detail"].lower() + + def test_same_projects_different_type_not_duplicate(self, client): + """Одна пара проектов с разными type — не дубликат.""" + self._create_projects(client) + r1 = client.post("/api/project-links", json={ + "from_project": "p1", "to_project": "dup_p2", "type": "depends_on" + }) + r2 = client.post("/api/project-links", json={ + "from_project": "p1", "to_project": "dup_p2", "type": "references" + }) + assert r1.status_code == 201 + assert r2.status_code == 201 diff --git a/tests/test_runner.py b/tests/test_runner.py index e382c80..acfbf7c 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -2713,7 +2713,7 @@ class TestPMStepPipelineLog: run_pipeline(conn, "VDOL-001", steps) # pm_result=None по умолчанию pm_logs = conn.execute( - "SELECT * FROM pipeline_log WHERE message='PM step: task decomposed'" + "SELECT * FROM pipeline_log WHERE message='PM start: task planning' OR message LIKE 'PM done:%'" ).fetchall() assert len(pm_logs) == 0 @@ -2741,7 +2741,7 @@ class TestPMStepPipelineLog: ) pm_logs = conn.execute( - "SELECT * FROM pipeline_log WHERE message='PM step: task decomposed'" + "SELECT * FROM pipeline_log WHERE message='PM start: task planning' OR message LIKE 'PM done:%'" ).fetchall() assert len(pm_logs) == 0