tool verbose

This commit is contained in:
wassname
2026-04-19 15:41:15 +08:00
parent e7e3e86550
commit 5aa37b7a99
7 changed files with 515 additions and 76 deletions
+61 -7
View File
@@ -111,6 +111,10 @@ test("Menu helpers build model menu state and parse callback actions", () => {
kind: "status",
action: "model",
});
assert.deepEqual(parseTelegramMenuCallbackAction("status:trace"), {
kind: "status",
action: "trace",
});
assert.deepEqual(parseTelegramMenuCallbackAction("thinking:set:high"), {
kind: "thinking:set",
level: "high",
@@ -443,6 +447,7 @@ test("Menu helpers execute model callback actions across update, switch, and res
test("Menu helpers handle status and thinking callback actions", async () => {
const events: string[] = [];
let traceVisible = true;
const reasoningModel = {
provider: "openai",
id: "gpt-5",
@@ -465,6 +470,41 @@ test("Menu helpers handle status and thinking callback actions", async () => {
updateThinkingMenuMessage: async () => {
events.push("status:thinking");
},
updateStatusMessage: async () => {
events.push("status:update");
},
setTraceVisible: (nextTraceVisible) => {
traceVisible = nextTraceVisible;
events.push(`trace:${nextTraceVisible ? "on" : "off"}`);
},
getTraceVisible: () => traceVisible,
answerCallbackQuery: async (_id, text) => {
events.push(`answer:${text ?? ""}`);
},
},
),
true,
);
assert.equal(
await handleTelegramStatusMenuCallbackAction(
"callback-trace",
"status:trace",
reasoningModel as never,
{
updateModelMenuMessage: async () => {
events.push("unexpected:model");
},
updateThinkingMenuMessage: async () => {
events.push("unexpected:thinking");
},
updateStatusMessage: async () => {
events.push("status:update");
},
setTraceVisible: (nextTraceVisible) => {
traceVisible = nextTraceVisible;
events.push(`trace:${nextTraceVisible ? "on" : "off"}`);
},
getTraceVisible: () => traceVisible,
answerCallbackQuery: async (_id, text) => {
events.push(`answer:${text ?? ""}`);
},
@@ -504,6 +544,13 @@ test("Menu helpers handle status and thinking callback actions", async () => {
updateThinkingMenuMessage: async () => {
events.push("unexpected:thinking");
},
updateStatusMessage: async () => {
events.push("unexpected:status");
},
setTraceVisible: () => {
events.push("unexpected:trace");
},
getTraceVisible: () => traceVisible,
answerCallbackQuery: async (_id, text) => {
events.push(`answer:${text ?? ""}`);
},
@@ -513,10 +560,13 @@ test("Menu helpers handle status and thinking callback actions", async () => {
);
assert.equal(events[0], "status:model");
assert.equal(events[1], "answer:");
assert.equal(events[2], "set:high");
assert.equal(events[2], "trace:off");
assert.equal(events[3], "status:update");
assert.equal(events[4], "answer:Thinking: high");
assert.equal(events[5], "answer:This model has no reasoning controls.");
assert.equal(events[4], "answer:Trace: off");
assert.equal(events[5], "set:high");
assert.equal(events[6], "status:update");
assert.equal(events[7], "answer:Thinking: high");
assert.equal(events[8], "answer:This model has no reasoning controls.");
});
test("Menu helpers build pure render payloads before transport", () => {
@@ -536,6 +586,7 @@ test("Menu helpers build pure render payloads before transport", () => {
"<b>Status</b>",
modelA as never,
"medium",
true,
);
assert.equal(modelPayload.nextMode, "model");
assert.equal(modelPayload.text, "<b>Choose a model:</b>");
@@ -586,6 +637,7 @@ test("Menu helpers update and send interactive menu messages", async () => {
"<b>Status</b>",
modelA as never,
"medium",
true,
deps,
);
const sentStatusId = await sendTelegramStatusMessage(
@@ -593,6 +645,7 @@ test("Menu helpers update and send interactive menu messages", async () => {
"<b>Status</b>",
modelA as never,
"medium",
true,
deps,
);
const sentModelId = await sendTelegramModelMenuMessage(state, modelA as never, deps);
@@ -638,8 +691,9 @@ test("Menu helpers build model, thinking, and status UI payloads", () => {
thinkingMarkup.inline_keyboard.some((row) => row[0]?.text === "✅ medium"),
true,
);
const statusMarkup = buildStatusReplyMarkup(modelA as never, "medium");
assert.equal(statusMarkup.inline_keyboard.length, 2);
const noReasoningMarkup = buildStatusReplyMarkup(modelB as never, "medium");
assert.equal(noReasoningMarkup.inline_keyboard.length, 1);
const statusMarkup = buildStatusReplyMarkup(modelA as never, "medium", true);
assert.equal(statusMarkup.inline_keyboard.length, 3);
assert.equal(statusMarkup.inline_keyboard[1]?.[0]?.callback_data, "status:trace");
const noReasoningMarkup = buildStatusReplyMarkup(modelB as never, "medium", false);
assert.equal(noReasoningMarkup.inline_keyboard.length, 2);
});
+83 -25
View File
@@ -6,10 +6,68 @@
import assert from "node:assert/strict";
import test from "node:test";
import { __telegramTestUtils } from "../index.ts";
import {
MAX_MESSAGE_LENGTH,
buildTelegramAssistantPreviewText,
buildTelegramAssistantTranscriptMarkdown,
renderTelegramMessage,
} from "../lib/rendering.ts";
test("Assistant trace helpers build compact previews and fuller transcripts", () => {
const blocks = [
{ type: "text", text: "Answer intro" },
{ type: "thinking", text: "Need to inspect the config first." },
{
type: "tool_call",
name: "read_config",
argsText: '{"path":"config.json"}',
},
{ type: "text", text: "\n\nFinal answer." },
] as const;
assert.equal(
buildTelegramAssistantPreviewText(blocks as never, true),
[
"Answer intro\n\nFinal answer.",
'[thinking] Need to inspect the config first.\n[tool] read_config {"path":"config.json"}',
].join("\n\n"),
);
assert.equal(
buildTelegramAssistantPreviewText(blocks as never, false),
"Answer intro\n\nFinal answer.",
);
assert.equal(
buildTelegramAssistantTranscriptMarkdown(blocks as never, true),
[
"Answer intro",
"**Thinking**\n> Need to inspect the config first.",
'**Tool call** `read_config` `{"path":"config.json"}`',
"Final answer.",
].join("\n\n"),
);
assert.equal(
buildTelegramAssistantTranscriptMarkdown(blocks as never, false),
"Answer intro\n\nFinal answer.",
);
});
test("Assistant trace transcript uses code fences for long tool arguments", () => {
const markdown = buildTelegramAssistantTranscriptMarkdown(
[
{ type: "text", text: "Answer" },
{
type: "tool_call",
name: "write_file",
argsText: '{\n "path": "out/report.md",\n "content": "long body"\n}',
},
],
true,
);
assert.match(markdown, /\*\*Tool call\*\* `write_file`/);
assert.match(markdown, /```json/);
});
test("Nested lists stay out of code blocks", () => {
const chunks = __telegramTestUtils.renderTelegramMessage(
const chunks = renderTelegramMessage(
"- Level 1\n - Level 2\n - Level 3 with **bold** text",
{ mode: "markdown" },
);
@@ -27,7 +85,7 @@ test("Nested lists stay out of code blocks", () => {
});
test("Fenced code blocks preserve literal markdown", () => {
const chunks = __telegramTestUtils.renderTelegramMessage(
const chunks = renderTelegramMessage(
'~~~ts\nconst value = "**raw**";\n~~~',
{ mode: "markdown" },
);
@@ -37,7 +95,7 @@ test("Fenced code blocks preserve literal markdown", () => {
});
test("Underscores inside words do not become italic", () => {
const chunks = __telegramTestUtils.renderTelegramMessage(
const chunks = renderTelegramMessage(
"Path: foo_bar_baz.txt and **bold**",
{ mode: "markdown" },
);
@@ -47,7 +105,7 @@ test("Underscores inside words do not become italic", () => {
});
test("Quoted nested lists stay in blockquote rendering", () => {
const chunks = __telegramTestUtils.renderTelegramMessage(
const chunks = renderTelegramMessage(
"> Quoted intro\n> - nested item\n> - deeper item",
{ mode: "markdown" },
);
@@ -59,7 +117,7 @@ test("Quoted nested lists stay in blockquote rendering", () => {
});
test("Numbered lists use monospace numeric markers", () => {
const chunks = __telegramTestUtils.renderTelegramMessage(
const chunks = renderTelegramMessage(
"1. first\n 2. second",
{ mode: "markdown" },
);
@@ -69,7 +127,7 @@ test("Numbered lists use monospace numeric markers", () => {
});
test("Nested blockquotes flatten into one Telegram blockquote with indentation", () => {
const chunks = __telegramTestUtils.renderTelegramMessage(
const chunks = renderTelegramMessage(
"> outer\n>> inner\n>>> deepest",
{ mode: "markdown" },
);
@@ -82,7 +140,7 @@ test("Nested blockquotes flatten into one Telegram blockquote with indentation",
});
test("Markdown tables render as literal monospace blocks without outer side borders", () => {
const chunks = __telegramTestUtils.renderTelegramMessage(
const chunks = renderTelegramMessage(
"| Name | Value |\n| --- | --- |\n| **x** | `y` |",
{ mode: "markdown" },
);
@@ -96,7 +154,7 @@ test("Markdown tables render as literal monospace blocks without outer side bord
});
test("Links, code spans, and underscore-heavy text coexist safely", () => {
const chunks = __telegramTestUtils.renderTelegramMessage(
const chunks = renderTelegramMessage(
"See [docs](https://example.com), run `foo_bar()` and keep foo_bar.txt literal",
{ mode: "markdown" },
);
@@ -114,12 +172,12 @@ test("Long quoted blocks stay chunked with balanced blockquote tags", () => {
{ length: 500 },
(_, index) => `> quoted **${index}** line`,
).join("\n");
const chunks = __telegramTestUtils.renderTelegramMessage(markdown, {
const chunks = renderTelegramMessage(markdown, {
mode: "markdown",
});
assert.ok(chunks.length > 1);
for (const chunk of chunks) {
assert.ok(chunk.text.length <= __telegramTestUtils.MAX_MESSAGE_LENGTH);
assert.ok(chunk.text.length <= MAX_MESSAGE_LENGTH);
assert.equal(
(chunk.text.match(/<blockquote>/g) ?? []).length,
(chunk.text.match(/<\/blockquote>/g) ?? []).length,
@@ -132,12 +190,12 @@ test("Long markdown replies stay chunked below Telegram limits", () => {
{ length: 600 },
(_, index) => `- item **${index}**`,
).join("\n");
const chunks = __telegramTestUtils.renderTelegramMessage(markdown, {
const chunks = renderTelegramMessage(markdown, {
mode: "markdown",
});
assert.ok(chunks.length > 1);
for (const chunk of chunks) {
assert.ok(chunk.text.length <= __telegramTestUtils.MAX_MESSAGE_LENGTH);
assert.ok(chunk.text.length <= MAX_MESSAGE_LENGTH);
assert.equal(
(chunk.text.match(/<b>/g) ?? []).length,
(chunk.text.match(/<\/b>/g) ?? []).length,
@@ -151,12 +209,12 @@ test("Long mixed links and code spans stay chunked with balanced inline tags", (
(_, index) =>
`Paragraph ${index}: see [docs ${index}](https://example.com/${index}), run \`code_${index}()\`, and keep foo_bar_${index}.txt literal`,
).join("\n\n");
const chunks = __telegramTestUtils.renderTelegramMessage(markdown, {
const chunks = renderTelegramMessage(markdown, {
mode: "markdown",
});
assert.ok(chunks.length > 1);
for (const chunk of chunks) {
assert.ok(chunk.text.length <= __telegramTestUtils.MAX_MESSAGE_LENGTH);
assert.ok(chunk.text.length <= MAX_MESSAGE_LENGTH);
assert.equal(
(chunk.text.match(/<a /g) ?? []).length,
(chunk.text.match(/<\/a>/g) ?? []).length,
@@ -180,12 +238,12 @@ test("Long multi-block markdown keeps quotes and code fences structurally balanc
"```",
].join("\n");
}).join("\n\n");
const chunks = __telegramTestUtils.renderTelegramMessage(markdown, {
const chunks = renderTelegramMessage(markdown, {
mode: "markdown",
});
assert.ok(chunks.length > 1);
for (const chunk of chunks) {
assert.ok(chunk.text.length <= __telegramTestUtils.MAX_MESSAGE_LENGTH);
assert.ok(chunk.text.length <= MAX_MESSAGE_LENGTH);
assert.equal(
(chunk.text.match(/<blockquote>/g) ?? []).length,
(chunk.text.match(/<\/blockquote>/g) ?? []).length,
@@ -206,12 +264,12 @@ test("Chunked mixed block transitions keep quote and list structure balanced", (
`plain paragraph ${index} with [link](https://example.com/${index})`,
].join("\n");
}).join("\n\n");
const chunks = __telegramTestUtils.renderTelegramMessage(markdown, {
const chunks = renderTelegramMessage(markdown, {
mode: "markdown",
});
assert.ok(chunks.length > 1);
for (const chunk of chunks) {
assert.ok(chunk.text.length <= __telegramTestUtils.MAX_MESSAGE_LENGTH);
assert.ok(chunk.text.length <= MAX_MESSAGE_LENGTH);
assert.equal(
(chunk.text.match(/<blockquote>/g) ?? []).length,
(chunk.text.match(/<\/blockquote>/g) ?? []).length,
@@ -232,12 +290,12 @@ test("Chunked code fence transitions keep code blocks closed before following pr
`After code **${index}** and \`inline_${index}()\``,
].join("\n");
}).join("\n\n");
const chunks = __telegramTestUtils.renderTelegramMessage(markdown, {
const chunks = renderTelegramMessage(markdown, {
mode: "markdown",
});
assert.ok(chunks.length > 1);
for (const chunk of chunks) {
assert.ok(chunk.text.length <= __telegramTestUtils.MAX_MESSAGE_LENGTH);
assert.ok(chunk.text.length <= MAX_MESSAGE_LENGTH);
assert.equal(
(chunk.text.match(/<pre><code/g) ?? []).length,
(chunk.text.match(/<\/code><\/pre>/g) ?? []).length,
@@ -253,12 +311,12 @@ test("Long inline formatting paragraphs stay balanced across chunk boundaries",
const markdown = Array.from({ length: 500 }, (_, index) => {
return `Segment ${index} keeps **bold_${index}** with \`code_${index}()\`, [link_${index}](https://example.com/${index}), and foo_bar_${index}.txt literal.`;
}).join(" ");
const chunks = __telegramTestUtils.renderTelegramMessage(markdown, {
const chunks = renderTelegramMessage(markdown, {
mode: "markdown",
});
assert.ok(chunks.length > 1);
for (const chunk of chunks) {
assert.ok(chunk.text.length <= __telegramTestUtils.MAX_MESSAGE_LENGTH);
assert.ok(chunk.text.length <= MAX_MESSAGE_LENGTH);
assert.equal(
(chunk.text.match(/<b>/g) ?? []).length,
(chunk.text.match(/<\/b>/g) ?? []).length,
@@ -286,12 +344,12 @@ test("Chunked list, code, quote, and prose cycles stay balanced across transitio
`Plain paragraph ${index} with \`inline_${index}()\``,
].join("\n");
}).join("\n\n");
const chunks = __telegramTestUtils.renderTelegramMessage(markdown, {
const chunks = renderTelegramMessage(markdown, {
mode: "markdown",
});
assert.ok(chunks.length > 1);
for (const chunk of chunks) {
assert.ok(chunk.text.length <= __telegramTestUtils.MAX_MESSAGE_LENGTH);
assert.ok(chunk.text.length <= MAX_MESSAGE_LENGTH);
assert.equal(
(chunk.text.match(/<pre><code/g) ?? []).length,
(chunk.text.match(/<\/code><\/pre>/g) ?? []).length,