2026-08-25 18:40:35 +08:00
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
"""任务中心·扫码接单:TaskService 状态机 + Repository 访问(自包含内存库)。"""
|
|
|
|
|
|
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 # noqa: F401 (注册表到 Base)
|
|
|
|
|
|
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 _grab_published(**kw):
|
2026-08-25 19:30:16 +08:00
|
|
|
|
base = dict(
|
2026-08-25 18:40:35 +08:00
|
|
|
|
task_code="TK-004-GRAB", title="短视频剪辑", mode="grab",
|
|
|
|
|
|
status="published", category="技术开发", budget_min=1500, budget_max=3000,
|
2026-08-25 19:30:16 +08:00
|
|
|
|
display_priority=30,
|
2026-08-25 18:40:35 +08:00
|
|
|
|
)
|
2026-08-25 19:30:16 +08:00
|
|
|
|
base.update(kw)
|
|
|
|
|
|
return base
|
2026-08-25 18:40:35 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-08-25 19:30:16 +08:00
|
|
|
|
@pytest.mark.asyncio
|
2026-08-25 18:40:35 +08:00
|
|
|
|
async def test_claim_marks_claimed_and_records_claim():
|
|
|
|
|
|
"""published + grab → claimed(claimed_by/claimed_at),并写 task_claims 流水。"""
|
|
|
|
|
|
async with _db() as db:
|
|
|
|
|
|
t = await db.tasks.create(_grab_published())
|
|
|
|
|
|
actor = {"id": "u_opc_1", "username": "opc01", "nickname": "阿健"}
|
|
|
|
|
|
res = await TaskService(db).claim(t["task_code"], actor, source="scan")
|
|
|
|
|
|
assert res["status"] == "claimed"
|
|
|
|
|
|
assert res["claimed_by"] == "u_opc_1"
|
|
|
|
|
|
assert res["claimed_at"]
|
|
|
|
|
|
claims = await db.task_claims.list_by_task(t["id"])
|
|
|
|
|
|
assert claims and claims[0]["claim_source"] == "scan"
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-25 19:30:16 +08:00
|
|
|
|
@pytest.mark.asyncio
|
2026-08-25 18:40:35 +08:00
|
|
|
|
async def test_claim_disallowed_non_grab_mode():
|
|
|
|
|
|
"""bid 模式不可扫码接单。"""
|
|
|
|
|
|
async with _db() as db:
|
|
|
|
|
|
t = await db.tasks.create(_grab_published(title="UI", mode="bid"))
|
|
|
|
|
|
with pytest.raises(HTTPException):
|
|
|
|
|
|
await TaskService(db).claim(t["task_code"], {"id": "u1"}, "scan")
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-25 19:30:16 +08:00
|
|
|
|
@pytest.mark.asyncio
|
2026-08-25 18:40:35 +08:00
|
|
|
|
async def test_claim_unknown_code_404():
|
|
|
|
|
|
async with _db() as db:
|
|
|
|
|
|
with pytest.raises(HTTPException):
|
|
|
|
|
|
await TaskService(db).claim("TK-NOT-EXIST", {"id": "u1"}, "scan")
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-25 19:30:16 +08:00
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
|
async def test_exclusive_grab_second_claim_rejected():
|
|
|
|
|
|
"""独占 grab:再 claim 应报错(限额语义见 test_task_system)。"""
|
2026-08-25 18:40:35 +08:00
|
|
|
|
async with _db() as db:
|
2026-08-25 19:30:16 +08:00
|
|
|
|
t = await db.tasks.create(_grab_published(exclusive=True, headcount=1))
|
2026-08-25 18:40:35 +08:00
|
|
|
|
await TaskService(db).claim(t["task_code"], {"id": "u1"}, "scan")
|
|
|
|
|
|
with pytest.raises(HTTPException):
|
|
|
|
|
|
await TaskService(db).claim(t["task_code"], {"id": "u2"}, "scan")
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-25 19:30:16 +08:00
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
|
async def test_unlimited_grab_allows_multiple_takers():
|
|
|
|
|
|
"""非独占、不限人数的 grab 允许多人接单。"""
|
|
|
|
|
|
async with _db() as db:
|
|
|
|
|
|
t = await db.tasks.create(_grab_published())
|
|
|
|
|
|
r1 = await TaskService(db).claim(t["task_code"], {"id": "u1"}, "scan")
|
|
|
|
|
|
r2 = await TaskService(db).claim(t["task_code"], {"id": "u2"}, "scan")
|
|
|
|
|
|
assert r1["status"] == "claimed" and r2["status"] == "claimed"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
2026-08-25 18:40:35 +08:00
|
|
|
|
async def test_doing_then_complete():
|
|
|
|
|
|
"""claimed → doing → completed。"""
|
|
|
|
|
|
async with _db() as db:
|
|
|
|
|
|
t = await db.tasks.create(_grab_published())
|
|
|
|
|
|
actor = {"id": "u_opc_1", "username": "opc01", "nickname": "阿健"}
|
|
|
|
|
|
await TaskService(db).claim(t["task_code"], actor, source="scan")
|
|
|
|
|
|
doing = await TaskService(db).start_doing(t["id"], actor)
|
|
|
|
|
|
assert doing["status"] == "doing" and doing["doing_at"]
|
|
|
|
|
|
done = await TaskService(db).complete(t["id"], actor)
|
|
|
|
|
|
assert done["status"] == "completed"
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-25 19:30:16 +08:00
|
|
|
|
@pytest.mark.asyncio
|
2026-08-25 18:40:35 +08:00
|
|
|
|
async def test_list_published_returns_display_fields():
|
|
|
|
|
|
"""大屏取数:list_published 返回新字段且按 display_priority 降序。"""
|
|
|
|
|
|
async with _db() as db:
|
|
|
|
|
|
await db.tasks.create(_grab_published(task_code="TK-LOW", display_priority=1))
|
|
|
|
|
|
await db.tasks.create(_grab_published(task_code="TK-HIGH", display_priority=99))
|
|
|
|
|
|
items = await db.tasks.list_published()
|
|
|
|
|
|
codes = [t["task_code"] for t in items]
|
|
|
|
|
|
assert "TK-LOW" in codes and "TK-HIGH" in codes
|
|
|
|
|
|
assert codes.index("TK-HIGH") < codes.index("TK-LOW")
|
|
|
|
|
|
assert all("task_code" in t and "claimed_by" in t for t in items)
|