Files
2026-07-13 13:12:33 +08:00

4912 lines
200 KiB
Python

"""Opt-in real-browser chat surface e2e without provider spend."""
from __future__ import annotations
import json
import os
import socket
import subprocess
import sys
import textwrap
import time
from pathlib import Path
import httpx
import pytest
pytestmark = pytest.mark.webui_browser
def _free_port() -> int:
with socket.socket() as sock:
sock.bind(("127.0.0.1", 0))
return int(sock.getsockname()[1])
def _npm() -> str:
return "npm.cmd" if os.name == "nt" else "npm"
def _node() -> str:
return "node.exe" if os.name == "nt" else "node"
def _install_playwright(work_dir: Path) -> None:
result = subprocess.run(
[_npm(), "--prefix", str(work_dir), "install", "playwright"],
cwd=Path.cwd(),
check=False,
capture_output=True,
text=True,
timeout=120,
)
assert result.returncode == 0, result.stderr or result.stdout
def _wait_for_health(port: int, server: subprocess.Popen[str]) -> None:
url = f"http://127.0.0.1:{port}/health"
deadline = time.monotonic() + 20.0
last_error = ""
while time.monotonic() < deadline:
if server.poll() is not None:
stdout = server.stdout.read() if server.stdout else ""
stderr = server.stderr.read() if server.stderr else ""
raise AssertionError(
f"gateway exited early code={server.returncode}\nstdout={stdout}\nstderr={stderr}"
)
try:
response = httpx.get(url, timeout=1.0)
if response.status_code == 200 and response.json().get("ok") is True:
return
except Exception as exc: # noqa: BLE001 - surfaced on timeout.
last_error = str(exc)
time.sleep(0.1)
raise AssertionError(f"gateway did not become healthy: {last_error}")
def _stop_process(process: subprocess.Popen[str]) -> None:
process.terminate()
try:
process.wait(timeout=8)
except subprocess.TimeoutExpired:
process.kill()
process.wait(timeout=8)
def test_per_turn_bubble_chip_differs_across_turns_in_real_browser(tmp_path: Path) -> None:
"""P4-AC6: per-turn .msg-meta__tokens chip reflects per-turn token counts.
Two synthetic turns are injected via the RPC event bus (no LLM spend).
Turn 1 uses input_tokens=11; turn 2 uses input_tokens=19. The test
asserts that:
- chip[0].input != chip[1].input (per-turn semantics, values differ)
- chip[1].input >= chip[0].input (monotonic across a session with growing
context, satisfied trivially because 19 > 11)
"""
if os.environ.get("OPENSQUILLA_WEBUI_BROWSER_CHAT_E2E") != "1":
pytest.skip("set OPENSQUILLA_WEBUI_BROWSER_CHAT_E2E=1 to run chat browser e2e")
port = _free_port()
server_script = tmp_path / "webui_bubble_server.py"
browser_script = tmp_path / "webui_bubble_browser.js"
server_script.write_text(
textwrap.dedent(
f"""
import uvicorn
from opensquilla.gateway.app import create_gateway_app
from opensquilla.gateway.config import AuthConfig, GatewayConfig
config = GatewayConfig(
host="127.0.0.1",
port={port},
auth=AuthConfig(mode="none"),
)
app = create_gateway_app(config)
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port={port}, log_level="warning")
"""
),
encoding="utf-8",
)
browser_script.write_text(
textwrap.dedent(
r"""
const { chromium } = require("playwright");
// Inject a synthetic streaming turn into the chat JS event bus.
// text_delta fires through the named listener; done fires through the
// wildcard (*) listener which is how chat.js processes terminal events.
async function injectTurn(page, inputTokens, outputTokens) {
await page.evaluate(
({ inputTokens, outputTokens }) => {
const rpc = App.getRpc();
const ls = rpc._listeners;
// 1. text_delta — named listener — creates the stream bubble
const deltaHandlers = ls.get("session.event.text_delta");
if (deltaHandlers) {
deltaHandlers.forEach(h => h({ text: "hi" }));
}
// 2. session.event.done — wildcard (*) listener — attaches
// the .msg-meta__tokens chip to the finished bubble
const wildHandlers = ls.get("*");
if (wildHandlers) {
wildHandlers.forEach(h =>
h("session.event.done", {
input_tokens: inputTokens,
output_tokens: outputTokens,
text: "hi",
})
);
}
},
{ inputTokens, outputTokens }
);
}
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
const errors = [];
page.on("pageerror", err => errors.push(String(err)));
await page.goto(process.env.TARGET_URL, {
waitUntil: "domcontentloaded",
timeout: 30000,
});
await page.waitForSelector("#chat-textarea", { timeout: 15000 });
// Wait for WebSocket RPC connection (chat.js needs it to register listeners)
await page.waitForFunction(
() =>
typeof App !== "undefined" &&
App.getRpc &&
App.getRpc()?.state === "connected",
{ timeout: 15000 }
);
// Turn 1: input=11, output=5
await injectTurn(page, 11, 5);
await page.waitForSelector(".msg-meta__tokens", { timeout: 5000 });
// Turn 2: input=19, output=7
await injectTurn(page, 19, 7);
// Wait for the second chip to appear
await page.waitForFunction(
() => document.querySelectorAll(".msg-meta__tokens").length >= 2,
null, { timeout: 5000 }
);
const chips = await page.evaluate(() =>
Array.from(document.querySelectorAll(".msg-meta__tokens")).map(el => el.textContent)
);
// Parse "↑X ↓Y" into { input: X, output: Y }
function parseChip(text) {
const m = text.match(/↑(\d+(?:\.\d+)?[KMk]?)\s*↓(\d+(?:\.\d+)?[KMk]?)/);
if (!m) return null;
function tok(s) {
const n = parseFloat(s);
if (s.endsWith("K") || s.endsWith("k")) return Math.round(n * 1000);
if (s.endsWith("M")) return Math.round(n * 1000000);
return n;
}
return { input: tok(m[1]), output: tok(m[2]) };
}
const parsed = chips.slice(0, 2).map(parseChip);
const result = {
chipCount: chips.length,
chip0: parsed[0],
chip1: parsed[1],
pageErrors: errors,
};
await browser.close();
console.log(JSON.stringify(result));
})().catch(err => {
console.error(err && err.stack ? err.stack : String(err));
process.exit(1);
});
"""
),
encoding="utf-8",
)
env = os.environ.copy()
env["OPENSQUILLA_STATE_DIR"] = str(tmp_path / "state")
env["OPENSQUILLA_LOG_DIR"] = str(tmp_path / "logs")
server = subprocess.Popen(
[sys.executable, str(server_script)],
cwd=Path.cwd(),
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
)
try:
_wait_for_health(port, server)
_install_playwright(tmp_path)
result = subprocess.run(
[_node(), str(browser_script)],
cwd=tmp_path,
env=dict(env, TARGET_URL=f"http://127.0.0.1:{port}/control/chat"),
check=False,
capture_output=True,
text=True,
timeout=90,
)
assert result.returncode == 0, result.stderr or result.stdout
payload = json.loads(result.stdout.strip().splitlines()[-1])
finally:
_stop_process(server)
assert payload["pageErrors"] == [], payload["pageErrors"]
assert payload["chipCount"] >= 2, f"expected >=2 chips, got {payload['chipCount']}"
chip0 = payload["chip0"]
chip1 = payload["chip1"]
assert chip0 is not None, "chip0 did not parse"
assert chip1 is not None, "chip1 did not parse"
# Per-turn semantics: each bubble shows the tokens for that turn, not the
# session accumulator, so consecutive turns with different token counts must
# produce different chip values.
assert chip0["input"] != chip1["input"], (
f"chip input_tokens should differ between turns: chip0={chip0}, chip1={chip1}"
)
# Monotonic: second turn's per-turn input >= first turn's (19 > 11 by construction)
assert chip1["input"] >= chip0["input"], (
f"chip1.input ({chip1['input']}) should be >= chip0.input ({chip0['input']})"
)
def test_chat_view_loads_and_reaches_gateway_http_status_in_real_browser(tmp_path: Path) -> None:
if os.environ.get("OPENSQUILLA_WEBUI_BROWSER_CHAT_E2E") != "1":
pytest.skip("set OPENSQUILLA_WEBUI_BROWSER_CHAT_E2E=1 to run chat browser e2e")
port = _free_port()
server_script = tmp_path / "webui_chat_server.py"
browser_script = tmp_path / "webui_chat_browser.js"
server_script.write_text(
textwrap.dedent(
f"""
import uvicorn
from opensquilla.gateway.app import create_gateway_app
from opensquilla.gateway.config import AuthConfig, GatewayConfig
config = GatewayConfig(
host="127.0.0.1",
port={port},
auth=AuthConfig(mode="none"),
)
app = create_gateway_app(config)
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port={port}, log_level="warning")
"""
),
encoding="utf-8",
)
browser_script.write_text(
textwrap.dedent(
"""
const { chromium } = require("playwright");
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
const errors = [];
page.on("pageerror", err => errors.push(String(err)));
const response = await page.goto(process.env.TARGET_URL, {
waitUntil: "domcontentloaded",
timeout: 30000,
});
await page.waitForSelector("#chat-textarea", { timeout: 15000 });
const status = await page.evaluate(async () => {
const res = await fetch("/api/system/status");
return await res.json();
});
const bodyText = await page.locator("body").innerText();
const result = {
statusCode: response ? response.status() : 0,
title: await page.title(),
textareaCount: await page.locator("#chat-textarea").count(),
sendButtonCount: await page.locator("#chat-btn-send").count(),
activeChatNav: await page.locator('.nav-item.is-active[data-path="/chat"]').count(),
gatewayStatus: status.status,
authMode: status.auth_mode,
hasRemovedToolName:
bodyText.includes("generate_image") ||
bodyText.includes("spawn_subagent") ||
bodyText.includes("send_message"),
pageErrors: errors,
};
await browser.close();
console.log(JSON.stringify(result));
})().catch(err => {
console.error(err && err.stack ? err.stack : String(err));
process.exit(1);
});
"""
),
encoding="utf-8",
)
env = os.environ.copy()
env["OPENSQUILLA_STATE_DIR"] = str(tmp_path / "state")
env["OPENSQUILLA_LOG_DIR"] = str(tmp_path / "logs")
server = subprocess.Popen(
[sys.executable, str(server_script)],
cwd=Path.cwd(),
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
)
try:
_wait_for_health(port, server)
_install_playwright(tmp_path)
result = subprocess.run(
[_node(), str(browser_script)],
cwd=tmp_path,
env=dict(env, TARGET_URL=f"http://127.0.0.1:{port}/control/chat"),
check=False,
capture_output=True,
text=True,
timeout=60,
)
assert result.returncode == 0, result.stderr or result.stdout
payload = json.loads(result.stdout.strip().splitlines()[-1])
finally:
_stop_process(server)
assert payload == {
"statusCode": 200,
"title": "OpenSquilla Control",
"textareaCount": 1,
"sendButtonCount": 1,
"activeChatNav": 1,
"gatewayStatus": "running",
"authMode": "none",
"hasRemovedToolName": False,
"pageErrors": [],
}
def test_replay_gap_history_refresh_preserves_scroll_in_real_browser(tmp_path: Path) -> None:
if os.environ.get("OPENSQUILLA_WEBUI_BROWSER_CHAT_E2E") != "1":
pytest.skip("set OPENSQUILLA_WEBUI_BROWSER_CHAT_E2E=1 to run chat browser e2e")
port = _free_port()
server_script = tmp_path / "webui_gap_scroll_server.py"
browser_script = tmp_path / "webui_gap_scroll_browser.js"
server_script.write_text(
textwrap.dedent(
f"""
import uvicorn
from opensquilla.gateway.app import create_gateway_app
from opensquilla.gateway.config import AuthConfig, GatewayConfig
config = GatewayConfig(
host="127.0.0.1",
port={port},
auth=AuthConfig(mode="none"),
)
app = create_gateway_app(config)
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port={port}, log_level="warning")
"""
),
encoding="utf-8",
)
browser_script.write_text(
textwrap.dedent(
r"""
const { chromium } = require("playwright");
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1365, height: 768 } });
const errors = [];
page.on("pageerror", err => errors.push(String(err)));
page.on("console", msg => {
if (msg.type() === "error") errors.push(msg.text());
});
await page.goto(process.env.TARGET_URL, {
waitUntil: "domcontentloaded",
timeout: 30000,
});
await page.waitForFunction(
() =>
typeof App !== "undefined" &&
App.getRpc &&
App.getRpc()?.state === "connected",
{ timeout: 15000 }
);
await page.evaluate(() => {
const rpc = App.getRpc();
const originalCall = rpc.call.bind(rpc);
const messages = [];
for (let i = 1; i <= 70; i += 1) {
const label = i % 2 ? "User" : "Assistant";
messages.push({
role: i % 2 ? "user" : "assistant",
text: `${label} history row ${String(i).padStart(2, "0")} `.repeat(8),
timestamp: `2026-06-03T00:${String(i % 60).padStart(2, "0")}:00.000Z`,
message_id: `gap-probe-${i}`,
});
}
window.__gapProbe = { subscribeCount: 0, historyCount: 0, gapInjected: false };
rpc.call = async (method, params) => {
if (method === "chat.history") {
window.__gapProbe.historyCount += 1;
return {
messages,
has_more: false,
history_scope: "complete",
oldest_cursor: null,
newest_cursor: null,
compaction_summaries: [],
};
}
if (method === "sessions.messages.subscribe") {
window.__gapProbe.subscribeCount += 1;
if (window.__gapProbe.subscribeCount <= 1) {
return {
subscribed: true,
key: params && params.key,
current_stream_seq: 12,
replay_complete: true,
replayed_count: 0,
run_status: "idle",
};
}
window.__gapProbe.gapInjected = true;
return {
subscribed: true,
key: params && params.key,
current_stream_seq: 18,
replay_complete: false,
replay_gap_reason: "stream_buffer_empty",
replayed_count: 0,
run_status: "idle",
};
}
return originalCall(method, params);
};
});
await page.evaluate(() =>
Router.navigate(
"/chat?session=" +
encodeURIComponent("agent:main:webchat:gap-playwright-regression")
)
);
await page.waitForSelector("#chat-thread .msg", { timeout: 15000 });
await page.waitForFunction(
() => document.querySelectorAll("#chat-thread .msg").length >= 60,
{ timeout: 15000 }
);
const before = await page.evaluate(() => {
const thread = document.querySelector("#chat-thread");
thread.scrollTop = 120;
return {
scrollTop: thread.scrollTop,
scrollHeight: thread.scrollHeight,
clientHeight: thread.clientHeight,
bottomDistance: thread.scrollHeight - thread.clientHeight - thread.scrollTop,
messageCount: document.querySelectorAll("#chat-thread .msg").length,
};
});
await page.evaluate(() => {
const handlers = App.getRpc()._listeners.get("_state");
if (handlers) handlers.forEach(h => h("connected"));
});
await page.waitForFunction(
() => window.__gapProbe && window.__gapProbe.gapInjected === true,
{ timeout: 15000 }
);
await page.waitForFunction(
() => window.__gapProbe && window.__gapProbe.historyCount >= 3,
{ timeout: 15000 }
);
const after = await page.evaluate(() => {
const thread = document.querySelector("#chat-thread");
return {
scrollTop: thread.scrollTop,
scrollHeight: thread.scrollHeight,
clientHeight: thread.clientHeight,
bottomDistance: thread.scrollHeight - thread.clientHeight - thread.scrollTop,
messageCount: document.querySelectorAll("#chat-thread .msg").length,
toasts: Array.from(document.querySelectorAll(".toast")).map(el =>
el.textContent.trim()
),
probe: window.__gapProbe,
};
});
await browser.close();
console.log(JSON.stringify({ before, after, errors }));
})().catch(err => {
console.error(err && err.stack ? err.stack : String(err));
process.exit(1);
});
"""
),
encoding="utf-8",
)
env = os.environ.copy()
env["OPENSQUILLA_STATE_DIR"] = str(tmp_path / "state")
env["OPENSQUILLA_LOG_DIR"] = str(tmp_path / "logs")
server = subprocess.Popen(
[sys.executable, str(server_script)],
cwd=Path.cwd(),
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
)
try:
_wait_for_health(port, server)
_install_playwright(tmp_path)
result = subprocess.run(
[_node(), str(browser_script)],
cwd=tmp_path,
env=dict(env, TARGET_URL=f"http://127.0.0.1:{port}/control/overview"),
check=False,
capture_output=True,
text=True,
timeout=90,
)
assert result.returncode == 0, result.stderr or result.stdout
payload = json.loads(result.stdout.strip().splitlines()[-1])
finally:
_stop_process(server)
assert payload["errors"] == [], payload["errors"]
assert payload["before"]["messageCount"] >= 60
assert payload["after"]["messageCount"] == payload["before"]["messageCount"]
assert payload["after"]["probe"]["gapInjected"] is True
assert payload["after"]["bottomDistance"] > 1000, payload
assert payload["after"]["scrollTop"] <= payload["before"]["scrollTop"] + 80, payload
assert "Session stream gap detected; reloading transcript." not in payload["after"]["toasts"]
def test_chat_topbar_one_row_layout_in_real_browser(tmp_path: Path) -> None:
if os.environ.get("OPENSQUILLA_WEBUI_BROWSER_CHAT_E2E") != "1":
pytest.skip("set OPENSQUILLA_WEBUI_BROWSER_CHAT_E2E=1 to run chat browser e2e")
port = _free_port()
server_script = tmp_path / "webui_chat_topbar_server.py"
browser_script = tmp_path / "webui_chat_topbar_browser.js"
server_script.write_text(
textwrap.dedent(
f"""
import uvicorn
from opensquilla.gateway.app import create_gateway_app
from opensquilla.gateway.config import AuthConfig, GatewayConfig
config = GatewayConfig(
host="127.0.0.1",
port={port},
auth=AuthConfig(mode="none"),
)
app = create_gateway_app(config)
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port={port}, log_level="warning")
"""
),
encoding="utf-8",
)
browser_script.write_text(
textwrap.dedent(
r"""
const { chromium } = require("playwright");
async function emit(page, event, payload, meta = {}) {
await page.evaluate(
({ event, payload, meta }) => {
const rpc = App.getRpc();
const handlers = rpc._listeners.get(event);
if (handlers) handlers.forEach(h => h(payload, meta));
const wild = rpc._listeners.get("*");
if (wild) wild.forEach(h => h(event, payload, meta));
},
{ event, payload, meta }
);
}
async function measure(page, viewport) {
await page.setViewportSize({ width: viewport.width, height: viewport.height });
const session =
"agent:main:webchat:very-long-session-key-with-extra-segments-1234567890";
await page.goto(process.env.TARGET_URL + "?session=" + encodeURIComponent(session), {
waitUntil: "domcontentloaded",
timeout: 30000,
});
await page.waitForSelector("#chat-textarea", { timeout: 15000 });
await page.waitForSelector("#topbar-center:not(.hidden)", { timeout: 5000 });
await emit(page, "session.event.done", {
text: "",
contextStatus: {
contextTokens: 90000,
contextWindowTokens: 100000,
pressure: 0.9,
},
});
await emit(page, "task.running", { task_id: "topbar-task", session_key: session });
await page.waitForFunction(() => {
const el = document.querySelector("#chat-run-status");
return el && el.textContent.trim() === "Running";
});
if (viewport.showApproval) {
await page.evaluate(() => {
const inline = document.querySelector("#approval-inline");
inline.textContent = "Approval required";
inline.setAttribute("aria-label", "Approval required");
inline.title = "Approval required";
inline.classList.remove("hidden");
});
}
await page.waitForTimeout(80);
const snapshot = await page.evaluate(() => {
const box = (selector) => {
const el = document.querySelector(selector);
if (!el) return null;
const r = el.getBoundingClientRect();
return {
left: r.left,
right: r.right,
top: r.top,
bottom: r.bottom,
width: r.width,
height: r.height,
visible:
r.width > 0 &&
r.height > 0 &&
getComputedStyle(el).display !== "none" &&
getComputedStyle(el).visibility !== "hidden",
};
};
const center = document.querySelector("#topbar-center");
const label = document.querySelector(".topbar-center .chat-label");
const warning = document.querySelector("#chat-ctx-warn");
const chipKey = document.querySelector("#chat-session-chip-key");
const centerBox = box("#topbar-center");
const topbarBox = box(".topbar");
const topbarRightBox = box(".topbar-right");
const childBoxes = Array.from(center ? center.children : []).map((el) => {
const r = el.getBoundingClientRect();
return {
id: el.id || el.className || el.tagName,
left: r.left,
right: r.right,
width: r.width,
display: getComputedStyle(el).display,
};
});
return {
width: window.innerWidth,
scrollWidth: document.documentElement.scrollWidth,
topbarCount: document.querySelectorAll(".topbar").length,
chatHeaderCount: document.querySelectorAll(".chat-header").length,
centerHidden: center ? center.classList.contains("hidden") : true,
centerBox,
topbarBox,
topbarRightBox,
chip: box("#chat-session-chip"),
chipKeyText: chipKey ? chipKey.textContent : "",
copy: box("#chat-session-copy"),
runStatus: box("#chat-run-status"),
runStatusText: document.querySelector("#chat-run-status")?.textContent || "",
warning: box("#chat-ctx-warn"),
warningText: warning ? warning.textContent : "",
labelDisplay: label ? getComputedStyle(label).display : "",
theme: box("#theme-toggle"),
approval: box("#approval-inline"),
approvalText: document.querySelector("#approval-inline")?.textContent || "",
approvalLabel:
document.querySelector("#approval-inline")?.getAttribute("aria-label") || "",
approvalCount: document.querySelectorAll("#approval-inline").length,
topbarChildren: childBoxes,
};
});
await page.locator("#theme-toggle").click();
const themeAfterClick = await page.evaluate(
() => document.documentElement.getAttribute("data-theme")
);
return { name: viewport.name, snapshot, themeAfterClick };
}
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
const errors = [];
page.on("pageerror", err => errors.push(String(err)));
const viewports = [
{ name: "desktop", width: 1365, height: 768 },
{ name: "wide", width: 1600, height: 900 },
{ name: "tablet", width: 768, height: 900 },
{ name: "mobile", width: 390, height: 844, showApproval: true },
];
const layouts = [];
for (const viewport of viewports) layouts.push(await measure(page, viewport));
await page.setViewportSize({ width: 1365, height: 768 });
await page.goto(process.env.TARGET_URL, {
waitUntil: "domcontentloaded",
timeout: 30000,
});
await page.waitForSelector("#chat-textarea", { timeout: 15000 });
await page.evaluate(() => Router.navigate("/overview"));
await page.waitForSelector(".ov-stage__title", { timeout: 5000 });
const cleanup = await page.evaluate(() => ({
centerHidden:
document.querySelector("#topbar-center")?.classList.contains("hidden"),
centerText: document.querySelector("#topbar-center")?.textContent.trim(),
chatIds:
document.querySelectorAll(
"#chat-session-chip, #chat-session-chip-key, " +
"#chat-session-copy, #chat-run-status, #chat-ctx-warn"
).length,
}));
await browser.close();
console.log(JSON.stringify({ layouts, cleanup, pageErrors: errors }));
})().catch(err => {
console.error(err && err.stack ? err.stack : String(err));
process.exit(1);
});
"""
),
encoding="utf-8",
)
env = os.environ.copy()
env["OPENSQUILLA_STATE_DIR"] = str(tmp_path / "state")
env["OPENSQUILLA_LOG_DIR"] = str(tmp_path / "logs")
server = subprocess.Popen(
[sys.executable, str(server_script)],
cwd=Path.cwd(),
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
)
try:
_wait_for_health(port, server)
_install_playwright(tmp_path)
result = subprocess.run(
[_node(), str(browser_script)],
cwd=tmp_path,
env=dict(env, TARGET_URL=f"http://127.0.0.1:{port}/control/chat"),
check=False,
capture_output=True,
text=True,
timeout=120,
)
assert result.returncode == 0, result.stderr or result.stdout
payload = json.loads(result.stdout.strip().splitlines()[-1])
finally:
_stop_process(server)
assert payload["pageErrors"] == []
assert payload["cleanup"] == {"centerHidden": True, "centerText": "", "chatIds": 0}
layouts = {item["name"]: item["snapshot"] for item in payload["layouts"]}
for name, snap in layouts.items():
assert snap["scrollWidth"] <= snap["width"], (name, snap)
assert snap["topbarCount"] == 1, (name, snap)
assert snap["chatHeaderCount"] == 0, (name, snap)
assert snap["centerHidden"] is False, (name, snap)
assert snap["chip"]["visible"], (name, snap)
assert snap["copy"]["visible"], (name, snap)
assert snap["runStatus"]["visible"], (name, snap)
assert snap["runStatusText"] == "Running", (name, snap)
assert snap["theme"]["visible"], (name, snap)
assert snap["approvalCount"] == 1, (name, snap)
assert snap["chipKeyText"].startswith("agent:main:webchat:very-long-session-key")
assert snap["centerBox"]["right"] <= snap["topbarRightBox"]["left"] + 1, (name, snap)
center = snap["centerBox"]
for child in snap["topbarChildren"]:
if child["display"] == "none":
continue
assert child["left"] >= center["left"] - 1, (name, child, center)
assert child["right"] <= center["right"] + 1, (name, child, center)
for name in ("desktop", "wide"):
snap = layouts[name]
assert snap["labelDisplay"] != "none"
assert snap["warning"]["visible"], (name, snap)
assert snap["warningText"].startswith("Request ctx 90%"), (name, snap)
assert layouts["tablet"]["labelDisplay"] == "none"
assert layouts["mobile"]["labelDisplay"] == "none"
assert layouts["mobile"]["approval"]["visible"], layouts["mobile"]
assert layouts["mobile"]["approval"]["width"] <= 40, layouts["mobile"]
assert layouts["mobile"]["approvalText"] == "Approval required"
assert layouts["mobile"]["approvalLabel"] == "Approval required"
assert layouts["mobile"]["warning"]["visible"] is False
assert all(item["themeAfterClick"] in {"dark", "light"} for item in payload["layouts"])
def test_chat_slash_menu_aligns_with_composer_in_real_browser(tmp_path: Path) -> None:
if os.environ.get("OPENSQUILLA_WEBUI_BROWSER_CHAT_E2E") != "1":
pytest.skip("set OPENSQUILLA_WEBUI_BROWSER_CHAT_E2E=1 to run chat browser e2e")
port = _free_port()
server_script = tmp_path / "webui_chat_slash_server.py"
browser_script = tmp_path / "webui_chat_slash_browser.js"
server_script.write_text(
textwrap.dedent(
f"""
import uvicorn
from opensquilla.gateway.app import create_gateway_app
from opensquilla.gateway.config import AuthConfig, GatewayConfig
config = GatewayConfig(
host="127.0.0.1",
port={port},
auth=AuthConfig(mode="none"),
)
app = create_gateway_app(config)
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port={port}, log_level="warning")
"""
),
encoding="utf-8",
)
browser_script.write_text(
textwrap.dedent(
r"""
const { chromium } = require("playwright");
async function waitRpc(page) {
await page.waitForFunction(
() =>
typeof App !== "undefined" &&
App.getRpc &&
App.getRpc()?.state === "connected",
{ timeout: 15000 }
);
}
async function installMocks(page) {
await page.evaluate(() => {
const rpc = App.getRpc();
const originalCall = rpc.call.bind(rpc);
rpc.call = (method, params = {}) => {
if (method === "tools.search_provider") {
return Promise.resolve({ provider: "none" });
}
if (method === "config.get") {
return Promise.resolve({
permissions: { default_mode: "ask" },
squilla_router: { enabled: true, rollout_phase: "full", tiers: {} },
});
}
if (method === "chat.history") {
return Promise.resolve({
messages: [],
history_scope: "complete",
has_more: false,
});
}
if (method === "sessions.messages.subscribe") {
return Promise.resolve({
subscribed: true,
key: params.key,
current_stream_seq: 0,
replay_complete: true,
replayed_count: 0,
run_status: "idle",
});
}
if (method === "commands.list_for_surface") {
return Promise.resolve({
commands: [
{
name: "/compact",
description: [
"Compact older context in the current session with an intentionally",
"long command description that must truncate instead of stretching",
"the composer tray.",
].join(" "),
execution: { action: "sessions.contextCompact" },
},
],
});
}
if (method === "sessions.contextCompact") {
return Promise.resolve({ compacted: false, reason: "no_entries" });
}
return originalCall(method, params);
};
});
}
async function openChat(page, viewport) {
await page.setViewportSize({ width: viewport.width, height: viewport.height });
await page.goto(process.env.TARGET_URL, {
waitUntil: "domcontentloaded",
timeout: 30000,
});
await waitRpc(page);
await installMocks(page);
await page.evaluate(() =>
Router.navigate("/chat?session=agent:main:webchat:slash-layout")
);
await page.waitForSelector("#chat-textarea", { timeout: 15000 });
await page.locator("#chat-textarea").fill("/compact");
await page.waitForSelector(".chat-slash:not(.hidden)", { timeout: 5000 });
await page.waitForTimeout(80);
}
async function measure(page, viewport) {
await openChat(page, viewport);
const layout = await page.evaluate(() => {
const box = (selector) => {
const el = document.querySelector(selector);
const r = el.getBoundingClientRect();
return {
left: r.left,
right: r.right,
width: r.width,
height: r.height,
visible:
r.width > 0 &&
r.height > 0 &&
getComputedStyle(el).display !== "none" &&
getComputedStyle(el).visibility !== "hidden",
};
};
const desc = document.querySelector(".chat-slash-desc");
const slash = document.querySelector(".chat-slash");
const input = document.querySelector(".chat-input-bar");
return {
width: window.innerWidth,
scrollWidth: document.documentElement.scrollWidth,
slash: box(".chat-slash"),
input: box(".chat-input-bar"),
composer: box("#chat-composer"),
attach: box("#chat-btn-attach"),
newChat: box("#chat-btn-new"),
exportButton: box("#chat-btn-export"),
send: box("#chat-btn-send"),
slashParentId: slash?.parentElement?.id || "",
slashBeforeInput: !!(
slash &&
input &&
slash.compareDocumentPosition(input) &
Node.DOCUMENT_POSITION_FOLLOWING
),
descClientWidth: desc ? desc.clientWidth : 0,
descScrollWidth: desc ? desc.scrollWidth : 0,
};
});
await page.locator("#chat-textarea").press("ArrowDown");
const afterArrow = await page.locator(".chat-slash:not(.hidden)").count();
await page.locator("#chat-textarea").press("Escape");
const afterEscape = await page.locator(".chat-slash:not(.hidden)").count();
await page.locator("#chat-textarea").fill("/compact");
await page.waitForSelector(".chat-slash:not(.hidden)", { timeout: 5000 });
await page.locator("#chat-textarea").press("Enter");
await page.waitForFunction(
() => document.querySelector("#chat-textarea").value === ""
);
const afterEnterValue = await page.locator("#chat-textarea").inputValue();
return {
name: viewport.name,
layout,
afterArrow,
afterEscape,
afterEnterValue,
};
}
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
const errors = [];
page.on("pageerror", err => errors.push(String(err)));
const viewports = [
{ name: "desktop", width: 1365, height: 768 },
{ name: "wide", width: 2048, height: 900 },
{ name: "mobile", width: 390, height: 844 },
];
const layouts = [];
for (const viewport of viewports) layouts.push(await measure(page, viewport));
await browser.close();
console.log(JSON.stringify({ layouts, pageErrors: errors }));
})().catch(err => {
console.error(err && err.stack ? err.stack : String(err));
process.exit(1);
});
"""
),
encoding="utf-8",
)
env = os.environ.copy()
env["OPENSQUILLA_STATE_DIR"] = str(tmp_path / "state")
env["OPENSQUILLA_LOG_DIR"] = str(tmp_path / "logs")
server = subprocess.Popen(
[sys.executable, str(server_script)],
cwd=Path.cwd(),
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
)
try:
_wait_for_health(port, server)
_install_playwright(tmp_path)
result = subprocess.run(
[_node(), str(browser_script)],
cwd=tmp_path,
env=dict(env, TARGET_URL=f"http://127.0.0.1:{port}/control/"),
check=False,
capture_output=True,
text=True,
timeout=120,
)
assert result.returncode == 0, result.stderr or result.stdout
payload = json.loads(result.stdout.strip().splitlines()[-1])
finally:
_stop_process(server)
assert payload["pageErrors"] == []
layouts = {item["name"]: item for item in payload["layouts"]}
for name, item in layouts.items():
layout = item["layout"]
assert layout["scrollWidth"] <= layout["width"], (name, layout)
assert layout["slash"]["visible"], (name, layout)
assert layout["slashParentId"] == "chat-composer", (name, layout)
assert layout["slashBeforeInput"] is True, (name, layout)
if name == "mobile":
assert layout["slash"]["left"] >= layout["composer"]["left"], (name, layout)
assert layout["slash"]["right"] <= layout["composer"]["right"], (name, layout)
else:
assert abs(layout["slash"]["left"] - layout["input"]["left"]) <= 1, (name, layout)
assert abs(layout["slash"]["right"] - layout["input"]["right"]) <= 1, (name, layout)
assert layout["descScrollWidth"] > layout["descClientWidth"], (name, layout)
assert item["afterArrow"] == 1, item
assert item["afterEscape"] == 0, item
assert item["afterEnterValue"] == "", item
assert layout["attach"]["visible"], (name, layout)
assert layout["newChat"]["visible"], (name, layout)
assert layout["send"]["visible"], (name, layout)
assert layouts["desktop"]["layout"]["slash"]["width"] < 1000
assert layouts["wide"]["layout"]["slash"]["width"] < 1000
assert layouts["mobile"]["layout"]["exportButton"]["visible"] is False
def test_chat_escape_in_composer_aborts_streaming_turn_in_real_browser(
tmp_path: Path,
) -> None:
if os.environ.get("OPENSQUILLA_WEBUI_BROWSER_CHAT_E2E") != "1":
pytest.skip("set OPENSQUILLA_WEBUI_BROWSER_CHAT_E2E=1 to run chat browser e2e")
port = _free_port()
server_script = tmp_path / "webui_escape_abort_server.py"
browser_script = tmp_path / "webui_escape_abort_browser.js"
server_script.write_text(
textwrap.dedent(
f"""
import uvicorn
from opensquilla.gateway.app import create_gateway_app
from opensquilla.gateway.config import AuthConfig, GatewayConfig
config = GatewayConfig(
host="127.0.0.1",
port={port},
auth=AuthConfig(mode="none"),
)
app = create_gateway_app(config)
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port={port}, log_level="warning")
"""
),
encoding="utf-8",
)
browser_script.write_text(
textwrap.dedent(
r"""
const { chromium } = require("playwright");
async function waitRpc(page) {
await page.waitForFunction(
() =>
typeof App !== "undefined" &&
App.getRpc &&
App.getRpc()?.state === "connected",
{ timeout: 15000 }
);
}
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
const errors = [];
page.on("pageerror", err => errors.push(String(err)));
await page.goto(process.env.TARGET_URL, {
waitUntil: "domcontentloaded",
timeout: 30000,
});
await page.waitForSelector("#chat-textarea", { timeout: 15000 });
await waitRpc(page);
await page.evaluate(() => {
const rpc = App.getRpc();
const originalCall = rpc.call.bind(rpc);
window.__escapeAbort = { sendCalls: [], abortCalls: [] };
rpc.call = (method, params = {}) => {
if (method === "tools.search_provider") {
return Promise.resolve({ provider: "none" });
}
if (method === "config.get") {
return Promise.resolve({
permissions: { default_mode: "ask" },
squilla_router: { enabled: true, rollout_phase: "full", tiers: {} },
});
}
if (method === "chat.history") {
return Promise.resolve({
messages: [],
history_scope: "complete",
has_more: false,
});
}
if (method === "sessions.messages.subscribe") {
return Promise.resolve({
subscribed: true,
key: params.key,
current_stream_seq: 0,
replay_complete: true,
replayed_count: 0,
run_status: "idle",
});
}
if (method === "chat.send") {
window.__escapeAbort.sendCalls.push(params);
return Promise.resolve({ task_id: "escape-abort-task" });
}
if (method === "chat.abort") {
window.__escapeAbort.abortCalls.push(params);
return Promise.resolve({ aborted: true, key: params.sessionKey });
}
return originalCall(method, params);
};
});
await page.evaluate(() =>
Router.navigate("/chat?session=agent:main:webchat:escape-abort")
);
await page.waitForSelector("#chat-textarea", { timeout: 15000 });
await page.fill("#chat-textarea", "turn to abort from textarea");
await page.click("#chat-btn-send");
await page.waitForFunction(
() =>
window.__escapeAbort.sendCalls.length === 1 &&
OpenSquillaChatDiag.snapshot().isStreaming === true,
null, { timeout: 5000 }
);
await page.focus("#chat-textarea");
const before = await page.evaluate(() => ({
focusId: document.activeElement?.id || "",
isStreaming: OpenSquillaChatDiag.snapshot().isStreaming,
stopVisible:
!document.querySelector("#chat-btn-stop")?.classList.contains("hidden"),
}));
await page.locator("#chat-textarea").press("Escape");
await page.waitForFunction(
() => window.__escapeAbort.abortCalls.length === 1,
null, { timeout: 5000 }
);
await page.waitForFunction(
() => OpenSquillaChatDiag.snapshot().isStreaming === false,
null, { timeout: 5000 }
);
const afterState = await page.evaluate(() => ({
isStreaming: OpenSquillaChatDiag.snapshot().isStreaming,
streamingCount: document.querySelectorAll(".msg.streaming").length,
thinkingCount: document.querySelectorAll(".msg.thinking").length,
stopVisible:
!document.querySelector("#chat-btn-stop")?.classList.contains("hidden"),
abortCalls: window.__escapeAbort.abortCalls,
}));
const after = { ...afterState, pageErrors: errors };
await browser.close();
console.log(JSON.stringify({ before, after }));
})().catch(err => {
console.error(err && err.stack ? err.stack : String(err));
process.exit(1);
});
"""
),
encoding="utf-8",
)
env = os.environ.copy()
env["OPENSQUILLA_STATE_DIR"] = str(tmp_path / "state")
env["OPENSQUILLA_LOG_DIR"] = str(tmp_path / "logs")
server = subprocess.Popen(
[sys.executable, str(server_script)],
cwd=Path.cwd(),
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
)
try:
_wait_for_health(port, server)
_install_playwright(tmp_path)
result = subprocess.run(
[_node(), str(browser_script)],
cwd=tmp_path,
env=dict(env, TARGET_URL=f"http://127.0.0.1:{port}/control/chat"),
check=False,
capture_output=True,
text=True,
timeout=90,
)
assert result.returncode == 0, result.stderr or result.stdout
payload = json.loads(result.stdout.strip().splitlines()[-1])
finally:
_stop_process(server)
before = payload["before"]
after = payload["after"]
assert before["focusId"] == "chat-textarea", before
assert before["isStreaming"] is True, before
assert before["stopVisible"] is True, before
assert after["abortCalls"] == [
{
"sessionKey": "agent:main:webchat:escape-abort",
"source": "webui_escape",
}
], after
assert after["isStreaming"] is False, after
assert after["streamingCount"] == 0, after
assert after["thinkingCount"] == 0, after
assert after["stopVisible"] is False, after
assert after["pageErrors"] == [], after
def test_chat_tool_result_full_view_handles_large_payloads_in_real_browser(
tmp_path: Path,
) -> None:
if os.environ.get("OPENSQUILLA_WEBUI_BROWSER_CHAT_E2E") != "1":
pytest.skip("set OPENSQUILLA_WEBUI_BROWSER_CHAT_E2E=1 to run chat browser e2e")
port = _free_port()
server_script = tmp_path / "webui_chat_tool_result_server.py"
browser_script = tmp_path / "webui_chat_tool_result_browser.js"
server_script.write_text(
textwrap.dedent(
f"""
import uvicorn
from opensquilla.gateway.app import create_gateway_app
from opensquilla.gateway.config import AuthConfig, GatewayConfig
config = GatewayConfig(
host="127.0.0.1",
port={port},
auth=AuthConfig(mode="none"),
)
app = create_gateway_app(config)
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port={port}, log_level="warning")
"""
),
encoding="utf-8",
)
browser_script.write_text(
textwrap.dedent(
r"""
const { chromium } = require("playwright");
async function waitRpc(page) {
await page.waitForFunction(
() =>
typeof App !== "undefined" &&
App.getRpc &&
App.getRpc()?.state === "connected",
{ timeout: 15000 }
);
}
async function installMocks(page) {
await page.evaluate(() => {
const rpc = App.getRpc();
const originalCall = rpc.call.bind(rpc);
rpc.call = (method, params = {}) => {
if (method === "tools.search_provider") {
return Promise.resolve({ provider: "none" });
}
if (method === "config.get") {
return Promise.resolve({
permissions: { default_mode: "ask" },
squilla_router: { enabled: true, rollout_phase: "full", tiers: {} },
});
}
if (method === "chat.history") {
const longOutput = [
"labubu-1.jpg -> 1788872 chars base64",
"labubu-2.jpg -> 836304 chars base64",
"labubu-3.jpg -> 2109592 chars base64",
"single-unbroken-token-" + "x".repeat(1800),
].join("\\n");
return Promise.resolve({
messages: [
{
role: "assistant",
text: "",
timestamp: "2026-05-31T00:00:00Z",
tool_calls: [
{
type: "tool_use",
name: "execute_code",
tool_use_id: "tool-large-result",
input: { code: "print_large_payload()" },
},
{
type: "tool_result",
tool_use_id: "tool-large-result",
result: {
exit_code: 0,
stdout: longOutput,
stderr: "",
timed_out: false,
elapsed_ms: 213,
},
},
],
},
],
history_scope: "complete",
has_more: false,
});
}
if (method === "sessions.messages.subscribe") {
return Promise.resolve({
subscribed: true,
key: params.key,
current_stream_seq: 0,
replay_complete: true,
replayed_count: 0,
run_status: "idle",
});
}
return originalCall(method, params);
};
});
}
async function measure(page, viewport) {
await page.setViewportSize({ width: viewport.width, height: viewport.height });
await page.goto(process.env.TARGET_URL, {
waitUntil: "domcontentloaded",
timeout: 30000,
});
await waitRpc(page);
await installMocks(page);
await page.evaluate(() =>
Router.navigate("/chat?session=agent:main:webchat:tool-result-full")
);
await page.waitForSelector(".chat-tools-summary", { timeout: 15000 });
await page.locator(".chat-tools-summary").first().click();
await page.waitForSelector(".chat-tool-view-btn", {
state: "visible",
timeout: 5000,
});
await page.locator(".chat-tool-view-btn").first().click();
await page.waitForSelector(".modal .chat-tool-result-full", { timeout: 5000 });
const layout = await page.evaluate(() => {
const rect = (selector) => {
const el = document.querySelector(selector);
const r = el.getBoundingClientRect();
return {
left: r.left,
right: r.right,
top: r.top,
bottom: r.bottom,
width: r.width,
height: r.height,
};
};
const pre = document.querySelector(".chat-tool-result-full");
const button = document.querySelector(".chat-tool-view-btn");
return {
width: window.innerWidth,
scrollWidth: document.documentElement.scrollWidth,
modal: rect(".modal"),
pre: rect(".chat-tool-result-full"),
preScrollWidth: pre.scrollWidth,
preClientWidth: pre.clientWidth,
buttonType: button ? button.getAttribute("type") : "",
text: pre ? pre.textContent.slice(0, 80) : "",
};
});
await page.locator(".modal-foot button", { hasText: "Close" }).click();
await page.waitForSelector(".modal-backdrop", { state: "detached", timeout: 5000 });
const afterClose = await page.locator(".modal-backdrop").count();
return { name: viewport.name, layout, afterClose };
}
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
const errors = [];
page.on("pageerror", err => errors.push(String(err)));
const viewports = [
{ name: "desktop", width: 1365, height: 768 },
{ name: "mobile", width: 390, height: 844 },
];
const layouts = [];
for (const viewport of viewports) layouts.push(await measure(page, viewport));
await browser.close();
console.log(JSON.stringify({ layouts, pageErrors: errors }));
})().catch(err => {
console.error(err && err.stack ? err.stack : String(err));
process.exit(1);
});
"""
),
encoding="utf-8",
)
env = os.environ.copy()
env["OPENSQUILLA_STATE_DIR"] = str(tmp_path / "state")
env["OPENSQUILLA_LOG_DIR"] = str(tmp_path / "logs")
server = subprocess.Popen(
[sys.executable, str(server_script)],
cwd=Path.cwd(),
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
)
try:
_wait_for_health(port, server)
_install_playwright(tmp_path)
result = subprocess.run(
[_node(), str(browser_script)],
cwd=tmp_path,
env=dict(env, TARGET_URL=f"http://127.0.0.1:{port}/control/"),
check=False,
capture_output=True,
text=True,
timeout=120,
)
assert result.returncode == 0, result.stderr or result.stdout
payload = json.loads(result.stdout.strip().splitlines()[-1])
finally:
_stop_process(server)
assert payload["pageErrors"] == []
layouts = {item["name"]: item for item in payload["layouts"]}
for name, item in layouts.items():
layout = item["layout"]
assert item["afterClose"] == 0, item
assert layout["scrollWidth"] <= layout["width"], (name, layout)
assert layout["buttonType"] == "button", (name, layout)
assert "labubu-1.jpg" in layout["text"], (name, layout)
assert layout["modal"]["left"] >= 0, (name, layout)
assert layout["modal"]["right"] <= layout["width"], (name, layout)
assert layout["pre"]["right"] <= layout["modal"]["right"], (name, layout)
assert layout["preScrollWidth"] <= layout["preClientWidth"] + 2, (name, layout)
def test_chat_compaction_events_render_recoverable_toasts_in_real_browser(
tmp_path: Path,
) -> None:
if os.environ.get("OPENSQUILLA_WEBUI_BROWSER_CHAT_E2E") != "1":
pytest.skip("set OPENSQUILLA_WEBUI_BROWSER_CHAT_E2E=1 to run chat browser e2e")
port = _free_port()
server_script = tmp_path / "webui_chat_compaction_server.py"
browser_script = tmp_path / "webui_chat_compaction_browser.js"
server_script.write_text(
textwrap.dedent(
f"""
import uvicorn
from opensquilla.gateway.app import create_gateway_app
from opensquilla.gateway.config import AuthConfig, GatewayConfig
config = GatewayConfig(
host="127.0.0.1",
port={port},
auth=AuthConfig(mode="none"),
)
app = create_gateway_app(config)
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port={port}, log_level="warning")
"""
),
encoding="utf-8",
)
browser_script.write_text(
textwrap.dedent(
r"""
const { chromium } = require("playwright");
async function emitEvent(page, event, payload, meta = {}) {
await page.evaluate(
({ event, payload, meta }) => {
const rpc = App.getRpc();
const handlers = rpc._listeners.get(event);
if (handlers) {
handlers.forEach(h => h(payload, meta));
}
const wild = rpc._listeners.get("*");
if (wild) wild.forEach(h => h(event, payload, meta));
},
{ event, payload, meta }
);
}
async function emitCompaction(page, payload, meta = {}) {
return emitEvent(page, "session.event.compaction", payload, meta);
}
async function lastSeparatorText(page) {
return await page.evaluate(() => {
const separators = document.querySelectorAll(".chat-context-separator");
return separators.length
? separators[separators.length - 1].textContent
: "";
});
}
async function lastSeparatorPlacement(page) {
return await page.evaluate(() => {
const thread = document.querySelector("#chat-thread");
const separators = document.querySelectorAll(".chat-context-separator");
const separator = separators[separators.length - 1];
if (!thread || !separator) return null;
const threadBox = thread.getBoundingClientRect();
const separatorBox = separator.getBoundingClientRect();
return {
bottomGap: Math.round(threadBox.bottom - separatorBox.bottom),
topGap: Math.round(separatorBox.top - threadBox.top),
className: separator.className,
scrollTop: Math.round(thread.scrollTop),
scrollHeight: Math.round(thread.scrollHeight),
clientHeight: Math.round(thread.clientHeight),
};
});
}
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
const errors = [];
page.on("pageerror", err => errors.push(String(err)));
await page.goto(process.env.TARGET_URL, {
waitUntil: "domcontentloaded",
timeout: 30000,
});
await page.waitForSelector("#chat-textarea", { timeout: 15000 });
await page.waitForFunction(
() =>
typeof App !== "undefined" &&
App.getRpc &&
App.getRpc()?.state === "connected",
{ timeout: 15000 }
);
await page.evaluate(() => {
const rpc = App.getRpc();
const originalCall = rpc.call.bind(rpc);
const originalToast = UI.toast.bind(UI);
window.__compactUx = { chatCalls: [], toastCalls: [] };
UI.toast = (message, type = "info", duration = 3000) => {
window.__compactUx.toastCalls.push({ message, type, duration });
return originalToast(message, type, duration);
};
rpc.call = (method, params = {}) => {
if (method === "chat.send") {
window.__compactUx.chatCalls.push({ method, params });
return Promise.resolve({
task_id: "compact-ux-" + window.__compactUx.chatCalls.length,
});
}
return originalCall(method, params);
};
});
await emitCompaction(page, { status: "started", source: "manual" });
await page.waitForSelector(".chat-context-separator", { timeout: 5000 });
await page.waitForTimeout(600);
const startedStatusVisible = await lastSeparatorText(page);
const manualSeparatorStarted = startedStatusVisible;
const manualSeparatorStartedPlacement = await lastSeparatorPlacement(page);
await emitCompaction(page, { status: "skipped", source: "manual" });
await page.waitForTimeout(250);
const skippedStatusVisible = await lastSeparatorText(page);
await emitCompaction(page, {
status: "completed",
source: "manual",
tokens_before: 3500,
tokens_after: 1300,
});
await page.waitForFunction(
() => document.body.textContent.includes("context compacted"),
null, { timeout: 5000 }
);
await emitCompaction(
page,
{ status: "failed", source: "manual", message: "old replay" },
{ replayed: true }
);
await page.waitForTimeout(250);
await emitCompaction(page, { status: "started", source: "manual" });
await page.fill("#chat-textarea", "queued during compact");
await page.click("#chat-btn-send");
await page.waitForTimeout(150);
const queuedBeforeSkipped = await page.evaluate(
() => window.__compactUx.chatCalls.length
);
await emitCompaction(page, { status: "skipped", source: "manual" });
await page.waitForFunction(
() =>
window.__compactUx.chatCalls
.some(c => c.params.message === "queued during compact"),
null, { timeout: 5000 }
);
await emitEvent(page, "session.event.done", { text: "compact queued answer" });
await page.waitForTimeout(150);
await page.fill("#chat-textarea", "seed automatic compact turn");
await page.click("#chat-btn-send");
await page.waitForTimeout(150);
const callsBeforeAutomaticCompact = await page.evaluate(
() => window.__compactUx.chatCalls.length
);
await emitCompaction(page, {
status: "started",
source: "automatic",
phase: "preflight",
});
await page.waitForTimeout(800);
const automaticStartedStatusVisible = await lastSeparatorText(page);
const automaticSeparatorStarted = automaticStartedStatusVisible;
const automaticSeparatorStartedPlacement = await lastSeparatorPlacement(page);
await page.fill("#chat-textarea", "queued during automatic compact");
await page.click("#chat-btn-send");
await page.waitForTimeout(150);
const automaticQueuedBeforeCompleted = await page.evaluate(
() => window.__compactUx.chatCalls.length
);
await emitCompaction(page, {
status: "observed",
source: "automatic",
event: "compaction.chunk_summarized",
tokens_before: 5000,
tokens_after: 2600,
});
await page.waitForTimeout(800);
const automaticObservedStatusVisible = await lastSeparatorText(page);
const automaticSeparatorObserved = automaticObservedStatusVisible;
await emitCompaction(page, {
status: "completed",
source: "automatic",
tokens_before: 5000,
tokens_after: 1800,
});
await page.waitForTimeout(800);
const automaticSeparatorCompleted = await lastSeparatorText(page);
const automaticDidNotDrainBeforeDone = await page.evaluate(
expected => window.__compactUx.chatCalls.length === expected,
callsBeforeAutomaticCompact
);
await emitEvent(page, "session.event.done", { text: "automatic compact answer" });
await page.waitForFunction(
() =>
window.__compactUx.chatCalls
.some(c => c.params.message === "queued during automatic compact"),
null, { timeout: 5000 }
);
// Wait >1500ms so the dedup guard resets before emitting another
// "started" event with the same source signature.
await page.waitForTimeout(1600);
await emitCompaction(page, {
status: "started",
source: "automatic",
phase: "preflight",
});
await page.waitForTimeout(800);
await emitCompaction(page, {
status: "skipped",
source: "automatic",
phase: "preflight",
reason: "structured_content_noop",
user_visible: false,
});
await page.waitForTimeout(300);
const automaticNoopSeparatorHidden = await page
.locator(".chat-context-separator")
.count();
const automaticNoopBodyText = await page.evaluate(
() => document.body.textContent
);
await page.waitForTimeout(1600);
await emitCompaction(page, {
status: "started",
source: "automatic",
phase: "preflight",
});
await page.waitForTimeout(800);
await emitCompaction(page, {
status: "skipped",
source: "automatic",
phase: "preflight",
reason: "empty_summary",
user_visible: true,
});
await page.waitForTimeout(800);
const automaticNonBenignSkipStatus = await lastSeparatorText(page);
await page.waitForTimeout(1600);
await emitCompaction(page, {
status: "started",
source: "automatic",
phase: "preflight",
});
await page.waitForTimeout(800);
await emitCompaction(page, {
status: "emergency_ephemeral",
source: "automatic",
phase: "preflight",
reason: "empty_summary",
tokens_before: 5000,
tokens_after: 2400,
});
await page.waitForTimeout(800);
const emergencyStatusVisible = await lastSeparatorText(page);
await page.waitForTimeout(1600);
await emitCompaction(page, { status: "started", source: "manual" });
await page.waitForTimeout(800);
await page.fill("#chat-textarea", "queued after blocking compact failure");
await page.click("#chat-btn-send");
await page.waitForTimeout(150);
await emitCompaction(page, {
status: "failed",
source: "manual",
reason: "compaction_insufficient",
message: "still over budget",
refused: true,
});
await page.waitForTimeout(800);
const failedStatusVisible = await lastSeparatorText(page);
const bodyText = await page.locator("body").innerText();
const toastMessages = await page.evaluate(
() => window.__compactUx.toastCalls.map(t => t.message)
);
// Diagnostic: log actual rail text for debugging
console.error("DIAG_EMERGENCY:", JSON.stringify(emergencyStatusVisible));
console.error("DIAG_FAILED:", JSON.stringify(failedStatusVisible));
const result = {
hasStartedToast: toastMessages.includes("Compacting context..."),
hasSkippedToast: toastMessages.includes(
"Already within context budget; no compact was applied."
),
hasCompletedToast: toastMessages.some(m => m.includes("Context compacted")),
hasEmergencyToast: toastMessages.includes(
"Continuing with temporary context compaction for this turn"
),
hasFailedToast: toastMessages.some(
m => m.includes("Compact failed: still over budget")
),
hasReplayedFailureToast: toastMessages.some(m => m.includes("old replay")),
startedStatusVisible: startedStatusVisible.includes("context compacting"),
manualRailStarted:
manualSeparatorStarted.includes("context compacting"),
manualRailBottomAnchored:
manualSeparatorStartedPlacement &&
manualSeparatorStartedPlacement.bottomGap <= 60 &&
manualSeparatorStartedPlacement.className
.includes("chat-context-separator--session"),
skippedStatusVisible: skippedStatusVisible.includes(
"no compaction needed"
),
failedStatusVisible:
failedStatusVisible.includes("compaction failed"),
queuedBeforeSkipped,
skippedDrainedQueuedSend: await page.evaluate(
() => window.__compactUx.chatCalls
.some(c => c.params.message === "queued during compact")
),
automaticStartedStatusVisible: automaticStartedStatusVisible.includes(
"context compacting"
),
automaticRailStarted:
automaticSeparatorStarted.includes("context compacting"),
automaticRailBottomAnchored:
automaticSeparatorStartedPlacement &&
automaticSeparatorStartedPlacement.bottomGap <= 60 &&
automaticSeparatorStartedPlacement.className
.includes("chat-context-separator--session"),
automaticObservedStatusVisible: automaticObservedStatusVisible.includes(
"context compacting"
),
automaticRailObserved:
automaticSeparatorObserved.includes("context compacting"),
automaticRailCompleted:
automaticSeparatorCompleted.includes("context compacted"),
automaticQueuedBeforeCompleted:
automaticQueuedBeforeCompleted === callsBeforeAutomaticCompact,
automaticDidNotDrainBeforeDone,
automaticDrainedAfterDone: await page.evaluate(
() => window.__compactUx.chatCalls
.some(c => c.params.message === "queued during automatic compact")
),
emergencyStatusVisible:
emergencyStatusVisible.includes("temporary compaction") &&
!emergencyStatusVisible.includes("empty summary"),
automaticNoopRailHidden: automaticNoopSeparatorHidden === 0,
automaticNoopSkippedHidden:
!automaticNoopBodyText.includes("Context compaction skipped") &&
!automaticNoopBodyText.includes("structured content noop"),
automaticNonBenignSkipVisible:
automaticNonBenignSkipStatus.includes("compaction skipped") &&
!automaticNonBenignSkipStatus.includes("empty summary"),
blockingFailureKeptPending:
(await page.locator("#chat-pending").innerText())
.includes("queued after blocking compact"),
blockingFailureDidNotDrain: await page.evaluate(
() => !window.__compactUx.chatCalls
.some(c => c.params.message === "queued after blocking compact failure")
),
blockingFailureDidNotRecoverComposer:
(await page.locator("#chat-textarea").inputValue()) === "",
pageErrors: errors,
};
await browser.close();
console.log(JSON.stringify(result));
})().catch(err => {
console.error(err && err.stack ? err.stack : String(err));
process.exit(1);
});
"""
),
encoding="utf-8",
)
env = os.environ.copy()
env["OPENSQUILLA_STATE_DIR"] = str(tmp_path / "state")
env["OPENSQUILLA_LOG_DIR"] = str(tmp_path / "logs")
server = subprocess.Popen(
[sys.executable, str(server_script)],
cwd=Path.cwd(),
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
)
try:
_wait_for_health(port, server)
_install_playwright(tmp_path)
result = subprocess.run(
[_node(), str(browser_script)],
cwd=tmp_path,
env=dict(env, TARGET_URL=f"http://127.0.0.1:{port}/control/chat"),
check=False,
capture_output=True,
text=True,
timeout=90,
)
assert result.returncode == 0, result.stderr or result.stdout
payload = json.loads(result.stdout.strip().splitlines()[-1])
finally:
_stop_process(server)
assert payload == {
"hasStartedToast": False,
"hasSkippedToast": False,
"hasCompletedToast": False,
"hasEmergencyToast": True,
"hasFailedToast": True,
"hasReplayedFailureToast": False,
"startedStatusVisible": True,
"manualRailStarted": True,
"manualRailBottomAnchored": True,
"skippedStatusVisible": True,
"failedStatusVisible": True,
"queuedBeforeSkipped": 0,
"skippedDrainedQueuedSend": True,
"automaticStartedStatusVisible": True,
"automaticRailStarted": True,
"automaticRailBottomAnchored": True,
"automaticObservedStatusVisible": True,
"automaticRailObserved": True,
"automaticRailCompleted": True,
"automaticQueuedBeforeCompleted": True,
"automaticDidNotDrainBeforeDone": True,
"automaticDrainedAfterDone": True,
"emergencyStatusVisible": True,
"automaticNoopRailHidden": True,
"automaticNoopSkippedHidden": True,
"automaticNonBenignSkipVisible": True,
"blockingFailureKeptPending": True,
"blockingFailureDidNotDrain": True,
"blockingFailureDidNotRecoverComposer": True,
"pageErrors": [],
}
def test_router_fx_live_then_reopen_stays_settled_in_real_browser(tmp_path: Path) -> None:
if os.environ.get("OPENSQUILLA_WEBUI_BROWSER_CHAT_E2E") != "1":
pytest.skip("set OPENSQUILLA_WEBUI_BROWSER_CHAT_E2E=1 to run chat browser e2e")
port = _free_port()
server_script = tmp_path / "webui_router_fx_server.py"
browser_script = tmp_path / "webui_router_fx_browser.js"
server_script.write_text(
textwrap.dedent(
f"""
import uvicorn
from opensquilla.gateway.app import create_gateway_app
from opensquilla.gateway.config import AuthConfig, GatewayConfig
config = GatewayConfig(
host="127.0.0.1",
port={port},
auth=AuthConfig(mode="none"),
)
app = create_gateway_app(config)
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port={port}, log_level="warning")
"""
),
encoding="utf-8",
)
browser_script.write_text(
textwrap.dedent(
r"""
const { chromium } = require("playwright");
async function waitRpc(page) {
await page.waitForFunction(
() =>
typeof App !== "undefined" &&
App.getRpc &&
App.getRpc()?.state === "connected",
{ timeout: 15000 }
);
}
async function emit(page, event, payload, meta = {}) {
await page.evaluate(
({ event, payload, meta }) => {
const rpc = App.getRpc();
const named = rpc._listeners.get(event);
if (named) named.forEach(h => h(payload, meta));
const wild = rpc._listeners.get("*");
if (wild) wild.forEach(h => h(event, payload, meta));
},
{ event, payload, meta }
);
}
async function snapshot(page) {
return await page.evaluate(() => {
const strips = Array.from(document.querySelectorAll(".router-fx"));
const strip = strips[0] || null;
const cells = Array.from(document.querySelectorAll(".router-fx-cell .nm"));
const animations = strips.flatMap(el =>
el.getAnimations({ subtree: true })
.filter(anim => anim.playState !== "finished" && anim.playState !== "idle")
.map(anim => anim.animationName || "")
);
const overflows = cells.filter(el => el.scrollWidth > el.clientWidth + 1)
.map(el => el.textContent);
const grid = document.querySelector(".router-fx-grid");
const gridRect = grid ? grid.getBoundingClientRect() : null;
return {
count: strips.length,
state: strip?.dataset.state || "",
renderMode: strip?.dataset.renderMode || "",
liveCount: document.querySelectorAll(".router-fx[data-live='true']").length,
scanningCount: document.querySelectorAll(
".router-fx[data-scanning='true']"
).length,
selectorVisible: document.querySelectorAll(".router-fx-selector.visible").length,
bursts: document.querySelectorAll(".router-fx-burst").length,
pinging: document.querySelectorAll(".router-fx-cell.pinging").length,
animations,
aria: strip?.getAttribute("aria-label") || "",
winner: document.querySelector(".router-fx-cell.win .nm")?.textContent || "",
winners: strips.map(
el => el.querySelector(".router-fx-cell.win .nm")?.textContent || ""
),
cellCount: cells.length,
cellLabels: cells.map(el => el.textContent || ""),
hasLongLabel: cells.some(el => el.textContent === "gemini-3.1-flash-lite"),
overflows,
gridWithinViewport: gridRect
? gridRect.left >= -1 && gridRect.right <= window.innerWidth + 1
: false,
};
});
}
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
const errors = [];
page.on("pageerror", err => errors.push(String(err)));
await page.goto(process.env.TARGET_URL, {
waitUntil: "domcontentloaded",
timeout: 30000,
});
await waitRpc(page);
await page.evaluate(() => {
localStorage.setItem(
"opensquilla-router-fx",
JSON.stringify({ enabled: true, variant: "default" })
);
window.__routerFxTest = {
sessionKey: "agent:main:webchat:router-fx-panel",
historyMessages: [],
chatCalls: [],
routerConfig: {
enabled: true,
rollout_phase: "full",
tiers: {
c1: { model: "openrouter/deepseek-v4-flash" },
c2: { model: "openrouter/gemini-3.1-flash-lite" },
c3: { model: "openrouter/qwen3.6-max" },
image_model: {
model: "openrouter/kimi-k2.6",
supports_image: true,
image_only: true,
},
},
},
};
const rpc = App.getRpc();
const originalCall = rpc.call.bind(rpc);
rpc.call = (method, params = {}) => {
if (method === "tools.search_provider") {
return Promise.resolve({ provider: "none" });
}
if (method === "config.get") {
return Promise.resolve({
permissions: { default_mode: "ask" },
squilla_router: window.__routerFxTest.routerConfig,
});
}
if (method === "chat.history") {
if (window.__routerFxTest.deferHistory) {
return new Promise(resolve => {
window.__routerFxTest.resolveHistory = () => {
window.__routerFxTest.deferHistory = false;
resolve({
messages: window.__routerFxTest.historyMessages,
history_scope: "complete",
has_more: false,
});
};
});
}
return Promise.resolve({
messages: window.__routerFxTest.historyMessages,
history_scope: "complete",
has_more: false,
});
}
if (method === "sessions.messages.subscribe") {
return Promise.resolve({
subscribed: true,
key: params.key,
current_stream_seq: 100,
replay_complete: true,
replayed_count: 0,
run_status: "idle",
});
}
if (method === "chat.send") {
window.__routerFxTest.chatCalls.push({ method, params });
return Promise.resolve({ task_id: "router-fx-task" });
}
return originalCall(method, params);
};
});
await page.evaluate(() =>
Router.navigate("/chat?session=agent:main:webchat:router-fx-panel")
);
await page.waitForSelector("#chat-textarea", { timeout: 15000 });
await page.waitForFunction(
() => document.querySelector("#toggle-router")?.checked === true,
{ timeout: 15000 }
);
await page.fill("#chat-textarea", "route this with a long label");
await page.click("#chat-btn-send");
await page.waitForSelector(".router-fx[data-live='true'][data-scanning='true']", {
timeout: 5000,
});
const sessionKey = await page.locator("#chat-session-chip-key").innerText();
await emit(page, "session.event.router_decision", {
session_key: sessionKey,
stream_seq: 101,
tier: "c1",
model: "openrouter/deepseek-v4-flash",
routing_source: "squilla_router",
routing_applied: true,
});
await page.waitForSelector(".router-fx[data-state='settled'] .router-fx-cell.win", {
timeout: 5000,
});
const liveSettled = await snapshot(page);
await page.evaluate(() => {
window.__routerFxTest.historyMessages = [
{
role: "user",
text: "route this with a long label",
message_id: "router-fx-u1",
timestamp: "2026-05-30T00:00:00Z",
},
{
role: "assistant",
text: "done",
message_id: "router-fx-a1",
timestamp: "2026-05-30T00:00:01Z",
model: "openrouter/deepseek-v4-flash",
usage: {
model: "openrouter/deepseek-v4-flash",
routed_model: "openrouter/deepseek-v4-flash",
routed_tier: "c1",
routing_source: "squilla_router",
routing_applied: true,
input_tokens: 11,
output_tokens: 2,
},
},
];
Router.navigate("/overview");
Router.navigate("/chat?session=agent:main:webchat:router-fx-panel");
});
await page.waitForSelector(
".router-fx[data-render-mode='history'][data-state='settled']",
{ timeout: 15000 }
);
const reopened = await snapshot(page);
await emit(page, "session.event.router_decision", {
session_key: sessionKey,
stream_seq: 102,
tier: "c1",
model: "openrouter/deepseek-v4-flash",
routing_source: "squilla_router",
routing_applied: true,
}, { replayed: true });
await page.waitForTimeout(700);
const afterReplay = await snapshot(page);
await page.setViewportSize({ width: 520, height: 720 });
await page.waitForTimeout(300);
const narrow = await snapshot(page);
await page.setViewportSize({ width: 1280, height: 720 });
await page.evaluate(() => {
window.__routerFxTest.historyMessages = [
{
role: "user",
text: "first routed turn",
message_id: "router-fx-u-race-1",
timestamp: "2026-05-30T00:00:00Z",
},
{
role: "assistant",
text: "first done",
message_id: "router-fx-a-race-1",
timestamp: "2026-05-30T00:00:01Z",
model: "openrouter/deepseek-v4-flash",
usage: {
model: "openrouter/deepseek-v4-flash",
routed_model: "openrouter/deepseek-v4-flash",
routed_tier: "c1",
routing_source: "squilla_router",
routing_applied: true,
input_tokens: 11,
output_tokens: 2,
},
},
];
Router.navigate("/overview");
Router.navigate("/chat?session=agent:main:webchat:router-fx-panel");
});
await page.waitForFunction(
() =>
document.querySelectorAll(".router-fx").length === 1 &&
document.querySelector(".router-fx-cell.win .nm")?.textContent ===
"deepseek-v4-flash",
{ timeout: 15000 }
);
await page.evaluate(() => {
window.__routerFxTest.historyMessages = [
{
role: "user",
text: "first routed turn",
message_id: "router-fx-u-race-1",
timestamp: "2026-05-30T00:00:00Z",
},
{
role: "assistant",
text: "first done",
message_id: "router-fx-a-race-1",
timestamp: "2026-05-30T00:00:01Z",
model: "openrouter/deepseek-v4-flash",
usage: {
model: "openrouter/deepseek-v4-flash",
routed_model: "openrouter/deepseek-v4-flash",
routed_tier: "c1",
routing_source: "squilla_router",
routing_applied: true,
input_tokens: 11,
output_tokens: 2,
},
},
{
role: "user",
text: "second routed turn",
message_id: "router-fx-u-race-2",
timestamp: "2026-05-30T00:00:02Z",
},
{
role: "assistant",
text: "second done",
message_id: "router-fx-a-race-2",
timestamp: "2026-05-30T00:00:03Z",
model: "openrouter/gemini-3.1-flash-lite",
usage: {
model: "openrouter/gemini-3.1-flash-lite",
routed_model: "openrouter/gemini-3.1-flash-lite",
routed_tier: "c2",
routing_source: "squilla_router",
routing_applied: true,
input_tokens: 13,
output_tokens: 3,
},
},
];
window.__routerFxTest.deferHistory = true;
Router.navigate("/overview");
Router.navigate("/chat?session=agent:main:webchat:router-fx-panel");
});
await emit(page, "session.event.router_decision", {
session_key: sessionKey,
stream_seq: 103,
tier: "c2",
model: "openrouter/gemini-3.1-flash-lite",
routing_source: "squilla_router",
routing_applied: true,
}, { replayed: true });
await page.waitForTimeout(250);
const duringHistoryHydrationReplay = await snapshot(page);
await page.evaluate(() => window.__routerFxTest.resolveHistory());
await page.waitForFunction(
() => {
const winners = Array.from(document.querySelectorAll(".router-fx"))
.map(el => el.querySelector(".router-fx-cell.win .nm")?.textContent || "");
return winners.length === 2 &&
winners.includes("deepseek-v4-flash") &&
winners.includes("gemini-3.1-flash-lite");
},
{ timeout: 15000 }
);
await page.waitForTimeout(300);
const afterHistoryHydrationReplay = await snapshot(page);
await page.evaluate(() => {
window.__routerFxTest.historyMessages = [
{
role: "user",
text: "describe image",
message_id: "router-fx-u-image",
timestamp: "2026-05-30T00:00:04Z",
attachments: [{
mime: "image/png",
type: "image/png",
name: "preview.png",
data: "data:image/png;base64,iVBORw0KGgo=",
}],
},
{
role: "assistant",
text: "image done",
message_id: "router-fx-a-image",
timestamp: "2026-05-30T00:00:05Z",
model: "openrouter/kimi-k2.6",
usage: {
model: "openrouter/kimi-k2.6",
routed_model: "openrouter/kimi-k2.6",
routed_tier: "image_model",
routing_source: "image_route",
routing_applied: true,
input_tokens: 21,
output_tokens: 5,
},
},
];
Router.navigate("/overview");
Router.navigate("/chat?session=agent:main:webchat:router-fx-panel");
});
await page.waitForFunction(
() =>
document.querySelector(".msg.user .msg-attachments") &&
document.querySelectorAll(".router-fx").length === 0,
{ timeout: 15000 }
);
const singleImageCandidate = await snapshot(page);
await page.evaluate(() => {
window.__routerFxTest.routerConfig = {
enabled: true,
rollout_phase: "full",
tiers: {
t1: { model: "openrouter/shared-model" },
t2: { model: "other-provider/shared-model" },
},
};
window.__routerFxTest.historyMessages = [
{
role: "user",
text: "same model different provider",
message_id: "router-fx-u-same-display",
timestamp: "2026-05-30T00:00:06Z",
},
{
role: "assistant",
text: "same done",
message_id: "router-fx-a-same-display",
timestamp: "2026-05-30T00:00:07Z",
model: "openrouter/shared-model",
usage: {
model: "openrouter/shared-model",
routed_model: "openrouter/shared-model",
routed_tier: "t1",
routing_source: "squilla_router",
routing_applied: true,
input_tokens: 8,
output_tokens: 2,
},
},
];
Router.navigate("/overview");
Router.navigate("/chat?session=agent:main:webchat:router-fx-panel");
});
await page.waitForFunction(
() =>
document.querySelector(".msg.user")?.textContent.includes(
"same model different provider"
) &&
document.querySelectorAll(".router-fx").length === 0,
{ timeout: 15000 }
);
const sameDisplaySingleCandidate = await snapshot(page);
const result = {
liveSettled,
reopened,
afterReplay,
narrow,
duringHistoryHydrationReplay,
afterHistoryHydrationReplay,
singleImageCandidate,
sameDisplaySingleCandidate,
pageErrors: errors,
};
await browser.close();
console.log(JSON.stringify(result));
})().catch(err => {
console.error(err && err.stack ? err.stack : String(err));
process.exit(1);
});
"""
),
encoding="utf-8",
)
env = os.environ.copy()
env["OPENSQUILLA_STATE_DIR"] = str(tmp_path / "state")
env["OPENSQUILLA_LOG_DIR"] = str(tmp_path / "logs")
server = subprocess.Popen(
[sys.executable, str(server_script)],
cwd=Path.cwd(),
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
)
try:
_wait_for_health(port, server)
_install_playwright(tmp_path)
result = subprocess.run(
[_node(), str(browser_script)],
cwd=tmp_path,
env=dict(env, TARGET_URL=f"http://127.0.0.1:{port}/control/"),
check=False,
capture_output=True,
text=True,
timeout=90,
)
assert result.returncode == 0, result.stderr or result.stdout
payload = json.loads(result.stdout.strip().splitlines()[-1])
finally:
_stop_process(server)
assert payload["pageErrors"] == [], payload["pageErrors"]
assert payload["liveSettled"]["state"] == "settled"
assert payload["liveSettled"]["renderMode"] == "live"
assert payload["liveSettled"]["liveCount"] == 0
assert payload["liveSettled"]["scanningCount"] == 0
assert payload["liveSettled"]["selectorVisible"] == 0
assert payload["liveSettled"]["winner"] == "deepseek-v4-flash"
assert "Router selected deepseek-v4-flash" in payload["liveSettled"]["aria"]
assert payload["liveSettled"]["cellCount"] == 3
assert set(payload["liveSettled"]["cellLabels"]) == {
"deepseek-v4-flash",
"gemini-3.1-flash-lite",
"qwen3.6-max",
}
assert "kimi-k2.6" not in payload["liveSettled"]["cellLabels"]
for key in ("reopened", "afterReplay", "narrow"):
snap = payload[key]
assert snap["count"] == 1, snap
assert snap["state"] == "settled", snap
assert snap["renderMode"] == "history", snap
assert snap["liveCount"] == 0, snap
assert snap["scanningCount"] == 0, snap
assert snap["selectorVisible"] == 0, snap
assert snap["bursts"] == 0, snap
assert snap["pinging"] == 0, snap
assert snap["animations"] == [], snap
assert snap["winner"] == "deepseek-v4-flash", snap
assert snap["hasLongLabel"] is True, snap
assert snap["cellCount"] == 3, snap
assert "kimi-k2.6" not in snap["cellLabels"], snap
assert snap["overflows"] == [], snap
assert snap["gridWithinViewport"] is True, snap
during = payload["duringHistoryHydrationReplay"]
assert during["count"] in (0, 1), during
if during["count"] == 1:
assert during["winners"] == ["deepseek-v4-flash"], during
assert during["liveCount"] == 0, during
assert during["scanningCount"] == 0, during
assert during["selectorVisible"] == 0, during
assert during["animations"] == [], during
after_hydration = payload["afterHistoryHydrationReplay"]
assert after_hydration["count"] == 2, after_hydration
assert after_hydration["renderMode"] == "history", after_hydration
assert after_hydration["liveCount"] == 0, after_hydration
assert after_hydration["scanningCount"] == 0, after_hydration
assert after_hydration["selectorVisible"] == 0, after_hydration
assert after_hydration["bursts"] == 0, after_hydration
assert after_hydration["pinging"] == 0, after_hydration
assert after_hydration["animations"] == [], after_hydration
assert after_hydration["overflows"] == [], after_hydration
assert set(after_hydration["winners"]) == {
"deepseek-v4-flash",
"gemini-3.1-flash-lite",
}, after_hydration
image_single = payload["singleImageCandidate"]
assert image_single["count"] == 0, image_single
assert image_single["cellCount"] == 0, image_single
assert image_single["liveCount"] == 0, image_single
assert image_single["scanningCount"] == 0, image_single
same_display_single = payload["sameDisplaySingleCandidate"]
assert same_display_single["count"] == 0, same_display_single
assert same_display_single["cellCount"] == 0, same_display_single
assert same_display_single["liveCount"] == 0, same_display_single
assert same_display_single["scanningCount"] == 0, same_display_single
def test_webchat_large_paste_auto_attaches_text_in_real_browser(tmp_path: Path) -> None:
if os.environ.get("OPENSQUILLA_WEBUI_BROWSER_CHAT_E2E") != "1":
pytest.skip("set OPENSQUILLA_WEBUI_BROWSER_CHAT_E2E=1 to run chat browser e2e")
port = _free_port()
server_script = tmp_path / "webui_large_paste_server.py"
browser_script = tmp_path / "webui_large_paste_browser.js"
server_script.write_text(
textwrap.dedent(
f"""
import uvicorn
from opensquilla.gateway.app import create_gateway_app
from opensquilla.gateway.config import AuthConfig, GatewayConfig
config = GatewayConfig(
host="127.0.0.1",
port={port},
auth=AuthConfig(mode="none"),
)
app = create_gateway_app(config)
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port={port}, log_level="warning")
"""
),
encoding="utf-8",
)
browser_script.write_text(
textwrap.dedent(
r"""
const { chromium } = require("playwright");
async function waitRpc(page) {
await page.waitForFunction(
() =>
typeof App !== "undefined" &&
App.getRpc &&
App.getRpc()?.state === "connected",
{ timeout: 15000 }
);
}
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
const errors = [];
page.on("pageerror", err => errors.push(String(err)));
await page.goto(process.env.TARGET_URL, {
waitUntil: "domcontentloaded",
timeout: 30000,
});
await page.waitForSelector("#chat-textarea", { timeout: 15000 });
await waitRpc(page);
await page.evaluate(() => {
window.__largePaste = { calls: [] };
const rpc = App.getRpc();
const originalCall = rpc.call.bind(rpc);
rpc.call = (method, params = {}) => {
if (method === "tools.search_provider") {
return Promise.resolve({ provider: "none" });
}
if (method === "config.get") {
return Promise.resolve({
permissions: { default_mode: "ask" },
squilla_router: { enabled: false, rollout_phase: "off", tiers: {} },
});
}
if (method === "chat.history") {
return Promise.resolve({
messages: [],
history_scope: "complete",
has_more: false,
});
}
if (method === "sessions.messages.subscribe") {
return Promise.resolve({
subscribed: true,
key: params.key,
current_stream_seq: 0,
replay_complete: true,
replayed_count: 0,
run_status: "idle",
});
}
if (method === "chat.send") {
window.__largePaste.calls.push({ method, params });
return Promise.resolve({ task_id: "large-paste-task" });
}
return originalCall(method, params);
};
});
await page.fill("#chat-textarea", "x".repeat(20000));
await page.click("#chat-btn-send");
await page.waitForFunction(
() => window.__largePaste?.calls?.length === 1,
null, { timeout: 5000 }
);
const payload = await page.evaluate(() => {
const call = window.__largePaste.calls[0];
const params = call.params;
const attachment = params.attachments?.[0] || {};
const attachmentData = String(attachment.data || "");
const inputProvenance = params.inputProvenance || {};
const inputNormalization = inputProvenance.input_normalization || {};
let decodedAttachmentText = "";
try {
const bytes = Uint8Array.from(atob(attachmentData), char =>
char.charCodeAt(0)
);
decodedAttachmentText = new TextDecoder().decode(bytes);
} catch (_err) {
decodedAttachmentText = "";
}
const userBubbleText = document.querySelector(".msg.user")?.textContent || "";
const attachmentText =
document.querySelector(".msg.user .msg-attachments")?.textContent || "";
const attachmentChip = document.querySelector(".msg.user .msg-file-chip");
return {
callMethod: call.method,
message: params.message,
messageLength: String(params.message || "").length,
attachmentCount: Array.isArray(params.attachments)
? params.attachments.length
: 0,
attachmentName: attachment.name || "",
attachmentMime: attachment.mime || attachment.type || "",
attachmentDataLength: attachmentData.length,
attachmentDataLooksBase64: /^[A-Za-z0-9+/]+={0,2}$/.test(attachmentData),
attachmentDecodedLength: decodedAttachmentText.length,
attachmentDecodedMatchesPaste: decodedAttachmentText === "x".repeat(20000),
inputProvenanceKind: inputProvenance.kind || "",
inputProvenanceSource: inputProvenance.source || "",
inputNormalizationGuardAction: inputNormalization.guard_action || "",
inputNormalizationOriginalChars: inputNormalization.original_chars || 0,
inputNormalizationMaterialEstimatedTokens:
inputNormalization.material_estimated_tokens || 0,
inputNormalizationGeneratedAttachmentCount:
inputNormalization.generated_attachment_count || 0,
bubbleHasPlaceholder: userBubbleText.includes(
"Please process the attached pasted text."
),
bubbleHasLongRawPaste: userBubbleText.includes("x".repeat(500)),
attachmentChipHasGeneratedName: attachmentText.includes("webchat-paste-"),
attachmentChipTag: attachmentChip?.tagName || "",
attachmentChipDownload: attachmentChip?.getAttribute("download") || "",
attachmentChipHref: attachmentChip?.getAttribute("href") || "",
};
});
await page.evaluate((attachmentName) => {
const rpc = App.getRpc();
rpc.call = (method, params = {}) => {
if (method === "tools.search_provider") {
return Promise.resolve({ provider: "none" });
}
if (method === "config.get") {
return Promise.resolve({
permissions: { default_mode: "ask" },
squilla_router: { enabled: false, rollout_phase: "off", tiers: {} },
});
}
if (method === "sessions.messages.subscribe") {
return Promise.resolve({
subscribed: true,
key: params.key,
current_stream_seq: 0,
replay_complete: true,
replayed_count: 0,
run_status: "idle",
});
}
if (method === "chat.history") {
return Promise.resolve({
messages: [{
role: "user",
text: "Please process the attached pasted text.",
timestamp: new Date().toISOString(),
attachments: [{
sha256_ref: "d".repeat(64),
name: attachmentName,
mime: "text/plain",
size: 20000,
download_url:
`/api/v1/attachments/${"d".repeat(64)}`
+ "?sessionKey=agent%3Amain%3Awebchat%3Atest"
+ `&name=${encodeURIComponent(attachmentName)}`
+ "&mime=text%2Fplain",
}],
}],
history_scope: "complete",
has_more: false,
});
}
return Promise.resolve({});
};
ChatView.destroy();
ChatView.render(document.getElementById("content"));
}, payload.attachmentName);
await page.waitForSelector(".msg.user .msg-file-chip[href^='/api/v1/attachments/']", {
timeout: 5000,
});
const historyPayload = await page.evaluate(() => {
const chip = document.querySelector(".msg.user .msg-file-chip");
return {
tag: chip?.tagName || "",
download: chip?.getAttribute("download") || "",
href: chip?.getAttribute("href") || "",
};
});
Object.assign(payload, {
historyAttachmentChipTag: historyPayload.tag,
historyAttachmentChipDownload: historyPayload.download,
historyAttachmentChipHref: historyPayload.href,
});
payload.pageErrors = errors;
await browser.close();
console.log(JSON.stringify(payload));
})().catch(err => {
console.error(err && err.stack ? err.stack : String(err));
process.exit(1);
});
"""
),
encoding="utf-8",
)
env = os.environ.copy()
env["OPENSQUILLA_STATE_DIR"] = str(tmp_path / "state")
env["OPENSQUILLA_LOG_DIR"] = str(tmp_path / "logs")
server = subprocess.Popen(
[sys.executable, str(server_script)],
cwd=Path.cwd(),
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
)
try:
_wait_for_health(port, server)
_install_playwright(tmp_path)
result = subprocess.run(
[_node(), str(browser_script)],
cwd=tmp_path,
env=dict(env, TARGET_URL=f"http://127.0.0.1:{port}/control/chat"),
check=False,
capture_output=True,
text=True,
timeout=90,
)
assert result.returncode == 0, result.stderr or result.stdout
payload = json.loads(result.stdout.strip().splitlines()[-1])
finally:
_stop_process(server)
assert payload["callMethod"] == "chat.send", payload
assert payload["message"] == "Please process the attached pasted text.", payload
assert payload["messageLength"] < 20000, payload
assert payload["attachmentCount"] == 1, payload
assert payload["attachmentName"].startswith("webchat-paste-"), payload
assert payload["attachmentName"].endswith(".txt"), payload
assert payload["attachmentMime"] == "text/plain", payload
assert payload["attachmentDataLength"] > 1000, payload
assert payload["attachmentDataLooksBase64"] is True, payload
assert payload["attachmentDecodedLength"] == 20000, payload
assert payload["attachmentDecodedMatchesPaste"] is True, payload
assert payload["inputProvenanceKind"] == "web_message", payload
assert payload["inputProvenanceSource"] == "WebChat", payload
assert payload["inputNormalizationGuardAction"] == "generated_text_attachment", payload
assert payload["inputNormalizationOriginalChars"] == 20000, payload
assert payload["inputNormalizationMaterialEstimatedTokens"] == 5000, payload
assert payload["inputNormalizationGeneratedAttachmentCount"] == 1, payload
assert payload["bubbleHasPlaceholder"] is True, payload
assert payload["bubbleHasLongRawPaste"] is False, payload
assert payload["attachmentChipHasGeneratedName"] is True, payload
assert payload["attachmentChipTag"] == "A", payload
assert payload["attachmentChipDownload"].startswith("webchat-paste-"), payload
assert payload["attachmentChipHref"].startswith("data:text/plain;base64,"), payload
assert payload["historyAttachmentChipTag"] == "A", payload
assert payload["historyAttachmentChipDownload"].startswith("webchat-paste-"), payload
assert payload["historyAttachmentChipHref"].startswith("/api/v1/attachments/"), payload
assert payload["pageErrors"] == [], payload
def test_image_paste_consumes_attachment_without_inserting_wsl_path_text_in_real_browser(
tmp_path: Path,
) -> None:
if os.environ.get("OPENSQUILLA_WEBUI_BROWSER_CHAT_E2E") != "1":
pytest.skip("set OPENSQUILLA_WEBUI_BROWSER_CHAT_E2E=1 to run chat browser e2e")
port = _free_port()
server_script = tmp_path / "webui_image_paste_server.py"
browser_script = tmp_path / "webui_image_paste_browser.js"
server_script.write_text(
textwrap.dedent(
f"""
import uvicorn
from opensquilla.gateway.app import create_gateway_app
from opensquilla.gateway.config import AuthConfig, GatewayConfig
config = GatewayConfig(
host="127.0.0.1",
port={port},
auth=AuthConfig(mode="none"),
)
app = create_gateway_app(config)
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port={port}, log_level="warning")
"""
),
encoding="utf-8",
)
browser_script.write_text(
textwrap.dedent(
r"""
const { chromium } = require("playwright");
async function waitRpc(page) {
await page.waitForFunction(
() =>
typeof App !== "undefined" &&
App.getRpc &&
App.getRpc()?.state === "connected",
{ timeout: 15000 }
);
}
async function dispatchSyntheticPaste(page, { includeImage, text }) {
return await page.evaluate(async ({ includeImage, text }) => {
const textarea = document.querySelector("#chat-textarea");
const items = [];
if (includeImage) {
const b64 =
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAFgwJ/lHE7+QAAAABJRU5ErkJggg==";
const bytes = Uint8Array.from(atob(b64), ch => ch.charCodeAt(0));
const file = new File([bytes], "361d381f.png", { type: "image/png" });
items.push({
type: "image/png",
getAsFile: () => file,
});
}
items.push({
type: "text/plain",
getAsFile: () => null,
});
const event = new Event("paste", { bubbles: true, cancelable: true });
Object.defineProperty(event, "clipboardData", {
value: {
items,
getData: type => (type === "text/plain" ? text : ""),
},
});
document.dispatchEvent(event);
if (!event.defaultPrevented) {
textarea.value += text;
textarea.dispatchEvent(new Event("input", { bubbles: true }));
}
await new Promise(resolve => setTimeout(resolve, 250));
return {
defaultPrevented: event.defaultPrevented,
textareaValue: textarea.value,
previewHidden: document
.querySelector("#chat-attach-preview")
?.classList.contains("hidden"),
attachmentThumbs: document
.querySelectorAll("#chat-attach-preview .attachment-thumb")
.length,
attachmentChips: document
.querySelectorAll("#chat-attach-preview .attachment-chip")
.length,
};
}, { includeImage, text });
}
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
const errors = [];
page.on("pageerror", err => errors.push(String(err)));
await page.goto(process.env.TARGET_URL, {
waitUntil: "domcontentloaded",
timeout: 30000,
});
await page.waitForSelector("#chat-textarea", { timeout: 15000 });
await waitRpc(page);
const wslPath = "/tmp/.wsl-screenshot-cli/"
+ "361d381f306061eb91d29b48ca876da8ab0ee3374a57bcfaa9212c3b7105858.png";
const imagePaste = await dispatchSyntheticPaste(page, {
includeImage: true,
text: wslPath,
});
await page.locator(".attachment-remove").click();
await page.fill("#chat-textarea", "");
const textOnlyPaste = await dispatchSyntheticPaste(page, {
includeImage: false,
text: wslPath,
});
await browser.close();
console.log(JSON.stringify({
imagePaste,
textOnlyPaste,
pageErrors: errors,
}));
})().catch(err => {
console.error(err && err.stack ? err.stack : String(err));
process.exit(1);
});
"""
),
encoding="utf-8",
)
env = os.environ.copy()
env["OPENSQUILLA_STATE_DIR"] = str(tmp_path / "state")
env["OPENSQUILLA_LOG_DIR"] = str(tmp_path / "logs")
server = subprocess.Popen(
[sys.executable, str(server_script)],
cwd=Path.cwd(),
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
)
try:
_wait_for_health(port, server)
_install_playwright(tmp_path)
result = subprocess.run(
[_node(), str(browser_script)],
cwd=tmp_path,
env=dict(env, TARGET_URL=f"http://127.0.0.1:{port}/control/chat"),
check=False,
capture_output=True,
text=True,
timeout=90,
)
assert result.returncode == 0, result.stderr or result.stdout
payload = json.loads(result.stdout.strip().splitlines()[-1])
finally:
_stop_process(server)
assert payload["imagePaste"]["defaultPrevented"] is True, payload
assert payload["imagePaste"]["textareaValue"] == "", payload
assert payload["imagePaste"]["previewHidden"] is False, payload
assert payload["imagePaste"]["attachmentThumbs"] == 1, payload
assert payload["textOnlyPaste"]["defaultPrevented"] is False, payload
assert payload["textOnlyPaste"]["textareaValue"].startswith(
"/tmp/.wsl-screenshot-cli/"
), payload
assert payload["pageErrors"] == [], payload
def test_chat_image_attachment_layout_stays_aligned_in_real_browser(
tmp_path: Path,
) -> None:
if os.environ.get("OPENSQUILLA_WEBUI_BROWSER_CHAT_E2E") != "1":
pytest.skip("set OPENSQUILLA_WEBUI_BROWSER_CHAT_E2E=1 to run chat browser e2e")
port = _free_port()
server_script = tmp_path / "webui_image_attachment_layout_server.py"
browser_script = tmp_path / "webui_image_attachment_layout_browser.js"
server_script.write_text(
textwrap.dedent(
f"""
import uvicorn
from opensquilla.gateway.app import create_gateway_app
from opensquilla.gateway.config import AuthConfig, GatewayConfig
config = GatewayConfig(
host="127.0.0.1",
port={port},
auth=AuthConfig(mode="none"),
)
app = create_gateway_app(config)
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port={port}, log_level="warning")
"""
),
encoding="utf-8",
)
browser_script.write_text(
textwrap.dedent(
r"""
const { chromium } = require("playwright");
async function waitRpc(page) {
await page.waitForFunction(
() =>
typeof App !== "undefined" &&
App.getRpc &&
App.getRpc()?.state === "connected",
{ timeout: 15000 }
);
}
async function installMocks(page) {
await page.evaluate(() => {
window.__attachmentLayout = { sends: [] };
const rpc = App.getRpc();
const originalCall = rpc.call.bind(rpc);
rpc.call = (method, params = {}) => {
if (method === "tools.search_provider") {
return Promise.resolve({ provider: "none" });
}
if (method === "config.get") {
return Promise.resolve({
permissions: { default_mode: "ask" },
squilla_router: { enabled: false, rollout_phase: "off", tiers: {} },
});
}
if (method === "chat.history") {
return Promise.resolve({
messages: [],
history_scope: "complete",
has_more: false,
});
}
if (method === "sessions.messages.subscribe") {
return Promise.resolve({
subscribed: true,
key: params.key,
current_stream_seq: 0,
replay_complete: true,
replayed_count: 0,
run_status: "idle",
});
}
if (method === "chat.send") {
window.__attachmentLayout.sends.push(params);
return Promise.resolve({ task_id: "attachment-layout-task" });
}
return originalCall(method, params);
};
});
}
async function pasteWideImage(page) {
await page.evaluate(async () => {
const canvas = document.createElement("canvas");
canvas.width = 1600;
canvas.height = 120;
const ctx = canvas.getContext("2d");
ctx.fillStyle = "#101014";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#ff6a00";
ctx.fillRect(0, 54, canvas.width, 12);
ctx.fillStyle = "#ffffff";
ctx.font = "28px sans-serif";
ctx.fillText("ultra wide screenshot", 40, 48);
const blob = await new Promise(resolve => canvas.toBlob(resolve, "image/png"));
const file = new File([blob], "wide-screenshot.png", { type: "image/png" });
const event = new Event("paste", { bubbles: true, cancelable: true });
Object.defineProperty(event, "clipboardData", {
value: {
items: [
{ type: "image/png", getAsFile: () => file },
],
getData: () => "",
},
});
document.dispatchEvent(event);
});
await page.waitForSelector("#chat-attach-preview:not(.hidden) .attachment-thumb", {
timeout: 5000,
});
}
async function measure(page, viewport) {
await page.setViewportSize({ width: viewport.width, height: viewport.height });
await page.goto(process.env.TARGET_URL, {
waitUntil: "domcontentloaded",
timeout: 30000,
});
await waitRpc(page);
await installMocks(page);
await page.evaluate(() =>
Router.navigate("/chat?session=agent:main:webchat:attachment-layout")
);
await page.waitForSelector("#chat-textarea", { timeout: 15000 });
await pasteWideImage(page);
const pending = await page.evaluate(() => {
const box = (selector) => {
const el = document.querySelector(selector);
const r = el.getBoundingClientRect();
return {
left: r.left,
right: r.right,
top: r.top,
bottom: r.bottom,
width: r.width,
height: r.height,
visible:
r.width > 0 &&
r.height > 0 &&
getComputedStyle(el).display !== "none" &&
getComputedStyle(el).visibility !== "hidden",
};
};
return {
width: window.innerWidth,
scrollWidth: document.documentElement.scrollWidth,
preview: box("#chat-attach-preview"),
input: box(".chat-input-bar"),
composer: box("#chat-composer"),
thumb: box("#chat-attach-preview .attachment-thumb"),
remove: box("#chat-attach-preview .attachment-remove"),
};
});
await page.locator("#chat-textarea").fill("这是什么");
await page.locator("#chat-btn-send").click();
await page.waitForFunction(
() => window.__attachmentLayout?.sends?.length === 1,
{ timeout: 5000 }
);
await page.waitForSelector(".msg.user .msg-thumb", { timeout: 5000 });
const sent = await page.evaluate(() => {
const box = (selector) => {
const el = document.querySelector(selector);
const r = el.getBoundingClientRect();
return {
left: r.left,
right: r.right,
top: r.top,
bottom: r.bottom,
width: r.width,
height: r.height,
visible:
r.width > 0 &&
r.height > 0 &&
getComputedStyle(el).display !== "none" &&
getComputedStyle(el).visibility !== "hidden",
};
};
return {
width: window.innerWidth,
scrollWidth: document.documentElement.scrollWidth,
body: box(".msg.user .msg-body--has-attachments"),
text: box(".msg.user .msg-attachment-text"),
attachments: box(".msg.user .msg-attachments"),
thumb: box(".msg.user .msg-thumb"),
sendAttachmentCount: window.__attachmentLayout.sends[0].attachments?.length || 0,
sendMessage: window.__attachmentLayout.sends[0].message || "",
};
});
return { name: viewport.name, pending, sent };
}
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
const errors = [];
page.on("pageerror", err => errors.push(String(err)));
const viewports = [
{ name: "desktop", width: 1365, height: 768 },
{ name: "wide", width: 2048, height: 900 },
{ name: "mobile", width: 390, height: 844 },
];
const layouts = [];
for (const viewport of viewports) layouts.push(await measure(page, viewport));
await browser.close();
console.log(JSON.stringify({ layouts, pageErrors: errors }));
})().catch(err => {
console.error(err && err.stack ? err.stack : String(err));
process.exit(1);
});
"""
),
encoding="utf-8",
)
env = os.environ.copy()
env["OPENSQUILLA_STATE_DIR"] = str(tmp_path / "state")
env["OPENSQUILLA_LOG_DIR"] = str(tmp_path / "logs")
server = subprocess.Popen(
[sys.executable, str(server_script)],
cwd=Path.cwd(),
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
)
try:
_wait_for_health(port, server)
_install_playwright(tmp_path)
result = subprocess.run(
[_node(), str(browser_script)],
cwd=tmp_path,
env=dict(env, TARGET_URL=f"http://127.0.0.1:{port}/control/chat"),
check=False,
capture_output=True,
text=True,
timeout=120,
)
assert result.returncode == 0, result.stderr or result.stdout
payload = json.loads(result.stdout.strip().splitlines()[-1])
finally:
_stop_process(server)
assert payload["pageErrors"] == []
layouts = {item["name"]: item for item in payload["layouts"]}
for name, item in layouts.items():
pending = item["pending"]
sent = item["sent"]
assert pending["scrollWidth"] <= pending["width"], (name, pending)
assert sent["scrollWidth"] <= sent["width"], (name, sent)
assert pending["preview"]["visible"], (name, pending)
assert pending["remove"]["visible"], (name, pending)
assert pending["preview"]["left"] >= 0, (name, pending)
assert pending["preview"]["right"] <= pending["width"], (name, pending)
assert pending["thumb"]["right"] <= pending["preview"]["right"], (name, pending)
assert sent["sendAttachmentCount"] == 1, (name, sent)
assert sent["sendMessage"] == "这是什么", (name, sent)
assert sent["text"]["right"] <= sent["body"]["right"] + 1, (name, sent)
assert sent["thumb"]["width"] <= 280, (name, sent)
assert sent["thumb"]["height"] <= 160, (name, sent)
assert sent["thumb"]["right"] <= sent["width"], (name, sent)
assert sent["attachments"]["right"] <= sent["body"]["right"] + 1, (name, sent)
if name == "mobile":
assert pending["preview"]["left"] >= pending["composer"]["left"], (name, pending)
assert pending["preview"]["right"] <= pending["composer"]["right"], (name, pending)
else:
assert abs(pending["preview"]["left"] - pending["input"]["left"]) <= 1, (
name,
pending,
)
assert abs(pending["preview"]["right"] - pending["input"]["right"]) <= 1, (
name,
pending,
)
def test_completed_reconnect_without_replay_refreshes_history_in_real_browser(
tmp_path: Path,
) -> None:
if os.environ.get("OPENSQUILLA_WEBUI_BROWSER_CHAT_E2E") != "1":
pytest.skip("set OPENSQUILLA_WEBUI_BROWSER_CHAT_E2E=1 to run chat browser e2e")
port = _free_port()
server_script = tmp_path / "webui_completed_reconnect_server.py"
browser_script = tmp_path / "webui_completed_reconnect_browser.js"
server_script.write_text(
textwrap.dedent(
f"""
import uvicorn
from opensquilla.gateway.app import create_gateway_app
from opensquilla.gateway.config import AuthConfig, GatewayConfig
config = GatewayConfig(
host="127.0.0.1",
port={port},
auth=AuthConfig(mode="none"),
)
app = create_gateway_app(config)
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port={port}, log_level="warning")
"""
),
encoding="utf-8",
)
browser_script.write_text(
textwrap.dedent(
r"""
const { chromium } = require("playwright");
async function waitRpc(page) {
await page.waitForFunction(
() =>
typeof App !== "undefined" &&
App.getRpc &&
App.getRpc()?.state === "connected",
{ timeout: 15000 }
);
}
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
const errors = [];
page.on("pageerror", err => errors.push(String(err)));
await page.goto(process.env.TARGET_URL, {
waitUntil: "domcontentloaded",
timeout: 30000,
});
await waitRpc(page);
await page.evaluate(() => {
window.__completedReconnectTest = {
sessionKey: "agent:main:webchat:completed-reconnect",
subscribeCalls: 0,
historyCalls: 0,
historyMessages: [
{
role: "user",
text: "late single chunk",
message_id: "completed-reconnect-u1",
timestamp: "2026-05-31T00:38:14Z",
},
],
};
const rpc = App.getRpc();
const originalCall = rpc.call.bind(rpc);
rpc.call = (method, params = {}) => {
if (method === "tools.search_provider") {
return Promise.resolve({ provider: "none" });
}
if (method === "config.get") {
return Promise.resolve({
permissions: { default_mode: "ask" },
squilla_router: {
enabled: true,
rollout_phase: "full",
tiers: {
c1: { model: "openrouter/deepseek-v4-flash" },
},
},
});
}
if (method === "chat.history") {
window.__completedReconnectTest.historyCalls += 1;
return Promise.resolve({
messages: window.__completedReconnectTest.historyMessages,
history_scope: "complete",
has_more: false,
});
}
if (method === "sessions.messages.subscribe") {
window.__completedReconnectTest.subscribeCalls += 1;
return new Promise(resolve => {
setTimeout(() => {
window.__completedReconnectTest.historyMessages = [
{
role: "user",
text: "late single chunk",
message_id: "completed-reconnect-u1",
timestamp: "2026-05-31T00:38:14Z",
},
{
role: "assistant",
text:
"未能解析回复:\n - field 'intent': " +
"'不要用这个skill实施' not in choices",
message_id: "completed-reconnect-a1",
timestamp: "2026-05-31T00:38:28Z",
model: "openrouter/deepseek-v4-flash",
usage: {
model: "openrouter/deepseek-v4-flash",
routed_model: "openrouter/deepseek-v4-flash",
routed_tier: "c1",
routing_source: "squilla_router",
routing_applied: true,
input_tokens: 0,
output_tokens: 0,
},
},
];
resolve({
subscribed: true,
key: params.key,
current_stream_seq: 0,
replay_complete: true,
replayed_count: 0,
run_status: "idle",
last_task: {
task_id: "completed-reconnect-task",
status: "succeeded",
terminal_reason: "completed",
},
});
}, 800);
});
}
return originalCall(method, params);
};
});
await page.evaluate(() =>
Router.navigate("/chat?session=agent:main:webchat:completed-reconnect")
);
await page.waitForSelector("#chat-textarea", { timeout: 15000 });
await page.waitForFunction(
() =>
document.querySelector(".msg.user")?.textContent.includes(
"late single chunk"
),
null, { timeout: 5000 }
);
const userOnly = await page.evaluate(() => ({
assistantHasReply: Array.from(document.querySelectorAll(".msg.assistant"))
.some(el => el.textContent.includes("未能解析回复")),
historyCalls: window.__completedReconnectTest.historyCalls,
subscribeCalls: window.__completedReconnectTest.subscribeCalls,
}));
await page.waitForFunction(
() => Array.from(document.querySelectorAll(".msg.assistant"))
.some(el => el.textContent.includes("未能解析回复")),
null, { timeout: 5000 }
);
const afterTerminalSubscribe = await page.evaluate(() => ({
assistantHasReply: Array.from(document.querySelectorAll(".msg.assistant"))
.some(el => el.textContent.includes("未能解析回复")),
thinkingCount: document.querySelectorAll(".msg.thinking").length,
streamingCount: document.querySelectorAll(".msg.streaming").length,
routerWinner: document.querySelector(".router-fx-cell.win .nm")?.textContent || "",
historyCalls: window.__completedReconnectTest.historyCalls,
subscribeCalls: window.__completedReconnectTest.subscribeCalls,
}));
await browser.close();
console.log(JSON.stringify({
userOnly,
afterTerminalSubscribe: {
...afterTerminalSubscribe,
pageErrors: errors,
},
}));
})().catch(err => {
console.error(err && err.stack ? err.stack : String(err));
process.exit(1);
});
"""
),
encoding="utf-8",
)
env = os.environ.copy()
env["OPENSQUILLA_STATE_DIR"] = str(tmp_path / "state")
env["OPENSQUILLA_LOG_DIR"] = str(tmp_path / "logs")
server = subprocess.Popen(
[sys.executable, str(server_script)],
cwd=Path.cwd(),
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
)
try:
_wait_for_health(port, server)
_install_playwright(tmp_path)
result = subprocess.run(
[_node(), str(browser_script)],
cwd=tmp_path,
env=dict(env, TARGET_URL=f"http://127.0.0.1:{port}/control/"),
check=False,
capture_output=True,
text=True,
timeout=90,
)
assert result.returncode == 0, result.stderr or result.stdout
payload = json.loads(result.stdout.strip().splitlines()[-1])
finally:
_stop_process(server)
assert payload["userOnly"]["assistantHasReply"] is False, payload
after = payload["afterTerminalSubscribe"]
assert after["assistantHasReply"] is True, after
assert after["thinkingCount"] == 0, after
assert after["streamingCount"] == 0, after
assert after["routerWinner"] == "deepseek-v4-flash", after
assert after["historyCalls"] >= 2, after
assert after["pageErrors"] == [], after
def test_replayed_savings_done_restores_reply_without_replaying_popup_in_real_browser(
tmp_path: Path,
) -> None:
if os.environ.get("OPENSQUILLA_WEBUI_BROWSER_CHAT_E2E") != "1":
pytest.skip("set OPENSQUILLA_WEBUI_BROWSER_CHAT_E2E=1 to run chat browser e2e")
port = _free_port()
server_script = tmp_path / "webui_savings_replay_server.py"
browser_script = tmp_path / "webui_savings_replay_browser.js"
server_script.write_text(
textwrap.dedent(
f"""
import uvicorn
from opensquilla.gateway.app import create_gateway_app
from opensquilla.gateway.config import AuthConfig, GatewayConfig
config = GatewayConfig(
host="127.0.0.1",
port={port},
auth=AuthConfig(mode="none"),
)
app = create_gateway_app(config)
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port={port}, log_level="warning")
"""
),
encoding="utf-8",
)
browser_script.write_text(
textwrap.dedent(
r"""
const { chromium } = require("playwright");
async function waitRpc(page) {
await page.waitForFunction(
() =>
typeof App !== "undefined" &&
App.getRpc &&
App.getRpc()?.state === "connected",
{ timeout: 15000 }
);
}
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
const errors = [];
page.on("pageerror", err => errors.push(String(err)));
await page.goto(process.env.TARGET_URL, {
waitUntil: "domcontentloaded",
timeout: 30000,
});
await waitRpc(page);
await page.evaluate(() => {
window.__savingsReplayTest = {
fireCalls: 0,
sessionKey: "agent:main:webchat:savings-replay",
};
const originalFire = window.SavingsFX.fire.bind(window.SavingsFX);
window.SavingsFX.fire = (...args) => {
window.__savingsReplayTest.fireCalls += 1;
return originalFire(...args);
};
const rpc = App.getRpc();
const originalCall = rpc.call.bind(rpc);
rpc.call = (method, params = {}) => {
if (method === "tools.search_provider") {
return Promise.resolve({ provider: "none" });
}
if (method === "config.get") {
return Promise.resolve({
permissions: { default_mode: "ask" },
squilla_router: {
enabled: true,
rollout_phase: "full",
tiers: {
c1: { model: "openrouter/deepseek-v4-flash" },
},
},
});
}
if (method === "chat.history") {
return Promise.resolve({
messages: [],
history_scope: "complete",
has_more: false,
});
}
if (method === "sessions.messages.subscribe") {
return Promise.resolve({
subscribed: true,
key: params.key,
current_stream_seq: 0,
replay_complete: true,
replayed_count: 0,
run_status: "idle",
});
}
return originalCall(method, params);
};
});
await page.evaluate(() =>
Router.navigate("/chat?session=agent:main:webchat:savings-replay")
);
await page.waitForSelector("#chat-textarea", { timeout: 15000 });
await page.evaluate(() => {
const rpc = App.getRpc();
const ls = rpc._listeners;
const sessionKey = window.__savingsReplayTest.sessionKey;
const deltaHandlers = ls.get("session.event.text_delta");
if (deltaHandlers) {
deltaHandlers.forEach(h => h({
session_key: sessionKey,
stream_seq: 41,
text: "replayed savings answer",
}));
}
const wildHandlers = ls.get("*");
if (wildHandlers) {
wildHandlers.forEach(h => h("session.event.done", {
session_key: sessionKey,
stream_seq: 42,
text: "replayed savings answer",
input_tokens: 17800,
output_tokens: 661,
model: "openrouter/deepseek-v4-flash",
routed_model: "openrouter/deepseek-v4-flash",
routed_tier: "c1",
routing_source: "squilla_router",
routing_applied: true,
total_savings_pct: 97,
total_savings_usd: 0.01,
}, { replayed: true }));
}
});
await page.waitForFunction(
() => Array.from(document.querySelectorAll(".msg.assistant"))
.some(el => el.textContent.includes("replayed savings answer")),
null, { timeout: 5000 }
);
await page.waitForTimeout(300);
const payload = await page.evaluate(() => ({
assistantHasReply: Array.from(document.querySelectorAll(".msg.assistant"))
.some(el => el.textContent.includes("replayed savings answer")),
savedChipVisible: Array.from(document.querySelectorAll(".msg-meta__saved"))
.some(el => el.textContent.includes("Saved")),
fireCalls: window.__savingsReplayTest.fireCalls,
floatingLabels: document.querySelectorAll(".savings-float").length,
}));
await browser.close();
console.log(JSON.stringify({ ...payload, pageErrors: errors }));
})().catch(err => {
console.error(err && err.stack ? err.stack : String(err));
process.exit(1);
});
"""
),
encoding="utf-8",
)
env = os.environ.copy()
env["OPENSQUILLA_STATE_DIR"] = str(tmp_path / "state")
env["OPENSQUILLA_LOG_DIR"] = str(tmp_path / "logs")
server = subprocess.Popen(
[sys.executable, str(server_script)],
cwd=Path.cwd(),
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
)
try:
_wait_for_health(port, server)
_install_playwright(tmp_path)
result = subprocess.run(
[_node(), str(browser_script)],
cwd=tmp_path,
env=dict(env, TARGET_URL=f"http://127.0.0.1:{port}/control/"),
check=False,
capture_output=True,
text=True,
timeout=90,
)
assert result.returncode == 0, result.stderr or result.stdout
payload = json.loads(result.stdout.strip().splitlines()[-1])
finally:
_stop_process(server)
assert payload["assistantHasReply"] is True, payload
assert payload["savedChipVisible"] is True, payload
assert payload["fireCalls"] == 0, payload
assert payload["floatingLabels"] == 0, payload
assert payload["pageErrors"] == [], payload
def test_webui_edit_middle_message_forks_without_history_leak(tmp_path: Path) -> None:
if os.environ.get("OPENSQUILLA_WEBUI_BROWSER_CHAT_E2E") != "1":
pytest.skip("set OPENSQUILLA_WEBUI_BROWSER_CHAT_E2E=1 to run chat browser e2e")
port = _free_port()
server_script = tmp_path / "webui_branch_edit_server.py"
browser_script = tmp_path / "webui_branch_edit_browser.js"
server_script.write_text(
textwrap.dedent(
f"""
import uvicorn
from opensquilla.gateway.app import create_gateway_app
from opensquilla.gateway.config import AuthConfig, GatewayConfig
config = GatewayConfig(
host="127.0.0.1",
port={port},
auth=AuthConfig(mode="none"),
control_ui={{"frontend": "legacy"}},
)
app = create_gateway_app(config)
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port={port}, log_level="warning")
"""
),
encoding="utf-8",
)
browser_script.write_text(
textwrap.dedent(
r"""
const { chromium } = require("playwright");
async function waitRpc(page) {
await page.waitForFunction(
() =>
typeof App !== "undefined" &&
App.getRpc &&
App.getRpc()?.state === "connected",
{ timeout: 15000 }
);
}
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
const errors = [];
page.on("pageerror", err => errors.push(String(err)));
await page.goto(process.env.TARGET_URL, {
waitUntil: "domcontentloaded",
timeout: 30000,
});
await waitRpc(page);
await page.evaluate(() => {
const parentKey = "agent:main:webchat:branch-edit-parent";
const childKey = "agent:main:webchat:branch-edit-child";
const parentMessages = [
{
role: "user",
text: "A marker",
timestamp: "2026-07-03T00:00:01.000Z",
message_id: "msg-A",
},
{
role: "assistant",
text: "ack A",
timestamp: "2026-07-03T00:00:02.000Z",
message_id: "msg-ack-A",
},
{
role: "user",
text: "B marker",
timestamp: "2026-07-03T00:00:03.000Z",
message_id: "msg-B",
},
{
role: "assistant",
text: "ack B",
timestamp: "2026-07-03T00:00:04.000Z",
message_id: "msg-ack-B",
},
{
role: "user",
text: "C marker must stay only on parent",
timestamp: "2026-07-03T00:00:05.000Z",
message_id: "msg-C",
},
];
const childMessages = [
{
role: "user",
text: "A marker",
timestamp: "2026-07-03T00:00:01.000Z",
message_id: "child-msg-A",
},
{
role: "assistant",
text: "ack A",
timestamp: "2026-07-03T00:00:02.000Z",
message_id: "child-msg-ack-A",
},
{
role: "user",
text: "B edited",
timestamp: "2026-07-03T00:00:06.000Z",
message_id: "child-msg-B-edited",
},
];
const rpc = App.getRpc();
const originalCall = rpc.call.bind(rpc);
window.__branchEdit = {
parentKey,
childKey,
parentMessages,
childMessages,
chatCalls: [],
historyCalls: [],
};
rpc.call = (method, params = {}) => {
if (method === "tools.search_provider") {
return Promise.resolve({ provider: "none" });
}
if (method === "config.get") {
return Promise.resolve({
permissions: { default_mode: "ask" },
squilla_router: { enabled: false, rollout_phase: "off", tiers: {} },
});
}
if (method === "sessions.messages.subscribe") {
return Promise.resolve({
subscribed: true,
key: params.key,
current_stream_seq: 0,
replay_complete: true,
replayed_count: 0,
run_status: "idle",
});
}
if (method === "chat.history") {
const key = params.sessionKey || parentKey;
window.__branchEdit.historyCalls.push(key);
return Promise.resolve({
messages: key === childKey ? childMessages : parentMessages,
history_scope: "complete",
has_more: false,
});
}
if (method === "chat.send") {
window.__branchEdit.chatCalls.push(params);
return Promise.resolve({
sessionKey: childKey,
key: childKey,
task_id: "branch-edit-task",
});
}
return originalCall(method, params);
};
});
await page.evaluate(() =>
Router.navigate(
"/chat?session=" +
encodeURIComponent(window.__branchEdit.parentKey)
)
);
await page.waitForSelector("#chat-textarea", { timeout: 15000 });
await page.waitForFunction(
() => document.querySelectorAll("#chat-thread .msg.user").length >= 3,
null, { timeout: 15000 }
);
await page.locator("#chat-thread .msg.user").nth(1).hover();
await page.locator("#chat-thread .msg.user")
.nth(1)
.locator('[data-action="edit"]')
.click();
await page.waitForFunction(
() => document.querySelector("#chat-textarea")?.value.includes("B marker"),
null, { timeout: 5000 }
);
await page.fill("#chat-textarea", "B edited");
await page.click("#chat-btn-send");
await page.waitForFunction(
() => window.__branchEdit.chatCalls.length === 1,
null, { timeout: 5000 }
);
await page.waitForFunction(
() =>
window.__branchEdit.chatCalls[0].forkBeforeMessageId === "msg-B",
null, { timeout: 5000 }
);
await page.evaluate(() => {
ChatView.destroy();
ChatView.render(document.getElementById("content"));
});
await page.waitForFunction(
() =>
window.__branchEdit.historyCalls
.some(key => key === window.__branchEdit.childKey),
null, { timeout: 15000 }
);
await page.waitForSelector("#chat-thread .msg.user", { timeout: 15000 });
const payload = await page.evaluate(() => {
const bodyText = document.querySelector("#chat-thread")?.innerText || "";
return {
chatSendPayload: window.__branchEdit.chatCalls[0],
historyCalls: window.__branchEdit.historyCalls,
parentHistoryHasC: window.__branchEdit.parentMessages
.some(msg => String(msg.text || "").includes("C marker")),
childHistoryHasC: window.__branchEdit.childMessages
.some(msg => String(msg.text || "").includes("C marker")),
childUiHasC: bodyText.includes("C marker"),
childUiHasEditedB: bodyText.includes("B edited"),
sessionChip: document.querySelector("#chat-session-chip-key")?.innerText || "",
};
});
payload.pageErrors = errors;
await browser.close();
console.log(JSON.stringify(payload));
})().catch(err => {
console.error(err && err.stack ? err.stack : String(err));
process.exit(1);
});
"""
),
encoding="utf-8",
)
env = os.environ.copy()
env["OPENSQUILLA_STATE_DIR"] = str(tmp_path / "state")
env["OPENSQUILLA_LOG_DIR"] = str(tmp_path / "logs")
server = subprocess.Popen(
[sys.executable, str(server_script)],
cwd=Path.cwd(),
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
)
try:
_wait_for_health(port, server)
_install_playwright(tmp_path)
result = subprocess.run(
[_node(), str(browser_script)],
cwd=tmp_path,
env=dict(env, TARGET_URL=f"http://127.0.0.1:{port}/control/"),
check=False,
capture_output=True,
text=True,
timeout=90,
)
assert result.returncode == 0, result.stderr or result.stdout
payload = json.loads(result.stdout.strip().splitlines()[-1])
finally:
_stop_process(server)
assert payload["pageErrors"] == [], payload["pageErrors"]
assert payload["chatSendPayload"]["message"] == "B edited"
assert payload["chatSendPayload"]["forkBeforeMessageId"] == "msg-B"
assert payload["parentHistoryHasC"] is True
assert payload["childHistoryHasC"] is False
assert payload["childUiHasC"] is False
assert payload["childUiHasEditedB"] is True
assert payload["sessionChip"] == "agent:main:webchat:branch-edit-child"
def test_webui_hotfix_flows_in_real_browser(tmp_path: Path) -> None:
if os.environ.get("OPENSQUILLA_WEBUI_BROWSER_CHAT_E2E") != "1":
pytest.skip("set OPENSQUILLA_WEBUI_BROWSER_CHAT_E2E=1 to run chat browser e2e")
port = _free_port()
server_script = tmp_path / "webui_hotfix_server.py"
browser_script = tmp_path / "webui_hotfix_browser.js"
server_script.write_text(
textwrap.dedent(
f"""
import uvicorn
from opensquilla.gateway.app import create_gateway_app
from opensquilla.gateway.config import AuthConfig, GatewayConfig
config = GatewayConfig(
host="127.0.0.1",
port={port},
auth=AuthConfig(mode="none"),
)
app = create_gateway_app(config)
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port={port}, log_level="warning")
"""
),
encoding="utf-8",
)
browser_script.write_text(
textwrap.dedent(
r"""
const { chromium } = require("playwright");
async function waitRpc(page) {
await page.waitForFunction(
() =>
typeof App !== "undefined" &&
App.getRpc &&
App.getRpc()?.state === "connected",
{ timeout: 15000 }
);
}
async function emit(page, event, payload) {
await page.evaluate(
({ event, payload }) => {
const rpc = App.getRpc();
const named = rpc._listeners.get(event);
if (named) named.forEach(h => h(payload));
const wild = rpc._listeners.get("*");
if (wild) wild.forEach(h => h(event, payload));
},
{ event, payload }
);
}
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
const errors = [];
page.on("pageerror", err => errors.push(String(err)));
await page.goto(process.env.TARGET_URL, {
waitUntil: "domcontentloaded",
timeout: 30000,
});
await page.waitForSelector("#chat-textarea", { timeout: 15000 });
await waitRpc(page);
await page.evaluate(() => {
const rpc = App.getRpc();
const originalCall = rpc.call.bind(rpc);
window.__hotfix = {
chatCalls: [],
channelStatusCalls: 0,
delayNextWait: false,
logInFlight: 0,
logMaxInFlight: 0,
logTailCalls: 0,
};
const originalWait = rpc.waitForConnection.bind(rpc);
rpc.waitForConnection = () => {
if (!window.__hotfix.delayNextWait) return originalWait();
window.__hotfix.delayNextWait = false;
return new Promise(resolve => setTimeout(resolve, 300));
};
rpc.call = (method, params = {}) => {
if (method === "chat.send") {
window.__hotfix.chatCalls.push({ method, params });
return Promise.resolve({ task_id: "task-" + window.__hotfix.chatCalls.length });
}
if (method === "sessions.list") {
return Promise.resolve({
sessions: [{
key: "fake-session",
title: "Fake session",
updated_at: new Date().toISOString(),
message_count: 1,
}],
});
}
if (method === "agents.list") {
return Promise.resolve({ agents: [] });
}
if (method === "sessions.delete") {
return Promise.resolve({ deleted: [], errors: ["denied"] });
}
if (method === "channels.status") {
window.__hotfix.channelStatusCalls += 1;
return Promise.resolve({ channels: [] });
}
if (method === "logs.status") {
return Promise.resolve({ enabled: true, path: "debug.log" });
}
if (method === "logs.tail") {
const callNo = ++window.__hotfix.logTailCalls;
window.__hotfix.logInFlight += 1;
window.__hotfix.logMaxInFlight = Math.max(
window.__hotfix.logMaxInFlight,
window.__hotfix.logInFlight
);
return new Promise((resolve, reject) => {
const delay = callNo === 1 ? 200 : 4500;
setTimeout(() => {
window.__hotfix.logInFlight -= 1;
if (callNo === 1) {
reject(new Error("tail down"));
return;
}
resolve({
cursor: callNo,
lines: [{ level: "INFO", message: "log line " + callNo }],
});
}, delay);
});
}
return originalCall(method, params);
};
});
const sessionKey = await page.locator("#chat-session-chip-key").innerText();
await emit(
page,
"task.queued",
{ task_id: "old-task", session_key: "other-session" }
);
const statusAfterOtherTask = await page.locator("#chat-run-status").innerText();
await emit(page, "task.queued", { task_id: "current-task", session_key: sessionKey });
const statusAfterCurrentTask = await page.locator("#chat-run-status").innerText();
await emit(page, "sessions.changed", {
key: sessionKey,
run_status: "idle",
last_task: { status: "succeeded" },
reason: "turn_complete",
});
await emit(page, "sessions.changed", {
key: sessionKey,
run_status: "running",
active_task: { task_id: "running-task", status: "running" },
reason: "task_running",
});
await page.waitForFunction(
() => document.querySelector("#chat-run-status")?.innerText === "Running",
null, { timeout: 5000 }
);
await emit(page, "task.queued", {
task_id: "queued-behind-running",
session_key: sessionKey,
queue_depth: 1,
queue_position: 2,
});
await page.waitForTimeout(150);
const statusAfterQueuedBehindRunning =
await page.locator("#chat-run-status").innerText();
await emit(page, "sessions.changed", {
key: sessionKey,
run_status: "idle",
last_task: { status: "succeeded" },
reason: "turn_complete",
});
await page.fill("#chat-textarea", "first prompt");
await page.click("#chat-btn-send");
await page.waitForFunction(
() => window.__hotfix.chatCalls.some(c => c.params.message === "first prompt"),
null, { timeout: 5000 }
);
await emit(page, "session.event.done", { text: "first answer" });
await page.waitForFunction(
() => document.querySelectorAll(".msg.assistant").length >= 1,
null, { timeout: 5000 }
);
await page.fill("#chat-textarea", "second prompt");
await page.click("#chat-btn-send");
await page.waitForFunction(
() => window.__hotfix.chatCalls.some(c => c.params.message === "second prompt"),
null, { timeout: 5000 }
);
await emit(page, "session.event.done", { text: "second answer" });
await page.waitForFunction(
() => document.querySelectorAll(".msg.assistant").length >= 2,
null, { timeout: 5000 }
);
await page.locator(".msg.assistant").first().hover();
await page.locator(".msg.assistant")
.first()
.locator('[data-action="regenerate"]')
.click();
await page.waitForFunction(
() =>
window.__hotfix.chatCalls
.filter(c => c.params.message === "first prompt")
.length >= 2,
null, { timeout: 5000 }
);
const regenerateMessages = await page.evaluate(
() => window.__hotfix.chatCalls.map(c => c.params.message)
);
const bubblesAfterRegenerate = await page.locator(".msg").count();
await page.fill("#chat-textarea", "queued while streaming");
await page.click("#chat-btn-send");
await page.fill("#chat-textarea", "draft typed during stream");
await emit(page, "session.event.done", { text: "regenerated answer" });
await page.waitForFunction(
() =>
window.__hotfix.chatCalls
.some(c => c.params.message === "queued while streaming"),
null, { timeout: 5000 }
);
const draftAfterQueueDrain = await page.locator("#chat-textarea").inputValue();
await page.fill("#chat-textarea", "queued from terminal session change");
await page.click("#chat-btn-send");
await emit(page, "sessions.changed", {
key: sessionKey,
run_status: "idle",
last_task: { status: "succeeded" },
reason: "turn_complete",
});
await page.waitForFunction(
() =>
window.__hotfix.chatCalls
.some(c => c.params.message === "queued from terminal session change"),
null, { timeout: 5000 }
);
await emit(page, "session.event.done", { text: "terminal queued answer" });
await page.waitForFunction(
() => document.querySelector("#chat-run-status")?.innerText === "Idle",
null, { timeout: 5000 }
);
await page.fill("#chat-textarea", "first before error");
await page.click("#chat-btn-send");
await page.waitForFunction(
() => window.__hotfix.chatCalls
.some(c => c.params.message === "first before error"),
null, { timeout: 5000 }
);
await page.fill("#chat-textarea", "queued before error");
await page.click("#chat-btn-send");
await page.waitForFunction(
() => document.querySelector("#chat-pending")
?.innerText.includes("queued before error"),
null, { timeout: 5000 }
);
const queuedBeforeErrorSentPrematurely = await page.evaluate(
() => window.__hotfix.chatCalls
.some(c => c.params.message === "queued before error")
);
await emit(page, "session.event.error", {
session_key: sessionKey,
message: "synthetic failure",
code: "failed",
});
await page.waitForFunction(
() => document.querySelector("#chat-textarea")
?.value.includes("queued before error"),
null, { timeout: 5000 }
);
const errorRecoveredComposer = await page.locator("#chat-textarea").inputValue();
const queuedBeforeErrorSentAfterFailure = await page.evaluate(
() => window.__hotfix.chatCalls
.some(c => c.params.message === "queued before error")
);
await page.fill("#chat-textarea", "");
await page.fill("#chat-textarea", "approval turn");
await page.click("#chat-btn-send");
await page.waitForFunction(
() => window.__hotfix.chatCalls.some(c => c.params.message === "approval turn"),
null, { timeout: 5000 }
);
await page.evaluate((sessionKey) => {
window.dispatchEvent(new CustomEvent("opensquilla:approvals-pending", {
detail: {
pending: [{ id: "approval-1", namespace: "exec", sessionKey }],
count: 1,
},
}));
}, sessionKey);
await page.waitForFunction(
() => document.querySelector("#chat-run-status")
?.innerText === "Waiting for approval",
null, { timeout: 5000 }
);
const approvalStatusDuring = await page.locator("#chat-run-status").innerText();
await page.evaluate(() => {
window.dispatchEvent(new CustomEvent("opensquilla:approvals-pending", {
detail: { pending: [], count: 0 },
}));
});
await page.waitForFunction(
() => document.querySelector("#chat-run-status")?.innerText === "Running",
null, { timeout: 5000 }
);
const approvalStatusAfterResolve = await page.locator("#chat-run-status").innerText();
await emit(page, "session.event.done", { text: "approval answer" });
await page.waitForFunction(
() => document.querySelector("#chat-run-status")?.innerText === "Idle",
null, { timeout: 5000 }
);
await page.evaluate(() => Router.navigate("/config"));
await page.waitForSelector("#cfg-yaml-area", { state: "attached", timeout: 15000 });
await page.click('[data-cfg-mode="yaml"]');
const yamlBefore = await page.locator("#cfg-yaml-area").inputValue();
const yamlDraft = yamlBefore + "\n# hotfix draft";
await page.fill("#cfg-yaml-area", yamlDraft);
await page.click('[data-cfg-mode="form"]');
await page.click('[data-cfg-mode="yaml"]');
const yamlAfterToggle = await page.locator("#cfg-yaml-area").inputValue();
await page.evaluate(() => Router.navigate("/chat"));
await page.waitForSelector("#chat-textarea", { timeout: 15000 });
await page.evaluate(() => Router.navigate("/config"));
await page.waitForSelector("#cfg-yaml-area", { state: "attached", timeout: 15000 });
const configFormDisplay = await page
.locator("#cfg-form-view")
.evaluate(el => getComputedStyle(el).display);
const configYamlDisplay = await page
.locator("#cfg-yaml-view")
.evaluate(el => getComputedStyle(el).display);
const formButtonActive = await page
.locator('[data-cfg-mode="form"]')
.evaluate(el => el.classList.contains("is-active"));
await page.evaluate(() => {
window.__hotfix.delayNextWait = true;
window.__hotfix.channelGuardStartedAt = Date.now();
Router.navigate("/channels");
Router.navigate("/chat");
});
await page.waitForSelector("#chat-textarea", { timeout: 15000 });
await page.waitForFunction(
() =>
window.__hotfix.channelStatusCalls === 0 &&
Date.now() - window.__hotfix.channelGuardStartedAt >= 600,
{ timeout: 2000 }
);
const channelStatusCallsAfterDestroyedWait = await page.evaluate(
() => window.__hotfix.channelStatusCalls
);
await page.evaluate(() => Router.navigate("/sessions"));
await page.waitForSelector('[data-del-key="fake-session"]', { timeout: 15000 });
await page.click('[data-del-key="fake-session"]');
await page.locator(".modal .btn-danger").click();
await page.waitForFunction(
() => document.body.innerText.includes("Delete failed: denied"),
null, { timeout: 5000 }
);
const sessionDeleteBody = await page.locator("body").innerText();
await page.evaluate(() => Router.navigate("/logs"));
await page.waitForSelector("#logs-display", { timeout: 15000 });
await page.waitForFunction(
() => document.body.innerText.includes("Log refresh failed"),
null, { timeout: 5000 }
);
await page.waitForFunction(
() => window.__hotfix.logTailCalls >= 2 && window.__hotfix.logInFlight === 0,
{ timeout: 10000 }
);
const logState = await page.evaluate(() => ({
maxInFlight: window.__hotfix.logMaxInFlight,
tailCalls: window.__hotfix.logTailCalls,
}));
const result = {
statusAfterOtherTask,
statusAfterCurrentTask,
statusAfterQueuedBehindRunning,
regenerateMessages,
bubblesAfterRegenerate,
draftAfterQueueDrain,
queuedBeforeErrorSentPrematurely,
errorRecoveredComposer,
queuedBeforeErrorSentAfterFailure,
approvalStatusDuring,
approvalStatusAfterResolve,
yamlDraftPreserved: yamlAfterToggle === yamlDraft,
configResetToForm:
configFormDisplay !== "none" &&
configYamlDisplay === "none" &&
formButtonActive,
channelStatusCallsAfterDestroyedWait,
sessionDeleteFailed: sessionDeleteBody.includes("Delete failed: denied"),
sessionDeleteFalseSuccess: sessionDeleteBody.includes("Session deleted"),
logState,
pageErrors: errors,
};
await browser.close();
console.log(JSON.stringify(result));
})().catch(err => {
console.error(err && err.stack ? err.stack : String(err));
process.exit(1);
});
"""
),
encoding="utf-8",
)
env = os.environ.copy()
env["OPENSQUILLA_STATE_DIR"] = str(tmp_path / "state")
env["OPENSQUILLA_LOG_DIR"] = str(tmp_path / "logs")
server = subprocess.Popen(
[sys.executable, str(server_script)],
cwd=Path.cwd(),
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
)
try:
_wait_for_health(port, server)
_install_playwright(tmp_path)
result = subprocess.run(
[_node(), str(browser_script)],
cwd=tmp_path,
env=dict(env, TARGET_URL=f"http://127.0.0.1:{port}/control/chat"),
check=False,
capture_output=True,
text=True,
timeout=120,
)
assert result.returncode == 0, result.stderr or result.stdout
payload = json.loads(result.stdout.strip().splitlines()[-1])
finally:
_stop_process(server)
assert payload["pageErrors"] == [], payload["pageErrors"]
assert payload["statusAfterOtherTask"] == "Idle"
assert payload["statusAfterCurrentTask"] == "Queued"
assert payload["statusAfterQueuedBehindRunning"] == "Running"
assert payload["regenerateMessages"].count("first prompt") >= 2
first_regenerate = payload["regenerateMessages"].index("first prompt")
second_send = payload["regenerateMessages"].index("second prompt")
assert first_regenerate < second_send
assert payload["bubblesAfterRegenerate"] <= 3
assert payload["draftAfterQueueDrain"] == "draft typed during stream"
assert payload["queuedBeforeErrorSentPrematurely"] is False
assert "queued before error" in payload["errorRecoveredComposer"]
assert payload["queuedBeforeErrorSentAfterFailure"] is False
assert payload["approvalStatusDuring"] == "Waiting for approval"
assert payload["approvalStatusAfterResolve"] == "Running"
assert payload["yamlDraftPreserved"] is True
assert payload["configResetToForm"] is True
assert payload["channelStatusCallsAfterDestroyedWait"] == 0
assert payload["sessionDeleteFailed"] is True
assert payload["sessionDeleteFalseSuccess"] is False
assert payload["logState"]["tailCalls"] >= 2
assert payload["logState"]["maxInFlight"] == 1
def test_meta_skill_ribbon_renders_and_progresses_in_real_browser(tmp_path: Path) -> None:
"""P0-1 ribbon: inject meta-skill events through the RPC bus and assert
the ribbon renders, chip states advance through running → succeeded, and
the counter reaches its declared total.
We bypass the meta-skill scheduler — the events are pushed directly to
chat.js's ``_rpc.on`` listeners. This isolates the front-end contract
(announce → state → completed) from scheduler timing.
"""
if os.environ.get("OPENSQUILLA_WEBUI_BROWSER_CHAT_E2E") != "1":
pytest.skip("set OPENSQUILLA_WEBUI_BROWSER_CHAT_E2E=1 to run chat browser e2e")
port = _free_port()
server_script = tmp_path / "webui_meta_ribbon_server.py"
browser_script = tmp_path / "webui_meta_ribbon_browser.js"
out_path = tmp_path / "meta_ribbon_payload.json"
server_script.write_text(
textwrap.dedent(
f"""
import uvicorn
from opensquilla.gateway.app import create_gateway_app
from opensquilla.gateway.config import AuthConfig, GatewayConfig
config = GatewayConfig(
host="127.0.0.1",
port={port},
auth=AuthConfig(mode="none"),
)
app = create_gateway_app(config)
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port={port}, log_level="warning")
"""
),
encoding="utf-8",
)
browser_script.write_text(
textwrap.dedent(
r"""
const { chromium } = require("playwright");
const fs = require("fs");
function fire(page, name, payload) {
return page.evaluate(
({ name, payload }) => {
const rpc = App.getRpc();
const ls = rpc._listeners;
const handlers = ls.get(name);
if (handlers) handlers.forEach(h => h(payload));
},
{ name, payload }
);
}
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
const errors = [];
page.on("pageerror", err => errors.push(String(err)));
await page.goto(process.env.TARGET_URL, {
waitUntil: "domcontentloaded",
timeout: 30000,
});
await page.waitForSelector("#chat-textarea", { timeout: 15000 });
await page.waitForFunction(
() =>
typeof App !== "undefined" &&
App.getRpc &&
App.getRpc()?.state === "connected",
{ timeout: 15000 }
);
const RUN_ID = "r-meta-1";
await fire(page, "session.event.meta_run_announced", {
run_id: RUN_ID,
meta_skill_name: "meta-stub-two-step",
steps: [
{ id: "intake", label: "意图提取", kind: "llm_chat", depends_on: [] },
{ id: "summary", label: "总结", kind: "llm_chat", depends_on: ["intake"] },
],
total: 2,
parent_run_id: null,
});
await page.waitForSelector(".meta-ribbon", { timeout: 5000 });
const chipsAfterAnnounce = await page.$$eval(
".meta-ribbon-chips .chip",
els => els.map(e => e.className)
);
await fire(page, "session.event.meta_step_state", {
run_id: RUN_ID, step_id: "intake", state: "running",
});
await fire(page, "session.event.meta_step_state", {
run_id: RUN_ID, step_id: "intake", state: "succeeded",
});
await fire(page, "session.event.meta_step_state", {
run_id: RUN_ID, step_id: "summary", state: "running",
});
await fire(page, "session.event.meta_step_state", {
run_id: RUN_ID, step_id: "summary", state: "succeeded",
});
await fire(page, "session.event.meta_run_completed", {
run_id: RUN_ID,
outcome: "ok",
completed_steps: ["intake", "summary"],
failed_steps: [],
skipped_steps: [],
});
await page.waitForFunction(
() =>
document.querySelector(".meta-ribbon-counter")?.textContent === "2/2",
{ timeout: 5000 }
);
const finalCounter = await page.$eval(
".meta-ribbon-counter",
el => el.textContent
);
const finalChips = await page.$$eval(
".meta-ribbon-chips .chip",
els => els.map(e => e.className)
);
const title = await page.$eval(
".meta-ribbon-title",
el => el.textContent
);
fs.writeFileSync(process.env.OUT_PATH, JSON.stringify({
chipsAfterAnnounce,
finalCounter,
finalChips,
title,
errors,
}));
await browser.close();
})();
"""
),
encoding="utf-8",
)
proc = subprocess.Popen(
[sys.executable, str(server_script)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
try:
deadline = time.time() + 30
ready = False
while time.time() < deadline:
try:
r = httpx.get(f"http://127.0.0.1:{port}/health", timeout=1.0)
if r.status_code < 500:
ready = True
break
except httpx.HTTPError:
pass
time.sleep(0.1)
if not ready:
pytest.skip("gateway did not start in time")
subprocess.run(
["node", str(browser_script)],
check=True,
env={
**os.environ,
"TARGET_URL": f"http://127.0.0.1:{port}/",
"OUT_PATH": str(out_path),
},
timeout=60,
)
finally:
proc.terminate()
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
proc.kill()
payload = json.loads(out_path.read_text(encoding="utf-8"))
assert not payload["errors"], f"page errors: {payload['errors']}"
assert payload["title"] == "meta-stub-two-step"
assert payload["finalCounter"] == "2/2"
# All chips landed in succeeded class on the success path.
assert len(payload["chipsAfterAnnounce"]) == 2
for cls in payload["finalChips"]:
assert "succeeded" in cls, f"chip class did not reach succeeded: {cls}"
def test_issue344_stale_task_events_do_not_leak_into_active_turn_in_real_browser(
tmp_path: Path,
) -> None:
"""issue 344: a stale task's late events must not leak into the active turn.
Legacy static chat UI (frontend=legacy, the surface hosting chat.js). task-B
owns the live stream; we then inject task-A's late tool_use_start and terminal
failure. With the task-id guard they are dropped (recorded as
``*.drop.foreign_task`` in the chat diagnostics log) and never render, while
task-B's own tool and an untagged legacy event still render. Without the guard
the stale tool card and "turn failed" error leaked into task-B's turn.
"""
if os.environ.get("OPENSQUILLA_WEBUI_BROWSER_CHAT_E2E") != "1":
pytest.skip("set OPENSQUILLA_WEBUI_BROWSER_CHAT_E2E=1 to run chat browser e2e")
port = _free_port()
server_script = tmp_path / "issue344_server.py"
browser_script = tmp_path / "issue344_browser.js"
server_script.write_text(
textwrap.dedent(
f"""
import uvicorn
from opensquilla.gateway.app import create_gateway_app
from opensquilla.gateway.config import AuthConfig, GatewayConfig
config = GatewayConfig(
host="127.0.0.1",
port={port},
auth=AuthConfig(mode="none"),
)
app = create_gateway_app(config)
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port={port}, log_level="warning")
"""
),
encoding="utf-8",
)
browser_script.write_text(
textwrap.dedent(
r"""
const { chromium } = require("playwright");
// Dispatch an RPC frame through chat.js's own listeners: named events
// hit their dedicated handler; terminal events ride the wildcard (*)
// listener, exactly as the live RPC bus delivers them.
async function emit(page, event, payload, wild) {
await page.evaluate(({ event, payload, wild }) => {
const ls = App.getRpc()._listeners;
if (wild) {
const h = ls.get("*");
if (h) h.forEach(fn => fn(event, payload));
} else {
const h = ls.get(event);
if (h) h.forEach(fn => fn(payload));
}
}, { event, payload, wild: !!wild });
}
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
const pageErrors = [];
page.on("pageerror", e => pageErrors.push(String(e)));
await page.goto(process.env.TARGET_URL, {
waitUntil: "domcontentloaded",
timeout: 30000,
});
await page.waitForSelector("#chat-textarea", { timeout: 15000 });
await page.waitForFunction(
() =>
typeof App !== "undefined" &&
App.getRpc &&
App.getRpc()?.state === "connected",
{ timeout: 15000 }
);
// Record guard drops in the chat diagnostics log.
await page.evaluate(() =>
localStorage.setItem("opensquilla.chat.debug.enabled", "1")
);
// task-B takes over the live stream and renders its own work.
await emit(page, "task.running", { task_id: "task-B" });
await emit(page, "session.event.text_delta", {
text: "Generating HTML",
task_id: "task-B",
});
await emit(page, "session.event.tool_use_start", {
task_id: "task-B",
tool_use_id: "tb1",
tool_name: "write_html",
});
// Stale task-A tool — must be dropped.
await emit(page, "session.event.tool_use_start", {
task_id: "task-A",
tool_use_id: "ta1",
tool_name: "create_pdf_py_stale",
});
// Untagged legacy event — must still render (lenient guard).
await emit(page, "session.event.tool_use_start", {
tool_use_id: "leg1",
tool_name: "legacy_untagged_tool",
});
// Stale task-A terminal failure — must not end the stream or render.
await emit(
page,
"task.failed",
{ task_id: "task-A", terminal_message: "STALE_PPTX_FAILURE_MARKER" },
true
);
await page.waitForTimeout(300);
const result = await page.evaluate(() => {
const log = JSON.parse(
localStorage.getItem("opensquilla.chat.debugLog") || "[]"
);
const labels = log.map(e => e.label);
const appendNames = log
.filter(e => e.label === "tool_call.append.done")
.map(e => (e.data && e.data.name) || "");
const bodyText = document.body ? document.body.textContent : "";
return {
droppedStaleTool: labels.includes(
"event.tool_use_start.drop.foreign_task"
),
droppedStaleTerminal: labels.includes(
"event.terminal.drop.foreign_task"
),
appendNames,
bodyHasActiveTool: bodyText.includes("write_html"),
bodyHasUntaggedTool: bodyText.includes("legacy_untagged_tool"),
bodyHasStaleTool: bodyText.includes("create_pdf_py_stale"),
bodyHasStaleError: bodyText.includes("STALE_PPTX_FAILURE_MARKER"),
};
});
result.pageErrors = pageErrors;
console.log(JSON.stringify(result));
await browser.close();
})().catch(err => {
console.error(err && err.stack ? err.stack : String(err));
process.exit(1);
});
"""
),
encoding="utf-8",
)
env = os.environ.copy()
env["OPENSQUILLA_STATE_DIR"] = str(tmp_path / "state")
env["OPENSQUILLA_LOG_DIR"] = str(tmp_path / "logs")
# Serve the legacy static chat UI (the surface that hosts chat.js).
env["OPENSQUILLA_CONTROL_UI_FRONTEND"] = "legacy"
server = subprocess.Popen(
[sys.executable, str(server_script)],
cwd=Path.cwd(),
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
)
try:
_wait_for_health(port, server)
_install_playwright(tmp_path)
result = subprocess.run(
[_node(), str(browser_script)],
cwd=tmp_path,
env=dict(env, TARGET_URL=f"http://127.0.0.1:{port}/control/chat"),
check=False,
capture_output=True,
text=True,
timeout=90,
)
assert result.returncode == 0, result.stderr or result.stdout
payload = json.loads(result.stdout.strip().splitlines()[-1])
finally:
_stop_process(server)
assert payload["pageErrors"] == [], payload["pageErrors"]
# Guard fired: the stale task's late tool + terminal were dropped.
assert payload["droppedStaleTool"], "stale task-A tool_use_start was not dropped"
assert payload["droppedStaleTerminal"], "stale task-A terminal was not dropped"
# Stale task-A artifacts never rendered into task-B's turn.
assert not payload["bodyHasStaleTool"], "stale tool card leaked into the turn"
assert not payload["bodyHasStaleError"], "stale terminal error leaked into the turn"
assert "create_pdf_py_stale" not in payload["appendNames"]
# task-B's own tool and untagged legacy events still flow.
assert payload["bodyHasActiveTool"], "active task tool did not render"
assert payload["bodyHasUntaggedTool"], "untagged legacy tool was wrongly dropped"
assert "write_html" in payload["appendNames"]
assert "legacy_untagged_tool" in payload["appendNames"]