A bot hook (bot_hooks) is a secret URL, POST /api/hooks/<token>, whose calls queue the hook's brief plus the raw body as a task, per user and bot like a routine. Only the token's hash is kept (shown once, rotatable); an optional HMAC-SHA256 signing secret is stored encrypted. Calls are rate limited per address and per hook, recorded in bot_hook_events (pruned daily), deduplicated by delivery id, and a call while the last run still waits joins its brief instead of queueing another. The payload is fenced data and HookRunNote tells the bot never to take instructions from it; failed runs retry like routines. A hook can instead watch a computer's browser notifications: the app installs a small watcher that owns org.freedesktop.Notifications on the sandbox's session bus, so Chrome hands it every notification (it only looks for that service at start-up, so Chrome is restarted once, never mid-task), and it posts each one to its hooks, optionally filtered. Computers reach the app at COMPUTER_APP_URL or the app container's name. Users make hooks in the conversation's Hooks modal and manage them all on /admin/routines; bots never create or change hooks. Co-Authored-By: Claude Opus 5.5 <[email protected]>
48 lines
1.7 KiB
TypeScript
48 lines
1.7 KiB
TypeScript
import { Database } from "bun:sqlite";
|
|
import { describe, expect, test } from "bun:test";
|
|
import mergeHookCallSql from "./merge-hook-call-sql";
|
|
|
|
/**
|
|
* A burst of hook calls makes one waiting run: a call joins the last run
|
|
* only while it waits untried. Run against real SQLite, since a wrong
|
|
* condition would either change the brief of a run already working or
|
|
* pile every call into a run that is retrying.
|
|
*/
|
|
const db = new Database(":memory:");
|
|
|
|
db.run(`CREATE TABLE bots_queue (id INTEGER PRIMARY KEY, status TEXT, attempts INTEGER, instructions TEXT, updated_at INTEGER)`);
|
|
db.run(`INSERT INTO bots_queue VALUES
|
|
(1, 'pending', 0, 'Brief', 0),
|
|
(2, 'processing', 1, 'Brief', 0),
|
|
(3, 'pending', 1, 'Brief', 0),
|
|
(4, 'completed', 1, 'Brief', 0),
|
|
(5, 'pending', NULL, NULL, 0)`);
|
|
|
|
function merge(job_id: number) {
|
|
const { sql, values } = mergeHookCallSql({ job_id, block: "CALL", now: 42 });
|
|
|
|
return db.query(sql).all(...values).length > 0;
|
|
}
|
|
|
|
function brief(job_id: number) {
|
|
return (db.query(`SELECT instructions FROM bots_queue WHERE id = ?`).get(job_id) as { instructions: string })
|
|
.instructions;
|
|
}
|
|
|
|
describe("mergeHookCallSql", () => {
|
|
test("joins a run still waiting, untried", () => {
|
|
expect(merge(1)).toBe(true);
|
|
expect(brief(1)).toBe("Brief\n\nAnother call came in before this run started:\n\nCALL");
|
|
expect(merge(5)).toBe(true);
|
|
expect(brief(5)).toContain("CALL");
|
|
});
|
|
|
|
test("leaves a working, retrying or finished run alone", () => {
|
|
expect(merge(2)).toBe(false);
|
|
expect(merge(3)).toBe(false);
|
|
expect(merge(4)).toBe(false);
|
|
expect(brief(2)).toBe("Brief");
|
|
expect(brief(3)).toBe("Brief");
|
|
});
|
|
});
|