Deploy web Studio / deploy (push) Successful in 3m16s
A 401/403 with an HTML error page (nginx) showed as "HTTP 401: 401 Authorization Required 401 Authorization Required nginx". Now: "The endpoint wants an API key: enter it in ⚙" before one is set, "The endpoint refused the API key (...)" after.
162 lines
8.2 KiB
Python
162 lines
8.2 KiB
Python
"""Agent window checks, shared by check_studio.py (isokit studio) and
|
|
check_web.py (web Studio). The page's agent must use the mock model
|
|
(tools/mock_llm.py, key test-key); `start_mock()` runs one."""
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
MOCK_PORT = 7932
|
|
MOCK = f"http://127.0.0.1:{MOCK_PORT}"
|
|
|
|
|
|
def start_mock():
|
|
p = subprocess.Popen([sys.executable, str(Path(__file__).with_name("mock_llm.py")), str(MOCK_PORT)],
|
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
|
p.stdout.readline() # the banner: listening
|
|
return p
|
|
|
|
|
|
def seen():
|
|
return json.loads(urllib.request.urlopen(MOCK + "/_requests").read())
|
|
|
|
|
|
def chats():
|
|
return [r for r in seen() if r.get("path") == "/v1/chat/completions"]
|
|
|
|
|
|
def run(pg, objects, shot, check, can_set_key):
|
|
"""objects(): the scene's objects now. can_set_key: the key is set in the
|
|
window (web), not by the Studio server's environment."""
|
|
pg.locator("#agent").click()
|
|
check(pg.locator("#agwin").is_visible(), "✦ Agent opens the agent window")
|
|
pg.wait_for_function("() => /mock-1/.test(document.getElementById('agwho').textContent)", timeout=8000)
|
|
check(pg.locator("#agwho .ag-on").count() == 1, "the window connects to the model: " + pg.locator("#agwho").inner_text())
|
|
check(pg.locator(".ag-sug").count() >= 3, "an empty chat offers suggestions")
|
|
shot("agent-empty.jpg")
|
|
|
|
# a turn with two tool calls, then an answer
|
|
n0 = len(objects())
|
|
pg.locator("#agtext").fill("Plant a tree at 6, 4 please")
|
|
pg.locator("#agtext").press("Enter")
|
|
pg.wait_for_selector(".ag-bot:not(.ag-live) .ag-text code.ag-obj", timeout=15000)
|
|
objs = objects()
|
|
new = [o for o in objs if o.get("name") == "Agent tree"]
|
|
check(len(objs) == n0 + 1 and len(new) == 1 and new[0]["x"] == 6 and new[0]["y"] == 4,
|
|
"the agent's add_object lands in the scene at (6, 4)")
|
|
tid = new[0]["id"] if new else "?"
|
|
check(pg.locator(".ag-tool.ag-ok").count() == 2, "both tool calls show as done")
|
|
check(pg.locator(f'.ag-tool code.ag-obj[data-obj="{tid}"]').count() == 1, "the add_object row links the new object")
|
|
check(pg.locator(f'#objects li[data-id="{tid}"]').count() == 1, "the object list shows the agent's tree at once")
|
|
check(pg.locator(f'.iso-svg g[data-obj="{tid}"]').count() >= 1, "the map draws the agent's tree")
|
|
check(pg.locator(".ag-think summary").first.inner_text().startswith("Thought for"), "the reasoning folds away: "
|
|
+ pg.locator(".ag-think summary").first.inner_text())
|
|
txt = pg.locator(".ag-bot .ag-text").last.inner_text()
|
|
check("Planted" in txt and tid in txt, "the answer streams in: " + txt.replace("\n", " ")[:70])
|
|
check(pg.locator(".ag-bot .ag-text li").count() == 2 and pg.locator(".ag-bot .ag-text b").count() == 1,
|
|
"markdown list and bold render")
|
|
shot("agent-chat.jpg")
|
|
|
|
c = chats()
|
|
first, second = c[-2]["body"], c[-1]["body"]
|
|
sysmsg = first["messages"][0]
|
|
names = [t["function"]["name"] for t in first.get("tools", [])]
|
|
check(all(r["auth"] == "Bearer test-key" for r in c[-2:]), "the key reaches the model")
|
|
check(sysmsg["role"] == "system" and "isokit Studio" in sysmsg["content"] and "Nothing is selected" in sysmsg["content"]
|
|
and "project_info" in sysmsg["content"], "the system prompt has the MCP instructions and the Studio state")
|
|
check(len(names) == 17 and {"add_object", "apply_ops", "select_object", "set_view"} <= set(names),
|
|
f"the model gets 15 MCP tools + 2 page tools ({len(names)})")
|
|
check(first.get("stream") is True and first.get("model") == "mock-1", "streamed, first offered model by default")
|
|
tool_ids = [m.get("tool_call_id") for m in second["messages"] if m["role"] == "tool"]
|
|
check(tool_ids == ["call_a", "call_b"], f"tool results go back with their call ids {tool_ids}")
|
|
|
|
# the id in the answer selects the object
|
|
pg.locator(f'.ag-bot .ag-text code.ag-obj[data-obj="{tid}"]').click()
|
|
pg.wait_for_timeout(500)
|
|
check(("id " + tid) in pg.locator("#right .st-sub").inner_text(), "clicking the id selects the agent's tree")
|
|
# the selection reaches the model in the next turn; a plain answer
|
|
pg.locator("#agtext").fill("hello")
|
|
pg.locator("#agtext").press("Enter")
|
|
pg.wait_for_function("() => !document.querySelector('.ag-busy')", timeout=10000)
|
|
check(('"id":"%s"' % tid) in chats()[-1]["body"]["messages"][0]["content"], "the system prompt names the selection")
|
|
|
|
# Undo takes the agent's edit back
|
|
pg.locator("#undo").click()
|
|
pg.wait_for_timeout(700)
|
|
check(not any(o.get("name") == "Agent tree" for o in objects()), "Undo removes the agent's tree")
|
|
|
|
# Stop a long answer
|
|
pg.locator("#agtext").fill("tell me a slow story")
|
|
pg.locator("#agtext").press("Enter")
|
|
pg.wait_for_timeout(900)
|
|
check(pg.locator("#agsend").inner_text() == "Stop", "while the agent works, Send turns into Stop")
|
|
pg.locator("#agsend").click()
|
|
pg.wait_for_selector(".ag-note:has-text('Stopped')", timeout=5000)
|
|
check(pg.locator("#agsend").inner_text() == "Send" and "word1" in pg.locator(".ag-bot .ag-text").last.inner_text(),
|
|
"Stop ends the turn and keeps what came")
|
|
|
|
# settings: pick another model
|
|
def settings():
|
|
pg.wait_for_timeout(900) # a good save folds the form away after a moment
|
|
if pg.locator("#agsetf").is_hidden():
|
|
pg.locator("#agset").click()
|
|
|
|
def save():
|
|
pg.locator('#agsetf button[type="submit"]').click()
|
|
pg.wait_for_timeout(700)
|
|
|
|
settings()
|
|
check(pg.locator("#agsetf").is_visible(), "⚙ shows the settings")
|
|
opts = pg.locator("#agmodel option").all_inner_texts()
|
|
check("mock-2" in opts, f"the model list comes from the endpoint {opts}")
|
|
pg.locator("#agmodel").select_option("mock-2")
|
|
save()
|
|
pg.wait_for_function("() => /mock-2/.test(document.getElementById('agwho').textContent)", timeout=5000)
|
|
pg.locator("#agtext").fill("hello again")
|
|
pg.locator("#agtext").press("Enter")
|
|
pg.wait_for_function("() => !document.querySelector('.ag-busy')", timeout=10000)
|
|
check(chats()[-1]["body"]["model"] == "mock-2", "the chosen model is used")
|
|
if can_set_key:
|
|
settings()
|
|
pg.locator("#agkey").fill("wrong-key")
|
|
save()
|
|
check("refused the API key" in pg.locator("#agsetmsg").inner_text() and "invalid api key" in pg.locator("#agsetmsg").inner_text(),
|
|
"a wrong key shows the endpoint's refusal: " + pg.locator("#agsetmsg").inner_text())
|
|
check(pg.locator("#agwho .ag-off").count() == 1, "the status dot turns red")
|
|
shot("agent-badkey.jpg")
|
|
pg.locator("#agkey").fill("test-key")
|
|
save()
|
|
check(pg.locator("#agwho .ag-on").count() == 1, "the right key connects again")
|
|
else:
|
|
settings()
|
|
check(pg.locator("#agurl").is_disabled() and "ISOKIT_LLM_KEY" in pg.locator("#agkey").get_attribute("placeholder"),
|
|
"endpoint and key set by the environment are shown as such")
|
|
shot("agent-settings.jpg")
|
|
settings()
|
|
pg.locator("#agmodel").select_option("")
|
|
save()
|
|
check("mock-1" in pg.locator("#agwho").inner_text(), "back to the first offered model")
|
|
|
|
# move and resize the window
|
|
box = pg.locator("#agwin").bounding_box()
|
|
hb = pg.locator(".ag-logo").bounding_box()
|
|
pg.mouse.move(hb["x"] + 5, hb["y"] + 5)
|
|
pg.mouse.down()
|
|
pg.mouse.move(hb["x"] - 95, hb["y"] - 35, steps=5)
|
|
pg.mouse.up()
|
|
box2 = pg.locator("#agwin").bounding_box()
|
|
check(abs(box2["x"] - box["x"] + 100) < 3 and abs(box2["y"] - box["y"] + 40) < 3, "the window moves by its title bar")
|
|
gb = pg.locator("#aggrip").bounding_box()
|
|
pg.mouse.move(gb["x"] + 4, gb["y"] + 4)
|
|
pg.mouse.down()
|
|
pg.mouse.move(gb["x"] - 56, gb["y"] - 26, steps=5)
|
|
pg.mouse.up()
|
|
box3 = pg.locator("#agwin").bounding_box()
|
|
check(abs(box3["width"] - box2["width"] - 60) < 3 and abs(box3["height"] - box2["height"] - 30) < 3, "the top-left grip resizes it")
|
|
|
|
pg.locator("#agnew").click()
|
|
check(pg.locator(".ag-empty").count() == 1 and pg.locator(".ag-tool").count() == 0, "New chat clears the conversation")
|
|
pg.locator("#agclose").click()
|
|
check(pg.locator("#agwin").is_hidden() and pg.locator("#agent").get_attribute("aria-pressed") == "false", "✕ closes the window")
|