Add support for message history pagination, preserve scroll in rendering, and improve draft/message workflows.
This commit is contained in:
@@ -116,6 +116,102 @@ async function request(baseUrl, path, {method = "GET", body, jar} = {}) {
|
||||
return {status: response.status, json};
|
||||
}
|
||||
|
||||
test("public health checks reach the backend without exposing diagnostics or requiring a session", async t => {
|
||||
const server = await startTestServer();
|
||||
t.after(() => server.close());
|
||||
let checks = 0;
|
||||
server.app.locals.modelService.checkHealth = async () => { checks++; return {ok: checks === 1, backendUrl: "private-backend", error: "private detail"}; };
|
||||
assert.deepEqual(await request(server.baseUrl, "/health"), {status: 200, json: {ok: true}});
|
||||
assert.deepEqual(await request(server.baseUrl, "/health"), {status: 503, json: {ok: false}});
|
||||
assert.equal(checks, 2);
|
||||
assert.equal((await request(server.baseUrl, "/admin/diagnostics")).status, 403);
|
||||
});
|
||||
|
||||
test("new passwords are limited to 72 UTF-8 bytes while legacy credentials remain usable", async t => {
|
||||
const server = await startTestServer({config: {authRateLimitMax: 100}});
|
||||
t.after(() => server.close());
|
||||
for (const password of ["a".repeat(73), "é".repeat(37), "😀".repeat(19)]) {
|
||||
const result = await request(server.baseUrl, "/register", {method: "POST", body: {username: "too-long", password}});
|
||||
assert.equal(result.status, 400);
|
||||
assert.equal(result.json.code, "PASSWORD_TOO_LONG");
|
||||
assert.equal(server.app.locals.repositories.getUser("too-long"), undefined);
|
||||
}
|
||||
const jar = new CookieJar();
|
||||
const boundary = "é".repeat(36);
|
||||
assert.equal((await request(server.baseUrl, "/register", {method: "POST", jar, body: {username: "boundary", password: boundary}})).status, 200);
|
||||
const change = newPassword => request(server.baseUrl, "/settings/password", {method: "PUT", jar, body: {currentPassword: boundary, newPassword}});
|
||||
assert.equal((await change("a".repeat(73))).json.code, "PASSWORD_TOO_LONG");
|
||||
assert.equal((await change("😀".repeat(19))).json.code, "PASSWORD_TOO_LONG");
|
||||
assert.equal((await change("a".repeat(72))).status, 200);
|
||||
const legacy = "a".repeat(72) + "existing suffix";
|
||||
server.app.locals.repositories.insertUser("legacy", await bcrypt.hash(legacy, 4));
|
||||
assert.equal((await request(server.baseUrl, "/login", {method: "POST", jar, body: {username: "legacy", password: legacy}})).status, 200);
|
||||
assert.equal((await request(server.baseUrl, "/settings/password", {method: "PUT", jar, body: {currentPassword: legacy, newPassword: "new-password"}})).status, 200);
|
||||
});
|
||||
|
||||
test("history pages are bounded, preserve branch identity and batch message metadata", async t => {
|
||||
const server = await startTestServer(); t.after(() => server.close());
|
||||
const jar = new CookieJar();
|
||||
await request(server.baseUrl, "/register", {method: "POST", jar, body: {username: "paged", password: "password123"}});
|
||||
const repo = server.app.locals.repositories;
|
||||
const chat = repo.createChat("paged", "Long conversation");
|
||||
let parent = null;
|
||||
const ids = [];
|
||||
for (let index = 0; index < 125; index++) {
|
||||
parent = Number(repo.insertBranchChatMessage(chat.id, index % 2 ? "bot" : "user", `Message ${index}`, null, "test", "complete", null, parent).lastInsertRowid);
|
||||
ids.push(parent);
|
||||
}
|
||||
repo.saveMessageVersion(chat.id, ids[123], {content: "Alternate answer", modelName: "other-model"});
|
||||
const sibling = Number(repo.insertBranchChatMessage(chat.id, "bot", "Other branch", null, "test", "complete", null, ids[122]).lastInsertRowid);
|
||||
repo.updateChatActiveLeaf(chat.id, "paged", parent);
|
||||
t.mock.method(repo, "listMessageVersions", () => assert.fail("History should batch versions"));
|
||||
t.mock.method(repo, "listSiblingChatMessages", () => assert.fail("History should batch siblings"));
|
||||
t.mock.method(server.app.locals.attachmentService, "list", () => assert.fail("History should batch attachments"));
|
||||
const first = await request(server.baseUrl, `/chats/${chat.id}/messages`, {jar});
|
||||
assert.equal(first.status, 200); assert.equal(first.json.messages.length, 50);
|
||||
assert.equal(first.json.messages[0].id, ids[75]);
|
||||
const answer = first.json.messages.find(message => message.id === ids[123]);
|
||||
assert.equal(answer.versionCount, 2); assert.equal(answer.nextSiblingId, sibling);
|
||||
const page = first.json.pagination;
|
||||
const second = await request(server.baseUrl, `/chats/${chat.id}/messages?before=${page.before}&leaf=${page.leafId}`, {jar});
|
||||
assert.deepEqual(second.json.messages.map(message => message.id), ids.slice(25, 75));
|
||||
const third = await request(server.baseUrl, `/chats/${chat.id}/messages?before=${second.json.pagination.before}&leaf=${page.leafId}`, {jar});
|
||||
assert.deepEqual(third.json.messages.map(message => message.id), ids.slice(0, 25));
|
||||
assert.equal(third.json.pagination.hasMore, false);
|
||||
const oldMatch = await request(server.baseUrl, `/chats/${chat.id}/messages?around=${ids[4]}`, {jar});
|
||||
assert.equal(oldMatch.json.matchOnBranch, true); assert.equal(oldMatch.json.pagination.hasNewer, true);
|
||||
assert.equal(oldMatch.json.messages.at(-1).id, ids[4]);
|
||||
assert.equal((await request(server.baseUrl, `/chats/${chat.id}/messages?before=${sibling}`, {jar})).status, 400);
|
||||
repo.updateChatActiveLeaf(chat.id, "paged", sibling);
|
||||
assert.equal((await request(server.baseUrl, `/chats/${chat.id}/messages?before=${page.before}&leaf=${page.leafId}`, {jar})).status, 409);
|
||||
assert.equal((await request(server.baseUrl, `/chats/${chat.id}/messages?limit=101`, {jar})).status, 400);
|
||||
assert.equal((await request(server.baseUrl, `/chats/${chat.id}/branches`, {jar})).json.nodes.length, 126);
|
||||
const stranger = new CookieJar();
|
||||
await request(server.baseUrl, "/register", {method: "POST", jar: stranger, body: {username: "stranger", password: "password123"}});
|
||||
for (const suffix of ["messages", "branches", `messages/${ids[123]}/versions`, "memories"]) {
|
||||
assert.equal((await request(server.baseUrl, `/chats/${chat.id}/${suffix}`, {jar: stranger})).status, 404);
|
||||
}
|
||||
});
|
||||
|
||||
test("conversation memory exposes context estimates and supports validated summary corrections", async t => {
|
||||
const server = await startTestServer(); t.after(() => server.close());
|
||||
const jar = new CookieJar();
|
||||
await request(server.baseUrl, "/register", {method: "POST", jar, body: {username: "memory-editor", password: "password123"}});
|
||||
const repo = server.app.locals.repositories;
|
||||
const chat = repo.createChat("memory-editor", "Memory");
|
||||
repo.createChatMemory("memory-editor", chat, "Prefers concise answers.");
|
||||
repo.updateChatContextSummary(chat.id, "memory-editor", "Old summary", 0);
|
||||
const loaded = (await request(server.baseUrl, `/chats/${chat.id}/memories`, {jar})).json;
|
||||
assert.equal(loaded.summary, "Old summary"); assert.ok(loaded.context.estimatedTokens > 0);
|
||||
const update = body => request(server.baseUrl, `/chats/${chat.id}/summary`, {method: "PUT", jar, body});
|
||||
assert.equal((await update({summary: "Corrected summary", previousSummary: "Old summary"})).status, 200);
|
||||
assert.equal(repo.getChat(chat.id, "memory-editor").context_summary, "Corrected summary");
|
||||
assert.equal((await update({summary: "Stale edit", previousSummary: "Old summary"})).status, 409);
|
||||
assert.equal((await update({summary: "x".repeat(loaded.summaryMaxChars + 1)})).status, 400);
|
||||
assert.equal((await update({summary: "", previousSummary: "Corrected summary"})).status, 200);
|
||||
assert.equal(repo.getChat(chat.id, "memory-editor").context_summary, null);
|
||||
});
|
||||
|
||||
test("auth flow validates inputs and persists session", async () => {
|
||||
const server = await startTestServer();
|
||||
const jar = new CookieJar();
|
||||
|
||||
Reference in New Issue
Block a user