fix(product-suite): keep retries in current results
This commit is contained in:
+142
-19
@@ -697,6 +697,9 @@ class SuiteTaskState:
|
|||||||
settings: dict = field(default_factory=product_suite.default_suite_settings)
|
settings: dict = field(default_factory=product_suite.default_suite_settings)
|
||||||
current_job_ids: list = field(default_factory=list)
|
current_job_ids: list = field(default_factory=list)
|
||||||
show_history: bool = False
|
show_history: bool = False
|
||||||
|
generation_job_ids: list = field(default_factory=list)
|
||||||
|
generation_mode: str = "batch"
|
||||||
|
generation_retry_job_id: int = None
|
||||||
worker: object = None
|
worker: object = None
|
||||||
thread: object = None
|
thread: object = None
|
||||||
generation_run_token: str = ""
|
generation_run_token: str = ""
|
||||||
@@ -1582,6 +1585,9 @@ class ProductSuiteTab(QWidget):
|
|||||||
state.project_binding_state = ""
|
state.project_binding_state = ""
|
||||||
state.last_saved_prompt = ""
|
state.last_saved_prompt = ""
|
||||||
state.current_job_ids = []
|
state.current_job_ids = []
|
||||||
|
state.generation_job_ids = []
|
||||||
|
state.generation_mode = "batch"
|
||||||
|
state.generation_retry_job_id = None
|
||||||
state.done = state.failed = state.total = 0
|
state.done = state.failed = state.total = 0
|
||||||
state.started_at = None
|
state.started_at = None
|
||||||
if state is self._displayed_state:
|
if state is self._displayed_state:
|
||||||
@@ -2517,12 +2523,22 @@ class ProductSuiteTab(QWidget):
|
|||||||
if state is None:
|
if state is None:
|
||||||
return
|
return
|
||||||
if state.generation_running():
|
if state.generation_running():
|
||||||
|
retrying = state.generation_mode == "retry"
|
||||||
if state.generation_stop_requested:
|
if state.generation_stop_requested:
|
||||||
self._status("正在停止当前套图任务", "warning")
|
self._status(
|
||||||
|
"正在停止当前图片重试"
|
||||||
|
if retrying
|
||||||
|
else "正在停止当前套图任务",
|
||||||
|
"warning",
|
||||||
|
)
|
||||||
return
|
return
|
||||||
if self._confirm(
|
if self._confirm(
|
||||||
"停止生成套图",
|
"停止图片重试" if retrying else "停止生成套图",
|
||||||
"确认取消当前任务吗?已提交任务会在安全边界停止。",
|
(
|
||||||
|
"确认停止当前图片重试吗?已提交任务会在安全边界停止。"
|
||||||
|
if retrying
|
||||||
|
else "确认取消当前任务吗?已提交任务会在安全边界停止。"
|
||||||
|
),
|
||||||
destructive=True,
|
destructive=True,
|
||||||
):
|
):
|
||||||
state.generation_stop_requested = True
|
state.generation_stop_requested = True
|
||||||
@@ -2533,14 +2549,21 @@ class ProductSuiteTab(QWidget):
|
|||||||
"stop_requested",
|
"stop_requested",
|
||||||
)
|
)
|
||||||
self._apply_running_state(state)
|
self._apply_running_state(state)
|
||||||
self._status("已请求停止当前套图任务", "warning")
|
self._status(
|
||||||
|
"已请求停止当前图片重试"
|
||||||
|
if retrying
|
||||||
|
else "已请求停止当前套图任务",
|
||||||
|
"warning",
|
||||||
|
)
|
||||||
return
|
return
|
||||||
self.start_generation(state)
|
self.start_generation(state)
|
||||||
|
|
||||||
def start_generation(self, state, specs=None):
|
def start_generation(self, state, specs=None, *, retry_job_id=None):
|
||||||
if state.generation_running():
|
if state.generation_running():
|
||||||
self._status("当前套图任务仍在生成", "warning")
|
self._status("当前套图任务仍在生成", "warning")
|
||||||
return False
|
return False
|
||||||
|
retry_job_id = int(retry_job_id) if retry_job_id is not None else None
|
||||||
|
retrying = retry_job_id is not None
|
||||||
if state is self._displayed_state:
|
if state is self._displayed_state:
|
||||||
self._save_controls_to_state(state)
|
self._save_controls_to_state(state)
|
||||||
template_text = None
|
template_text = None
|
||||||
@@ -2595,10 +2618,14 @@ class ProductSuiteTab(QWidget):
|
|||||||
state.generation_run_token = run_token
|
state.generation_run_token = run_token
|
||||||
state.generation_stop_requested = False
|
state.generation_stop_requested = False
|
||||||
state.generation_terminal_streak = 0
|
state.generation_terminal_streak = 0
|
||||||
|
state.generation_job_ids = []
|
||||||
|
state.generation_mode = "retry" if retrying else "batch"
|
||||||
|
state.generation_retry_job_id = retry_job_id
|
||||||
state.done = 0
|
state.done = 0
|
||||||
state.failed = 0
|
state.failed = 0
|
||||||
state.total = len(specs)
|
state.total = len(specs)
|
||||||
state.started_at = time.monotonic()
|
state.started_at = time.monotonic()
|
||||||
|
if not retrying:
|
||||||
state.current_job_ids = []
|
state.current_job_ids = []
|
||||||
state.show_history = False
|
state.show_history = False
|
||||||
self._generation_run_states[run_token] = state.key
|
self._generation_run_states[run_token] = state.key
|
||||||
@@ -2611,17 +2638,27 @@ class ProductSuiteTab(QWidget):
|
|||||||
state,
|
state,
|
||||||
run_token,
|
run_token,
|
||||||
"started",
|
"started",
|
||||||
{"total": len(specs), "job_ids": 0},
|
{
|
||||||
|
"total": len(specs),
|
||||||
|
"job_ids": 0,
|
||||||
|
"mode": state.generation_mode,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
if state is self._displayed_state:
|
if state is self._displayed_state and not retrying:
|
||||||
self._loading = True
|
self._loading = True
|
||||||
try:
|
try:
|
||||||
self.history_button.setChecked(False)
|
self.history_button.setChecked(False)
|
||||||
finally:
|
finally:
|
||||||
self._loading = False
|
self._loading = False
|
||||||
|
if state is self._displayed_state:
|
||||||
self._apply_running_state(state)
|
self._apply_running_state(state)
|
||||||
self._refresh_results(state)
|
self._refresh_results(state)
|
||||||
self._status("商品套图生成已开始,共%d张;可切换到其他任务" % len(specs), "info")
|
self._status(
|
||||||
|
"图片重试已开始;可切换到其他任务"
|
||||||
|
if retrying
|
||||||
|
else "商品套图生成已开始,共%d张;可切换到其他任务" % len(specs),
|
||||||
|
"info",
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _generation_signal_token(self, payload=None):
|
def _generation_signal_token(self, payload=None):
|
||||||
@@ -2651,7 +2688,7 @@ class ProductSuiteTab(QWidget):
|
|||||||
state.failed = int(payload.get("failed", state.failed) or 0)
|
state.failed = int(payload.get("failed", state.failed) or 0)
|
||||||
job_ids = payload.get("job_ids")
|
job_ids = payload.get("job_ids")
|
||||||
if job_ids is not None:
|
if job_ids is not None:
|
||||||
state.current_job_ids = [int(job_id) for job_id in job_ids]
|
self._set_generation_job_ids(state, job_ids)
|
||||||
state.generation_terminal_streak = 0
|
state.generation_terminal_streak = 0
|
||||||
if state is self._displayed_state:
|
if state is self._displayed_state:
|
||||||
self._refresh_results(state)
|
self._refresh_results(state)
|
||||||
@@ -2670,7 +2707,15 @@ class ProductSuiteTab(QWidget):
|
|||||||
{"has_error": True},
|
{"has_error": True},
|
||||||
level="ERROR",
|
level="ERROR",
|
||||||
)
|
)
|
||||||
self._status("商品套图生成失败:%s" % _user_error(error), "danger")
|
self._status(
|
||||||
|
(
|
||||||
|
"图片重试失败:%s"
|
||||||
|
if state.generation_mode == "retry"
|
||||||
|
else "商品套图生成失败:%s"
|
||||||
|
)
|
||||||
|
% _user_error(error),
|
||||||
|
"danger",
|
||||||
|
)
|
||||||
|
|
||||||
@Slot(dict)
|
@Slot(dict)
|
||||||
def _on_generation_finished_signal(self, result):
|
def _on_generation_finished_signal(self, result):
|
||||||
@@ -2678,6 +2723,8 @@ class ProductSuiteTab(QWidget):
|
|||||||
state = self._generation_state(token)
|
state = self._generation_state(token)
|
||||||
if state is None:
|
if state is None:
|
||||||
return
|
return
|
||||||
|
if result.get("job_ids") is not None:
|
||||||
|
self._set_generation_job_ids(state, result.get("job_ids"))
|
||||||
self._log_generation_lifecycle(
|
self._log_generation_lifecycle(
|
||||||
state,
|
state,
|
||||||
token,
|
token,
|
||||||
@@ -2779,7 +2826,7 @@ class ProductSuiteTab(QWidget):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _generation_job_ids(self, state):
|
def _generation_job_ids(self, state):
|
||||||
job_ids = list(state.current_job_ids)
|
job_ids = list(state.generation_job_ids)
|
||||||
if not job_ids and state.worker is not None:
|
if not job_ids and state.worker is not None:
|
||||||
worker_job_ids = getattr(state.worker, "job_ids", [])
|
worker_job_ids = getattr(state.worker, "job_ids", [])
|
||||||
if not isinstance(worker_job_ids, (list, tuple, set)):
|
if not isinstance(worker_job_ids, (list, tuple, set)):
|
||||||
@@ -2789,9 +2836,30 @@ class ProductSuiteTab(QWidget):
|
|||||||
for job_id in list(worker_job_ids or [])
|
for job_id in list(worker_job_ids or [])
|
||||||
]
|
]
|
||||||
if job_ids:
|
if job_ids:
|
||||||
state.current_job_ids = job_ids
|
self._set_generation_job_ids(state, job_ids)
|
||||||
return job_ids
|
return job_ids
|
||||||
|
|
||||||
|
def _set_generation_job_ids(self, state, job_ids):
|
||||||
|
normalized = []
|
||||||
|
for job_id in job_ids or []:
|
||||||
|
value = int(job_id)
|
||||||
|
if value not in normalized:
|
||||||
|
normalized.append(value)
|
||||||
|
state.generation_job_ids = normalized
|
||||||
|
if state.generation_mode != "retry":
|
||||||
|
state.current_job_ids = list(normalized)
|
||||||
|
return
|
||||||
|
retry_job_id = state.generation_retry_job_id
|
||||||
|
current = [int(job_id) for job_id in state.current_job_ids]
|
||||||
|
if retry_job_id in current:
|
||||||
|
index = current.index(retry_job_id)
|
||||||
|
current[index:index + 1] = normalized
|
||||||
|
elif not state.show_history:
|
||||||
|
for job_id in normalized:
|
||||||
|
if job_id not in current:
|
||||||
|
current.append(job_id)
|
||||||
|
state.current_job_ids = current
|
||||||
|
|
||||||
def _generation_job_snapshot(self, state):
|
def _generation_job_snapshot(self, state):
|
||||||
job_ids = self._generation_job_ids(state)
|
job_ids = self._generation_job_ids(state)
|
||||||
counts = {
|
counts = {
|
||||||
@@ -2865,6 +2933,7 @@ class ProductSuiteTab(QWidget):
|
|||||||
def _finalize_generation(self, state, run_token, result, *, source):
|
def _finalize_generation(self, state, run_token, result, *, source):
|
||||||
if self._generation_state(run_token) is not state:
|
if self._generation_state(run_token) is not state:
|
||||||
return False
|
return False
|
||||||
|
retrying = state.generation_mode == "retry"
|
||||||
snapshot = self._generation_job_snapshot(state)
|
snapshot = self._generation_job_snapshot(state)
|
||||||
if snapshot["job_ids"] and (
|
if snapshot["job_ids"] and (
|
||||||
snapshot["all_terminal"]
|
snapshot["all_terminal"]
|
||||||
@@ -2890,6 +2959,8 @@ class ProductSuiteTab(QWidget):
|
|||||||
state.generation_run_token = ""
|
state.generation_run_token = ""
|
||||||
state.generation_stop_requested = False
|
state.generation_stop_requested = False
|
||||||
state.generation_terminal_streak = 0
|
state.generation_terminal_streak = 0
|
||||||
|
state.generation_job_ids = []
|
||||||
|
state.generation_retry_job_id = None
|
||||||
state.worker = None
|
state.worker = None
|
||||||
state.thread = None
|
state.thread = None
|
||||||
state.done = success + failed + cancelled
|
state.done = success + failed + cancelled
|
||||||
@@ -2912,6 +2983,7 @@ class ProductSuiteTab(QWidget):
|
|||||||
"cancelled": cancelled,
|
"cancelled": cancelled,
|
||||||
"active": active,
|
"active": active,
|
||||||
"elapsed_seconds": elapsed,
|
"elapsed_seconds": elapsed,
|
||||||
|
"mode": "retry" if retrying else "batch",
|
||||||
},
|
},
|
||||||
level="WARNING" if active or result.get("ok") is False else "INFO",
|
level="WARNING" if active or result.get("ok") is False else "INFO",
|
||||||
)
|
)
|
||||||
@@ -2923,15 +2995,34 @@ class ProductSuiteTab(QWidget):
|
|||||||
or "生成线程已结束,部分任务可稍后继续查询"
|
or "生成线程已结束,部分任务可稍后继续查询"
|
||||||
)
|
)
|
||||||
if state is self._displayed_state:
|
if state is self._displayed_state:
|
||||||
self._message("商品套图生成未完整结束", _user_error(message))
|
self._message(
|
||||||
|
"图片重试未完整结束"
|
||||||
|
if retrying
|
||||||
|
else "商品套图生成未完整结束",
|
||||||
|
_user_error(message),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self._status(
|
self._status(
|
||||||
"套图任务%d生成未完整结束" % state.serial,
|
(
|
||||||
|
"套图任务%d图片重试未完整结束"
|
||||||
|
if retrying
|
||||||
|
else "套图任务%d生成未完整结束"
|
||||||
|
)
|
||||||
|
% state.serial,
|
||||||
"danger",
|
"danger",
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
if stop_requested or cancelled:
|
if stop_requested or cancelled:
|
||||||
if state is self._displayed_state:
|
if state is self._displayed_state:
|
||||||
|
if retrying:
|
||||||
|
self._message(
|
||||||
|
"图片重试已停止",
|
||||||
|
"本次重试:成功%d张,失败%d张,停止%d张;"
|
||||||
|
"已提交任务可稍后继续查询;总用时%d秒。"
|
||||||
|
% (success, failed, cancelled, elapsed),
|
||||||
|
icon=QMessageBox.Information,
|
||||||
|
)
|
||||||
|
else:
|
||||||
self._message(
|
self._message(
|
||||||
"商品套图生成已停止",
|
"商品套图生成已停止",
|
||||||
"本轮共%d张:成功%d张,失败%d张,停止%d张;"
|
"本轮共%d张:成功%d张,失败%d张,停止%d张;"
|
||||||
@@ -2940,18 +3031,36 @@ class ProductSuiteTab(QWidget):
|
|||||||
icon=QMessageBox.Information,
|
icon=QMessageBox.Information,
|
||||||
)
|
)
|
||||||
self._status(
|
self._status(
|
||||||
"商品套图生成已停止:成功%d张,失败%d张,停止%d张"
|
(
|
||||||
|
"图片重试已停止:成功%d张,失败%d张,停止%d张"
|
||||||
|
if retrying
|
||||||
|
else "商品套图生成已停止:成功%d张,失败%d张,停止%d张"
|
||||||
|
)
|
||||||
% (success, failed, cancelled),
|
% (success, failed, cancelled),
|
||||||
"warning",
|
"warning",
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
if state is self._displayed_state:
|
if state is self._displayed_state:
|
||||||
|
if retrying:
|
||||||
|
self._message(
|
||||||
|
"图片重试成功" if success and not failed else "图片重试失败",
|
||||||
|
"本次重试:成功%d张,失败%d张,停止%d张;总用时%d秒。"
|
||||||
|
% (success, failed, cancelled, elapsed),
|
||||||
|
icon=QMessageBox.Information,
|
||||||
|
)
|
||||||
|
else:
|
||||||
self._message(
|
self._message(
|
||||||
"商品套图生成完成",
|
"商品套图生成完成",
|
||||||
"本轮共%d张:成功%d张,失败%d张,停止%d张;总用时%d秒。"
|
"本轮共%d张:成功%d张,失败%d张,停止%d张;总用时%d秒。"
|
||||||
% (total, success, failed, cancelled, elapsed),
|
% (total, success, failed, cancelled, elapsed),
|
||||||
icon=QMessageBox.Information,
|
icon=QMessageBox.Information,
|
||||||
)
|
)
|
||||||
|
if retrying:
|
||||||
|
self._status(
|
||||||
|
"图片重试成功" if success and not failed else "图片重试失败,请查看失败卡片",
|
||||||
|
"success" if success and not failed else "danger",
|
||||||
|
)
|
||||||
|
else:
|
||||||
self._status(
|
self._status(
|
||||||
"商品套图生成完成:成功%d张,失败%d张" % (success, failed),
|
"商品套图生成完成:成功%d张,失败%d张" % (success, failed),
|
||||||
"success",
|
"success",
|
||||||
@@ -3012,6 +3121,8 @@ class ProductSuiteTab(QWidget):
|
|||||||
self.generate_button.setText(
|
self.generate_button.setText(
|
||||||
"正在停止..."
|
"正在停止..."
|
||||||
if state.generation_stop_requested
|
if state.generation_stop_requested
|
||||||
|
else "停止重试"
|
||||||
|
if state.generation_mode == "retry"
|
||||||
else "停止生成"
|
else "停止生成"
|
||||||
)
|
)
|
||||||
self.generate_button.setStyleSheet(
|
self.generate_button.setStyleSheet(
|
||||||
@@ -3035,7 +3146,8 @@ class ProductSuiteTab(QWidget):
|
|||||||
return
|
return
|
||||||
elapsed = int(max(0, time.monotonic() - state.started_at)) if state.started_at else 0
|
elapsed = int(max(0, time.monotonic() - state.started_at)) if state.started_at else 0
|
||||||
self.elapsed_label.setText(
|
self.elapsed_label.setText(
|
||||||
"套图 %d/%d(%d秒) · 失败 %d" % (
|
"%s %d/%d(%d秒) · 失败 %d" % (
|
||||||
|
"重试" if state.generation_mode == "retry" else "套图",
|
||||||
state.done,
|
state.done,
|
||||||
state.total,
|
state.total,
|
||||||
elapsed,
|
elapsed,
|
||||||
@@ -3067,8 +3179,12 @@ class ProductSuiteTab(QWidget):
|
|||||||
return []
|
return []
|
||||||
if state.show_history:
|
if state.show_history:
|
||||||
return jobs
|
return jobs
|
||||||
current = set(state.current_job_ids)
|
by_id = {int(job.id): job for job in jobs}
|
||||||
return [job for job in jobs if int(job.id) in current]
|
return [
|
||||||
|
by_id[int(job_id)]
|
||||||
|
for job_id in state.current_job_ids
|
||||||
|
if int(job_id) in by_id
|
||||||
|
]
|
||||||
|
|
||||||
def _refresh_results(self, state):
|
def _refresh_results(self, state):
|
||||||
while self.result_grid.count():
|
while self.result_grid.count():
|
||||||
@@ -3122,12 +3238,19 @@ class ProductSuiteTab(QWidget):
|
|||||||
if state.generation_running():
|
if state.generation_running():
|
||||||
self._message("当前任务正在生成", "请等待当前生成结束或停止后再重试单张图片。")
|
self._message("当前任务正在生成", "请等待当前生成结束或停止后再重试单张图片。")
|
||||||
return
|
return
|
||||||
|
if str(getattr(job, "status", "") or "") not in {
|
||||||
|
"failed",
|
||||||
|
"expired",
|
||||||
|
"cancelled",
|
||||||
|
}:
|
||||||
|
self._status("当前图片无需重试", "warning")
|
||||||
|
return
|
||||||
spec = {
|
spec = {
|
||||||
"source_asset_id": job.source_asset_id,
|
"source_asset_id": job.source_asset_id,
|
||||||
"job_type": job.job_type,
|
"job_type": job.job_type,
|
||||||
"prompt": job.prompt,
|
"prompt": job.prompt,
|
||||||
}
|
}
|
||||||
self.start_generation(state, specs=[spec])
|
self.start_generation(state, specs=[spec], retry_job_id=job.id)
|
||||||
|
|
||||||
def _show_job_menu(self, job, global_position):
|
def _show_job_menu(self, job, global_position):
|
||||||
menu = QMenu(self)
|
menu = QMenu(self)
|
||||||
|
|||||||
+5
-2
@@ -3,7 +3,7 @@ id: T-640
|
|||||||
title: 商品套图单张失败重试保持当前轮结果
|
title: 商品套图单张失败重试保持当前轮结果
|
||||||
phase: 7
|
phase: 7
|
||||||
deps: [T-639]
|
deps: [T-639]
|
||||||
status: TODO
|
status: DONE
|
||||||
created: 2026-07-16
|
created: 2026-07-16
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -107,4 +107,7 @@ git diff --check
|
|||||||
|
|
||||||
## 执行记录
|
## 执行记录
|
||||||
|
|
||||||
- 待执行。
|
- 2026-07-16:`SuiteTaskState` 新增内存态 `generation_job_ids`、`generation_mode` 与 `generation_retry_job_id`。`generation_job_ids` 专门供当前活动请求的进度、停止、线程结束和终态看门狗使用;`current_job_ids` 只表示当前结果区的有效槽位,解决单张重试时旧成功 job 被错误纳入本次汇总的问题。未修改 SQLite schema。
|
||||||
|
- 2026-07-16:失败、过期或停止卡片的「重试」改为显式单张重试模式。worker 仍新建 job 保留历史和计费记录;新 job ID 返回后会原位替换当前结果区中的旧失败 job,其他成功图片保持不变。当前结果改按 `current_job_ids` 顺序展示;历史视图继续展示原失败与每次重试记录,从历史发起重试时不会强制退出历史视图。
|
||||||
|
- 2026-07-16:重试运行状态、停止确认、进度文案和完成弹窗改为「图片重试」语义;重试成功、失败、停止和未完整结束分别给中文反馈。正常整轮生成、停止、T-639 `run_token` 隔离与终态兜底保持原行为。
|
||||||
|
- 2026-07-16:补齐当前轮原位替换、历史记录保留、活动快照只统计新 job、重试再次失败仍可继续重试、历史视图保持和真实 QThread 完整退出测试。相关 42 项测试通过;在不包含用户未提交默认提示词改名的临时隔离 worktree 中,全量 535 项 unittest 通过,Ruff、`compileall` 与 `git diff --check` 均通过。
|
||||||
|
|||||||
@@ -856,6 +856,7 @@ class ProductSuiteGuiTests(TempDirMixin, unittest.TestCase):
|
|||||||
state.thread = mock.Mock()
|
state.thread = mock.Mock()
|
||||||
state.generation_run_token = "watchdog-run"
|
state.generation_run_token = "watchdog-run"
|
||||||
state.current_job_ids = [job.id for job in jobs]
|
state.current_job_ids = [job.id for job in jobs]
|
||||||
|
state.generation_job_ids = [job.id for job in jobs]
|
||||||
state.total = len(jobs)
|
state.total = len(jobs)
|
||||||
state.started_at = time.monotonic()
|
state.started_at = time.monotonic()
|
||||||
tab._generation_run_states["watchdog-run"] = state.key
|
tab._generation_run_states["watchdog-run"] = state.key
|
||||||
@@ -972,6 +973,7 @@ class ProductSuiteGuiTests(TempDirMixin, unittest.TestCase):
|
|||||||
state.thread = mock.Mock()
|
state.thread = mock.Mock()
|
||||||
state.generation_run_token = "thread-fallback"
|
state.generation_run_token = "thread-fallback"
|
||||||
state.current_job_ids = [job.id]
|
state.current_job_ids = [job.id]
|
||||||
|
state.generation_job_ids = [job.id]
|
||||||
state.total = 1
|
state.total = 1
|
||||||
state.started_at = time.monotonic()
|
state.started_at = time.monotonic()
|
||||||
tab._generation_run_states["thread-fallback"] = state.key
|
tab._generation_run_states["thread-fallback"] = state.key
|
||||||
@@ -1127,6 +1129,260 @@ class ProductSuiteGuiTests(TempDirMixin, unittest.TestCase):
|
|||||||
|
|
||||||
self.assert_removed(temp_dir)
|
self.assert_removed(temp_dir)
|
||||||
|
|
||||||
|
def test_failed_job_retry_replaces_current_slot_and_keeps_history(self):
|
||||||
|
with self.make_temp_dir() as temp_dir:
|
||||||
|
config = self._config(temp_dir)
|
||||||
|
project, sources = self._create_project_with_assets(temp_dir, config, 1)
|
||||||
|
source = sources[0]
|
||||||
|
|
||||||
|
success_path = os.path.join(temp_dir, "success.jpg")
|
||||||
|
retry_path = os.path.join(temp_dir, "retry.jpg")
|
||||||
|
self._write_image(success_path)
|
||||||
|
self._write_image(retry_path)
|
||||||
|
success_asset = image_studio.add_asset(
|
||||||
|
project.id,
|
||||||
|
"generated_main",
|
||||||
|
local_path=success_path,
|
||||||
|
parent_asset_id=source.id,
|
||||||
|
path=config["db_path"],
|
||||||
|
)
|
||||||
|
success_job = image_studio.create_job(
|
||||||
|
project.id,
|
||||||
|
source_asset_id=source.id,
|
||||||
|
job_type="白底图",
|
||||||
|
prompt="成功图",
|
||||||
|
path=config["db_path"],
|
||||||
|
)
|
||||||
|
success_job = image_studio.update_job_status(
|
||||||
|
success_job.id,
|
||||||
|
"succeeded",
|
||||||
|
output_asset_id=success_asset.id,
|
||||||
|
path=config["db_path"],
|
||||||
|
)
|
||||||
|
failed_job = image_studio.create_job(
|
||||||
|
project.id,
|
||||||
|
source_asset_id=source.id,
|
||||||
|
job_type="场景图",
|
||||||
|
prompt="失败图",
|
||||||
|
path=config["db_path"],
|
||||||
|
)
|
||||||
|
failed_job = image_studio.update_job_status(
|
||||||
|
failed_job.id,
|
||||||
|
"failed",
|
||||||
|
error="上游生成失败",
|
||||||
|
path=config["db_path"],
|
||||||
|
)
|
||||||
|
|
||||||
|
tab = ProductSuiteTab(config=config, db_path=config["db_path"])
|
||||||
|
self.addCleanup(tab.close)
|
||||||
|
state = tab._displayed_state
|
||||||
|
state.account_alias = "alias-a"
|
||||||
|
state.item_id = project.item_id
|
||||||
|
state.project_id = project.id
|
||||||
|
state.project_binding_state = project.binding_state
|
||||||
|
state.current_job_ids = [success_job.id, failed_job.id]
|
||||||
|
tab._load_state(state)
|
||||||
|
messages = []
|
||||||
|
tab._message = lambda title, message, **kwargs: messages.append(
|
||||||
|
(title, message)
|
||||||
|
)
|
||||||
|
|
||||||
|
def fake_run_jobs(jobs, **kwargs):
|
||||||
|
job = list(jobs)[0]
|
||||||
|
retry_asset = image_studio.add_asset(
|
||||||
|
project.id,
|
||||||
|
"generated_main",
|
||||||
|
local_path=retry_path,
|
||||||
|
parent_asset_id=source.id,
|
||||||
|
path=config["db_path"],
|
||||||
|
)
|
||||||
|
image_studio.update_job_status(
|
||||||
|
job.id,
|
||||||
|
"succeeded",
|
||||||
|
output_asset_id=retry_asset.id,
|
||||||
|
path=config["db_path"],
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"total": 1,
|
||||||
|
"success": 1,
|
||||||
|
"failed": 0,
|
||||||
|
"cancelled": 0,
|
||||||
|
"jobs": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
with mock.patch(
|
||||||
|
"app.gui.workers.image_studio_generation.run_jobs",
|
||||||
|
side_effect=fake_run_jobs,
|
||||||
|
):
|
||||||
|
tab.retry_job(failed_job)
|
||||||
|
generation_thread = state.thread
|
||||||
|
deadline = time.monotonic() + 3
|
||||||
|
while state.worker is not None and time.monotonic() < deadline:
|
||||||
|
QTest.qWait(20)
|
||||||
|
self.app.processEvents()
|
||||||
|
while (
|
||||||
|
generation_thread is not None
|
||||||
|
and generation_thread.isRunning()
|
||||||
|
and time.monotonic() < deadline
|
||||||
|
):
|
||||||
|
QTest.qWait(20)
|
||||||
|
self.app.processEvents()
|
||||||
|
self.assertFalse(generation_thread.isRunning())
|
||||||
|
|
||||||
|
all_jobs = image_studio.list_jobs(project.id, path=config["db_path"])
|
||||||
|
retry_jobs = [
|
||||||
|
job
|
||||||
|
for job in all_jobs
|
||||||
|
if job.id not in {success_job.id, failed_job.id}
|
||||||
|
]
|
||||||
|
self.assertEqual(1, len(retry_jobs))
|
||||||
|
retry_job = retry_jobs[0]
|
||||||
|
self.assertEqual(
|
||||||
|
[success_job.id, retry_job.id],
|
||||||
|
state.current_job_ids,
|
||||||
|
)
|
||||||
|
self.assertEqual([], state.generation_job_ids)
|
||||||
|
self.assertEqual(
|
||||||
|
[success_job.id, retry_job.id],
|
||||||
|
[job.id for job in tab._jobs_for_state(state)],
|
||||||
|
)
|
||||||
|
self.assertEqual("图片重试成功", messages[-1][0])
|
||||||
|
|
||||||
|
state.show_history = True
|
||||||
|
history_ids = {job.id for job in tab._jobs_for_state(state)}
|
||||||
|
self.assertEqual(
|
||||||
|
{success_job.id, failed_job.id, retry_job.id},
|
||||||
|
history_ids,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assert_removed(temp_dir)
|
||||||
|
|
||||||
|
def test_retry_tracks_only_new_job_and_preserves_history_view(self):
|
||||||
|
with self.make_temp_dir() as temp_dir:
|
||||||
|
config = self._config(temp_dir)
|
||||||
|
project, sources = self._create_project_with_assets(temp_dir, config, 1)
|
||||||
|
source = sources[0]
|
||||||
|
success_job = image_studio.create_job(
|
||||||
|
project.id,
|
||||||
|
source_asset_id=source.id,
|
||||||
|
job_type="白底图",
|
||||||
|
prompt="成功图",
|
||||||
|
path=config["db_path"],
|
||||||
|
)
|
||||||
|
success_job = image_studio.update_job_status(
|
||||||
|
success_job.id,
|
||||||
|
"succeeded",
|
||||||
|
path=config["db_path"],
|
||||||
|
)
|
||||||
|
failed_job = image_studio.create_job(
|
||||||
|
project.id,
|
||||||
|
source_asset_id=source.id,
|
||||||
|
job_type="场景图",
|
||||||
|
prompt="失败图",
|
||||||
|
path=config["db_path"],
|
||||||
|
)
|
||||||
|
failed_job = image_studio.update_job_status(
|
||||||
|
failed_job.id,
|
||||||
|
"failed",
|
||||||
|
path=config["db_path"],
|
||||||
|
)
|
||||||
|
retry_job = image_studio.create_job(
|
||||||
|
project.id,
|
||||||
|
source_asset_id=source.id,
|
||||||
|
job_type="场景图",
|
||||||
|
prompt="重试图",
|
||||||
|
path=config["db_path"],
|
||||||
|
)
|
||||||
|
|
||||||
|
tab = ProductSuiteTab(config=config, db_path=config["db_path"])
|
||||||
|
self.addCleanup(tab.close)
|
||||||
|
state = tab._displayed_state
|
||||||
|
state.account_alias = "alias-a"
|
||||||
|
state.item_id = project.item_id
|
||||||
|
state.project_id = project.id
|
||||||
|
state.project_binding_state = project.binding_state
|
||||||
|
state.current_job_ids = [success_job.id, failed_job.id]
|
||||||
|
state.generation_mode = "retry"
|
||||||
|
state.generation_retry_job_id = failed_job.id
|
||||||
|
state.total = 1
|
||||||
|
|
||||||
|
tab._set_generation_job_ids(state, [retry_job.id])
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
[success_job.id, retry_job.id],
|
||||||
|
state.current_job_ids,
|
||||||
|
)
|
||||||
|
self.assertEqual([retry_job.id], state.generation_job_ids)
|
||||||
|
snapshot = tab._generation_job_snapshot(state)
|
||||||
|
self.assertEqual(1, snapshot["job_ids"])
|
||||||
|
self.assertEqual(1, snapshot["active"])
|
||||||
|
|
||||||
|
image_studio.update_job_status(
|
||||||
|
retry_job.id,
|
||||||
|
"failed",
|
||||||
|
error="重试仍失败",
|
||||||
|
path=config["db_path"],
|
||||||
|
)
|
||||||
|
state.worker = mock.Mock()
|
||||||
|
state.thread = mock.Mock()
|
||||||
|
state.generation_run_token = "retry-failed"
|
||||||
|
state.started_at = time.monotonic()
|
||||||
|
tab._generation_run_states["retry-failed"] = state.key
|
||||||
|
messages = []
|
||||||
|
tab._message = lambda title, message, **kwargs: messages.append(
|
||||||
|
(title, message)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(
|
||||||
|
tab._finalize_generation(
|
||||||
|
state,
|
||||||
|
"retry-failed",
|
||||||
|
{"total": 1, "success": 0, "failed": 1},
|
||||||
|
source="worker",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual("图片重试失败", messages[-1][0])
|
||||||
|
retry_cards = [
|
||||||
|
card
|
||||||
|
for card in tab.findChildren(SuiteResultCard)
|
||||||
|
if card.job.id == retry_job.id
|
||||||
|
]
|
||||||
|
self.assertEqual(1, len(retry_cards))
|
||||||
|
self.assertTrue(
|
||||||
|
any(
|
||||||
|
button.text() == "重试"
|
||||||
|
for button in retry_cards[0].findChildren(QPushButton)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
state.show_history = True
|
||||||
|
tab._load_state(state)
|
||||||
|
with mock.patch.object(
|
||||||
|
tab,
|
||||||
|
"_start_thread",
|
||||||
|
return_value=mock.Mock(),
|
||||||
|
):
|
||||||
|
self.assertTrue(
|
||||||
|
tab.start_generation(
|
||||||
|
state,
|
||||||
|
specs=[
|
||||||
|
{
|
||||||
|
"source_asset_id": source.id,
|
||||||
|
"job_type": failed_job.job_type,
|
||||||
|
"prompt": failed_job.prompt,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
retry_job_id=failed_job.id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertTrue(state.show_history)
|
||||||
|
self.assertTrue(tab.history_button.isChecked())
|
||||||
|
state.worker = None
|
||||||
|
state.thread = None
|
||||||
|
state.generation_run_token = ""
|
||||||
|
|
||||||
|
self.assert_removed(temp_dir)
|
||||||
|
|
||||||
def test_original_list_expands_without_internal_scrollbars(self):
|
def test_original_list_expands_without_internal_scrollbars(self):
|
||||||
with self.make_temp_dir() as temp_dir:
|
with self.make_temp_dir() as temp_dir:
|
||||||
config = self._config(temp_dir)
|
config = self._config(temp_dir)
|
||||||
|
|||||||
Reference in New Issue
Block a user