Files
diegosouzapw--omniroute/tests/unit/gitlab-tool-exchange-bounded-6220.test.ts
2026-07-13 13:39:12 +08:00

96 lines
3.9 KiB
TypeScript

/**
* Regression test for #6220 (follow-up) — the tool-exchange feedback fix
* (gitlab-tool-result-feedback-6220) taught the GitLab Duo executor to serialize the
* FULL multi-turn conversation into the code_suggestions prompt so the model sees the
* tool result. But GitLab's AI-Gateway `code_suggestions` endpoint is a single-file
* `generation` API with a `small_file` validation guard: once the folded history grew
* large, `transformRequest` sent an oversized `content_above_cursor` AND duplicated the
* whole thing into `user_instruction`, and the gateway rejected turn-N with
* `422 {"detail":"Validation error"}` (tokens 0/0, pre-inference).
*
* Fix: BOUND the serialized tool-exchange prompt — keep system + latest user message +
* the most-recent tool round, cap oversized tool results, and stop duplicating the full
* prompt into `user_instruction` (it now carries only the short latest user message).
* The most-recent tool result must still be present so the model keeps its observation.
*
* The upstream 422→200 clearing is VPS-only (Hard Rule #18); this unit test covers the
* bounding logic (char/length caps + tool-result presence), which is the root cause.
*/
import test from "node:test";
import assert from "node:assert/strict";
import { GitlabExecutor, buildPrompt } from "../../open-sse/executors/gitlab.ts";
// A tool result large enough to blow the small_file generation contract if unbounded.
const HUGE_TOOL_RESULT =
"TOOLRESULT_START " + "x".repeat(60_000) + " TOOLRESULT_END";
function buildLongToolConversation() {
const messages: Array<Record<string, unknown>> = [
{ role: "system", content: "You are a helpful coding assistant." },
{ role: "user", content: "List the files and summarize the repo." },
];
// ≥10 messages: several assistant tool_calls + tool results folded back.
for (let i = 0; i < 5; i++) {
messages.push({
role: "assistant",
content: "",
tool_calls: [
{
id: `call_${i}`,
type: "function",
function: { name: "read_file", arguments: `{"path":"file_${i}.ts"}` },
},
],
});
messages.push({
role: "tool",
tool_call_id: `call_${i}`,
name: "read_file",
content: i === 4 ? HUGE_TOOL_RESULT : `contents of file_${i}`,
});
}
return messages;
}
test("buildPrompt: long tool-exchange history is bounded (char cap) [#6220]", () => {
const messages = buildLongToolConversation();
const prompt = buildPrompt(messages);
// Sane bound for the small_file generation contract.
assert.ok(
prompt.length < 30_000,
`bounded prompt should stay under 30k chars, got ${prompt.length}`
);
// The most-recent tool result must still be present (its head survives the cap).
assert.match(prompt, /TOOLRESULT_START/);
});
test("transformRequest: content_above_cursor and user_instruction are both bounded [#6220]", () => {
const executor = new GitlabExecutor("gitlab-duo");
const messages = buildLongToolConversation();
const out = executor.transformRequest(
"gitlab-duo/model",
{ messages },
false,
{} as never
) as Record<string, unknown>;
const currentFile = out.current_file as Record<string, unknown>;
const contentAbove = String(currentFile.content_above_cursor || "");
const userInstruction = String(out.user_instruction || "");
// content_above_cursor carries the (bounded) folded history + the tool observation.
assert.ok(
contentAbove.length < 30_000,
`content_above_cursor should be bounded, got ${contentAbove.length}`
);
assert.match(contentAbove, /TOOLRESULT_START/);
// user_instruction must NOT duplicate the whole huge prompt — the duplication was the
// likely offending 422 field. It carries only the short latest user message.
assert.ok(
userInstruction.length < 5_000,
`user_instruction should be short (not the full prompt), got ${userInstruction.length}`
);
assert.match(userInstruction, /summarize the repo/);
});