Add buildReplyHeader helper with truncation and media descriptors

This commit is contained in:
EugeneTes
2026-09-22 08:42:59 +00:00
parent 0febacfee7
commit 38ca7cbfea
2 changed files with 150 additions and 1 deletions

View File

@@ -3,7 +3,7 @@ import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import type { Transcriber } from "../stt/openai.js";
import { type InboundDeps, normalizeMessage, saveToInbox } from "./inbound.js";
import { buildReplyHeader, type InboundDeps, normalizeMessage, saveToInbox } from "./inbound.js";
describe("normalizeMessage", () => {
let dir: string;
@@ -177,3 +177,112 @@ describe("saveToInbox", () => {
}
});
});
describe("buildReplyHeader", () => {
const BOT_ID = 42;
it("returns undefined when there is no reply and no quote", () => {
expect(buildReplyHeader(undefined, undefined, BOT_ID)).toBeUndefined();
});
it("labels a plain reply to a bot message with 'your message'", () => {
const header = buildReplyHeader({ from: { id: BOT_ID, is_bot: true }, text: "Which file?" }, undefined, BOT_ID);
expect(header).toBe('[Reply to your message: "Which file?"]');
});
it("labels a plain reply to the owner's own earlier message", () => {
const header = buildReplyHeader({ from: { id: 99, is_bot: false }, text: "Buy milk" }, undefined, BOT_ID);
expect(header).toBe('[Reply to your own earlier message: "Buy milk"]');
});
it("falls back to 'the earlier message' when `from` is absent", () => {
const header = buildReplyHeader({ text: "hello" }, undefined, BOT_ID);
expect(header).toBe('[Reply to the earlier message: "hello"]');
});
it("uses the quote text and 'quoting your message' wording for a reply-with-quote to the bot", () => {
const header = buildReplyHeader(
{ from: { id: BOT_ID, is_bot: true }, text: "The full assistant answer with lots of context." },
{ text: "assistant answer" },
BOT_ID,
);
expect(header).toBe('[Reply quoting your message: "assistant answer"]');
});
it("uses 'quoting your own earlier message' when the source is the owner's own", () => {
const header = buildReplyHeader(
{ from: { id: 99, is_bot: false }, text: "shopping list" },
{ text: "shopping" },
BOT_ID,
);
expect(header).toBe('[Reply quoting your own earlier message: "shopping"]');
});
it("treats an empty quote.text as if there were no quote (falls through to plain reply)", () => {
const header = buildReplyHeader(
{ from: { id: BOT_ID, is_bot: true }, text: "The answer" },
{ text: "" },
BOT_ID,
);
expect(header).toBe('[Reply to your message: "The answer"]');
});
it("describes a photo-only replied-to message", () => {
const header = buildReplyHeader({ from: { id: BOT_ID, is_bot: true }, photo: [{}] }, undefined, BOT_ID);
expect(header).toBe('[Reply to your message: "(photo)"]');
});
it("describes a voice-only replied-to message", () => {
const header = buildReplyHeader({ from: { id: BOT_ID, is_bot: true }, voice: {} }, undefined, BOT_ID);
expect(header).toBe('[Reply to your message: "(voice message)"]');
});
it("describes a document with a filename", () => {
const header = buildReplyHeader(
{ from: { id: BOT_ID, is_bot: true }, document: { file_name: "notes.txt" } },
undefined,
BOT_ID,
);
expect(header).toBe('[Reply to your message: "(document: notes.txt)"]');
});
it("describes an unnamed document", () => {
const header = buildReplyHeader({ from: { id: BOT_ID, is_bot: true }, document: {} }, undefined, BOT_ID);
expect(header).toBe('[Reply to your message: "(document)"]');
});
it("uses the caption instead of the media descriptor when both are present", () => {
const header = buildReplyHeader(
{ from: { id: BOT_ID, is_bot: true }, photo: [{}], caption: "the sunset" },
undefined,
BOT_ID,
);
expect(header).toBe('[Reply to your message: "the sunset"]');
});
it("truncates replied-to text longer than 500 characters with an ellipsis", () => {
const long = "a".repeat(600);
const header = buildReplyHeader({ from: { id: BOT_ID, is_bot: true }, text: long }, undefined, BOT_ID);
expect(header).toBe(`[Reply to your message: "${"a".repeat(499)}…"]`);
});
it("truncates a very long quote in the same way", () => {
const long = "b".repeat(600);
const header = buildReplyHeader(
{ from: { id: BOT_ID, is_bot: true }, text: "irrelevant" },
{ text: long },
BOT_ID,
);
expect(header).toBe(`[Reply quoting your message: "${"b".repeat(499)}…"]`);
});
it("labels as 'your message' when botUserId is missing but is_bot is true", () => {
const header = buildReplyHeader({ from: { id: 0, is_bot: true }, text: "hi" }, undefined, undefined);
expect(header).toBe('[Reply to your message: "hi"]');
});
it("falls back to '(message)' when the replied-to message has no usable fields", () => {
const header = buildReplyHeader({ from: { id: BOT_ID, is_bot: true } }, undefined, BOT_ID);
expect(header).toBe('[Reply to your message: "(message)"]');
});
});

View File

@@ -164,3 +164,43 @@ export async function saveToInbox(workspaceDir: string, originalName: string, by
function errorText(err: unknown): string {
return err instanceof Error ? err.message : String(err);
}
/** Builds the `[Reply ...]` header line for the agent, or returns undefined when there is nothing to reply to. */
export function buildReplyHeader(
reply: RepliedMessage | undefined,
quote: Quote | undefined,
botUserId: number | undefined,
): string | undefined {
if (!reply) return undefined;
const isFromBot =
reply.from !== undefined &&
(botUserId !== undefined ? reply.from.id === botUserId : reply.from.is_bot === true);
const isFromOwner = reply.from !== undefined && !isFromBot;
const who = isFromBot
? "your message"
: isFromOwner
? "your own earlier message"
: "the earlier message";
const quoted = quote?.text ?? "";
if (quoted.length > 0) {
return `[Reply quoting ${who}: "${truncateReplyContext(quoted)}"]`;
}
const content = truncateReplyContext(replyContent(reply));
return `[Reply to ${who}: "${content}"]`;
}
function replyContent(reply: RepliedMessage): string {
if (reply.text && reply.text.length > 0) return reply.text;
if (reply.caption && reply.caption.length > 0) return reply.caption;
if (reply.photo && reply.photo.length > 0) return "(photo)";
if (reply.voice) return "(voice message)";
if (reply.audio) return reply.audio.file_name ? `(audio: ${reply.audio.file_name})` : "(audio)";
if (reply.document) return reply.document.file_name ? `(document: ${reply.document.file_name})` : "(document)";
return "(message)";
}
function truncateReplyContext(text: string): string {
const chars = Array.from(text);
if (chars.length <= MAX_REPLY_CONTEXT_CHARS) return text;
return `${chars.slice(0, MAX_REPLY_CONTEXT_CHARS - 1).join("")}…`;
}