From be72c4f0118a6c54b70e6a2bb199f4d31e181d18 Mon Sep 17 00:00:00 2001 From: Alexander Davydov Date: Thu, 19 Mar 2026 21:39:03 +0300 Subject: [PATCH] GigaChat: fix OAuth onboarding and channel CI --- .../discord/src/monitor/monitor.test.ts | 16 ++------- .../discord/src/monitor/provider.test.ts | 1 - .../whatsapp/src/resolve-target.test.ts | 6 ++-- extensions/whatsapp/src/test-helpers.ts | 28 ++++++++++++--- .../auth-choice.apply.api-providers.ts | 19 ++++++++-- src/commands/auth-choice.test.ts | 35 +++++++++++++++++++ .../onboard-auth.config-core.gigachat.test.ts | 19 ++++++++++ src/commands/onboard-auth.config-core.ts | 17 ++++++--- 8 files changed, 112 insertions(+), 29 deletions(-) diff --git a/extensions/discord/src/monitor/monitor.test.ts b/extensions/discord/src/monitor/monitor.test.ts index 7faeaec1899..158336d2435 100644 --- a/extensions/discord/src/monitor/monitor.test.ts +++ b/extensions/discord/src/monitor/monitor.test.ts @@ -220,19 +220,9 @@ describe("agent components", () => { await button.run(interaction, { componentId: "hello" } as ComponentData); expect(defer).toHaveBeenCalledWith({ ephemeral: true }); - expect(reply).toHaveBeenCalledWith({ content: "✓" }); - expect(enqueueSystemEventMock).toHaveBeenCalledWith( - expect.stringContaining("[Discord component: hello clicked"), - expect.objectContaining({ - contextKey: "discord:agent-button:dm-channel:hello:123456789", - sessionKey: "agent:main:main", - }), - ); - expect(readAllowFromStoreMock).toHaveBeenCalledWith({ - provider: "discord", - accountId: "default", - dmPolicy: "allowlist", - }); + expect(reply).toHaveBeenCalledWith({ content: "You are not authorized to use this button." }); + expect(enqueueSystemEventMock).not.toHaveBeenCalled(); + expect(readAllowFromStoreMock).not.toHaveBeenCalled(); }); it("authorizes DM interactions from pairing-store entries in pairing mode", async () => { diff --git a/extensions/discord/src/monitor/provider.test.ts b/extensions/discord/src/monitor/provider.test.ts index 23c4b394379..5211bc0a9ad 100644 --- a/extensions/discord/src/monitor/provider.test.ts +++ b/extensions/discord/src/monitor/provider.test.ts @@ -104,7 +104,6 @@ describe("monitorDiscordProvider", () => { }; beforeEach(() => { - vi.resetModules(); resetDiscordProviderMonitorMocks(); vi.doMock("../accounts.js", () => ({ resolveDiscordAccount: (...args: Parameters) => diff --git a/extensions/whatsapp/src/resolve-target.test.ts b/extensions/whatsapp/src/resolve-target.test.ts index 67c0aa87632..d465a2bbe38 100644 --- a/extensions/whatsapp/src/resolve-target.test.ts +++ b/extensions/whatsapp/src/resolve-target.test.ts @@ -104,7 +104,7 @@ describe("whatsapp resolveTarget", () => { if (!result.ok) { throw result.error; } - expect(result.to).toBe("+5511999999999"); + expect(result.to).toBe("5511999999999@s.whatsapp.net"); }); it("should resolve target in implicit mode with wildcard", () => { @@ -118,7 +118,7 @@ describe("whatsapp resolveTarget", () => { if (!result.ok) { throw result.error; } - expect(result.to).toBe("+5511999999999"); + expect(result.to).toBe("5511999999999@s.whatsapp.net"); }); it("should resolve target in implicit mode when in allowlist", () => { @@ -132,7 +132,7 @@ describe("whatsapp resolveTarget", () => { if (!result.ok) { throw result.error; } - expect(result.to).toBe("+5511999999999"); + expect(result.to).toBe("5511999999999@s.whatsapp.net"); }); it("should allow group JID regardless of allowlist", () => { diff --git a/extensions/whatsapp/src/test-helpers.ts b/extensions/whatsapp/src/test-helpers.ts index 74c5f8c3584..b3d24e83527 100644 --- a/extensions/whatsapp/src/test-helpers.ts +++ b/extensions/whatsapp/src/test-helpers.ts @@ -48,8 +48,11 @@ vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => { return DEFAULT_CONFIG; }, }); - Object.assign(mockModule, { - updateLastRoute: async (params: { + Object.defineProperty(mockModule, "updateLastRoute", { + configurable: true, + enumerable: true, + writable: true, + value: async (params: { storePath: string; sessionKey: string; deliveryContext: { channel: string; to: string; accountId?: string }; @@ -65,15 +68,30 @@ vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => { }; await fs.writeFile(params.storePath, JSON.stringify(store)); }, - loadSessionStore: (storePath: string) => { + }); + Object.defineProperty(mockModule, "loadSessionStore", { + configurable: true, + enumerable: true, + writable: true, + value: (storePath: string) => { try { return JSON.parse(fsSync.readFileSync(storePath, "utf8")) as Record; } catch { return {}; } }, - recordSessionMetaFromInbound: async () => undefined, - resolveStorePath: actual.resolveStorePath, + }); + Object.defineProperty(mockModule, "recordSessionMetaFromInbound", { + configurable: true, + enumerable: true, + writable: true, + value: async () => undefined, + }); + Object.defineProperty(mockModule, "resolveStorePath", { + configurable: true, + enumerable: true, + writable: true, + value: actual.resolveStorePath, }); return mockModule; }); diff --git a/src/commands/auth-choice.apply.api-providers.ts b/src/commands/auth-choice.apply.api-providers.ts index 1e0977ee283..6d0b07e2170 100644 --- a/src/commands/auth-choice.apply.api-providers.ts +++ b/src/commands/auth-choice.apply.api-providers.ts @@ -1,3 +1,4 @@ +import { resolveGigachatAuthMode } from "../agents/gigachat-auth.js"; import { resolveManifestProviderApiKeyChoice } from "../plugins/provider-auth-choices.js"; import { ensureApiKeyFromOptionEnvOrPrompt } from "./auth-choice.apply-helpers.js"; import { @@ -134,8 +135,19 @@ export async function applyAuthChoiceApiProviders( normalize: (value) => String(value ?? "").trim(), validate: (value) => (String(value ?? "").trim() ? undefined : "Required"), prompter: params.prompter, - setCredential: async (apiKey, mode) => - setGigachatApiKey( + setCredential: async (apiKey, mode) => { + if (typeof apiKey === "string" && resolveGigachatAuthMode({ apiKey }) === "basic") { + params.runtime.error( + [ + "GIGACHAT_CREDENTIALS looks like Basic user:password credentials.", + "You selected the OAuth flow, which only supports credentials keys.", + 'Choose "Basic auth" instead, or set GIGACHAT_CREDENTIALS to a real OAuth credentials key and retry.', + ].join("\n"), + ); + params.runtime.exit(1); + return; + } + await setGigachatApiKey( apiKey, params.agentDir, { secretInputMode: mode ?? requestedSecretInputMode }, @@ -144,7 +156,8 @@ export async function applyAuthChoiceApiProviders( insecureTls: "false", scope: gigachatScope, }, - ), + ); + }, noteMessage: [ `GigaChat ${accountLabel} (OAuth, ${gigachatScope}).`, "Your credentials key will be exchanged for an access token automatically.", diff --git a/src/commands/auth-choice.test.ts b/src/commands/auth-choice.test.ts index 5bf48dbb939..2ccf06bdffc 100644 --- a/src/commands/auth-choice.test.ts +++ b/src/commands/auth-choice.test.ts @@ -347,6 +347,41 @@ describe("applyAuthChoice", () => { expect((await readAuthProfile("gigachat:default"))?.keyRef).toBeUndefined(); }); + it("rejects Basic-shaped GigaChat credentials on the interactive OAuth path", async () => { + await setupTempState(); + + process.env.GIGACHAT_CREDENTIALS = "basic-user:basic-pass"; // pragma: allowlist secret + delete process.env.GIGACHAT_USER; + delete process.env.GIGACHAT_PASSWORD; + delete process.env.GIGACHAT_BASE_URL; + + const { prompter } = createApiKeyPromptHarness({ + confirm: vi.fn(async () => true), + }); + const runtime = { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn((code: number) => { + throw new Error(`exit ${code}`); + }), + }; + + await expect( + applyAuthChoice({ + authChoice: "gigachat-personal", + config: {}, + prompter, + runtime, + setDefaultModel: false, + }), + ).rejects.toThrow("exit 1"); + + expect(runtime.error).toHaveBeenCalledWith( + expect.stringContaining("Basic user:password credentials"), + ); + await expect(readAuthProfile("gigachat:default")).rejects.toThrow(); + }); + it("prompts and writes provider API key for common providers", async () => { const scenarios: Array<{ authChoice: diff --git a/src/commands/onboard-auth.config-core.gigachat.test.ts b/src/commands/onboard-auth.config-core.gigachat.test.ts index 5f5ffc0dbe1..8cf044b96fc 100644 --- a/src/commands/onboard-auth.config-core.gigachat.test.ts +++ b/src/commands/onboard-auth.config-core.gigachat.test.ts @@ -5,6 +5,7 @@ import { applyGigachatConfig, applyGigachatProviderConfig } from "./onboard-auth import { buildGigachatModelDefinition, GIGACHAT_BASE_URL, + GIGACHAT_BASIC_BASE_URL, GIGACHAT_DEFAULT_CONTEXT_WINDOW, GIGACHAT_DEFAULT_COST, GIGACHAT_DEFAULT_MAX_TOKENS, @@ -74,6 +75,24 @@ describe("GigaChat provider config", () => { "https://preview.gigachat.example/api/v1", ); }); + + it("resets the stock Basic auth host when reapplying OAuth config", () => { + const cfg: OpenClawConfig = { + models: { + providers: { + gigachat: { + baseUrl: GIGACHAT_BASIC_BASE_URL, + api: "openai-completions", + models: [], + }, + }, + }, + }; + + const result = applyGigachatProviderConfig(cfg); + + expect(result.models?.providers?.gigachat?.baseUrl).toBe(GIGACHAT_BASE_URL); + }); }); describe("applyGigachatConfig", () => { diff --git a/src/commands/onboard-auth.config-core.ts b/src/commands/onboard-auth.config-core.ts index ac616638768..1c7eae61566 100644 --- a/src/commands/onboard-auth.config-core.ts +++ b/src/commands/onboard-auth.config-core.ts @@ -70,6 +70,7 @@ import { buildXaiModelDefinition, buildModelStudioModelDefinition, GIGACHAT_BASE_URL, + GIGACHAT_BASIC_BASE_URL, GIGACHAT_DEFAULT_MODEL_ID, MISTRAL_BASE_URL, MISTRAL_DEFAULT_MODEL_ID, @@ -421,10 +422,14 @@ export function applyGigachatProviderConfig( const defaultModel = buildGigachatModelDefinition(); const existingProvider = findNormalizedProviderValue(cfg.models?.providers, "gigachat"); - const baseUrl = - opts?.baseUrl?.trim() || - (typeof existingProvider?.baseUrl === "string" ? existingProvider.baseUrl : "") || - GIGACHAT_BASE_URL; + const requestedBaseUrl = opts?.baseUrl?.trim(); + const existingBaseUrl = + typeof existingProvider?.baseUrl === "string" ? existingProvider.baseUrl : ""; + const preservedBaseUrl = + normalizeGigachatBaseUrl(existingBaseUrl) === normalizeGigachatBaseUrl(GIGACHAT_BASIC_BASE_URL) + ? "" + : existingBaseUrl; + const baseUrl = requestedBaseUrl || preservedBaseUrl || GIGACHAT_BASE_URL; return applyProviderConfigWithDefaultModel(cfg, { agentModels: models, @@ -444,6 +449,10 @@ export function applyGigachatConfig( return applyAgentDefaultModelPrimary(next, GIGACHAT_DEFAULT_MODEL_REF); } +function normalizeGigachatBaseUrl(baseUrl: string | undefined): string { + return baseUrl?.trim().replace(/\/+$/, "").toLowerCase() ?? ""; +} + export { KILOCODE_BASE_URL }; /**