test(backend): 100% coverage for task_dependencies service
This commit is contained in:
committed by
OpenClaw Agent (Ishaan)
parent
4ac600d4c6
commit
4cc6b4300e
@@ -41,6 +41,7 @@ dev = [
|
|||||||
"pytest-cov==6.0.0",
|
"pytest-cov==6.0.0",
|
||||||
"coverage[toml]==7.6.10",
|
"coverage[toml]==7.6.10",
|
||||||
"ruff==0.6.9",
|
"ruff==0.6.9",
|
||||||
|
"aiosqlite==0.21.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
|
|||||||
163
backend/tests/test_task_dependencies_integration.py
Normal file
163
backend/tests/test_task_dependencies_integration.py
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
|
||||||
|
from sqlmodel import SQLModel
|
||||||
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
|
from app.models.boards import Board
|
||||||
|
from app.models.task_dependencies import TaskDependency
|
||||||
|
from app.models.tasks import Task
|
||||||
|
from app.services import task_dependencies as td
|
||||||
|
|
||||||
|
|
||||||
|
async def _make_engine() -> AsyncEngine:
|
||||||
|
# Single shared in-memory db per engine.
|
||||||
|
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
|
||||||
|
async with engine.begin() as conn:
|
||||||
|
await conn.run_sync(SQLModel.metadata.create_all)
|
||||||
|
return engine
|
||||||
|
|
||||||
|
|
||||||
|
async def _make_session(engine: AsyncEngine) -> AsyncSession:
|
||||||
|
return AsyncSession(engine)
|
||||||
|
|
||||||
|
|
||||||
|
async def _seed_board_and_tasks(
|
||||||
|
session: AsyncSession, *, board_id: UUID, task_ids: list[UUID]
|
||||||
|
) -> None:
|
||||||
|
session.add(Board(id=board_id, name="b", slug="b"))
|
||||||
|
for tid in task_ids:
|
||||||
|
session.add(Task(id=tid, board_id=board_id, title=f"t-{tid}", description=None))
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_validate_dependency_update_rejects_self_dependency() -> None:
|
||||||
|
engine = await _make_engine()
|
||||||
|
async with await _make_session(engine) as session:
|
||||||
|
board_id = uuid4()
|
||||||
|
task_id = uuid4()
|
||||||
|
await _seed_board_and_tasks(session, board_id=board_id, task_ids=[task_id])
|
||||||
|
|
||||||
|
with pytest.raises(HTTPException) as exc:
|
||||||
|
await td.validate_dependency_update(
|
||||||
|
session,
|
||||||
|
board_id=board_id,
|
||||||
|
task_id=task_id,
|
||||||
|
depends_on_task_ids=[task_id],
|
||||||
|
)
|
||||||
|
assert exc.value.status_code == 422
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_validate_dependency_update_404s_when_dependency_missing() -> None:
|
||||||
|
engine = await _make_engine()
|
||||||
|
async with await _make_session(engine) as session:
|
||||||
|
board_id = uuid4()
|
||||||
|
task_id = uuid4()
|
||||||
|
dep_id = uuid4()
|
||||||
|
await _seed_board_and_tasks(session, board_id=board_id, task_ids=[task_id])
|
||||||
|
|
||||||
|
with pytest.raises(HTTPException) as exc:
|
||||||
|
await td.validate_dependency_update(
|
||||||
|
session,
|
||||||
|
board_id=board_id,
|
||||||
|
task_id=task_id,
|
||||||
|
depends_on_task_ids=[dep_id],
|
||||||
|
)
|
||||||
|
assert exc.value.status_code == 404
|
||||||
|
detail = exc.value.detail
|
||||||
|
assert isinstance(detail, dict)
|
||||||
|
assert detail["missing_task_ids"] == [str(dep_id)]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_validate_dependency_update_detects_cycle() -> None:
|
||||||
|
engine = await _make_engine()
|
||||||
|
async with await _make_session(engine) as session:
|
||||||
|
board_id = uuid4()
|
||||||
|
a, b = uuid4(), uuid4()
|
||||||
|
await _seed_board_and_tasks(session, board_id=board_id, task_ids=[a, b])
|
||||||
|
|
||||||
|
# existing edge a -> b
|
||||||
|
session.add(TaskDependency(board_id=board_id, task_id=a, depends_on_task_id=b))
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
# update b -> a introduces cycle
|
||||||
|
with pytest.raises(HTTPException) as exc:
|
||||||
|
await td.validate_dependency_update(
|
||||||
|
session,
|
||||||
|
board_id=board_id,
|
||||||
|
task_id=b,
|
||||||
|
depends_on_task_ids=[a],
|
||||||
|
)
|
||||||
|
assert exc.value.status_code == 409
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_dependency_queries_and_replace_and_dependents() -> None:
|
||||||
|
engine = await _make_engine()
|
||||||
|
async with await _make_session(engine) as session:
|
||||||
|
board_id = uuid4()
|
||||||
|
t1, t2, t3 = uuid4(), uuid4(), uuid4()
|
||||||
|
await _seed_board_and_tasks(session, board_id=board_id, task_ids=[t1, t2, t3])
|
||||||
|
|
||||||
|
# seed deps: t1 depends on t2 then t3
|
||||||
|
session.add(TaskDependency(board_id=board_id, task_id=t1, depends_on_task_id=t2))
|
||||||
|
session.add(TaskDependency(board_id=board_id, task_id=t1, depends_on_task_id=t3))
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
# cover empty input short-circuit
|
||||||
|
assert await td.dependency_ids_by_task_id(session, board_id=board_id, task_ids=[]) == {}
|
||||||
|
|
||||||
|
deps_map = await td.dependency_ids_by_task_id(session, board_id=board_id, task_ids=[t1, t2])
|
||||||
|
assert deps_map[t1] == [t2, t3]
|
||||||
|
assert deps_map.get(t2, []) == []
|
||||||
|
|
||||||
|
# mark t2 done, t3 not
|
||||||
|
task2 = await session.get(Task, t2)
|
||||||
|
assert task2 is not None
|
||||||
|
task2.status = td.DONE_STATUS
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
# cover empty input short-circuit
|
||||||
|
assert await td.dependency_status_by_id(session, board_id=board_id, dependency_ids=[]) == {}
|
||||||
|
|
||||||
|
status_map = await td.dependency_status_by_id(session, board_id=board_id, dependency_ids=[t2, t3])
|
||||||
|
assert status_map[t2] == td.DONE_STATUS
|
||||||
|
assert status_map[t3] != td.DONE_STATUS
|
||||||
|
|
||||||
|
blocked = await td.blocked_by_for_task(session, board_id=board_id, task_id=t1)
|
||||||
|
assert blocked == [t3]
|
||||||
|
|
||||||
|
# cover early return when no deps provided
|
||||||
|
assert (
|
||||||
|
await td.blocked_by_for_task(session, board_id=board_id, task_id=t1, dependency_ids=[])
|
||||||
|
== []
|
||||||
|
)
|
||||||
|
|
||||||
|
# replace deps with duplicates (deduped) -> [t3]
|
||||||
|
out = await td.replace_task_dependencies(
|
||||||
|
session,
|
||||||
|
board_id=board_id,
|
||||||
|
task_id=t1,
|
||||||
|
depends_on_task_ids=[t3, t3],
|
||||||
|
)
|
||||||
|
await session.commit()
|
||||||
|
assert out == [t3]
|
||||||
|
|
||||||
|
deps_map2 = await td.dependency_ids_by_task_id(session, board_id=board_id, task_ids=[t1])
|
||||||
|
assert deps_map2[t1] == [t3]
|
||||||
|
|
||||||
|
dependents = await td.dependent_task_ids(session, board_id=board_id, dependency_task_id=t3)
|
||||||
|
assert dependents == [t1]
|
||||||
|
|
||||||
|
# also exercise explicit dependency_ids passed
|
||||||
|
blocked2 = await td.blocked_by_for_task(
|
||||||
|
session, board_id=board_id, task_id=t1, dependency_ids=[t3]
|
||||||
|
)
|
||||||
|
assert blocked2 == [t3]
|
||||||
14
backend/uv.lock
generated
14
backend/uv.lock
generated
@@ -2,6 +2,18 @@ version = 1
|
|||||||
revision = 3
|
revision = 3
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aiosqlite"
|
||||||
|
version = "0.21.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/13/7d/8bca2bf9a247c2c5dfeec1d7a5f40db6518f88d314b8bca9da29670d2671/aiosqlite-0.21.0.tar.gz", hash = "sha256:131bb8056daa3bc875608c631c678cda73922a2d4ba8aec373b19f18c17e7aa3", size = 13454, upload-time = "2025-02-03T07:30:16.235Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/10/6c25ed6de94c49f88a91fa5018cb4c0f3625f31d5be9f771ebe5cc7cd506/aiosqlite-0.21.0-py3-none-any.whl", hash = "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0", size = 15792, upload-time = "2025-02-03T07:30:13.6Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "alembic"
|
name = "alembic"
|
||||||
version = "1.13.2"
|
version = "1.13.2"
|
||||||
@@ -572,6 +584,7 @@ dependencies = [
|
|||||||
|
|
||||||
[package.optional-dependencies]
|
[package.optional-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
|
{ name = "aiosqlite" },
|
||||||
{ name = "black" },
|
{ name = "black" },
|
||||||
{ name = "coverage" },
|
{ name = "coverage" },
|
||||||
{ name = "flake8" },
|
{ name = "flake8" },
|
||||||
@@ -586,6 +599,7 @@ dev = [
|
|||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
|
{ name = "aiosqlite", marker = "extra == 'dev'", specifier = "==0.21.0" },
|
||||||
{ name = "alembic", specifier = "==1.13.2" },
|
{ name = "alembic", specifier = "==1.13.2" },
|
||||||
{ name = "black", marker = "extra == 'dev'", specifier = "==24.10.0" },
|
{ name = "black", marker = "extra == 'dev'", specifier = "==24.10.0" },
|
||||||
{ name = "coverage", extras = ["toml"], marker = "extra == 'dev'", specifier = "==7.6.10" },
|
{ name = "coverage", extras = ["toml"], marker = "extra == 'dev'", specifier = "==7.6.10" },
|
||||||
|
|||||||
Reference in New Issue
Block a user