Files
OmniRoute/tests/unit/kiro-long-tool-description-docs.test.ts
Dohyun Jung 5f3f25e541 fix(kiro): keep relocated tool documentation on multi-turn requests (#9036)
Validated in local merge-train (devbox-vm-06-dev002) @ combined-tip (FAST gates green: static + changed tests + vitest — only pre-existing audit.test.ts flake). Evidence: /home/diegosouzapw/dev/proxys/OmniRoute/.claude/worktrees/merge-train-20260805-213228-suite.log
2026-08-05 21:43:00 -03:00

199 lines
6.4 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import { buildKiroPayload } from "../../open-sse/translator/request/openai-to-kiro.ts";
// Kiro rejects a `toolSpecification.description` longer than ~10000 chars, so the
// translator relocates an oversized description into the current turn's content
// and leaves a pointer in the schema (mirroring kiro-gateway's
// `converters_core.py::process_tools_with_long_descriptions`).
//
// The relocation used to be carried on the message object as `_toolDocs`, which
// broke in two ways: the tool-bearing user turn is moved into `history` on every
// multi-turn request, so the docs were dropped and the model saw only the
// pointer; and the field leaked into the upstream payload, which Kiro rejects
// because it refuses unknown top-level keys.
const POINTER = "[Full documentation in system prompt under '## Tool: big_tool']";
const DOCS_HEADING = "# Tool Documentation";
function bigToolWithDescriptionLength(length: number) {
return [
{
type: "function",
function: {
name: "big_tool",
description: "D".repeat(length),
parameters: { type: "object", properties: {} },
},
},
];
}
const TURN_SHAPES = {
"single user turn": [{ role: "user", content: "a" }],
"multi-turn conversation": [
{ role: "user", content: "a" },
{ role: "assistant", content: "b" },
{ role: "user", content: "c" },
],
"deep multi-turn conversation": [
{ role: "user", content: "a" },
{ role: "assistant", content: "b" },
{ role: "user", content: "c" },
{ role: "assistant", content: "d" },
{ role: "user", content: "e" },
],
"assistant-first conversation": [
{ role: "assistant", content: "hello" },
{ role: "user", content: "hi" },
],
// No user turn at all: currentMessage is the synthesized filler turn, which
// reaches the tools schema through the fallback attachment path.
"no user messages": [{ role: "assistant", content: "only" }],
};
test("relocated tool documentation reaches the current turn for every turn shape", () => {
for (const [label, messages] of Object.entries(TURN_SHAPES)) {
const payload = buildKiroPayload(
"claude-sonnet-4.5",
{ messages, tools: bigToolWithDescriptionLength(12000) },
true,
{}
);
const current = payload.conversationState.currentMessage.userInputMessage;
assert.ok(
current.content.includes(DOCS_HEADING),
`${label}: full tool documentation must be prepended to the current turn`
);
assert.ok(
current.content.includes("D".repeat(12000)),
`${label}: the relocated description text itself must survive`
);
assert.equal(
current.userInputMessageContext?.tools[0].toolSpecification.description,
POINTER,
`${label}: the oversized description must be replaced by the pointer`
);
}
});
test("relocation never leaks an unknown field into the upstream payload", () => {
for (const [label, messages] of Object.entries(TURN_SHAPES)) {
const payload = buildKiroPayload(
"claude-sonnet-4.5",
{ messages, tools: bigToolWithDescriptionLength(12000) },
true,
{}
);
assert.ok(
!JSON.stringify(payload).includes("_toolDocs"),
`${label}: Kiro rejects unknown top-level keys, so _toolDocs must not be serialized`
);
}
});
test("a description within the limit is left alone and adds no documentation block", () => {
const payload = buildKiroPayload(
"claude-sonnet-4.5",
{
messages: TURN_SHAPES["multi-turn conversation"],
tools: [
{
type: "function",
function: {
name: "ok_tool",
description: "short desc",
parameters: { type: "object", properties: {} },
},
},
],
},
true,
{}
);
const current = payload.conversationState.currentMessage.userInputMessage;
assert.equal(
current.userInputMessageContext?.tools[0].toolSpecification.description,
"short desc",
"a description under the limit must reach Kiro verbatim"
);
assert.ok(
!current.content.includes(DOCS_HEADING),
"no documentation block should be injected when nothing was relocated"
);
});
// A description exactly at the limit must pass through: the guard triggers only
// above it, and an off-by-one here would relocate a description Kiro accepts.
test("the relocation boundary triggers above the limit, not at it", () => {
const messages = TURN_SHAPES["multi-turn conversation"];
const atLimit = buildKiroPayload(
"claude-sonnet-4.5",
{ messages, tools: bigToolWithDescriptionLength(10000) },
true,
{}
);
const atLimitCurrent = atLimit.conversationState.currentMessage.userInputMessage;
assert.equal(
atLimitCurrent.userInputMessageContext?.tools[0].toolSpecification.description.length,
10000,
"a description exactly at the limit must not be relocated"
);
assert.ok(
!atLimitCurrent.content.includes(DOCS_HEADING),
"no documentation block at the boundary"
);
const overLimit = buildKiroPayload(
"claude-sonnet-4.5",
{ messages, tools: bigToolWithDescriptionLength(10001) },
true,
{}
);
assert.equal(
overLimit.conversationState.currentMessage.userInputMessage.userInputMessageContext?.tools[0]
.toolSpecification.description,
POINTER,
"one char over the limit must be relocated"
);
});
// Only the oversized tool is relocated; a mixed inventory must keep the short
// descriptions inline so the model still sees them next to the schema.
test("only oversized descriptions are relocated in a mixed tool inventory", () => {
const payload = buildKiroPayload(
"claude-sonnet-4.5",
{
messages: TURN_SHAPES["multi-turn conversation"],
tools: [
{
type: "function",
function: {
name: "small_tool",
description: "compact",
parameters: { type: "object", properties: {} },
},
},
...bigToolWithDescriptionLength(12000),
],
},
true,
{}
);
const current = payload.conversationState.currentMessage.userInputMessage;
const specs = current.userInputMessageContext?.tools;
assert.equal(specs[0].toolSpecification.description, "compact");
assert.equal(specs[1].toolSpecification.description, POINTER);
assert.ok(current.content.includes("## Tool: big_tool"));
assert.ok(
!current.content.includes("## Tool: small_tool"),
"a tool that was never relocated must not get a documentation section"
);
});