Files
DumiFlex-ComfyUI-Wildcard-P…/tests/engine/test_debug_explain.py
T
Claude 4d178e2b7e feat(debug): record per-module run detail for WP Debug
Canvas Context runs now set an explain flag. With it, each trace row carries
the module's display name and a handler detail dict: which derivation branch
fired, with every test's result and the value it read; a wildcard's option,
odds, pool and the constraints that re-weighted it; a constraint's reach and
rule counts. Warnings a module raised name it as their owner, and trace rows,
warnings and nested @{} picks are tagged with the Context node's graph id.
WP Debug forwards the nested-pick log, node seeds, internal flags, rolled
axes, constraint hits and multi-pick items.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wwo837fQ1tm6RacaQGYom1
2026-09-29 14:00:48 +00:00

167 lines
6.7 KiB
Python

"""Explain mode — the per-module detail the WP Debug node shows.
A run opts in with ``ctx["__wp_explain__"] = True`` (the canvas Context node
does). Each trace row then carries the module's display name and a ``detail``
dict from its handler, and warnings raised while a module ran name it as
their owner. Without the flag the trace is unchanged.
"""
from __future__ import annotations
from engine.pipeline import PipelineEngine
def _wildcard(uid, binding, values, name="", **instance):
return {
"type": "wildcard", "id": uid, "_uid": uid, "name": name,
"payload": {
"var_binding": binding,
"options": [
{"id": v, "value": v, "weight": w, "sub_categories": []}
for v, w in values
],
},
"instance": {"variable_binding": binding, **instance},
}
def _t(var, op="equals", value=""):
return {"var": var, "op": op, "value": value}
def _derivation(rules, uid="der00001", instance=None):
return {
"type": "derivation", "id": uid, "_uid": uid, "meta": {"name": "Lighting"},
"payload": {"rules": rules}, "instance": instance or {},
}
def _explain_run(modules, seed=0, **ctx_vars):
ctx = {"__wp_explain__": True, **ctx_vars}
return PipelineEngine().run(modules, ctx=ctx, seed=seed)
def _row(ctx, uid):
return next(r for r in ctx["__wp_trace__"] if r.get("_uid") == uid)
def test_trace_is_unchanged_without_the_flag():
ctx = PipelineEngine().run([_wildcard("w1", "mood", [("calm", 1)], name="Mood")], seed=1)
row = _row(ctx, "w1")
assert "detail" not in row and "name" not in row
assert "__wp_module_detail__" not in ctx
def test_trace_rows_carry_name_and_detail():
ctx = _explain_run([_wildcard("w1", "mood", [("calm", 1), ("tense", 3)], name="Mood")])
row = _row(ctx, "w1")
assert row["name"] == "Mood"
d = row["detail"]
assert d["option_id"] == ctx["mood"]
assert d["pool"] == 2 and d["live"] == 2
assert d["chance"] == (0.25 if ctx["mood"] == "calm" else 0.75)
assert "__wp_module_detail__" not in ctx
def test_name_falls_back_to_library_meta():
ctx = _explain_run([_derivation([])])
assert _row(ctx, "der00001")["name"] == "Lighting"
def test_multi_pick_lists_every_option():
w = _wildcard("w1", "props", [("a", 1), ("b", 1), ("c", 1)], pick_min=2, pick_max=2)
d = _row(_explain_run([w]), "w1")["detail"]
assert len(d["option_ids"]) == 2 and d["range"] == [2, 2]
def test_derivation_reports_the_fired_branch_and_each_test():
rule = {
"id": "r1",
"branches": [
{"condition": _t("time", value="day"),
"action": {"target_var": "light", "mode": "replace", "value": "sun"}},
{"condition": {"match": "all", "conditions": [
_t("time", value="night"),
{"match": "any", "conditions": [_t("sky", value="rain"), _t("sky", value="fog")]},
]},
"action": {"target_var": "light", "mode": "replace", "value": "lamps"}},
{"condition": _t("time", op="exists"),
"action": {"target_var": "light", "mode": "replace", "value": "never"}},
],
"else": {"action": {"target_var": "light", "mode": "replace", "value": "dim"}},
}
ctx = _explain_run([_derivation([rule])], time="night", sky="fog")
assert ctx["light"] == "lamps"
(rd,) = _row(ctx, "der00001")["detail"]["rules"]
assert rd["fired"] == 1 and rd["has_else"] is True
# Branches after the one that fired are never evaluated.
assert [b["index"] for b in rd["branches"]] == [0, 1]
assert rd["branches"][0]["matched"] is False
assert rd["branches"][0]["condition"] == {
"var": "time", "op": "equals", "value": "day", "actual": "night", "result": False,
}
group = rd["branches"][1]["condition"]
assert group["match"] == "all" and group["result"] is True
inner = group["conditions"][1]
assert inner["match"] == "any"
# Every member of a group is evaluated, even past the first match.
assert [c["result"] for c in inner["conditions"]] == [False, True]
assert rd["action"] == {
"target": "light", "mode": "replace", "value": "lamps", "result": "lamps",
}
def _set(var, value):
return {"target_var": var, "mode": "replace", "value": value}
def test_derivation_else_disabled_and_unset_reads():
rules = [
{"id": "r1", "branches": [{"condition": _t("missing", value="x"),
"action": {"target_var": "a", "mode": "replace", "value": "1"}}],
"else": {"action": {"target_var": "a", "mode": "replace", "value": "fallback"}}},
{"id": "r2", "branches": [{"condition": _t("a", value="fallback"),
"action": _set("b", "2")}]},
{"id": "r3", "branches": [{"condition": _t("a", op="exists"),
"action": _set("c", "3")}]},
]
ctx = _explain_run([_derivation(rules, instance={"disabled_rule_ids": ["r3"]})])
r1, r2, r3 = _row(ctx, "der00001")["detail"]["rules"]
assert r1["fired"] == "else" and r1["branches"][0]["condition"]["actual"] is None
# r2 sees r1's write — the explanation is taken when the rule runs.
assert r2["fired"] == 0 and r2["branches"][0]["condition"]["actual"] == "fallback"
assert r3["disabled"] is True and r3["fired"] is None
def test_constraint_detail_and_pick_attribution():
con = {
"type": "constraint", "id": "con00001", "_uid": "con00001",
"payload": {
"source_wildcard_id": "src00001", "target_wildcard_id": "tgt00001",
"matrix": {},
"exceptions": [{"source_value": "maid", "target_value": "apron",
"mode": "only", "factor": 1.0}],
"target_select": {"mode": "first"},
},
"instance": {},
}
ctx = _explain_run([
_wildcard("src00001", "role", [("maid", 1)]),
con,
_wildcard("tgt00001", "outfit", [("apron", 1), ("jacket", 1)]),
])
cd = _row(ctx, "con00001")["detail"]
assert cd == {"uid": "con00001", "reach": {"mode": "first"}, "cells": 0,
"exceptions": 1, "only": True}
td = _row(ctx, "tgt00001")["detail"]
assert td["live"] == 1 and td["chance"] == 1.0
assert td["constraints"] == [{"id": "con00001", "uid": "con00001", "name": "",
"source": "src00001", "source_value": "maid"}]
def test_warnings_name_the_module_that_raised_them():
w = _wildcard("w1", "look", [("@{deadbeef#hat} on", 1)], name="Look")
ctx = _explain_run([w])
(warn,) = [x for x in ctx["__wp_warnings__"] if x["type"] == "unknown_ref"]
assert warn["owner_uid"] == "w1" and warn["owner_id"] == "w1"
assert warn["detail"]["name"] == "hat"