ea904d3a50
- Task 加 published_at/delivery_days/headcount/exclusive/publisher_id/category_id;新增 TaskCategory 分类字典表(35 类)。
- mode 归一 grab/bid/assign/recommend(旧 designated→assign、dispatch→recommend);TaskService 加 assign/recommend/select_recommend,claim 支持独占/限额、published+claimed 均可抢(多人)。
- TaskClaim 支持 source=assign/recommend、status=assigned/recommended/withdrawn + count_active_by_task;TaskCategoryRepository;Database 装配 task_categories。
- 端口:operator GET /admin/task-categories + POST tasks/{id}/assign|recommend|select;opc 同;park /api/tasks 返回 mode/deadline/delivery_days/headcount/exclusive/publisher_name。
- 迁移 0009(tasks 加列+task_categories)已应用+stamp;seed 35 分类字典 + task 新字段(含独占/推荐演示) + 补种已初始化库。
- 测试 test_task_system.py(独占/限额/指派/推荐/list_published/publish/category)+补齐 test_task_claim 语义, 15 passed。
Co-Authored-By: Claude <noreply@anthropic.com>
117 lines
4.6 KiB
Python
117 lines
4.6 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""任务系统完善:独占/限额/指派/推荐/字段(自包含内存库)。"""
|
|
from __future__ import annotations
|
|
|
|
from contextlib import asynccontextmanager
|
|
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
|
|
|
from app.infrastructure.models import Base, TaskCategory # noqa: F401
|
|
from app.infrastructure.repositories import Database
|
|
from app.services.task_service import TaskService
|
|
|
|
|
|
@asynccontextmanager
|
|
async def _db():
|
|
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
|
|
async with engine.begin() as conn:
|
|
await conn.run_sync(Base.metadata.create_all)
|
|
Session = async_sessionmaker(engine, expire_on_commit=False)
|
|
async with Session() as session:
|
|
yield Database(session=session)
|
|
await engine.dispose()
|
|
|
|
|
|
def _task(mode="grab", **kw):
|
|
base = dict(task_code="TK-X", title="任务", mode=mode, status="published",
|
|
category="技术开发", delivery_days=10, headcount=0, exclusive=False)
|
|
base.update(kw)
|
|
return base
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_exclusive_claim_rejects_second():
|
|
"""独占任务仅一人可接。"""
|
|
async with _db() as db:
|
|
t = await db.tasks.create(_task(exclusive=True, headcount=1))
|
|
a = {"id": "u1", "username": "opc01", "nickname": "阿健"}
|
|
await TaskService(db).claim(t["task_code"], a, source="scan")
|
|
with pytest.raises(HTTPException):
|
|
await TaskService(db).claim(t["task_code"], {"id": "u2"}, "scan")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_headcount_limit_rejects_overflow():
|
|
"""限额任务达上限后拒绝再接。"""
|
|
async with _db() as db:
|
|
t = await db.tasks.create(_task(headcount=2))
|
|
await TaskService(db).claim(t["task_code"], {"id": "u1"}, "scan")
|
|
await TaskService(db).claim(t["task_code"], {"id": "u2"}, "scan")
|
|
with pytest.raises(HTTPException):
|
|
await TaskService(db).claim(t["task_code"], {"id": "u3"}, "scan")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_assign_marks_claimed():
|
|
"""指派:assign 模式 → claimed(claimed_by=taker) + TaskClaim assigned。"""
|
|
async with _db() as db:
|
|
t = await db.tasks.create(_task(mode="assign"))
|
|
await TaskService(db).assign(t["id"], "u_taker", {"id": "u_pub"})
|
|
assert t is not None
|
|
after = await db.tasks.get(t["id"])
|
|
assert after["status"] == "claimed"
|
|
assert after["claimed_by"] == "u_taker"
|
|
claims = await db.task_claims.list_by_task(t["id"])
|
|
assert claims[0]["status"] == "assigned" and claims[0]["claim_source"] == "assign"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_assign_rejects_non_assign_mode():
|
|
async with _db() as db:
|
|
t = await db.tasks.create(_task(mode="grab"))
|
|
with pytest.raises(HTTPException):
|
|
await TaskService(db).assign(t["id"], "u_taker", {})
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_recommend_then_select():
|
|
"""推荐:→ 候选人 recommended → 选定 → claimed + assigned,其余 withdrawn。"""
|
|
async with _db() as db:
|
|
t = await db.tasks.create(_task(mode="recommend"))
|
|
actor = {"id": "u_pub", "username": "pub"}
|
|
recs = await TaskService(db).recommend(t["id"], ["u1", "u2"], actor)
|
|
assert len(recs) == 2
|
|
assert all(r["status"] == "recommended" for r in recs)
|
|
after = await TaskService(db).select_recommend(t["id"], "u1", actor)
|
|
assert after["status"] == "claimed" and after["claimed_by"] == "u1"
|
|
claims = await db.task_claims.list_by_task(t["id"])
|
|
by_user = {c["claimer_user_id"]: c["status"] for c in claims}
|
|
assert by_user == {"u1": "assigned", "u2": "withdrawn"}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_published_returns_new_fields():
|
|
"""大屏取数:list_published 返回新字段。"""
|
|
async with _db() as db:
|
|
t = await db.tasks.create(_task(delivery_days=21, headcount=5, exclusive=False, category_id="tc_08"))
|
|
items = await db.tasks.list_published()
|
|
found = next(i for i in items if i["id"] == t["id"])
|
|
for k in ("delivery_days", "headcount", "exclusive", "published_at", "publisher_id", "category_id"):
|
|
assert k in found
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_publish_sets_published_at():
|
|
async with _db() as db:
|
|
t = await db.tasks.create(_task(status="pending"))
|
|
after = await db.tasks.publish(t["id"])
|
|
assert after["status"] == "published" and after["published_at"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_category_list_empty_ok():
|
|
async with _db() as db:
|
|
assert await db.task_categories.list() == []
|