The thinking row and the header menu both carry Stop while a bot has work open for the user anywhere: every pending or processing job of that bot in their conversations is failed as stopped, its task closed, a Stopped step recorded and one closing message posted per session, so the thread stops waiting and the bot goes idle. The handler running the job is aborted from this process through a registry, so a job stuck waiting on a model stops at once instead of on its next heartbeat, and a late face change after the abort is dropped. The release stage is now said in the app too: Settings > General reports "Cobble v0.1.5 (alpha)" next to development, and the README and RELEASING.md say what alpha means for installs.
70 lines
2.9 KiB
TypeScript
70 lines
2.9 KiB
TypeScript
import { Database } from "bun:sqlite";
|
|
import { describe, expect, test } from "bun:test";
|
|
import openBotJobsSql from "./open-bot-jobs-sql";
|
|
import type { SqlCondition } from "@/src/utils/crew-access/types";
|
|
|
|
/**
|
|
* What "stop the bot" pulls the plug on: a bot's open jobs for one user,
|
|
* across every one of their conversations with it. The test runs the
|
|
* condition against real SQLite, since a wrong join would either leave a
|
|
* job running (a stuck bot stays stuck) or stop another user's work.
|
|
*/
|
|
const db = new Database(":memory:");
|
|
|
|
db.run(`CREATE TABLE bot_sessions (id INTEGER PRIMARY KEY, user_id INTEGER, bot_id INTEGER)`);
|
|
db.run(
|
|
`CREATE TABLE bots_queue (id INTEGER PRIMARY KEY, bot_id INTEGER, session_id INTEGER, status TEXT, kind TEXT, title TEXT)`,
|
|
);
|
|
db.run(`INSERT INTO bot_sessions VALUES (100, 7, 2), (101, 7, 2), (102, 8, 2), (103, 9, 3)`);
|
|
db.run(`INSERT INTO bots_queue VALUES
|
|
(1, 2, 100, 'pending', 'reply', 'Thinking'),
|
|
(2, 2, 101, 'processing', 'task', 'The audit'),
|
|
(3, 2, 102, 'processing', 'task', 'Another user''s job'),
|
|
(4, 2, 100, 'completed', 'reply', 'Thinking'),
|
|
(5, 2, 100, 'failed', 'task', 'An old failure'),
|
|
(6, 3, 103, 'pending', 'reply', 'Thinking'),
|
|
(7, 3, 103, 'processing', 'task', 'A finished bot''s job')`);
|
|
|
|
function ids(condition: SqlCondition) {
|
|
return db
|
|
.query(`SELECT q.id FROM bots_queue q WHERE ${condition.sql} ORDER BY q.id`)
|
|
.all(...condition.values)
|
|
.map((row) => (row as { id: number }).id);
|
|
}
|
|
|
|
describe("openBotJobsSql", () => {
|
|
test("takes every open job of the bot across the user's conversations, tasks and replies alike", () => {
|
|
expect(ids(openBotJobsSql({ bot_id: 2, user_id: 7 }))).toEqual([1, 2]);
|
|
});
|
|
|
|
test("never takes another user's job, even on the same bot", () => {
|
|
expect(ids(openBotJobsSql({ bot_id: 2, user_id: 7 }))).not.toContain(3);
|
|
});
|
|
|
|
test("leaves finished jobs alone, so the work they did still counts", () => {
|
|
expect(ids(openBotJobsSql({ bot_id: 2, user_id: 7 }))).not.toContain(4);
|
|
expect(ids(openBotJobsSql({ bot_id: 2, user_id: 7 }))).not.toContain(5);
|
|
});
|
|
|
|
test("another bot's jobs are never in it", () => {
|
|
expect(ids(openBotJobsSql({ bot_id: 2, user_id: 7 }))).not.toContain(6);
|
|
expect(ids(openBotJobsSql({ bot_id: 3, user_id: 9 }))).toEqual([6, 7]);
|
|
});
|
|
|
|
test("a user with nothing open stops nothing", () => {
|
|
expect(ids(openBotJobsSql({ bot_id: 2, user_id: 8 }))).toEqual([3]);
|
|
expect(ids(openBotJobsSql({ bot_id: 2, user_id: 99 }))).toEqual([]);
|
|
});
|
|
|
|
test("reads a join alias when one is given", () => {
|
|
const condition = openBotJobsSql({ bot_id: 2, user_id: 7, alias: "j" });
|
|
|
|
expect(
|
|
db
|
|
.query(`SELECT j.id FROM bots_queue j WHERE ${condition.sql} ORDER BY j.id`)
|
|
.all(...condition.values)
|
|
.map((row) => (row as { id: number }).id),
|
|
).toEqual([1, 2]);
|
|
});
|
|
});
|