Files
ComfyAssets-ComfyUI_PromptM…/tests/test_pair_restore_semantics.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

120 lines
4.6 KiB
Python

"""Restores preserve pair ownership and bounded, readable JSON provenance."""
import json
import os
import tempfile
import unittest.mock
try:
from tests.open_handles import assert_closed
except ImportError: # discovered with tests/ as the top-level directory
from open_handles import assert_closed
from database.models import PromptModel
from database.operations import PromptDatabase
class TestPairRestoreSemantics(unittest.TestCase):
def setUp(self):
directory = tempfile.TemporaryDirectory()
self.addCleanup(directory.cleanup)
self.addCleanup(assert_closed, self, directory.name)
self.path = os.path.join(directory.name, "restore.db")
self.db = PromptDatabase(self.path)
self.addCleanup(self.db.close_all)
self.pair_id = self.db.save_prompt_pair("portrait", "blurry")
pair = self.db.get_prompt_pair(self.pair_id)
self.image_id = self.db.link_image_to_prompt(
pair["positive_prompt_id"], os.path.join(directory.name, "image.png")
)
def raw_link(self, provenance, pair_id=None):
with self.db.model.get_connection() as conn:
conn.execute("DELETE FROM image_prompt_pairs")
conn.execute(
"INSERT INTO image_prompt_pairs(image_id,pair_id,provenance) "
"VALUES(?,?,?)",
(self.image_id, pair_id or self.pair_id, provenance),
)
conn.commit()
def test_restore_rejects_pair_with_different_image_positive_owner(self):
other = self.db.save_prompt_pair("landscape", "blurry")
self.raw_link("{}", other)
ok, reason = PromptModel.verify_database_file(self.path)
self.assertFalse(ok)
self.assertIn("positive", reason.lower())
def test_restore_rejects_unreadable_nonobject_and_nonfinite_provenance(self):
for value in (
"not JSON",
"[]",
"null",
"false",
"42",
'"text"',
'{"value":NaN}',
'{"value":Infinity}',
'{"value":-Infinity}',
'{"value":1e999}',
b'{"blob":true}',
):
with self.subTest(value=value):
self.raw_link(value)
ok, reason = PromptModel.verify_database_file(self.path)
self.assertFalse(ok)
self.assertIn("provenance", reason.lower())
def test_restore_retains_valid_nested_object_provenance(self):
provenance = {"format": "future", "nested": [{"empty": "", "value": 2.5}]}
self.raw_link(json.dumps(provenance))
self.assertEqual(PromptModel.verify_database_file(self.path), (True, "ok"))
self.assertEqual(
self.db.get_image_prompt_pairs(self.image_id)[0]["provenance"], provenance
)
def test_restore_rejects_excessive_json_nesting_without_raising(self):
self.raw_link('{"nested":' * 1500 + "0" + "}" * 1500)
ok, reason = PromptModel.verify_database_file(self.path)
self.assertFalse(ok)
self.assertIn("provenance", reason.lower())
def test_restore_and_runtime_share_the_provenance_size_limit(self):
provenance = {"large": "x" * (4_000_000 + 1)}
with self.assertRaises(ValueError):
self.db.link_image_to_pair(self.pair_id, self.image_id, provenance)
self.raw_link(json.dumps(provenance))
ok, reason = PromptModel.verify_database_file(self.path)
self.assertFalse(ok)
self.assertIn("provenance", reason.lower())
def test_runtime_normalizes_deep_json_encoding_error(self):
provenance = {}
child = provenance
for _ in range(1500):
child["nested"] = {}
child = child["nested"]
with self.assertRaises(ValueError):
self.db.link_image_to_pair(self.pair_id, self.image_id, provenance)
self.assertEqual(self.db.get_image_prompt_pairs(self.image_id), [])
def test_runtime_converts_encoder_recursion_failure_to_validation_error(self):
with unittest.mock.patch(
"database.pairs.json.dumps", side_effect=RecursionError("depth")
):
with self.assertRaisesRegex(ValueError, "JSON"):
self.db.link_image_to_pair(self.pair_id, self.image_id, {})
def test_restore_converts_parser_recursion_failure_to_rejection(self):
self.raw_link("{}")
with unittest.mock.patch(
"database.models.json.loads", side_effect=RecursionError("depth")
):
ok, reason = PromptModel.verify_database_file(self.path)
self.assertFalse(ok)
self.assertIn("provenance", reason.lower())
if __name__ == "__main__":
unittest.main()