# -*- 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() == []