From 07bdfa559ffa502b0b34875e5994c173324500c3 Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Fri, 30 Oct 2026 14:58:11 -0700 Subject: [PATCH] fix(nodes): Save pair also gives a new prompt its category and tags (#546) The node's Save pair button posts to /prompt_manager/pairs, which created the prompt row empty; under the fill-on-create rule a later run could never fill it. --- TESTING.md | 6 +++--- py/api/pairs.py | 7 ++++++- tests/comfyui/pair-node.spec.mjs | 9 +++++++++ tests/test_api_pairs.py | 26 ++++++++++++++++++++++++++ 4 files changed, 44 insertions(+), 4 deletions(-) diff --git a/TESTING.md b/TESTING.md index abafc620..9112508c 100644 --- a/TESTING.md +++ b/TESTING.md @@ -2890,11 +2890,11 @@ change: ComfyUI needs a restart. ### Pair node gives a new prompt its category and tags (#546) -A Pair save writes the category and tags to the positive prompt only when that save creates the prompt row (maintainer -ruling). An existing prompt never changes on a run: empty, cleared or set by a person, including one first seen as +A Pair save, by a run or the node's Save pair button, writes the category and tags to the positive prompt only when that +save creates the prompt row (maintainer ruling). An existing prompt never changes on a run: empty, cleared or set by a person, including one first seen as another pair's negative. The negative stays empty, the saved pair keeps its own metadata, and `save_prompt_pair` writes no prompt metadata unless called with `fill_new_positive`. No backfill; optional repair is #550. -Tests: `tests/test_prompt_pair_node.py`. A Python change: ComfyUI needs a restart. +Tests: `tests/test_prompt_pair_node.py`, `tests/test_api_pairs.py`, `tests/comfyui/pair-node.spec.mjs`. A Python change: ComfyUI needs a restart. ### Delete… on the archived shelf (#492) diff --git a/py/api/pairs.py b/py/api/pairs.py index 775f0e8a..102271a4 100644 --- a/py/api/pairs.py +++ b/py/api/pairs.py @@ -102,7 +102,12 @@ class PairRoutesMixin: validate_prompt_text(negative) fields = metadata_fields(data) pair_id = await self._run_in_executor( - self.db.save_prompt_pair, positive, negative, **fields + self.db.save_prompt_pair, + positive, + negative, + # The node's Save pair button: as a run, a new prompt gets the metadata. + fill_new_positive=True, + **fields, ) pair = await self._run_in_executor(self.db.get_prompt_pair, pair_id) return web.json_response({"success": True, "pair": pair}) diff --git a/tests/comfyui/pair-node.spec.mjs b/tests/comfyui/pair-node.spec.mjs index c9a4c485..7dda84a4 100644 --- a/tests/comfyui/pair-node.spec.mjs +++ b/tests/comfyui/pair-node.spec.mjs @@ -60,6 +60,15 @@ test('paired explicit Save persists an empty negative through actual API and pre await expect(root.getByRole('status').filter({hasText:'Pair saved.'})).toBeVisible();const response=await request.get(`/prompt_manager/pairs?text=${encodeURIComponent(text)}`);expect(response.ok()).toBe(true);expect((await response.json()).pairs).toEqual(expect.arrayContaining([expect.objectContaining({positive_text:text,negative_text:''})]));expect(await values(page,id)).toMatchObject({text,negative_text:''}); }); +test('Save pair gives a new prompt the category and tags shown on the node',async({page,request},testInfo)=>{ + await addPair(page);const root=page.locator('.pm-pair');const text=`Saved pair metadata ${testInfo.project.name}`;await root.getByRole('textbox',{name:'Positive prompt',exact:true}).fill(text); + await root.getByText('Metadata · category / tags',{exact:true}).click();await root.getByRole('combobox',{name:'Category',exact:true}).fill('Saved category');await root.getByRole('combobox',{name:'Tags',exact:true}).fill('saved-tag'); + await root.getByRole('button',{name:'Save pair',exact:true}).click();await expect(root.getByRole('status').filter({hasText:'Pair saved.'})).toBeVisible(); + const [pair]=(await (await request.get(`/prompt_manager/pairs?text=${encodeURIComponent(text)}`)).json()).pairs; + const library=await (await request.get(`/prompt_manager/library?text=${encodeURIComponent(text)}&limit=10`)).json();const prompt=library.items.find(item=>item.id===pair.positive_prompt_id); + expect([prompt.category,prompt.tags]).toEqual(['Saved category',['saved-tag']]); +}); + test('paired library filters search both texts, persist on reload and keep editor metadata separate',async({page,request},testInfo)=>{ const token=`paired-filter-${testInfo.project.name}`;await seed(request,`Matching ${token}`,`Negative ${token}`);const other=await request.post('/prompt_manager/pairs',{data:{positive_text:`Other ${token}`,negative_text:'different',category:'Other paired category',tags:['other-pair-tag']}});expect(other.ok()).toBe(true); const id=await addPair(page);const root=page.locator('.pm-pair');await root.getByRole('textbox',{name:'Positive prompt',exact:true}).fill('Draft positive');await root.getByRole('textbox',{name:'Negative prompt',exact:true}).fill('Draft negative'); diff --git a/tests/test_api_pairs.py b/tests/test_api_pairs.py index 846e9964..2dcbb414 100644 --- a/tests/test_api_pairs.py +++ b/tests/test_api_pairs.py @@ -1,6 +1,7 @@ """Pair routes preserve shared text and expose portable combinations.""" from tests.test_api_prompts import PromptAPITestCase +from utils.hashing import generate_prompt_hash class TestPairAPI(PromptAPITestCase): @@ -9,6 +10,31 @@ class TestPairAPI(PromptAPITestCase): payload.update(overrides) return await self.client.post("/prompt_manager/pairs", json=payload) + async def _prompt_saved_by_route(self, text, **fields): + response = await self._create(positive_text=text, **fields) + self.assertEqual(response.status, 200) + return self.api.db.get_prompt_by_hash(generate_prompt_hash(text)) + + async def test_a_new_prompt_gets_the_saved_category_and_tags(self): + prompt = await self._prompt_saved_by_route( + "route cat", category="Animals", tags=["studio"] + ) + self.assertEqual(prompt["category"], "Animals") + self.assertEqual(prompt["tags"], ["studio"]) + + async def test_saving_never_changes_an_existing_prompt(self): + self.api.db.save_prompt( + "route dog", + category="Mine", + tags=["kept"], + prompt_hash=generate_prompt_hash("route dog"), + ) + prompt = await self._prompt_saved_by_route( + "route dog", category="Animals", tags=["studio"] + ) + self.assertEqual(prompt["category"], "Mine") + self.assertEqual(prompt["tags"], ["kept"]) + async def test_create_reuse_read_and_search(self): response = await self._create(tags=["studio"], title="Portrait") self.assertEqual(response.status, 200)