feat(collect): show per-item collection activity timer

This commit is contained in:
chengma
2026-07-14 11:10:36 +08:00
parent da1da13fdf
commit 0abd7ffdff
4 changed files with 494 additions and 9 deletions
+218 -1
View File
@@ -20,6 +20,35 @@ def CollectWorker(*args, **kwargs):
def WriteBackWorker(*args, **kwargs):
return _call_package_attr("WriteBackWorker", _RealWriteBackWorker, *args, **kwargs)
COLLECT_ACTIVITY_STEP_LABELS = {
"preflight": "检查账号",
"match_account": "匹配账号",
"check_login": "检查账号登录",
"prepare_task": "准备采集",
"open_product": "打开商品页",
"wait_ready": "等待商品页加载",
"read_title": "读取标题",
"read_cover": "读取封面",
"download_cover": "下载封面",
"save_result": "保存采集结果",
"db_write": "保存采集结果",
}
def _collect_activity_step_label(step):
return COLLECT_ACTIVITY_STEP_LABELS.get(str(step or ""), "处理当前商品")
def _format_collect_elapsed(seconds):
total = max(0, int(seconds or 0))
hours, remainder = divmod(total, 3600)
minutes, seconds = divmod(remainder, 60)
if hours:
return f"{hours}:{minutes:02d}:{seconds:02d}"
return f"{minutes:02d}:{seconds:02d}"
class CollectTab(QWidget):
"""Tab 1: import Excel files and list imported tasks."""
@@ -56,6 +85,15 @@ class CollectTab(QWidget):
self.write_back_worker = None
self.write_back_thread = None
self.last_collect_run_id = None
self._collect_run_started_at = None
self._collect_task_started_at = None
self._collect_task_elapsed_seconds = 0
self._collect_activity_payload = {}
self._collect_terminal_text = ""
self._collect_stop_requested = False
self._collect_elapsed_timer = QTimer(self)
self._collect_elapsed_timer.setInterval(1000)
self._collect_elapsed_timer.timeout.connect(self._refresh_collect_activity)
self.import_button = QPushButton("导入 Excel...")
self.refresh_button = QPushButton("刷新")
@@ -101,6 +139,14 @@ class CollectTab(QWidget):
self.summary_label = QLabel("未导入任务")
self.summary_label.setTextFormat(Qt.RichText)
self.batch_progress_label = _build_batch_progress_overview("collectBatchProgressOverview")
self.collect_activity_label = QLabel("")
self.collect_activity_label.setObjectName("collectActivityLabel")
self.collect_activity_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
activity_sample = "正在采集 999/999 · 等待商品页加载 · 本条 99:59"
activity_width = self.collect_activity_label.fontMetrics().horizontalAdvance(activity_sample) + 24
self.collect_activity_label.setFixedWidth(activity_width)
self.collect_activity_label.setVisible(False)
self._set_collect_activity_style("muted")
self.match_detail_label = QLabel("")
self.show_all_button = QPushButton("全部")
self.show_unmatched_button = QPushButton("未匹配(0)")
@@ -112,6 +158,12 @@ class CollectTab(QWidget):
summary_layout.addWidget(self.show_all_button)
summary_layout.addWidget(self.show_unmatched_button)
batch_progress_layout = QHBoxLayout()
batch_progress_layout.setContentsMargins(0, 0, 0, 0)
batch_progress_layout.setSpacing(8)
batch_progress_layout.addWidget(self.batch_progress_label, 1)
batch_progress_layout.addWidget(self.collect_activity_label)
self.table = QTableView()
self.model = TaskTableModel(self.table)
self.table.setModel(self.model)
@@ -142,7 +194,7 @@ class CollectTab(QWidget):
layout.addLayout(filter_layout)
layout.addLayout(summary_layout)
layout.addWidget(self.match_detail_label)
layout.addWidget(self.batch_progress_label)
layout.addLayout(batch_progress_layout)
layout.addWidget(self.empty_state_card)
layout.addWidget(self.table, 1)
layout.addWidget(QLabel("采集运行日志"))
@@ -168,6 +220,156 @@ class CollectTab(QWidget):
def _set_status(self, message, level=None):
_emit_status(self.status_callback, message, level=level)
def _set_collect_activity_style(self, level):
color = _status_level_color(level)
self.collect_activity_label.setStyleSheet(
"QLabel#collectActivityLabel { "
"background: #f6f8fa; border: 1px solid #d0d7de; "
"border-radius: 6px; padding: 8px 10px; "
f"color: {color}; font-weight: 600; "
"}"
)
def _start_collect_activity(self):
now = time.monotonic()
self._collect_run_started_at = now
self._collect_task_started_at = None
self._collect_task_elapsed_seconds = 0
self._collect_activity_payload = {
"state": "preflight_started",
"total": 0,
"step": "preflight",
}
self._collect_terminal_text = ""
self._collect_stop_requested = False
self._refresh_collect_activity(now=now)
if not self._collect_elapsed_timer.isActive():
self._collect_elapsed_timer.start()
def _on_collect_activity(self, payload):
event = dict(payload or {})
state = str(event.get("state") or "")
now = time.monotonic()
if self._collect_run_started_at is None:
self._collect_run_started_at = now
self._collect_terminal_text = ""
if not self._collect_elapsed_timer.isActive():
self._collect_elapsed_timer.start()
if state == "preflight_started":
self._collect_activity_payload = event
elif state == "task_started":
self._collect_task_started_at = now
self._collect_task_elapsed_seconds = 0
self._collect_activity_payload = event
elif state in {"task_step", "task_finished"}:
current = dict(self._collect_activity_payload)
current.update(event)
self._collect_activity_payload = current
if state == "task_finished" and self._collect_task_started_at is not None:
self._collect_task_elapsed_seconds = max(
0,
int(now - self._collect_task_started_at),
)
self._collect_task_started_at = None
self._refresh_collect_activity(now=now)
def _refresh_collect_activity(self, now=None):
if self._collect_terminal_text:
self.collect_activity_label.setVisible(True)
self.collect_activity_label.setText(self._collect_terminal_text)
return
if self._collect_run_started_at is None:
self.collect_activity_label.setVisible(False)
return
current_time = time.monotonic() if now is None else now
run_elapsed = max(0, int(current_time - self._collect_run_started_at))
if self._collect_task_started_at is not None:
task_elapsed = max(0, int(current_time - self._collect_task_started_at))
else:
task_elapsed = self._collect_task_elapsed_seconds
event = self._collect_activity_payload
state = str(event.get("state") or "")
index = int(event.get("index") or 0)
total = int(event.get("total") or 0)
progress = f"{index}/{total}" if index and total else str(index or total or "")
step_label = _collect_activity_step_label(event.get("step"))
if self._collect_stop_requested:
if index:
text = f"正在停止 · 本条 {_format_collect_elapsed(task_elapsed)}"
else:
text = f"正在停止 · 已等待 {_format_collect_elapsed(run_elapsed)}"
level = "warning"
elif state in {"task_started", "task_step"}:
text = (
f"正在采集 {progress} · {step_label} · "
f"本条 {_format_collect_elapsed(task_elapsed)}"
)
level = "info"
elif state == "task_finished":
result_label = {
"success": "已完成",
"failed": "失败",
"skipped": "略过",
}.get(str(event.get("result") or ""), "已结束")
text = (
f"第 {progress} 条 · {result_label} · "
f"本条 {_format_collect_elapsed(task_elapsed)}"
)
level = "danger" if event.get("result") == "failed" else "muted"
else:
text = f"正在检查账号 · {_format_collect_elapsed(run_elapsed)}"
level = "info"
tooltip_parts = []
if event.get("item_id"):
tooltip_parts.append(f"商品ID:{event.get('item_id')}")
if event.get("alias"):
tooltip_parts.append(f"账号别名:{event.get('alias')}")
if state in {"task_started", "task_step"}:
tooltip_parts.append(f"当前阶段:{step_label}")
self.collect_activity_label.setToolTip("\n".join(tooltip_parts))
self._set_collect_activity_style(level)
self.collect_activity_label.setText(text)
self.collect_activity_label.setVisible(True)
def _finish_collect_activity(self, outcome, payload=None):
now = time.monotonic()
if self._collect_run_started_at is None:
total_elapsed = 0
else:
total_elapsed = max(0, int(now - self._collect_run_started_at))
self._collect_elapsed_timer.stop()
self._collect_run_started_at = None
self._collect_task_started_at = None
self._collect_task_elapsed_seconds = 0
self._collect_activity_payload = {}
self._collect_stop_requested = False
if outcome == "blocked":
text = "采集未开始 · 检查未通过"
level = "warning"
tooltip = "采集前检查未通过,请按弹窗提示处理"
elif outcome == "cancelled":
text = f"采集已停止 · 总用时 {_format_collect_elapsed(total_elapsed)}"
level = "warning"
tooltip = "本轮采集已停止"
elif outcome == "error":
text = "采集已结束 · 请查看运行日志"
level = "danger"
tooltip = "采集异常结束,请查看下方采集运行日志"
else:
text = f"采集完成 · 总用时 {_format_collect_elapsed(total_elapsed)}"
level = "success"
tooltip = "本轮采集已经完成"
self._collect_terminal_text = text
self.collect_activity_label.setToolTip(tooltip)
self._set_collect_activity_style(level)
self.collect_activity_label.setText(text)
self.collect_activity_label.setVisible(True)
def _on_collect_log(self, message):
self._append_collect_log(message)
self._set_status(message)
@@ -522,6 +724,9 @@ class CollectTab(QWidget):
config=self.config,
diagnostic_log_dir=diagnostics.DEFAULT_LOG_DIR,
)
activity_signal = getattr(worker, "activity", None)
if activity_signal is not None:
activity_signal.connect(self._on_collect_activity)
worker.progress.connect(self._on_collect_progress)
worker.row_updated.connect(self._on_collect_row_updated)
worker.log.connect(self._on_collect_log)
@@ -534,11 +739,14 @@ class CollectTab(QWidget):
self.collect_worker = worker
self.collect_thread = thread
self._set_collect_running(True)
self._start_collect_activity()
thread.start()
def stop_collect(self, checked=False):
if self.collect_worker is not None:
self.collect_worker.cancel()
self._collect_stop_requested = True
self._refresh_collect_activity()
self._set_status("正在停止采集...")
def write_back_old_data(self, checked=False):
@@ -616,6 +824,8 @@ class CollectTab(QWidget):
def _forget_collect_thread(self, thread):
if self.collect_thread is thread:
if self._collect_elapsed_timer.isActive():
self._finish_collect_activity("error")
self.collect_thread = None
self.collect_worker = None
@@ -643,6 +853,12 @@ class CollectTab(QWidget):
def _on_collect_finished(self, payload):
self._set_collect_running(False)
if payload.get("blocked"):
self._finish_collect_activity("blocked", payload)
elif payload.get("error"):
self._finish_collect_activity("error", payload)
else:
self._finish_collect_activity("finished", payload)
self.last_collect_run_id = payload.get("run_id") or self.last_collect_run_id
self.refresh_tasks()
self._load_latest_collect_run_log()
@@ -747,6 +963,7 @@ class CollectTab(QWidget):
def _on_collect_cancelled(self, payload):
self._set_collect_running(False)
self._finish_collect_activity("cancelled", payload)
self.refresh_tasks()
self._set_status(
"采集已停止:完成{done}/{total}".format(
+115 -1
View File
@@ -7,6 +7,11 @@ import re
import threading
import time
try:
from PySide6.QtCore import Signal
except ModuleNotFoundError: # pragma: no cover - GUI import guard
Signal = None
from .. import ai, image_studio, image_studio_export, image_studio_generation, image_studio_images
from ..collect_skip import ALIAS_UNMATCHED, LOGIN_REQUIRED, empty_skip_reason_counts
from .widgets import *
@@ -1743,6 +1748,9 @@ class ApplyWorker(BaseWorker):
class CollectWorker(BaseWorker):
"""Collect old title and cover for imported tasks."""
if Signal is not None:
activity = Signal(dict)
LOGIN_CHECK_ATTEMPTS = 3
LOGIN_CHECK_RETRY_DELAY_SECONDS = 2.0
@@ -1785,6 +1793,11 @@ class CollectWorker(BaseWorker):
skip_reason_counts = empty_skip_reason_counts()
self._run_id = self._create_run_log(eligible, batch_ids)
self._emit_activity(
"preflight_started",
total=total,
step="preflight",
)
self._log_run_event(
f"step=preflight result=start detail=采集运行开始 total={total}"
)
@@ -1822,9 +1835,16 @@ class CollectWorker(BaseWorker):
level="warning",
)
for task in eligible:
for index, task in enumerate(eligible, start=1):
if self.should_cancel():
break
self._emit_activity(
"task_started",
task=task,
index=index,
total=total,
step="match_account",
)
account = account_by_alias.get(str(task.alias).strip())
if account is None:
skipped += 1
@@ -1842,6 +1862,14 @@ class CollectWorker(BaseWorker):
task=task,
level="warning",
)
self._emit_activity(
"task_finished",
task=task,
index=index,
total=total,
step="match_account",
result="skipped",
)
self._emit_progress(done, total, collected, skipped, failed)
continue
@@ -1862,9 +1890,24 @@ class CollectWorker(BaseWorker):
task=task,
level="warning",
)
self._emit_activity(
"task_finished",
task=task,
index=index,
total=total,
step="check_login",
result="skipped",
)
self._emit_progress(done, total, collected, skipped, failed)
continue
self._emit_activity(
"task_step",
task=task,
index=index,
total=total,
step="check_login",
)
status = self._confirmed_login_status(account, context="midrun", task=task)
if self._is_definitive_logged_out(status):
alias = str(task.alias).strip()
@@ -1885,6 +1928,14 @@ class CollectWorker(BaseWorker):
task=task,
level="warning",
)
self._emit_activity(
"task_finished",
task=task,
index=index,
total=total,
step="check_login",
result="skipped",
)
self._emit_progress(done, total, collected, skipped, failed)
continue
if not status.get("logged_in"):
@@ -1900,10 +1951,18 @@ class CollectWorker(BaseWorker):
started = time.monotonic()
current_step = "db_write"
activity_result = "success"
def on_step(step):
nonlocal current_step
current_step = str(step)
self._emit_activity(
"task_step",
task=task,
index=index,
total=total,
step=current_step,
)
self._log_run_event(
"step={step} result=start detail=任务 {task_id} 商品 {item_id}".format(
step=current_step,
@@ -1914,6 +1973,13 @@ class CollectWorker(BaseWorker):
)
try:
self._emit_activity(
"task_step",
task=task,
index=index,
total=total,
step="prepare_task",
)
self._log_run_event(
"step=db_write result=start detail=任务 {task_id} 商品 {item_id} 标记采集运行".format(
task_id=task.id,
@@ -1932,6 +1998,13 @@ class CollectWorker(BaseWorker):
on_step=on_step,
)
current_step = "db_write"
self._emit_activity(
"task_step",
task=task,
index=index,
total=total,
step="save_result",
)
self._log_run_event(
"step=db_write result=start detail=任务 {task_id} 商品 {item_id} 保存采集结果".format(
task_id=task.id,
@@ -1965,6 +2038,7 @@ class CollectWorker(BaseWorker):
task=task,
)
except Exception as exc:
activity_result = "failed"
failed += 1
error = str(exc) or exc.__class__.__name__
safe_error = diagnostics.redact_log_text(error)
@@ -1993,6 +2067,14 @@ class CollectWorker(BaseWorker):
)
finally:
done += 1
self._emit_activity(
"task_finished",
task=task,
index=index,
total=total,
step=current_step,
result=activity_result,
)
self._emit_progress(done, total, collected, skipped, failed)
summary = self._summary(
@@ -2115,6 +2197,38 @@ class CollectWorker(BaseWorker):
payload["reason"] = reason
return payload
def _emit_activity(
self,
state,
*,
task=None,
index=0,
total=0,
step=None,
result=None,
):
signal = getattr(self, "activity", None)
if signal is None:
return
payload = {
"state": str(state),
"index": int(index or 0),
"total": int(total or 0),
}
if task is not None:
payload.update(
{
"task_id": getattr(task, "id", None),
"item_id": str(getattr(task, "item_id", "") or ""),
"alias": str(getattr(task, "alias", "") or ""),
}
)
if step:
payload["step"] = str(step)
if result:
payload["result"] = str(result)
signal.emit(payload)
def _emit_progress(self, done, total, collected, skipped, failed):
self.progress.emit(
{
+8 -1
View File
@@ -3,7 +3,7 @@ id: T-628
title: 采集当前记录阶段与单条耗时指示器
phase: 2
deps: [T-620]
status: TODO
status: DONE
created: 2026-07-14
---
@@ -86,3 +86,10 @@ created: 2026-07-14
- 不修改任务 `stage/status`、SQLite schema、Excel schema、回写流程或批次统计口径。
- 不把计时器放进 Worker 线程,不让 GUI 每秒查询 SQLite,也不把每秒 tick 写入运行日志。
- 不改变②AI生成、③更新蝦皮及⑥商品套图的计时显示。
## 执行记录
- 2026-07-14:`CollectWorker` 新增独立 `activity` 结构化信号,在前置检查、每条记录开始、采集步骤变化及单条结束时发送稳定 payload;每条记录进入循环即发 `task_started`,登录检查和 `editor.collect()` 步骤只更新阶段,不改变原有进度、日志、状态写库和终态信号。
- 2026-07-14:①「导入采集」在批次进度同行右侧新增固定宽度运行指示器,显示前置检查、`N/总数`、当前中文阶段和本条 `MM:SS`;基于 `time.monotonic()` 每秒刷新,每条开始重置,步骤变化不重置,停止请求期间继续显示实际等待,完成、阻断、停止和异常后冻结中文终态。
- 2026-07-14:新增 Worker 活动事件顺序和 GUI 计时生命周期测试,覆盖三条记录的成功/略过事件、步骤不重置、下一条归零、停止等待、终态冻结、小时格式及中文 tooltip;用户可见终态不展示底层英文异常。
- 验证:当前工作区全套 GUI 测试除任务开始前已存在的默认封面提示词 `papa1.txt` 改名造成的 2 项旧断言外,采集相关测试全部通过;在仅覆盖 T-628 四个文件的隔离 worktree 中运行 `python -m ruff check app tests main.py`、`py -3.10 -m compileall app main.py`、`py -3.10 -m unittest discover -s tests`(461 项通过)及 `git diff --check`,结果全部通过。
+153 -6
View File
@@ -65,6 +65,7 @@ from app.gui import (
)
from app.gui import file_manager
import app.gui.workers as gui_workers
from app.gui.tabs.collect import _format_collect_elapsed
from app.gui.tabs.generate import CoverGalleryDialog, OriginalImageDialog
from app.gui.main_window import _fit_and_center_window
@@ -7391,6 +7392,130 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assert_removed(temp_dir)
def test_collect_activity_tracks_step_resets_each_task_and_freezes_on_stop(self):
with self.make_temp_dir() as temp_dir:
tab = CollectTab(config=self.make_config(temp_dir))
self.addCleanup(tab.close)
self.assertEqual("collectActivityLabel", tab.collect_activity_label.objectName())
self.assertTrue(tab.collect_activity_label.isHidden())
self.assertEqual("1:01:01", _format_collect_elapsed(3661))
with mock.patch("app.gui.tabs.collect.time.monotonic", return_value=100.0):
tab._start_collect_activity()
self.assertTrue(tab._collect_elapsed_timer.isActive())
self.assertIn("正在检查账号", tab.collect_activity_label.text())
self.assertIn("00:00", tab.collect_activity_label.text())
with mock.patch("app.gui.tabs.collect.time.monotonic", return_value=105.0):
tab._on_collect_activity(
{
"state": "preflight_started",
"total": 2,
"step": "preflight",
}
)
self.assertIn("正在检查账号", tab.collect_activity_label.text())
self.assertIn("00:05", tab.collect_activity_label.text())
with mock.patch("app.gui.tabs.collect.time.monotonic", return_value=110.0):
tab._on_collect_activity(
{
"state": "task_started",
"index": 1,
"total": 2,
"task_id": 1,
"item_id": "51100639510",
"alias": "alias-a",
"step": "match_account",
}
)
self.assertEqual(
"正在采集 1/2 · 匹配账号 · 本条 00:00",
tab.collect_activity_label.text(),
)
with mock.patch("app.gui.tabs.collect.time.monotonic", return_value=117.0):
tab._on_collect_activity(
{
"state": "task_step",
"index": 1,
"total": 2,
"task_id": 1,
"item_id": "51100639510",
"alias": "alias-a",
"step": "wait_ready",
}
)
self.assertEqual(
"正在采集 1/2 · 等待商品页加载 · 本条 00:07",
tab.collect_activity_label.text(),
)
self.assertIn("商品ID:51100639510", tab.collect_activity_label.toolTip())
self.assertIn("当前阶段:等待商品页加载", tab.collect_activity_label.toolTip())
with mock.patch("app.gui.tabs.collect.time.monotonic", return_value=120.0):
tab._on_collect_activity(
{
"state": "task_started",
"index": 2,
"total": 2,
"task_id": 2,
"item_id": "51100639511",
"alias": "alias-a",
"step": "check_login",
}
)
self.assertEqual(
"正在采集 2/2 · 检查账号登录 · 本条 00:00",
tab.collect_activity_label.text(),
)
with mock.patch("app.gui.tabs.collect.time.monotonic", return_value=125.0):
tab._collect_stop_requested = True
tab._refresh_collect_activity()
self.assertEqual("正在停止 · 本条 00:05", tab.collect_activity_label.text())
with mock.patch("app.gui.tabs.collect.time.monotonic", return_value=130.0):
tab._finish_collect_activity("cancelled")
self.assertEqual("采集已停止 · 总用时 00:30", tab.collect_activity_label.text())
self.assertFalse(tab._collect_elapsed_timer.isActive())
frozen_text = tab.collect_activity_label.text()
with mock.patch("app.gui.tabs.collect.time.monotonic", return_value=999.0):
tab._refresh_collect_activity()
self.assertEqual(frozen_text, tab.collect_activity_label.text())
self.assert_removed(temp_dir)
def test_collect_activity_terminal_states_use_chinese_semantics(self):
with self.make_temp_dir() as temp_dir:
tab = CollectTab(config=self.make_config(temp_dir))
self.addCleanup(tab.close)
with mock.patch("app.gui.tabs.collect.time.monotonic", return_value=10.0):
tab._start_collect_activity()
with mock.patch("app.gui.tabs.collect.time.monotonic", return_value=15.0):
tab._finish_collect_activity("blocked")
self.assertEqual("采集未开始 · 检查未通过", tab.collect_activity_label.text())
self.assertFalse(tab._collect_elapsed_timer.isActive())
with mock.patch("app.gui.tabs.collect.time.monotonic", return_value=20.0):
tab._start_collect_activity()
with mock.patch("app.gui.tabs.collect.time.monotonic", return_value=25.0):
tab._finish_collect_activity("error", {"error": "后台线程异常"})
self.assertEqual("采集已结束 · 请查看运行日志", tab.collect_activity_label.text())
self.assertEqual("采集异常结束,请查看下方采集运行日志", tab.collect_activity_label.toolTip())
with mock.patch("app.gui.tabs.collect.time.monotonic", return_value=30.0):
tab._start_collect_activity()
with mock.patch("app.gui.tabs.collect.time.monotonic", return_value=40.0):
tab._finish_collect_activity("finished")
self.assertEqual("采集完成 · 总用时 00:10", tab.collect_activity_label.text())
self.assertFalse(tab._collect_elapsed_timer.isActive())
self.assert_removed(temp_dir)
def test_collect_tab_soft_deletes_batch_and_refreshes_workflow_tabs(self):
with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir)
@@ -7706,14 +7831,17 @@ class GuiTests(TempDirMixin, unittest.TestCase):
"old_cover_path": task["old_cover_path"],
}
worker = CollectWorker(
tasks,
db_path=cfg["db_path"],
config=cfg,
preflight=False,
)
activity_events = []
worker.activity.connect(activity_events.append)
with mock.patch("app.gui.accounts.detect_login", side_effect=fake_login), \
mock.patch("app.gui.editor.collect", side_effect=fake_collect) as collect:
summary = CollectWorker(
tasks,
db_path=cfg["db_path"],
config=cfg,
preflight=False,
).execute()
summary = worker.execute()
self.assertTrue(summary["ok"])
self.assertEqual(3, summary["total"])
@@ -7728,6 +7856,25 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assertEqual([batch_id], summary["batch_ids"])
self.assertIsInstance(summary["run_id"], int)
collect.assert_called_once()
self.assertEqual("preflight_started", activity_events[0]["state"])
self.assertEqual(3, activity_events[0]["total"])
task_starts = [event for event in activity_events if event["state"] == "task_started"]
self.assertEqual([1, 2, 3], [event["index"] for event in task_starts])
self.assertEqual(
["51100639510", "51100639511", "51100639512"],
[event["item_id"] for event in task_starts],
)
self.assertTrue(
any(
event["state"] == "task_step" and event.get("step") == "download_cover"
for event in activity_events
)
)
task_finishes = [event for event in activity_events if event["state"] == "task_finished"]
self.assertEqual(
["success", "skipped", "skipped"],
[event["result"] for event in task_finishes],
)
updated = db.list_tasks(batch_id=batch_id, path=cfg["db_path"])
by_alias = {task.alias: task for task in updated}
self.assertEqual("collected", by_alias["alias-a"].stage)