- An answer past Antigravity's size is filed away unread (eight LoRA searches in the exam): search_models answers small (loaders once, four trigger words, a LoRA's author weight); describe_canvas gives a big canvas's last nodes a line each; mcp_fit.py keeps any other list answer within the size and names what it left out. - An empty canvas: run_workflow, describe_canvas and get_situation say to open a workflow first; get_image_info says a picture's workflow opens with open_workflow. - ask_user does not put a "test" question to the user; a task's picture count counts what its runs made. - AMB failed to load now and then: ComfyUI loads every file at once and ui_ai_workbench.js set ui_ai_toolbox.js's sideHost before it was made when the toolbox file ran first. Connected on first draw now; tests/load_order.mjs runs every file first. - input_facts moved to api/picture_mask.py to keep mcp_tools.py short. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
83 lines
5.2 KiB
Python
83 lines
5.2 KiB
Python
"""get_situation (api/mcp_situation.py): the task, the canvas in brief and the next step in one
|
|
answer that stays small. The page's brief is stood in for; tasks go to a temporary folder."""
|
|
import sys
|
|
import time
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
|
|
PLUGIN = Path(__file__).resolve().parents[1]
|
|
sys.path[:0] = [str(PLUGIN), str(PLUGIN.parents[1]), str(PLUGIN / "tests")]
|
|
|
|
from api import ai_decisions, ai_panels, ai_task_runs, ai_tasks, mcp_situation # noqa: E402
|
|
from api.mcp_bridge import PageUnavailable # noqa: E402
|
|
from api.mcp_tools import tool_text # noqa: E402
|
|
from test_ai_tasks import isolate, ok # noqa: E402
|
|
|
|
CTX = {"client": "Flash"}
|
|
BRIEF = {"workflow": "海边.json", "node_count": 3, "lines": ["#3 KSampler | model←#1 latent_image←?LATENT | gives LATENT"],
|
|
"unwired": [{"node": 3, "input": "latent_image", "type": "LATENT", "fits": ["#4 LATENT"]}]}
|
|
|
|
|
|
class SituationTests(unittest.TestCase):
|
|
def setUp(self):
|
|
isolate()
|
|
self.page = mock.patch.object(mcp_situation, "ask_page", return_value=dict(BRIEF, lines=list(BRIEF["lines"])))
|
|
self.asked = self.page.start()
|
|
self.addCleanup(self.page.stop)
|
|
ai_tasks.record("Flash", "describe_canvas", {}, ok({}), time.time(), True)
|
|
self.task = ai_tasks.begin_task("Flash", "海边少女")["id"]
|
|
|
|
def test_the_task_the_canvas_and_what_to_do_next(self):
|
|
ai_panels.leave_message(self.task, "背景再亮一点")
|
|
ai_decisions.log("Flash", "风格", "水彩", "用户选的", task_id=self.task)
|
|
shown = ai_panels.show("Flash", "选一个", [{"type": "choices", "id": "pick", "options": [{"label": "A"}, {"label": "B"}]}], task_id=self.task)
|
|
got = mcp_situation.get_situation(CTX)
|
|
self.asked.assert_called_once_with("describe_canvas", {"brief": True}, timeout=15)
|
|
self.assertEqual(got["task"]["title"], "海边少女")
|
|
self.assertEqual(got["task"]["asked"]["text"], "背景再亮一点")
|
|
self.assertEqual(got["task"]["waiting_for_user"], 1)
|
|
self.assertIn("风格: 水彩", got["task"]["decisions"][0])
|
|
self.assertEqual((got["run"]["state"], got["canvas"]["unwired"][0]["input"]), ("none", "latent_image"))
|
|
self.assertIn("get_answers", got["next"]) # the user's answer first
|
|
step = next(item for item in ai_tasks.get_task(self.task)["steps"] if item.get("panel", {}).get("id") == shown["id"])
|
|
ai_panels.answer(self.task, step["id"], "pick", "o1")
|
|
self.assertIn("connect_nodes", mcp_situation.get_situation(CTX)["next"]) # then the wire that is missing
|
|
|
|
def test_next_follows_the_run_and_the_errors(self):
|
|
nothing = {"asks": 0}
|
|
self.assertIn("run_status", mcp_situation._next(nothing, {"state": "running"}, BRIEF))
|
|
self.assertIn("canvas.errors", mcp_situation._next(nothing, {"state": "error"}, {"errors": [{"node": 3}]}))
|
|
missing = {"errors": [{"node": 66, "error": "Value not in list: unet_name: 'z.safetensors' not in []"}]}
|
|
self.assertIn("check_workflow_models", mcp_situation._next(nothing, {"state": "error"}, missing))
|
|
self.assertIn("zoom_image", mcp_situation._next(nothing, {"state": "done", "pictures": ["a.png"]}, {"unwired": []}))
|
|
self.assertIn("task.asked", mcp_situation._next(nothing, {"state": "none"}, {"unwired": []}))
|
|
self.assertIn("open_workflow", mcp_situation._next(nothing, {"state": "none"}, {"node_count": 0, "unwired": []}), "an empty canvas")
|
|
with mock.patch.object(ai_task_runs, "status", return_value={"state": "done", "summary": "finished", "pictures": ["a.png"], "seconds": 3}):
|
|
self.assertEqual(mcp_situation.get_situation(CTX)["run"], {"state": "done", "summary": "finished", "pictures": ["a.png"]})
|
|
|
|
def test_no_page_no_task_and_the_size(self):
|
|
self.asked.side_effect = PageUnavailable("ComfyUI is not open in a browser on this computer.")
|
|
self.assertIn("not open", mcp_situation.get_situation(CTX)["canvas"]["unavailable"])
|
|
self.asked.side_effect = None
|
|
self.assertIsNone(mcp_situation.get_situation({"client": "Nobody"})["task"])
|
|
self.asked.return_value = {"node_count": 60, "lines": [f"#{at} SomeVeryLongNodeTypeName {'x' * 120}" for at in range(30)],
|
|
"more_nodes": 30, "unwired": []}
|
|
got = mcp_situation.get_situation(CTX)
|
|
self.assertLessEqual(len(tool_text(got)), mcp_situation.BUDGET)
|
|
self.assertEqual(len(got["canvas"]["lines"]) + got["canvas"]["more_nodes"], 60) # what was left out is counted
|
|
|
|
def test_chain_budget_keeps_missing_inputs_and_counts_omissions(self):
|
|
self.asked.return_value = {"node_count": 8, "lines": ["x" * 140] * 8,
|
|
"chains": ["x" * 240] * 6, "more_chains": 2,
|
|
"unwired": [{"node": 3, "input": "positive", "type": "CONDITIONING",
|
|
"fits": ["x" * 100] * 3}] * 8}
|
|
got = mcp_situation.get_situation(CTX)
|
|
self.assertLessEqual(len(tool_text(got)), mcp_situation.BUDGET)
|
|
self.assertEqual(len(got["canvas"]["unwired"]), 8)
|
|
self.assertEqual(len(got["canvas"]["chains"]) + got["canvas"]["more_chains"], 8)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|