Files
codex-app-server/cloud/tests/test_task_list_pagination.py
host e5b6c45c83 console: 任务列表服务端分页 + 控制台分页面签
任务列表分页(服务端 + 前端):
- GET /tasks 新增 limit(1..100,非法回 400)、offset、
  order=desc、status/executor/q 服务端过滤;回 total(过滤后
  计数)与 status_counts(全项目统计,不受过滤与分页影响,
  数据卡片不再把当前页当成全项目);无 limit 时旧行为不变
  (全量、正序,协调子任务与旧调用不受影响)。
- 前端每页 20 条、最新在前,上一页/下一页 + 页码;筛选与
  搜索改服务端过滤(搜索防抖 300ms),切筛选/切项目/派发
  后回第 1 页,10 秒刷新与刷新按钮停在当前页;新测试
  test_task_list_pagination.py 5 项。

分页面签:
- 顶栏锚点导航改成 5 个页签:工作台(派发 + 任务列表 +
  对话 + 详情)、项目(项目 + 成员)、环境与 Workers、
  技能与自动化(技能 + 自动化 + 分诊)、管理与 Key;
  数据卡片仍在页签上方常驻。选中任务存在 App 层 store
  里,切页不丢,回来仍是同一任务、10 秒刷新续同步;
  页签同步到 location.hash(#tab-workbench 等),刷新与
  分享链接落在同一页签;窄屏工作台仍保持派发在前的顺序。
- 工作台页签仍是左(任务)右(派发/对话/详情)两栏,
  沿用等高内部滚动规则;其它页签单列堆叠。
2026-10-30 02:25:48 +08:00

136 lines
5.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""任务列表服务端分页:limit/offset/总数/服务端过滤/状态统计。
用真实 HTTP 服务 + 临时 SQLite:建 5 个任务(2 completed、
1 failed、2 queued 交错 prompt),分别断言:
- 无 limit 时旧行为不变:全量、created_at 正序、total=5;
- limit=2&order=desc 时第 1 页是最新的 2 条,total=5;
- offset=2 时第 2 页是接下来 2 条,与第 1 页不重叠;
- status 过滤只回该状态,total 是过滤后计数;
- q 过滤按 id/prompt 子串(不区分大小写);
- limit=0 / limit=101 / offset=-1 回 400;
- status_counts 是全项目统计,不受当前过滤与分页影响。
"""
import http.client
import json
import os
import sys
import tempfile
import threading
import unittest
from pathlib import Path
try:
from cloud.auth import AuthStore
from cloud.server import create_server
except ImportError:
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cloud.auth import AuthStore
from cloud.server import create_server
class ListPaginationTestCase(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.db_path = os.path.join(self.tmp.name, "control.db")
self.auth = AuthStore(self.db_path)
self.owner_id, self.owner_key = self.auth.create_user("owner")
self.server = create_server(self.db_path, "127.0.0.1", 0)
self.port = self.server.server_address[1]
self.thread = threading.Thread(
target=self.server.serve_forever, daemon=True)
self.thread.start()
status, self.project = self._req(
"POST", "/projects", {"name": "demo"}, self.owner_key)
self.assertEqual(status, 201)
self.ids = []
for prompt in ["alpha one", "beta two", "alpha three", "gamma four", "alpha five"]:
status, t = self._req("POST", "/tasks",
{"project_id": self.project["id"], "prompt": prompt},
self.owner_key)
self.assertEqual(status, 201)
self.ids.append(t["id"])
# 直接改库状态:前 2 条 completed,第 3 条 failed,其余 queued。
import sqlite3
conn = sqlite3.connect(self.db_path)
conn.execute("UPDATE tasks SET status='completed' WHERE id IN (?, ?)",
(self.ids[0], self.ids[1]))
conn.execute("UPDATE tasks SET status='failed' WHERE id=?", (self.ids[2],))
conn.commit()
conn.close()
def tearDown(self):
self.server.shutdown()
self.server.server_close()
self.thread.join(timeout=5)
try:
self.auth.conn.close()
except Exception:
pass
self.tmp.cleanup()
def _req(self, method, path, body=None, key=None):
conn = http.client.HTTPConnection("127.0.0.1", self.port, timeout=10)
hdrs = {}
payload = None
if body is not None:
payload = json.dumps(body).encode("utf-8")
hdrs["Content-Type"] = "application/json"
if key is not None:
hdrs["X-API-Key"] = key
try:
conn.request(method, path, body=payload, headers=hdrs)
resp = conn.getresponse()
data = resp.read()
finally:
conn.close()
try:
parsed = json.loads(data.decode("utf-8")) if data else None
except ValueError:
parsed = None
return resp.status, parsed
def _list(self, qs=""):
return self._req("GET", "/tasks?project_id=" + self.project["id"] + qs,
None, self.owner_key)
def test_no_limit_is_old_behaviour(self):
status, d = self._list()
self.assertEqual(status, 200)
self.assertEqual([t["id"] for t in d["tasks"]], self.ids)
self.assertEqual(d["total"], 5)
def test_desc_pages_do_not_overlap(self):
status, p1 = self._list("&limit=2&order=desc")
self.assertEqual(status, 200)
self.assertEqual(p1["total"], 5)
self.assertEqual([t["id"] for t in p1["tasks"]],
[self.ids[4], self.ids[3]])
status, p2 = self._list("&limit=2&offset=2&order=desc")
self.assertEqual(status, 200)
self.assertEqual([t["id"] for t in p2["tasks"]],
[self.ids[2], self.ids[1]])
self.assertEqual(set(t["id"] for t in p1["tasks"]).isdisjoint(
set(t["id"] for t in p2["tasks"])), True)
def test_status_and_q_filters(self):
status, d = self._list("&status=completed")
self.assertEqual(status, 200)
self.assertEqual(d["total"], 2)
self.assertEqual(set(t["status"] for t in d["tasks"]), {"completed"})
status, d = self._list("&q=ALPHA")
self.assertEqual(status, 200)
self.assertEqual(d["total"], 3)
def test_status_counts_ignore_filter_and_page(self):
status, d = self._list("&status=completed&limit=1&order=desc")
self.assertEqual(status, 200)
self.assertEqual(len(d["tasks"]), 1)
self.assertEqual(d["status_counts"], {
"completed": 2, "failed": 1, "queued": 2})
def test_invalid_pagination_is_400(self):
for qs in ["&limit=0", "&limit=101", "&limit=2&offset=-1", "&limit=abc"]:
status, _ = self._list(qs)
self.assertEqual(status, 400, qs)