import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import { enqueueMessage, readQueuedMessages, removeQueuedMessage, } from "../web/static/js/chat_queue.js"; class MemoryStorage { constructor() { this.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); } } test("queued messages are ordered and isolated by user and task", () => { const storage = new MemoryStorage(); enqueueMessage(storage, "u1", "t1", { id: "1", content: "第一条" }); enqueueMessage(storage, "u1", "t1", { id: "2", content: "第二条" }); enqueueMessage(storage, "u1", "t2", { id: "3", content: "另一对话" }); enqueueMessage(storage, "u2", "t1", { id: "4", content: "另一用户" }); assert.deepEqual(readQueuedMessages(storage, "u1", "t1").map(x => x.id), ["1", "2"]); assert.equal(readQueuedMessages(storage, "u1", "t2")[0].content, "另一对话"); assert.equal(readQueuedMessages(storage, "u2", "t1")[0].content, "另一用户"); }); test("queued messages can be removed without disturbing later entries", () => { const storage = new MemoryStorage(); enqueueMessage(storage, "u1", "t1", { id: "1", content: "第一条" }); enqueueMessage(storage, "u1", "t1", { id: "2", content: "第二条" }); const remaining = removeQueuedMessage(storage, "u1", "t1", "1"); assert.deepEqual(remaining.map(x => x.id), ["2"]); assert.deepEqual(readQueuedMessages(storage, "u1", "t1").map(x => x.id), ["2"]); }); test("a task queue is bounded", () => { const storage = new MemoryStorage(); for (let i = 0; i < 25; i++) { enqueueMessage(storage, "u1", "t1", { id: String(i), content: String(i) }); } const items = readQueuedMessages(storage, "u1", "t1"); assert.equal(items.length, 20); assert.equal(items[0].id, "5"); assert.equal(items.at(-1).id, "24"); }); test("composer uses one Codex-style action button for stop and queued send", () => { const html = readFileSync(new URL("../web/static/dev.html", import.meta.url), "utf8"); const chat = readFileSync(new URL("../web/static/js/chat.js", import.meta.url), "utf8"); assert.match(html, /id="chat-action"/); assert.doesNotMatch(html, /id="chat-stop"/); assert.match(html, /\.msg\.queued/); assert.match(chat, /card\.className = "msg user queued"/); assert.match(chat, /function composerHasPayload\(\)/); assert.match(chat, /btn\.textContent = "停止"/); assert.match(chat, /if \(composerHasPayload\(\)\)[\s\S]*queueCurrentMessage\(\)/); assert.match(chat, /_actionMode === "loading" \|\| _actionMode === "submitting"[\s\S]*btn\.disabled = true/); assert.match(chat, /setActionMode\("submitting"\)/); assert.match(chat, /if \(state\.taskId === taskId\) \$\("chat-input"\)\.value = ""/); assert.match(chat, /if \(state\.taskId === taskId\) setActionMode\("streaming"\)/); assert.match(chat, /function queueCurrentMessage\(\)/); assert.match(chat, /async function dispatchNextQueuedMessage\(taskId\)/); assert.match(chat, /void dispatchNextQueuedMessage\(ctx\.taskId\)/); assert.match(chat, /removeQueuedMessage\([\s\S]*streamSse\(r\.events_url, run\)/); }); test("automatic title polling starts before a long-running response finishes", () => { const chat = readFileSync(new URL("../web/static/js/chat.js", import.meta.url), "utf8"); const sendMessage = chat.indexOf("async function sendMessage("); const poll = chat.indexOf("void pollAutoTitle(taskId)", sendMessage); const stream = chat.indexOf("streamSse(r.events_url, run)", sendMessage); assert.ok(poll > sendMessage, "sendMessage should start title polling"); assert.ok(poll < stream, "title polling should start before waiting on the SSE run"); });