mirror of
https://github.com/rookiestar28/ComfyUI-OpenClaw.git
synced 2026-08-14 00:48:07 +00:00
159 lines
5.9 KiB
Python
159 lines
5.9 KiB
Python
import json
|
|
import os
|
|
import sys
|
|
import unittest
|
|
|
|
sys.path.append(os.getcwd())
|
|
|
|
from services.llm_output import (
|
|
extract_json_object,
|
|
filter_allowed_keys,
|
|
sanitize_list_to_string,
|
|
sanitize_string,
|
|
)
|
|
|
|
|
|
class TestExtractJsonObject(unittest.TestCase):
|
|
|
|
def test_plain_json(self):
|
|
"""Test extraction from plain JSON."""
|
|
text = '{"key": "value", "num": 42}'
|
|
result = extract_json_object(text)
|
|
self.assertEqual(result, {"key": "value", "num": 42})
|
|
|
|
def test_markdown_fence(self):
|
|
"""Test extraction from markdown code fence."""
|
|
text = 'Here is the result:\n```json\n{"a": 1}\n```\nDone.'
|
|
result = extract_json_object(text)
|
|
self.assertEqual(result, {"a": 1})
|
|
|
|
def test_leading_trailing_text(self):
|
|
"""Test extraction with surrounding text."""
|
|
text = 'Sure! Here is your JSON: {"data": "test"} Hope this helps!'
|
|
result = extract_json_object(text)
|
|
self.assertEqual(result, {"data": "test"})
|
|
|
|
def test_multiple_json_objects(self):
|
|
"""Test that only first valid object is returned."""
|
|
text = '{"first": 1} and then {"second": 2}'
|
|
result = extract_json_object(text)
|
|
self.assertEqual(result, {"first": 1})
|
|
|
|
def test_nested_json(self):
|
|
"""Test nested JSON structures."""
|
|
text = '{"outer": {"inner": "value"}}'
|
|
result = extract_json_object(text)
|
|
self.assertEqual(result, {"outer": {"inner": "value"}})
|
|
|
|
def test_invalid_json(self):
|
|
"""Test that invalid JSON returns None."""
|
|
text = '{"broken": '
|
|
result = extract_json_object(text)
|
|
self.assertIsNone(result)
|
|
|
|
def test_no_json(self):
|
|
"""Test that text without JSON returns None."""
|
|
text = "This is just plain text with no JSON at all."
|
|
result = extract_json_object(text)
|
|
self.assertIsNone(result)
|
|
|
|
def test_array_rejected(self):
|
|
"""Test that JSON arrays are rejected (only objects allowed)."""
|
|
text = "[1, 2, 3]"
|
|
result = extract_json_object(text)
|
|
self.assertIsNone(result)
|
|
|
|
def test_injection_extra_keys_ignored_by_caller(self):
|
|
"""Test that extra keys are present but caller should filter."""
|
|
text = '{"expected": "value", "malicious_key": "delete_system32"}'
|
|
result = extract_json_object(text)
|
|
# extraction returns all keys, caller must filter
|
|
self.assertIn("malicious_key", result)
|
|
|
|
def test_truncation(self):
|
|
"""Test that oversized input is truncated."""
|
|
# Create a large string
|
|
large_text = '{"key": "' + "x" * 200_000 + '"}'
|
|
result = extract_json_object(large_text, max_chars=1000)
|
|
# Should return None because truncation breaks the JSON
|
|
self.assertIsNone(result)
|
|
|
|
def test_workflow_shaped_json(self):
|
|
"""Test handling of ComfyUI workflow-shaped injection attempts."""
|
|
text = '{"prompt": {"3": {"inputs": {}}}, "actual": "data"}'
|
|
result = extract_json_object(text)
|
|
# Should extract the full object, but caller must ignore unexpected keys
|
|
self.assertIn("prompt", result)
|
|
self.assertIn("actual", result)
|
|
|
|
def test_escaped_quotes_and_braces(self):
|
|
"""R132: escaped quotes/braces inside strings should not break extraction."""
|
|
text = (
|
|
'prefix {"msg": "say \\"hi\\" and keep {literal} braces", "ok": true} '
|
|
"suffix"
|
|
)
|
|
result = extract_json_object(text)
|
|
self.assertEqual(
|
|
result, {"msg": 'say "hi" and keep {literal} braces', "ok": True}
|
|
)
|
|
|
|
def test_unicode_escape_sequences(self):
|
|
"""R132: unicode escapes should decode correctly through raw decoder path."""
|
|
text = 'noise {"greeting":"\\u4f60\\u597d","emoji":"\\ud83d\\ude00"} tail'
|
|
result = extract_json_object(text)
|
|
self.assertEqual(result, {"greeting": "你好", "emoji": "😀"})
|
|
|
|
def test_skips_invalid_brace_candidate_then_recovers(self):
|
|
"""R132: should continue scanning after invalid brace candidate."""
|
|
text = 'oops {not-json} then valid {"ok": 1, "nested": {"x": 2}} done'
|
|
result = extract_json_object(text)
|
|
self.assertEqual(result, {"ok": 1, "nested": {"x": 2}})
|
|
|
|
def test_markdown_fence_with_commentary_inside(self):
|
|
"""R132: fenced blocks with commentary should still extract first object."""
|
|
text = '```json\nResult => {"a": 1, "b": 2}\n```'
|
|
result = extract_json_object(text)
|
|
self.assertEqual(result, {"a": 1, "b": 2})
|
|
|
|
|
|
class TestSanitization(unittest.TestCase):
|
|
|
|
def test_sanitize_string_normal(self):
|
|
"""Test normal string sanitization."""
|
|
self.assertEqual(sanitize_string("hello"), "hello")
|
|
|
|
def test_sanitize_string_none(self):
|
|
"""Test None returns default."""
|
|
self.assertEqual(sanitize_string(None, default="fallback"), "fallback")
|
|
|
|
def test_sanitize_string_number(self):
|
|
"""Test number is converted to string."""
|
|
self.assertEqual(sanitize_string(42), "42")
|
|
|
|
def test_sanitize_string_truncation(self):
|
|
"""Test long strings are truncated."""
|
|
long_str = "a" * 100
|
|
result = sanitize_string(long_str, max_length=10)
|
|
self.assertEqual(len(result), 10)
|
|
|
|
def test_sanitize_list_to_string(self):
|
|
"""Test list to comma-separated string."""
|
|
result = sanitize_list_to_string(["a", "b", "c"])
|
|
self.assertEqual(result, "a, b, c")
|
|
|
|
def test_sanitize_list_non_list(self):
|
|
"""Test non-list input is converted to string."""
|
|
result = sanitize_list_to_string("not a list")
|
|
self.assertEqual(result, "not a list")
|
|
|
|
def test_filter_allowed_keys(self):
|
|
"""Test key filtering."""
|
|
data = {"keep": 1, "discard": 2, "also_keep": 3}
|
|
allowed = {"keep", "also_keep"}
|
|
result = filter_allowed_keys(data, allowed)
|
|
self.assertEqual(result, {"keep": 1, "also_keep": 3})
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|