Files
ComfyAssets-ComfyUI_PromptM…/tests/test_pair_execution.py
T
Vito bae542c9ac feat(history): persist exact execution prompt contexts (#213)
* feat(history): define exact bounded execution contexts

* fix(history): retain observable sources and validate evidence

* feat(history): migrate execution contexts to schema v2

* feat(history): persist immutable execution context snapshots

* fix(history): preserve package identity for execution contexts

* feat(history): bound context payloads beside lifecycle finalization

* fix(history): fence context commits and resolve stored run identity

* fix(history): bound persisted identifier bytes before restore reads

* feat(history): bind exact runtime context to execution attempts

* test(history): discover owner invalidation callback regression

* feat(history): capture cached prompt strings without mutating host caches

* test(history): verify durable contexts on real ComfyUI hosts

* fix(history): scope transforms to captured source paths

* test(ci): update execution context tooling fixtures (#209)

* fix(history): harden context validation and worker fixtures (#209)

* fix(history): honor close deadlines after early joins (#209)
2026-10-17 19:51:22 -07:00

349 lines
14 KiB
Python

"""Embedded actual encoding evidence is used only for its traced paired node."""
import json
import logging
import tempfile
import unittest
from pathlib import Path
from unittest.mock import Mock
from PIL import Image
from PIL.PngImagePlugin import PngInfo
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.operations import PromptDatabase
from prompt_manager_pair import PromptManagerPair
from utils.metadata_extractor import ComfyUIMetadataExtractor
from utils.prompt_pair_resolver import resolve_prompt_pair
KEY = "comfyassets_prompt_pairs"
def graph(positive="cat", negative="blur"):
return {
"1": {
"class_type": "PromptManagerPair",
"inputs": {"text": positive, "negative_text": negative},
},
"5": {
"class_type": "KSampler",
"inputs": {"positive": ["1", 0], "negative": ["1", 1]},
},
}
class TestPairExecution(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.addCleanup(assert_closed, self, self.tmp.name)
self.db = PromptDatabase(str(Path(self.tmp.name) / "test.db"))
self.addCleanup(self.db.close_all)
self.node = PromptManagerPair.__new__(PromptManagerPair)
self.node.db = self.db
self.node.logger = logging.getLogger("test.execution")
self.node.prompt_tracker = Mock()
self.node.comfyui_integration = Mock()
self.node._inject_lora_trigger_words = lambda text: text + " LORA"
self.extra = {"workflow": {"unchanged": True}, "sibling": {"keep": True}}
def execute(self, text="cat", negative="blur", **kwargs):
return self.node.encode_prompt(
Mock(),
text,
negative_text=negative,
unique_id="1",
extra_pnginfo=self.extra,
**kwargs,
)
def resolve(self, source=None, extra=None):
return resolve_prompt_pair(
{"prompt": source or graph(), **(self.extra if extra is None else extra)}
)
def test_schema_requests_extra_pnginfo_and_actual_positive_lora_is_preserved(self):
self.assertEqual(
PromptManagerPair.INPUT_TYPES()["hidden"]["extra_pnginfo"], "EXTRA_PNGINFO"
)
self.execute(prepend_text="trigger")
source = graph()
source["1"]["inputs"]["prepend_text"] = "trigger"
result = self.resolve(source)
self.assertEqual(result["state"], "resolved")
self.assertEqual(result["positive"]["text"], "trigger cat LORA")
self.assertEqual(result["positive"]["storage_text"], "cat")
self.assertEqual(result["positive"]["provenance"]["text_source"], "execution")
self.assertEqual(result["negative"]["text"], "blur")
self.assertEqual(self.extra["workflow"], {"unchanged": True})
self.assertEqual(self.extra["sibling"], {"keep": True})
def test_one_unknown_linked_execution_is_recoverable_without_fifo_guess(self):
source = graph(positive=["2", 0])
source["2"] = {"class_type": "PromptSearchList", "inputs": {}}
self.execute(text="actual batch cat")
result = self.resolve(source)
self.assertEqual(result["state"], "resolved")
self.assertEqual(result["positive"]["storage_text"], "actual batch cat")
self.assertEqual(result["positive"]["text"], "actual batch cat LORA")
def test_distinct_batch_records_are_unresolved_instead_of_using_last_execution(
self,
):
source = graph(positive=["2", 0])
source["2"] = {"class_type": "PromptSearchList", "inputs": {}}
self.execute(text="first")
self.execute(text="second")
result = self.resolve(source)
self.assertEqual(result["state"], "unresolved")
self.assertIn("multiple", result["reason"].lower())
def test_empty_negative_runtime_record_remains_explicit(self):
self.execute(negative="")
result = self.resolve(graph(negative=""))
self.assertEqual(result["state"], "resolved")
self.assertEqual(result["negative"]["text"], "")
def test_cached_graph_without_execution_evidence_is_explicitly_inferred(self):
result = self.resolve(extra={})
self.assertEqual(result["state"], "resolved")
self.assertEqual(
result["positive"]["provenance"]["text_source"], "graph-inferred"
)
def test_stale_or_malformed_evidence_is_not_accepted(self):
self.execute(text="stale")
self.assertEqual(self.resolve()["state"], "unresolved")
for value in (
"not json",
[],
{"version": 900, "nodes": {}},
{"version": 1, "nodes": {"1": {"records": [{"bad": True}]}}},
):
with self.subTest(value=value):
self.assertEqual(
self.resolve(extra={KEY: value})["state"], "unresolved"
)
def test_unrelated_node_evidence_does_not_override_plain_encoders(self):
source = graph()
source["1"] = {
"class_type": "CLIPTextEncode",
"inputs": {"text": "plain positive"},
}
source["2"] = {
"class_type": "CLIPTextEncode",
"inputs": {"text": "plain negative"},
}
source["5"]["inputs"]["negative"] = ["2", 0]
result = self.resolve(source, extra={KEY: "broken"})
self.assertEqual(result["state"], "resolved")
self.assertEqual(result["positive"]["text"], "plain positive")
def test_png_extractor_preserves_namespace_for_live_and_raw_scan_readers(self):
self.execute()
path = Path(self.tmp.name) / "image.png"
info = PngInfo()
info.add_text("prompt", json.dumps(graph()))
info.add_text(KEY, json.dumps(self.extra[KEY]))
Image.new("RGB", (2, 2)).save(path, pnginfo=info)
parsed = ComfyUIMetadataExtractor().extract_metadata(str(path))
self.assertEqual(resolve_prompt_pair(parsed)["positive"]["text"], "cat LORA")
with Image.open(path) as image:
raw = dict(image.text)
raw["prompt"] = json.loads(raw["prompt"])
self.assertEqual(resolve_prompt_pair(raw)["positive"]["text"], "cat LORA")
def test_recording_limits_fail_closed_and_repeat_identical_values_are_safe(self):
from utils.pair_execution import MAX_EXECUTION_RECORDS
self.execute()
self.execute()
self.assertEqual(self.resolve()["state"], "resolved")
for index in range(MAX_EXECUTION_RECORDS + 1):
self.execute(text=str(index))
self.assertEqual(self.resolve()["state"], "unresolved")
def test_live_metadata_lookup_uses_unique_execution_for_unknown_source(self):
from utils.image_monitor import ImageGenerationHandler
source = graph(positive=["2", 0])
source["2"] = {"class_type": "PromptSearchList", "inputs": {}}
self.execute(text="actual")
handler = ImageGenerationHandler.__new__(ImageGenerationHandler)
handler.db_manager = self.db
handler.logger = logging.getLogger("test.execution")
state, context = handler._find_prompt_from_metadata(
{"prompt": source, **self.extra}
)
self.assertEqual(state, "linked")
expected = self.db.list_prompt_pairs()["pairs"][0]["positive_prompt_id"]
self.assertEqual(context["id"], expected)
def test_submission_clears_old_execution_evidence_without_changing_workflow(self):
from utils.usage_tracking import handle_queued_prompt, QueuedRuns
self.execute()
request = {"prompt": graph(), "extra_data": {"extra_pnginfo": self.extra}}
handle_queued_prompt(request, QueuedRuns(), pair_queued=QueuedRuns())
self.assertNotIn(KEY, self.extra)
self.assertEqual(self.extra["workflow"], {"unchanged": True})
self.assertEqual(
self.resolve()["positive"]["provenance"]["text_source"], "graph-inferred"
)
def test_known_invalid_output_slot_or_cycles_cannot_be_overridden(self):
self.execute()
source = graph(positive=["2", 4])
source["2"] = {"class_type": "PrimitiveString", "inputs": {"value": "cat"}}
self.assertEqual(self.resolve(source)["state"], "unresolved")
source["2"] = {
"class_type": "StringConcatenate",
"inputs": {"string_a": ["2", 0], "string_b": "cat"},
}
source["1"]["inputs"]["text"] = ["2", 0]
self.assertEqual(self.resolve(source)["state"], "unresolved")
def test_live_image_association_keeps_actual_encoding_and_repeated_scan_is_stable(
self,
):
from utils.image_monitor import ImageGenerationHandler
from database.pair_ingestion import ingest_resolved_pair
self.execute()
metadata = {"prompt": graph(), **self.extra}
handler = ImageGenerationHandler.__new__(ImageGenerationHandler)
handler.db_manager = self.db
handler.logger = logging.getLogger("test.execution")
_, context = handler._find_prompt_from_metadata(metadata)
path = str(Path(self.tmp.name) / "actual.png")
handler.link_image_to_prompt(path, context, metadata)
pair_context = self.db.get_image_prompt_info(path)["pair_context"]
self.assertEqual(pair_context["positive_text"], "cat LORA")
repeated = ingest_resolved_pair(
self.db, path, metadata, resolve_prompt_pair(metadata)
)
self.assertEqual(repeated["status"], "unchanged")
def test_unknown_batch_with_multiple_executions_is_not_exactly_linked(self):
from utils.image_monitor import ImageGenerationHandler
source = graph(positive=["2", 0])
source["2"] = {"class_type": "PromptSearchList", "inputs": {}}
self.execute(text="first")
self.execute(text="second")
handler = ImageGenerationHandler.__new__(ImageGenerationHandler)
handler.db_manager = self.db
handler.logger = logging.getLogger("test.execution")
state, context = handler._find_prompt_from_metadata(
{"prompt": source, **self.extra}
)
self.assertEqual(state, "unknown")
self.assertIsNone(context)
def test_execution_evidence_bounds_and_malformed_structures_fail_closed(self):
from utils import pair_execution as evidence
from unittest.mock import patch
kwargs = dict(
text="cat",
negative_text="blur",
positive_text="cat LORA",
negative_effective_text="blur",
)
evidence.record_pair_execution(None, "1", **kwargs)
envelope = {}
evidence.record_pair_execution(envelope, None, **kwargs)
self.assertEqual(envelope, {})
evidence.record_pair_execution(envelope, "1", **kwargs)
self.assertIsNone(evidence.read_pair_execution(envelope, "untraced"))
malformed = (
{"version": 1, "nodes": []},
{"version": 1, "truncated": True, "nodes": {}},
{"version": 1, "nodes": {"1": []}},
{"version": 1, "nodes": {"1": {"records": []}}},
{"version": 1, "nodes": {"1": {"records": "bad"}}},
{
"version": 1,
"nodes": {
"1": {
"records": [
{
**kwargs,
"prepend_text": "",
"append_text": "",
"negative_effective_text": "changed",
}
]
}
},
},
)
for value in malformed:
with self.subTest(value=value):
with self.assertRaises(evidence.ExecutionEvidenceError):
evidence.read_pair_execution({KEY: value}, "1")
oversized = {}
evidence.record_pair_execution(
oversized,
"1",
**{**kwargs, "text": "x" * (evidence.MAX_EXECUTION_TEXT + 1)},
)
with self.assertRaises(evidence.ExecutionEvidenceError):
evidence.read_pair_execution(oversized, "1")
with patch.object(evidence, "MAX_EXECUTION_JSON", 10):
with self.assertRaises(evidence.ExecutionEvidenceError):
evidence.read_pair_execution({KEY: json.dumps(envelope[KEY])}, "1")
limited = {}
evidence.record_pair_execution(limited, "1", **kwargs)
with self.assertRaises(evidence.ExecutionEvidenceError):
evidence.read_pair_execution(limited, "1")
with patch.object(evidence, "MAX_EXECUTION_NODES", 1):
evidence.record_pair_execution(envelope, "2", **kwargs)
evidence.record_pair_execution(envelope, "3", **kwargs)
with self.assertRaises(evidence.ExecutionEvidenceError):
evidence.read_pair_execution(envelope, "1")
def test_corrupted_existing_namespace_cannot_break_valid_encoding(self):
from utils.pair_execution import (
record_pair_execution,
read_pair_execution,
ExecutionEvidenceError,
)
kwargs = dict(
text="cat",
negative_text="blur",
positive_text="cat LORA",
negative_effective_text="blur",
)
for nodes in ([], {"1": {"records": "bad"}}, {"other": {"bad": object()}}):
extra = {KEY: {"version": 1, "nodes": nodes}}
record_pair_execution(extra, "1", **kwargs)
with self.assertRaises(ExecutionEvidenceError):
read_pair_execution(extra, "1")
def test_png_evidence_cannot_populate_unobserved_attempt(self):
from utils.execution_context_capture import ContextCapture
from utils.execution_context_budget import ContextBudget
from utils.execution_history_events import RunStart
from utils.execution_history_writer import HistoryWriterStatus
identity = "11111111-1111-4111-8111-111111111111"
self.execute()
state = HistoryWriterStatus(identity, True, 0, 0, True, 1)
manager = ContextCapture(1, lambda: state, ContextBudget())
self.addCleanup(manager.revoke_all)
cap = manager.begin(
RunStart(identity, identity, 1, identity, identity, 0), 1, graph(), None
)
sealed = manager.seal(cap)
if sealed.lease:
self.addCleanup(sealed.lease.release)
self.assertIsNone(sealed.bundle.contexts[0].positive.effective.value)
self.assertIsNone(sealed.bundle.contexts[0].negative.effective.value)