Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions app/src/lib/copilot/repair-history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ function isToolResult(message: Message): message is Message & ToolResult {
*/
export function repairUnansweredToolCalls(
messages: ReadonlyArray<Message>,
newId: () => string = () => newId(),
/*
* Named so it cannot shadow the import it defaults to. A parameter called `newId` defaulting to
* `newId()` resolves to itself and recurses until the stack goes, and it does so only on the
* repair branch, which every test avoided by passing its own. Biome said so, as an unused import.
*/
mintId: () => string = newId,
): ReadonlyArray<Message> {
const answered = new Set<string>();
for (const message of messages) {
Expand All @@ -47,7 +52,7 @@ export function repairUnansweredToolCalls(
// OpenAI requires the results to follow their calls, and some providers require the order to
// match the `tool_calls` array as well.
repaired.push({
id: newId(),
id: mintId(),
role: "tool",
toolCallId: call.id,
content: UNANSWERED,
Expand Down
36 changes: 36 additions & 0 deletions app/tests/repair-history.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,39 @@ describe("repairing a history before it is sent", () => {
expect(repairUnansweredToolCalls(messages, ids)).toBe(messages);
});
});

/**
* The default id source, which every test above replaces with its own.
*
* That is the reason a self-recursive default survived review: the parameter exists so a test can
* make ids predictable, so no test ever ran the default, and the default only runs on the repair
* branch. This one calls it the way the only real caller does, with the argument omitted.
*/
describe("repairUnansweredToolCalls without an id source", () => {
test("repairs using its own ids rather than recursing", () => {
const messages: Message[] = [
{ id: "a", role: "user", content: "go" },
{
id: "b",
role: "assistant",
content: "",
toolCalls: [
{
id: "call-1",
type: "function",
function: { name: "act", arguments: "{}" },
},
],
},
];

const repaired = repairUnansweredToolCalls(messages);

expect(repaired).toHaveLength(3);
const result = repaired[2] as Message & { toolCallId: string };
expect(result.role).toBe("tool");
expect(result.toolCallId).toBe("call-1");
// A real id, not an empty string and not the call's own id.
expect(result.id).toMatch(/^[0-9a-f-]{36}$/);
});
});