* fix(history): count cached re-runs of runtime-linked text as tracked runs (#403) Since #347 a cached re-run of runtime-linked prompt text (PreviewAny or an LLM into a PromptManager text input) counts in the Library, but its history capture had no raw text, so the history page's "tracked runs" stayed at the number of jobs in which the node executed. The #347 receipt now also keeps the raw, prepend and append text the node gave the history capture. When a cached node's graph inference leaves a field unknown, the capture takes it from the receipt that matches the cache entry, labeled inferred/graph_semantics, so the job joins the prompt's history scope and gets its prompt hint. No schema change and no new evidence vocabulary; no backfill. Fails closed on no receipt, a disagreeing known field, equal outputs with different fields, oversized or invalid text and a full budget (least recently used receipts give up their fields first; counting stays). * docs(testing): link the whitespace history gap to #438
588 lines
24 KiB
Python
588 lines
24 KiB
Python
"""Read-only cached STRING evidence and cooperative executor notifications."""
|
|
|
|
from collections import namedtuple
|
|
from contextvars import Context
|
|
from dataclasses import replace
|
|
import gc
|
|
import sys
|
|
import weakref
|
|
import threading
|
|
import types
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
from test_execution_context_graph import transformed_input_graph
|
|
|
|
from utils.execution_context_budget import ContextBudget
|
|
from utils.execution_context_capture import ContextCapture
|
|
from utils.execution_history_events import RunStart
|
|
from utils.execution_history_writer import HistoryWriterStatus
|
|
|
|
try:
|
|
from utils import execution_context_host as host
|
|
except ImportError:
|
|
host = None
|
|
|
|
SESSION = "11111111-1111-4111-8111-111111111111"
|
|
EPOCH = "22222222-2222-4222-8222-222222222222"
|
|
JOB = "33333333-3333-4333-8333-333333333333"
|
|
Entry = namedtuple("CacheEntry", "ui outputs")
|
|
|
|
|
|
class HostTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.assertIsNotNone(host, "read-only host adapter is missing")
|
|
self.queue = object()
|
|
self.pool = ContextBudget()
|
|
self.status = HistoryWriterStatus(EPOCH, True, 0, 0, True, 3)
|
|
self.capture = ContextCapture(id(self.queue), lambda: self.status, self.pool)
|
|
self.addCleanup(self.capture.revoke_all)
|
|
self.calls = []
|
|
calls = self.calls
|
|
|
|
class BasicCache:
|
|
def __init__(self):
|
|
self.entries = {}
|
|
|
|
def get_local(self, node_id):
|
|
calls.append(("local", node_id))
|
|
return self.entries.get(node_id)
|
|
|
|
async def get(self, node_id):
|
|
raise AssertionError("async get must never be called")
|
|
|
|
class HierarchicalCache(BasicCache):
|
|
pass
|
|
|
|
class LRUCache(BasicCache):
|
|
pass
|
|
|
|
class RAMPressureCache(LRUCache):
|
|
pass
|
|
|
|
class NullCache:
|
|
def get_local(self, node_id):
|
|
raise AssertionError("null cache is not usable")
|
|
|
|
class PromptExecutor:
|
|
def add_message(self, event, data, broadcast):
|
|
calls.append(("original", event, data, broadcast))
|
|
if event == "raise":
|
|
raise RuntimeError("original")
|
|
return "original-return"
|
|
|
|
self.execution = types.ModuleType("execution")
|
|
self.execution.PromptExecutor = PromptExecutor
|
|
self.execution.CacheEntry = Entry
|
|
self.caching = types.ModuleType("comfy_execution.caching")
|
|
for cls in (
|
|
BasicCache,
|
|
HierarchicalCache,
|
|
LRUCache,
|
|
RAMPressureCache,
|
|
NullCache,
|
|
):
|
|
setattr(self.caching, cls.__name__, cls)
|
|
package = types.ModuleType("comfy_execution")
|
|
package.caching = self.caching
|
|
self.patch = patch.dict(
|
|
sys.modules,
|
|
{
|
|
"comfy_execution": package,
|
|
"execution": self.execution,
|
|
"comfy_execution.caching": self.caching,
|
|
},
|
|
)
|
|
self.patch.start()
|
|
self.addCleanup(self.patch.stop)
|
|
self.executor = PromptExecutor()
|
|
self.executor.server = types.SimpleNamespace(prompt_queue=self.queue)
|
|
self.executor.caches = types.SimpleNamespace(outputs=HierarchicalCache())
|
|
|
|
def begin(
|
|
self,
|
|
kind="PromptManager",
|
|
raw=" raw ",
|
|
prepend=" pre ",
|
|
append=" post ",
|
|
negative="negative",
|
|
):
|
|
g = {
|
|
"p": {
|
|
"class_type": kind,
|
|
"inputs": {
|
|
"text": raw,
|
|
"negative_text": negative,
|
|
"prepend_text": prepend,
|
|
"append_text": append,
|
|
},
|
|
},
|
|
"clip": {"class_type": "CLIPTextEncode", "inputs": {"text": ["p", 0]}},
|
|
"s": {
|
|
"class_type": "KSampler",
|
|
"inputs": {
|
|
"positive": (
|
|
["clip", 0] if kind == "PromptManagerText" else ["p", 0]
|
|
),
|
|
"negative": (
|
|
["p", 1] if kind == "PromptManagerPair" else ["clip", 0]
|
|
),
|
|
},
|
|
},
|
|
}
|
|
if any(type(value) is list for value in (raw, prepend, negative)):
|
|
g["dynamic"] = {"class_type": "RuntimeGenerator", "inputs": {}}
|
|
g["stateful"] = {
|
|
"class_type": "PromptManagerText",
|
|
"inputs": {"text": "looks literal"},
|
|
}
|
|
self.cap = self.capture.begin(
|
|
RunStart(SESSION, SESSION, 1, JOB, EPOCH, 0), 3, g, None
|
|
)
|
|
return self.cap
|
|
|
|
def observe(self, event, executor=None, **data):
|
|
host.observe_cached_context(
|
|
executor or self.executor, event, {"prompt_id": JOB, **data}
|
|
)
|
|
|
|
def seal(self):
|
|
sealed = self.capture.seal(self.cap)
|
|
if sealed.lease:
|
|
self.addCleanup(sealed.lease.release)
|
|
return sealed.bundle.contexts[0]
|
|
|
|
def test_original_return_exception_and_wrapper_cleanup_preserved(self):
|
|
cls = self.execution.PromptExecutor
|
|
original = cls.add_message
|
|
|
|
def earlier(executor, *args, **kwargs):
|
|
self.calls.append("earlier")
|
|
return original(executor, *args, **kwargs)
|
|
|
|
cls.add_message = earlier
|
|
seen = []
|
|
stop_a = host.subscribe_executor_messages("a", lambda *args: seen.append(args))
|
|
stop_b = host.subscribe_executor_messages("b", lambda *args: seen.append(args))
|
|
wrapped = cls.add_message
|
|
|
|
def later(executor, *args, **kwargs):
|
|
self.calls.append("later")
|
|
return wrapped(executor, *args, **kwargs)
|
|
|
|
cls.add_message = later
|
|
data = {"prompt_id": JOB}
|
|
self.assertEqual(
|
|
self.executor.add_message("ok", data, broadcast=False), "original-return"
|
|
)
|
|
self.assertEqual(len(seen), 2)
|
|
self.assertIs(seen[0][0], self.executor)
|
|
self.assertIs(seen[0][1][1], data)
|
|
with self.assertRaisesRegex(RuntimeError, "original"):
|
|
self.executor.add_message("raise", data, False)
|
|
self.assertEqual(len(seen), 2)
|
|
stop_a()
|
|
self.executor.add_message("ok", data, False)
|
|
self.assertEqual(len(seen), 3)
|
|
stop_b()
|
|
self.assertIs(cls.add_message, later)
|
|
self.executor.add_message("ok", data, False)
|
|
self.assertEqual(len(seen), 3)
|
|
stop_c = host.subscribe_executor_messages("c", lambda *args: seen.append(args))
|
|
self.executor.add_message("ok", data, False)
|
|
self.assertEqual(len(seen), 4)
|
|
stop_c()
|
|
|
|
def test_cache_reads_only_local_string_slots(self):
|
|
for mode in ("HierarchicalCache", "LRUCache", "RAMPressureCache"):
|
|
for kind, values in (
|
|
("PromptManager", [object(), ["cached"]]),
|
|
("PromptManagerFocused", [object(), ["cached"]]),
|
|
("PromptManagerSplit", [object(), ["cached"]]),
|
|
("PromptManagerText", [["cached"]]),
|
|
("PromptManagerPair", [object(), object(), ["cached"], [""]]),
|
|
):
|
|
with self.subTest(mode=mode, kind=kind):
|
|
self.begin(kind)
|
|
cache = getattr(self.caching, mode)()
|
|
entry = Entry({"secret": object()}, values)
|
|
cache.entries["p"] = entry
|
|
self.executor.caches.outputs = cache
|
|
self.observe("execution_start")
|
|
self.observe("execution_cached", nodes=["p"])
|
|
result = self.seal()
|
|
self.assertEqual(result.positive.effective.value, "cached")
|
|
self.assertEqual(
|
|
result.positive.effective.source, "host_cache_output"
|
|
)
|
|
if kind == "PromptManagerPair":
|
|
self.assertEqual(result.negative.effective.value, "")
|
|
self.assertIs(cache.entries["p"], entry)
|
|
self.assertEqual(set(cache.__dict__), {"entries"})
|
|
self.assertTrue(all(call[0] == "local" for call in self.calls))
|
|
|
|
def test_changed_trigger_configuration_keeps_cached_effective_text(self):
|
|
self.begin()
|
|
self.executor.caches.outputs.entries["p"] = Entry(
|
|
None, [object(), ["old injected text"]]
|
|
)
|
|
self.observe("execution_start")
|
|
self.observe("execution_cached", nodes=["p"])
|
|
result = self.seal().positive
|
|
self.assertEqual(result.effective.value, "old injected text")
|
|
self.assertEqual(result.raw.value, " raw ")
|
|
self.assertEqual(result.prepend.value, " pre ")
|
|
self.assertEqual(result.append.value, " post ")
|
|
self.assertEqual(result.raw.provenance, "inferred")
|
|
|
|
def test_dynamic_cached_raw_is_unknown(self):
|
|
for dynamic in (["dynamic", 0], ["stateful", 0]):
|
|
for field in ("raw", "prepend"):
|
|
with self.subTest(dynamic=dynamic, field=field):
|
|
self.begin(**{field: dynamic})
|
|
self.executor.caches.outputs.entries["p"] = Entry(
|
|
None, [object(), ["cached"]]
|
|
)
|
|
self.observe("execution_start")
|
|
self.observe("execution_cached", nodes=["p"])
|
|
result = self.seal().positive
|
|
self.assertEqual(result.effective.value, "cached")
|
|
self.assertIsNone(getattr(result, field).value)
|
|
self.assertEqual(result.append.value, " post ")
|
|
if field == "raw":
|
|
self.assertEqual(result.prepend.value, " pre ")
|
|
else:
|
|
self.assertEqual(result.raw.value, " raw ")
|
|
|
|
def test_upstream_transform_preserves_final_pm_cached_effective_only(self):
|
|
for field, snapshot_field in (
|
|
("text", "raw"),
|
|
("prepend_text", "prepend"),
|
|
("append_text", "append"),
|
|
):
|
|
for unresolved in (False, True):
|
|
with self.subTest(field=field, unresolved=unresolved):
|
|
g = transformed_input_graph(field, unresolved)
|
|
g["outer"] = {
|
|
"class_type": "StringConcatenate",
|
|
"inputs": {"string_a": ["p", 2], "string_b": " outer"},
|
|
}
|
|
g["clip"] = {
|
|
"class_type": "CLIPTextEncode",
|
|
"inputs": {"text": ["outer", 0]},
|
|
}
|
|
g["s"]["inputs"]["negative"] = ["clip", 0]
|
|
self.cap = self.capture.begin(
|
|
RunStart(SESSION, SESSION, 1, JOB, EPOCH, 0),
|
|
3,
|
|
g,
|
|
None,
|
|
)
|
|
self.executor.caches.outputs.entries["p"] = Entry(
|
|
None, [object(), object(), [" cached injected "], [""]]
|
|
)
|
|
self.observe("execution_start")
|
|
self.observe("execution_cached", nodes=["upstream", "p"])
|
|
result = self.seal()
|
|
self.assertIsNone(result.negative.effective.value)
|
|
side = result.positive
|
|
self.assertEqual(side.effective.value, " cached injected ")
|
|
self.assertEqual(side.effective.provenance, "recorded")
|
|
self.assertEqual(side.effective.source, "host_cache_output")
|
|
for name, expected in (
|
|
("raw", " cat "),
|
|
("prepend", "pre"),
|
|
("append", "post"),
|
|
):
|
|
evidence = getattr(side, name)
|
|
if name == snapshot_field:
|
|
self.assertIsNone(evidence.value)
|
|
self.assertEqual(evidence.provenance, "unknown")
|
|
else:
|
|
self.assertEqual(evidence.value, expected)
|
|
self.assertEqual(evidence.provenance, "inferred")
|
|
|
|
def test_provider_local_reset_eviction_and_unknown_modes_fail_closed(self):
|
|
for mode in (
|
|
"NullCache",
|
|
"unknown",
|
|
"reset",
|
|
"evicted",
|
|
"malformed",
|
|
"provider-local",
|
|
):
|
|
with self.subTest(mode=mode):
|
|
self.begin()
|
|
cache = self.caching.HierarchicalCache()
|
|
if mode == "NullCache":
|
|
cache = self.caching.NullCache()
|
|
elif mode == "unknown":
|
|
cache = types.SimpleNamespace(
|
|
get_local=lambda _: self.fail("unknown cache read")
|
|
)
|
|
elif mode == "malformed":
|
|
cache.entries["p"] = Entry(None, [object(), [object()]])
|
|
elif mode in ("reset", "provider-local"):
|
|
cache.entries["p"] = Entry(
|
|
None, [object(), ["resident provider output"]]
|
|
)
|
|
self.executor.caches.outputs = cache
|
|
self.observe("execution_start")
|
|
if mode == "reset":
|
|
self.executor.caches.outputs = self.caching.HierarchicalCache()
|
|
self.observe("execution_cached", nodes=["p"])
|
|
result = self.seal().positive
|
|
self.assertEqual(
|
|
result.effective.value,
|
|
"resident provider output" if mode == "provider-local" else None,
|
|
)
|
|
|
|
def test_worker_queue_job_executor_and_generation_fences(self):
|
|
for failure in (
|
|
"unbound",
|
|
"thread",
|
|
"queue",
|
|
"job",
|
|
"executor",
|
|
"recursive",
|
|
"generation",
|
|
"no-start",
|
|
):
|
|
with self.subTest(failure=failure):
|
|
self.begin()
|
|
self.executor.server.prompt_queue = self.queue
|
|
self.executor.caches.outputs.entries["p"] = Entry(
|
|
None, [object(), ["cached"]]
|
|
)
|
|
if failure != "no-start":
|
|
self.observe("execution_start")
|
|
target = self.executor
|
|
if failure == "queue":
|
|
self.executor.server.prompt_queue = object()
|
|
if failure in ("executor", "recursive"):
|
|
target = self.execution.PromptExecutor()
|
|
target.server = self.executor.server
|
|
target.caches = self.executor.caches
|
|
if failure == "recursive":
|
|
self.observe("execution_start", target)
|
|
target = self.executor
|
|
data = {
|
|
"prompt_id": "wrong" if failure == "job" else JOB,
|
|
"nodes": ["p"],
|
|
}
|
|
if failure == "generation":
|
|
self.status = replace(self.status, capture_generation=4)
|
|
|
|
def action():
|
|
host.observe_cached_context(target, "execution_cached", data)
|
|
|
|
if failure == "unbound":
|
|
Context().run(action)
|
|
elif failure == "thread":
|
|
thread = threading.Thread(target=action)
|
|
thread.start()
|
|
thread.join(2)
|
|
else:
|
|
action()
|
|
if failure == "generation":
|
|
self.assertIsNone(self.capture.seal(self.cap).bundle)
|
|
self.status = replace(self.status, capture_generation=3)
|
|
else:
|
|
self.assertIsNone(self.seal().positive.effective.value)
|
|
|
|
def test_missing_cache_preserves_known_affixes_when_raw_is_dynamic(self):
|
|
self.begin(raw=["dynamic", 0])
|
|
self.observe("execution_start")
|
|
self.observe("execution_cached", nodes=["p"])
|
|
result = self.seal().positive
|
|
self.assertIsNone(result.raw.value)
|
|
self.assertIsNone(result.effective.value)
|
|
self.assertEqual(result.prepend.value, " pre ")
|
|
self.assertEqual(result.append.value, " post ")
|
|
|
|
def test_cache_entries_tensors_and_executor_are_not_retained(self):
|
|
class Tensor:
|
|
pass
|
|
|
|
tensor = Tensor()
|
|
tensor_ref = weakref.ref(tensor)
|
|
self.begin()
|
|
self.executor.caches.outputs.entries["p"] = Entry(tensor, [tensor, ["cached"]])
|
|
self.observe("execution_start")
|
|
self.observe("execution_cached", nodes=["p"])
|
|
self.executor.caches.outputs.entries.clear()
|
|
executor_ref = weakref.ref(self.executor)
|
|
del tensor
|
|
del self.executor
|
|
gc.collect()
|
|
self.assertIsNone(tensor_ref())
|
|
self.assertIsNone(executor_ref())
|
|
self.assertEqual(self.seal().positive.effective.value, "cached")
|
|
|
|
def test_unknown_and_malformed_cache_payloads_are_bounded(self):
|
|
for values in (["x"] * 65, ["x" * 65537], [object()], ["x", "x"]):
|
|
with self.subTest(count=len(values)):
|
|
self.begin()
|
|
self.executor.caches.outputs.entries["p"] = Entry(
|
|
None, [object(), values]
|
|
)
|
|
self.observe("execution_start")
|
|
self.observe("execution_cached", nodes=["p"])
|
|
result = self.seal().positive
|
|
self.assertIsNone(result.effective.value)
|
|
self.assertEqual(
|
|
result.append.value, " post " if len(values) != 2 else None
|
|
)
|
|
|
|
def test_disposed_subscriptions_do_not_retain_dynamic_executor_classes(self):
|
|
def install_and_remove():
|
|
class TemporaryExecutor:
|
|
def add_message(self, *args, **kwargs):
|
|
return None
|
|
|
|
self.execution.PromptExecutor = TemporaryExecutor
|
|
ref = weakref.ref(TemporaryExecutor)
|
|
stop = host.subscribe_executor_messages("temporary", lambda *args: None)
|
|
stop()
|
|
del self.execution.PromptExecutor
|
|
return ref
|
|
|
|
ref = install_and_remove()
|
|
gc.collect()
|
|
self.assertIsNone(ref())
|
|
|
|
# -- #403: a cached runtime text takes its fields from the reused execution
|
|
|
|
class Receipts:
|
|
"""A fake ``CachedRuns.recorded_fields``: fixed fields, calls recorded."""
|
|
|
|
def __init__(self, fields):
|
|
self.fields, self.calls = fields, []
|
|
|
|
def recorded_fields(self, prompt_id, node_id, values):
|
|
self.calls.append((prompt_id, node_id, values))
|
|
return self.fields
|
|
|
|
def observe_with(self, receipts, outputs, **begin):
|
|
self.begin(**begin)
|
|
self.executor.caches.outputs.entries["p"] = Entry(None, outputs)
|
|
self.observe("execution_start")
|
|
host.observe_cached_context(
|
|
self.executor,
|
|
"execution_cached",
|
|
{"prompt_id": JOB, "nodes": ["p"]},
|
|
receipts=receipts,
|
|
)
|
|
return self.seal()
|
|
|
|
def test_cached_runtime_raw_comes_from_the_matching_receipt(self):
|
|
receipts = self.Receipts([(("LLM raw", " pre ", " post "),)])
|
|
result = self.observe_with(
|
|
receipts, [object(), ["cached"]], raw=["dynamic", 0]
|
|
).positive
|
|
self.assertEqual(receipts.calls, [(JOB, "p", (("cached",),))])
|
|
self.assertEqual(result.raw.value, "LLM raw")
|
|
self.assertEqual(
|
|
(result.raw.provenance, result.raw.source), ("inferred", "graph_semantics")
|
|
)
|
|
self.assertEqual(result.effective.value, "cached")
|
|
self.assertEqual(result.effective.source, "host_cache_output")
|
|
# Fields the graph already knows are never replaced
|
|
self.assertEqual(result.prepend.value, " pre ")
|
|
self.assertEqual(result.prepend.source, "graph_literal")
|
|
self.assertEqual(result.append.value, " post ")
|
|
|
|
def test_cached_runtime_prepend_comes_from_the_matching_receipt(self):
|
|
receipts = self.Receipts([((" raw ", "LLM pre", " post "),)])
|
|
result = self.observe_with(
|
|
receipts, [object(), ["cached"]], prepend=["dynamic", 0]
|
|
).positive
|
|
self.assertEqual(result.prepend.value, "LLM pre")
|
|
self.assertEqual(result.prepend.source, "graph_semantics")
|
|
self.assertEqual(result.raw.value, " raw ")
|
|
self.assertEqual(result.raw.source, "graph_literal")
|
|
|
|
def test_a_receipt_disagreeing_with_a_known_field_fills_nothing(self):
|
|
receipts = self.Receipts([(("LLM raw", "other pre", " post "),)])
|
|
result = self.observe_with(
|
|
receipts, [object(), ["cached"]], raw=["dynamic", 0]
|
|
).positive
|
|
self.assertIsNone(result.raw.value)
|
|
self.assertEqual(result.prepend.value, " pre ")
|
|
|
|
def test_pair_runtime_negative_takes_the_negative_fields(self):
|
|
receipts = self.Receipts(
|
|
[((" raw ", " pre ", " post "), ("LLM negative", "", ""))]
|
|
)
|
|
result = self.observe_with(
|
|
receipts,
|
|
[object(), object(), ["cached positive"], ["LLM negative"]],
|
|
kind="PromptManagerPair",
|
|
negative=["dynamic", 0],
|
|
)
|
|
self.assertEqual(result.negative.raw.value, "LLM negative")
|
|
self.assertEqual(result.negative.raw.source, "graph_semantics")
|
|
self.assertEqual(result.positive.raw.value, " raw ")
|
|
self.assertEqual(result.positive.raw.source, "graph_literal")
|
|
|
|
def test_known_typed_fields_are_never_replaced_by_a_receipt(self):
|
|
receipts = self.Receipts([(("other", "other", "other"),)])
|
|
result = self.observe_with(receipts, [object(), ["cached"]]).positive
|
|
self.assertEqual(
|
|
(result.raw.value, result.prepend.value, result.append.value),
|
|
(" raw ", " pre ", " post "),
|
|
)
|
|
self.assertEqual(result.raw.source, "graph_literal")
|
|
|
|
def test_no_matching_receipt_keeps_the_cached_raw_unknown(self):
|
|
for fields in (None, [None], [], "bad", [(("a", "b"),)], [((1, "", ""),)]):
|
|
with self.subTest(fields=fields):
|
|
result = self.observe_with(
|
|
self.Receipts(fields), [object(), ["cached"]], raw=["dynamic", 0]
|
|
).positive
|
|
self.assertIsNone(result.raw.value)
|
|
self.assertEqual(result.effective.value, "cached")
|
|
|
|
def test_a_failing_receipt_lookup_keeps_the_capture(self):
|
|
class Broken:
|
|
def recorded_fields(self, *args):
|
|
raise RuntimeError("boom")
|
|
|
|
result = self.observe_with(
|
|
Broken(), [object(), ["cached"]], raw=["dynamic", 0]
|
|
).positive
|
|
self.assertIsNone(result.raw.value)
|
|
self.assertEqual(result.effective.value, "cached")
|
|
|
|
def test_pair_slots_take_their_own_receipt_fields(self):
|
|
receipts = self.Receipts(
|
|
[(("LLM positive", " pre ", " post "), ("LLM negative", "", ""))]
|
|
)
|
|
result = self.observe_with(
|
|
receipts,
|
|
[object(), object(), ["cached positive"], ["cached negative"]],
|
|
kind="PromptManagerPair",
|
|
raw=["dynamic", 0],
|
|
)
|
|
self.assertEqual(
|
|
receipts.calls[0], (JOB, "p", (("cached positive",), ("cached negative",)))
|
|
)
|
|
self.assertEqual(result.positive.raw.value, "LLM positive")
|
|
self.assertEqual(result.positive.effective.value, "cached positive")
|
|
# The negative editor is typed: the graph's value stays
|
|
self.assertEqual(result.negative.raw.value, "negative")
|
|
self.assertEqual(result.negative.raw.source, "graph_literal")
|
|
|
|
def test_default_receipts_are_the_process_cached_runs(self):
|
|
from utils import cached_runs
|
|
|
|
receipts = self.Receipts([(("LLM raw", " pre ", " post "),)])
|
|
with patch.object(cached_runs, "CACHED_RUNS", receipts):
|
|
self.begin(raw=["dynamic", 0])
|
|
self.executor.caches.outputs.entries["p"] = Entry(
|
|
None, [object(), ["cached"]]
|
|
)
|
|
self.observe("execution_start")
|
|
self.observe("execution_cached", nodes=["p"])
|
|
self.assertEqual(self.seal().positive.raw.value, "LLM raw")
|