项目文件夹

文件
2026-07-13 13:39:12 +08:00

217 行
8.7 KiB
TypeScript

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
/**
* Tests for composerToolCalls.ts — DeepSeek inline tool-call parser.
* Ported from decolua/9router#1335 (noestelar), adapted to OmniRoute
* node:test conventions.
*/
import test from "node:test";
import assert from "node:assert/strict";
import {
hasComposerToolCalls,
parseComposerToolCalls,
createStreamingState,
feedStreamingChunk,
} from "../../open-sse/utils/composerToolCalls.ts";
// ─── hasComposerToolCalls ─────────────────────────────────────────────────────
test("hasComposerToolCalls: returns false for plain text", () => {
assert.equal(hasComposerToolCalls("Hello world"), false);
});
test("hasComposerToolCalls: returns false for empty string", () => {
assert.equal(hasComposerToolCalls(""), false);
});
test("hasComposerToolCalls: detects full-width pipe markers", () => {
const text =
"<tool▁calls▁begin><tool▁call▁begin>\ntool_name\n<tool▁sep>arg\nval\n<tool▁call▁end><tool▁calls▁end>";
assert.equal(hasComposerToolCalls(text), true);
});
test("hasComposerToolCalls: detects ASCII fallback markers", () => {
const text =
"<|tool_calls_begin|><|tool_call_begin|>\ntool_name\n<|tool_sep|>arg\nval\n<|tool_call_end|><|tool_calls_end|>";
assert.equal(hasComposerToolCalls(text), true);
});
// ─── parseComposerToolCalls ───────────────────────────────────────────────────
test("parseComposerToolCalls: returns unchanged text when no markers present", () => {
const result = parseComposerToolCalls("Hello world");
assert.equal(result.content, "Hello world");
assert.deepEqual(result.toolCalls, []);
});
test("parseComposerToolCalls: parses a single tool call with two args", () => {
const text =
"Searching now.\n<tool▁calls▁begin><tool▁call▁begin>\nsearch_files\n" +
"<tool▁sep>pattern\n*cron*.py\n" +
"<tool▁sep>path\n/home/user/.hermes\n" +
"<tool▁call▁end><tool▁calls▁end>";
const result = parseComposerToolCalls(text);
assert.equal(result.content, "Searching now.");
assert.equal(result.toolCalls.length, 1);
const tc = result.toolCalls[0];
assert.equal(tc.type, "function");
assert.equal(tc.function.name, "search_files");
const args = JSON.parse(tc.function.arguments);
assert.equal(args.pattern, "*cron*.py");
assert.equal(args.path, "/home/user/.hermes");
// ID must follow call_<...> pattern
assert.match(tc.id, /^call_/);
});
test("parseComposerToolCalls: strips markers and returns residual preamble", () => {
const text =
"Preamble.\n<tool▁calls▁begin><tool▁call▁begin>\nwrite_file\n" +
"<tool▁sep>path\n/tmp/x.txt\n<tool▁sep>content\nhello\n" +
"<tool▁call▁end><tool▁calls▁end>\nTrailing.";
const result = parseComposerToolCalls(text);
// Both preamble and trailing should be in content
assert.ok(result.content.includes("Preamble.") || result.content.includes("Trailing."));
assert.equal(result.toolCalls.length, 1);
assert.equal(result.toolCalls[0].function.name, "write_file");
// No marker should remain in content
assert.ok(!result.content.includes("tool▁calls▁begin"));
assert.ok(!result.content.includes(""));
});
test("parseComposerToolCalls: parses multiple tool calls", () => {
const text =
"<tool▁calls▁begin>" +
"<tool▁call▁begin>\ntool_a\n<tool▁sep>arg\nval_a\n<tool▁call▁end>" +
"<tool▁call▁begin>\ntool_b\n<tool▁sep>arg\nval_b\n<tool▁call▁end>" +
"<tool▁calls▁end>";
const result = parseComposerToolCalls(text);
assert.equal(result.toolCalls.length, 2);
assert.equal(result.toolCalls[0].function.name, "tool_a");
assert.equal(result.toolCalls[1].function.name, "tool_b");
});
test("parseComposerToolCalls: coerces JSON object arg value", () => {
const text =
"<tool▁calls▁begin><tool▁call▁begin>\njson_tool\n" +
'<tool▁sep>data\n{"key":"value"}\n' +
"<tool▁call▁end><tool▁calls▁end>";
const result = parseComposerToolCalls(text);
const args = JSON.parse(result.toolCalls[0].function.arguments);
assert.deepEqual(args.data, { key: "value" });
});
test("parseComposerToolCalls: coerces integer arg value", () => {
const text =
"<tool▁calls▁begin><tool▁call▁begin>\nset_timeout\n" +
"<tool▁sep>ms\n3000\n" +
"<tool▁call▁end><tool▁calls▁end>";
const result = parseComposerToolCalls(text);
const args = JSON.parse(result.toolCalls[0].function.arguments);
assert.equal(args.ms, 3000);
});
test("parseComposerToolCalls: coerces boolean arg value", () => {
const text =
"<tool▁calls▁begin><tool▁call▁begin>\nset_flag\n" +
"<tool▁sep>enabled\ntrue\n" +
"<tool▁call▁end><tool▁calls▁end>";
const result = parseComposerToolCalls(text);
const args = JSON.parse(result.toolCalls[0].function.arguments);
assert.equal(args.enabled, true);
});
test("parseComposerToolCalls: returns empty toolCalls for null/undefined input", () => {
// @ts-expect-error testing runtime safety
const result = parseComposerToolCalls(null);
assert.equal(result.content, "");
assert.deepEqual(result.toolCalls, []);
});
test("parseComposerToolCalls: accepts ASCII fallback markers", () => {
const text =
"<|tool_calls_begin|><|tool_call_begin|>\nmy_tool\n" +
"<|tool_sep|>arg\nvalue\n" +
"<|tool_call_end|><|tool_calls_end|>";
const result = parseComposerToolCalls(text);
assert.equal(result.toolCalls.length, 1);
assert.equal(result.toolCalls[0].function.name, "my_tool");
});
// ─── Streaming parser ─────────────────────────────────────────────────────────
test("feedStreamingChunk: emits safe text before the marker block", () => {
const state = createStreamingState();
const out = feedStreamingChunk(state, "Safe text before.");
assert.equal(out.safeDelta, "Safe text before.");
assert.equal(out.ready, false);
assert.equal(out.holdback, false);
});
test("feedStreamingChunk: holds back partial opening marker at tail", () => {
const state = createStreamingState();
const out = feedStreamingChunk(state, "Working on it.<tool▁call");
assert.equal(out.safeDelta, "Working on it.");
assert.equal(out.holdback, true);
assert.equal(out.ready, false);
});
test("feedStreamingChunk: suppresses text once opening marker is seen", () => {
const state = createStreamingState();
// First: safe text only
feedStreamingChunk(state, "Preamble.");
// Second: opening marker arrives mid-accumulation
const out = feedStreamingChunk(state, "Preamble.<tool▁calls▁begin>");
assert.equal(out.safeDelta, "");
assert.equal(out.holdback, true);
});
test("feedStreamingChunk: flushes tool calls once the closing marker arrives", () => {
const state = createStreamingState();
const acc =
"ok\n<tool▁calls▁begin><tool▁call▁begin>\nwrite_file\n" +
"<tool▁sep>path\n/tmp/x\n<tool▁sep>content\nhi\n" +
"<tool▁call▁end><tool▁calls▁end>";
// Simulate it arriving in two halves
feedStreamingChunk(state, acc.slice(0, 30));
const out = feedStreamingChunk(state, acc);
assert.equal(out.ready, true);
assert.equal(out.toolCalls.length, 1);
assert.equal(out.toolCalls[0].function.name, "write_file");
const args = JSON.parse(out.toolCalls[0].function.arguments);
assert.deepEqual(args, { path: "/tmp/x", content: "hi" });
});
test("feedStreamingChunk: does not leak partial opening marker split across frames", () => {
const state = createStreamingState();
const a = feedStreamingChunk(state, "Working on it.<tool▁call");
assert.equal(a.safeDelta, "Working on it.");
assert.equal(a.holdback, true);
const b = feedStreamingChunk(state, "Working on it.<tool▁calls▁begin>");
assert.equal(b.safeDelta, "");
assert.equal(b.holdback, true);
});
test("feedStreamingChunk: emits no tool calls when block closes empty", () => {
const state = createStreamingState();
const out = feedStreamingChunk(state, "<tool▁calls▁begin><tool▁calls▁end>");
assert.equal(out.ready, true);
assert.deepEqual(out.toolCalls, []);
});
test("feedStreamingChunk: noop after done state", () => {
const state = createStreamingState();
state.done = true;
const out = feedStreamingChunk(state, "some text");
assert.equal(out.safeDelta, "");
assert.equal(out.ready, false);
});