Files
ComfyAssets-ComfyUI_PromptM…/tests/test_foreign_save_to_library.py
T
Vito Sansevero dc1258d0bd fix(monitor): an undecided not_saved waits for its run's record (#247, #90)
A not_saved from the ladder's undecided step is not final before a record
exists: the recorded saver may still resolve another branch. The settle
lookup now keeps such a file waiting; with the record it stores nothing and
logs the opted-out line. An early not_saved (a switched-off node upstream of
any sampler's positive) never reaches the lookup and stays final.

Tests: the undecided case (literal and wired switch) waits, then skips; a
switched-off node feeding one branch of a shared-prefix run never waits and
stores nothing, not even the branch the recorded saver names.
2026-10-28 20:15:20 -07:00

293 lines
13 KiB
Python

"""#247 PR 2 x #90: the monitor's foreign path never stores switched-off text.
A file whose run's prompt has Save to library off is the opted-out path's,
not the foreign path's; a foreign graph whose ladder decision is
``not_saved`` (a switched-off node the #90 walk does not reach) stores
nothing at settle, at the re-check after the seal, or from recorded history,
and leaves no wait behind.
"""
import os
import unittest
from tests import saver_anchor_graphs as shapes
from tests import test_run_attribution as attribution
from tests import test_save_to_library as save_tests
from tests.test_pair_scan import sampled_graph
from utils.foreign_runs import ForeignRecheck
from utils.hashing import generate_prompt_hash
from py.config import GalleryConfig # after tests: it stubs the host
LOGGER = "prompt_manager.image_monitor"
OFF_TEXT = "SECRET OFF TEXT"
SKIPPED = "Save to library is off for this run's prompt, skipping image"
# The class dict, not the class: a TestCase bound here would run again
Fixture = vars(attribution.TestMonitorForeignRuns)
def off_behind(middle="MyNode", key="value"):
"""A switched-off PromptManagerText reaching a core CLIPTextEncode through
``middle``'s ``key`` input: the #90 walk does not follow ``key``."""
graph = save_tests.run_graph()
graph["1"] = {
"class_type": "PromptManagerText",
"inputs": {"text": OFF_TEXT, "save_to_library": False},
}
graph["7"] = {"class_type": middle, "inputs": {key: ["1", 0]}}
graph["8"] = {
"class_type": "CLIPTextEncode",
"inputs": {"text": ["7", 0], "clip": ["4", 1]},
}
graph["3"]["inputs"]["positive"] = ["8", 0]
return graph
def off_text_unwired():
"""Only history can decide; the legacy answer is a dangling switched-off
node's own text (the ladder says ``not_saved`` once it may ask history)."""
graph = sampled_graph("unused", "blurry")
graph["1"]["inputs"]["text"] = ["30", 0]
graph["30"] = {"class_type": "VendorPromptMaker", "inputs": {"prompt": OFF_TEXT}}
graph["40"] = {
"class_type": "PromptManagerText",
"inputs": {"text": OFF_TEXT, "save_to_library": False},
}
return graph
class TestForeignPathKeepsSaveToLibrary(attribution.RecordedRuns, unittest.TestCase):
setUp = Fixture["setUp"]
close_databases = Fixture["close_databases"]
wait_for = Fixture["wait_for"]
image = Fixture["image"]
linked_text = Fixture["linked_text"]
key = Fixture["key"]
prompt_count = Fixture["prompt_count"]
settled = Fixture["settled"]
def reference(self, path):
return {
"folder_type": "output",
"subfolder": "dataset",
"filename": os.path.basename(path),
}
def seal(self, path, positives=(), nodes=("9",)):
"""Record one run reporting ``path`` (a new run id for every call)."""
self.runs = getattr(self, "runs", -1) + 1
self.record_run(
self.db,
self.output,
[("dataset", os.path.basename(path))],
list(positives),
item=self.runs,
nodes=list(nodes),
)
def recheck(self, path):
"""The re-check the seal schedules, run through the monitor's entry."""
with self.assertLogs(LOGGER, "INFO") as logs:
self.handler.process_new_image(path, ForeignRecheck(self.reference(path)))
return "\n".join(logs.output)
def assert_nothing_stored(self, path):
self.assertIsNone(self.linked_text(path))
self.assertEqual(self.prompt_count(), 0)
self.assertIsNone(self.db.get_prompt_by_hash(generate_prompt_hash(OFF_TEXT)))
self.assertEqual(self.handler.awaiting_foreign(), [])
self.assertEqual(list(self.handler._foreign_due), [])
def test_q1_the_only_prompt_manager_node_off_is_opted_out_not_foreign(self):
graph = save_tests.run_graph(save=False)
# The switched-off positive is the only PromptManager node
graph["2"] = {"class_type": "CLIPTextEncode", "inputs": {"text": "blurry"}}
sampler_less = {
"1": {
"class_type": "PromptManagerText",
"inputs": {"text": OFF_TEXT, "save_to_library": False},
}
}
for name, graph in (("sampled", graph), ("sampler-less", sampler_less)):
with self.subTest(name=name):
self.metadata = {"prompt": graph}
state, _ = self.handler._find_prompt_from_metadata(self.metadata)
self.assertEqual(state, "opted_out")
path = self.image(f"IMG_GEN_{name}_00001_.png")
with self.assertLogs(LOGGER, "INFO") as logs:
self.handler.process_new_image(path)
self.assertIn(SKIPPED, "\n".join(logs.output))
self.seal(path)
self.assertTrue(self.settled())
self.assertIn(SKIPPED, self.recheck(path))
self.assert_nothing_stored(path)
def test_q2_an_off_text_node_behind_a_pass_through_stores_nothing(self):
for middle, key in (("MyNode", "value"), ("easy showAnything", "anything")):
with self.subTest(middle=middle):
self.metadata = {"prompt": off_behind(middle, key)}
state, _ = self.handler._find_prompt_from_metadata(self.metadata)
self.assertEqual(state, "foreign")
path = self.image(f"IMG_GEN_{key}_00001_.png")
with self.assertLogs(LOGGER, "INFO") as logs:
self.handler.process_new_image(path)
self.assertIn(SKIPPED, "\n".join(logs.output))
self.assert_nothing_stored(path)
self.seal(path)
self.assertTrue(self.settled())
self.assertIn(SKIPPED, self.recheck(path))
self.assert_nothing_stored(path)
def test_q2_a_wired_switch_behind_a_pass_through_stores_nothing(self):
graph = off_behind()
graph["1"]["inputs"]["save_to_library"] = ["20", 0]
graph["20"] = {"class_type": "SomeBoolNode", "inputs": {}}
self.metadata = {"prompt": graph}
path = self.image("IMG_GEN_wired_00001_.png")
wired = "Save to library is wired from a node the image cannot show"
with self.assertLogs(LOGGER, "INFO") as logs:
self.handler.process_new_image(path)
self.assertIn(wired, "\n".join(logs.output))
self.seal(path)
self.assertTrue(self.settled())
self.assertIn(wired, self.recheck(path))
self.assert_nothing_stored(path)
def test_q3_an_undecided_not_saved_waits_for_the_record_then_skips(self):
"""No record yet: a not_saved of the undecided step is not final (a
recorded saver may resolve another branch), so the file waits for its
run; the decision with the record stores nothing and logs the skip."""
wired = off_text_unwired()
wired["40"]["inputs"]["save_to_library"] = ["20", 0]
wired["20"] = {"class_type": "SomeBoolNode", "inputs": {}}
cases = (
("off", off_text_unwired(), SKIPPED),
("wired", wired, "Save to library is wired from a node"),
)
for name, graph, line in cases:
with self.subTest(name=name):
self.metadata = {"prompt": graph}
path = self.image(f"vendor_{name}_00001_.png")
self.handler.process_new_image(path)
self.assertIsNone(self.linked_text(path))
self.assertEqual(self.handler.awaiting_foreign(), [self.key(path)])
with self.assertLogs(LOGGER, "INFO") as logs:
self.seal(path)
self.assertTrue(
self.wait_for(lambda: not self.handler.awaiting_foreign())
)
self.assertTrue(self.settled())
self.assertIn(line, "\n".join(logs.output))
self.assert_nothing_stored(path)
def test_q3_an_early_not_saved_never_waits_even_with_a_saver_branch(self):
"""Review: a switched-off node feeding one branch's positive is the
ladder's early, final not_saved (PR 1's #90 rule covers every
sampler's positive), so the file never waits and nothing is stored,
not even the plain branch the recorded saver names."""
graph = shapes.shared_prefix()
graph["20"]["inputs"]["text"] = ["50", 0]
graph["50"] = {
"class_type": "PromptManagerText",
"inputs": {"text": OFF_TEXT, "save_to_library": False},
}
self.metadata = {"prompt": graph}
path = self.image("IMG_GEN_00001_.png")
with self.assertLogs(LOGGER, "INFO") as logs:
self.handler.process_new_image(path)
self.assertIn(SKIPPED, "\n".join(logs.output))
self.assert_nothing_stored(path)
self.seal(path, nodes=("13",))
self.assertTrue(self.settled())
self.assertIn(SKIPPED, self.recheck(path))
self.assert_nothing_stored(path)
self.assertIsNone(
self.db.get_prompt_by_hash(generate_prompt_hash(shapes.BRANCH_TEXTS[0]))
)
def test_q4_an_off_node_on_the_negative_only_still_links_the_positive(self):
positive = "a lighthouse on a cliff at dusk"
plain = sampled_graph(positive, "unused")
plain["2"] = {
"class_type": "PromptManager",
"inputs": {"text": OFF_TEXT, "save_to_library": False},
}
behind = sampled_graph(positive, "unused")
behind["2"]["inputs"]["text"] = ["7", 0]
behind["7"] = {
"class_type": "PromptManagerText",
"inputs": {"text": OFF_TEXT, "save_to_library": False},
}
for name, graph in (("direct", plain), ("behind an encoder", behind)):
with self.subTest(name=name):
self.metadata = {"prompt": graph}
state, _ = self.handler._find_prompt_from_metadata(self.metadata)
self.assertEqual(state, "foreign")
path = self.image(f"plain_{len(name)}_00001_.png")
self.handler.process_new_image(path)
self.assertEqual(self.linked_text(path), positive)
self.assertIsNone(
self.db.get_prompt_by_hash(generate_prompt_hash(OFF_TEXT))
)
pairs = self.db.list_prompt_pairs()["pairs"]
self.assertEqual(pairs, [])
self.assertEqual(self.handler.awaiting_foreign(), [])
def test_q5_recorded_history_never_brings_back_switched_off_text(self):
"""Even a recorded run whose contexts hold the off text (an older
history row) cannot name it, at settle or at the re-check."""
cases = {"pass-through": off_behind(), "dangling": off_text_unwired()}
for name, graph in cases.items():
with self.subTest(name=name):
self.metadata = {"prompt": graph}
path = self.image(f"IMG_GEN_{len(name)}_00001_.png")
self.seal(path, positives=[OFF_TEXT], nodes=("9",))
self.handler.process_new_image(path)
self.assert_nothing_stored(path)
self.assertIn(SKIPPED, self.recheck(path))
self.assert_nothing_stored(path)
def test_q6_with_foreign_saving_off_save_to_library_is_unchanged(self):
GalleryConfig.SAVE_FOREIGN_RUNS = False
opted_out = save_tests.run_graph(save=False)
self.metadata = {"prompt": opted_out}
skipped = self.image("IMG_GEN_off_00001_.png")
with self.assertLogs(LOGGER, "INFO") as logs:
self.handler.process_new_image(skipped)
self.assertIn(SKIPPED, "\n".join(logs.output))
self.metadata = {"prompt": off_behind()}
foreign = self.image("IMG_GEN_foreign_00001_.png")
with self.assertLogs(LOGGER, "INFO") as logs:
self.handler.process_new_image(foreign)
self.assertIn("No PromptManager prompt in workflow", "\n".join(logs.output))
for path in (skipped, foreign):
self.assert_nothing_stored(path)
# A switched-on PromptManager positive still links by its metadata
stored = self.db.save_prompt(
text=save_tests.POSITIVE,
prompt_hash=generate_prompt_hash(save_tests.POSITIVE),
)
self.metadata = {"prompt": save_tests.run_graph(save=True)}
on = self.image("IMG_GEN_on_00001_.png")
self.handler.process_new_image(on)
self.assertEqual(self.linked_text(on), save_tests.POSITIVE)
self.assertEqual(
self.db.get_prompt_by_hash(generate_prompt_hash(save_tests.POSITIVE))["id"],
stored,
)
def test_the_shared_prefix_dataset_still_links_after_the_seal(self):
"""Regression guard (passes before the fix): the wait still works."""
self.metadata = {"prompt": shapes.shared_prefix()}
path = self.image("IMG_GEN_00001_.png")
self.handler.process_new_image(path)
self.assertEqual(self.handler.awaiting_foreign(), [self.key(path)])
self.seal(path, nodes=("13",))
self.assertTrue(self.wait_for(lambda: self.linked_text(path)))
self.assertTrue(self.settled())
self.assertEqual(self.linked_text(path), shapes.BRANCH_TEXTS[0])
if __name__ == "__main__":
unittest.main()