feat(ai): 设置里加「AI 生成队列」,看得见后台在算什么
队列本来是完全不可见的:页面上一句「排队生成中」说不出自己是下一个、第二十 个,还是已经放弃了——网关挂掉的时候,「还在生成」和「永远不会好」长得一模 一样。今天排查就是这么排的。 - GET /analysis/insight/queue 返回队列(running 在前,其次按优先级和年龄, 和 worker 实际取任务的顺序一致)、已生成的解读、scope 名到中文标签的映射 (前端不必再抄一份),以及消费者的限流配置 - POST /analysis/insight/queue/retry:手动把「已放弃」的重新排队,不等冷却。 自动重试要等冷却是为了不去捶一个正在抽风的上游;人按下重试是他自己判断值得 再试一次 - 页面在 设置 → AI 生成队列。插队的任务标「插队」——这是整个界面最想让人看见 的一件事:为什么是它排在最前面 - 「已生成」单独列:队列空了意味着「没有待办」,不是「什么都没生成过」, 没有这一节这两件事在界面上没法区分 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1105,3 +1105,64 @@ class TestGatewayCourtesy:
|
||||
def test_there_is_a_gap_between_jobs(self):
|
||||
"""Back-to-back is what saturates a four-thread box."""
|
||||
assert jobs.GAP_SECONDS > 0
|
||||
|
||||
|
||||
class TestQueueScreen:
|
||||
def test_queue_endpoint_requires_auth(self, client):
|
||||
assert client.get("/api/analysis/insight/queue").status_code == 401
|
||||
assert client.post("/api/analysis/insight/queue/retry").status_code == 401
|
||||
|
||||
def test_lists_jobs_labels_and_limits(self, client, auth, month):
|
||||
client.get("/api/analysis/insight?scope=sleep", headers=auth)
|
||||
body = client.get("/api/analysis/insight/queue", headers=auth).get_json()
|
||||
assert body["pending"] >= 1
|
||||
assert any(j["kind"] == "sleep" for j in body["jobs"])
|
||||
assert body["scopes"]["sleep"] == "睡眠", "the UI must not restate the list"
|
||||
assert body["settings"]["concurrency"] == jobs.MAX_CONCURRENT
|
||||
|
||||
def test_an_open_screen_is_marked_as_having_jumped_the_queue(
|
||||
self, client, auth, month
|
||||
):
|
||||
jobs.enqueue(month["id"], "trends", "2026-08-30", priority=jobs.PRIORITY_PREFETCH)
|
||||
client.get("/api/analysis/insight?scope=sleep", headers=auth)
|
||||
by_kind = {j["kind"]: j for j in
|
||||
client.get("/api/analysis/insight/queue", headers=auth).get_json()["jobs"]}
|
||||
assert by_kind["sleep"]["interactive"] is True
|
||||
assert by_kind["trends"]["interactive"] is False
|
||||
|
||||
def test_running_jobs_are_listed_before_waiting_ones(self, client, auth, month):
|
||||
jobs.enqueue(month["id"], "health", "a")
|
||||
jobs.enqueue(month["id"], "sleep", "b")
|
||||
jobs._claim_next()
|
||||
listed = client.get("/api/analysis/insight/queue", headers=auth).get_json()["jobs"]
|
||||
assert listed[0]["status"] == "running"
|
||||
|
||||
def test_generated_answers_are_listed_separately(
|
||||
self, client, auth, month, gateway, monkeypatch
|
||||
):
|
||||
"""An empty queue means "nothing left to do", not "nothing was done" —
|
||||
the two look the same without this."""
|
||||
answer(monkeypatch, json.dumps({
|
||||
"headline": "h", "points": [], "actions": [], "confidence": "low",
|
||||
}, ensure_ascii=False))
|
||||
analysis_svc.generate_scope_insight(month["id"], "sleep")
|
||||
body = client.get("/api/analysis/insight/queue", headers=auth).get_json()
|
||||
assert any(i["kind"] == "sleep" for i in body["insights"])
|
||||
|
||||
def test_retry_puts_given_up_jobs_back(self, client, auth, month):
|
||||
jobs.enqueue(month["id"], "sleep", "s")
|
||||
row = jobs._claim_next()
|
||||
for _ in range(jobs.MAX_ATTEMPTS):
|
||||
jobs._finish(row["id"], "boom")
|
||||
assert jobs.status_of(month["id"], "sleep", "s")["status"] == "failed"
|
||||
|
||||
resp = client.post("/api/analysis/insight/queue/retry", headers=auth)
|
||||
assert resp.status_code == 200
|
||||
assert jobs.status_of(month["id"], "sleep", "s")["status"] == "pending"
|
||||
assert jobs.status_of(month["id"], "sleep", "s")["attempts"] == 0
|
||||
|
||||
def test_the_queue_is_per_account(self, client, auth, month, make_user):
|
||||
other = make_user("other@example.com")
|
||||
jobs.enqueue(other["id"], "sleep", "s")
|
||||
body = client.get("/api/analysis/insight/queue", headers=auth).get_json()
|
||||
assert body["jobs"] == []
|
||||
|
||||
Reference in New Issue
Block a user