mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 09:41:11 +00:00
test: keep provider auth onboarding tests off runtime auth
This commit is contained in:
@@ -26,6 +26,59 @@ const TEST_AUTH_STORE_VERSION = 1;
|
||||
const TEST_MAIN_AUTH_STORE_KEY = "__main__";
|
||||
|
||||
const ensureWorkspaceAndSessionsMock = vi.hoisted(() => vi.fn(async (..._args: unknown[]) => {}));
|
||||
const readConfigFileSnapshotMock = vi.hoisted(() =>
|
||||
vi.fn(async () => {
|
||||
const [{ default: fs }, { default: path }, { default: crypto }] = await Promise.all([
|
||||
import("node:fs/promises"),
|
||||
import("node:path"),
|
||||
import("node:crypto"),
|
||||
]);
|
||||
const configPath = process.env.OPENCLAW_CONFIG_PATH;
|
||||
if (!configPath) {
|
||||
throw new Error("OPENCLAW_CONFIG_PATH must be set for provider auth onboarding tests");
|
||||
}
|
||||
let raw: string | null = null;
|
||||
try {
|
||||
raw = await fs.readFile(configPath, "utf-8");
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
const parsed = raw ? (JSON.parse(raw) as Record<string, unknown>) : {};
|
||||
const hash = raw === null ? undefined : crypto.createHash("sha256").update(raw).digest("hex");
|
||||
return {
|
||||
path: path.resolve(configPath),
|
||||
exists: raw !== null,
|
||||
valid: true,
|
||||
raw,
|
||||
hash,
|
||||
config: structuredClone(parsed),
|
||||
sourceConfig: structuredClone(parsed),
|
||||
runtimeConfig: structuredClone(parsed),
|
||||
};
|
||||
}),
|
||||
);
|
||||
const replaceConfigFileMock = vi.hoisted(() =>
|
||||
vi.fn(async (params: { nextConfig: unknown }) => {
|
||||
const [{ default: fs }, { default: path }] = await Promise.all([
|
||||
import("node:fs/promises"),
|
||||
import("node:path"),
|
||||
]);
|
||||
const configPath = process.env.OPENCLAW_CONFIG_PATH;
|
||||
if (!configPath) {
|
||||
throw new Error("OPENCLAW_CONFIG_PATH must be set for provider auth onboarding tests");
|
||||
}
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||
await fs.writeFile(configPath, `${JSON.stringify(params.nextConfig, null, 2)}\n`, "utf-8");
|
||||
return {
|
||||
path: configPath,
|
||||
previousHash: null,
|
||||
snapshot: {},
|
||||
nextConfig: params.nextConfig,
|
||||
};
|
||||
}),
|
||||
);
|
||||
const testAuthProfileStores = vi.hoisted(
|
||||
() => new Map<string, { version: number; profiles: Record<string, Record<string, unknown>> }>(),
|
||||
);
|
||||
@@ -88,6 +141,15 @@ function upsertAuthProfile(params: {
|
||||
writeRuntimeAuthSnapshots();
|
||||
}
|
||||
|
||||
vi.mock("../config/config.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../config/config.js")>("../config/config.js");
|
||||
return {
|
||||
...actual,
|
||||
readConfigFileSnapshot: readConfigFileSnapshotMock,
|
||||
replaceConfigFile: replaceConfigFileMock,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("./onboard-non-interactive/local/auth-choice.plugin-providers.js", async () => {
|
||||
const [
|
||||
{ resolveDefaultAgentId, resolveAgentDir, resolveAgentWorkspaceDir },
|
||||
@@ -782,7 +844,6 @@ const NON_INTERACTIVE_DEFAULT_OPTIONS = {
|
||||
|
||||
let runNonInteractiveSetup: typeof import("./onboard-non-interactive.js").runNonInteractiveSetup;
|
||||
let clearRuntimeAuthProfileStoreSnapshots: typeof import("../agents/auth-profiles.js").clearRuntimeAuthProfileStoreSnapshots;
|
||||
let ensureAuthProfileStore: typeof import("../agents/auth-profiles.js").ensureAuthProfileStore;
|
||||
let replaceRuntimeAuthProfileStoreSnapshots: typeof import("../agents/auth-profiles.js").replaceRuntimeAuthProfileStoreSnapshots;
|
||||
let resetFileLockStateForTest: typeof import("../infra/file-lock.js").resetFileLockStateForTest;
|
||||
let clearPluginDiscoveryCache: typeof import("../plugins/discovery.js").clearPluginDiscoveryCache;
|
||||
@@ -980,7 +1041,7 @@ async function expectApiKeyProfile(params: {
|
||||
key: string;
|
||||
metadata?: Record<string, string>;
|
||||
}): Promise<void> {
|
||||
const store = ensureAuthProfileStore();
|
||||
const store = getOrCreateTestAuthStore();
|
||||
const profile = store.profiles[params.profileId];
|
||||
expect(profile?.type).toBe("api_key");
|
||||
if (profile?.type === "api_key") {
|
||||
@@ -994,11 +1055,8 @@ async function expectApiKeyProfile(params: {
|
||||
|
||||
async function loadProviderAuthOnboardModules(): Promise<void> {
|
||||
({ runNonInteractiveSetup } = await import("./onboard-non-interactive.js"));
|
||||
({
|
||||
clearRuntimeAuthProfileStoreSnapshots,
|
||||
ensureAuthProfileStore,
|
||||
replaceRuntimeAuthProfileStoreSnapshots,
|
||||
} = await import("../agents/auth-profiles.js"));
|
||||
({ clearRuntimeAuthProfileStoreSnapshots, replaceRuntimeAuthProfileStoreSnapshots } =
|
||||
await import("../agents/auth-profiles.js"));
|
||||
({ resetFileLockStateForTest } = await import("../infra/file-lock.js"));
|
||||
({ clearPluginDiscoveryCache } = await import("../plugins/discovery.js"));
|
||||
({ clearPluginManifestRegistryCache } = await import("../plugins/manifest-registry.js"));
|
||||
@@ -1230,8 +1288,7 @@ describe("onboard (non-interactive): provider auth", () => {
|
||||
const token = `${cleanToken.slice(0, 30)}\r${cleanToken.slice(30)}`;
|
||||
|
||||
await runNonInteractiveSetupWithDefaults(runtime, {
|
||||
authChoice: "token",
|
||||
tokenProvider: "anthropic",
|
||||
authChoice: "setup-token",
|
||||
token,
|
||||
tokenProfileId: "anthropic:default",
|
||||
});
|
||||
@@ -1240,7 +1297,7 @@ describe("onboard (non-interactive): provider auth", () => {
|
||||
expect(cfg.auth?.profiles?.["anthropic:default"]?.provider).toBe("anthropic");
|
||||
expect(cfg.auth?.profiles?.["anthropic:default"]?.mode).toBe("token");
|
||||
expect(cfg.agents?.defaults?.model?.primary).toBe("anthropic/claude-sonnet-4-6");
|
||||
expect(ensureAuthProfileStore().profiles["anthropic:default"]).toMatchObject({
|
||||
expect(getOrCreateTestAuthStore().profiles["anthropic:default"]).toMatchObject({
|
||||
provider: "anthropic",
|
||||
type: "token",
|
||||
token: cleanToken,
|
||||
@@ -1358,7 +1415,7 @@ describe("onboard (non-interactive): provider auth", () => {
|
||||
skipSkills: true,
|
||||
});
|
||||
|
||||
const store = ensureAuthProfileStore();
|
||||
const store = getOrCreateTestAuthStore();
|
||||
for (const profileId of ["opencode:default", "opencode-go:default"]) {
|
||||
const profile = store.profiles[profileId];
|
||||
expect(profile?.type).toBe("api_key");
|
||||
@@ -1376,66 +1433,6 @@ describe("onboard (non-interactive): provider auth", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("configures vLLM via the provider plugin in non-interactive mode", async () => {
|
||||
await withOnboardEnv("openclaw-onboard-vllm-non-interactive-", async (env) => {
|
||||
const cfg = await runOnboardingAndReadConfig(env, {
|
||||
authChoice: "vllm",
|
||||
customBaseUrl: "http://127.0.0.1:8100/v1",
|
||||
customApiKey: "vllm-test-key", // pragma: allowlist secret
|
||||
customModelId: "Qwen/Qwen3-8B",
|
||||
});
|
||||
|
||||
expect(cfg.auth?.profiles?.["vllm:default"]?.provider).toBe("vllm");
|
||||
expect(cfg.auth?.profiles?.["vllm:default"]?.mode).toBe("api_key");
|
||||
expect(cfg.models?.providers?.vllm).toEqual({
|
||||
baseUrl: "http://127.0.0.1:8100/v1",
|
||||
api: "openai-completions",
|
||||
apiKey: "VLLM_API_KEY",
|
||||
models: [
|
||||
expect.objectContaining({
|
||||
id: "Qwen/Qwen3-8B",
|
||||
}),
|
||||
],
|
||||
});
|
||||
expect(cfg.agents?.defaults?.model?.primary).toBe("vllm/Qwen/Qwen3-8B");
|
||||
await expectApiKeyProfile({
|
||||
profileId: "vllm:default",
|
||||
provider: "vllm",
|
||||
key: "vllm-test-key",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("configures SGLang via the provider plugin in non-interactive mode", async () => {
|
||||
await withOnboardEnv("openclaw-onboard-sglang-non-interactive-", async (env) => {
|
||||
const cfg = await runOnboardingAndReadConfig(env, {
|
||||
authChoice: "sglang",
|
||||
customBaseUrl: "http://127.0.0.1:31000/v1",
|
||||
customApiKey: "sglang-test-key", // pragma: allowlist secret
|
||||
customModelId: "Qwen/Qwen3-32B",
|
||||
});
|
||||
|
||||
expect(cfg.auth?.profiles?.["sglang:default"]?.provider).toBe("sglang");
|
||||
expect(cfg.auth?.profiles?.["sglang:default"]?.mode).toBe("api_key");
|
||||
expect(cfg.models?.providers?.sglang).toEqual({
|
||||
baseUrl: "http://127.0.0.1:31000/v1",
|
||||
api: "openai-completions",
|
||||
apiKey: "SGLANG_API_KEY",
|
||||
models: [
|
||||
expect.objectContaining({
|
||||
id: "Qwen/Qwen3-32B",
|
||||
}),
|
||||
],
|
||||
});
|
||||
expect(cfg.agents?.defaults?.model?.primary).toBe("sglang/Qwen/Qwen3-32B");
|
||||
await expectApiKeyProfile({
|
||||
profileId: "sglang:default",
|
||||
provider: "sglang",
|
||||
key: "sglang-test-key",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("stores LiteLLM API key in the default auth profile", async () => {
|
||||
await withOnboardEnv("openclaw-onboard-litellm-", async (env) => {
|
||||
const cfg = await runOnboardingAndReadConfig(env, {
|
||||
@@ -1646,24 +1643,6 @@ describe("onboard (non-interactive): provider auth", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("uses matching profile fallback for non-interactive custom provider auth", async () => {
|
||||
await withOnboardEnv(
|
||||
"openclaw-onboard-custom-provider-profile-fallback-",
|
||||
async ({ configPath, runtime }) => {
|
||||
upsertAuthProfile({
|
||||
profileId: `${CUSTOM_LOCAL_PROVIDER_ID}:default`,
|
||||
credential: {
|
||||
type: "api_key",
|
||||
provider: CUSTOM_LOCAL_PROVIDER_ID,
|
||||
key: "custom-profile-key",
|
||||
},
|
||||
});
|
||||
await runCustomLocalNonInteractive(runtime);
|
||||
expect(await readCustomLocalProviderApiKey(configPath)).toBe("custom-profile-key");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("fails custom provider auth when compatibility is invalid", async () => {
|
||||
await withOnboardEnv(
|
||||
"openclaw-onboard-custom-provider-invalid-compat-",
|
||||
|
||||
107
src/commands/onboard-non-interactive/api-keys.test.ts
Normal file
107
src/commands/onboard-non-interactive/api-keys.test.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { resolveNonInteractiveApiKey } from "./api-keys.js";
|
||||
|
||||
const resolveEnvApiKey = vi.hoisted(() => vi.fn());
|
||||
vi.mock("../../agents/model-auth.js", () => ({
|
||||
resolveEnvApiKey,
|
||||
}));
|
||||
|
||||
const authStore = vi.hoisted(
|
||||
() =>
|
||||
({
|
||||
version: 1,
|
||||
profiles: {} as Record<string, { type: "api_key"; provider: string; key: string }>,
|
||||
}) as const,
|
||||
);
|
||||
const resolveApiKeyForProfile = vi.hoisted(() =>
|
||||
vi.fn(async (params: { profileId: string }) => {
|
||||
const profile = authStore.profiles[params.profileId];
|
||||
return profile?.type === "api_key" ? { apiKey: profile.key, source: "profile" } : null;
|
||||
}),
|
||||
);
|
||||
vi.mock("../../agents/auth-profiles.js", () => ({
|
||||
ensureAuthProfileStore: vi.fn(() => authStore),
|
||||
resolveApiKeyForProfile,
|
||||
resolveAuthProfileOrder: vi.fn(() => Object.keys(authStore.profiles)),
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
for (const profileId of Object.keys(authStore.profiles)) {
|
||||
delete authStore.profiles[profileId];
|
||||
}
|
||||
});
|
||||
|
||||
function createRuntime() {
|
||||
return {
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
describe("resolveNonInteractiveApiKey", () => {
|
||||
it("returns explicit flag keys before resolving env or plugin-backed setup", async () => {
|
||||
const runtime = createRuntime();
|
||||
resolveEnvApiKey.mockImplementation(() => {
|
||||
throw new Error("env lookup should not run for an explicit plaintext flag");
|
||||
});
|
||||
|
||||
const result = await resolveNonInteractiveApiKey({
|
||||
provider: "xai",
|
||||
cfg: {},
|
||||
flagValue: "xai-flag-key",
|
||||
flagName: "--xai-api-key",
|
||||
envVar: "XAI_API_KEY",
|
||||
runtime: runtime as never,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ key: "xai-flag-key", source: "flag" });
|
||||
expect(resolveEnvApiKey).not.toHaveBeenCalled();
|
||||
expect(runtime.exit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects flag input in secret-ref mode without broad env discovery", async () => {
|
||||
const runtime = createRuntime();
|
||||
resolveEnvApiKey.mockReturnValue(null);
|
||||
|
||||
const result = await resolveNonInteractiveApiKey({
|
||||
provider: "xai",
|
||||
cfg: {},
|
||||
flagValue: "xai-flag-key",
|
||||
flagName: "--xai-api-key",
|
||||
envVar: "XAI_API_KEY",
|
||||
runtime: runtime as never,
|
||||
secretInputMode: "ref",
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(resolveEnvApiKey).not.toHaveBeenCalled();
|
||||
expect(runtime.exit).toHaveBeenCalledWith(1);
|
||||
expect(runtime.error).toHaveBeenCalledWith(expect.stringContaining("--secret-input-mode ref"));
|
||||
});
|
||||
|
||||
it("falls back to a matching API-key profile after flag and env are absent", async () => {
|
||||
const runtime = createRuntime();
|
||||
authStore.profiles["custom-models-custom-local:default"] = {
|
||||
type: "api_key",
|
||||
provider: "custom-models-custom-local",
|
||||
key: "custom-profile-key",
|
||||
};
|
||||
resolveEnvApiKey.mockReturnValue(null);
|
||||
|
||||
const result = await resolveNonInteractiveApiKey({
|
||||
provider: "custom-models-custom-local",
|
||||
cfg: {},
|
||||
flagName: "--custom-api-key",
|
||||
envVar: "CUSTOM_API_KEY",
|
||||
runtime: runtime as never,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ key: "custom-profile-key", source: "profile" });
|
||||
expect(resolveApiKeyForProfile).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
profileId: "custom-models-custom-local:default",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -62,28 +62,39 @@ export async function resolveNonInteractiveApiKey(params: {
|
||||
secretInputMode?: SecretInputMode;
|
||||
}): Promise<{ key: string; source: NonInteractiveApiKeySource; envVarName?: string } | null> {
|
||||
const flagKey = normalizeOptionalSecretInput(params.flagValue);
|
||||
const envResolved = resolveEnvApiKey(params.provider);
|
||||
const explicitEnvVar = params.envVarName?.trim();
|
||||
const explicitEnvKey = explicitEnvVar
|
||||
? normalizeOptionalSecretInput(process.env[explicitEnvVar])
|
||||
: undefined;
|
||||
const resolvedEnvKey = envResolved?.apiKey ?? explicitEnvKey;
|
||||
const resolvedEnvVarName = parseEnvVarNameFromSourceLabel(envResolved?.source) ?? explicitEnvVar;
|
||||
const explicitEnvVar = params.envVarName?.trim() || params.envVar.trim();
|
||||
const resolveExplicitEnvKey = () => normalizeOptionalSecretInput(process.env[explicitEnvVar]);
|
||||
const resolveEnvKey = () => {
|
||||
const envResolved = resolveEnvApiKey(params.provider);
|
||||
const explicitEnvKey = explicitEnvVar
|
||||
? normalizeOptionalSecretInput(process.env[explicitEnvVar])
|
||||
: undefined;
|
||||
return {
|
||||
key: envResolved?.apiKey ?? explicitEnvKey,
|
||||
envVarName: parseEnvVarNameFromSourceLabel(envResolved?.source) ?? explicitEnvVar,
|
||||
};
|
||||
};
|
||||
|
||||
const useSecretRefMode = params.secretInputMode === "ref"; // pragma: allowlist secret
|
||||
if (useSecretRefMode) {
|
||||
if (!resolvedEnvKey && flagKey) {
|
||||
params.runtime.error(
|
||||
[
|
||||
`${params.flagName} cannot be used with --secret-input-mode ref unless ${params.envVar} is set in env.`,
|
||||
`Set ${params.envVar} in env and omit ${params.flagName}, or use --secret-input-mode plaintext.`,
|
||||
].join("\n"),
|
||||
);
|
||||
params.runtime.exit(1);
|
||||
return null;
|
||||
if (useSecretRefMode && flagKey) {
|
||||
const explicitEnvKey = resolveExplicitEnvKey();
|
||||
if (explicitEnvKey) {
|
||||
return { key: explicitEnvKey, source: "env", envVarName: explicitEnvVar };
|
||||
}
|
||||
if (resolvedEnvKey) {
|
||||
if (!resolvedEnvVarName) {
|
||||
params.runtime.error(
|
||||
[
|
||||
`${params.flagName} cannot be used with --secret-input-mode ref unless ${params.envVar} is set in env.`,
|
||||
`Set ${params.envVar} in env and omit ${params.flagName}, or use --secret-input-mode plaintext.`,
|
||||
].join("\n"),
|
||||
);
|
||||
params.runtime.exit(1);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (useSecretRefMode) {
|
||||
const resolvedEnv = resolveEnvKey();
|
||||
if (resolvedEnv.key) {
|
||||
if (!resolvedEnv.envVarName) {
|
||||
params.runtime.error(
|
||||
[
|
||||
`--secret-input-mode ref requires an explicit environment variable for provider "${params.provider}".`,
|
||||
@@ -93,7 +104,7 @@ export async function resolveNonInteractiveApiKey(params: {
|
||||
params.runtime.exit(1);
|
||||
return null;
|
||||
}
|
||||
return { key: resolvedEnvKey, source: "env", envVarName: resolvedEnvVarName };
|
||||
return { key: resolvedEnv.key, source: "env", envVarName: resolvedEnv.envVarName };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,8 +112,9 @@ export async function resolveNonInteractiveApiKey(params: {
|
||||
return { key: flagKey, source: "flag" };
|
||||
}
|
||||
|
||||
if (resolvedEnvKey) {
|
||||
return { key: resolvedEnvKey, source: "env", envVarName: resolvedEnvVarName };
|
||||
const resolvedEnv = resolveEnvKey();
|
||||
if (resolvedEnv.key) {
|
||||
return { key: resolvedEnv.key, source: "env", envVarName: resolvedEnv.envVarName };
|
||||
}
|
||||
|
||||
if (params.allowProfile ?? true) {
|
||||
|
||||
159
src/plugins/provider-self-hosted-setup.test.ts
Normal file
159
src/plugins/provider-self-hosted-setup.test.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { configureOpenAICompatibleSelfHostedProviderNonInteractive } from "./provider-self-hosted-setup.js";
|
||||
import type { ProviderAuthMethodNonInteractiveContext } from "./types.js";
|
||||
|
||||
const upsertAuthProfileWithLock = vi.hoisted(() => vi.fn(async () => null));
|
||||
vi.mock("../agents/auth-profiles/upsert-with-lock.js", () => ({
|
||||
upsertAuthProfileWithLock,
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
function createRuntime() {
|
||||
return {
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
log: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
function createContext(params: {
|
||||
providerId: string;
|
||||
baseUrl?: string;
|
||||
apiKey?: string;
|
||||
modelId?: string;
|
||||
}): ProviderAuthMethodNonInteractiveContext {
|
||||
const resolved = {
|
||||
key: params.apiKey ?? "self-hosted-test-key",
|
||||
source: "flag" as const,
|
||||
};
|
||||
return {
|
||||
authChoice: params.providerId,
|
||||
config: { agents: { defaults: {} } },
|
||||
baseConfig: { agents: { defaults: {} } },
|
||||
opts: {
|
||||
customBaseUrl: params.baseUrl,
|
||||
customApiKey: params.apiKey,
|
||||
customModelId: params.modelId,
|
||||
},
|
||||
runtime: createRuntime() as never,
|
||||
agentDir: "/tmp/openclaw-self-hosted-test-agent",
|
||||
resolveApiKey: vi.fn<ProviderAuthMethodNonInteractiveContext["resolveApiKey"]>(
|
||||
async () => resolved,
|
||||
),
|
||||
toApiKeyCredential: vi.fn<ProviderAuthMethodNonInteractiveContext["toApiKeyCredential"]>(
|
||||
({ provider, resolved: apiKeyResult }) => ({
|
||||
type: "api_key",
|
||||
provider,
|
||||
key: apiKeyResult.key,
|
||||
}),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function readPrimaryModel(config: Awaited<ReturnType<typeof configureSelfHostedTestProvider>>) {
|
||||
const model = config?.agents?.defaults?.model;
|
||||
return model && typeof model === "object" ? model.primary : undefined;
|
||||
}
|
||||
|
||||
async function configureSelfHostedTestProvider(params: {
|
||||
ctx: ProviderAuthMethodNonInteractiveContext;
|
||||
providerId: string;
|
||||
providerLabel: string;
|
||||
envVar: string;
|
||||
}) {
|
||||
return await configureOpenAICompatibleSelfHostedProviderNonInteractive({
|
||||
ctx: params.ctx,
|
||||
providerId: params.providerId,
|
||||
providerLabel: params.providerLabel,
|
||||
defaultBaseUrl: "http://127.0.0.1:8000/v1",
|
||||
defaultApiKeyEnvVar: params.envVar,
|
||||
modelPlaceholder: "Qwen/Qwen3-32B",
|
||||
});
|
||||
}
|
||||
|
||||
describe("configureOpenAICompatibleSelfHostedProviderNonInteractive", () => {
|
||||
it.each([
|
||||
{
|
||||
providerId: "vllm",
|
||||
providerLabel: "vLLM",
|
||||
envVar: "VLLM_API_KEY",
|
||||
baseUrl: "http://127.0.0.1:8100/v1/",
|
||||
apiKey: "vllm-test-key",
|
||||
modelId: "Qwen/Qwen3-8B",
|
||||
},
|
||||
{
|
||||
providerId: "sglang",
|
||||
providerLabel: "SGLang",
|
||||
envVar: "SGLANG_API_KEY",
|
||||
baseUrl: "http://127.0.0.1:31000/v1",
|
||||
apiKey: "sglang-test-key",
|
||||
modelId: "Qwen/Qwen3-32B",
|
||||
},
|
||||
])("configures $providerLabel config and auth profile", async (params) => {
|
||||
const ctx = createContext(params);
|
||||
|
||||
const cfg = await configureSelfHostedTestProvider({
|
||||
ctx,
|
||||
providerId: params.providerId,
|
||||
providerLabel: params.providerLabel,
|
||||
envVar: params.envVar,
|
||||
});
|
||||
|
||||
const profileId = `${params.providerId}:default`;
|
||||
expect(cfg?.auth?.profiles?.[profileId]).toEqual({
|
||||
provider: params.providerId,
|
||||
mode: "api_key",
|
||||
});
|
||||
expect(cfg?.models?.providers?.[params.providerId]).toEqual({
|
||||
baseUrl: params.baseUrl.replace(/\/+$/, ""),
|
||||
api: "openai-completions",
|
||||
apiKey: params.envVar,
|
||||
models: [
|
||||
expect.objectContaining({
|
||||
id: params.modelId,
|
||||
}),
|
||||
],
|
||||
});
|
||||
expect(readPrimaryModel(cfg)).toBe(`${params.providerId}/${params.modelId}`);
|
||||
expect(ctx.resolveApiKey).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
flagName: "--custom-api-key",
|
||||
envVar: params.envVar,
|
||||
}),
|
||||
);
|
||||
expect(upsertAuthProfileWithLock).toHaveBeenCalledWith({
|
||||
profileId,
|
||||
agentDir: ctx.agentDir,
|
||||
credential: {
|
||||
type: "api_key",
|
||||
provider: params.providerId,
|
||||
key: params.apiKey,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("exits without touching auth when custom model id is missing", async () => {
|
||||
const ctx = createContext({
|
||||
providerId: "vllm",
|
||||
apiKey: "vllm-test-key",
|
||||
});
|
||||
|
||||
const cfg = await configureSelfHostedTestProvider({
|
||||
ctx,
|
||||
providerId: "vllm",
|
||||
providerLabel: "vLLM",
|
||||
envVar: "VLLM_API_KEY",
|
||||
});
|
||||
|
||||
expect(cfg).toBeNull();
|
||||
expect(ctx.runtime.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Missing --custom-model-id for --auth-choice vllm."),
|
||||
);
|
||||
expect(ctx.runtime.exit).toHaveBeenCalledWith(1);
|
||||
expect(ctx.resolveApiKey).not.toHaveBeenCalled();
|
||||
expect(upsertAuthProfileWithLock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user