Files
diegosouzapw--omniroute/tests/unit/chatgpt-web-tools-5240.test.ts
2026-07-13 13:39:12 +08:00

269 lines
9.1 KiB
TypeScript

// Tool-call emulation for the ChatGPT Web executor (#5240).
//
// chatgpt-web was omitted from the #3259 prompt-emulation rollout: body.tools
// was never read and both response builders hardcoded finish_reason:"stop".
// These tests live in a dedicated file (chatgpt-web.test.ts is a frozen
// god-file at the file-size cap and cannot grow).
import test from "node:test";
import assert from "node:assert/strict";
const { ChatGptWebExecutor, __resetChatGptWebCachesForTesting } = await import(
"../../open-sse/executors/chatgpt-web.ts"
);
const { __setTlsFetchOverrideForTesting } = await import(
"../../open-sse/services/chatgptTlsClient.ts"
);
// ─── Minimal TLS-fetch mock ──────────────────────────────────────────────────
// Tailored to the tool-call flow (gpt-5.3-instant, non-thinking): root/DPL,
// session→accessToken, sentinel→token (no PoW), conv→SSE. Warmup GETs fall
// through to 404, which the executor tolerates.
function makeHeaders(map: Record<string, string> = {}) {
const h = new Headers();
for (const [k, v] of Object.entries(map)) h.set(k, String(v));
return h;
}
function sseText(events: unknown[]): string {
return events.map((e) => `data: ${JSON.stringify(e)}\r\n\r\n`).join("") + "data: [DONE]\r\n\r\n";
}
/** A single finished assistant turn whose text is `parts`. */
function convWithAssistantText(parts: string) {
return [
{
conversation_id: "tc-1",
message: {
id: "tm-1",
author: { role: "assistant" },
content: { content_type: "text", parts: [parts] },
status: "in_progress",
},
},
{
conversation_id: "tc-1",
message: {
id: "tm-1",
author: { role: "assistant" },
content: { content_type: "text", parts: [parts] },
status: "finished_successfully",
},
},
];
}
function installMockFetch(convEvents: unknown[]) {
const calls = { urls: [] as string[], bodies: [] as unknown[] };
__setTlsFetchOverrideForTesting(async (url: string, opts: any = {}) => {
const u = String(url);
calls.urls.push(u);
calls.bodies.push(opts.body);
const json = (body: unknown, status = 200) => ({
status,
headers: makeHeaders({ "Content-Type": "application/json" }),
text: JSON.stringify(body),
body: null,
});
if ((u === "https://chatgpt.com/" || u === "https://chatgpt.com") && (opts.method || "GET") === "GET") {
return {
status: 200,
headers: makeHeaders({ "Content-Type": "text/html" }),
text: '<html data-build="prod-test123"><script src="https://cdn.oaistatic.com/_next/static/chunks/main-test.js"></script></html>',
body: null,
};
}
if (u.includes("/api/auth/session")) {
return json({
accessToken: "jwt-abc",
expires: new Date(Date.now() + 3600_000).toISOString(),
user: { id: "user-1" },
});
}
if (u.includes("/sentinel/chat-requirements")) {
return json({ token: "req-token", proofofwork: { required: false } });
}
if (
u.endsWith("/backend-api/f/conversation") ||
u.endsWith("/backend-api/conversation") ||
/\/backend-api\/(f\/)?conversation\?/.test(u)
) {
return {
status: 200,
headers: makeHeaders({ "Content-Type": "text/event-stream" }),
text: sseText(convEvents),
body: null,
};
}
// Warmup (/me, /conversations, /models) — tolerated.
return { status: 404, headers: makeHeaders(), text: "not mocked", body: null };
});
return {
calls,
restore() {
__setTlsFetchOverrideForTesting(null);
},
};
}
const WEATHER_TOOL = {
type: "function",
function: {
name: "get_weather",
description: "Get the current weather for a location",
parameters: {
type: "object",
properties: { location: { type: "string" } },
required: ["location"],
},
},
};
const TOOL_CALL_TEXT = '<tool>{"name":"get_weather","arguments":{"location":"Tokyo"}}</tool>';
function baseOpts(extra: Record<string, unknown>) {
return {
model: "gpt-5.3-instant",
credentials: { apiKey: "test" },
signal: AbortSignal.timeout(10_000),
log: null,
...extra,
};
}
test("Tools request-side: <tool> contract is serialized into the upstream system message (#5240)", async () => {
__resetChatGptWebCachesForTesting();
const m = installMockFetch(convWithAssistantText("ok"));
try {
const executor = new ChatGptWebExecutor();
await executor.execute(
baseOpts({
body: {
messages: [{ role: "user", content: "What is the weather in Tokyo?" }],
tools: [WEATHER_TOOL],
},
stream: false,
}) as any
);
const convIdx = m.calls.urls.findIndex((u) => u.endsWith("/backend-api/f/conversation"));
assert.ok(convIdx >= 0, "conversation endpoint was hit");
const convBody = JSON.parse(m.calls.bodies[convIdx] as string);
const systemMsg = convBody.messages.find((mm: any) => mm.author.role === "system");
assert.ok(systemMsg, "a system message carrying the tool contract was sent");
const systemText = systemMsg.content.parts.join("");
assert.match(systemText, /<tool>/, "system prompt instructs the model to emit <tool> blocks");
assert.match(systemText, /get_weather/, "system prompt lists the requested tool");
} finally {
m.restore();
}
});
test("Tools non-stream: <tool>{...}</tool> text becomes OpenAI tool_calls + finish_reason (#5240)", async () => {
__resetChatGptWebCachesForTesting();
const m = installMockFetch(convWithAssistantText(TOOL_CALL_TEXT));
try {
const executor = new ChatGptWebExecutor();
const result = await executor.execute(
baseOpts({
body: {
messages: [{ role: "user", content: "What is the weather in Tokyo?" }],
tools: [WEATHER_TOOL],
},
stream: false,
}) as any
);
assert.equal(result.response.status, 200);
const json = await result.response.json();
assert.equal(json.choices[0].finish_reason, "tool_calls");
const tc = json.choices[0].message.tool_calls;
assert.ok(Array.isArray(tc) && tc.length === 1, "exactly one tool_call");
assert.equal(tc[0].type, "function");
assert.equal(tc[0].function.name, "get_weather");
assert.equal(typeof tc[0].function.arguments, "string", "arguments is a JSON string");
assert.deepEqual(JSON.parse(tc[0].function.arguments), { location: "Tokyo" });
assert.equal(json.choices[0].message.content, null);
} finally {
m.restore();
}
});
test("Tools stream: terminal chunk carries delta.tool_calls + finish_reason tool_calls (#5240)", async () => {
__resetChatGptWebCachesForTesting();
const m = installMockFetch(convWithAssistantText(TOOL_CALL_TEXT));
try {
const executor = new ChatGptWebExecutor();
const result = await executor.execute(
baseOpts({
body: {
messages: [{ role: "user", content: "What is the weather in Tokyo?" }],
tools: [WEATHER_TOOL],
stream: true,
},
stream: true,
}) as any
);
assert.equal(result.response.status, 200);
assert.equal(result.response.headers.get("Content-Type"), "text/event-stream");
const text = await result.response.text();
const chunks = text
.split("\n")
.filter((l) => l.startsWith("data: ") && !l.includes("[DONE]"))
.map((l) => JSON.parse(l.slice(6)));
const toolChunk = chunks.find((c) => c.choices[0].delta && c.choices[0].delta.tool_calls);
assert.ok(toolChunk, "a chunk carries delta.tool_calls");
assert.equal(toolChunk.choices[0].finish_reason, "tool_calls");
const tc = toolChunk.choices[0].delta.tool_calls;
assert.equal(tc[0].function.name, "get_weather");
assert.deepEqual(JSON.parse(tc[0].function.arguments), { location: "Tokyo" });
const lastLine = text.trim().split("\n").filter(Boolean).pop();
assert.equal(lastLine, "data: [DONE]");
} finally {
m.restore();
}
});
test("Tools regression: no-tools request still streams plain content with finish_reason stop (#5240)", async () => {
__resetChatGptWebCachesForTesting();
const m = installMockFetch(convWithAssistantText("Just plain text, no tools."));
try {
const executor = new ChatGptWebExecutor();
const result = await executor.execute(
baseOpts({
body: { messages: [{ role: "user", content: "hi" }], stream: true },
stream: true,
}) as any
);
const text = await result.response.text();
const chunks = text
.split("\n")
.filter((l) => l.startsWith("data: ") && !l.includes("[DONE]"))
.map((l) => JSON.parse(l.slice(6)));
assert.ok(
chunks.some(
(c) => c.choices[0].delta && c.choices[0].delta.content === "Just plain text, no tools."
),
"plain content is streamed"
);
assert.ok(
chunks.every((c) => !(c.choices[0].delta && c.choices[0].delta.tool_calls)),
"no tool_calls emitted without a tools array"
);
const finishChunk = chunks.find((c) => c.choices[0].finish_reason);
assert.equal(finishChunk.choices[0].finish_reason, "stop");
} finally {
m.restore();
}
});