444 lines
14 KiB
Python
444 lines
14 KiB
Python
"""Product contracts for the model-aware Krea 2 SIGMAS scheduler node."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
import json
|
|
import sys
|
|
from dataclasses import FrozenInstanceError
|
|
from types import SimpleNamespace
|
|
from typing import Any, cast
|
|
|
|
import pytest
|
|
from comfyui_sigmax.core import CompatibilityLevel, ScheduleContractError, SigmaDomain
|
|
from comfyui_sigmax.nodes import model_aware_sigma_scheduler as model_aware_module
|
|
from comfyui_sigmax.nodes.krea2_sigma_scheduler import build_krea2_sigma_schedule
|
|
from comfyui_sigmax.nodes.model_aware_sigma_scheduler import (
|
|
MODEL_AWARE_SIGMA_NODE_ID,
|
|
MODEL_AWARE_SIGMA_NODE_SCHEMA_ID,
|
|
ModelAwareScheduleError,
|
|
ModelAwareSigmaNodeResult,
|
|
ModelAwareSigmaScheduler,
|
|
ModelFamilyProbe,
|
|
build_model_aware_sigma_schedule,
|
|
probe_model_family,
|
|
)
|
|
from comfyui_sigmax.profiles import HostCapabilities
|
|
|
|
|
|
def _krea2_model() -> object:
|
|
krea2 = type("Krea2", (), {})
|
|
return SimpleNamespace(model=krea2())
|
|
|
|
|
|
def _info(result: ModelAwareSigmaNodeResult) -> dict[str, Any]:
|
|
decoded = json.loads(result.schedule_info_json)
|
|
assert isinstance(decoded, dict)
|
|
return decoded
|
|
|
|
|
|
def _arguments(**changes: object) -> dict[str, object]:
|
|
values: dict[str, object] = {
|
|
"model": _krea2_model(),
|
|
"variant": "Turbo",
|
|
"steps": 8,
|
|
"width": 1024,
|
|
"height": 1024,
|
|
"strict_official": True,
|
|
"start_step": 0,
|
|
"end_step": -1,
|
|
}
|
|
values.update(changes)
|
|
return values
|
|
|
|
|
|
def test_node_declares_model_aware_legacy_current_schema() -> None:
|
|
inputs = ModelAwareSigmaScheduler.INPUT_TYPES()
|
|
|
|
assert MODEL_AWARE_SIGMA_NODE_ID == "Sigmax.ModelAwareSigmaScheduler"
|
|
assert MODEL_AWARE_SIGMA_NODE_SCHEMA_ID == "sigmax.model-aware-sigma-node/1"
|
|
assert ModelAwareSigmaScheduler.RETURN_TYPES == ("SIGMAS", "STRING")
|
|
assert ModelAwareSigmaScheduler.RETURN_NAMES == ("sigmas", "schedule_info")
|
|
assert ModelAwareSigmaScheduler.FUNCTION == "build"
|
|
assert ModelAwareSigmaScheduler.CATEGORY == "Sigmax/scheduling"
|
|
assert ModelAwareSigmaScheduler.OUTPUT_NODE is False
|
|
assert inputs["required"]["model"] == ("MODEL",)
|
|
assert inputs["required"]["variant"][0] == ("Auto", "Turbo", "RAW")
|
|
assert set(inputs["required"]) == {
|
|
"model",
|
|
"variant",
|
|
"steps",
|
|
"width",
|
|
"height",
|
|
"strict_official",
|
|
"start_step",
|
|
"end_step",
|
|
}
|
|
assert inputs == ModelAwareSigmaScheduler.INPUT_TYPES()
|
|
assert inputs is not ModelAwareSigmaScheduler.INPUT_TYPES()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("model", "expected_signals"),
|
|
(
|
|
(_krea2_model(), ("model_class:krea2",)),
|
|
(
|
|
SimpleNamespace(
|
|
model=SimpleNamespace(
|
|
model_config=type("Krea2", (), {"unet_config": {}})(),
|
|
)
|
|
),
|
|
("model_config_class:krea2",),
|
|
),
|
|
(
|
|
SimpleNamespace(
|
|
model=SimpleNamespace(
|
|
model_config=SimpleNamespace(unet_config={"image_model": "krea2"})
|
|
)
|
|
),
|
|
("unet_config.image_model:krea2",),
|
|
),
|
|
),
|
|
)
|
|
def test_probe_accepts_only_bounded_public_krea2_family_signals(
|
|
model: object,
|
|
expected_signals: tuple[str, ...],
|
|
) -> None:
|
|
probe = probe_model_family(model)
|
|
|
|
assert probe == ModelFamilyProbe(
|
|
schema_id="sigmax.model-family-probe/1",
|
|
family="krea2",
|
|
supported=True,
|
|
signals=expected_signals,
|
|
reason_code="model.family_krea2",
|
|
)
|
|
|
|
|
|
def test_probe_does_not_invoke_foreign_methods_or_leak_object_representations() -> None:
|
|
calls: list[str] = []
|
|
|
|
class Foreign:
|
|
def get_model_object(self, name: str) -> object:
|
|
calls.append(name)
|
|
return object()
|
|
|
|
def __repr__(self) -> str:
|
|
raise AssertionError("foreign object representation must not be evaluated")
|
|
|
|
probe = probe_model_family(SimpleNamespace(model=Foreign()))
|
|
|
|
assert calls == []
|
|
assert probe.supported is False
|
|
assert probe.signals == ()
|
|
assert "Foreign" not in json.dumps(
|
|
{
|
|
"family": probe.family,
|
|
"reason_code": probe.reason_code,
|
|
"signals": list(probe.signals),
|
|
}
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("descriptor_kind", ("property", "method"))
|
|
def test_probe_ignores_executable_model_descriptors(descriptor_kind: str) -> None:
|
|
calls: list[str] = []
|
|
|
|
if descriptor_kind == "property":
|
|
|
|
class PropertyDescriptorModel:
|
|
@property
|
|
def model(self) -> object:
|
|
calls.append("property")
|
|
return _krea2_model()
|
|
|
|
descriptor_model: object = PropertyDescriptorModel()
|
|
else:
|
|
|
|
class MethodDescriptorModel:
|
|
def model(self) -> object:
|
|
calls.append("method")
|
|
return _krea2_model()
|
|
|
|
descriptor_model = MethodDescriptorModel()
|
|
|
|
probe = probe_model_family(descriptor_model)
|
|
|
|
assert calls == []
|
|
assert probe.reason_code == "model.family_unsupported"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"model",
|
|
(
|
|
object(),
|
|
SimpleNamespace(model=None),
|
|
SimpleNamespace(model=SimpleNamespace(model_config=SimpleNamespace())),
|
|
SimpleNamespace(model=SimpleNamespace(model_config=SimpleNamespace(unet_config=[]))),
|
|
),
|
|
)
|
|
def test_malformed_or_unsupported_models_fail_closed(model: object) -> None:
|
|
with pytest.raises(ModelAwareScheduleError) as captured:
|
|
build_model_aware_sigma_schedule(**_arguments(model=model))
|
|
|
|
assert captured.value.reason_code in {
|
|
"model.family_unsupported",
|
|
"model.probe_invalid",
|
|
}
|
|
assert "C:\\" not in str(captured.value)
|
|
|
|
|
|
def test_auto_mode_exposes_family_only_ambiguity_without_returning_sigmas() -> None:
|
|
with pytest.raises(ModelAwareScheduleError) as captured:
|
|
build_model_aware_sigma_schedule(**_arguments(variant="Auto"))
|
|
|
|
error = captured.value
|
|
decision = json.loads(error.decision_json)
|
|
assert error.reason_code == "model.identity_ambiguous"
|
|
assert decision["schema"] == MODEL_AWARE_SIGMA_NODE_SCHEMA_ID
|
|
assert decision["model_probe"]["family"] == "krea2"
|
|
assert decision["variant_resolution"]["status"] == "ambiguous"
|
|
assert decision["variant_resolution"]["reason_codes"] == [
|
|
"insufficient_variant_evidence",
|
|
"krea2_family_does_not_identify_variant",
|
|
"model_class.krea2_family",
|
|
]
|
|
assert "sigmas" not in decision
|
|
assert "fallback" not in decision
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("variant", "steps"),
|
|
(("Turbo", 8), ("RAW", 52)),
|
|
)
|
|
def test_explicit_variant_uses_exact_profile_capability_gate_and_m4_01_schedule(
|
|
variant: str,
|
|
steps: int,
|
|
) -> None:
|
|
result = build_model_aware_sigma_schedule(**_arguments(variant=variant, steps=steps))
|
|
baseline = build_krea2_sigma_schedule(
|
|
variant=variant,
|
|
steps=steps,
|
|
width=1024,
|
|
height=1024,
|
|
strict_official=True,
|
|
start_step=0,
|
|
end_step=-1,
|
|
)
|
|
info = _info(result)
|
|
capability = info["capability_decision"]
|
|
|
|
assert result.domain is SigmaDomain.UNIT_FLOW
|
|
assert result.sigmas == baseline.sigmas
|
|
assert result.selected_variant == variant.casefold()
|
|
assert capability["level"] == CompatibilityLevel.ALLOW.value
|
|
assert capability["reason_codes"] == ["core.compatible"]
|
|
assert capability["model_identity"]["decisive_source"] == "explicit_selection"
|
|
assert capability["model_identity"]["status"] == "confirmed"
|
|
assert capability["profile_key"] == (
|
|
"krea2.turbo.official@1" if variant == "Turbo" else "krea2.raw.official@1"
|
|
)
|
|
assert capability["profile_fingerprint"].startswith("sha256:")
|
|
assert info["profile"]["evidence"] == "official"
|
|
assert info["profile"]["key"] == capability["profile_key"]
|
|
assert info["host_evidence"] == {
|
|
"kind": "static_contract",
|
|
"host_id": "comfyui",
|
|
"host_version": "0.29.0",
|
|
"host_revision": "e651b7bef55a5376343dcb1c0edb79f0142c985e", # pragma: allowlist secret
|
|
}
|
|
assert info["schedule"] == json.loads(baseline.schedule_info_json)
|
|
|
|
|
|
def test_non_official_recipe_never_upgrades_profile_or_schedule_evidence() -> None:
|
|
result = build_model_aware_sigma_schedule(
|
|
**_arguments(variant="Turbo", steps=12, strict_official=False)
|
|
)
|
|
info = _info(result)
|
|
|
|
assert info["profile"]["evidence"] == "official"
|
|
assert info["schedule"]["profile"]["evidence"] == "modified"
|
|
assert info["schedule"]["profile"]["recipe"] == "krea2.turbo.modified-12"
|
|
|
|
|
|
def test_model_aware_information_is_deterministic_and_result_is_immutable() -> None:
|
|
first = build_model_aware_sigma_schedule(**_arguments())
|
|
second = build_model_aware_sigma_schedule(**_arguments())
|
|
|
|
assert first == second
|
|
assert first.schedule_info_json == second.schedule_info_json
|
|
with pytest.raises(FrozenInstanceError):
|
|
first.sigmas = () # type: ignore[misc]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"changes",
|
|
(
|
|
{"schema_id": "other"},
|
|
{"family": "other"},
|
|
{"supported": cast(Any, 1)},
|
|
{"family": None, "supported": True},
|
|
{"signals": cast(Any, [])},
|
|
{"signals": ("",)},
|
|
{"signals": ("z", "a")},
|
|
{"signals": ("a", "a")},
|
|
{"reason_code": "invalid"},
|
|
{"reason_code": cast(Any, 1)},
|
|
),
|
|
)
|
|
def test_model_family_probe_contract_rejects_invalid_values(
|
|
changes: dict[str, object],
|
|
) -> None:
|
|
arguments: dict[str, object] = {
|
|
"schema_id": "sigmax.model-family-probe/1",
|
|
"family": "krea2",
|
|
"supported": True,
|
|
"signals": ("model_class:krea2",),
|
|
"reason_code": "model.family_krea2",
|
|
}
|
|
arguments.update(changes)
|
|
|
|
with pytest.raises(ScheduleContractError):
|
|
ModelFamilyProbe(**cast(Any, arguments))
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"arguments",
|
|
(
|
|
{"reason_code": "invalid", "action": "fix", "decision_json": "{}"},
|
|
{"reason_code": cast(Any, 1), "action": "fix", "decision_json": "{}"},
|
|
{"reason_code": "model.test", "action": "", "decision_json": "{}"},
|
|
{"reason_code": "model.test", "action": cast(Any, 1), "decision_json": "{}"},
|
|
{"reason_code": "model.test", "action": "fix", "decision_json": ""},
|
|
{"reason_code": "model.test", "action": "fix", "decision_json": cast(Any, 1)},
|
|
),
|
|
)
|
|
def test_model_aware_error_contract_rejects_invalid_values(
|
|
arguments: dict[str, object],
|
|
) -> None:
|
|
with pytest.raises(ScheduleContractError):
|
|
ModelAwareScheduleError(**cast(Any, arguments))
|
|
|
|
|
|
def test_information_projection_rejects_non_json_values() -> None:
|
|
with pytest.raises(ScheduleContractError, match="canonical JSON"):
|
|
model_aware_module._canonical_json({"invalid": object()})
|
|
|
|
|
|
def test_rejected_capability_decision_stops_before_schedule_construction(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
original_host = model_aware_module._static_host_capabilities()
|
|
rejected_host = HostCapabilities(
|
|
evidence_version=original_host.evidence_version,
|
|
host_id=original_host.host_id,
|
|
host_version=original_host.host_version,
|
|
host_revision=original_host.host_revision,
|
|
capabilities=(),
|
|
)
|
|
monkeypatch.setattr(
|
|
model_aware_module,
|
|
"_static_host_capabilities",
|
|
lambda: rejected_host,
|
|
)
|
|
|
|
with pytest.raises(ModelAwareScheduleError) as captured:
|
|
build_model_aware_sigma_schedule(**_arguments())
|
|
|
|
decision = json.loads(captured.value.decision_json)
|
|
assert captured.value.reason_code == "host.capability_missing"
|
|
assert decision["capability_decision"]["level"] == "reject"
|
|
assert decision["capability_decision"]["reason_codes"] == ["host.capability_missing"]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"changes",
|
|
(
|
|
{"variant": "unknown"},
|
|
{"variant": 1},
|
|
{"model": None},
|
|
),
|
|
)
|
|
def test_invalid_public_inputs_fail_before_tensor_conversion(
|
|
changes: dict[str, object],
|
|
) -> None:
|
|
with pytest.raises((ModelAwareScheduleError, ScheduleContractError)):
|
|
build_model_aware_sigma_schedule(**_arguments(**changes))
|
|
|
|
|
|
def test_runtime_node_converts_only_after_model_and_capability_resolution(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
calls: list[tuple[float, ...]] = []
|
|
|
|
def float_tensor(values: tuple[float, ...]) -> object:
|
|
calls.append(tuple(values))
|
|
return SimpleNamespace(values=tuple(values), device="cpu")
|
|
|
|
monkeypatch.setitem(sys.modules, "torch", SimpleNamespace(FloatTensor=float_tensor))
|
|
output = ModelAwareSigmaScheduler().build(**_arguments())
|
|
|
|
tensor = cast(SimpleNamespace, output[0])
|
|
assert tensor.device == "cpu"
|
|
assert calls == [tensor.values]
|
|
assert json.loads(output[1])["schema"] == MODEL_AWARE_SIGMA_NODE_SCHEMA_ID
|
|
|
|
|
|
def test_auto_rejection_occurs_before_torch_import(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
imports: list[str] = []
|
|
|
|
def forbidden_import(name: str) -> object:
|
|
imports.append(name)
|
|
raise AssertionError("Torch must not be imported for a rejected decision")
|
|
|
|
monkeypatch.setattr(importlib, "import_module", forbidden_import)
|
|
with pytest.raises(ModelAwareScheduleError):
|
|
ModelAwareSigmaScheduler().build(**_arguments(variant="Auto"))
|
|
|
|
assert imports == []
|
|
|
|
|
|
@pytest.mark.parametrize("failure", ("missing_module", "missing_float_tensor"))
|
|
def test_runtime_node_fails_actionably_without_torch_contract(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
failure: str,
|
|
) -> None:
|
|
if failure == "missing_module":
|
|
|
|
def missing_module(name: str) -> object:
|
|
raise ImportError(name)
|
|
|
|
monkeypatch.setattr(importlib, "import_module", missing_module)
|
|
else:
|
|
monkeypatch.setattr(importlib, "import_module", lambda name: SimpleNamespace())
|
|
|
|
with pytest.raises(RuntimeError, match="requires Torch FloatTensor"):
|
|
ModelAwareSigmaScheduler().build(**_arguments())
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"changes",
|
|
(
|
|
{"selected_variant": "other"},
|
|
{"domain": SigmaDomain.DISCRETE_TRAINING_INDEX},
|
|
{"sigmas": cast(Any, [1.0, 0.0])},
|
|
{"sigmas": (0.0,)},
|
|
{"schedule_info_json": ""},
|
|
{"schedule_info_json": cast(Any, 1)},
|
|
),
|
|
)
|
|
def test_model_aware_result_contract_rejects_invalid_values(
|
|
changes: dict[str, object],
|
|
) -> None:
|
|
arguments: dict[str, object] = {
|
|
"selected_variant": "turbo",
|
|
"domain": SigmaDomain.UNIT_FLOW,
|
|
"sigmas": (1.0, 0.0),
|
|
"schedule_info_json": "{}",
|
|
}
|
|
arguments.update(changes)
|
|
with pytest.raises(ScheduleContractError):
|
|
ModelAwareSigmaNodeResult(**cast(Any, arguments))
|