Merge branch 'main' into fix/issue-43909
This commit is contained in:
commit
7e45798f34
@ -77,12 +77,9 @@ vi.mock("./client.js", () => ({
|
||||
createFeishuClient: mockCreateFeishuClient,
|
||||
}));
|
||||
|
||||
vi.mock("../../../src/acp/persistent-bindings.route.js", () => ({
|
||||
vi.mock("openclaw/plugin-sdk/conversation-runtime", () => ({
|
||||
resolveConfiguredAcpRoute: (params: unknown) => mockResolveConfiguredAcpRoute(params),
|
||||
ensureConfiguredAcpRouteReady: (params: unknown) => mockEnsureConfiguredAcpRouteReady(params),
|
||||
}));
|
||||
|
||||
vi.mock("../../../src/infra/outbound/session-binding-service.js", () => ({
|
||||
getSessionBindingService: () => ({
|
||||
resolveByConversation: mockResolveBoundConversation,
|
||||
touch: mockTouchBinding,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { FeishuConfig, ResolvedFeishuAccount } from "./types.js";
|
||||
|
||||
const clientCtorMock = vi.hoisted(() => vi.fn());
|
||||
const wsClientCtorMock = vi.hoisted(() =>
|
||||
vi.fn(function wsClientCtor() {
|
||||
return { connected: true };
|
||||
@ -22,22 +23,6 @@ const mockBaseHttpInstance = vi.hoisted(() => ({
|
||||
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,
|
||||
@ -45,6 +30,7 @@ import {
|
||||
FEISHU_HTTP_TIMEOUT_MS,
|
||||
FEISHU_HTTP_TIMEOUT_MAX_MS,
|
||||
FEISHU_HTTP_TIMEOUT_ENV_VAR,
|
||||
setFeishuClientRuntimeForTest,
|
||||
} from "./client.js";
|
||||
|
||||
const proxyEnvKeys = ["https_proxy", "HTTPS_PROXY", "http_proxy", "HTTP_PROXY"] as const;
|
||||
@ -78,6 +64,21 @@ beforeEach(() => {
|
||||
delete process.env[key];
|
||||
}
|
||||
vi.clearAllMocks();
|
||||
setFeishuClientRuntimeForTest({
|
||||
sdk: {
|
||||
AppType: { SelfBuild: "self" } as never,
|
||||
Domain: {
|
||||
Feishu: "https://open.feishu.cn",
|
||||
Lark: "https://open.larksuite.com",
|
||||
} as never,
|
||||
LoggerLevel: { info: "info" } as never,
|
||||
Client: clientCtorMock as never,
|
||||
WSClient: wsClientCtorMock as never,
|
||||
EventDispatcher: vi.fn() as never,
|
||||
defaultHttpInstance: mockBaseHttpInstance as never,
|
||||
},
|
||||
HttpsProxyAgent: httpsProxyAgentCtorMock as never,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -94,6 +95,7 @@ afterEach(() => {
|
||||
} else {
|
||||
process.env[FEISHU_HTTP_TIMEOUT_ENV_VAR] = priorFeishuTimeoutEnv;
|
||||
}
|
||||
setFeishuClientRuntimeForTest();
|
||||
});
|
||||
|
||||
describe("createFeishuClient HTTP timeout", () => {
|
||||
@ -102,7 +104,7 @@ describe("createFeishuClient HTTP timeout", () => {
|
||||
});
|
||||
|
||||
const getLastClientHttpInstance = () => {
|
||||
const calls = (LarkClient as unknown as ReturnType<typeof vi.fn>).mock.calls;
|
||||
const calls = clientCtorMock.mock.calls;
|
||||
const lastCall = calls[calls.length - 1]?.[0] as
|
||||
| { httpInstance?: { get: (...args: unknown[]) => Promise<unknown> } }
|
||||
| undefined;
|
||||
@ -122,7 +124,7 @@ describe("createFeishuClient HTTP 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 calls = clientCtorMock.mock.calls;
|
||||
const lastCall = calls[calls.length - 1][0] as { httpInstance?: unknown };
|
||||
expect(lastCall.httpInstance).toBeDefined();
|
||||
});
|
||||
@ -130,7 +132,7 @@ describe("createFeishuClient HTTP timeout", () => {
|
||||
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 calls = clientCtorMock.mock.calls;
|
||||
const lastCall = calls[calls.length - 1][0] as {
|
||||
httpInstance: { post: (...args: unknown[]) => Promise<unknown> };
|
||||
};
|
||||
@ -152,7 +154,7 @@ describe("createFeishuClient HTTP timeout", () => {
|
||||
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 calls = clientCtorMock.mock.calls;
|
||||
const lastCall = calls[calls.length - 1][0] as {
|
||||
httpInstance: { get: (...args: unknown[]) => Promise<unknown> };
|
||||
};
|
||||
@ -241,7 +243,7 @@ describe("createFeishuClient HTTP timeout", () => {
|
||||
config: { httpTimeoutMs: 45_000 },
|
||||
});
|
||||
|
||||
const calls = (LarkClient as unknown as ReturnType<typeof vi.fn>).mock.calls;
|
||||
const calls = clientCtorMock.mock.calls;
|
||||
expect(calls.length).toBe(2);
|
||||
|
||||
const lastCall = calls[calls.length - 1][0] as {
|
||||
|
||||
@ -2,6 +2,30 @@ import * as Lark from "@larksuiteoapi/node-sdk";
|
||||
import { HttpsProxyAgent } from "https-proxy-agent";
|
||||
import type { FeishuConfig, FeishuDomain, ResolvedFeishuAccount } from "./types.js";
|
||||
|
||||
type FeishuClientSdk = Pick<
|
||||
typeof Lark,
|
||||
| "AppType"
|
||||
| "Client"
|
||||
| "defaultHttpInstance"
|
||||
| "Domain"
|
||||
| "EventDispatcher"
|
||||
| "LoggerLevel"
|
||||
| "WSClient"
|
||||
>;
|
||||
|
||||
const defaultFeishuClientSdk: FeishuClientSdk = {
|
||||
AppType: Lark.AppType,
|
||||
Client: Lark.Client,
|
||||
defaultHttpInstance: Lark.defaultHttpInstance,
|
||||
Domain: Lark.Domain,
|
||||
EventDispatcher: Lark.EventDispatcher,
|
||||
LoggerLevel: Lark.LoggerLevel,
|
||||
WSClient: Lark.WSClient,
|
||||
};
|
||||
|
||||
let feishuClientSdk: FeishuClientSdk = defaultFeishuClientSdk;
|
||||
let httpsProxyAgentCtor: typeof HttpsProxyAgent = HttpsProxyAgent;
|
||||
|
||||
/** Default HTTP timeout for Feishu API requests (30 seconds). */
|
||||
export const FEISHU_HTTP_TIMEOUT_MS = 30_000;
|
||||
export const FEISHU_HTTP_TIMEOUT_MAX_MS = 300_000;
|
||||
@ -14,7 +38,7 @@ function getWsProxyAgent(): HttpsProxyAgent<string> | undefined {
|
||||
process.env.http_proxy ||
|
||||
process.env.HTTP_PROXY;
|
||||
if (!proxyUrl) return undefined;
|
||||
return new HttpsProxyAgent(proxyUrl);
|
||||
return new httpsProxyAgentCtor(proxyUrl);
|
||||
}
|
||||
|
||||
// Multi-account client cache
|
||||
@ -28,10 +52,10 @@ const clientCache = new Map<
|
||||
|
||||
function resolveDomain(domain: FeishuDomain | undefined): Lark.Domain | string {
|
||||
if (domain === "lark") {
|
||||
return Lark.Domain.Lark;
|
||||
return feishuClientSdk.Domain.Lark;
|
||||
}
|
||||
if (domain === "feishu" || !domain) {
|
||||
return Lark.Domain.Feishu;
|
||||
return feishuClientSdk.Domain.Feishu;
|
||||
}
|
||||
return domain.replace(/\/+$/, ""); // Custom URL for private deployment
|
||||
}
|
||||
@ -42,7 +66,8 @@ function resolveDomain(domain: FeishuDomain | undefined): Lark.Domain | string {
|
||||
* (e.g. when the Feishu API is slow, causing per-chat queue deadlocks).
|
||||
*/
|
||||
function createTimeoutHttpInstance(defaultTimeoutMs: number): Lark.HttpInstance {
|
||||
const base: Lark.HttpInstance = Lark.defaultHttpInstance as unknown as Lark.HttpInstance;
|
||||
const base: Lark.HttpInstance =
|
||||
feishuClientSdk.defaultHttpInstance as unknown as Lark.HttpInstance;
|
||||
|
||||
function injectTimeout<D>(opts?: Lark.HttpRequestOptions<D>): Lark.HttpRequestOptions<D> {
|
||||
return { timeout: defaultTimeoutMs, ...opts } as Lark.HttpRequestOptions<D>;
|
||||
@ -129,10 +154,10 @@ export function createFeishuClient(creds: FeishuClientCredentials): Lark.Client
|
||||
}
|
||||
|
||||
// Create new client with timeout-aware HTTP instance
|
||||
const client = new Lark.Client({
|
||||
const client = new feishuClientSdk.Client({
|
||||
appId,
|
||||
appSecret,
|
||||
appType: Lark.AppType.SelfBuild,
|
||||
appType: feishuClientSdk.AppType.SelfBuild,
|
||||
domain: resolveDomain(domain),
|
||||
httpInstance: createTimeoutHttpInstance(defaultHttpTimeoutMs),
|
||||
});
|
||||
@ -158,11 +183,11 @@ export function createFeishuWSClient(account: ResolvedFeishuAccount): Lark.WSCli
|
||||
}
|
||||
|
||||
const agent = getWsProxyAgent();
|
||||
return new Lark.WSClient({
|
||||
return new feishuClientSdk.WSClient({
|
||||
appId,
|
||||
appSecret,
|
||||
domain: resolveDomain(domain),
|
||||
loggerLevel: Lark.LoggerLevel.info,
|
||||
loggerLevel: feishuClientSdk.LoggerLevel.info,
|
||||
...(agent ? { agent } : {}),
|
||||
});
|
||||
}
|
||||
@ -171,7 +196,7 @@ export function createFeishuWSClient(account: ResolvedFeishuAccount): Lark.WSCli
|
||||
* Create an event dispatcher for an account.
|
||||
*/
|
||||
export function createEventDispatcher(account: ResolvedFeishuAccount): Lark.EventDispatcher {
|
||||
return new Lark.EventDispatcher({
|
||||
return new feishuClientSdk.EventDispatcher({
|
||||
encryptKey: account.encryptKey,
|
||||
verificationToken: account.verificationToken,
|
||||
});
|
||||
@ -194,3 +219,13 @@ export function clearClientCache(accountId?: string): void {
|
||||
clientCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export function setFeishuClientRuntimeForTest(overrides?: {
|
||||
sdk?: Partial<FeishuClientSdk>;
|
||||
HttpsProxyAgent?: typeof HttpsProxyAgent;
|
||||
}): void {
|
||||
feishuClientSdk = overrides?.sdk
|
||||
? { ...defaultFeishuClientSdk, ...overrides.sdk }
|
||||
: defaultFeishuClientSdk;
|
||||
httpsProxyAgentCtor = overrides?.HttpsProxyAgent ?? HttpsProxyAgent;
|
||||
}
|
||||
|
||||
@ -1,11 +1,18 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const createFeishuClientMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("./client.js", () => ({
|
||||
createFeishuClient: createFeishuClientMock,
|
||||
const clientCtorMock = vi.hoisted(() => vi.fn());
|
||||
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({}),
|
||||
}));
|
||||
|
||||
import { clearClientCache, setFeishuClientRuntimeForTest } from "./client.js";
|
||||
import { FEISHU_PROBE_REQUEST_TIMEOUT_MS, probeFeishu, clearProbeCache } from "./probe.js";
|
||||
|
||||
const DEFAULT_CREDS = { appId: "cli_123", appSecret: "secret" } as const; // pragma: allowlist secret
|
||||
@ -28,9 +35,15 @@ function makeRequestFn(response: Record<string, unknown>) {
|
||||
return vi.fn().mockResolvedValue(response);
|
||||
}
|
||||
|
||||
function installClientCtor(requestFn: unknown) {
|
||||
clientCtorMock.mockImplementation(function MockFeishuClient(this: { request: unknown }) {
|
||||
this.request = requestFn;
|
||||
} as never);
|
||||
}
|
||||
|
||||
function setupClient(response: Record<string, unknown>) {
|
||||
const requestFn = makeRequestFn(response);
|
||||
createFeishuClientMock.mockReturnValue({ request: requestFn });
|
||||
installClientCtor(requestFn);
|
||||
return requestFn;
|
||||
}
|
||||
|
||||
@ -60,7 +73,7 @@ async function expectErrorResultCached(params: {
|
||||
expectedError: string;
|
||||
ttlMs: number;
|
||||
}) {
|
||||
createFeishuClientMock.mockReturnValue({ request: params.requestFn });
|
||||
installClientCtor(params.requestFn);
|
||||
|
||||
const first = await probeFeishu(DEFAULT_CREDS);
|
||||
const second = await probeFeishu(DEFAULT_CREDS);
|
||||
@ -95,11 +108,25 @@ async function readSequentialDefaultProbePair() {
|
||||
describe("probeFeishu", () => {
|
||||
beforeEach(() => {
|
||||
clearProbeCache();
|
||||
vi.restoreAllMocks();
|
||||
clearClientCache();
|
||||
vi.clearAllMocks();
|
||||
setFeishuClientRuntimeForTest({
|
||||
sdk: {
|
||||
AppType: { SelfBuild: "self" } as never,
|
||||
Domain: {
|
||||
Feishu: "https://open.feishu.cn",
|
||||
Lark: "https://open.larksuite.com",
|
||||
} as never,
|
||||
Client: clientCtorMock as never,
|
||||
defaultHttpInstance: mockBaseHttpInstance as never,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clearProbeCache();
|
||||
clearClientCache();
|
||||
setFeishuClientRuntimeForTest();
|
||||
});
|
||||
|
||||
it("returns error when credentials are missing", async () => {
|
||||
@ -141,7 +168,7 @@ describe("probeFeishu", () => {
|
||||
it("returns timeout error when request exceeds timeout", async () => {
|
||||
await withFakeTimers(async () => {
|
||||
const requestFn = vi.fn().mockImplementation(() => new Promise(() => {}));
|
||||
createFeishuClientMock.mockReturnValue({ request: requestFn });
|
||||
installClientCtor(requestFn);
|
||||
|
||||
const promise = probeFeishu(DEFAULT_CREDS, { timeoutMs: 1_000 });
|
||||
await vi.advanceTimersByTimeAsync(1_000);
|
||||
@ -152,7 +179,6 @@ describe("probeFeishu", () => {
|
||||
});
|
||||
|
||||
it("returns aborted when abort signal is already aborted", async () => {
|
||||
createFeishuClientMock.mockClear();
|
||||
const abortController = new AbortController();
|
||||
abortController.abort();
|
||||
|
||||
@ -162,7 +188,7 @@ describe("probeFeishu", () => {
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({ ok: false, error: "probe aborted" });
|
||||
expect(createFeishuClientMock).not.toHaveBeenCalled();
|
||||
expect(clientCtorMock).not.toHaveBeenCalled();
|
||||
});
|
||||
it("returns cached result on subsequent calls within TTL", async () => {
|
||||
const requestFn = setupSuccessClient();
|
||||
|
||||
@ -1,7 +1,27 @@
|
||||
import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
||||
import type { Dirent } from "node:fs";
|
||||
import { delimiter, dirname, join } from "node:path";
|
||||
import { CLIENT_ID_KEYS, CLIENT_SECRET_KEYS } from "./oauth.shared.js";
|
||||
|
||||
type CredentialFs = {
|
||||
existsSync: (path: Parameters<typeof existsSync>[0]) => ReturnType<typeof existsSync>;
|
||||
readFileSync: (path: Parameters<typeof readFileSync>[0], encoding: "utf8") => string;
|
||||
realpathSync: (path: Parameters<typeof realpathSync>[0]) => string;
|
||||
readdirSync: (
|
||||
path: Parameters<typeof readdirSync>[0],
|
||||
options: { withFileTypes: true },
|
||||
) => Dirent[];
|
||||
};
|
||||
|
||||
const defaultFs: CredentialFs = {
|
||||
existsSync,
|
||||
readFileSync,
|
||||
realpathSync,
|
||||
readdirSync,
|
||||
};
|
||||
|
||||
let credentialFs: CredentialFs = defaultFs;
|
||||
|
||||
function resolveEnv(keys: string[]): string | undefined {
|
||||
for (const key of keys) {
|
||||
const value = process.env[key]?.trim();
|
||||
@ -18,6 +38,10 @@ export function clearCredentialsCache(): void {
|
||||
cachedGeminiCliCredentials = null;
|
||||
}
|
||||
|
||||
export function setOAuthCredentialsFsForTest(overrides?: Partial<CredentialFs>): void {
|
||||
credentialFs = overrides ? { ...defaultFs, ...overrides } : defaultFs;
|
||||
}
|
||||
|
||||
export function extractGeminiCliCredentials(): { clientId: string; clientSecret: string } | null {
|
||||
if (cachedGeminiCliCredentials) {
|
||||
return cachedGeminiCliCredentials;
|
||||
@ -29,7 +53,7 @@ export function extractGeminiCliCredentials(): { clientId: string; clientSecret:
|
||||
return null;
|
||||
}
|
||||
|
||||
const resolvedPath = realpathSync(geminiPath);
|
||||
const resolvedPath = credentialFs.realpathSync(geminiPath);
|
||||
const geminiCliDirs = resolveGeminiCliDirs(geminiPath, resolvedPath);
|
||||
|
||||
let content: string | null = null;
|
||||
@ -55,10 +79,9 @@ export function extractGeminiCliCredentials(): { clientId: string; clientSecret:
|
||||
"oauth2.js",
|
||||
),
|
||||
];
|
||||
|
||||
for (const path of searchPaths) {
|
||||
if (existsSync(path)) {
|
||||
content = readFileSync(path, "utf8");
|
||||
if (credentialFs.existsSync(path)) {
|
||||
content = credentialFs.readFileSync(path, "utf8");
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -67,7 +90,7 @@ export function extractGeminiCliCredentials(): { clientId: string; clientSecret:
|
||||
}
|
||||
const found = findFile(geminiCliDir, "oauth2.js", 10);
|
||||
if (found) {
|
||||
content = readFileSync(found, "utf8");
|
||||
content = credentialFs.readFileSync(found, "utf8");
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -116,7 +139,7 @@ function findInPath(name: string): string | null {
|
||||
for (const dir of (process.env.PATH ?? "").split(delimiter)) {
|
||||
for (const ext of exts) {
|
||||
const path = join(dir, name + ext);
|
||||
if (existsSync(path)) {
|
||||
if (credentialFs.existsSync(path)) {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
@ -129,7 +152,7 @@ function findFile(dir: string, name: string, depth: number): string | null {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
for (const entry of credentialFs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const path = join(dir, entry.name);
|
||||
if (entry.isFile() && entry.name === name) {
|
||||
return path;
|
||||
|
||||
@ -21,23 +21,11 @@ vi.mock("../../src/infra/net/fetch-guard.js", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock fs module before importing the module under test
|
||||
const mockExistsSync = vi.fn();
|
||||
const mockReadFileSync = vi.fn();
|
||||
const mockRealpathSync = vi.fn();
|
||||
const mockReaddirSync = vi.fn();
|
||||
|
||||
vi.mock("node:fs", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("node:fs")>();
|
||||
return {
|
||||
...actual,
|
||||
existsSync: (...args: Parameters<typeof actual.existsSync>) => mockExistsSync(...args),
|
||||
readFileSync: (...args: Parameters<typeof actual.readFileSync>) => mockReadFileSync(...args),
|
||||
realpathSync: (...args: Parameters<typeof actual.realpathSync>) => mockRealpathSync(...args),
|
||||
readdirSync: (...args: Parameters<typeof actual.readdirSync>) => mockReaddirSync(...args),
|
||||
};
|
||||
});
|
||||
|
||||
describe("extractGeminiCliCredentials", () => {
|
||||
const normalizePath = (value: string) =>
|
||||
value.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
||||
@ -51,6 +39,20 @@ describe("extractGeminiCliCredentials", () => {
|
||||
|
||||
let originalPath: string | undefined;
|
||||
|
||||
async function loadCredentialsModule() {
|
||||
return await import("./oauth.credentials.js");
|
||||
}
|
||||
|
||||
async function installMockFs() {
|
||||
const { setOAuthCredentialsFsForTest } = await loadCredentialsModule();
|
||||
setOAuthCredentialsFsForTest({
|
||||
existsSync: (...args) => mockExistsSync(...args),
|
||||
readFileSync: (...args) => mockReadFileSync(...args),
|
||||
realpathSync: (...args) => mockRealpathSync(...args),
|
||||
readdirSync: (...args) => mockReaddirSync(...args),
|
||||
});
|
||||
}
|
||||
|
||||
function makeFakeLayout() {
|
||||
const binDir = join(rootDir, "fake", "bin");
|
||||
const geminiPath = join(binDir, "gemini");
|
||||
@ -157,17 +159,20 @@ describe("extractGeminiCliCredentials", () => {
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
originalPath = process.env.PATH;
|
||||
await installMockFs();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
afterEach(async () => {
|
||||
process.env.PATH = originalPath;
|
||||
const { setOAuthCredentialsFsForTest } = await loadCredentialsModule();
|
||||
setOAuthCredentialsFsForTest();
|
||||
});
|
||||
|
||||
it("returns null when gemini binary is not in PATH", async () => {
|
||||
process.env.PATH = "/nonexistent";
|
||||
mockExistsSync.mockReturnValue(false);
|
||||
|
||||
const { extractGeminiCliCredentials, clearCredentialsCache } = await import("./oauth.js");
|
||||
const { extractGeminiCliCredentials, clearCredentialsCache } = await loadCredentialsModule();
|
||||
clearCredentialsCache();
|
||||
expect(extractGeminiCliCredentials()).toBeNull();
|
||||
});
|
||||
@ -175,7 +180,7 @@ describe("extractGeminiCliCredentials", () => {
|
||||
it("extracts credentials from oauth2.js in known path", async () => {
|
||||
installGeminiLayout({ oauth2Exists: true, oauth2Content: FAKE_OAUTH2_CONTENT });
|
||||
|
||||
const { extractGeminiCliCredentials, clearCredentialsCache } = await import("./oauth.js");
|
||||
const { extractGeminiCliCredentials, clearCredentialsCache } = await loadCredentialsModule();
|
||||
clearCredentialsCache();
|
||||
const result = extractGeminiCliCredentials();
|
||||
|
||||
@ -185,7 +190,7 @@ describe("extractGeminiCliCredentials", () => {
|
||||
it("extracts credentials when PATH entry is an npm global shim", async () => {
|
||||
installNpmShimLayout({ oauth2Exists: true, oauth2Content: FAKE_OAUTH2_CONTENT });
|
||||
|
||||
const { extractGeminiCliCredentials, clearCredentialsCache } = await import("./oauth.js");
|
||||
const { extractGeminiCliCredentials, clearCredentialsCache } = await loadCredentialsModule();
|
||||
clearCredentialsCache();
|
||||
const result = extractGeminiCliCredentials();
|
||||
|
||||
@ -195,7 +200,7 @@ describe("extractGeminiCliCredentials", () => {
|
||||
it("returns null when oauth2.js cannot be found", async () => {
|
||||
installGeminiLayout({ oauth2Exists: false, readdir: [] });
|
||||
|
||||
const { extractGeminiCliCredentials, clearCredentialsCache } = await import("./oauth.js");
|
||||
const { extractGeminiCliCredentials, clearCredentialsCache } = await loadCredentialsModule();
|
||||
clearCredentialsCache();
|
||||
expect(extractGeminiCliCredentials()).toBeNull();
|
||||
});
|
||||
@ -203,7 +208,7 @@ describe("extractGeminiCliCredentials", () => {
|
||||
it("returns null when oauth2.js lacks credentials", async () => {
|
||||
installGeminiLayout({ oauth2Exists: true, oauth2Content: "// no credentials here" });
|
||||
|
||||
const { extractGeminiCliCredentials, clearCredentialsCache } = await import("./oauth.js");
|
||||
const { extractGeminiCliCredentials, clearCredentialsCache } = await loadCredentialsModule();
|
||||
clearCredentialsCache();
|
||||
expect(extractGeminiCliCredentials()).toBeNull();
|
||||
});
|
||||
@ -211,7 +216,7 @@ describe("extractGeminiCliCredentials", () => {
|
||||
it("caches credentials after first extraction", async () => {
|
||||
installGeminiLayout({ oauth2Exists: true, oauth2Content: FAKE_OAUTH2_CONTENT });
|
||||
|
||||
const { extractGeminiCliCredentials, clearCredentialsCache } = await import("./oauth.js");
|
||||
const { extractGeminiCliCredentials, clearCredentialsCache } = await loadCredentialsModule();
|
||||
clearCredentialsCache();
|
||||
|
||||
// First call
|
||||
|
||||
@ -4,10 +4,14 @@ import { describe, expect, it, vi } from "vitest";
|
||||
const uploadGoogleChatAttachmentMock = vi.hoisted(() => vi.fn());
|
||||
const sendGoogleChatMessageMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("./api.js", () => ({
|
||||
sendGoogleChatMessage: sendGoogleChatMessageMock,
|
||||
uploadGoogleChatAttachment: uploadGoogleChatAttachmentMock,
|
||||
}));
|
||||
vi.mock("./api.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("./api.js")>();
|
||||
return {
|
||||
...actual,
|
||||
sendGoogleChatMessage: sendGoogleChatMessageMock,
|
||||
uploadGoogleChatAttachment: uploadGoogleChatAttachmentMock,
|
||||
};
|
||||
});
|
||||
|
||||
import { googlechatPlugin } from "./channel.js";
|
||||
import { setGoogleChatRuntime } from "./runtime.js";
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user