kin: auto-commit after pipeline
This commit is contained in:
parent
a7e6e8ad35
commit
d552b8bd45
3 changed files with 473 additions and 1 deletions
329
tests/test_revising_status.py
Normal file
329
tests/test_revising_status.py
Normal file
|
|
@ -0,0 +1,329 @@
|
||||||
|
"""Tests for KIN-127: 'revising' status and recursive child completion blocking.
|
||||||
|
|
||||||
|
Covers:
|
||||||
|
- VALID_TASK_STATUSES contains 'revising'
|
||||||
|
- update_task → done redirects to 'revising' when open children exist
|
||||||
|
- Cascade: child→done triggers parent revising→done
|
||||||
|
- completed_at semantics for revising vs real done
|
||||||
|
- get_children() and has_open_children()
|
||||||
|
- API: PATCH status=done, GET /children, GET ?parent_task_id
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from core.db import init_db
|
||||||
|
from core import models
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Fixtures
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def conn():
|
||||||
|
"""Fresh in-memory DB for each test."""
|
||||||
|
c = init_db(db_path=":memory:")
|
||||||
|
yield c
|
||||||
|
c.close()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def proj(conn):
|
||||||
|
"""DB connection with a seeded project."""
|
||||||
|
models.create_project(conn, "p1", "P1", "/p1")
|
||||||
|
return conn
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# API client fixture (mirrors test_api.py pattern)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client(tmp_path):
|
||||||
|
import web.api as api_module
|
||||||
|
api_module.DB_PATH = tmp_path / "test.db"
|
||||||
|
from web.api import app
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
return TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# 1. VALID_TASK_STATUSES
|
||||||
|
# ===========================================================================
|
||||||
|
|
||||||
|
def test_revising_in_valid_statuses():
|
||||||
|
"""'revising' должен присутствовать в VALID_TASK_STATUSES."""
|
||||||
|
assert "revising" in models.VALID_TASK_STATUSES
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# 2. update_task: done → revising when open children exist
|
||||||
|
# ===========================================================================
|
||||||
|
|
||||||
|
def test_done_with_open_children_becomes_revising(proj):
|
||||||
|
"""Попытка parent→done при открытом child возвращает status='revising'."""
|
||||||
|
models.create_task(proj, "P1-001", "p1", "Parent")
|
||||||
|
models.create_task(proj, "P1-001a", "p1", "Child", parent_task_id="P1-001", status="pending")
|
||||||
|
|
||||||
|
result = models.update_task(proj, "P1-001", status="done")
|
||||||
|
|
||||||
|
assert result["status"] == "revising"
|
||||||
|
|
||||||
|
|
||||||
|
def test_done_without_children_stays_done(proj):
|
||||||
|
"""Задача без детей переходит в 'done' без изменений."""
|
||||||
|
models.create_task(proj, "P1-001", "p1", "Task")
|
||||||
|
|
||||||
|
result = models.update_task(proj, "P1-001", status="done")
|
||||||
|
|
||||||
|
assert result["status"] == "done"
|
||||||
|
|
||||||
|
|
||||||
|
def test_done_with_all_children_done(proj):
|
||||||
|
"""Все дети завершены → parent→done проходит как обычно."""
|
||||||
|
models.create_task(proj, "P1-001", "p1", "Parent")
|
||||||
|
models.create_task(proj, "P1-001a", "p1", "Child A", parent_task_id="P1-001", status="done")
|
||||||
|
models.create_task(proj, "P1-001b", "p1", "Child B", parent_task_id="P1-001", status="done")
|
||||||
|
|
||||||
|
result = models.update_task(proj, "P1-001", status="done")
|
||||||
|
|
||||||
|
assert result["status"] == "done"
|
||||||
|
|
||||||
|
|
||||||
|
def test_done_with_cancelled_children(proj):
|
||||||
|
"""Дети со статусом 'cancelled' считаются закрытыми → parent→done разрешён."""
|
||||||
|
models.create_task(proj, "P1-001", "p1", "Parent")
|
||||||
|
models.create_task(proj, "P1-001a", "p1", "Child A", parent_task_id="P1-001", status="cancelled")
|
||||||
|
models.create_task(proj, "P1-001b", "p1", "Child B", parent_task_id="P1-001", status="done")
|
||||||
|
|
||||||
|
result = models.update_task(proj, "P1-001", status="done")
|
||||||
|
|
||||||
|
assert result["status"] == "done"
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# 3. Cascade: child→done → parent revising→done
|
||||||
|
# ===========================================================================
|
||||||
|
|
||||||
|
def test_cascade_parent_completion(proj):
|
||||||
|
"""child→done автоматически переводит parent из 'revising' в 'done'."""
|
||||||
|
models.create_task(proj, "P1-001", "p1", "Parent")
|
||||||
|
models.create_task(proj, "P1-001a", "p1", "Child", parent_task_id="P1-001", status="pending")
|
||||||
|
|
||||||
|
# parent→done с открытым ребёнком → revising
|
||||||
|
models.update_task(proj, "P1-001", status="done")
|
||||||
|
assert models.get_task(proj, "P1-001")["status"] == "revising"
|
||||||
|
|
||||||
|
# child→done → каскад поднимает parent в done
|
||||||
|
models.update_task(proj, "P1-001a", status="done")
|
||||||
|
assert models.get_task(proj, "P1-001")["status"] == "done"
|
||||||
|
|
||||||
|
|
||||||
|
def test_deep_cascade(proj):
|
||||||
|
"""3 уровня: grandchild→done → child (revising)→done → parent (revising)→done."""
|
||||||
|
models.create_task(proj, "P1-001", "p1", "Parent")
|
||||||
|
models.create_task(proj, "P1-001a", "p1", "Child", parent_task_id="P1-001", status="pending")
|
||||||
|
models.create_task(proj, "P1-001a1", "p1", "Grandchild", parent_task_id="P1-001a", status="pending")
|
||||||
|
|
||||||
|
# parent→done → revising (есть открытый child, у child открытый grandchild)
|
||||||
|
models.update_task(proj, "P1-001", status="done")
|
||||||
|
assert models.get_task(proj, "P1-001")["status"] == "revising"
|
||||||
|
|
||||||
|
# child→done → revising (есть открытый grandchild)
|
||||||
|
models.update_task(proj, "P1-001a", status="done")
|
||||||
|
assert models.get_task(proj, "P1-001a")["status"] == "revising"
|
||||||
|
# parent остаётся revising
|
||||||
|
assert models.get_task(proj, "P1-001")["status"] == "revising"
|
||||||
|
|
||||||
|
# grandchild→done → cascades: child→done, parent→done
|
||||||
|
models.update_task(proj, "P1-001a1", status="done")
|
||||||
|
assert models.get_task(proj, "P1-001a1")["status"] == "done"
|
||||||
|
assert models.get_task(proj, "P1-001a")["status"] == "done"
|
||||||
|
assert models.get_task(proj, "P1-001")["status"] == "done"
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# 4. completed_at semantics
|
||||||
|
# ===========================================================================
|
||||||
|
|
||||||
|
def test_completed_at_not_set_on_revising(proj):
|
||||||
|
"""При переходе в 'revising' completed_at должен оставаться NULL."""
|
||||||
|
models.create_task(proj, "P1-001", "p1", "Parent")
|
||||||
|
models.create_task(proj, "P1-001a", "p1", "Child", parent_task_id="P1-001", status="pending")
|
||||||
|
|
||||||
|
result = models.update_task(proj, "P1-001", status="done")
|
||||||
|
|
||||||
|
assert result["status"] == "revising"
|
||||||
|
assert result["completed_at"] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_completed_at_set_on_real_done(proj):
|
||||||
|
"""При каскадном переходе parent из revising → done ставится completed_at."""
|
||||||
|
models.create_task(proj, "P1-001", "p1", "Parent")
|
||||||
|
models.create_task(proj, "P1-001a", "p1", "Child", parent_task_id="P1-001", status="pending")
|
||||||
|
|
||||||
|
models.update_task(proj, "P1-001", status="done") # → revising
|
||||||
|
models.update_task(proj, "P1-001a", status="done") # cascade → parent done
|
||||||
|
|
||||||
|
parent = models.get_task(proj, "P1-001")
|
||||||
|
assert parent["status"] == "done"
|
||||||
|
assert parent["completed_at"] is not None
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# 5. get_children()
|
||||||
|
# ===========================================================================
|
||||||
|
|
||||||
|
def test_get_children_returns_direct_children(proj):
|
||||||
|
"""get_children возвращает только прямых потомков."""
|
||||||
|
models.create_task(proj, "P1-001", "p1", "Parent")
|
||||||
|
models.create_task(proj, "P1-001a", "p1", "Child A", parent_task_id="P1-001")
|
||||||
|
models.create_task(proj, "P1-001b", "p1", "Child B", parent_task_id="P1-001")
|
||||||
|
models.create_task(proj, "P1-001a1", "p1", "Grandchild", parent_task_id="P1-001a")
|
||||||
|
|
||||||
|
children = models.get_children(proj, "P1-001")
|
||||||
|
|
||||||
|
assert len(children) == 2
|
||||||
|
ids = {c["id"] for c in children}
|
||||||
|
assert ids == {"P1-001a", "P1-001b"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_children_empty_for_leaf_task(proj):
|
||||||
|
"""get_children возвращает [] для задачи без детей."""
|
||||||
|
models.create_task(proj, "P1-001", "p1", "Task")
|
||||||
|
|
||||||
|
assert models.get_children(proj, "P1-001") == []
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# 6. has_open_children() — recursive
|
||||||
|
# ===========================================================================
|
||||||
|
|
||||||
|
def test_has_open_children_recursive(proj):
|
||||||
|
"""has_open_children рекурсивно проверяет через 3 уровня вложенности."""
|
||||||
|
models.create_task(proj, "P1-001", "p1", "Parent")
|
||||||
|
models.create_task(proj, "P1-001a", "p1", "Child", parent_task_id="P1-001", status="done")
|
||||||
|
models.create_task(proj, "P1-001a1", "p1", "Grandchild", parent_task_id="P1-001a", status="pending")
|
||||||
|
|
||||||
|
# Child сам done, но у него открытый grandchild — должен вернуть True
|
||||||
|
assert models.has_open_children(proj, "P1-001") is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_has_open_children_false_when_all_closed(proj):
|
||||||
|
"""has_open_children возвращает False когда все потомки closed."""
|
||||||
|
models.create_task(proj, "P1-001", "p1", "Parent")
|
||||||
|
models.create_task(proj, "P1-001a", "p1", "Child", parent_task_id="P1-001", status="done")
|
||||||
|
models.create_task(proj, "P1-001a1", "p1", "Grandchild", parent_task_id="P1-001a", status="cancelled")
|
||||||
|
|
||||||
|
assert models.has_open_children(proj, "P1-001") is False
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# 7. list_tasks — parent_task_id filter
|
||||||
|
# ===========================================================================
|
||||||
|
|
||||||
|
def test_list_tasks_parent_filter(proj):
|
||||||
|
"""list_tasks(parent_task_id=...) возвращает только дочерние задачи."""
|
||||||
|
models.create_task(proj, "P1-001", "p1", "Parent")
|
||||||
|
models.create_task(proj, "P1-001a", "p1", "Child A", parent_task_id="P1-001")
|
||||||
|
models.create_task(proj, "P1-001b", "p1", "Child B", parent_task_id="P1-001")
|
||||||
|
models.create_task(proj, "P1-002", "p1", "Other root task")
|
||||||
|
|
||||||
|
result = models.list_tasks(proj, parent_task_id="P1-001")
|
||||||
|
|
||||||
|
assert len(result) == 2
|
||||||
|
ids = {t["id"] for t in result}
|
||||||
|
assert ids == {"P1-001a", "P1-001b"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_tasks_parent_none_filter(proj):
|
||||||
|
"""list_tasks(parent_task_id='__none__') возвращает только корневые задачи."""
|
||||||
|
models.create_task(proj, "P1-001", "p1", "Root Task")
|
||||||
|
models.create_task(proj, "P1-001a", "p1", "Child", parent_task_id="P1-001")
|
||||||
|
|
||||||
|
result = models.list_tasks(proj, parent_task_id="__none__")
|
||||||
|
|
||||||
|
ids = {t["id"] for t in result}
|
||||||
|
assert "P1-001" in ids
|
||||||
|
assert "P1-001a" not in ids
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# 8. API tests
|
||||||
|
# ===========================================================================
|
||||||
|
|
||||||
|
def test_patch_task_revising_status(client, tmp_path):
|
||||||
|
"""PATCH /api/tasks/{id} status=done с открытым child → ответ содержит status='revising'."""
|
||||||
|
import web.api as api_module
|
||||||
|
from core.db import init_db
|
||||||
|
from core import models as m
|
||||||
|
|
||||||
|
client.post("/api/projects", json={"id": "p1", "name": "P1", "path": "/p1"})
|
||||||
|
r = client.post("/api/tasks", json={"project_id": "p1", "title": "Parent Task"})
|
||||||
|
parent_id = r.json()["id"]
|
||||||
|
|
||||||
|
# Seed child directly via models (TaskCreate API has no parent_task_id field)
|
||||||
|
conn = init_db(api_module.DB_PATH)
|
||||||
|
m.create_task(conn, parent_id + "a", "p1", "Child Task",
|
||||||
|
parent_task_id=parent_id, status="pending")
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
r = client.patch(f"/api/tasks/{parent_id}", json={"status": "done"})
|
||||||
|
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json()["status"] == "revising"
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_children_endpoint(client):
|
||||||
|
"""GET /api/tasks/{id}/children возвращает список прямых дочерних задач."""
|
||||||
|
import web.api as api_module
|
||||||
|
from core.db import init_db
|
||||||
|
from core import models as m
|
||||||
|
|
||||||
|
client.post("/api/projects", json={"id": "p1", "name": "P1", "path": "/p1"})
|
||||||
|
r = client.post("/api/tasks", json={"project_id": "p1", "title": "Parent Task"})
|
||||||
|
parent_id = r.json()["id"]
|
||||||
|
|
||||||
|
# Seed children via models
|
||||||
|
conn = init_db(api_module.DB_PATH)
|
||||||
|
m.create_task(conn, parent_id + "a", "p1", "Child A", parent_task_id=parent_id)
|
||||||
|
m.create_task(conn, parent_id + "b", "p1", "Child B", parent_task_id=parent_id)
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
r = client.get(f"/api/tasks/{parent_id}/children")
|
||||||
|
|
||||||
|
assert r.status_code == 200
|
||||||
|
children = r.json()
|
||||||
|
assert len(children) == 2
|
||||||
|
titles = {c["title"] for c in children}
|
||||||
|
assert titles == {"Child A", "Child B"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_children_endpoint_not_found(client):
|
||||||
|
"""GET /api/tasks/{id}/children для несуществующей задачи → 404."""
|
||||||
|
r = client.get("/api/tasks/NONEXISTENT/children")
|
||||||
|
assert r.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_tasks_parent_filter_api(client):
|
||||||
|
"""GET /api/tasks?parent_task_id={id} фильтрует задачи по родителю."""
|
||||||
|
import web.api as api_module
|
||||||
|
from core.db import init_db
|
||||||
|
from core import models as m
|
||||||
|
|
||||||
|
client.post("/api/projects", json={"id": "p1", "name": "P1", "path": "/p1"})
|
||||||
|
r = client.post("/api/tasks", json={"project_id": "p1", "title": "Parent Task"})
|
||||||
|
parent_id = r.json()["id"]
|
||||||
|
client.post("/api/tasks", json={"project_id": "p1", "title": "Other Root Task"})
|
||||||
|
|
||||||
|
# Seed child via models
|
||||||
|
conn = init_db(api_module.DB_PATH)
|
||||||
|
m.create_task(conn, parent_id + "a", "p1", "Child Task", parent_task_id=parent_id)
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
r = client.get(f"/api/tasks?parent_task_id={parent_id}")
|
||||||
|
|
||||||
|
assert r.status_code == 200
|
||||||
|
tasks = r.json()
|
||||||
|
assert len(tasks) == 1
|
||||||
|
assert tasks[0]["title"] == "Child Task"
|
||||||
141
tests/test_revising_status_regression.py
Normal file
141
tests/test_revising_status_regression.py
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
"""Regression tests for KIN-127 bug: parent_task_id silently ignored in POST /api/tasks.
|
||||||
|
|
||||||
|
Root cause: TaskCreate schema was missing parent_task_id field, so child tasks
|
||||||
|
created via API had no parent link in DB. Fixed by adding parent_task_id to TaskCreate
|
||||||
|
and passing it to models.create_task.
|
||||||
|
|
||||||
|
These tests use the API end-to-end (no models bypass) to prevent regression.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client(tmp_path):
|
||||||
|
import web.api as api_module
|
||||||
|
api_module.DB_PATH = tmp_path / "test.db"
|
||||||
|
from web.api import app
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
c = TestClient(app)
|
||||||
|
c.post("/api/projects", json={"id": "p1", "name": "P1", "path": "/p1"})
|
||||||
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 1. parent_task_id сохраняется при создании задачи через API
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def test_create_task_with_parent_task_id_saves_link(client):
|
||||||
|
"""POST /api/tasks с parent_task_id — ссылка на родителя сохраняется в БД."""
|
||||||
|
r = client.post("/api/tasks", json={"project_id": "p1", "title": "Parent"})
|
||||||
|
assert r.status_code == 200
|
||||||
|
parent_id = r.json()["id"]
|
||||||
|
|
||||||
|
r = client.post("/api/tasks", json={
|
||||||
|
"project_id": "p1",
|
||||||
|
"title": "Child",
|
||||||
|
"parent_task_id": parent_id,
|
||||||
|
})
|
||||||
|
assert r.status_code == 200
|
||||||
|
child = r.json()
|
||||||
|
assert child["parent_task_id"] == parent_id
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_task_without_parent_task_id_has_no_parent(client):
|
||||||
|
"""POST /api/tasks без parent_task_id — задача создаётся как корневая."""
|
||||||
|
r = client.post("/api/tasks", json={"project_id": "p1", "title": "Root task"})
|
||||||
|
assert r.status_code == 200
|
||||||
|
task = r.json()
|
||||||
|
assert task.get("parent_task_id") is None
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 2. /children возвращает задачи, созданные через API с parent_task_id
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def test_children_endpoint_returns_api_created_children(client):
|
||||||
|
"""GET /children видит дочерние задачи, созданные через POST /api/tasks."""
|
||||||
|
r = client.post("/api/tasks", json={"project_id": "p1", "title": "Parent"})
|
||||||
|
parent_id = r.json()["id"]
|
||||||
|
|
||||||
|
client.post("/api/tasks", json={
|
||||||
|
"project_id": "p1", "title": "Child A", "parent_task_id": parent_id,
|
||||||
|
})
|
||||||
|
client.post("/api/tasks", json={
|
||||||
|
"project_id": "p1", "title": "Child B", "parent_task_id": parent_id,
|
||||||
|
})
|
||||||
|
|
||||||
|
r = client.get(f"/api/tasks/{parent_id}/children")
|
||||||
|
assert r.status_code == 200
|
||||||
|
titles = {c["title"] for c in r.json()}
|
||||||
|
assert titles == {"Child A", "Child B"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_children_endpoint_empty_for_task_created_without_parent(client):
|
||||||
|
"""GET /children возвращает [] для задачи без дочерних задач."""
|
||||||
|
r = client.post("/api/tasks", json={"project_id": "p1", "title": "Leaf"})
|
||||||
|
task_id = r.json()["id"]
|
||||||
|
|
||||||
|
r = client.get(f"/api/tasks/{task_id}/children")
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json() == []
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 3. Фильтр ?parent_task_id работает с API-созданными дочерними задачами
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def test_list_tasks_parent_filter_with_api_created_children(client):
|
||||||
|
"""GET /api/tasks?parent_task_id={id} находит задачи, созданные через API."""
|
||||||
|
r = client.post("/api/tasks", json={"project_id": "p1", "title": "Parent"})
|
||||||
|
parent_id = r.json()["id"]
|
||||||
|
|
||||||
|
client.post("/api/tasks", json={
|
||||||
|
"project_id": "p1", "title": "Child", "parent_task_id": parent_id,
|
||||||
|
})
|
||||||
|
client.post("/api/tasks", json={"project_id": "p1", "title": "Other root"})
|
||||||
|
|
||||||
|
r = client.get(f"/api/tasks?parent_task_id={parent_id}")
|
||||||
|
assert r.status_code == 200
|
||||||
|
tasks = r.json()
|
||||||
|
assert len(tasks) == 1
|
||||||
|
assert tasks[0]["title"] == "Child"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 4. End-to-end: PATCH status=done → revising, дочерняя задача создана через API
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def test_patch_done_becomes_revising_when_child_created_via_api(client):
|
||||||
|
"""E2E: дочерняя задача создана через POST /api/tasks → parent PATCH done → revising."""
|
||||||
|
r = client.post("/api/tasks", json={"project_id": "p1", "title": "Parent"})
|
||||||
|
parent_id = r.json()["id"]
|
||||||
|
|
||||||
|
client.post("/api/tasks", json={
|
||||||
|
"project_id": "p1",
|
||||||
|
"title": "Child (open)",
|
||||||
|
"parent_task_id": parent_id,
|
||||||
|
})
|
||||||
|
|
||||||
|
r = client.patch(f"/api/tasks/{parent_id}", json={"status": "done"})
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json()["status"] == "revising"
|
||||||
|
|
||||||
|
|
||||||
|
def test_patch_done_when_api_child_is_done(client):
|
||||||
|
"""E2E: все дочерние задачи (созданные через API) done → parent PATCH done → done."""
|
||||||
|
r = client.post("/api/tasks", json={"project_id": "p1", "title": "Parent"})
|
||||||
|
parent_id = r.json()["id"]
|
||||||
|
|
||||||
|
r = client.post("/api/tasks", json={
|
||||||
|
"project_id": "p1",
|
||||||
|
"title": "Child",
|
||||||
|
"parent_task_id": parent_id,
|
||||||
|
})
|
||||||
|
child_id = r.json()["id"]
|
||||||
|
|
||||||
|
client.patch(f"/api/tasks/{child_id}", json={"status": "done"})
|
||||||
|
|
||||||
|
r = client.patch(f"/api/tasks/{parent_id}", json={"status": "done"})
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json()["status"] == "done"
|
||||||
|
|
@ -712,6 +712,7 @@ class TaskCreate(BaseModel):
|
||||||
category: str | None = None
|
category: str | None = None
|
||||||
acceptance_criteria: str | None = None
|
acceptance_criteria: str | None = None
|
||||||
labels: list[str] | None = None
|
labels: list[str] | None = None
|
||||||
|
parent_task_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/tasks")
|
@app.post("/api/tasks")
|
||||||
|
|
@ -732,7 +733,8 @@ def create_task(body: TaskCreate):
|
||||||
t = models.create_task(conn, task_id, body.project_id, body.title,
|
t = models.create_task(conn, task_id, body.project_id, body.title,
|
||||||
priority=body.priority, brief=brief, category=category,
|
priority=body.priority, brief=brief, category=category,
|
||||||
acceptance_criteria=body.acceptance_criteria,
|
acceptance_criteria=body.acceptance_criteria,
|
||||||
labels=body.labels)
|
labels=body.labels,
|
||||||
|
parent_task_id=body.parent_task_id)
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
# Auto-trigger: if task has 'auto' label, launch pipeline in background
|
# Auto-trigger: if task has 'auto' label, launch pipeline in background
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue