Files
cobble/src/bots-queue/bot-messages-to-harness.test.ts
T
tben f2802c1bf3 Let bots set a picture from a file they can see
A coordinator asked to set an uploaded logo as a workspace picture could
not: the picture tools take a media id, and nothing ever gave the bot one.
Attachments reached the model as "[Attached image \"logo.png\"]", every
file tool answers with a [file:ID] ref, and an SVG is deliberately stored
as a document rather than an image.

- copyMediaAsPicture copies an approved image into a new picture row
  (bot-image/workspace-image). The source keeps its row and bytes, so a
  message that attached it still loads and changing a picture can never
  take a file away from a conversation.
- grabPictureSource resolves a picture by [file:ID] (the same scope gate
  every file tool uses) or by the user's own upload; parseFileRef reads
  the [file:12] form.
- The new set_picture tool ships on every run: a bot sets its own face,
  the coordinator can also dress a worker. update_workspace gained
  picture_file_id, and setBotPicture/setProfileImage share the copy and
  the delete.
- removeReplacedPicture deletes a replaced picture only when it really is
  one, never a chat upload.
- uploadAndRecordMedia writes images in their own format: PNG stays PNG
  with its transparency, WebP stays WebP, GIF becomes PNG, everything
  else JPEG, and the row records the real mime type (it used to say jpeg
  for every image).
- Pictures change live: BotUpdatedData carries image_media_id and a new
  server:workspace-updated pushes workspace pictures, relayed between
  processes.
- Attachments are labelled with their media id, and BotFilesNote tells a
  bot that a [file:ID] can become a picture.
- grabRunScopes is now the single source of a run's reachable folders,
  shared by the file tools, imports and the picture tools.
2026-10-23 05:24:55 +01:00

156 lines
5.3 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import botMessagesToHarness from "./bot-messages-to-harness";
const message = (author: "user" | "bot", text: string) => ({ author, text });
describe("botMessagesToHarness", () => {
test("maps authors to roles", () => {
expect(
botMessagesToHarness({
messages: [message("user", "hi"), message("bot", "hello")],
}),
).toEqual([
{ role: "user", content: "hi" },
{ role: "assistant", content: "hello" },
]);
});
test("joins back-to-back messages from one side", () => {
expect(
botMessagesToHarness({
messages: [message("user", "one"), message("user", "two")],
}),
).toEqual([{ role: "user", content: "one\n\ntwo" }]);
});
test("starts with the user and skips empty messages", () => {
expect(
botMessagesToHarness({
messages: [
message("bot", "welcome"),
message("user", " "),
message("user", "hi"),
],
}),
).toEqual([{ role: "user", content: "hi" }]);
});
});
describe("botMessagesToHarness with replies", () => {
test("quotes what a user message answers", () => {
expect(
botMessagesToHarness({
messages: [
{ author: "user", text: "hi" },
{ author: "bot", text: "The report is ready." },
{
author: "user",
text: "Great, send it over.",
reply_to: { id: 4, author: "bot", text: "The report is ready." },
},
],
}),
).toEqual([
{ role: "user", content: "hi" },
{ role: "assistant", content: "The report is ready." },
{
role: "user",
content: `[Replying to your earlier message: "The report is ready."]\n\nGreat, send it over.`,
},
]);
});
test("says a quoted file-only message was a file", () => {
expect(
botMessagesToHarness({
messages: [
{
author: "user",
text: "What was that?",
reply_to: { id: 5, author: "user", text: "" },
},
],
}),
).toEqual([
{
role: "user",
content: `[Replying to the user's own earlier message: "(a file)"]\n\nWhat was that?`,
},
]);
});
});
describe("botMessagesToHarness with a file-only reply", () => {
test("keeps the quote when the reply has no text", () => {
expect(
botMessagesToHarness({
messages: [
{
id: 7,
author: "user",
text: "",
reply_to: { id: 4, author: "bot", text: "Send me the logo." },
attachments: [{ id: 9, name: "logo.png", media_type: "image", url: "/media/9" }],
},
],
}),
).toEqual([
{
role: "user",
content: `[Replying to your earlier message: "Send me the logo."]\n\n[Attached earlier: "logo.png", media id 9]`,
},
]);
});
});
describe("botMessagesToHarness with files", () => {
const attachments = [{ id: 9, name: "shot.png", media_type: "image" as const, url: "/media/9" }];
test("puts a message's loaded files after its text", () => {
const parts = new Map([[1, [{ type: "image" as const, url: "data:image/jpeg;base64,AA", name: "shot.png" }]]]);
expect(
botMessagesToHarness({
messages: [{ id: 1, author: "user", text: "What's this?", attachments }],
parts,
}),
).toEqual([
{
role: "user",
content: [
{ type: "text", text: "What's this?" },
{ type: "image", url: "data:image/jpeg;base64,AA", name: "shot.png" },
],
},
]);
});
test("names files it didn't load, and keeps a file-only message", () => {
expect(
botMessagesToHarness({
messages: [{ id: 2, author: "user", text: "", attachments }],
}),
).toEqual([
{
role: "user",
/** Its media id rides along, so a picture can be set from it. */
content: `[Attached earlier: "shot.png", media id 9]`,
},
]);
});
});
describe("botMessagesToHarness with mentions", () => {
test("names the bots and workspaces a user message mentions", () => {
expect(
botMessagesToHarness({
messages: [message("user", "ask [bot:3]"), message("bot", "[bot:3] is on it")],
refs: { bots: new Map([[3, { name: "Ada" }]]), workspaces: new Map() },
}),
).toEqual([
{ role: "user", content: "ask [bot:3]\n\n[Mentions: [bot:3] is Ada]" },
{ role: "assistant", content: "[bot:3] is on it" },
]);
});
});