Files
cobble/src/functions/backend/db/bots-queue/open-bot-jobs-sql.test.ts
T
tben fcad4b49d6 Let a user stop a bot, and say the app is still alpha
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.
2026-10-23 10:19:41 +01:00

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]);
});
});