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.
This commit is contained in:
Vito Sansevero
2026-10-30 14:58:11 -07:00
parent d20842ac7a
commit 07bdfa559f
4 changed files with 44 additions and 4 deletions
+3 -3
View File
@@ -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)
+6 -1
View File
@@ -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})
+9
View File
@@ -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');
+26
View File
@@ -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)