openclaw/extensions/feishu/src/client.test.ts

325 lines
11 KiB
TypeScript
Raw Normal View History

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { FeishuConfig, ResolvedFeishuAccount } from "./types.js";
const wsClientCtorMock = vi.hoisted(() =>
vi.fn(function wsClientCtor() {
return { connected: true };
}),
);
const httpsProxyAgentCtorMock = vi.hoisted(() =>
vi.fn(function httpsProxyAgentCtor(proxyUrl: string) {
return { proxyUrl };
}),
);
const mockBaseHttpInstance = vi.hoisted(() => ({
request: vi.fn().mockResolvedValue({}),
get: vi.fn().mockResolvedValue({}),
post: vi.fn().mockResolvedValue({}),
put: vi.fn().mockResolvedValue({}),
patch: vi.fn().mockResolvedValue({}),
delete: vi.fn().mockResolvedValue({}),
head: vi.fn().mockResolvedValue({}),
options: vi.fn().mockResolvedValue({}),
}));
vi.mock("@larksuiteoapi/node-sdk", () => ({
AppType: { SelfBuild: "self" },
Domain: { Feishu: "https://open.feishu.cn", Lark: "https://open.larksuite.com" },
LoggerLevel: { info: "info" },
Client: vi.fn(),
WSClient: wsClientCtorMock,
EventDispatcher: vi.fn(),
defaultHttpInstance: mockBaseHttpInstance,
}));
vi.mock("https-proxy-agent", () => ({
HttpsProxyAgent: httpsProxyAgentCtorMock,
}));
import { Client as LarkClient } from "@larksuiteoapi/node-sdk";
import {
createFeishuClient,
createFeishuWSClient,
clearClientCache,
FEISHU_HTTP_TIMEOUT_MS,
FEISHU_HTTP_TIMEOUT_MAX_MS,
FEISHU_HTTP_TIMEOUT_ENV_VAR,
} from "./client.js";
const proxyEnvKeys = ["https_proxy", "HTTPS_PROXY", "http_proxy", "HTTP_PROXY"] as const;
type ProxyEnvKey = (typeof proxyEnvKeys)[number];
let priorProxyEnv: Partial<Record<ProxyEnvKey, string | undefined>> = {};
let priorFeishuTimeoutEnv: string | undefined;
const baseAccount: ResolvedFeishuAccount = {
accountId: "main",
selectionSource: "explicit",
enabled: true,
configured: true,
appId: "app_123",
appSecret: "secret_123", // pragma: allowlist secret
domain: "feishu",
config: {} as FeishuConfig,
};
function firstWsClientOptions(): { agent?: unknown } {
const calls = wsClientCtorMock.mock.calls as unknown as Array<[options: { agent?: unknown }]>;
return calls[0]?.[0] ?? {};
}
beforeEach(() => {
priorProxyEnv = {};
priorFeishuTimeoutEnv = process.env[FEISHU_HTTP_TIMEOUT_ENV_VAR];
delete process.env[FEISHU_HTTP_TIMEOUT_ENV_VAR];
for (const key of proxyEnvKeys) {
priorProxyEnv[key] = process.env[key];
delete process.env[key];
}
vi.clearAllMocks();
});
afterEach(() => {
for (const key of proxyEnvKeys) {
const value = priorProxyEnv[key];
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
if (priorFeishuTimeoutEnv === undefined) {
delete process.env[FEISHU_HTTP_TIMEOUT_ENV_VAR];
} else {
process.env[FEISHU_HTTP_TIMEOUT_ENV_VAR] = priorFeishuTimeoutEnv;
}
});
describe("createFeishuClient HTTP timeout", () => {
beforeEach(() => {
clearClientCache();
});
const getLastClientHttpInstance = () => {
const calls = (LarkClient as unknown as ReturnType<typeof vi.fn>).mock.calls;
const lastCall = calls[calls.length - 1]?.[0] as
| { httpInstance?: { get: (...args: unknown[]) => Promise<unknown> } }
| undefined;
return lastCall?.httpInstance;
};
const expectGetCallTimeout = async (timeout: number) => {
const httpInstance = getLastClientHttpInstance();
expect(httpInstance).toBeDefined();
await httpInstance?.get("https://example.com/api");
expect(mockBaseHttpInstance.get).toHaveBeenCalledWith(
"https://example.com/api",
expect.objectContaining({ timeout }),
);
};
it("passes a custom httpInstance with default timeout to Lark.Client", () => {
createFeishuClient({ appId: "app_1", appSecret: "secret_1", accountId: "timeout-test" }); // pragma: allowlist secret
const calls = (LarkClient as unknown as ReturnType<typeof vi.fn>).mock.calls;
const lastCall = calls[calls.length - 1][0] as { httpInstance?: unknown };
expect(lastCall.httpInstance).toBeDefined();
});
it("injects default timeout into HTTP request options", async () => {
createFeishuClient({ appId: "app_2", appSecret: "secret_2", accountId: "timeout-inject" }); // pragma: allowlist secret
const calls = (LarkClient as unknown as ReturnType<typeof vi.fn>).mock.calls;
const lastCall = calls[calls.length - 1][0] as {
httpInstance: { post: (...args: unknown[]) => Promise<unknown> };
};
const httpInstance = lastCall.httpInstance;
await httpInstance.post(
"https://example.com/api",
{ data: 1 },
{ headers: { "X-Custom": "yes" } },
);
expect(mockBaseHttpInstance.post).toHaveBeenCalledWith(
"https://example.com/api",
{ data: 1 },
expect.objectContaining({ timeout: FEISHU_HTTP_TIMEOUT_MS, headers: { "X-Custom": "yes" } }),
);
});
it("allows explicit timeout override per-request", async () => {
createFeishuClient({ appId: "app_3", appSecret: "secret_3", accountId: "timeout-override" }); // pragma: allowlist secret
const calls = (LarkClient as unknown as ReturnType<typeof vi.fn>).mock.calls;
const lastCall = calls[calls.length - 1][0] as {
httpInstance: { get: (...args: unknown[]) => Promise<unknown> };
};
const httpInstance = lastCall.httpInstance;
await httpInstance.get("https://example.com/api", { timeout: 5_000 });
expect(mockBaseHttpInstance.get).toHaveBeenCalledWith(
"https://example.com/api",
expect.objectContaining({ timeout: 5_000 }),
);
});
it("uses config-configured default timeout when provided", async () => {
createFeishuClient({
appId: "app_4",
appSecret: "secret_4", // pragma: allowlist secret
accountId: "timeout-config",
config: { httpTimeoutMs: 45_000 },
});
await expectGetCallTimeout(45_000);
});
it("falls back to default timeout when configured timeout is invalid", async () => {
createFeishuClient({
appId: "app_5",
appSecret: "secret_5", // pragma: allowlist secret
accountId: "timeout-config-invalid",
config: { httpTimeoutMs: -1 },
});
await expectGetCallTimeout(FEISHU_HTTP_TIMEOUT_MS);
});
Secrets: add inline allowlist review set (#38314) * Secrets: add inline allowlist review set * Secrets: narrow detect-secrets file exclusions * Secrets: exclude Docker fingerprint false positive * Secrets: allowlist test and docs false positives * Secrets: refresh baseline after allowlist updates * Secrets: fix gateway chat fixture pragma * Secrets: format pre-commit config * Android: keep talk mode fixture JSON valid * Feishu: rely on client timeout injection * Secrets: allowlist provider auth test fixtures * Secrets: allowlist onboard search fixtures * Secrets: allowlist onboard mode fixture * Secrets: allowlist gateway auth mode fixture * Secrets: allowlist APNS wake test key * Secrets: allowlist gateway reload fixtures * Secrets: allowlist moonshot video fixture * Secrets: allowlist auto audio fixture * Secrets: allowlist tiny audio fixture * Secrets: allowlist embeddings fixtures * Secrets: allowlist resolve fixtures * Secrets: allowlist target registry pattern fixtures * Secrets: allowlist gateway chat env fixture * Secrets: refresh baseline after fixture allowlists * Secrets: reapply gateway chat env allowlist * Secrets: reapply gateway chat env allowlist * Secrets: stabilize gateway chat env allowlist * Secrets: allowlist runtime snapshot save fixture * Secrets: allowlist oauth profile fixtures * Secrets: allowlist compaction identifier fixture * Secrets: allowlist model auth fixture * Secrets: allowlist model status fixtures * Secrets: allowlist custom onboarding fixture * Secrets: allowlist mattermost token summary fixtures * Secrets: allowlist gateway auth suite fixtures * Secrets: allowlist channel summary fixture * Secrets: allowlist provider usage auth fixtures * Secrets: allowlist media proxy fixture * Secrets: allowlist secrets audit fixtures * Secrets: refresh baseline after final fixture allowlists * Feishu: prefer explicit client timeout * Feishu: test direct timeout precedence
2026-03-06 19:35:26 -05:00
it("uses env timeout override when provided and no direct timeout is set", async () => {
process.env[FEISHU_HTTP_TIMEOUT_ENV_VAR] = "60000";
createFeishuClient({
appId: "app_8",
appSecret: "secret_8", // pragma: allowlist secret
accountId: "timeout-env-override",
config: { httpTimeoutMs: 45_000 },
});
await expectGetCallTimeout(60_000);
});
Secrets: add inline allowlist review set (#38314) * Secrets: add inline allowlist review set * Secrets: narrow detect-secrets file exclusions * Secrets: exclude Docker fingerprint false positive * Secrets: allowlist test and docs false positives * Secrets: refresh baseline after allowlist updates * Secrets: fix gateway chat fixture pragma * Secrets: format pre-commit config * Android: keep talk mode fixture JSON valid * Feishu: rely on client timeout injection * Secrets: allowlist provider auth test fixtures * Secrets: allowlist onboard search fixtures * Secrets: allowlist onboard mode fixture * Secrets: allowlist gateway auth mode fixture * Secrets: allowlist APNS wake test key * Secrets: allowlist gateway reload fixtures * Secrets: allowlist moonshot video fixture * Secrets: allowlist auto audio fixture * Secrets: allowlist tiny audio fixture * Secrets: allowlist embeddings fixtures * Secrets: allowlist resolve fixtures * Secrets: allowlist target registry pattern fixtures * Secrets: allowlist gateway chat env fixture * Secrets: refresh baseline after fixture allowlists * Secrets: reapply gateway chat env allowlist * Secrets: reapply gateway chat env allowlist * Secrets: stabilize gateway chat env allowlist * Secrets: allowlist runtime snapshot save fixture * Secrets: allowlist oauth profile fixtures * Secrets: allowlist compaction identifier fixture * Secrets: allowlist model auth fixture * Secrets: allowlist model status fixtures * Secrets: allowlist custom onboarding fixture * Secrets: allowlist mattermost token summary fixtures * Secrets: allowlist gateway auth suite fixtures * Secrets: allowlist channel summary fixture * Secrets: allowlist provider usage auth fixtures * Secrets: allowlist media proxy fixture * Secrets: allowlist secrets audit fixtures * Secrets: refresh baseline after final fixture allowlists * Feishu: prefer explicit client timeout * Feishu: test direct timeout precedence
2026-03-06 19:35:26 -05:00
it("prefers direct timeout over env override", async () => {
process.env[FEISHU_HTTP_TIMEOUT_ENV_VAR] = "60000";
createFeishuClient({
appId: "app_10",
appSecret: "secret_10", // pragma: allowlist secret
Secrets: add inline allowlist review set (#38314) * Secrets: add inline allowlist review set * Secrets: narrow detect-secrets file exclusions * Secrets: exclude Docker fingerprint false positive * Secrets: allowlist test and docs false positives * Secrets: refresh baseline after allowlist updates * Secrets: fix gateway chat fixture pragma * Secrets: format pre-commit config * Android: keep talk mode fixture JSON valid * Feishu: rely on client timeout injection * Secrets: allowlist provider auth test fixtures * Secrets: allowlist onboard search fixtures * Secrets: allowlist onboard mode fixture * Secrets: allowlist gateway auth mode fixture * Secrets: allowlist APNS wake test key * Secrets: allowlist gateway reload fixtures * Secrets: allowlist moonshot video fixture * Secrets: allowlist auto audio fixture * Secrets: allowlist tiny audio fixture * Secrets: allowlist embeddings fixtures * Secrets: allowlist resolve fixtures * Secrets: allowlist target registry pattern fixtures * Secrets: allowlist gateway chat env fixture * Secrets: refresh baseline after fixture allowlists * Secrets: reapply gateway chat env allowlist * Secrets: reapply gateway chat env allowlist * Secrets: stabilize gateway chat env allowlist * Secrets: allowlist runtime snapshot save fixture * Secrets: allowlist oauth profile fixtures * Secrets: allowlist compaction identifier fixture * Secrets: allowlist model auth fixture * Secrets: allowlist model status fixtures * Secrets: allowlist custom onboarding fixture * Secrets: allowlist mattermost token summary fixtures * Secrets: allowlist gateway auth suite fixtures * Secrets: allowlist channel summary fixture * Secrets: allowlist provider usage auth fixtures * Secrets: allowlist media proxy fixture * Secrets: allowlist secrets audit fixtures * Secrets: refresh baseline after final fixture allowlists * Feishu: prefer explicit client timeout * Feishu: test direct timeout precedence
2026-03-06 19:35:26 -05:00
accountId: "timeout-direct-override",
httpTimeoutMs: 120_000,
config: { httpTimeoutMs: 45_000 },
});
await expectGetCallTimeout(120_000);
Secrets: add inline allowlist review set (#38314) * Secrets: add inline allowlist review set * Secrets: narrow detect-secrets file exclusions * Secrets: exclude Docker fingerprint false positive * Secrets: allowlist test and docs false positives * Secrets: refresh baseline after allowlist updates * Secrets: fix gateway chat fixture pragma * Secrets: format pre-commit config * Android: keep talk mode fixture JSON valid * Feishu: rely on client timeout injection * Secrets: allowlist provider auth test fixtures * Secrets: allowlist onboard search fixtures * Secrets: allowlist onboard mode fixture * Secrets: allowlist gateway auth mode fixture * Secrets: allowlist APNS wake test key * Secrets: allowlist gateway reload fixtures * Secrets: allowlist moonshot video fixture * Secrets: allowlist auto audio fixture * Secrets: allowlist tiny audio fixture * Secrets: allowlist embeddings fixtures * Secrets: allowlist resolve fixtures * Secrets: allowlist target registry pattern fixtures * Secrets: allowlist gateway chat env fixture * Secrets: refresh baseline after fixture allowlists * Secrets: reapply gateway chat env allowlist * Secrets: reapply gateway chat env allowlist * Secrets: stabilize gateway chat env allowlist * Secrets: allowlist runtime snapshot save fixture * Secrets: allowlist oauth profile fixtures * Secrets: allowlist compaction identifier fixture * Secrets: allowlist model auth fixture * Secrets: allowlist model status fixtures * Secrets: allowlist custom onboarding fixture * Secrets: allowlist mattermost token summary fixtures * Secrets: allowlist gateway auth suite fixtures * Secrets: allowlist channel summary fixture * Secrets: allowlist provider usage auth fixtures * Secrets: allowlist media proxy fixture * Secrets: allowlist secrets audit fixtures * Secrets: refresh baseline after final fixture allowlists * Feishu: prefer explicit client timeout * Feishu: test direct timeout precedence
2026-03-06 19:35:26 -05:00
});
it("clamps env timeout override to max bound", async () => {
process.env[FEISHU_HTTP_TIMEOUT_ENV_VAR] = String(FEISHU_HTTP_TIMEOUT_MAX_MS + 123_456);
createFeishuClient({
appId: "app_9",
appSecret: "secret_9", // pragma: allowlist secret
accountId: "timeout-env-clamp",
});
await expectGetCallTimeout(FEISHU_HTTP_TIMEOUT_MAX_MS);
});
it("recreates cached client when configured timeout changes", async () => {
createFeishuClient({
appId: "app_6",
appSecret: "secret_6", // pragma: allowlist secret
accountId: "timeout-cache-change",
config: { httpTimeoutMs: 30_000 },
});
createFeishuClient({
appId: "app_6",
appSecret: "secret_6", // pragma: allowlist secret
accountId: "timeout-cache-change",
config: { httpTimeoutMs: 45_000 },
});
const calls = (LarkClient as unknown as ReturnType<typeof vi.fn>).mock.calls;
expect(calls.length).toBe(2);
const lastCall = calls[calls.length - 1][0] as {
httpInstance: { get: (...args: unknown[]) => Promise<unknown> };
};
await lastCall.httpInstance.get("https://example.com/api");
expect(mockBaseHttpInstance.get).toHaveBeenCalledWith(
"https://example.com/api",
expect.objectContaining({ timeout: 45_000 }),
);
});
});
describe("createFeishuWSClient proxy handling", () => {
it("does not set a ws proxy agent when proxy env is absent", () => {
createFeishuWSClient(baseAccount);
expect(httpsProxyAgentCtorMock).not.toHaveBeenCalled();
const options = firstWsClientOptions();
expect(options?.agent).toBeUndefined();
});
it("uses proxy env precedence: https_proxy first, then HTTPS_PROXY, then http_proxy/HTTP_PROXY", () => {
// NOTE: On Windows, environment variables are case-insensitive, so it's not
// possible to set both https_proxy and HTTPS_PROXY to different values.
// Keep this test cross-platform by asserting precedence via mutually-exclusive
// setups.
process.env.https_proxy = "http://lower-https:8001";
process.env.http_proxy = "http://lower-http:8003";
process.env.HTTP_PROXY = "http://upper-http:8004";
createFeishuWSClient(baseAccount);
// On Windows env keys are case-insensitive, so setting HTTPS_PROXY may
// overwrite https_proxy. We assert https proxies still win over http.
const expectedProxy = process.env.https_proxy || process.env.HTTPS_PROXY;
expect(expectedProxy).toBeTruthy();
expect(httpsProxyAgentCtorMock).toHaveBeenCalledTimes(1);
expect(httpsProxyAgentCtorMock).toHaveBeenCalledWith(expectedProxy);
const options = firstWsClientOptions();
expect(options.agent).toEqual({ proxyUrl: expectedProxy });
});
feat(secrets): expand SecretRef coverage across user-supplied credentials (#29580) * feat(secrets): expand secret target coverage and gateway tooling * docs(secrets): align gateway and CLI secret docs * chore(protocol): regenerate swift gateway models for secrets methods * fix(config): restore talk apiKey fallback and stabilize runner test * ci(windows): reduce test worker count for shard stability * ci(windows): raise node heap for test shard stability * test(feishu): make proxy env precedence assertion windows-safe * fix(gateway): resolve auth password SecretInput refs for clients * fix(gateway): resolve remote SecretInput credentials for clients * fix(secrets): skip inactive refs in command snapshot assignments * fix(secrets): scope gateway.remote refs to effective auth surfaces * fix(secrets): ignore memory defaults when enabled agents disable search * fix(secrets): honor Google Chat serviceAccountRef inheritance * fix(secrets): address tsgo errors in command and gateway collectors * fix(secrets): avoid auth-store load in providers-only configure * fix(gateway): defer local password ref resolution by precedence * fix(secrets): gate telegram webhook secret refs by webhook mode * fix(secrets): gate slack signing secret refs to http mode * fix(secrets): skip telegram botToken refs when tokenFile is set * fix(secrets): gate discord pluralkit refs by enabled flag * fix(secrets): gate discord voice tts refs by voice enabled * test(secrets): make runtime fixture modes explicit * fix(cli): resolve local qr password secret refs * fix(cli): fail when gateway leaves command refs unresolved * fix(gateway): fail when local password SecretRef is unresolved * fix(gateway): fail when required remote SecretRefs are unresolved * fix(gateway): resolve local password refs only when password can win * fix(cli): skip local password SecretRef resolution on qr token override * test(gateway): cast SecretRef fixtures to OpenClawConfig * test(secrets): activate mode-gated targets in runtime coverage fixture * fix(cron): support SecretInput webhook tokens safely * fix(bluebubbles): support SecretInput passwords across config paths * fix(msteams): make appPassword SecretInput-safe in onboarding/token paths * fix(bluebubbles): align SecretInput schema helper typing * fix(cli): clarify secrets.resolve version-skew errors * refactor(secrets): return structured inactive paths from secrets.resolve * refactor(gateway): type onboarding secret writes as SecretInput * chore(protocol): regenerate swift models for secrets.resolve * feat(secrets): expand extension credential secretref support * fix(secrets): gate web-search refs by active provider * fix(onboarding): detect SecretRef credentials in extension status * fix(onboarding): allow keeping existing ref in secret prompt * fix(onboarding): resolve gateway password SecretRefs for probe and tui * fix(onboarding): honor secret-input-mode for local gateway auth * fix(acp): resolve gateway SecretInput credentials * fix(secrets): gate gateway.remote refs to remote surfaces * test(secrets): cover pattern matching and inactive array refs * docs(secrets): clarify secrets.resolve and remote active surfaces * fix(bluebubbles): keep existing SecretRef during onboarding * fix(tests): resolve CI type errors in new SecretRef coverage * fix(extensions): replace raw fetch with SSRF-guarded fetch * test(secrets): mark gateway remote targets active in runtime coverage * test(infra): normalize home-prefix expectation across platforms * fix(cli): only resolve local qr password refs in password mode * test(cli): cover local qr token mode with unresolved password ref * docs(cli): clarify local qr password ref resolution behavior * refactor(extensions): reuse sdk SecretInput helpers * fix(wizard): resolve onboarding env-template secrets before plaintext * fix(cli): surface secrets.resolve diagnostics in memory and qr * test(secrets): repair post-rebase runtime and fixtures * fix(gateway): skip remote password ref resolution when token wins * fix(secrets): treat tailscale remote gateway refs as active * fix(gateway): allow remote password fallback when token ref is unresolved * fix(gateway): ignore stale local password refs for none and trusted-proxy * fix(gateway): skip remote secret ref resolution on local call paths * test(cli): cover qr remote tailscale secret ref resolution * fix(secrets): align gateway password active-surface with auth inference * fix(cli): resolve inferred local gateway password refs in qr * fix(gateway): prefer resolvable remote password over token ref pre-resolution * test(gateway): cover none and trusted-proxy stale password refs * docs(secrets): sync qr and gateway active-surface behavior * fix: restore stability blockers from pre-release audit * Secrets: fix collector/runtime precedence contradictions * docs: align secrets and web credential docs * fix(rebase): resolve integration regressions after main rebase * fix(node-host): resolve gateway secret refs for auth * fix(secrets): harden secretinput runtime readers * gateway: skip inactive auth secretref resolution * cli: avoid gateway preflight for inactive secret refs * extensions: allow unresolved refs in onboarding status * tests: fix qr-cli module mock hoist ordering * Security: align audit checks with SecretInput resolution * Gateway: resolve local-mode remote fallback secret refs * Node host: avoid resolving inactive password secret refs * Secrets runtime: mark Slack appToken inactive for HTTP mode * secrets: keep inactive gateway remote refs non-blocking * cli: include agent memory secret targets in runtime resolution * docs(secrets): sync docs with active-surface and web search behavior * fix(secrets): keep telegram top-level token refs active for blank account tokens * fix(daemon): resolve gateway password secret refs for probe auth * fix(secrets): skip IRC NickServ ref resolution when NickServ is disabled * fix(secrets): align token inheritance and exec timeout defaults * docs(secrets): clarify active-surface notes in cli docs * cli: require secrets.resolve gateway capability * gateway: log auth secret surface diagnostics * secrets: remove dead provider resolver module * fix(secrets): restore gateway auth precedence and fallback resolution * fix(tests): align plugin runtime mock typings --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-03-02 20:58:20 -06:00
it("accepts lowercase https_proxy when it is the configured HTTPS proxy var", () => {
process.env.https_proxy = "http://lower-https:8001";
createFeishuWSClient(baseAccount);
const expectedHttpsProxy = process.env.https_proxy || process.env.HTTPS_PROXY;
expect(httpsProxyAgentCtorMock).toHaveBeenCalledTimes(1);
expect(expectedHttpsProxy).toBeTruthy();
expect(httpsProxyAgentCtorMock).toHaveBeenCalledWith(expectedHttpsProxy);
const options = firstWsClientOptions();
expect(options.agent).toEqual({ proxyUrl: expectedHttpsProxy });
});
it("uses HTTPS_PROXY when https_proxy is unset", () => {
process.env.HTTPS_PROXY = "http://upper-https:8002";
process.env.http_proxy = "http://lower-http:8003";
createFeishuWSClient(baseAccount);
expect(httpsProxyAgentCtorMock).toHaveBeenCalledTimes(1);
expect(httpsProxyAgentCtorMock).toHaveBeenCalledWith("http://upper-https:8002");
const options = firstWsClientOptions();
expect(options.agent).toEqual({ proxyUrl: "http://upper-https:8002" });
});
it("passes HTTP_PROXY to ws client when https vars are unset", () => {
process.env.HTTP_PROXY = "http://upper-http:8999";
createFeishuWSClient(baseAccount);
expect(httpsProxyAgentCtorMock).toHaveBeenCalledTimes(1);
expect(httpsProxyAgentCtorMock).toHaveBeenCalledWith("http://upper-http:8999");
const options = firstWsClientOptions();
expect(options.agent).toEqual({ proxyUrl: "http://upper-http:8999" });
});
});