fix: 修复重新上报任务状态不一致 (#91)
This commit is contained in:
@@ -15,6 +15,7 @@ from PyQt5.QtWidgets import QApplication
|
||||
from qfluentwidgets import InfoBarPosition
|
||||
|
||||
from src.android_device_service import AndroidDeviceSearchError
|
||||
from src.db import open_database
|
||||
from src.pdd_ui import PDDTaskPage
|
||||
from src.admin_gateway import AdminGatewayError, AdminTask, SubmissionReceipt
|
||||
from src.pdd_ui_event import (
|
||||
@@ -32,7 +33,13 @@ from src.pdd_purchase_adapter import PddPurchaseAdapter, PurchasePageState
|
||||
from src.pdd_collect_service import PddCollectError
|
||||
from src.pdd_u2_purchase_adapter import create_u2_purchase_adapter
|
||||
from src.settings_repository import SettingsRepository
|
||||
from src.task_models import NewClaimedTask, TaskStatus, TaskSummary, TaskType
|
||||
from src.task_models import (
|
||||
NewClaimedTask,
|
||||
OutboxEventType,
|
||||
TaskStatus,
|
||||
TaskSummary,
|
||||
TaskType,
|
||||
)
|
||||
from src.task_repository import TaskRepository
|
||||
from src.ui_main import MainWindow
|
||||
|
||||
@@ -136,6 +143,7 @@ class RecordingResubmitGateway(RecordingClaimGateway):
|
||||
self.failures = dict(failures or {})
|
||||
self.delay = delay
|
||||
self.submit_calls = []
|
||||
self.failure_calls = []
|
||||
|
||||
def submit_result(self, task_id, idempotency_key, result):
|
||||
self.submit_calls.append((task_id, idempotency_key, result))
|
||||
@@ -146,6 +154,15 @@ class RecordingResubmitGateway(RecordingClaimGateway):
|
||||
raise error
|
||||
return SubmissionReceipt(True, f"RESULT-{task_id}", "2026-08-10T08:00:00Z")
|
||||
|
||||
def submit_failure(self, task_id, idempotency_key, failure):
|
||||
self.failure_calls.append((task_id, idempotency_key, failure))
|
||||
if self.delay:
|
||||
time.sleep(self.delay)
|
||||
error = self.failures.get(task_id)
|
||||
if error is not None:
|
||||
raise error
|
||||
return SubmissionReceipt(True, f"FAILURE-{task_id}", "2026-08-10T08:00:00Z")
|
||||
|
||||
|
||||
class FakeCollectResult:
|
||||
def to_pdd_data(self):
|
||||
@@ -457,6 +474,64 @@ class PDDTaskPageEventTest(unittest.TestCase):
|
||||
events.shutdown()
|
||||
page.deleteLater()
|
||||
|
||||
def test_resubmit_prioritizes_pending_failure_and_repairs_task_status(self):
|
||||
self._add_task(1)
|
||||
first = self.repository.start_collect_run("PDD-001", "USB-001")
|
||||
result = self.repository.save_collect_result(
|
||||
"PDD-001", first.attempt_id, FakeCollectResult().to_pdd_data()
|
||||
)
|
||||
self.repository.mark_outbox_sent(result.id)
|
||||
self.repository.prepare_collect_rerun("PDD-001")
|
||||
second = self.repository.start_collect_run("PDD-001", "USB-001")
|
||||
failure = self.repository.save_collect_failure(
|
||||
"PDD-001",
|
||||
second.attempt_id,
|
||||
TaskStatus.FAILED,
|
||||
"SKU_PANEL_NOT_FOUND",
|
||||
"规格面板加载超时",
|
||||
False,
|
||||
)
|
||||
connection = open_database(self.db_path)
|
||||
try:
|
||||
with connection:
|
||||
connection.execute(
|
||||
"UPDATE pdd_tasks SET status = 'succeeded',"
|
||||
" current_step = 'completed' WHERE remote_task_id = 'PDD-001'"
|
||||
)
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
page = PDDTaskPage()
|
||||
gateway = RecordingResubmitGateway()
|
||||
events = PDDTaskPageEvent(page, self.repository, claim_gateway=gateway)
|
||||
events.load_initial_tasks()
|
||||
page.taskModel.setData(
|
||||
page.taskModel.index(0, 0), Qt.Checked, Qt.CheckStateRole
|
||||
)
|
||||
|
||||
with patch("src.pdd_ui_event.MessageBox") as message_box:
|
||||
message_box.return_value.exec.return_value = True
|
||||
page.resubmitButton.click()
|
||||
self.assertTrue(wait_until(self.app, lambda: not events._resubmit_busy))
|
||||
|
||||
self.assertEqual(gateway.submit_calls, [])
|
||||
self.assertEqual(len(gateway.failure_calls), 1)
|
||||
task_id, idempotency_key, payload = gateway.failure_calls[0]
|
||||
self.assertEqual(task_id, "PDD-001")
|
||||
self.assertEqual(idempotency_key, failure.idempotency_key)
|
||||
self.assertEqual(payload, failure.payload_json)
|
||||
self.assertEqual(payload["status"], TaskStatus.FAILED.value)
|
||||
self.assertEqual(
|
||||
self.repository.get_outbox_event(failure.id).event_type,
|
||||
OutboxEventType.TASK_FAILURE,
|
||||
)
|
||||
repaired = self.repository.get_task("PDD-001")
|
||||
self.assertEqual(repaired.status, TaskStatus.FAILED)
|
||||
self.assertEqual(repaired.last_error_code, "SKU_PANEL_NOT_FOUND")
|
||||
self.device_checker.assert_not_called()
|
||||
events.shutdown()
|
||||
page.deleteLater()
|
||||
|
||||
def test_resubmit_confirmation_cancel_does_not_submit(self):
|
||||
self._add_task(1)
|
||||
page = PDDTaskPage()
|
||||
|
||||
@@ -234,7 +234,96 @@ class TaskRepositoryTests(unittest.TestCase):
|
||||
self.assertEqual(latest.id, result.id)
|
||||
self.assertEqual(latest.event_type, OutboxEventType.COLLECT_RESULT)
|
||||
self.assertNotEqual(latest.id, failure.id)
|
||||
self.assertEqual(
|
||||
self.repository.outbox_for_resubmit("TASK-REPORT").id,
|
||||
failure.id,
|
||||
)
|
||||
self.repository.mark_outbox_sent(failure.id)
|
||||
self.assertEqual(
|
||||
self.repository.outbox_for_resubmit("TASK-REPORT").id,
|
||||
result.id,
|
||||
)
|
||||
self.assertIsNone(self.repository.latest_result_outbox("MISSING"))
|
||||
self.assertIsNone(self.repository.outbox_for_resubmit("MISSING"))
|
||||
|
||||
def test_replaying_old_result_does_not_hide_latest_failure(self):
|
||||
self.repository.add_claimed_task(self._task("TASK-STATE"))
|
||||
first = self.repository.start_collect_run("TASK-STATE", "USB-001")
|
||||
result = self.repository.save_collect_result(
|
||||
"TASK-STATE",
|
||||
first.attempt_id,
|
||||
{"goods_id": "10001", "title": "旧结果", "skus": []},
|
||||
)
|
||||
self.repository.mark_outbox_sent(result.id)
|
||||
self.repository.prepare_collect_rerun("TASK-STATE")
|
||||
second = self.repository.start_collect_run("TASK-STATE", "USB-001")
|
||||
failure = self.repository.save_collect_failure(
|
||||
"TASK-STATE",
|
||||
second.attempt_id,
|
||||
TaskStatus.FAILED,
|
||||
"SKU_PANEL_NOT_FOUND",
|
||||
"规格面板加载超时",
|
||||
False,
|
||||
)
|
||||
|
||||
self.repository.mark_outbox_sending(result.id)
|
||||
self.repository.mark_outbox_sent(result.id)
|
||||
|
||||
detail = self.repository.get_task("TASK-STATE")
|
||||
self.assertEqual(detail.status, TaskStatus.FAILED)
|
||||
self.assertEqual(detail.last_error_code, "SKU_PANEL_NOT_FOUND")
|
||||
|
||||
# 模拟旧版本已经把主状态错误改成“已完成”。发送最新失败信息后
|
||||
# 应当依据最新执行记录恢复,而不是继续保留假完成状态。
|
||||
connection = open_database(self.db_path)
|
||||
try:
|
||||
with connection:
|
||||
connection.execute(
|
||||
"UPDATE pdd_tasks SET status = 'succeeded',"
|
||||
" current_step = 'completed' WHERE remote_task_id = ?",
|
||||
("TASK-STATE",),
|
||||
)
|
||||
finally:
|
||||
connection.close()
|
||||
self.repository.mark_outbox_sending(failure.id)
|
||||
self.repository.mark_outbox_sent(failure.id)
|
||||
|
||||
repaired = self.repository.get_task("TASK-STATE")
|
||||
self.assertEqual(repaired.status, TaskStatus.FAILED)
|
||||
self.assertEqual(repaired.current_step, "failed")
|
||||
self.assertEqual(repaired.last_error_message, "规格面板加载超时")
|
||||
|
||||
def test_replaying_old_failure_does_not_override_new_success(self):
|
||||
self.repository.add_claimed_task(self._task("TASK-NEW-SUCCESS"))
|
||||
first = self.repository.start_collect_run(
|
||||
"TASK-NEW-SUCCESS", "USB-001"
|
||||
)
|
||||
old_failure = self.repository.save_collect_failure(
|
||||
"TASK-NEW-SUCCESS",
|
||||
first.attempt_id,
|
||||
TaskStatus.FAILED,
|
||||
"OLD_FAILURE",
|
||||
"旧执行失败",
|
||||
False,
|
||||
)
|
||||
self.repository.mark_outbox_sent(old_failure.id)
|
||||
self.repository.prepare_collect_rerun("TASK-NEW-SUCCESS")
|
||||
second = self.repository.start_collect_run(
|
||||
"TASK-NEW-SUCCESS", "USB-001"
|
||||
)
|
||||
new_result = self.repository.save_collect_result(
|
||||
"TASK-NEW-SUCCESS",
|
||||
second.attempt_id,
|
||||
{"goods_id": "10001", "title": "新结果", "skus": []},
|
||||
)
|
||||
self.repository.mark_outbox_sent(new_result.id)
|
||||
|
||||
self.repository.mark_outbox_sending(old_failure.id)
|
||||
self.repository.mark_outbox_sent(old_failure.id)
|
||||
|
||||
detail = self.repository.get_task("TASK-NEW-SUCCESS")
|
||||
self.assertEqual(detail.status, TaskStatus.SUCCEEDED)
|
||||
self.assertEqual(detail.current_step, "completed")
|
||||
|
||||
def test_prepare_rerun_preserves_old_result_and_creates_new_attempt(self):
|
||||
self.repository.add_claimed_task(self._task("TASK-RERUN"))
|
||||
@@ -306,7 +395,7 @@ class TaskRepositoryTests(unittest.TestCase):
|
||||
True,
|
||||
)
|
||||
|
||||
with self.assertRaisesRegex(CollectRerunError, "未发送"):
|
||||
with self.assertRaisesRegex(CollectRerunError, "未上报的失败信息"):
|
||||
self.repository.validate_collect_rerun("RETRY-RERUN")
|
||||
|
||||
self.repository.mark_outbox_sent(event.id)
|
||||
@@ -332,7 +421,7 @@ class TaskRepositoryTests(unittest.TestCase):
|
||||
self.repository.mark_outbox_failed(event.id, "任务不存在")
|
||||
|
||||
with self.assertRaisesRegex(
|
||||
CollectRerunError, "上次结果提交失败:任务不存在"
|
||||
CollectRerunError, "上次失败信息提交失败:任务不存在"
|
||||
):
|
||||
self.repository.validate_collect_rerun("RETRY-REJECTED")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user