mirror of
https://github.com/clawdbot/clawdbot.git
synced 2026-03-17 17:33:45 +01:00
test: harden CI-sensitive test suites
This commit is contained in:
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -274,8 +274,8 @@ jobs:
|
||||
|
||||
- name: Run changed extension tests
|
||||
env:
|
||||
EXTENSION_ID: ${{ matrix.extension }}
|
||||
run: pnpm test:extension "$EXTENSION_ID"
|
||||
OPENCLAW_CHANGED_EXTENSION: ${{ matrix.extension }}
|
||||
run: pnpm test:extension "$OPENCLAW_CHANGED_EXTENSION"
|
||||
|
||||
# Types, lint, and format check.
|
||||
check:
|
||||
|
||||
@@ -77,14 +77,19 @@ vi.mock("./client.js", () => ({
|
||||
createFeishuClient: mockCreateFeishuClient,
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/conversation-runtime", () => ({
|
||||
resolveConfiguredAcpRoute: (params: unknown) => mockResolveConfiguredAcpRoute(params),
|
||||
ensureConfiguredAcpRouteReady: (params: unknown) => mockEnsureConfiguredAcpRouteReady(params),
|
||||
getSessionBindingService: () => ({
|
||||
resolveByConversation: mockResolveBoundConversation,
|
||||
touch: mockTouchBinding,
|
||||
}),
|
||||
}));
|
||||
vi.mock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => {
|
||||
const original =
|
||||
await importOriginal<typeof import("openclaw/plugin-sdk/conversation-runtime")>();
|
||||
return {
|
||||
...original,
|
||||
resolveConfiguredAcpRoute: (params: unknown) => mockResolveConfiguredAcpRoute(params),
|
||||
ensureConfiguredAcpRouteReady: (params: unknown) => mockEnsureConfiguredAcpRouteReady(params),
|
||||
getSessionBindingService: () => ({
|
||||
resolveByConversation: mockResolveBoundConversation,
|
||||
touch: mockTouchBinding,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
function createRuntimeEnv(): RuntimeEnv {
|
||||
return {
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { FeishuConfig, ResolvedFeishuAccount } from "./types.js";
|
||||
|
||||
const clientCtorMock = vi.hoisted(() => vi.fn());
|
||||
type CreateFeishuClient = typeof import("./client.js").createFeishuClient;
|
||||
type CreateFeishuWSClient = typeof import("./client.js").createFeishuWSClient;
|
||||
type ClearClientCache = typeof import("./client.js").clearClientCache;
|
||||
type SetFeishuClientRuntimeForTest = typeof import("./client.js").setFeishuClientRuntimeForTest;
|
||||
|
||||
const clientCtorMock = vi.hoisted(() =>
|
||||
vi.fn(function clientCtor() {
|
||||
return { connected: true };
|
||||
}),
|
||||
);
|
||||
const wsClientCtorMock = vi.hoisted(() =>
|
||||
vi.fn(function wsClientCtor() {
|
||||
return { connected: true };
|
||||
@@ -12,7 +21,6 @@ const httpsProxyAgentCtorMock = vi.hoisted(() =>
|
||||
return { proxyUrl };
|
||||
}),
|
||||
);
|
||||
|
||||
const mockBaseHttpInstance = vi.hoisted(() => ({
|
||||
request: vi.fn().mockResolvedValue({}),
|
||||
get: vi.fn().mockResolvedValue({}),
|
||||
@@ -23,19 +31,17 @@ const mockBaseHttpInstance = vi.hoisted(() => ({
|
||||
head: vi.fn().mockResolvedValue({}),
|
||||
options: vi.fn().mockResolvedValue({}),
|
||||
}));
|
||||
import {
|
||||
createFeishuClient,
|
||||
createFeishuWSClient,
|
||||
clearClientCache,
|
||||
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;
|
||||
type ProxyEnvKey = (typeof proxyEnvKeys)[number];
|
||||
|
||||
let createFeishuClient: CreateFeishuClient;
|
||||
let createFeishuWSClient: CreateFeishuWSClient;
|
||||
let clearClientCache: ClearClientCache;
|
||||
let setFeishuClientRuntimeForTest: SetFeishuClientRuntimeForTest;
|
||||
let FEISHU_HTTP_TIMEOUT_MS: number;
|
||||
let FEISHU_HTTP_TIMEOUT_MAX_MS: number;
|
||||
let FEISHU_HTTP_TIMEOUT_ENV_VAR: string;
|
||||
|
||||
let priorProxyEnv: Partial<Record<ProxyEnvKey, string | undefined>> = {};
|
||||
let priorFeishuTimeoutEnv: string | undefined;
|
||||
|
||||
@@ -55,7 +61,31 @@ function firstWsClientOptions(): { agent?: unknown } {
|
||||
return calls[0]?.[0] ?? {};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
vi.doMock("@larksuiteoapi/node-sdk", () => ({
|
||||
AppType: { SelfBuild: "self" },
|
||||
Domain: { Feishu: "https://open.feishu.cn", Lark: "https://open.larksuite.com" },
|
||||
LoggerLevel: { info: "info" },
|
||||
Client: clientCtorMock,
|
||||
WSClient: wsClientCtorMock,
|
||||
EventDispatcher: vi.fn(),
|
||||
defaultHttpInstance: mockBaseHttpInstance,
|
||||
}));
|
||||
vi.doMock("https-proxy-agent", () => ({
|
||||
HttpsProxyAgent: httpsProxyAgentCtorMock,
|
||||
}));
|
||||
|
||||
({
|
||||
createFeishuClient,
|
||||
createFeishuWSClient,
|
||||
clearClientCache,
|
||||
setFeishuClientRuntimeForTest,
|
||||
FEISHU_HTTP_TIMEOUT_MS,
|
||||
FEISHU_HTTP_TIMEOUT_MAX_MS,
|
||||
FEISHU_HTTP_TIMEOUT_ENV_VAR,
|
||||
} = await import("./client.js"));
|
||||
|
||||
priorProxyEnv = {};
|
||||
priorFeishuTimeoutEnv = process.env[FEISHU_HTTP_TIMEOUT_ENV_VAR];
|
||||
delete process.env[FEISHU_HTTP_TIMEOUT_ENV_VAR];
|
||||
@@ -104,7 +134,7 @@ describe("createFeishuClient HTTP timeout", () => {
|
||||
});
|
||||
|
||||
const getLastClientHttpInstance = () => {
|
||||
const calls = clientCtorMock.mock.calls;
|
||||
const calls = clientCtorMock.mock.calls as unknown as Array<[options: unknown]>;
|
||||
const lastCall = calls[calls.length - 1]?.[0] as
|
||||
| { httpInstance?: { get: (...args: unknown[]) => Promise<unknown> } }
|
||||
| undefined;
|
||||
@@ -124,21 +154,22 @@ 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 = clientCtorMock.mock.calls;
|
||||
const lastCall = calls[calls.length - 1][0] as { httpInstance?: unknown };
|
||||
expect(lastCall.httpInstance).toBeDefined();
|
||||
const calls = clientCtorMock.mock.calls as unknown as Array<[options: unknown]>;
|
||||
const lastCall = calls[calls.length - 1]?.[0] as { httpInstance?: unknown } | undefined;
|
||||
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 = clientCtorMock.mock.calls;
|
||||
const lastCall = calls[calls.length - 1][0] as {
|
||||
httpInstance: { post: (...args: unknown[]) => Promise<unknown> };
|
||||
};
|
||||
const httpInstance = lastCall.httpInstance;
|
||||
const calls = clientCtorMock.mock.calls as unknown as Array<[options: unknown]>;
|
||||
const lastCall = calls[calls.length - 1]?.[0] as
|
||||
| { httpInstance: { post: (...args: unknown[]) => Promise<unknown> } }
|
||||
| undefined;
|
||||
const httpInstance = lastCall?.httpInstance;
|
||||
|
||||
await httpInstance.post(
|
||||
expect(httpInstance).toBeDefined();
|
||||
await httpInstance?.post(
|
||||
"https://example.com/api",
|
||||
{ data: 1 },
|
||||
{ headers: { "X-Custom": "yes" } },
|
||||
@@ -154,13 +185,14 @@ 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 = clientCtorMock.mock.calls;
|
||||
const lastCall = calls[calls.length - 1][0] as {
|
||||
httpInstance: { get: (...args: unknown[]) => Promise<unknown> };
|
||||
};
|
||||
const httpInstance = lastCall.httpInstance;
|
||||
const calls = clientCtorMock.mock.calls as unknown as Array<[options: unknown]>;
|
||||
const lastCall = calls[calls.length - 1]?.[0] as
|
||||
| { httpInstance: { get: (...args: unknown[]) => Promise<unknown> } }
|
||||
| undefined;
|
||||
const httpInstance = lastCall?.httpInstance;
|
||||
|
||||
await httpInstance.get("https://example.com/api", { timeout: 5_000 });
|
||||
expect(httpInstance).toBeDefined();
|
||||
await httpInstance?.get("https://example.com/api", { timeout: 5_000 });
|
||||
|
||||
expect(mockBaseHttpInstance.get).toHaveBeenCalledWith(
|
||||
"https://example.com/api",
|
||||
@@ -243,13 +275,14 @@ describe("createFeishuClient HTTP timeout", () => {
|
||||
config: { httpTimeoutMs: 45_000 },
|
||||
});
|
||||
|
||||
const calls = clientCtorMock.mock.calls;
|
||||
const calls = clientCtorMock.mock.calls as unknown as Array<[options: unknown]>;
|
||||
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");
|
||||
const lastCall = calls[calls.length - 1]?.[0] as
|
||||
| { httpInstance: { get: (...args: unknown[]) => Promise<unknown> } }
|
||||
| undefined;
|
||||
expect(lastCall?.httpInstance).toBeDefined();
|
||||
await lastCall?.httpInstance.get("https://example.com/api");
|
||||
|
||||
expect(mockBaseHttpInstance.get).toHaveBeenCalledWith(
|
||||
"https://example.com/api",
|
||||
@@ -264,7 +297,7 @@ describe("createFeishuWSClient proxy handling", () => {
|
||||
|
||||
expect(httpsProxyAgentCtorMock).not.toHaveBeenCalled();
|
||||
const options = firstWsClientOptions();
|
||||
expect(options?.agent).toBeUndefined();
|
||||
expect(options.agent).toBeUndefined();
|
||||
});
|
||||
|
||||
it("uses proxy env precedence: https_proxy first, then HTTPS_PROXY, then http_proxy/HTTP_PROXY", () => {
|
||||
|
||||
@@ -2,6 +2,8 @@ import { describe, expect, it, vi } from "vitest";
|
||||
import { waitForCompactionRetryWithAggregateTimeout } from "./compaction-retry-aggregate-timeout.js";
|
||||
|
||||
type AggregateTimeoutParams = Parameters<typeof waitForCompactionRetryWithAggregateTimeout>[0];
|
||||
type TimeoutCallback = NonNullable<AggregateTimeoutParams["onTimeout"]>;
|
||||
type TimeoutCallbackMock = ReturnType<typeof vi.fn<TimeoutCallback>>;
|
||||
|
||||
async function withFakeTimers(run: () => Promise<void>) {
|
||||
vi.useFakeTimers();
|
||||
@@ -13,7 +15,7 @@ async function withFakeTimers(run: () => Promise<void>) {
|
||||
}
|
||||
}
|
||||
|
||||
function expectClearedTimeoutState(onTimeout: ReturnType<typeof vi.fn>, timedOut: boolean) {
|
||||
function expectClearedTimeoutState(onTimeout: TimeoutCallbackMock, timedOut: boolean) {
|
||||
if (timedOut) {
|
||||
expect(onTimeout).toHaveBeenCalledTimes(1);
|
||||
} else {
|
||||
@@ -25,18 +27,15 @@ function expectClearedTimeoutState(onTimeout: ReturnType<typeof vi.fn>, timedOut
|
||||
function buildAggregateTimeoutParams(
|
||||
overrides: Partial<AggregateTimeoutParams> &
|
||||
Pick<AggregateTimeoutParams, "waitForCompactionRetry">,
|
||||
): { params: AggregateTimeoutParams; onTimeoutSpy: ReturnType<typeof vi.fn> } {
|
||||
const onTimeoutSpy = vi.fn();
|
||||
const onTimeout = overrides.onTimeout ?? (() => onTimeoutSpy());
|
||||
): AggregateTimeoutParams & { onTimeout: TimeoutCallbackMock } {
|
||||
const onTimeout =
|
||||
(overrides.onTimeout as TimeoutCallbackMock | undefined) ?? vi.fn<TimeoutCallback>();
|
||||
return {
|
||||
params: {
|
||||
waitForCompactionRetry: overrides.waitForCompactionRetry,
|
||||
abortable: overrides.abortable ?? (async (promise) => await promise),
|
||||
aggregateTimeoutMs: overrides.aggregateTimeoutMs ?? 60_000,
|
||||
isCompactionStillInFlight: overrides.isCompactionStillInFlight,
|
||||
onTimeout,
|
||||
},
|
||||
onTimeoutSpy,
|
||||
waitForCompactionRetry: overrides.waitForCompactionRetry,
|
||||
abortable: overrides.abortable ?? (async (promise) => await promise),
|
||||
aggregateTimeoutMs: overrides.aggregateTimeoutMs ?? 60_000,
|
||||
isCompactionStillInFlight: overrides.isCompactionStillInFlight,
|
||||
onTimeout,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -44,7 +43,7 @@ describe("waitForCompactionRetryWithAggregateTimeout", () => {
|
||||
it("times out and fires callback when compaction retry never resolves", async () => {
|
||||
await withFakeTimers(async () => {
|
||||
const waitForCompactionRetry = vi.fn(async () => await new Promise<void>(() => {}));
|
||||
const { params, onTimeoutSpy } = buildAggregateTimeoutParams({ waitForCompactionRetry });
|
||||
const params = buildAggregateTimeoutParams({ waitForCompactionRetry });
|
||||
|
||||
const resultPromise = waitForCompactionRetryWithAggregateTimeout(params);
|
||||
|
||||
@@ -52,7 +51,7 @@ describe("waitForCompactionRetryWithAggregateTimeout", () => {
|
||||
const result = await resultPromise;
|
||||
|
||||
expect(result.timedOut).toBe(true);
|
||||
expectClearedTimeoutState(onTimeoutSpy, true);
|
||||
expectClearedTimeoutState(params.onTimeout, true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -72,15 +71,14 @@ describe("waitForCompactionRetryWithAggregateTimeout", () => {
|
||||
waitForCompactionRetry,
|
||||
isCompactionStillInFlight: () => compactionInFlight,
|
||||
});
|
||||
const { params: aggregateTimeoutParams, onTimeoutSpy } = params;
|
||||
|
||||
const resultPromise = waitForCompactionRetryWithAggregateTimeout(aggregateTimeoutParams);
|
||||
const resultPromise = waitForCompactionRetryWithAggregateTimeout(params);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(170_000);
|
||||
const result = await resultPromise;
|
||||
|
||||
expect(result.timedOut).toBe(false);
|
||||
expectClearedTimeoutState(onTimeoutSpy, false);
|
||||
expectClearedTimeoutState(params.onTimeout, false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -91,7 +89,7 @@ describe("waitForCompactionRetryWithAggregateTimeout", () => {
|
||||
setTimeout(() => {
|
||||
compactionInFlight = false;
|
||||
}, 90_000);
|
||||
const { params, onTimeoutSpy } = buildAggregateTimeoutParams({
|
||||
const params = buildAggregateTimeoutParams({
|
||||
waitForCompactionRetry,
|
||||
isCompactionStillInFlight: () => compactionInFlight,
|
||||
});
|
||||
@@ -102,19 +100,19 @@ describe("waitForCompactionRetryWithAggregateTimeout", () => {
|
||||
const result = await resultPromise;
|
||||
|
||||
expect(result.timedOut).toBe(true);
|
||||
expectClearedTimeoutState(onTimeoutSpy, true);
|
||||
expectClearedTimeoutState(params.onTimeout, true);
|
||||
});
|
||||
});
|
||||
|
||||
it("does not time out when compaction retry resolves", async () => {
|
||||
await withFakeTimers(async () => {
|
||||
const waitForCompactionRetry = vi.fn(async () => {});
|
||||
const { params, onTimeoutSpy } = buildAggregateTimeoutParams({ waitForCompactionRetry });
|
||||
const params = buildAggregateTimeoutParams({ waitForCompactionRetry });
|
||||
|
||||
const result = await waitForCompactionRetryWithAggregateTimeout(params);
|
||||
|
||||
expect(result.timedOut).toBe(false);
|
||||
expectClearedTimeoutState(onTimeoutSpy, false);
|
||||
expectClearedTimeoutState(params.onTimeout, false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -123,7 +121,7 @@ describe("waitForCompactionRetryWithAggregateTimeout", () => {
|
||||
const abortError = new Error("aborted");
|
||||
abortError.name = "AbortError";
|
||||
const waitForCompactionRetry = vi.fn(async () => await new Promise<void>(() => {}));
|
||||
const { params, onTimeoutSpy } = buildAggregateTimeoutParams({
|
||||
const params = buildAggregateTimeoutParams({
|
||||
waitForCompactionRetry,
|
||||
abortable: async () => {
|
||||
throw abortError;
|
||||
@@ -132,7 +130,7 @@ describe("waitForCompactionRetryWithAggregateTimeout", () => {
|
||||
|
||||
await expect(waitForCompactionRetryWithAggregateTimeout(params)).rejects.toThrow("aborted");
|
||||
|
||||
expectClearedTimeoutState(onTimeoutSpy, false);
|
||||
expectClearedTimeoutState(params.onTimeout, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,16 +2,25 @@ import type { AgentSession } from "@mariozechner/pi-coding-agent";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { applySystemPromptOverrideToSession, createSystemPromptOverride } from "./system-prompt.js";
|
||||
|
||||
type MutableSystemPromptFields = {
|
||||
type MutableSession = {
|
||||
_baseSystemPrompt?: string;
|
||||
_rebuildSystemPrompt?: (toolNames: string[]) => string;
|
||||
};
|
||||
|
||||
function createMockSession() {
|
||||
const setSystemPrompt = vi.fn();
|
||||
type MockSession = MutableSession & {
|
||||
agent: {
|
||||
setSystemPrompt: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
};
|
||||
|
||||
function createMockSession(): {
|
||||
session: MockSession;
|
||||
setSystemPrompt: ReturnType<typeof vi.fn>;
|
||||
} {
|
||||
const setSystemPrompt = vi.fn<(prompt: string) => void>();
|
||||
const session = {
|
||||
agent: { setSystemPrompt },
|
||||
} as unknown as AgentSession;
|
||||
} as MockSession;
|
||||
return { session, setSystemPrompt };
|
||||
}
|
||||
|
||||
@@ -19,9 +28,9 @@ function applyAndGetMutableSession(
|
||||
prompt: Parameters<typeof applySystemPromptOverrideToSession>[1],
|
||||
) {
|
||||
const { session, setSystemPrompt } = createMockSession();
|
||||
applySystemPromptOverrideToSession(session, prompt);
|
||||
applySystemPromptOverrideToSession(session as unknown as AgentSession, prompt);
|
||||
return {
|
||||
mutable: session as unknown as MutableSystemPromptFields,
|
||||
mutable: session,
|
||||
setSystemPrompt,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,12 +8,10 @@ vi.mock("../providers.js", () => ({
|
||||
resolvePluginProviders: (...args: unknown[]) => resolvePluginProvidersMock(...args),
|
||||
}));
|
||||
|
||||
const {
|
||||
buildProviderPluginMethodChoice,
|
||||
resolveProviderModelPickerEntries,
|
||||
resolveProviderPluginChoice,
|
||||
resolveProviderWizardOptions,
|
||||
} = await import("../provider-wizard.js");
|
||||
let buildProviderPluginMethodChoice: typeof import("../provider-wizard.js").buildProviderPluginMethodChoice;
|
||||
let resolveProviderModelPickerEntries: typeof import("../provider-wizard.js").resolveProviderModelPickerEntries;
|
||||
let resolveProviderPluginChoice: typeof import("../provider-wizard.js").resolveProviderPluginChoice;
|
||||
let resolveProviderWizardOptions: typeof import("../provider-wizard.js").resolveProviderWizardOptions;
|
||||
|
||||
function resolveExpectedWizardChoiceValues(providers: ProviderPlugin[]) {
|
||||
const values: string[] = [];
|
||||
@@ -72,7 +70,14 @@ function resolveExpectedModelPickerValues(providers: ProviderPlugin[]) {
|
||||
}
|
||||
|
||||
describe("provider wizard contract", () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
({
|
||||
buildProviderPluginMethodChoice,
|
||||
resolveProviderModelPickerEntries,
|
||||
resolveProviderPluginChoice,
|
||||
resolveProviderWizardOptions,
|
||||
} = await import("../provider-wizard.js"));
|
||||
resolvePluginProvidersMock.mockReset();
|
||||
resolvePluginProvidersMock.mockReturnValue(uniqueProviderContractProviders);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user