Allow message retries for "pending" status, refactor message context handling, remove workspace import/export routes, and add SQLite-backed rate limiting.
This commit is contained in:
+4
-1
@@ -235,9 +235,12 @@ test("conversation memory exposes context estimates and supports validated summa
|
||||
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 userMessage = repo.insertChatMessage(chat.id, "user", "I prefer short answers.");
|
||||
const assistantMessage = repo.insertChatMessage(chat.id, "bot", "I will keep them concise.", null, null, "complete", null, Number(userMessage.lastInsertRowid));
|
||||
repo.updateChatContextSummary(chat.id, "memory-editor", "Old summary", Number(assistantMessage.lastInsertRowid));
|
||||
const loaded = (await request(server.baseUrl, `/chats/${chat.id}/memories`, {jar})).json;
|
||||
assert.equal(loaded.summary, "Old summary"); assert.ok(loaded.context.estimatedTokens > 0);
|
||||
assert.deepEqual(loaded.summarySources.map(message => message.content), ["I prefer short answers.", "I will keep them concise."]);
|
||||
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");
|
||||
|
||||
@@ -75,7 +75,7 @@ async function setup(t, {visionResult = null, replyResult = null, visionModel =
|
||||
const image = format => sharp({create: {width: 20, height: 10, channels: 3, background: "blue"}}).toFormat(format).toBuffer();
|
||||
const chat = async () => (await (await api("/chats", {title: "Image test"})).json()).chat.id;
|
||||
return {app, api, upload, image, register, directory, requests, order, searchQueries, chat,
|
||||
failVision: () => { visionFailure = true; }, holdVision: () => { holdVision = true; return visionStarted; },
|
||||
failVision: () => { visionFailure = true; }, recoverVision: () => { visionFailure = false; }, holdVision: () => { holdVision = true; return visionStarted; },
|
||||
get visionAborted() { return visionAborted; }};
|
||||
}
|
||||
|
||||
@@ -239,6 +239,26 @@ test("vision failure prevents an ungrounded answer and malformed JSON falls back
|
||||
assert.throws(() => parseVisionAnalysis(""));
|
||||
});
|
||||
|
||||
test("resuming a failed image turn reuses its branch node and attachment", async t => {
|
||||
const s = await setup(t);
|
||||
const attachment = await (await s.upload(await s.image("png"))).json();
|
||||
const chatId = await s.chat();
|
||||
s.failVision();
|
||||
const failedWire = await (await s.api("/chat/stream", {chatId, message: "Describe the image", attachments: [attachment.id]})).text();
|
||||
assert.match(failedWire, /event: error/);
|
||||
const failedMessages = (await (await s.api(`/chats/${chatId}/messages`)).json()).messages;
|
||||
const interrupted = failedMessages.find(message => message.role === "user");
|
||||
assert.equal(interrupted.deliveryStatus, "failed");
|
||||
s.recoverVision();
|
||||
const resumedWire = await (await s.api("/chat/stream", {chatId, message: interrupted.content,
|
||||
attachments: [attachment.id], userMessageId: interrupted.id})).text();
|
||||
assert.match(resumedWire, /event: done/);
|
||||
const messages = (await (await s.api(`/chats/${chatId}/messages`)).json()).messages;
|
||||
assert.equal(messages.filter(message => message.role === "user").length, 1);
|
||||
assert.equal(messages[0].attachments[0].id, attachment.id);
|
||||
assert.equal(messages[0].deliveryStatus, "complete");
|
||||
});
|
||||
|
||||
test("browser disconnect cancels in-progress vision and never starts the final model", async t => {
|
||||
const s = await setup(t);
|
||||
const attachment = await (await s.upload(await s.image("png"))).json();
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import Database from "better-sqlite3";
|
||||
import {createRateLimiter, createSqliteRateLimitStore} from "../src/middleware/rateLimit.js";
|
||||
|
||||
test("SQLite rate limit store shares rolling-window hits between limiter instances", () => {
|
||||
const db = new Database(":memory:");
|
||||
const firstStore = createSqliteRateLimitStore(db);
|
||||
const secondStore = createSqliteRateLimitStore(db);
|
||||
const options = {windowMs: 1000, max: 2, keyGenerator: () => "account:test", message: "limited", cleanupIntervalMs: 0};
|
||||
const first = createRateLimiter({...options, store: firstStore});
|
||||
const second = createRateLimiter({...options, store: secondStore});
|
||||
const hit = limiter => {
|
||||
let allowed = false, status = null;
|
||||
limiter({}, {status(value) {status = value; return this;}, json() {}}, () => {allowed = true;});
|
||||
return {allowed, status};
|
||||
};
|
||||
|
||||
assert.deepEqual(hit(first), {allowed: true, status: null});
|
||||
assert.deepEqual(hit(second), {allowed: true, status: null});
|
||||
assert.deepEqual(hit(first), {allowed: false, status: 429});
|
||||
assert.equal(firstStore.consume("account:test", Date.now() + 1001, 1000, 2), true);
|
||||
first.close(); second.close(); db.close();
|
||||
});
|
||||
Reference in New Issue
Block a user