Files
KrisHD_AI/test/composer.test.js
T
Fretux c48e1b17c4
CI / check (22) (push) Successful in 10m3s
CI / check (24) (push) Successful in 9m34s
Fix admin authorization and harden chat requests
2026-10-05 22:49:29 +02:00

87 lines
4.7 KiB
JavaScript

import test from "node:test";
import assert from "node:assert/strict";
import {createComposerController} from "../public/js/composerController.js";
import {createConversationState} from "../public/js/conversationState.js";
const deferred = () => { let resolve; const promise = new Promise(done => { resolve = done; }); return {promise, resolve}; };
const noop = () => {};
const element = () => ({value: "Hello", style: {}, scrollHeight: 0, scrollTop: 0, clientHeight: 0,
classList: {toggle: noop, add: noop}, setAttribute: noop, focus: noop, querySelectorAll: () => []});
function fixture(overrides = {}) {
const state = createConversationState();
const elements = {msgInput: element(), sendBtn: element(), messagesDiv: element(), modelSelect: {value: "original-model"}};
const calls = {create: 0, streams: [], refresh: [], notices: []};
const controller = createComposerController({state, elements,
attachmentComposer: {pending: false, attachments: [], setBusy: noop, clear: noop, clearSentDraft: noop},
webSearchButton: element(), reasoningToggle: {enabled: true, setBusy: noop, reset: noop}, webSearchToggle: {enabled: true, reset: noop},
setChatActivity: noop, updateComposerPlaceholder: noop, setNotice: message => calls.notices.push(message),
createNewChat: async () => {
calls.create++;
const chat = overrides.creation ? await overrides.creation : {id: 1, title: "New chat"};
if (chat?.id) { state.activeChatId = chat.id; state.chatSessions.push(chat); }
return chat;
},
getChatById: id => state.chatSessions.find(chat => chat.id === id),
updateWorkspaceCopy: noop, renderChatList: noop, updateChatActionState: noop, scrollMessagesToBottom: noop,
addMessage: () => ({isConnected: true, querySelector: () => element(), remove: noop, replaceWith: noop}),
setMessageContent: noop, setActiveChat: async id => calls.refresh.push(id), loadSummary: noop, markOnboardingCompleted: noop, maybeShowPersonaNudge: noop,
api: {put: async () => overrides.rename ? await overrides.rename : {}, post: async () => assert.fail("Send cancellation must abort its own request"),
stream: async (_url, payload, handlers) => {
calls.streams.push(payload);
if (overrides.failure) throw new Error(overrides.failure);
handlers.onDone({reply: "Done"});
}}
});
return {state, elements, calls, ...controller};
}
test("rapid sends lock before chat creation and keep the captured chat/model after navigation during rename", async () => {
const creation = deferred(), rename = deferred();
const s = fixture({creation: creation.promise, rename: rename.promise});
const first = s.sendMessage();
await s.sendMessage();
assert.equal(s.state.isProcessing, true);
assert.equal(s.calls.create, 1);
creation.resolve({id: 7, title: "New chat"});
await new Promise(resolve => setImmediate(resolve));
await s.sendMessage();
s.state.activeChatId = 8;
s.elements.modelSelect.value = "changed-model";
s.elements.msgInput.value = "Draft in another conversation";
rename.resolve({});
await first;
assert.equal(s.calls.streams.length, 1);
assert.equal(s.calls.streams[0].chatId, 7);
assert.equal(s.calls.streams[0].model, "original-model");
assert.equal(s.calls.streams[0].reasoning, true);
assert.deepEqual(s.calls.refresh, []);
assert.deepEqual(s.state.currentMessages, []);
assert.equal(s.elements.msgInput.value, "Draft in another conversation");
assert.equal(s.state.isProcessing, false);
});
test("creation, rename and stream failures unlock sending and retain the draft", async () => {
for (const options of [{creation: Promise.resolve({error: "Creation failed"})}, {rename: Promise.resolve({error: "Rename failed"})}, {failure: "Stream failed"}]) {
const s = fixture(options);
await s.sendMessage();
assert.equal(s.state.isProcessing, false);
assert.equal(s.elements.msgInput.disabled, false);
assert.equal(s.elements.msgInput.value, "Hello");
assert.match(s.calls.notices.at(-1), /failed/);
if (options.failure) assert.equal(s.state.currentMessages[0].deliveryStatus, "failed");
else assert.equal(s.calls.streams.length, 0);
}
});
test("Stop during chat creation prevents a later stream and releases the send lock", async () => {
const creation = deferred();
const s = fixture({creation: creation.promise});
const pending = s.sendMessage();
await s.stopGeneration();
creation.resolve({id: 4, title: "New chat"});
await pending;
assert.equal(s.calls.streams.length, 0);
assert.equal(s.state.isProcessing, false);
assert.equal(s.elements.msgInput.value, "Hello");
});