Files
OmniRoute/tests/unit/ghe-copilot.test.ts
Tuan Dinh 6fec29ca2d fix(copilot): fallback to copilot-chat on 403 identity denial for standard provider (#13705)
* fix(copilot): fallback to copilot-chat on 403 identity denial for standard provider

* fix(copilot): document COPILOT_INTEGRATION_ID, extract identity fallback, add changelog

Adds the missing COPILOT_INTEGRATION_ID entry to .env.example (fixes
tests/unit/issue-7793-env-doc-sync-repro.test.ts), extracts the GitHub
Copilot 403 identity fallback out of open-sse/executors/base.ts into its
own module (open-sse/executors/copilotIdentityFallback.ts) to bring the
file back under the frozen file-size ratchet, and adds a changelog.d/fixes
fragment for the PR.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: tuandinh0801 <tuandinh0801@users.noreply.github.com>
2026-09-18 11:58:48 -03:00

238 lines
8.5 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import { GheCopilotExecutor } from "../../open-sse/executors/ghe-copilot.ts";
import { gheCopilotProvider } from "../../open-sse/config/providers/registry/ghe-copilot/index.ts";
import { GHE_COPILOT_TARGET } from "../../src/mitm/targets/ghe-copilot.ts";
import type { ProviderCredentials } from "../../open-sse/executors/base.ts";
test("GHE Copilot registry exposes Claude Opus 5", () => {
const opus5 = gheCopilotProvider.models.find((model) => model.id === "claude-opus-5");
assert.deepStrictEqual(opus5, {
id: "claude-opus-5",
name: "Claude Opus 5",
contextLength: 1000000,
maxOutputTokens: 64000,
unsupportedParams: ["temperature", "top_p", "top_k"],
});
});
test("GHE_COPILOT_TARGET has correct id and patterns", () => {
assert.strictEqual(GHE_COPILOT_TARGET.id, "ghe-copilot");
assert.deepStrictEqual(GHE_COPILOT_TARGET.endpointPatterns, [
"/chat/completions",
"/v1/chat/completions",
"/responses",
]);
});
test("buildUrl uses gheUrl for chat/completions with credentials", () => {
const executor = new GheCopilotExecutor({
gheUrl: "https://ghe.company.com",
clientId: "test-client",
clientSecret: "test-secret",
});
const credentials: ProviderCredentials = {
providerSpecificData: { gheUrl: "https://ghe.company.com" },
};
const url = executor.buildUrl("gpt-4o", true, 0, credentials);
assert.strictEqual(url, "https://ghe.company.com/chat/completions");
});
test("buildUrl uses gheUrl for responses endpoint with codex model", () => {
const executor = new GheCopilotExecutor({
gheUrl: "https://ghe.company.com",
clientId: "test-client",
clientSecret: "test-secret",
});
const credentials: ProviderCredentials = {
providerSpecificData: { gheUrl: "https://ghe.company.com" },
};
const url = executor.buildUrl("gpt-4o-codex", true, 0, credentials);
assert.strictEqual(url, "https://ghe.company.com/responses");
});
test("buildUrl uses responses endpoint for gpt-5.4-mini and gpt-5.6-sol", () => {
const executor = new GheCopilotExecutor({
gheUrl: "https://ghe.company.com",
clientId: "test-client",
clientSecret: "test-secret",
});
const credentials: ProviderCredentials = {
providerSpecificData: { gheUrl: "https://ghe.company.com" },
};
assert.strictEqual(
executor.buildUrl("gpt-5.4-mini", true, 0, credentials),
"https://ghe.company.com/responses"
);
assert.strictEqual(
executor.buildUrl("ghe-copilot/gpt-5.6-sol", true, 0, credentials),
"https://ghe.company.com/responses"
);
});
test("buildUrl routes Claude to the native /v1/messages shim (not chat/completions)", () => {
const executor = new GheCopilotExecutor({
gheUrl: "https://ghe.company.com",
clientId: "test-client",
clientSecret: "test-secret",
});
const credentials: ProviderCredentials = {
providerSpecificData: { gheUrl: "https://ghe.company.com" },
};
// Claude must ALWAYS use the Anthropic-native shim (prompt-cache token counts +
// lossless tool_use/tool_result/thinking blocks), same as github.com Copilot.
assert.strictEqual(
executor.buildUrl("claude-opus-5", true, 0, credentials),
"https://ghe.company.com/v1/messages"
);
});
test("buildUrl uses chat/completions endpoint for gemini models", () => {
const executor = new GheCopilotExecutor({
gheUrl: "https://ghe.company.com",
clientId: "test-client",
clientSecret: "test-secret",
});
const credentials: ProviderCredentials = {
providerSpecificData: { gheUrl: "https://ghe.company.com" },
};
// Gemini has no native shim on Copilot — it stays on /chat/completions.
assert.strictEqual(
executor.buildUrl("gemini-3.7-flash", true, 0, credentials),
"https://ghe.company.com/chat/completions"
);
});
test("buildUrl handles gheUrl with trailing slash", () => {
const exec = new GheCopilotExecutor({
gheUrl: "https://ghe.company.com/",
clientId: "test",
clientSecret: "test",
});
const credentials: ProviderCredentials = {
providerSpecificData: { gheUrl: "https://ghe.company.com/" },
};
const url = exec.buildUrl("gpt-4o", true, 0, credentials);
assert.strictEqual(url, "https://ghe.company.com/chat/completions");
});
test("buildUrl handles gheUrl already containing /chat/completions", () => {
const executor = new GheCopilotExecutor({
gheUrl: "https://ghe.company.com",
clientId: "test-client",
clientSecret: "test-secret",
});
const credentials: ProviderCredentials = {
providerSpecificData: { gheUrl: "https://ghe.company.com/chat/completions" },
};
const url = executor.buildUrl("gpt-4o", true, 0, credentials);
assert.strictEqual(url, "https://ghe.company.com/chat/completions");
});
test("buildUrl throws without gheUrl in credentials", () => {
const executor = new GheCopilotExecutor({
gheUrl: "https://ghe.company.com",
clientId: "test-client",
clientSecret: "test-secret",
});
const credentials: ProviderCredentials = { providerSpecificData: {} };
assert.throws(() => executor.buildUrl("gpt-4o", true, 0, credentials), {
message: "GHE Copilot executor requires gheUrl in providerSpecificData",
});
});
test("refreshCopilotToken delegates to GHE token endpoint", async () => {
const executor = new GheCopilotExecutor({
gheUrl: "https://ghe.company.com",
clientId: "test-client",
clientSecret: "test-secret",
});
const credentials: ProviderCredentials = {
providerSpecificData: { gheUrl: "https://ghe.company.com" },
copilotToken: "test-token",
};
const result = await executor.refreshCopilotToken("github-access-token", undefined, credentials);
assert.strictEqual(result, null);
});
test("refreshGitHubToken delegates to GHE OAuth endpoint", async () => {
const executor = new GheCopilotExecutor({
gheUrl: "https://ghe.company.com",
clientId: "test-client",
clientSecret: "test-secret",
});
const credentials: ProviderCredentials = {
providerSpecificData: { gheUrl: "https://ghe.company.com" },
};
const result = await executor.refreshGitHubToken("refresh-token", undefined, credentials);
assert.strictEqual(result, null);
});
test("executor extends GithubExecutor", () => {
const executor = new GheCopilotExecutor({
gheUrl: "https://ghe.company.com",
clientId: "test-client",
clientSecret: "test-secret",
});
assert.strictEqual(executor.constructor.name, "GheCopilotExecutor");
assert.strictEqual(executor.getProvider(), "ghe-copilot");
assert.strictEqual(executor.config.baseUrl, "https://api.githubcopilot.com/chat/completions");
});
test("isValidGheUrl accepts https enterprise hosts and rejects malformed or non-https input", async () => {
const { isValidGheUrl } = await import("../../src/shared/validation/providerSpecificData.ts");
assert.equal(isValidGheUrl("https://github.mycorp.example"), true);
assert.equal(isValidGheUrl("https://10.0.0.5"), true); // on-prem GHE on private IP is the primary use case
assert.equal(isValidGheUrl("http://github.mycorp.example"), false);
assert.equal(isValidGheUrl("javascript:alert(1)"), false);
assert.equal(isValidGheUrl("not a url"), false);
});
test("GheCopilotExecutor.execute does not trigger identity fallback on 403", async () => {
const executor = new GheCopilotExecutor({
gheUrl: "https://ghe.company.com",
clientId: "test-client",
clientSecret: "test-secret",
});
const originalFetch = globalThis.fetch;
let callCount = 0;
const seenIntegrationIds: string[] = [];
globalThis.fetch = async (_url, init: RequestInit = {}) => {
callCount++;
const headers = init.headers as Record<string, string>;
seenIntegrationIds.push(headers["copilot-integration-id"]);
return new Response(
JSON.stringify({ message: "Access denied: Enterprise Copilot 403 Forbidden" }),
{ status: 403, headers: { "Content-Type": "application/json" } }
);
};
try {
const credentials: ProviderCredentials = {
accessToken: "ghe-token",
providerSpecificData: {
gheUrl: "https://ghe.company.com",
copilotToken: "copilot-token",
},
};
const result = await executor.execute({
model: "gpt-4o",
body: { messages: [{ role: "user", content: "hi" }] },
stream: false,
credentials,
});
assert.equal(callCount, 1, "GHE Copilot must never retry on 403");
assert.deepEqual(seenIntegrationIds, ["copilot-developer-cli"]);
const res = result as { response: Response };
assert.equal(res.response.status, 403);
} finally {
globalThis.fetch = originalFetch;
}
});