fix(system): sort recent tasks by started_at desc

进行中任务可能尚无 finished_at;改为按 started_at 倒序,
缺开始时间的排末尾。测例用 finished 错开,避免误用完成时间。
This commit is contained in:
bot-ci committed 2026-11-02 19:38:34 +08:00
1 parent e7b48ba5d9
commit e6883f99a4
3 files changed
+23 -23

No files matched your search

+5 -5
View File
@@ -623,10 +623,10 @@ class DashboardQueries:
return None return None
@staticmethod @staticmethod
def _task_finished_sort_key(value: Any) -> tuple[int, int]: def _task_started_sort_key(value: Any) -> tuple[int, int]:
"""缺 finished_at 的排末尾;有值的按完成时间倒序。""" """缺 started_at 的排末尾;有值的按开始时间倒序(含进行中)。"""
if isinstance(value, Mapping): if isinstance(value, Mapping):
raw = value.get("finished_at") raw = value.get("started_at")
try: try:
return (0, -int(raw)) return (0, -int(raw))
except (TypeError, ValueError): except (TypeError, ValueError):
@@ -645,8 +645,8 @@ class DashboardQueries:
except (TypeError, json.JSONDecodeError): except (TypeError, json.JSONDecodeError):
value = {"raw": row["v"]} value = {"raw": row["v"]}
items.append((name, value)) items.append((name, value))
items.sort(key=lambda pair: self._task_finished_sort_key(pair[1])) items.sort(key=lambda pair: self._task_started_sort_key(pair[1]))
# 插入序即完成时间倒序;JSON/Object.entries 会保留。 # 插入序即开始时间倒序;JSON/Object.entries 会保留。
return dict(items) return dict(items)
def _task_running(self, conn: sqlite3.Connection) -> dict[str, Any]: def _task_running(self, conn: sqlite3.Connection) -> dict[str, Any]:
+5 -5
View File
@@ -7,11 +7,11 @@ import {
function taskRows(tasks) { function taskRows(tasks) {
const entries = Object.entries(tasks || {}).sort((a, b) => { const entries = Object.entries(tasks || {}).sort((a, b) => {
const fa = Number(a[1] && a[1].finished_at); const sa = Number(a[1] && a[1].started_at);
const fb = Number(b[1] && b[1].finished_at); const sb = Number(b[1] && b[1].started_at);
const aOk = Number.isFinite(fa); const aOk = Number.isFinite(sa);
const bOk = Number.isFinite(fb); const bOk = Number.isFinite(sb);
if (aOk && bOk) return fb - fa; if (aOk && bOk) return sb - sa;
if (aOk) return -1; if (aOk) return -1;
if (bOk) return 1; if (bOk) return 1;
return String(a[0]).localeCompare(String(b[0])); return String(a[0]).localeCompare(String(b[0]));
+13 -13
View File
@@ -682,33 +682,34 @@ def test_overview_adjacent_previous_with_seed_and_dual_fields():
db.close() db.close()
def test_system_tasks_sorted_by_finished_at_desc(): def test_system_tasks_sorted_by_started_at_desc():
"""最近任务按 finished_at 倒序;缺时间的排末尾。""" """最近任务按 started_at 倒序;缺开始时间的排末尾(含进行中无 finished)。"""
import json import json
with tempfile.TemporaryDirectory() as tmp: with tempfile.TemporaryDirectory() as tmp:
db, q = setup(tmp) db, q = setup(tmp)
try: try:
repo = Repository(db) repo = Repository(db)
# 故意打乱写入/键名字母序,避免 ORDER BY k 碰巧正确 # 故意打乱写入/键名字母序;finished_at 与 started_at 错开,避免误用完成时间
samples = ( samples = (
("zzz_old", FIXED_NOW - 300), # name, started_at, finished_at
("aaa_mid", FIXED_NOW - 100), ("zzz_old", FIXED_NOW - 300, FIXED_NOW - 10), # 开始最早,完成却很晚
("mmm_new", FIXED_NOW - 10), ("aaa_mid", FIXED_NOW - 100, FIXED_NOW - 90),
("no_time", None), ("mmm_new", FIXED_NOW - 10, None), # 进行中:只有 started
("no_time", None, FIXED_NOW), # 缺 started,应末尾
) )
for name, finished in samples: for name, started, finished in samples:
payload = { payload = {
"name": name, "name": name,
"ok": True, "ok": True if finished is not None else True,
"started_at": (finished - 5) if finished is not None else FIXED_NOW,
"error": None, "error": None,
"summary": {}, "summary": {},
} }
if started is not None:
payload["started_at"] = started
if finished is not None: if finished is not None:
payload["finished_at"] = finished payload["finished_at"] = finished
repo.kv_set(f"task:last:{name}", json.dumps(payload)) repo.kv_set(f"task:last:{name}", json.dumps(payload))
# 清掉 fixture 里的旧任务,只保留本用例
for legacy in ("snapshot", "fans", "discovery"): for legacy in ("snapshot", "fans", "discovery"):
repo.kv_delete(f"task:last:{legacy}") repo.kv_delete(f"task:last:{legacy}")
DASHBOARD_CACHE.clear() DASHBOARD_CACHE.clear()
@@ -717,7 +718,6 @@ def test_system_tasks_sorted_by_finished_at_desc():
"mmm_new", "aaa_mid", "zzz_old", "no_time", "mmm_new", "aaa_mid", "zzz_old", "no_time",
] ]
overview = q.overview() overview = q.overview()
# overview 也走 _task_records,顺序应一致
assert list(overview["tasks"].keys()) == list(system["tasks"].keys()) assert list(overview["tasks"].keys()) == list(system["tasks"].keys())
finally: finally:
db.close() db.close()
@@ -738,7 +738,7 @@ def main_test():
test_fans_candidates_system_and_export() test_fans_candidates_system_and_export()
test_system_status_uses_completed_date_and_running_flags() test_system_status_uses_completed_date_and_running_flags()
test_system_next_runs_follow_last_completion_not_wall_clock() test_system_next_runs_follow_last_completion_not_wall_clock()
test_system_tasks_sorted_by_finished_at_desc() test_system_tasks_sorted_by_started_at_desc()
test_growth_auto_falls_back_to_one_day() test_growth_auto_falls_back_to_one_day()
test_snapshot_recent_next_run_follows_completion() test_snapshot_recent_next_run_follows_completion()
test_overview_adjacent_previous_with_seed_and_dual_fields() test_overview_adjacent_previous_with_seed_and_dual_fields()