Files
ComfyAssets-ComfyUI_PromptM…/tests/test_pair_scan.py
Vito caa9649f32 feat: reuse positive and negative prompt combinations (#195)
* feat(db): add pair schema and stage restore migrations for #193

* feat(database): persist reusable ordered prompt pairs (#193)

* feat(api): expose reusable pairs and atomic metadata enrichment (#193)

* feat(metadata): resolve canonical prompt pairs with bounded provenance (#193)

* feat(scan): enrich image pairs without guessing or duplicate links (#193)

* fix(database): preserve pair tags during global maintenance (#193)

* feat(tracking): enrich live image pairs and cancel scans safely (#193)

* feat(nodes): encode paired prompts and count cached recipe runs (#193)

* fix(database): validate restored pair ownership and provenance (#193)

* feat(admin): report pair scan outcomes and cancellation (#193)

* feat(ui): add paired prompt editor and explicit recipe library for #193

* fix(autocomplete): include reusable pair metadata vocabulary (#193)

* fix(pairs): honor recent sorting and conservative scan outcomes (#193)

* fix(ui): protect paired draft saves and clarify negative variants (#193)

* fix(metadata): preserve bounded paired execution evidence (#193)

* fix(tracking): follow actual pair output roles and prefer execution evidence (#193)

* docs(pairs): document live validation and retain renderer screenshots (#193)

* test(api): inject disposable databases before API startup (#194)

* test(api): canonicalize portable temporary gallery roots (#194)

* docs(ui): prototype branching negative variant history (#193)

* docs(ui): show selected negative beneath positive in timeline concept (#193)

* docs: retain approved timeline for later gallery redesign (#193)

* test(db): close restore fixture connections before cleanup (#194)

* test(db): reuse existing handle guards in all pair fixtures (#194)

* chore: clarify rollback test and intentional exception handling (#193)
2026-10-17 08:00:42 -07:00

114 lines
4.8 KiB
Python

"""Metadata scanning enriches known pairs and conservatively leaves conflicts."""
import json
from pathlib import Path
from tests.test_api_admin import AdminAPITestCase
def graph(positive="cat", negative="blur"):
return {
"1": {"class_type": "CLIPTextEncode", "inputs": {"text": positive}},
"2": {"class_type": "CLIPTextEncode", "inputs": {"text": negative}},
"3": {
"class_type": "KSampler",
"inputs": {"positive": ["1", 0], "negative": ["2", 0]},
},
}
class TestPairScan(AdminAPITestCase):
def test_pair_scan_repeated_is_unchanged_and_empty_negative_is_supported(self):
path = self.output_dir / "pair.png"
meta = {"prompt": json.dumps(graph(negative=""))}
self.assertEqual(self.api._ingest_scanned_file(path, meta), "added")
self.assertEqual(self.api._ingest_scanned_file(path, meta), "unchanged")
pairs = self.api.db.list_prompt_pairs()["pairs"]
self.assertEqual(len(pairs), 1)
self.assertEqual(pairs[0]["negative_text"], "")
self.assertEqual(pairs[0]["run_count"], 0)
def test_ambiguous_sampler_scan_does_not_save_a_guess(self):
g = graph()
g["4"] = {**g["3"]}
meta = {"prompt": json.dumps(g)}
self.assertEqual(
self.api._ingest_scanned_file(self.output_dir / "multi.png", meta),
"ambiguous",
)
self.assertEqual(self.api.db.list_prompt_pairs()["total"], 0)
self.assertEqual(self.api.db.get_recent_prompts()["prompts"], [])
def test_batch_exposes_pair_counts_and_actual_unchanged_status(self):
meta = {"parameters": "cat\nNegative prompt: blur\nSteps: 20, Sampler: Euler"}
batch = [(Path(self.output_dir / "a.png"), meta)]
first = self.api._ingest_scan_batch_sync(batch)
self.assertEqual(first["pairs_added"], 1)
self.assertEqual(first["pair_links_added"], 1)
second = self.api._ingest_scan_batch_sync(batch)
self.assertEqual(second["unchanged"], 1)
self.assertEqual(second["linked"], 0)
self.assertEqual(second["pairs_added"], 0)
def test_legacy_single_prompt_second_pass_not_reported_as_new_link(self):
meta = {"prompt": json.dumps({"1": graph()["1"]})}
path = self.output_dir / "old.png"
self.assertEqual(self.api._ingest_scanned_file(path, meta), "added")
self.assertEqual(self.api._ingest_scanned_file(path, meta), "unchanged")
async def test_cancel_finishes_at_safe_boundary_and_restart_is_idempotent(self):
from py.api.scan_job import ScanJob
job = ScanJob()
self.api._scan_job = job
response = await self.client.post("/prompt_manager/scan/cancel")
self.assertEqual(response.status, 200)
self.assertTrue((await response.json())["cancellation_requested"])
counts = self.api._ingest_scan_batch_sync(
[(self.output_dir / "cancel.png", {"prompt": json.dumps(graph())})]
)
self.assertEqual(counts["processed"], 0)
self.assertEqual(self.api.db.list_prompt_pairs()["total"], 0)
job.finish()
self.api._scan_job = ScanJob()
first = self.api._ingest_scan_batch_sync(
[(self.output_dir / "cancel.png", {"prompt": json.dumps(graph())})]
)
self.assertEqual(first["pairs_added"], 1)
self.assertEqual(
self.api._ingest_scan_batch_sync(
[(self.output_dir / "cancel.png", {"prompt": json.dumps(graph())})]
)["unchanged"],
1,
)
def test_legacy_found_outcome_is_not_counted_twice(self):
result = self.api._ingest_scan_batch_sync(
[(self.output_dir / "blank.png", {"parameters": "Negative prompt: blur"})]
)
self.assertEqual(result["processed"], 1)
self.assertEqual(result["found"], 1)
def test_ambiguous_a1111_does_not_fall_back_to_legacy_guess(self):
meta = {
"parameters": (
"cat\nNegative prompt: blur\nNegative prompt: noise"
"\nSteps: 20, Sampler: Euler"
)
}
self.assertEqual(
self.api._ingest_scanned_file(self.output_dir / "ambiguous.png", meta),
"ambiguous",
)
self.assertEqual(self.api.db.get_recent_prompts()["prompts"], [])
def test_raw_png_workflow_is_stored_as_object_and_repeat_is_unchanged(self):
path = self.output_dir / "workflow.png"
workflow = {"nodes": [{"id": 1, "type": "PromptManagerPair"}]}
metadata = {"prompt": json.dumps(graph()), "workflow": json.dumps(workflow)}
self.assertEqual(self.api._ingest_scanned_file(path, metadata), "added")
self.assertEqual(
self.api.db.get_image_prompt_info(str(path))["workflow_data"], workflow
)
self.assertEqual(self.api._ingest_scanned_file(path, metadata), "unchanged")