feat: complete T-528 cmhub billing feedback
This commit is contained in:
+213
-1
@@ -12,7 +12,7 @@ sys.path.insert(0, os.path.dirname(__file__))
|
||||
from _helpers import TempDirMixin
|
||||
|
||||
from app import gui
|
||||
from app import accounts, appconfig, db, prompts
|
||||
from app import accounts, ai, appconfig, db, prompts
|
||||
|
||||
if gui.QT_IMPORT_ERROR is not None:
|
||||
raise unittest.SkipTest("PySide6 未安装")
|
||||
@@ -962,6 +962,9 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
self.assertEqual("标题 0/0", tab.title_progress_label.text())
|
||||
self.assertEqual("图片 0/0", tab.cover_progress_label.text())
|
||||
self.assertEqual("失败 0", tab.failed_progress_label.text())
|
||||
self.assertEqual("generateCmhubBalanceLabel", tab.cmhub_balance_label.objectName())
|
||||
self.assertEqual("cmhub余额:未获取", tab.cmhub_balance_label.text())
|
||||
self.assertTrue(tab.cmhub_balance_label.isHidden())
|
||||
self.assertEqual(0, tab.title_progress_bar.value())
|
||||
self.assertEqual(0, tab.cover_progress_bar.value())
|
||||
self.assertEqual(1, tab.title_progress_bar.maximum())
|
||||
@@ -981,6 +984,52 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_generate_tab_shows_cmhub_balance_and_billing_error(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg = self.make_config(temp_dir)
|
||||
cfg["ai"] = appconfig.ai_config(cfg)
|
||||
cfg["ai"]["backend"] = "cmhub"
|
||||
statuses = []
|
||||
tab = GenerateTab(config=cfg, status_callback=statuses.append)
|
||||
self.addCleanup(tab.close)
|
||||
|
||||
self.assertFalse(tab.cmhub_balance_label.isHidden())
|
||||
self.assertEqual("cmhub余额:未获取", tab.cmhub_balance_label.text())
|
||||
|
||||
tab._on_generate_progress(
|
||||
{
|
||||
"total": 1,
|
||||
"title_done": 1,
|
||||
"cover_done": 0,
|
||||
"cover_total": 0,
|
||||
"failed": 0,
|
||||
"generate_cover": False,
|
||||
"points_balance": 88,
|
||||
}
|
||||
)
|
||||
self.assertEqual("cmhub余额:88", tab.cmhub_balance_label.text())
|
||||
|
||||
with mock.patch("app.gui.QMessageBox.warning") as warning:
|
||||
tab._on_generate_finished(
|
||||
{
|
||||
"total": 1,
|
||||
"title_done": 0,
|
||||
"cover_done": 0,
|
||||
"cover_total": 0,
|
||||
"failed": 1,
|
||||
"billing_error": {
|
||||
"code": "insufficient_points",
|
||||
"message": "点数不足,请先充值。本轮未开始任务将停止。",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
warning.assert_called_once()
|
||||
self.assertIn("点数不足,请先充值", warning.call_args[0][2])
|
||||
self.assertIn("AI 生成已中止", statuses[-1])
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_generate_tab_persists_generate_cover_toggle(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg = self.make_config(temp_dir)
|
||||
@@ -1262,6 +1311,169 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_generate_worker_records_cmhub_billing_metadata(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg = self.make_config(temp_dir)
|
||||
cfg["ai"] = appconfig.ai_config(cfg)
|
||||
cfg["ai"]["backend"] = "cmhub"
|
||||
cfg["ai"]["generate_cover"] = True
|
||||
accounts.create_account("主店", "alias-a", debug_port=9222, config=cfg)
|
||||
batch_id = db.create_batch(["input.xlsx"], path=cfg["db_path"])
|
||||
db.insert_tasks(
|
||||
batch_id,
|
||||
[
|
||||
{
|
||||
"source_file_abs": os.path.join(temp_dir, "input.xlsx"),
|
||||
"source_sheet": "商品",
|
||||
"source_row": 2,
|
||||
"account_name": "Excel主店",
|
||||
"alias": "alias-a",
|
||||
"item_id": "51100639510",
|
||||
}
|
||||
],
|
||||
path=cfg["db_path"],
|
||||
)
|
||||
task = db.list_tasks(batch_id=batch_id, path=cfg["db_path"])[0]
|
||||
db.set_collected(task.id, "旧标题", "old.jpg", path=cfg["db_path"])
|
||||
tasks = db.list_tasks(batch_id=batch_id, path=cfg["db_path"])
|
||||
progress = []
|
||||
logs = []
|
||||
|
||||
def fake_generate_batch(tasks_arg, prompt_values, ai_cfg=None, on_progress=None, should_stop=None):
|
||||
on_progress({"total": 1, "title_done": 1, "cover_done": 0, "failed": 0})
|
||||
ai_cfg["on_event"](
|
||||
{
|
||||
"task": tasks_arg[0],
|
||||
"phase": "title",
|
||||
"step": "title_request",
|
||||
"result": "meta",
|
||||
"metadata": {
|
||||
"alias": "title-standard",
|
||||
"points_cost": 1,
|
||||
"points_balance": 88,
|
||||
"call_id": "call-1",
|
||||
},
|
||||
}
|
||||
)
|
||||
return {
|
||||
"ok": True,
|
||||
"total": 1,
|
||||
"title_done": 1,
|
||||
"cover_done": 1,
|
||||
"cover_total": 1,
|
||||
"generated_done": 1,
|
||||
"failed": 0,
|
||||
"generate_cover": True,
|
||||
}
|
||||
|
||||
worker = GenerateWorker(
|
||||
tasks,
|
||||
{"title": "标题提示", "cover": "封面提示"},
|
||||
db_path=cfg["db_path"],
|
||||
config=cfg,
|
||||
)
|
||||
worker.progress.connect(progress.append)
|
||||
worker.log.connect(logs.append)
|
||||
|
||||
with mock.patch("app.gui.ai.generate_batch", side_effect=fake_generate_batch):
|
||||
summary = worker.execute()
|
||||
|
||||
self.assertEqual(88, summary["points_balance"])
|
||||
self.assertEqual(88, progress[-1]["points_balance"])
|
||||
joined_logs = "\n".join(logs)
|
||||
self.assertIn("[计费] 商品 51100639510", joined_logs)
|
||||
self.assertIn("别名 title-standard", joined_logs)
|
||||
self.assertIn("扣点 1", joined_logs)
|
||||
self.assertIn("余额 88", joined_logs)
|
||||
self.assertIn("call_id=call-1", joined_logs)
|
||||
run_log = db.list_run_logs(limit=1, run_type="generate", path=cfg["db_path"])[0]
|
||||
self.assertEqual("cmhub", run_log.options["backend"])
|
||||
events = db.list_run_log_events(summary["run_id"], path=cfg["db_path"])
|
||||
messages = "\n".join(event.message for event in events)
|
||||
self.assertIn("[计费] 商品 51100639510", messages)
|
||||
self.assertIn("余额 88", messages)
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_generate_worker_stops_on_cmhub_insufficient_points(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg = self.make_config(temp_dir)
|
||||
cfg["ai"] = appconfig.ai_config(cfg)
|
||||
cfg["ai"]["backend"] = "cmhub"
|
||||
accounts.create_account("主店", "alias-a", debug_port=9222, config=cfg)
|
||||
batch_id = db.create_batch(["input.xlsx"], path=cfg["db_path"])
|
||||
db.insert_tasks(
|
||||
batch_id,
|
||||
[
|
||||
{
|
||||
"source_file_abs": os.path.join(temp_dir, "input.xlsx"),
|
||||
"source_sheet": "商品",
|
||||
"source_row": 2,
|
||||
"account_name": "Excel主店",
|
||||
"alias": "alias-a",
|
||||
"item_id": "51100639510",
|
||||
}
|
||||
],
|
||||
path=cfg["db_path"],
|
||||
)
|
||||
task = db.list_tasks(batch_id=batch_id, path=cfg["db_path"])[0]
|
||||
db.set_collected(task.id, "旧标题", "old.jpg", path=cfg["db_path"])
|
||||
tasks = db.list_tasks(batch_id=batch_id, path=cfg["db_path"])
|
||||
progress = []
|
||||
logs = []
|
||||
|
||||
def fake_generate_batch(tasks_arg, prompt_values, ai_cfg=None, on_progress=None, should_stop=None):
|
||||
exc = ai.CMHubError("insufficient_points", "点数不足,请先充值", status=402)
|
||||
ai_cfg["on_error"](
|
||||
{
|
||||
"task": tasks_arg[0],
|
||||
"phase": "title",
|
||||
"step": "title_request",
|
||||
"error": str(exc),
|
||||
"exception": exc,
|
||||
"code": exc.code,
|
||||
"status": exc.status,
|
||||
}
|
||||
)
|
||||
self.assertTrue(should_stop())
|
||||
return {
|
||||
"ok": False,
|
||||
"total": 1,
|
||||
"title_done": 0,
|
||||
"cover_done": 0,
|
||||
"cover_total": 0,
|
||||
"generated_done": 0,
|
||||
"failed": 1,
|
||||
"cancelled": should_stop(),
|
||||
"generate_cover": False,
|
||||
}
|
||||
|
||||
worker = GenerateWorker(
|
||||
tasks,
|
||||
{"title": "标题提示", "cover": "封面提示"},
|
||||
db_path=cfg["db_path"],
|
||||
config=cfg,
|
||||
)
|
||||
worker.progress.connect(progress.append)
|
||||
worker.log.connect(logs.append)
|
||||
|
||||
with mock.patch("app.gui.ai.generate_batch", side_effect=fake_generate_batch):
|
||||
summary = worker.execute()
|
||||
|
||||
self.assertFalse(summary["ok"])
|
||||
self.assertTrue(summary["cancelled"])
|
||||
self.assertEqual("insufficient_points", summary["billing_error"]["code"])
|
||||
self.assertIn("点数不足,请先充值", summary["billing_error"]["message"])
|
||||
self.assertEqual("insufficient_points", progress[-1]["billing_error"]["code"])
|
||||
joined_logs = "\n".join(logs)
|
||||
self.assertIn("[计费] 商品 51100639510", joined_logs)
|
||||
self.assertIn("点数不足,请先充值", joined_logs)
|
||||
run_log = db.list_run_logs(limit=1, run_type="generate", path=cfg["db_path"])[0]
|
||||
self.assertEqual("failed", run_log.status)
|
||||
self.assertEqual("insufficient_points", run_log.summary["billing_error"]["code"])
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_generate_worker_writes_run_log_and_diagnostic_log_on_image_failure(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg = self.make_config(temp_dir)
|
||||
|
||||
Reference in New Issue
Block a user