Files
DemonGatanjieu-Anomalous_Mo…/tests/test_ai_message_refs.py
T
DemonGatanjieuandClaude Opus 5.5 ecbebc049d Dock the message box at the foot of the task like a chat composer
The message box stays at the bottom of the task column however the timeline scrolls. Over the
input, what the message carries (ui_ai_tray.js): pictures with ✎ to mark up (sent on their own)
and ×, chips for what "@" names; gallery pictures are attached, not typed in as "@name". Beside
it the gallery and materials buttons with their counts; the input grows (Enter sends,
Shift+Enter breaks the line) and pictures alone may be sent. "New steps below" appears when the
AI adds steps while the user reads further up; one's own message scrolls into view. Pictures a
message carries show as small image cards with the usual tools. The ready page keeps the big
gallery and what is ready; the box below takes its pictures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 21:11:10 +08:00

83 lines
5.0 KiB
Python

"""What a message's "@" point at (ai_message_refs.py): checked when it is left, given to the AI
with the message, each thing in full or marked missing, pictures attached with what made them."""
import unittest
from unittest import mock
from test_ai_tasks import OUTPUT, isolate, ok
from test_ai_run_map import lora_workflow, picture
from api import ai_message_refs, ai_panels, ai_tasks
from api.mcp_tools import ToolError
class RefsTests(unittest.TestCase):
def setUp(self):
isolate()
self.task = ai_tasks.begin_task("A", "Refs")["id"]
patcher = mock.patch.object(ai_tasks.folder_paths, "get_output_directory", return_value=OUTPUT)
patcher.start()
self.addCleanup(patcher.stop)
def test_what_the_at_points_at_reaches_the_ai_in_full(self):
combo = {"name": "水彩风景", "model": "a.safetensors", "prompt": "watercolor"}
refs = [{"kind": "combos", "id": "水彩风景", "label": "水彩风景"}, {"kind": "combos", "id": "水彩风景"},
{"kind": "models", "id": "loras:0:gone.safetensors", "label": "gone"},
{"kind": "pictures", "id": picture("ref.png", lora_workflow()), "label": "ref.png"},
{"kind": "nodes", "id": "KSampler", "label": "K采样器"}]
step = ai_panels.leave_message(self.task, "用 @水彩风景 的感觉重画 @ref.png", refs)
self.assertEqual(len(step["refs"]), 4, "each thing once")
with mock.patch("api.mcp_tools.list_combos", return_value={"combos": [combo]}), \
mock.patch("api.mcp_tools.get_model", side_effect=ToolError("gone")):
result = ai_panels.attach_pending(ok({}), "A", task_id=self.task)
message = result["structuredContent"]["user_messages"][0]
by_kind = {item["kind"]: item for item in message["refs"]}
self.assertEqual(by_kind["combos"]["details"], combo)
self.assertEqual(by_kind["combos"]["said_as"], "@水彩风景")
self.assertTrue(by_kind["models"]["missing"])
self.assertEqual(by_kind["pictures"]["made_with"]["models"], ["Kirazuri_v4"])
self.assertIn("find_nodes", by_kind["nodes"]["note"])
self.assertEqual(sum(1 for item in result["content"] if item.get("type") == "image"), 1, "the picture itself goes along")
self.assertNotIn("_pictures", message)
def test_a_deleted_picture_is_marked_gone_and_the_ai_told_plainly(self):
import os
from api import ai_pictures, ai_task_changes
name = picture("bye.png")
ai_pictures.hand(self.task, [name, picture("stay.png")])
os.remove(os.path.join(OUTPUT, name))
marked = ai_pictures.mark_gone(ai_tasks.get_task(self.task))
results = marked["steps"][-1]["results"]
self.assertEqual([item.get("gone", False) for item in results], [True, False])
self.assertNotIn("gone", ai_tasks.get_task(self.task)["steps"][-1]["results"][0], "only the page's copy is marked")
words = ai_task_changes._change_text({"action": "image_delete", "target": "bye.png", "source": "gallery"})
self.assertIn("do not use, show or hand it in again", words)
def test_a_plain_message_has_no_refs_and_bad_refs_are_refused(self):
self.assertNotIn("refs", ai_panels.leave_message(self.task, "继续"))
# Pictures alone, no words: still a message.
alone = ai_panels.leave_message(self.task, "", [{"kind": "pictures", "id": "ref.png", "label": "ref.png"}])
self.assertEqual((alone["text"], [ref["id"] for ref in alone["refs"]]), ("", ["ref.png"]))
for refs in ("x", [{"kind": "files", "id": "a"}], [{"kind": "models", "id": ""}], [{"kind": "nodes", "id": str(n)} for n in range(13)]):
with self.assertRaises(ai_panels.PanelError, msg=refs):
ai_panels.leave_message(self.task, "看这个", refs)
self.assertEqual(ai_message_refs.clean(None), [])
def test_exact_names_are_resolved_before_search_limits(self):
from api import mcp_tools
names = [f'cat {i}' for i in range(60)] + ['cat']
combos = [{'name': name, 'data': {'promptEn': 'watercolor'}} for name in names]
recipes = [{'filename': f'{i}.json', 'data': {'name': name}} for i, name in enumerate(names)]
recipes.append({'filename': 'unnamed.json', 'data': {}})
with mock.patch.object(mcp_tools, '_list_notebooks', return_value=combos), \
mock.patch.object(mcp_tools, '_list_recipes', return_value=recipes):
for kind, lister in [('combos', mcp_tools.list_combos), ('workflows', mcp_tools.list_workflows)]:
self.assertNotIn('cat', [item['name'] for item in lister('cat', 5)[kind]])
result = ai_message_refs.message_of({'refs': [{'kind': kind, 'id': 'cat', 'label': 'cat'}]})
self.assertEqual(result['refs'][0]['details']['name'], 'cat')
self.assertIsNone(ai_message_refs.lookup(kind, 'missing'))
self.assertEqual(ai_message_refs.lookup('workflows', 'unnamed.json')['name'], 'unnamed.json')
if __name__ == "__main__":
unittest.main()