feat(history): define exact bounded execution contexts
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
"""Projection must retain declared roles without claiming sampler consumption."""
|
||||
|
||||
import unittest
|
||||
from test_execution_contexts import load_module
|
||||
|
||||
|
||||
def node(kind, **inputs):
|
||||
return {"class_type": kind, "inputs": inputs}
|
||||
|
||||
|
||||
def graph(kind="KSampler", source="PromptManagerPair"):
|
||||
return {
|
||||
"p": node(
|
||||
source,
|
||||
text=" cat ",
|
||||
negative_text="",
|
||||
prepend_text="pre",
|
||||
append_text="post",
|
||||
),
|
||||
"s": node(kind, positive=["p", 0], negative=["p", 1]),
|
||||
}
|
||||
|
||||
|
||||
class ProjectionTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.m = load_module("execution_context_graph")
|
||||
|
||||
def test_projection_preserves_roles(self):
|
||||
for kind in ("KSampler", "KSamplerAdvanced", "SamplerCustom"):
|
||||
g = graph(kind)
|
||||
result = self.m.project_contexts(g, None)
|
||||
self.assertEqual(len(result.recipes), 1)
|
||||
recipe = result.recipes[0]
|
||||
self.assertEqual(recipe.anchor_id, "s")
|
||||
self.assertEqual(recipe.positive.inferred.raw.value, " cat ")
|
||||
self.assertEqual(recipe.positive.inferred.effective.value, "pre cat post")
|
||||
self.assertEqual(recipe.negative.inferred.effective.value, "")
|
||||
self.assertEqual(recipe.positive.sources[0].output_slot, 0)
|
||||
g["s"]["inputs"]["positive"], g["s"]["inputs"]["negative"] = ["p", 1], [
|
||||
"p",
|
||||
0,
|
||||
]
|
||||
crossed = self.m.project_contexts(g, None).recipes[0]
|
||||
self.assertEqual(crossed.positive.inferred.raw.value, "")
|
||||
self.assertEqual(crossed.negative.sources[0].output_slot, 0)
|
||||
for source in ("PromptManager", "PromptManagerFocused", "PromptManagerSplit"):
|
||||
g = graph(source=source)
|
||||
g["clip"] = node("CLIPTextEncode", text=["p", 1])
|
||||
g["s"]["inputs"]["negative"] = ["clip", 0]
|
||||
recipe = self.m.project_contexts(g, None).recipes[0]
|
||||
self.assertEqual(recipe.negative.inferred.raw.value, " cat ")
|
||||
self.assertEqual(recipe.negative.sources[0].output_slot, 1)
|
||||
for source, slot in (
|
||||
("PromptManagerText", 0),
|
||||
("PromptManagerPair", 2),
|
||||
("PromptManagerPair", 3),
|
||||
):
|
||||
g = graph(source=source)
|
||||
g["clip"] = node("CLIPTextEncode", text=["p", slot])
|
||||
g["s"]["inputs"] = {"positive": ["clip", 0], "negative": ["clip", 0]}
|
||||
self.assertEqual(
|
||||
self.m.project_contexts(g, None)
|
||||
.recipes[0]
|
||||
.positive.sources[0]
|
||||
.output_slot,
|
||||
slot,
|
||||
)
|
||||
g = graph()
|
||||
g["p"] = node("CLIPTextEncode", text=["literal", 0])
|
||||
g["literal"] = node("PrimitiveString", value=" exact ")
|
||||
g["s"]["inputs"]["negative"] = ["p", 0]
|
||||
self.assertEqual(
|
||||
self.m.project_contexts(g, None)
|
||||
.recipes[0]
|
||||
.positive.inferred.effective.value,
|
||||
" exact ",
|
||||
)
|
||||
g["p"] = node("UnknownTransform", text="secret")
|
||||
role = self.m.project_contexts(g, None).recipes[0].positive
|
||||
self.assertIsNone(role.inferred)
|
||||
self.assertTrue(role.reason)
|
||||
|
||||
def test_output_selection_and_scalar_isolation(self):
|
||||
g = graph()
|
||||
g["other"] = node("KSampler", positive=["p", 0], negative=["p", 1])
|
||||
g["decode"] = node("VAEDecode", samples=["s", 0])
|
||||
g["save"] = node("SaveImage", images=["decode", 0])
|
||||
result = self.m.project_contexts(g, ["save"])
|
||||
self.assertEqual(tuple(r.anchor_id for r in result.recipes), ("s",))
|
||||
g["p"]["inputs"]["text"] = "changed"
|
||||
self.assertEqual(result.recipes[0].positive.inferred.raw.value, " cat ")
|
||||
self.assertEqual(self.m.project_contexts(g, []).recipes, ())
|
||||
for outputs in (["missing"], [object()], "save"):
|
||||
result = self.m.project_contexts(g, outputs)
|
||||
self.assertEqual(result.recipes, ())
|
||||
self.assertTrue(result.reason)
|
||||
|
||||
def test_projection_limits(self):
|
||||
for count, succeeds in ((500, True), (501, False)):
|
||||
g = graph()
|
||||
g.update({str(i): node("Unused") for i in range(count - 2)})
|
||||
self.assertEqual(bool(self.m.project_contexts(g, None).recipes), succeeds)
|
||||
for count, succeeds in ((16, True), (17, False)):
|
||||
g = {"p": graph()["p"]}
|
||||
g.update({str(i): graph()["s"] for i in range(count)})
|
||||
self.assertEqual(bool(self.m.project_contexts(g, None).recipes), succeeds)
|
||||
for depth, succeeds in ((64, True), (65, False)):
|
||||
g = graph()
|
||||
# The CLIP plus reroutes plus literal give exactly depth visited links.
|
||||
g["p"] = node("CLIPTextEncode", text=["0", 0])
|
||||
for i in range(depth - 2):
|
||||
g[str(i)] = node("Reroute", value=[str(i + 1), 0])
|
||||
g[str(depth - 2)] = node("PrimitiveString", value="known")
|
||||
role = self.m.project_contexts(g, None).recipes[0].positive
|
||||
self.assertEqual(role.inferred is not None, succeeds)
|
||||
g = graph()
|
||||
g["s" * 129] = g.pop("s")
|
||||
self.assertFalse(self.m.project_contexts(g, None).recipes)
|
||||
|
||||
def test_selected_custom_outputs_and_multistage_ancestry(self):
|
||||
g = graph()
|
||||
g["earlier"] = node("SamplerCustom", positive=["p", 0], negative=["p", 1])
|
||||
g["s"]["inputs"]["latent_image"] = ["earlier", 0]
|
||||
g["adapter"] = node("UnknownImageAdapter", source=["s", 0])
|
||||
g["custom"] = node("CustomOutput", images=["adapter", 0])
|
||||
result = self.m.project_contexts(g, ["custom"])
|
||||
self.assertEqual(tuple(r.anchor_id for r in result.recipes), ("s", "earlier"))
|
||||
|
||||
def test_total_resolution_visits_are_bounded(self):
|
||||
# Sixteen recipes each resolving two independent chains of length 16
|
||||
# exceed 500 total visits even though the graph has fewer than 500 nodes.
|
||||
g = {"v": node("PrimitiveString", value="known")}
|
||||
for i in range(15):
|
||||
g[str(i)] = node("Reroute", value=[str(i + 1), 0] if i < 14 else ["v", 0])
|
||||
g["clip"] = node("CLIPTextEncode", text=["0", 0])
|
||||
for i in range(16):
|
||||
g["s" + str(i)] = node(
|
||||
"KSampler", positive=["clip", 0], negative=["clip", 0]
|
||||
)
|
||||
result = self.m.project_contexts(g, None)
|
||||
self.assertTrue(result.reason)
|
||||
self.assertFalse(
|
||||
result.recipes
|
||||
and all(
|
||||
r.positive.inferred is not None and r.negative.inferred is not None
|
||||
for r in result.recipes
|
||||
)
|
||||
)
|
||||
|
||||
def test_known_concatenation_remains_inferred(self):
|
||||
g = graph(source="PromptManagerText")
|
||||
g["join"] = node(
|
||||
"StringConcatenate", string_a=["p", 0], string_b="!", delimiter=""
|
||||
)
|
||||
g["clip"] = node("CLIPTextEncode", text=["join", 0])
|
||||
g["s"]["inputs"] = {"positive": ["clip", 0], "negative": ["clip", 0]}
|
||||
role = self.m.project_contexts(g, None).recipes[0].positive
|
||||
self.assertIsNotNone(role.inferred)
|
||||
self.assertEqual(role.inferred.effective.value, "pre cat post!")
|
||||
self.assertEqual(role.inferred.effective.provenance, "graph-inferred")
|
||||
self.assertEqual(role.sources[0].node_id, "p")
|
||||
self.assertEqual(role.reason, "source-transformed")
|
||||
|
||||
def test_malformed_inputs_do_not_stringify(self):
|
||||
class HostValue:
|
||||
def __str__(self):
|
||||
raise AssertionError("host object stringified")
|
||||
|
||||
for g in (
|
||||
{HostValue(): node("KSampler")},
|
||||
{"s": node(HostValue())},
|
||||
{"s": node("KSampler", positive=HostValue())},
|
||||
):
|
||||
self.m.project_contexts(g, None)
|
||||
Reference in New Issue
Block a user