openclaw/src/agents/simple-completion-runtime.t...

344 lines
9.8 KiB
TypeScript

import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const hoisted = vi.hoisted(() => ({
resolveModelMock: vi.fn(),
getApiKeyForModelMock: vi.fn(),
applyLocalNoAuthHeaderOverrideMock: vi.fn(),
setRuntimeApiKeyMock: vi.fn(),
resolveCopilotApiTokenMock: vi.fn(),
}));
vi.mock("./pi-embedded-runner/model.js", () => ({
resolveModel: hoisted.resolveModelMock,
}));
vi.mock("./model-auth.js", () => ({
getApiKeyForModel: hoisted.getApiKeyForModelMock,
applyLocalNoAuthHeaderOverride: hoisted.applyLocalNoAuthHeaderOverrideMock,
}));
vi.mock("./github-copilot-token.js", () => ({
resolveCopilotApiToken: hoisted.resolveCopilotApiTokenMock,
}));
let prepareSimpleCompletionModel: typeof import("./simple-completion-runtime.js").prepareSimpleCompletionModel;
beforeAll(async () => {
({ prepareSimpleCompletionModel } = await import("./simple-completion-runtime.js"));
});
beforeEach(() => {
hoisted.resolveModelMock.mockReset();
hoisted.getApiKeyForModelMock.mockReset();
hoisted.applyLocalNoAuthHeaderOverrideMock.mockReset();
hoisted.setRuntimeApiKeyMock.mockReset();
hoisted.resolveCopilotApiTokenMock.mockReset();
hoisted.applyLocalNoAuthHeaderOverrideMock.mockImplementation((model: unknown) => model);
hoisted.resolveModelMock.mockReturnValue({
model: {
provider: "anthropic",
id: "claude-opus-4-6",
},
authStorage: {
setRuntimeApiKey: hoisted.setRuntimeApiKeyMock,
},
modelRegistry: {},
});
hoisted.getApiKeyForModelMock.mockResolvedValue({
apiKey: "sk-test",
source: "env:TEST_API_KEY",
mode: "api-key",
});
hoisted.resolveCopilotApiTokenMock.mockResolvedValue({
token: "copilot-runtime-token",
expiresAt: Date.now() + 60_000,
source: "cache:/tmp/copilot-token.json",
baseUrl: "https://api.individual.githubcopilot.com",
});
});
describe("prepareSimpleCompletionModel", () => {
it("resolves model auth and sets runtime api key", async () => {
hoisted.getApiKeyForModelMock.mockResolvedValueOnce({
apiKey: " sk-test ",
source: "env:TEST_API_KEY",
mode: "api-key",
});
const result = await prepareSimpleCompletionModel({
cfg: undefined,
provider: "anthropic",
modelId: "claude-opus-4-6",
agentDir: "/tmp/openclaw-agent",
});
expect(result).toEqual(
expect.objectContaining({
model: expect.objectContaining({
provider: "anthropic",
id: "claude-opus-4-6",
}),
auth: expect.objectContaining({
mode: "api-key",
source: "env:TEST_API_KEY",
}),
}),
);
expect(hoisted.setRuntimeApiKeyMock).toHaveBeenCalledWith("anthropic", "sk-test");
});
it("returns error when model resolution fails", async () => {
hoisted.resolveModelMock.mockReturnValueOnce({
error: "Unknown model: anthropic/missing-model",
authStorage: {
setRuntimeApiKey: hoisted.setRuntimeApiKeyMock,
},
modelRegistry: {},
});
const result = await prepareSimpleCompletionModel({
cfg: undefined,
provider: "anthropic",
modelId: "missing-model",
});
expect(result).toEqual({
error: "Unknown model: anthropic/missing-model",
});
expect(hoisted.getApiKeyForModelMock).not.toHaveBeenCalled();
});
it("returns error when api key is missing and mode is not allowlisted", async () => {
hoisted.getApiKeyForModelMock.mockResolvedValueOnce({
source: "models.providers.anthropic",
mode: "api-key",
});
const result = await prepareSimpleCompletionModel({
cfg: undefined,
provider: "anthropic",
modelId: "claude-opus-4-6",
});
expect(result).toEqual({
error: 'No API key resolved for provider "anthropic" (auth mode: api-key).',
auth: {
source: "models.providers.anthropic",
mode: "api-key",
},
});
expect(hoisted.setRuntimeApiKeyMock).not.toHaveBeenCalled();
});
it("continues without api key when auth mode is allowlisted", async () => {
hoisted.resolveModelMock.mockReturnValueOnce({
model: {
provider: "amazon-bedrock",
id: "anthropic.claude-sonnet-4-6",
},
authStorage: {
setRuntimeApiKey: hoisted.setRuntimeApiKeyMock,
},
modelRegistry: {},
});
hoisted.getApiKeyForModelMock.mockResolvedValueOnce({
source: "aws-sdk default chain",
mode: "aws-sdk",
});
const result = await prepareSimpleCompletionModel({
cfg: undefined,
provider: "amazon-bedrock",
modelId: "anthropic.claude-sonnet-4-6",
allowMissingApiKeyModes: ["aws-sdk"],
});
expect(result).toEqual(
expect.objectContaining({
model: expect.objectContaining({
provider: "amazon-bedrock",
id: "anthropic.claude-sonnet-4-6",
}),
auth: {
source: "aws-sdk default chain",
mode: "aws-sdk",
},
}),
);
expect(hoisted.setRuntimeApiKeyMock).not.toHaveBeenCalled();
});
it("exchanges github token when provider is github-copilot", async () => {
hoisted.resolveModelMock.mockReturnValueOnce({
model: {
provider: "github-copilot",
id: "gpt-4.1",
},
authStorage: {
setRuntimeApiKey: hoisted.setRuntimeApiKeyMock,
},
modelRegistry: {},
});
hoisted.getApiKeyForModelMock.mockResolvedValueOnce({
apiKey: "ghu_test",
source: "profile:github-copilot:default",
mode: "token",
});
await prepareSimpleCompletionModel({
cfg: undefined,
provider: "github-copilot",
modelId: "gpt-4.1",
});
expect(hoisted.resolveCopilotApiTokenMock).toHaveBeenCalledWith({
githubToken: "ghu_test",
});
expect(hoisted.setRuntimeApiKeyMock).toHaveBeenCalledWith(
"github-copilot",
"copilot-runtime-token",
);
});
it("returns exchanged copilot token in auth.apiKey for github-copilot provider", async () => {
hoisted.resolveModelMock.mockReturnValueOnce({
model: {
provider: "github-copilot",
id: "gpt-4.1",
},
authStorage: {
setRuntimeApiKey: hoisted.setRuntimeApiKeyMock,
},
modelRegistry: {},
});
hoisted.getApiKeyForModelMock.mockResolvedValueOnce({
apiKey: "ghu_original_github_token",
source: "profile:github-copilot:default",
mode: "token",
});
const result = await prepareSimpleCompletionModel({
cfg: undefined,
provider: "github-copilot",
modelId: "gpt-4.1",
});
expect(result).not.toHaveProperty("error");
if ("error" in result) {
return;
}
// The returned auth.apiKey should be the exchanged runtime token,
// not the original GitHub token
expect(result.auth.apiKey).toBe("copilot-runtime-token");
expect(result.auth.apiKey).not.toBe("ghu_original_github_token");
});
it("applies exchanged copilot baseUrl to returned model", async () => {
hoisted.resolveModelMock.mockReturnValueOnce({
model: {
provider: "github-copilot",
id: "gpt-4.1",
},
authStorage: {
setRuntimeApiKey: hoisted.setRuntimeApiKeyMock,
},
modelRegistry: {},
});
hoisted.getApiKeyForModelMock.mockResolvedValueOnce({
apiKey: "ghu_test",
source: "profile:github-copilot:default",
mode: "token",
});
hoisted.resolveCopilotApiTokenMock.mockResolvedValueOnce({
token: "copilot-runtime-token",
expiresAt: Date.now() + 60_000,
source: "cache:/tmp/copilot-token.json",
baseUrl: "https://api.copilot.enterprise.example",
});
const result = await prepareSimpleCompletionModel({
cfg: undefined,
provider: "github-copilot",
modelId: "gpt-4.1",
});
expect(result).not.toHaveProperty("error");
if ("error" in result) {
return;
}
expect(result.model).toEqual(
expect.objectContaining({
baseUrl: "https://api.copilot.enterprise.example",
}),
);
});
it("returns error when getApiKeyForModel throws", async () => {
hoisted.getApiKeyForModelMock.mockRejectedValueOnce(new Error("Profile not found: copilot"));
const result = await prepareSimpleCompletionModel({
cfg: undefined,
provider: "anthropic",
modelId: "claude-opus-4-6",
});
expect(result).toEqual({
error: 'Auth lookup failed for provider "anthropic": Profile not found: copilot',
});
expect(hoisted.setRuntimeApiKeyMock).not.toHaveBeenCalled();
});
it("applies local no-auth header override before returning model", async () => {
hoisted.resolveModelMock.mockReturnValueOnce({
model: {
provider: "local-openai",
id: "chat-local",
api: "openai-completions",
},
authStorage: {
setRuntimeApiKey: hoisted.setRuntimeApiKeyMock,
},
modelRegistry: {},
});
hoisted.getApiKeyForModelMock.mockResolvedValueOnce({
apiKey: "custom-local",
source: "models.providers.local-openai (synthetic local key)",
mode: "api-key",
});
hoisted.applyLocalNoAuthHeaderOverrideMock.mockReturnValueOnce({
provider: "local-openai",
id: "chat-local",
api: "openai-completions",
headers: { Authorization: null },
});
const result = await prepareSimpleCompletionModel({
cfg: undefined,
provider: "local-openai",
modelId: "chat-local",
});
expect(hoisted.applyLocalNoAuthHeaderOverrideMock).toHaveBeenCalledWith(
expect.objectContaining({
provider: "local-openai",
id: "chat-local",
}),
expect.objectContaining({
apiKey: "custom-local",
source: "models.providers.local-openai (synthetic local key)",
mode: "api-key",
}),
);
expect(result).toEqual(
expect.objectContaining({
model: expect.objectContaining({
headers: expect.objectContaining({ Authorization: null }),
}),
}),
);
});
});