mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-02 17:00:22 +00:00
test: stabilize full-suite execution
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
sendExecApprovalFollowup: vi.fn(),
|
||||
logWarn: vi.fn(),
|
||||
resolveExecApprovals: vi.fn(() => ({
|
||||
defaults: {
|
||||
security: "allowlist",
|
||||
@@ -21,14 +19,6 @@ const mocks = vi.hoisted(() => ({
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("./bash-tools.exec-approval-followup.js", () => ({
|
||||
sendExecApprovalFollowup: mocks.sendExecApprovalFollowup,
|
||||
}));
|
||||
|
||||
vi.mock("../logger.js", () => ({
|
||||
logWarn: mocks.logWarn,
|
||||
}));
|
||||
|
||||
vi.mock("../infra/exec-approvals.js", async (importOriginal) => {
|
||||
const mod = await importOriginal<typeof import("../infra/exec-approvals.js")>();
|
||||
return {
|
||||
@@ -43,8 +33,6 @@ let enforceStrictInlineEvalApprovalBoundary: typeof import("./bash-tools.exec-ho
|
||||
let resolveExecHostApprovalContext: typeof import("./bash-tools.exec-host-shared.js").resolveExecHostApprovalContext;
|
||||
let resolveExecApprovalUnavailableState: typeof import("./bash-tools.exec-host-shared.js").resolveExecApprovalUnavailableState;
|
||||
let buildExecApprovalPendingToolResult: typeof import("./bash-tools.exec-host-shared.js").buildExecApprovalPendingToolResult;
|
||||
let sendExecApprovalFollowup: typeof import("./bash-tools.exec-approval-followup.js").sendExecApprovalFollowup;
|
||||
let logWarn: typeof import("../logger.js").logWarn;
|
||||
|
||||
beforeAll(async () => {
|
||||
({
|
||||
@@ -55,14 +43,15 @@ beforeAll(async () => {
|
||||
resolveExecApprovalUnavailableState,
|
||||
buildExecApprovalPendingToolResult,
|
||||
} = await import("./bash-tools.exec-host-shared.js"));
|
||||
({ sendExecApprovalFollowup } = await import("./bash-tools.exec-approval-followup.js"));
|
||||
({ logWarn } = await import("../logger.js"));
|
||||
});
|
||||
|
||||
describe("sendExecApprovalFollowupResult", () => {
|
||||
const sendExecApprovalFollowup = vi.fn();
|
||||
const logWarn = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(sendExecApprovalFollowup).mockReset();
|
||||
vi.mocked(logWarn).mockReset();
|
||||
sendExecApprovalFollowup.mockReset();
|
||||
logWarn.mockReset();
|
||||
mocks.resolveExecApprovals.mockReset();
|
||||
mocks.resolveExecApprovals.mockReturnValue({
|
||||
defaults: {
|
||||
@@ -83,14 +72,15 @@ describe("sendExecApprovalFollowupResult", () => {
|
||||
});
|
||||
|
||||
it("logs repeated followup dispatch failures once per approval id and error message", async () => {
|
||||
vi.mocked(sendExecApprovalFollowup).mockRejectedValue(new Error("Channel is required"));
|
||||
sendExecApprovalFollowup.mockRejectedValue(new Error("Channel is required"));
|
||||
|
||||
const target = {
|
||||
approvalId: "approval-log-once",
|
||||
sessionKey: "agent:main:main",
|
||||
};
|
||||
await sendExecApprovalFollowupResult(target, "Exec finished");
|
||||
await sendExecApprovalFollowupResult(target, "Exec finished");
|
||||
const deps = { sendExecApprovalFollowup, logWarn };
|
||||
await sendExecApprovalFollowupResult(target, "Exec finished", deps);
|
||||
await sendExecApprovalFollowupResult(target, "Exec finished", deps);
|
||||
|
||||
expect(logWarn).toHaveBeenCalledTimes(1);
|
||||
expect(logWarn).toHaveBeenCalledWith(
|
||||
@@ -99,7 +89,8 @@ describe("sendExecApprovalFollowupResult", () => {
|
||||
});
|
||||
|
||||
it("evicts oldest followup failure dedupe keys after reaching the cap", async () => {
|
||||
vi.mocked(sendExecApprovalFollowup).mockRejectedValue(new Error("Channel is required"));
|
||||
sendExecApprovalFollowup.mockRejectedValue(new Error("Channel is required"));
|
||||
const deps = { sendExecApprovalFollowup, logWarn };
|
||||
|
||||
for (let i = 0; i <= maxExecApprovalFollowupFailureLogKeys; i += 1) {
|
||||
await sendExecApprovalFollowupResult(
|
||||
@@ -108,6 +99,7 @@ describe("sendExecApprovalFollowupResult", () => {
|
||||
sessionKey: "agent:main:main",
|
||||
},
|
||||
"Exec finished",
|
||||
deps,
|
||||
);
|
||||
}
|
||||
await sendExecApprovalFollowupResult(
|
||||
@@ -116,6 +108,7 @@ describe("sendExecApprovalFollowupResult", () => {
|
||||
sessionKey: "agent:main:main",
|
||||
},
|
||||
"Exec finished",
|
||||
deps,
|
||||
);
|
||||
|
||||
expect(logWarn).toHaveBeenCalledTimes(maxExecApprovalFollowupFailureLogKeys + 2);
|
||||
|
||||
@@ -90,6 +90,11 @@ export type ExecApprovalFollowupTarget = {
|
||||
turnSourceThreadId?: string | number;
|
||||
};
|
||||
|
||||
export type ExecApprovalFollowupResultDeps = {
|
||||
sendExecApprovalFollowup?: typeof sendExecApprovalFollowup;
|
||||
logWarn?: typeof logWarn;
|
||||
};
|
||||
|
||||
export type DefaultExecApprovalRequestArgs = {
|
||||
warnings: string[];
|
||||
approvalRunningNoticeMs: number;
|
||||
@@ -397,8 +402,11 @@ export function buildHeadlessExecApprovalDeniedMessage(params: {
|
||||
export async function sendExecApprovalFollowupResult(
|
||||
target: ExecApprovalFollowupTarget,
|
||||
resultText: string,
|
||||
deps: ExecApprovalFollowupResultDeps = {},
|
||||
): Promise<void> {
|
||||
await sendExecApprovalFollowup({
|
||||
const send = deps.sendExecApprovalFollowup ?? sendExecApprovalFollowup;
|
||||
const warn = deps.logWarn ?? logWarn;
|
||||
await send({
|
||||
approvalId: target.approvalId,
|
||||
sessionKey: target.sessionKey,
|
||||
turnSourceChannel: target.turnSourceChannel,
|
||||
@@ -412,7 +420,7 @@ export async function sendExecApprovalFollowupResult(
|
||||
if (!rememberExecApprovalFollowupFailureKey(key)) {
|
||||
return;
|
||||
}
|
||||
logWarn(`exec approval followup dispatch failed (id=${target.approvalId}): ${message}`);
|
||||
warn(`exec approval followup dispatch failed (id=${target.approvalId}): ${message}`);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,56 +1,37 @@
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { beforeAll, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.unmock("../plugins/manifest-registry.js");
|
||||
vi.unmock("../secrets/provider-env-vars.js");
|
||||
|
||||
const ORIGINAL_MODELSTUDIO_API_KEY = process.env.MODELSTUDIO_API_KEY;
|
||||
const ORIGINAL_XAI_API_KEY = process.env.XAI_API_KEY;
|
||||
let collectProviderApiKeys: typeof import("./live-auth-keys.js").collectProviderApiKeys;
|
||||
let clearPluginManifestRegistryCache: typeof import("../plugins/manifest-registry.js").clearPluginManifestRegistryCache;
|
||||
|
||||
async function loadModulesForTest(): Promise<void> {
|
||||
({ clearPluginManifestRegistryCache } = await import("../plugins/manifest-registry.js"));
|
||||
({ collectProviderApiKeys } = await import("./live-auth-keys.js"));
|
||||
}
|
||||
|
||||
function clearManifestRegistryCache(): void {
|
||||
clearPluginManifestRegistryCache();
|
||||
}
|
||||
|
||||
describe("collectProviderApiKeys", () => {
|
||||
beforeAll(async () => {
|
||||
vi.doUnmock("../plugins/manifest-registry.js");
|
||||
vi.doUnmock("../secrets/provider-env-vars.js");
|
||||
await loadModulesForTest();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
clearManifestRegistryCache();
|
||||
});
|
||||
it("honors provider auth env vars with nonstandard names", async () => {
|
||||
const env = { MODELSTUDIO_API_KEY: "modelstudio-live-key" };
|
||||
|
||||
afterEach(() => {
|
||||
clearManifestRegistryCache();
|
||||
if (ORIGINAL_MODELSTUDIO_API_KEY === undefined) {
|
||||
delete process.env.MODELSTUDIO_API_KEY;
|
||||
} else {
|
||||
process.env.MODELSTUDIO_API_KEY = ORIGINAL_MODELSTUDIO_API_KEY;
|
||||
}
|
||||
if (ORIGINAL_XAI_API_KEY === undefined) {
|
||||
delete process.env.XAI_API_KEY;
|
||||
} else {
|
||||
process.env.XAI_API_KEY = ORIGINAL_XAI_API_KEY;
|
||||
}
|
||||
});
|
||||
|
||||
it("honors manifest-declared provider auth env vars for nonstandard provider ids", async () => {
|
||||
process.env.MODELSTUDIO_API_KEY = "modelstudio-live-key";
|
||||
|
||||
expect(collectProviderApiKeys("alibaba")).toContain("modelstudio-live-key");
|
||||
expect(
|
||||
collectProviderApiKeys("alibaba", {
|
||||
env,
|
||||
providerEnvVars: ["MODELSTUDIO_API_KEY", "DASHSCOPE_API_KEY"],
|
||||
}),
|
||||
).toEqual(["modelstudio-live-key"]);
|
||||
});
|
||||
|
||||
it("dedupes manifest env vars against direct provider env naming", async () => {
|
||||
process.env.XAI_API_KEY = "xai-live-key";
|
||||
const env = { XAI_API_KEY: "xai-live-key" };
|
||||
|
||||
expect(collectProviderApiKeys("xai")).toEqual(["xai-live-key"]);
|
||||
expect(
|
||||
collectProviderApiKeys("xai", {
|
||||
env,
|
||||
providerEnvVars: ["XAI_API_KEY"],
|
||||
}),
|
||||
).toEqual(["xai-live-key"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,6 +21,11 @@ type ProviderApiKeyConfig = {
|
||||
fallbackVars: string[];
|
||||
};
|
||||
|
||||
type CollectProviderApiKeysOptions = {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
providerEnvVars?: readonly string[];
|
||||
};
|
||||
|
||||
const PROVIDER_API_KEY_CONFIG: Record<string, Omit<ProviderApiKeyConfig, "fallbackVars">> = {
|
||||
anthropic: {
|
||||
liveSingle: "OPENCLAW_LIVE_ANTHROPIC_KEY",
|
||||
@@ -58,9 +63,9 @@ function parseKeyList(raw?: string | null): string[] {
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function collectEnvPrefixedKeys(prefix: string): string[] {
|
||||
function collectEnvPrefixedKeys(prefix: string, env: NodeJS.ProcessEnv): string[] {
|
||||
const keys: string[] = [];
|
||||
for (const [name, value] of Object.entries(process.env)) {
|
||||
for (const [name, value] of Object.entries(env)) {
|
||||
if (!name.startsWith(prefix)) {
|
||||
continue;
|
||||
}
|
||||
@@ -102,28 +107,31 @@ function resolveProviderApiKeyConfig(provider: string): ProviderApiKeyConfig {
|
||||
};
|
||||
}
|
||||
|
||||
export function collectProviderApiKeys(provider: string): string[] {
|
||||
export function collectProviderApiKeys(
|
||||
provider: string,
|
||||
options: CollectProviderApiKeysOptions = {},
|
||||
): string[] {
|
||||
const env = options.env ?? process.env;
|
||||
const normalizedProvider = normalizeProviderId(provider);
|
||||
const config = resolveProviderApiKeyConfig(normalizedProvider);
|
||||
|
||||
const forcedSingle = config.liveSingle
|
||||
? normalizeOptionalString(process.env[config.liveSingle])
|
||||
? normalizeOptionalString(env[config.liveSingle])
|
||||
: undefined;
|
||||
if (forcedSingle) {
|
||||
return [forcedSingle];
|
||||
}
|
||||
|
||||
const fromList = parseKeyList(config.listVar ? process.env[config.listVar] : undefined);
|
||||
const primary = config.primaryVar
|
||||
? normalizeOptionalString(process.env[config.primaryVar])
|
||||
: undefined;
|
||||
const fromPrefixed = config.prefixedVar ? collectEnvPrefixedKeys(config.prefixedVar) : [];
|
||||
const fromList = parseKeyList(config.listVar ? env[config.listVar] : undefined);
|
||||
const primary = config.primaryVar ? normalizeOptionalString(env[config.primaryVar]) : undefined;
|
||||
const fromPrefixed = config.prefixedVar ? collectEnvPrefixedKeys(config.prefixedVar, env) : [];
|
||||
|
||||
const fallback = config.fallbackVars
|
||||
.map((envVar) => normalizeOptionalString(process.env[envVar]))
|
||||
.map((envVar) => normalizeOptionalString(env[envVar]))
|
||||
.filter(Boolean) as string[];
|
||||
const manifestFallback = getProviderEnvVars(normalizedProvider)
|
||||
.map((envVar) => normalizeOptionalString(process.env[envVar]))
|
||||
const manifestEnvVars = options.providerEnvVars ?? getProviderEnvVars(normalizedProvider);
|
||||
const manifestFallback = manifestEnvVars
|
||||
.map((envVar) => normalizeOptionalString(env[envVar]))
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
const seen = new Set<string>();
|
||||
|
||||
Reference in New Issue
Block a user