83 lines
3.3 KiB
Python
83 lines
3.3 KiB
Python
"""Schedule-patch decision hygiene (v2.16.0, plan S5).
|
|
|
|
_should_patch_schedule must resolve the sampling through the patcher
|
|
(patch -> backup -> live) so a leaked *ModelSamplingFlux from a previous run
|
|
cannot flip the decision. Characterization: clean-state outcomes are pinned;
|
|
history-independence: leak/backup states give the same answer as clean.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
import types
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
|
from comfy import model_sampling as comfy_ms # noqa: E402
|
|
from src.patch_utils import _should_patch_schedule # noqa: E402
|
|
|
|
|
|
class _Patcher:
|
|
def __init__(self, live, patches=None, backup=None):
|
|
self.model = types.SimpleNamespace(model_sampling=live)
|
|
self.object_patches = dict(patches or {})
|
|
self.object_patches_backup = dict(backup or {})
|
|
|
|
def get_model_object(self, name):
|
|
if name in self.object_patches:
|
|
return self.object_patches[name]
|
|
if name in self.object_patches_backup:
|
|
return self.object_patches_backup[name]
|
|
return getattr(self.model, name)
|
|
|
|
|
|
def _ms(name, *bases):
|
|
return type(name, bases or (object,), {})()
|
|
|
|
|
|
FluxLike = comfy_ms.ModelSamplingFlux
|
|
ContFlow = type("ModelSamplingContinuousFlow", (), {})
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestShouldPatchSchedule:
|
|
@pytest.mark.parametrize("flags", [(False, False), (True, False),
|
|
(False, True), (True, True)])
|
|
def test_characterization_flux_sampling(self, flags):
|
|
# Clean FLUX-style sampling: decision follows the isinstance only.
|
|
patcher = _Patcher(_ms("MS", FluxLike))
|
|
assert _should_patch_schedule(patcher, *flags) is True
|
|
|
|
def test_characterization_continuous_flow_not_patched(self):
|
|
patcher = _Patcher(_ms("MS", ContFlow))
|
|
assert _should_patch_schedule(patcher, False, False) is False
|
|
|
|
def test_characterization_qwen_and_zimage_force_patch(self):
|
|
patcher = _Patcher(_ms("MS", ContFlow))
|
|
assert _should_patch_schedule(patcher, True, False) is True
|
|
assert _should_patch_schedule(patcher, False, True) is True
|
|
|
|
def test_leak_does_not_flip_continuous_flow_decision(self):
|
|
# A leaked Flux-style sampling on the live attr (empty backup, no
|
|
# patch) is the documented KSampler-consistent residual: it IS what
|
|
# resolution returns here, so the decision follows it — pinned so a
|
|
# future change of this trade-off is explicit.
|
|
patcher = _Patcher(_ms("Leaked", FluxLike))
|
|
assert _should_patch_schedule(patcher, False, False) is True
|
|
|
|
def test_own_patch_decides_over_leaked_live(self):
|
|
# Graph carries a Flux-style patch over a continuous-flow live attr.
|
|
patcher = _Patcher(_ms("Live", ContFlow),
|
|
patches={"model_sampling": _ms("P", FluxLike)})
|
|
assert _should_patch_schedule(patcher, False, False) is True
|
|
|
|
def test_backup_original_decides_over_leaked_live(self):
|
|
# Unpatched patcher whose lineage backup holds the ORIGINAL
|
|
# continuous-flow sampler: the leak must not flip the decision.
|
|
patcher = _Patcher(_ms("Leaked", FluxLike),
|
|
backup={"model_sampling": _ms("Orig", ContFlow)})
|
|
assert _should_patch_schedule(patcher, False, False) is False
|