agent-zero/tests/test_chat_input_drafts.py
Alessandro 7eb819731c Preserve per-chat composer drafts
Store unsent composer text per chat in session storage and restore it when switching contexts or refreshing the page.

Keep the Welcome composer prompt intact when its first send creates a chat, and cover the behavior with a focused Node regression.
2026-08-19 12:28:42 +02:00

91 lines
3.3 KiB
Python

import base64
from pathlib import Path
import re
import shutil
import subprocess
import pytest
PROJECT_ROOT = Path(__file__).resolve().parents[1]
INPUT_STORE = PROJECT_ROOT / "webui/components/chat/input/input-store.js"
INDEX_JS = PROJECT_ROOT / "webui/index.js"
@pytest.mark.skipif(not shutil.which("node"), reason="Node.js is required")
def test_chat_input_keeps_separate_session_drafts() -> None:
index_source = INDEX_JS.read_text(encoding="utf-8")
set_context = index_source[index_source.index("export const setContext"):]
assert set_context.index("inputStore.setDraftContext(id);") < set_context.index("context = id;")
source = INPUT_STORE.read_text(encoding="utf-8")
source = re.sub(r"^import .*?;\n", "", source, flags=re.MULTILINE)
source = source[: source.index('const store = createStore("chatInput", model);')]
module_source = r"""
const shortcuts = {
getCurrentContextId: () => globalThis.__context,
callJsonApi: async () => ({}),
frontendNotification: () => {},
NotificationType: {},
NotificationPriority: {},
};
const fileBrowserStore = {};
const messageQueueStore = { hasQueue: false };
const attachmentsStore = {
attachments: [],
clearAttachments() { this.attachments = []; },
};
const chatsStore = { selected: "", selectedContext: null };
""" + source + "\nexport { model, chatsStore };\n"
module_url = "data:text/javascript;base64," + base64.b64encode(
module_source.encode("utf-8")
).decode("ascii")
script = f"""
const makeStorage = () => ({{
values: new Map(),
getItem(key) {{ return this.values.get(key) ?? null; }},
setItem(key, value) {{ this.values.set(key, String(value)); }},
removeItem(key) {{ this.values.delete(key); }},
}});
globalThis.sessionStorage = makeStorage();
globalThis.localStorage = makeStorage();
globalThis.document = {{ activeElement: null, getElementById: () => null, querySelectorAll: () => [] }};
globalThis.__context = null;
const {{ model, chatsStore }} = await import({module_url!r});
const assert = (condition, message) => {{ if (!condition) throw new Error(message); }};
globalThis.__context = "chat-a";
model.setDraftContext("chat-a");
model.message = "alpha draft";
assert(sessionStorage.getItem("a0:chat-draft:chat-a") === "alpha draft", "chat A was not saved");
globalThis.__context = "chat-b";
model.setDraftContext("chat-b");
assert(model.message === "", "a new chat inherited another chat's draft");
model.message = "beta draft";
globalThis.__context = "chat-a";
model.setDraftContext("chat-a");
assert(model.message === "alpha draft", "chat A was not restored");
model.message = "";
assert(sessionStorage.getItem("a0:chat-draft:chat-a") === null, "cleared draft remained stored");
globalThis.__context = null;
model.setDraftContext("");
model.message = "welcome prompt";
chatsStore.newChat = async () => {{
globalThis.__context = "chat-new";
chatsStore.selected = "chat-new";
model.setDraftContext("chat-new");
return "chat-new";
}};
let sent = "";
globalThis.sendMessage = async () => {{ sent = model.message; }};
await model.sendMessage();
assert(sent === "welcome prompt", "creating a chat erased the Welcome prompt");
assert(sessionStorage.getItem("a0:chat-draft:chat-new") === "welcome prompt", "first prompt did not follow its new chat");
"""
subprocess.run(["node", "--input-type=module", "-e", script], check=True, text=True)