b7bd41942d
Move chat pipeline validation, circuit breaker execution, proxy resolution, logging, and session header handling into dedicated helpers to keep the SSE handler smaller and easier to verify. Also fix shared API option precedence, rebuild skill version caches after deletions, ignore api.trycloudflare.com false positives, and add rate-limit manager test flush/reset hooks for deterministic coverage. Expand integration and unit coverage across chat routing, auth, cloud sync, skills, executors, streaming, DB helpers, proxy handling, and provider/model utilities.
302 lines
9.2 KiB
JavaScript
302 lines
9.2 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import { AntigravityExecutor } from "../../open-sse/executors/antigravity.ts";
|
|
|
|
async function withEnv(name, value, fn) {
|
|
const previous = process.env[name];
|
|
if (value === undefined) {
|
|
delete process.env[name];
|
|
} else {
|
|
process.env[name] = value;
|
|
}
|
|
|
|
try {
|
|
return await fn();
|
|
} finally {
|
|
if (previous === undefined) {
|
|
delete process.env[name];
|
|
} else {
|
|
process.env[name] = previous;
|
|
}
|
|
}
|
|
}
|
|
|
|
test("AntigravityExecutor.buildUrl always targets the streaming endpoint", () => {
|
|
const executor = new AntigravityExecutor();
|
|
assert.match(
|
|
executor.buildUrl("gemini-2.5-flash", true),
|
|
/\/v1internal:streamGenerateContent\?alt=sse$/
|
|
);
|
|
assert.equal(
|
|
executor.buildUrl("gemini-2.5-flash", false),
|
|
executor.buildUrl("gemini-2.5-flash", true)
|
|
);
|
|
});
|
|
|
|
test("AntigravityExecutor.buildHeaders includes auth and SSE accept", () => {
|
|
const executor = new AntigravityExecutor();
|
|
const headers = executor.buildHeaders({ accessToken: "ag-token" }, false);
|
|
|
|
assert.equal(headers.Authorization, "Bearer ag-token");
|
|
assert.equal(headers.Accept, "text/event-stream");
|
|
assert.equal(headers["X-OmniRoute-Source"], "omniroute");
|
|
});
|
|
|
|
test("AntigravityExecutor.transformRequest normalizes model, project and contents", async () => {
|
|
const executor = new AntigravityExecutor();
|
|
const body = {
|
|
request: {
|
|
contents: [
|
|
{
|
|
role: "model",
|
|
parts: [
|
|
{ thought: true, text: "skip me" },
|
|
{ thoughtSignature: "sig-only" },
|
|
{ text: "keep me" },
|
|
],
|
|
},
|
|
{
|
|
role: "model",
|
|
parts: [{ functionResponse: { name: "read_file", response: {} } }],
|
|
},
|
|
],
|
|
tools: [{ functionDeclarations: [{ name: "read_file" }] }],
|
|
},
|
|
};
|
|
|
|
const result = await executor.transformRequest("antigravity/gemini-3.1-pro", body, true, {
|
|
projectId: "project-1",
|
|
});
|
|
|
|
assert.equal(result.project, "project-1");
|
|
assert.equal(result.model, "gemini-3.1-pro-low");
|
|
assert.equal(result.userAgent, "antigravity");
|
|
assert.ok(result.request.sessionId);
|
|
assert.deepEqual(result.request.toolConfig, {
|
|
functionCallingConfig: { mode: "VALIDATED" },
|
|
});
|
|
assert.deepEqual(result.request.contents[0].parts, [{ text: "keep me" }]);
|
|
assert.equal(result.request.contents[1].role, "user");
|
|
});
|
|
|
|
test("AntigravityExecutor.transformRequest returns a structured error response when projectId is missing", async () => {
|
|
const executor = new AntigravityExecutor();
|
|
const result = await executor.transformRequest(
|
|
"gemini-2.5-flash",
|
|
{ request: { contents: [] } },
|
|
true,
|
|
{}
|
|
);
|
|
const payload = await result.json();
|
|
|
|
assert.equal(result.status, 422);
|
|
assert.equal(payload.error.code, "missing_project_id");
|
|
assert.match(payload.error.message, /Missing Google projectId/);
|
|
});
|
|
|
|
test("AntigravityExecutor.transformRequest allows body project overrides when the env flag is enabled", async () => {
|
|
const executor = new AntigravityExecutor();
|
|
|
|
await withEnv("OMNIROUTE_ALLOW_BODY_PROJECT_OVERRIDE", "1", async () => {
|
|
const result = await executor.transformRequest(
|
|
"antigravity/gemini-2.5-pro",
|
|
{
|
|
project: "body-project",
|
|
request: {
|
|
contents: [{ role: "user", parts: [{ text: "Hello" }] }],
|
|
sessionId: "session-fixed",
|
|
},
|
|
},
|
|
true,
|
|
{ projectId: "credential-project" }
|
|
);
|
|
|
|
assert.equal(result.project, "body-project");
|
|
assert.equal(result.request.sessionId, "session-fixed");
|
|
assert.equal(result.model, "gemini-2.5-pro");
|
|
});
|
|
});
|
|
|
|
test("AntigravityExecutor parses retry timing from headers and error strings", () => {
|
|
const executor = new AntigravityExecutor();
|
|
const headers = new Headers({
|
|
"retry-after": "120",
|
|
"x-ratelimit-reset-after": "30",
|
|
});
|
|
|
|
assert.equal(executor.parseRetryHeaders(headers), 120_000);
|
|
assert.equal(
|
|
executor.parseRetryFromErrorMessage("Your quota will reset after 2h7m23s"),
|
|
7_643_000
|
|
);
|
|
});
|
|
|
|
test("AntigravityExecutor.parseRetryHeaders falls back to reset-after and reset timestamps", () => {
|
|
const executor = new AntigravityExecutor();
|
|
const futureSeconds = Math.floor(Date.now() / 1000) + 90;
|
|
|
|
assert.equal(
|
|
executor.parseRetryHeaders(new Headers({ "x-ratelimit-reset-after": "45" })),
|
|
45_000
|
|
);
|
|
assert.ok(
|
|
executor.parseRetryHeaders(new Headers({ "x-ratelimit-reset": String(futureSeconds) })) >=
|
|
89_000
|
|
);
|
|
});
|
|
|
|
test("AntigravityExecutor.collectStreamToResponse turns SSE Gemini chunks into a chat completion", async () => {
|
|
const executor = new AntigravityExecutor();
|
|
const response = new Response(
|
|
[
|
|
'data: {"response":{"candidates":[{"content":{"parts":[{"text":"Hello "}]},"finishReason":"STOP"}]}}\n\n',
|
|
'data: {"response":{"candidates":[{"content":{"parts":[{"text":"world"}]},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":5,"candidatesTokenCount":3,"totalTokenCount":8}}}\n\n',
|
|
].join(""),
|
|
{
|
|
status: 200,
|
|
headers: { "Content-Type": "text/event-stream" },
|
|
}
|
|
);
|
|
|
|
const result = await executor.collectStreamToResponse(
|
|
response,
|
|
"gemini-2.5-flash",
|
|
"https://example.com",
|
|
{ Authorization: "Bearer ag-token" },
|
|
{ request: {} }
|
|
);
|
|
const payload = await result.response.json();
|
|
|
|
assert.equal(result.response.status, 200);
|
|
assert.equal(payload.object, "chat.completion");
|
|
assert.equal(payload.choices[0].message.content, "Hello world");
|
|
assert.equal(payload.choices[0].finish_reason, "stop");
|
|
assert.deepEqual(payload.usage, {
|
|
prompt_tokens: 5,
|
|
completion_tokens: 3,
|
|
total_tokens: 8,
|
|
});
|
|
});
|
|
|
|
test("AntigravityExecutor.refreshCredentials refreshes Google OAuth tokens", async () => {
|
|
const executor = new AntigravityExecutor();
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = async (url) => {
|
|
assert.match(String(url), /oauth2\.googleapis\.com\/token$/);
|
|
return new Response(
|
|
JSON.stringify({
|
|
access_token: "new-token",
|
|
refresh_token: "new-refresh",
|
|
expires_in: 3600,
|
|
}),
|
|
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
);
|
|
};
|
|
|
|
try {
|
|
const result = await executor.refreshCredentials(
|
|
{ refreshToken: "refresh", projectId: "project-1" },
|
|
null
|
|
);
|
|
assert.deepEqual(result, {
|
|
accessToken: "new-token",
|
|
refreshToken: "new-refresh",
|
|
expiresIn: 3600,
|
|
projectId: "project-1",
|
|
});
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("AntigravityExecutor.execute auto-retries short 429 responses and collects SSE for non-stream clients", async () => {
|
|
const executor = new AntigravityExecutor();
|
|
const originalFetch = globalThis.fetch;
|
|
const originalSetTimeout = globalThis.setTimeout;
|
|
const calls = [];
|
|
|
|
globalThis.fetch = async (url) => {
|
|
calls.push(String(url));
|
|
|
|
if (calls.length === 1) {
|
|
return new Response(JSON.stringify({ error: { message: "rate limited" } }), {
|
|
status: 429,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
|
|
return new Response(
|
|
[
|
|
'data: {"response":{"candidates":[{"content":{"parts":[{"text":"Hello "}]},"finishReason":"STOP"}]}}\n\n',
|
|
'data: {"response":{"candidates":[{"content":{"parts":[{"text":"again"}]},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":2,"candidatesTokenCount":3,"totalTokenCount":5}}}\n\n',
|
|
].join(""),
|
|
{
|
|
status: 200,
|
|
headers: { "Content-Type": "text/event-stream" },
|
|
}
|
|
);
|
|
};
|
|
globalThis.setTimeout = (callback) => {
|
|
callback();
|
|
return 0;
|
|
};
|
|
|
|
try {
|
|
const result = await executor.execute({
|
|
model: "antigravity/gemini-2.5-flash",
|
|
body: { request: { contents: [] } },
|
|
stream: false,
|
|
credentials: { accessToken: "token", projectId: "project-1" },
|
|
log: { debug() {}, warn() {} },
|
|
});
|
|
const payload = await result.response.json();
|
|
|
|
assert.equal(calls.length, 2);
|
|
assert.equal(result.response.status, 200);
|
|
assert.equal(payload.choices[0].message.content, "Hello again");
|
|
assert.deepEqual(payload.usage, {
|
|
prompt_tokens: 2,
|
|
completion_tokens: 3,
|
|
total_tokens: 5,
|
|
});
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
globalThis.setTimeout = originalSetTimeout;
|
|
}
|
|
});
|
|
|
|
test("AntigravityExecutor.execute embeds retryAfterMs when the upstream asks for a long wait", async () => {
|
|
const executor = new AntigravityExecutor();
|
|
const originalFetch = globalThis.fetch;
|
|
|
|
globalThis.fetch = async () =>
|
|
new Response(
|
|
JSON.stringify({
|
|
error: {
|
|
message: "Your quota will reset after 2h",
|
|
},
|
|
}),
|
|
{
|
|
status: 429,
|
|
headers: { "Content-Type": "application/json" },
|
|
}
|
|
);
|
|
|
|
try {
|
|
const result = await executor.execute({
|
|
model: "antigravity/gemini-2.5-flash",
|
|
body: { request: { contents: [] } },
|
|
stream: true,
|
|
credentials: { accessToken: "token", projectId: "project-1" },
|
|
log: { debug() {}, warn() {} },
|
|
});
|
|
const payload = await result.response.json();
|
|
|
|
assert.equal(result.response.status, 429);
|
|
assert.equal(payload.retryAfterMs, 7_200_000);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|