feat(product-suite): persist context and confirm actions

This commit is contained in:
chengma
2026-07-16 19:12:21 +08:00
parent 717ed2e35f
commit c9afaad24b
8 changed files with 459 additions and 36 deletions
+18
View File
@@ -41,6 +41,10 @@ class AppConfigTests(TempDirMixin, unittest.TestCase):
},
appconfig.product_suite_last_settings(config),
)
self.assertEqual(
"",
appconfig.product_suite_last_account_alias(config),
)
updated = appconfig.update_config(
{"ai": {"resolution": "2k"}},
@@ -84,6 +88,7 @@ class AppConfigTests(TempDirMixin, unittest.TestCase):
"chrome_path": "custom-chrome.exe",
"custom_section": {"keep": True},
"product_suite": {
"last_account_alias": ["invalid"],
"last_settings": {
"platform": "未知平台",
"country": "新加坡",
@@ -108,10 +113,15 @@ class AppConfigTests(TempDirMixin, unittest.TestCase):
},
appconfig.product_suite_last_settings(loaded),
)
self.assertEqual(
"",
appconfig.product_suite_last_account_alias(loaded),
)
updated = appconfig.update_config(
{
"product_suite": {
"last_account_alias": " alias-b ",
"last_settings": {
"platform": "Amazon",
"country": "中国台湾",
@@ -125,6 +135,10 @@ class AppConfigTests(TempDirMixin, unittest.TestCase):
self.assertEqual("custom-chrome.exe", updated["chrome_path"])
self.assertEqual({"keep": True}, updated["custom_section"])
self.assertEqual(
"alias-b",
appconfig.product_suite_last_account_alias(updated),
)
self.assertEqual("16:9", appconfig.product_suite_last_settings(updated)["ratio"])
with open(config_path, "r", encoding="utf-8") as fh:
persisted = json.load(fh)
@@ -132,6 +146,10 @@ class AppConfigTests(TempDirMixin, unittest.TestCase):
{"platform", "country", "language", "ratio"},
set(persisted["product_suite"]["last_settings"]),
)
self.assertEqual(
"alias-b",
persisted["product_suite"]["last_account_alias"],
)
self.assert_removed(temp_dir)
+11
View File
@@ -237,6 +237,17 @@ class ImageStudioTests(TempDirMixin, unittest.TestCase):
self.assertTrue(draft.item_id.startswith(image_studio.TEMPORARY_ITEM_PREFIX))
self.assertEqual(draft.item_id, draft.storage_key)
self.assertEqual(image_studio.PROJECT_BINDING_DRAFT, draft.binding_state)
self.assertTrue(image_studio.project_has_content(draft.id, path=db_path))
self.assertEqual(
[draft.id],
[
project.id
for project in image_studio.list_recoverable_draft_projects(
path=db_path
)
],
)
image_studio.update_project_prompt(draft.id, "", path=db_path)
self.assertFalse(image_studio.project_has_content(draft.id, path=db_path))
self.assertEqual([], image_studio.list_recoverable_draft_projects(path=db_path))
before_dirs = image_studio.project_image_dirs(os.path.join(temp_dir, "images"), draft)
+252 -11
View File
@@ -312,7 +312,11 @@ class ProductSuiteGuiTests(TempDirMixin, unittest.TestCase):
self.addCleanup(dialog.close)
expected = dialog.preview_edit.toPlainText()
with mock.patch.object(tab, "_start_thread", return_value=object()):
with mock.patch.object(
tab,
"_confirm",
return_value=True,
), mock.patch.object(tab, "_start_thread", return_value=object()):
self.assertTrue(tab.start_generation(state))
self.assertEqual(expected, state.worker.job_specs[0]["prompt"])
state.worker = None
@@ -532,6 +536,61 @@ class ProductSuiteGuiTests(TempDirMixin, unittest.TestCase):
self.assert_removed(temp_dir)
def test_recent_account_restores_and_missing_account_falls_back(self):
with self.make_temp_dir() as temp_dir:
config_path = os.path.join(temp_dir, "config.json")
config = appconfig.save_config(self._config(temp_dir), path=config_path)
accounts.create_account("主店", "alias-a", debug_port=9222, config=config)
accounts.create_account("副店", "alias-b", debug_port=9223, config=config)
first = ProductSuiteTab(
config=config,
config_path=config_path,
db_path=config["db_path"],
)
first.account_combo.setCurrentIndex(
first.account_combo.findData("alias-b")
)
self.app.processEvents()
self.assertEqual(
"alias-b",
appconfig.product_suite_last_account_alias(
appconfig.load_config(config_path)
),
)
first.close()
first.deleteLater()
self.app.processEvents()
second_config = appconfig.load_config(config_path)
second = ProductSuiteTab(
config=second_config,
config_path=config_path,
db_path=second_config["db_path"],
)
self.assertEqual("alias-b", second.account_combo.currentData())
second.close()
second.deleteLater()
self.app.processEvents()
accounts.delete_account("alias-b", config=second_config)
third_config = appconfig.load_config(config_path)
third = ProductSuiteTab(
config=third_config,
config_path=config_path,
db_path=third_config["db_path"],
)
self.addCleanup(third.close)
self.assertEqual("alias-a", third.account_combo.currentData())
self.assertEqual(
"alias-a",
appconfig.product_suite_last_account_alias(
appconfig.load_config(config_path)
),
)
self.assert_removed(temp_dir)
def test_task_tabs_keep_independent_prompt_and_settings(self):
with self.make_temp_dir() as temp_dir:
config = self._config(temp_dir)
@@ -681,24 +740,62 @@ class ProductSuiteGuiTests(TempDirMixin, unittest.TestCase):
self.assert_removed(temp_dir)
def test_prompt_without_project_stays_in_memory_until_draft_creation(self):
def test_prompt_without_project_creates_and_restores_draft(self):
with self.make_temp_dir() as temp_dir:
config = self._config(temp_dir)
accounts.create_account("主店", "alias-a", debug_port=9222, config=config)
tab = ProductSuiteTab(config=config, db_path=config["db_path"])
self.addCleanup(tab.close)
state = tab._displayed_state
tab.prompt_edit.setPlainText("尚未建立项目的卖点")
QTest.qWait(650)
self.app.processEvents()
self.assertEqual([], image_studio.list_projects(path=config["db_path"]))
draft = tab._create_draft_project(state)
projects = image_studio.list_projects(path=config["db_path"])
self.assertEqual(1, len(projects))
draft = projects[0]
self.assertIsNotNone(draft)
stored = image_studio.get_project(draft.id, path=config["db_path"])
self.assertEqual("尚未建立项目的卖点", stored.draft_prompt)
self.assertEqual("尚未建立项目的卖点", state.last_saved_prompt)
self.assertEqual(
[draft.id],
[
project.id
for project in image_studio.list_recoverable_draft_projects(
path=config["db_path"]
)
],
)
tab.close()
tab.deleteLater()
self.app.processEvents()
restored = ProductSuiteTab(config=config, db_path=config["db_path"])
self.addCleanup(restored.close)
restored_state = next(
value
for value in restored._states.values()
if value.project_id == draft.id
)
self.assertEqual("尚未建立项目的卖点", restored_state.prompt)
new_state = restored.add_task(inherit=False)
self.assertEqual("", new_state.prompt)
self.assert_removed(temp_dir)
def test_blank_prompt_without_project_does_not_create_draft(self):
with self.make_temp_dir() as temp_dir:
config = self._config(temp_dir)
accounts.create_account("主店", "alias-a", debug_port=9222, config=config)
tab = ProductSuiteTab(config=config, db_path=config["db_path"])
self.addCleanup(tab.close)
tab.prompt_edit.setPlainText(" ")
QTest.qWait(650)
self.app.processEvents()
self.assertEqual([], image_studio.list_projects(path=config["db_path"]))
self.assert_removed(temp_dir)
@@ -1220,14 +1317,18 @@ class ProductSuiteGuiTests(TempDirMixin, unittest.TestCase):
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
):
while generation_thread is not None and time.monotonic() < deadline:
try:
running = generation_thread.isRunning()
except RuntimeError:
generation_thread = None
break
if not running:
break
QTest.qWait(20)
self.app.processEvents()
self.assertFalse(generation_thread.isRunning())
if generation_thread is not None:
self.assertFalse(generation_thread.isRunning())
all_jobs = image_studio.list_jobs(project.id, path=config["db_path"])
retry_jobs = [
@@ -1710,6 +1811,146 @@ class ProductSuiteGuiTests(TempDirMixin, unittest.TestCase):
self.assert_removed(temp_dir)
def test_pull_confirmation_always_shows_account_item_and_existing_count(self):
with self.make_temp_dir() as temp_dir:
config = self._config(temp_dir)
account = accounts.create_account(
"主店",
"alias-a",
debug_port=9222,
config=config,
)
tab = ProductSuiteTab(config=config, db_path=config["db_path"])
self.addCleanup(tab.close)
state = tab._displayed_state
tab.item_id_edit.setText("51100639510")
confirmations = []
tab._confirm = lambda title, message, **kwargs: confirmations.append(
(title, message, kwargs)
) and False
tab.pull_main_images()
self.assertIsNone(state.pull_worker)
self.assertEqual("确认拉取蝦皮主图", confirmations[0][0])
self.assertIn("主店(alias-a)", confirmations[0][1])
self.assertIn("商品ID:51100639510", confirmations[0][1])
self.assertIn("当前可用商品原图:0张", confirmations[0][1])
self.assertIn("不会修改蝦皮线上商品", confirmations[0][1])
self.assertEqual("确认拉取", confirmations[0][2]["confirm_text"])
project = image_studio.create_or_get_project(
account,
item_id="51100639510",
path=config["db_path"],
)
source_path = os.path.join(temp_dir, "existing.png")
self._write_image(source_path)
image_studio.add_asset(
project.id,
image_studio.ASSET_KIND_ORIGINAL,
local_path=source_path,
path=config["db_path"],
)
state.project_id = project.id
state.project_binding_state = project.binding_state
state.item_id = project.item_id
tab.item_id_edit.setText(project.item_id)
tab._refresh_originals(state)
tab.pull_main_images()
self.assertIsNone(state.pull_worker)
self.assertIn("当前可用商品原图:1张", confirmations[1][1])
self.assertIn("本地手动添加图片会保留", confirmations[1][1])
self.assert_removed(temp_dir)
def test_generation_confirmation_prevents_job_creation_and_retry_bypasses_it(self):
with self.make_temp_dir() as temp_dir:
config = self._config(temp_dir)
account = accounts.create_account(
"主店",
"alias-a",
debug_port=9222,
config=config,
)
project = image_studio.create_or_get_project(
account,
item_id="51100639510",
path=config["db_path"],
)
source_path = os.path.join(temp_dir, "source.png")
self._write_image(source_path)
source = image_studio.add_asset(
project.id,
image_studio.ASSET_KIND_ORIGINAL,
local_path=source_path,
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.prompt = "轻便耐用,适合日常使用"
state.settings["per_image_primary"] = True
tab._load_state(state)
confirmations = []
tab._confirm = lambda title, message, **kwargs: confirmations.append(
(title, message, kwargs)
) and False
self.assertFalse(tab.start_generation(state))
self.assertEqual([], image_studio.list_jobs(project.id, path=config["db_path"]))
self.assertEqual("确认生成商品套图", confirmations[0][0])
self.assertIn("主店(alias-a)", confirmations[0][1])
self.assertIn("商品ID:51100639510", confirmations[0][1])
self.assertIn("可用商品原图:1张", confirmations[0][1])
self.assertIn("每张上传图分别作为主图生成:是", confirmations[0][1])
self.assertIn("图片比例:1:1", confirmations[0][1])
self.assertIn("生成总数:", confirmations[0][1])
self.assertIn("消耗 cmhub 点数", confirmations[0][1])
self.assertEqual("确认生成", confirmations[0][2]["confirm_text"])
self.assertEqual("返回修改", confirmations[0][2]["cancel_text"])
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"],
)
confirmations.clear()
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.assertEqual([], confirmations)
state.worker = None
state.thread = None
state.generation_run_token = ""
tab._generation_run_states.clear()
self.assert_removed(temp_dir)
def test_original_checkbox_click_and_keyboard_delete_keep_actions_separate(self):
original_list = ProductOriginalList()
self.addCleanup(original_list.close)