feat(product-suite): persist context and confirm actions
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user