Files
Xyc2016-Comfyui_Fd_Nodes/tests/test_binary_decision_normalizer.py
T
chenjunhong 11eb943642 feat: add FD_BinaryDecisionNormalizer for Doubao VLM binary decisions
Normalize VLM detection output to int 0/1 so easy anythingIndexSwitch never
receives None, while keeping raw VLM results observable via ShowText. Also
wrap Doubao HTTP request/parse/status handling in a single error boundary
that falls back to defaultPrompt on any failure.
2026-08-05 17:56:02 +08:00

165 lines
6.6 KiB
Python

#!/usr/bin/env python3
"""FD_BinaryDecisionNormalizer 与 normalize_binary_decision 离线测试。
任何输入下输出都必须是 int 且值属于 {0, 1},保证下游
easy anythingIndexSwitch 的 index 永远不会是 None 或浮点。
"""
import os
import unittest
from unittest.mock import Mock, patch
# 环境变量须在 config 导入前就位;强制赋值以免其他测试先导入
os.environ["FD_DOUBAO_KEY"] = os.environ.get("FD_DOUBAO_KEY") or "test-key"
os.environ["FD_DOUBAO_URL"] = os.environ.get("FD_DOUBAO_URL") or "https://example.com/v1/chat"
from src.Comfyui_Fd_Nodes.old_fd_nodes import ( # noqa: E402
FD_BinaryDecisionNormalizer,
FD_imgToText_Doubao,
normalize_binary_decision,
)
class NormalizeBinaryDecisionTest(unittest.TestCase):
def assert_binary(self, value, expected=None):
result = normalize_binary_decision(value)
self.assertIs(type(result), int, f"value={value!r} result={result!r}")
self.assertIn(result, (0, 1), f"value={value!r} result={result!r}")
if expected is not None:
self.assertEqual(result, expected, f"value={value!r}")
def test_plain_binary(self):
self.assert_binary("0", 0)
self.assert_binary("1", 1)
def test_int_and_float_inputs(self):
self.assert_binary(0, 0)
self.assert_binary(1, 1)
self.assert_binary(0.0, 0)
self.assert_binary(1.0, 1)
self.assert_binary(True, 1)
self.assert_binary(False, 0)
def test_whitespace_and_newlines(self):
self.assert_binary(" 0 ", 0)
self.assert_binary("\n1\n", 1)
self.assert_binary("\t0\t", 0)
def test_markdown_code_fence(self):
self.assert_binary("```\n0\n```", 0)
self.assert_binary("```text\n1\n```", 1)
self.assert_binary("```\n0", 0)
def test_single_token_explanation(self):
self.assert_binary("无 logo 叠加,结果:0", 0)
self.assert_binary("结论是 1(有水印)", 1)
self.assert_binary("图中无任何后期叠加标识,返回 0。", 0)
def test_empty_and_none(self):
for value in (None, "", " ", "\n\t\n", "```\n```", "```\n \n```"):
result = normalize_binary_decision(value)
self.assertIs(type(result), int)
self.assertIn(result, (0, 1))
def test_default_value_used_on_empty(self):
self.assertEqual(normalize_binary_decision(None, 1), 1)
self.assertEqual(normalize_binary_decision("", 1), 1)
self.assertEqual(normalize_binary_decision(" ", 0), 0)
def test_ambiguous_text_falls_back(self):
for value in ("0 与 1 均存在", "10", "01", "1.0", "0.0", "101"):
result = normalize_binary_decision(value)
self.assertIs(type(result), int)
self.assertIn(result, (0, 1))
self.assertEqual(result, 0)
def test_ambiguous_text_uses_given_default(self):
self.assertEqual(normalize_binary_decision("0 与 1 均存在", 1), 1)
self.assertEqual(normalize_binary_decision("10", 1), 1)
self.assertEqual(normalize_binary_decision("1.0", 1), 1)
self.assertEqual(normalize_binary_decision("2", 1), 1)
def test_non_binary_value_falls_back(self):
for value in ("2", "-1", "0.5", "abc", "yes", "watermark", ["0"]):
result = normalize_binary_decision(value)
self.assertIs(type(result), int)
self.assertIn(result, (0, 1))
def test_default_value_out_of_range_coerced(self):
self.assertEqual(normalize_binary_decision("", 7), 0)
self.assertEqual(normalize_binary_decision(None, -1), 0)
class BinaryDecisionNormalizerNodeTest(unittest.TestCase):
def setUp(self):
self.node = FD_BinaryDecisionNormalizer()
def test_return_type_declared_int(self):
self.assertEqual(FD_BinaryDecisionNormalizer.RETURN_TYPES, ("INT",))
def test_plain_inputs(self):
self.assertEqual(self.node.normalize("0"), (0,))
self.assertEqual(self.node.normalize("1"), (1,))
def test_explanation_and_fence(self):
self.assertEqual(self.node.normalize("检测无任何水印,结论 0"), (0,))
self.assertEqual(self.node.normalize("```\n1\n```"), (1,))
self.assertEqual(self.node.normalize("\n0\n"), (0,))
def test_garbage_uses_default_value(self):
self.assertEqual(self.node.normalize("", 0), (0,))
self.assertEqual(self.node.normalize("10", 0), (0,))
self.assertEqual(self.node.normalize("1.0", 0), (0,))
self.assertEqual(self.node.normalize(None, 1), (1,))
self.assertEqual(self.node.normalize("10", 1), (1,))
self.assertEqual(self.node.normalize("2", 1), (1,))
def test_node_output_always_binary_int(self):
for value in (None, "", "0", "1", "10", "1.0", "```\n0\n```", "结论:0", "abc"):
result = self.node.normalize(value, 0)[0]
self.assertIs(type(result), int)
self.assertIn(result, (0, 1))
class DoubaoNodeErrorBoundaryTest(unittest.TestCase):
def setUp(self):
self.node = FD_imgToText_Doubao()
def _patch_post(self, response):
return patch(
"src.Comfyui_Fd_Nodes.old_fd_nodes.requests.post",
return_value=response,
)
def test_transport_exception_returns_default_prompt(self):
with patch("src.Comfyui_Fd_Nodes.old_fd_nodes.requests.post", side_effect=RuntimeError("connection refused")):
self.assertEqual(self.node.gen("http://img", "p", "fallback"), ("fallback",))
def test_non_json_response_returns_default_prompt(self):
response = Mock()
response.content = b"<html>gateway error</html>"
with self._patch_post(response):
self.assertEqual(self.node.gen("http://img", "p", "fallback"), ("fallback",))
def test_failed_status_returns_default_prompt(self):
response = Mock()
response.content = b'{"status": false, "response": {"Result": "oops"}}'
with self._patch_post(response):
self.assertEqual(self.node.gen("http://img", "p", "fallback"), ("fallback",))
def test_missing_field_returns_default_prompt(self):
response = Mock()
response.content = b'{"status": true}'
with self._patch_post(response):
self.assertEqual(self.node.gen("http://img", "p", "fallback"), ("fallback",))
def test_success_returns_vlm_result(self):
response = Mock()
response.content = b'{"status": true, "response": {"Result": "1"}}'
with self._patch_post(response):
self.assertEqual(self.node.gen("http://img", "p", "fallback"), ("1",))
if __name__ == "__main__":
unittest.main()