GigaChat: fix OAuth onboarding and channel CI

This commit is contained in:
Alexander Davydov 2026-03-19 21:39:03 +03:00
parent 21c4a3bec5
commit be72c4f011
8 changed files with 112 additions and 29 deletions

View File

@ -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 () => {

View File

@ -104,7 +104,6 @@ describe("monitorDiscordProvider", () => {
};
beforeEach(() => {
vi.resetModules();
resetDiscordProviderMonitorMocks();
vi.doMock("../accounts.js", () => ({
resolveDiscordAccount: (...args: Parameters<typeof resolveDiscordAccountMock>) =>

View File

@ -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", () => {

View File

@ -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<string, unknown>;
} 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;
});

View File

@ -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.",

View File

@ -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:

View File

@ -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", () => {

View File

@ -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 };
/**