import test from "node:test";
import assert from "node:assert/strict";
import { runAgent } from "../src/agent.js";
import { renderMiniCPMPrompt, normalizedCall } from "../src/prompt.js";
import { DEFAULTS } from "../src/core.js";
const call = (args) => ({
index: 0,
id: "test-call",
function: { name: "create_html", arguments: args },
});
const chunk = (delta, finish) => ({
choices: [{ delta, finish_reason: finish }],
});
test("an unterminated tool call is reported, repaired with a larger budget, executed and followed up", async () => {
const history = [{ role: "user", content: "Create a black hole in HTML" }];
let requests = 0,
created = 0;
async function chat(messages, settings, tools, signal, emit) {
requests++;
if (requests === 1) {
await emit(
chunk(
{ tool_calls: [call('{"title":"Black hole","html":"')] },
"length",
),
);
return;
}
assert.doesNotThrow(() => renderMiniCPMPrompt(messages, { tools: [] }));
for (const m of messages)
for (const c of m.tool_calls || [])
assert.doesNotThrow(() => JSON.parse(c.function.arguments));
if (requests === 2) {
assert.ok(settings.maxTokens >= 8192);
assert.equal(settings.thinking, false);
assert.match(messages.at(-1).content, /cut off/);
await emit(
chunk(
{
tool_calls: [
call(
JSON.stringify({
title: "Black hole",
html: "
Black hole
",
}),
),
],
},
"tool_calls",
),
);
return;
}
assert.equal(JSON.parse(messages.at(-1).content).success, true);
await emit(chunk({ content: "Your black hole is ready." }, "stop"));
}
const events = [];
await runAgent({
chat,
history,
settings: { ...DEFAULTS, maxTokens: 1536, thinking: true },
signal: new AbortController().signal,
createArtifact: async (name, args) => {
created++;
assert.ok(args.html.includes(""));
return { id: "file", filename: "Black-hole.html" };
},
onEvent: (event) => events.push(event),
});
assert.equal(requests, 3);
assert.equal(created, 1);
assert.equal(events.find((e) => e.type === "tool-success").recovered, true);
assert.equal(events.at(-1).type, "done");
});
test("tool validation errors are fed back and do not terminate the turn", async () => {
let requests = 0,
attempts = 0;
const history = [{ role: "user", content: "Make a file" }];
async function chat(messages, settings, tools, signal, emit) {
requests++;
if (requests < 3) {
if (requests === 2) assert.match(messages.at(-1).content, /closing tag/);
await emit(
chunk(
{
tool_calls: [
call(
JSON.stringify({
title: "Page",
html:
requests === 1
? ''
: 'Fixed',
}),
),
],
},
"stop",
),
);
} else await emit(chunk({ content: "Done" }, "stop"));
}
await runAgent({
chat,
history,
settings: DEFAULTS,
signal: new AbortController().signal,
createArtifact: async (_, args) => {
attempts++;
if (args.html.startsWith("")) throw Error("Invalid closing tag");
return { id: "file", filename: "Page.html" };
},
});
assert.equal(requests, 3);
assert.equal(attempts, 2);
});
test("tool history serializes structured values once", () => {
const c = {
id: "x",
function: {
name: "create_spreadsheet",
arguments: JSON.stringify({ sheets: [{ rows: [[1, true, null]] }] }),
},
};
const prompt = renderMiniCPMPrompt([
{ role: "user", content: "Make a sheet" },
{ role: "assistant", content: "", tool_calls: [c] },
{ role: "tool", content: "success" },
]);
assert.equal(prompt.match(/\nsuccess"));
});
test("malformed older history remains representable", () => {
const c = normalizedCall({
function: { name: "create_html", arguments: '{"html":"unfinished' },
});
assert.equal(
JSON.parse(c.function.arguments).invalid_arguments,
'{"html":"unfinished',
);
});
test("consumer errors propagate and cancellation stops callback streaming", async () => {
const controller = new AbortController();
let delivered = 0;
const chat = async (m, s, t, signal, emit) => {
for (let i = 0; i < 10000; i++) {
signal.throwIfAborted();
await emit(chunk({ content: "x" }, null));
}
};
await assert.rejects(
runAgent({
chat,
history: [],
settings: DEFAULTS,
signal: controller.signal,
createArtifact: () => {},
onEvent: (e) => {
if (e.type === "chunk" && ++delivered === 500) controller.abort();
},
}),
{ name: "AbortError" },
);
assert.equal(delivered, 500);
await assert.rejects(
runAgent({
chat,
history: [],
settings: DEFAULTS,
signal: new AbortController().signal,
createArtifact: () => {},
onEvent: (e) => {
if (e.type === "chunk") throw Error("consumer failed");
},
}),
/consumer failed/,
);
});