test: lazy-load auth and gateway fixtures

This commit is contained in:
Peter Steinberger
2026-04-12 20:17:12 +01:00
parent c473b174c5
commit f619368769
3 changed files with 108 additions and 51 deletions

View File

@@ -1,12 +1,10 @@
import fs from "node:fs/promises";
import type { OAuthCredentials } from "@mariozechner/pi-ai";
import { afterEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { resolveAgentDir } from "../agents/agent-scope.js";
import type { OpenClawConfig } from "../config/config.js";
import { resolveAgentModelPrimaryValue } from "../config/model-input.js";
import type { ModelProviderConfig } from "../config/types.models.js";
import { createProviderApiKeyAuthMethod } from "../plugins/provider-api-key-auth.js";
import { providerApiKeyAuthRuntime } from "../plugins/provider-api-key-auth.runtime.js";
import type { ProviderAuthMethod, ProviderAuthResult, ProviderPlugin } from "../plugins/types.js";
import type { WizardPrompter } from "../wizard/prompts.js";
import { applyAuthChoice, resolvePreferredProviderForAuthChoice } from "./auth-choice.js";
@@ -23,6 +21,23 @@ import {
type DetectZaiEndpoint = typeof import("../plugins/provider-zai-endpoint.js").detectZaiEndpoint;
let providerApiKeyAuthModulePromise:
| Promise<typeof import("../plugins/provider-api-key-auth.js")>
| undefined;
let providerApiKeyAuthRuntimeModulePromise:
| Promise<typeof import("../plugins/provider-api-key-auth.runtime.js")>
| undefined;
async function getProviderApiKeyAuthModule() {
providerApiKeyAuthModulePromise ??= import("../plugins/provider-api-key-auth.js");
return await providerApiKeyAuthModulePromise;
}
async function getProviderApiKeyAuthRuntimeModule() {
providerApiKeyAuthRuntimeModulePromise ??= import("../plugins/provider-api-key-auth.runtime.js");
return await providerApiKeyAuthRuntimeModulePromise;
}
const GOOGLE_GEMINI_DEFAULT_MODEL = "google/gemini-3.1-pro-preview";
const MINIMAX_CN_API_BASE_URL = "https://api.minimax.chat/v1";
const ZAI_CODING_GLOBAL_BASE_URL = "https://api.z.ai/api/coding/paas/v4";
@@ -110,7 +125,7 @@ function providerConfigPatch(
};
}
function createApiKeyProvider(params: {
async function createApiKeyProvider(params: {
providerId: string;
label: string;
choiceId: string;
@@ -125,7 +140,8 @@ function createApiKeyProvider(params: {
noteMessage?: string;
noteTitle?: string;
applyConfig?: Partial<OpenClawConfig>;
}): ProviderPlugin {
}): Promise<ProviderPlugin> {
const { createProviderApiKeyAuthMethod } = await getProviderApiKeyAuthModule();
return {
id: params.providerId,
label: params.label,
@@ -179,7 +195,8 @@ function createFixedChoiceProvider(params: {
};
}
function createDefaultProviderPlugins() {
async function createDefaultProviderPlugins(): Promise<ProviderPlugin[]> {
const { providerApiKeyAuthRuntime } = await getProviderApiKeyAuthRuntimeModule();
const buildApiKeyCredential = providerApiKeyAuthRuntime.buildApiKeyCredential;
const ensureApiKeyFromOptionEnvOrPrompt =
providerApiKeyAuthRuntime.ensureApiKeyFromOptionEnvOrPrompt;
@@ -344,7 +361,7 @@ function createDefaultProviderPlugins() {
};
return [
createApiKeyProvider({
await createApiKeyProvider({
providerId: "anthropic",
label: "Anthropic API key",
choiceId: "apiKey",
@@ -353,7 +370,7 @@ function createDefaultProviderPlugins() {
envVar: "ANTHROPIC_API_KEY",
promptMessage: "Enter Anthropic API key",
}),
createApiKeyProvider({
await createApiKeyProvider({
providerId: "google",
label: "Gemini API key",
choiceId: "gemini-api-key",
@@ -363,7 +380,7 @@ function createDefaultProviderPlugins() {
promptMessage: "Enter Gemini API key",
defaultModel: GOOGLE_GEMINI_DEFAULT_MODEL,
}),
createApiKeyProvider({
await createApiKeyProvider({
providerId: "huggingface",
label: "Hugging Face API key",
choiceId: "huggingface-api-key",
@@ -373,7 +390,7 @@ function createDefaultProviderPlugins() {
promptMessage: "Enter Hugging Face API key",
defaultModel: "huggingface/Qwen/Qwen3-Coder-480B-A35B-Instruct",
}),
createApiKeyProvider({
await createApiKeyProvider({
providerId: "litellm",
label: "LiteLLM API key",
choiceId: "litellm-api-key",
@@ -383,7 +400,7 @@ function createDefaultProviderPlugins() {
promptMessage: "Enter LiteLLM API key",
defaultModel: "litellm/anthropic/claude-opus-4.6",
}),
createApiKeyProvider({
await createApiKeyProvider({
providerId: "minimax",
label: "MiniMax API key (Global)",
choiceId: "minimax-global-api",
@@ -394,7 +411,7 @@ function createDefaultProviderPlugins() {
profileId: "minimax:global",
defaultModel: "minimax/MiniMax-M2.7",
}),
createApiKeyProvider({
await createApiKeyProvider({
providerId: "minimax",
label: "MiniMax API key (CN)",
choiceId: "minimax-cn-api",
@@ -407,7 +424,7 @@ function createDefaultProviderPlugins() {
applyConfig: providerConfigPatch("minimax", { baseUrl: MINIMAX_CN_API_BASE_URL }),
expectedProviders: ["minimax", "minimax-cn"],
}),
createApiKeyProvider({
await createApiKeyProvider({
providerId: "mistral",
label: "Mistral API key",
choiceId: "mistral-api-key",
@@ -417,7 +434,7 @@ function createDefaultProviderPlugins() {
promptMessage: "Enter Mistral API key",
defaultModel: "mistral/mistral-large-latest",
}),
createApiKeyProvider({
await createApiKeyProvider({
providerId: "moonshot",
label: "Moonshot API key",
choiceId: "moonshot-api-key",
@@ -438,7 +455,7 @@ function createDefaultProviderPlugins() {
run: async () => ({ profiles: [] }),
},
}),
createApiKeyProvider({
await createApiKeyProvider({
providerId: "openai",
label: "OpenAI API key",
choiceId: "openai-api-key",
@@ -448,7 +465,7 @@ function createDefaultProviderPlugins() {
promptMessage: "Enter OpenAI API key",
defaultModel: "openai/gpt-5.4",
}),
createApiKeyProvider({
await createApiKeyProvider({
providerId: "opencode",
label: "OpenCode Zen",
choiceId: "opencode-zen",
@@ -462,7 +479,7 @@ function createDefaultProviderPlugins() {
noteMessage: "OpenCode uses one API key across the Zen and Go catalogs.",
noteTitle: "OpenCode",
}),
createApiKeyProvider({
await createApiKeyProvider({
providerId: "opencode-go",
label: "OpenCode Go",
choiceId: "opencode-go",
@@ -476,7 +493,7 @@ function createDefaultProviderPlugins() {
noteMessage: "OpenCode uses one API key across the Zen and Go catalogs.",
noteTitle: "OpenCode",
}),
createApiKeyProvider({
await createApiKeyProvider({
providerId: "openrouter",
label: "OpenRouter API key",
choiceId: "openrouter-api-key",
@@ -486,7 +503,7 @@ function createDefaultProviderPlugins() {
promptMessage: "Enter OpenRouter API key",
defaultModel: "openrouter/auto",
}),
createApiKeyProvider({
await createApiKeyProvider({
providerId: "qianfan",
label: "Qianfan API key",
choiceId: "qianfan-api-key",
@@ -496,7 +513,7 @@ function createDefaultProviderPlugins() {
promptMessage: "Enter Qianfan API key",
defaultModel: "qianfan/ernie-4.5-8k",
}),
createApiKeyProvider({
await createApiKeyProvider({
providerId: "synthetic",
label: "Synthetic API key",
choiceId: "synthetic-api-key",
@@ -506,7 +523,7 @@ function createDefaultProviderPlugins() {
promptMessage: "Enter Synthetic API key",
defaultModel: "synthetic/Synthetic-1",
}),
createApiKeyProvider({
await createApiKeyProvider({
providerId: "together",
label: "Together API key",
choiceId: "together-api-key",
@@ -516,7 +533,7 @@ function createDefaultProviderPlugins() {
promptMessage: "Enter Together API key",
defaultModel: "together/meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8",
}),
createApiKeyProvider({
await createApiKeyProvider({
providerId: "venice",
label: "Venice AI",
choiceId: "venice-api-key",
@@ -528,7 +545,7 @@ function createDefaultProviderPlugins() {
noteMessage: "Venice is a privacy-focused inference service.",
noteTitle: "Venice AI",
}),
createApiKeyProvider({
await createApiKeyProvider({
providerId: "vercel-ai-gateway",
label: "AI Gateway API key",
choiceId: "ai-gateway-api-key",
@@ -538,7 +555,7 @@ function createDefaultProviderPlugins() {
promptMessage: "Enter AI Gateway API key",
defaultModel: "vercel-ai-gateway/anthropic/claude-opus-4.6",
}),
createApiKeyProvider({
await createApiKeyProvider({
providerId: "xai",
label: "xAI API key",
choiceId: "xai-api-key",
@@ -548,7 +565,7 @@ function createDefaultProviderPlugins() {
promptMessage: "Enter xAI API key",
defaultModel: "xai/grok-4",
}),
createApiKeyProvider({
await createApiKeyProvider({
providerId: "xiaomi",
label: "Xiaomi API key",
choiceId: "xiaomi-api-key",
@@ -573,7 +590,7 @@ function createDefaultProviderPlugins() {
label: "Chutes",
auth: [chutesOAuthMethod],
},
createApiKeyProvider({
await createApiKeyProvider({
providerId: "kimi",
label: "Kimi Code API key",
choiceId: "kimi-code-api-key",
@@ -667,10 +684,17 @@ describe("applyAuthChoice", () => {
return (await readAuthProfiles()).profiles?.[profileId];
}
let defaultProviderPlugins: ProviderPlugin[] = [];
beforeAll(async () => {
defaultProviderPlugins = await createDefaultProviderPlugins();
resolvePluginProviders.mockReturnValue(defaultProviderPlugins);
});
afterEach(async () => {
vi.unstubAllGlobals();
resolvePluginProviders.mockReset();
resolvePluginProviders.mockReturnValue(createDefaultProviderPlugins());
resolvePluginProviders.mockReturnValue(defaultProviderPlugins);
runProviderModelSelectedHook.mockClear();
detectZaiEndpoint.mockReset();
detectZaiEndpoint.mockResolvedValue(null);
@@ -680,8 +704,6 @@ describe("applyAuthChoice", () => {
activeStateDir = null;
});
resolvePluginProviders.mockReturnValue(createDefaultProviderPlugins());
it("applies Anthropic setup-token auth when the provider exposes the setup flow", async () => {
await setupTempState();

View File

@@ -7,11 +7,7 @@ import type { Api, Model } from "@mariozechner/pi-ai";
import { afterEach, describe, expect, it } from "vitest";
import { resolveOpenClawAgentDir } from "../agents/agent-paths.js";
import { resolveAgentWorkspaceDir } from "../agents/agent-scope.js";
import {
type AuthProfileStore,
ensureAuthProfileStore,
saveAuthProfileStore,
} from "../agents/auth-profiles.js";
import type { AuthProfileStore } from "../agents/auth-profiles.js";
import {
collectAnthropicApiKeys,
isAnthropicBillingError,
@@ -33,7 +29,6 @@ import { shouldSuppressBuiltInModel } from "../agents/model-suppression.js";
import { ensureOpenClawModelsJson } from "../agents/models-config.js";
import { isRateLimitErrorMessage } from "../agents/pi-embedded-helpers/errors.js";
import { discoverAuthStorage, discoverModels } from "../agents/pi-model-discovery.js";
import { clearRuntimeConfigSnapshot, loadConfig } from "../config/config.js";
import type { ModelsConfig, OpenClawConfig, ModelProviderConfig } from "../config/types.js";
import { isTruthyEnvValue } from "../infra/env.js";
import { normalizeGoogleModelId } from "../plugin-sdk/google-model-id.js";
@@ -238,6 +233,19 @@ async function withGatewayLiveModelTimeout<T>(operation: Promise<T>, context: st
});
}
let gatewayConfigModulePromise: Promise<typeof import("../config/config.js")> | undefined;
let authProfilesModulePromise: Promise<typeof import("../agents/auth-profiles.js")> | undefined;
async function getGatewayConfigModule() {
gatewayConfigModulePromise ??= import("../config/config.js");
return await gatewayConfigModulePromise;
}
async function getAuthProfilesModule() {
authProfilesModulePromise ??= import("../agents/auth-profiles.js");
return await authProfilesModulePromise;
}
function logProgress(message: string): void {
process.stderr.write(`[live] ${message}\n`);
}
@@ -1230,14 +1238,15 @@ function buildLiveGatewayConfig(params: {
};
}
function sanitizeAuthConfig(params: {
async function sanitizeAuthConfig(params: {
cfg: OpenClawConfig;
agentDir: string;
}): OpenClawConfig["auth"] | undefined {
}): Promise<OpenClawConfig["auth"] | undefined> {
const auth = params.cfg.auth;
if (!auth) {
return auth;
}
const { ensureAuthProfileStore } = await getAuthProfilesModule();
const store = ensureAuthProfileStore(params.agentDir, {
allowKeychainPrompt: false,
});
@@ -1298,7 +1307,7 @@ function buildMinimaxProviderOverride(params: {
}
async function runGatewayModelSuite(params: GatewayModelSuiteParams) {
clearRuntimeConfigSnapshot();
(await getGatewayConfigModule()).clearRuntimeConfigSnapshot();
const runtimeEnv = enterProductionEnvForLiveRun();
const previous = {
configPath: process.env.OPENCLAW_CONFIG_PATH,
@@ -1330,6 +1339,7 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) {
const agentId = "dev";
const hostAgentDir = resolveOpenClawAgentDir();
const { ensureAuthProfileStore, saveAuthProfileStore } = await getAuthProfilesModule();
const hostStore = ensureAuthProfileStore(hostAgentDir, {
allowKeychainPrompt: false,
});
@@ -1363,7 +1373,7 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) {
const agentDir = resolveOpenClawAgentDir();
const sanitizedCfg: OpenClawConfig = {
...params.cfg,
auth: sanitizeAuthConfig({ cfg: params.cfg, agentDir }),
auth: await sanitizeAuthConfig({ cfg: params.cfg, agentDir }),
};
const nextCfg = buildLiveGatewayConfig({
cfg: sanitizedCfg,
@@ -1987,7 +1997,7 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) {
logProgress(`[${params.label}] skipped all models (missing profiles)`);
}
} finally {
clearRuntimeConfigSnapshot();
(await getGatewayConfigModule()).clearRuntimeConfigSnapshot();
restoreProductionEnvForLiveRun(runtimeEnv);
client.stop();
await server.close({ reason: "live test complete" });
@@ -2021,7 +2031,8 @@ describeLive("gateway live (dev agent, profile keys)", () => {
"runs meaningful prompts across models with available keys",
async () =>
await withSuppressedGatewayLiveWarnings(async () => {
clearRuntimeConfigSnapshot();
const { loadConfig } = await getGatewayConfigModule();
(await getGatewayConfigModule()).clearRuntimeConfigSnapshot();
const cfg = loadConfig();
await ensureOpenClawModelsJson(cfg);
@@ -2144,7 +2155,8 @@ describeLive("gateway live (dev agent, profile keys)", () => {
if (!ZAI_FALLBACK) {
return;
}
clearRuntimeConfigSnapshot();
const { loadConfig } = await getGatewayConfigModule();
(await getGatewayConfigModule()).clearRuntimeConfigSnapshot();
const runtimeEnv = enterProductionEnvForLiveRun();
const previous = {
configPath: process.env.OPENCLAW_CONFIG_PATH,
@@ -2302,7 +2314,10 @@ describeLive("gateway live (dev agent, profile keys)", () => {
throw new Error(`zai followup missing nonce: ${followupText}`);
}
} finally {
clearRuntimeConfigSnapshot();
{
const { clearRuntimeConfigSnapshot } = await getGatewayConfigModule();
clearRuntimeConfigSnapshot();
}
restoreProductionEnvForLiveRun(runtimeEnv);
client.stop();
await server.close({ reason: "live test complete" });

View File

@@ -3,18 +3,13 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import type { AssistantMessage, UserMessage } from "@mariozechner/pi-ai";
import { SessionManager } from "@mariozechner/pi-coding-agent";
import { afterAll, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
import { WebSocket } from "ws";
import { clearConfigCache, clearRuntimeConfigSnapshot } from "../config/config.js";
import { loadConfig } from "../config/config.js";
import { withSessionStoreLockForTest } from "../config/sessions/store.js";
import { isSessionPatchEvent, type InternalHookEvent } from "../hooks/internal-hooks.js";
import { withEnvAsync } from "../test-utils/env.js";
import { GATEWAY_CLIENT_IDS, GATEWAY_CLIENT_MODES } from "./protocol/client-info.js";
import { startGatewayServerHarness, type GatewayServerHarness } from "./server.e2e-ws-harness.js";
import { createToolSummaryPreviewTranscriptLines } from "./session-preview.test-helpers.js";
import { resolveGatewaySessionStoreTarget } from "./session-utils.js";
import {
connectOk,
embeddedRunMock,
@@ -27,6 +22,21 @@ import {
writeSessionStore,
} from "./test-helpers.js";
let sessionManagerModulePromise:
| Promise<typeof import("@mariozechner/pi-coding-agent")>
| undefined;
let gatewayConfigModulePromise: Promise<typeof import("../config/config.js")> | undefined;
async function getSessionManagerModule() {
sessionManagerModulePromise ??= import("@mariozechner/pi-coding-agent");
return await sessionManagerModulePromise;
}
async function getGatewayConfigModule() {
gatewayConfigModulePromise ??= import("../config/config.js");
return await gatewayConfigModulePromise;
}
async function getSessionsHandlers() {
return (await import("./server-methods/sessions.js")).sessionsHandlers;
}
@@ -226,7 +236,8 @@ async function writeSingleLineSession(dir: string, sessionId: string, content: s
);
}
function createCheckpointFixture(dir: string) {
async function createCheckpointFixture(dir: string) {
const { SessionManager } = await getSessionManagerModule();
const session = SessionManager.create(dir, dir);
const userMessage: UserMessage = {
role: "user",
@@ -348,7 +359,8 @@ function isInternalHookEvent(value: unknown): value is InternalHookEvent {
}
describe("gateway server sessions", () => {
beforeEach(() => {
beforeEach(async () => {
const { clearConfigCache, clearRuntimeConfigSnapshot } = await getGatewayConfigModule();
clearRuntimeConfigSnapshot();
clearConfigCache();
sessionCleanupMocks.clearSessionQueues.mockClear();
@@ -1296,7 +1308,8 @@ describe("gateway server sessions", () => {
test("sessions.compaction.* lists checkpoints and branches or restores from pre-compaction snapshots", async () => {
const { dir, storePath } = await createSessionStoreDir();
const fixture = createCheckpointFixture(dir);
const fixture = await createCheckpointFixture(dir);
const { SessionManager } = await getSessionManagerModule();
await writeSessionStore({
entries: {
main: {
@@ -1514,6 +1527,7 @@ describe("gateway server sessions", () => {
);
await withEnvAsync({ OPENCLAW_CONFIG_PATH: undefined }, async () => {
const { clearConfigCache, clearRuntimeConfigSnapshot } = await getGatewayConfigModule();
clearConfigCache();
clearRuntimeConfigSnapshot();
const cfg = {
@@ -3129,6 +3143,12 @@ describe("gateway server sessions", () => {
});
beforeResetHookState.hasBeforeResetHook = true;
const [{ loadConfig }, { resolveGatewaySessionStoreTarget }, { withSessionStoreLockForTest }] =
await Promise.all([
import("../config/config.js"),
import("./session-utils.js"),
import("../config/sessions/store.js"),
]);
const gatewayStorePath = resolveGatewaySessionStoreTarget({
cfg: loadConfig(),
key: "main",