The prompt hash folds case, so a run of "A Lighthouse" counts toward the saved "a lighthouse" but never joined its history. History now compares the hash's own key (prompt_key = strip + str.lower()); SQL calls Python for it through pm_prompt_key/pm_prompt_hash registered on every managed connection, since SQLite's lower() folds ASCII only. Hints match by text first (#438) and fall back to the dedup hash only when no text matches, so no existing hint becomes ambiguous. A history position from before this change still resumes.
216 lines
7.5 KiB
Python
216 lines
7.5 KiB
Python
"""
|
|
Tests for hashing utilities.
|
|
|
|
Tests is_duplicate_prompt and generate_prompt_hash, which are
|
|
currently untested. generate_prompt_hash is covered in test_basic.py
|
|
but we add edge cases here.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import unittest
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
from tests.background_stops import stop_tracker # noqa: E402
|
|
from utils.hashing import (
|
|
generate_prompt_hash,
|
|
is_duplicate_prompt,
|
|
STRIPPED_CODE_POINTS,
|
|
prompt_key,
|
|
register_prompt_functions,
|
|
stored_prompt_text,
|
|
stored_prompt_text_sql,
|
|
)
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
|
|
class TestSingleHashImplementation(unittest.TestCase):
|
|
"""generate_prompt_hash is the only prompt hash; nothing re-implements it."""
|
|
|
|
def test_node_base_delegates_to_generate_prompt_hash(self):
|
|
from prompt_manager_base import PromptManagerBase
|
|
|
|
node = PromptManagerBase.__new__(PromptManagerBase)
|
|
self.assertEqual(node._generate_hash(" A "), generate_prompt_hash(" A "))
|
|
self.assertEqual(node._generate_hash(" A "), generate_prompt_hash("a"))
|
|
|
|
def test_tracker_and_monitor_use_generate_prompt_hash(self):
|
|
for relative in (
|
|
os.path.join("utils", "prompt_tracker.py"),
|
|
os.path.join("utils", "image_monitor.py"),
|
|
"prompt_manager_base.py",
|
|
):
|
|
with open(os.path.join(ROOT, relative), encoding="utf-8") as f:
|
|
source = f.read()
|
|
self.assertFalse("hashlib.sha256" in source, f"inline hash in {relative}")
|
|
self.assertTrue("generate_prompt_hash" in source, relative)
|
|
|
|
def test_tracker_finds_an_existing_prompt_by_the_shared_hash(self):
|
|
import unittest.mock as mock
|
|
|
|
from utils.prompt_tracker import PromptTracker
|
|
|
|
db = mock.Mock()
|
|
db.get_prompt_by_hash.return_value = {"id": 42}
|
|
tracker = PromptTracker(db)
|
|
self.addCleanup(stop_tracker, self, tracker)
|
|
tracker.set_current_prompt(" Hello World ")
|
|
db.get_prompt_by_hash.assert_called_once_with(
|
|
generate_prompt_hash(" Hello World ")
|
|
)
|
|
self.assertEqual(tracker.get_current_prompt()["id"], 42)
|
|
|
|
def test_node_base_has_no_browser_opener(self):
|
|
from prompt_manager_base import PromptManagerBase
|
|
|
|
self.assertFalse(hasattr(PromptManagerBase, "_open_web_interface"))
|
|
self.assertFalse(os.path.exists(os.path.join(ROOT, "restart_gallery.py")))
|
|
|
|
|
|
class TestIsDuplicatePrompt(unittest.TestCase):
|
|
"""Test is_duplicate_prompt function."""
|
|
|
|
def test_identical_texts_are_duplicates(self):
|
|
self.assertTrue(is_duplicate_prompt("hello", "hello"))
|
|
|
|
def test_case_insensitive(self):
|
|
self.assertTrue(is_duplicate_prompt("Hello World", "hello world"))
|
|
|
|
def test_whitespace_insensitive(self):
|
|
self.assertTrue(is_duplicate_prompt(" hello ", "hello"))
|
|
|
|
def test_different_texts_not_duplicates(self):
|
|
self.assertFalse(is_duplicate_prompt("text A", "text B"))
|
|
|
|
def test_threshold_parameter_accepted(self):
|
|
# threshold is unused but should not cause errors
|
|
self.assertTrue(is_duplicate_prompt("same", "same", threshold=0.5))
|
|
|
|
def test_empty_strings_are_duplicates(self):
|
|
self.assertTrue(is_duplicate_prompt("", ""))
|
|
|
|
def test_unicode_comparison(self):
|
|
self.assertTrue(is_duplicate_prompt("日本語", "日本語"))
|
|
self.assertFalse(is_duplicate_prompt("日本語", "中文"))
|
|
|
|
|
|
class TestGeneratePromptHashEdgeCases(unittest.TestCase):
|
|
"""Additional edge cases for generate_prompt_hash."""
|
|
|
|
def test_non_string_raises_type_error(self):
|
|
with self.assertRaises(TypeError):
|
|
generate_prompt_hash(123)
|
|
|
|
def test_none_raises_type_error(self):
|
|
with self.assertRaises(TypeError):
|
|
generate_prompt_hash(None)
|
|
|
|
def test_empty_string(self):
|
|
h = generate_prompt_hash("")
|
|
self.assertEqual(len(h), 64)
|
|
|
|
def test_unicode_hash(self):
|
|
h = generate_prompt_hash("émojis 🎨 日本語")
|
|
self.assertEqual(len(h), 64)
|
|
|
|
def test_very_long_text(self):
|
|
h = generate_prompt_hash("x" * 100000)
|
|
self.assertEqual(len(h), 64)
|
|
|
|
|
|
class TestStoredPromptText(unittest.TestCase):
|
|
"""SQLite strips exactly the characters Python's str.strip() does (#438)."""
|
|
|
|
WHITESPACE = "".join(c for c in map(chr, range(0x110000)) if c.isspace())
|
|
|
|
def sql_strip(self, value):
|
|
import sqlite3
|
|
|
|
conn = sqlite3.connect(":memory:")
|
|
try:
|
|
return conn.execute(
|
|
f"SELECT {stored_prompt_text_sql('?')}", (value,)
|
|
).fetchone()[0]
|
|
finally:
|
|
conn.close()
|
|
|
|
def test_code_points_are_exactly_python_whitespace(self):
|
|
self.assertEqual("".join(map(chr, STRIPPED_CODE_POINTS)), self.WHITESPACE)
|
|
|
|
def test_python_rule_is_the_hash_strip(self):
|
|
self.assertEqual(stored_prompt_text(" a lighthouse\n"), "a lighthouse")
|
|
self.assertEqual(
|
|
generate_prompt_hash(stored_prompt_text(" A b\u3000")),
|
|
generate_prompt_hash(" A b\u3000"),
|
|
)
|
|
|
|
def test_sql_strips_every_python_whitespace_character(self):
|
|
for char in self.WHITESPACE:
|
|
with self.subTest(code=hex(ord(char))):
|
|
value = f"{char}a{char}b{char}"
|
|
self.assertEqual(self.sql_strip(value), stored_prompt_text(value))
|
|
mixed = self.WHITESPACE + "a lighthouse é" + self.WHITESPACE
|
|
self.assertEqual(self.sql_strip(mixed), "a lighthouse é")
|
|
|
|
def test_sql_keeps_what_python_keeps(self):
|
|
for value in ("\u200ba\u200b", "\ufeffa", "a\u0301", "", "x"):
|
|
with self.subTest(value=repr(value)):
|
|
self.assertEqual(self.sql_strip(value), stored_prompt_text(value))
|
|
|
|
|
|
class TestPromptKey(unittest.TestCase):
|
|
"""History compares the hash's own key; SQL calls Python for it (#460)."""
|
|
|
|
CASES = (
|
|
"A Lighthouse",
|
|
" \u00c9t\u00c9\n",
|
|
"\u03a3\u03a3",
|
|
"\u0130stanbul",
|
|
"\u1e9e",
|
|
"\u212a",
|
|
"ok",
|
|
)
|
|
|
|
def test_key_is_the_hash_normalization(self):
|
|
import hashlib
|
|
|
|
for value in self.CASES:
|
|
with self.subTest(value=repr(value)):
|
|
self.assertEqual(
|
|
generate_prompt_hash(value),
|
|
hashlib.sha256(prompt_key(value).encode("utf-8")).hexdigest(),
|
|
)
|
|
self.assertEqual(prompt_key(" A Lighthouse\n"), "a lighthouse")
|
|
|
|
def test_sql_functions_give_pythons_result_where_sql_lower_does_not(self):
|
|
import sqlite3
|
|
|
|
conn = sqlite3.connect(":memory:")
|
|
try:
|
|
register_prompt_functions(conn)
|
|
for value in self.CASES:
|
|
with self.subTest(value=repr(value)):
|
|
key, digest = conn.execute(
|
|
"SELECT pm_prompt_key(?), pm_prompt_hash(?)", (value, value)
|
|
).fetchone()
|
|
self.assertEqual(key, prompt_key(value))
|
|
self.assertEqual(digest, generate_prompt_hash(value))
|
|
self.assertEqual(
|
|
conn.execute(
|
|
"SELECT pm_prompt_key(1), pm_prompt_hash(NULL)"
|
|
).fetchone(),
|
|
(None, None),
|
|
)
|
|
# SQLite's own lower() folds ASCII only
|
|
self.assertNotEqual(
|
|
conn.execute("SELECT lower(?)", ("\u00c9",)).fetchone()[0], "\u00e9"
|
|
)
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|