mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 04:40:43 +00:00
perf: optimize remaining core tests
This commit is contained in:
@@ -52,7 +52,11 @@ function ghGraphQL(query, options = {}) {
|
||||
|
||||
function failOnGraphQLFailure(result, message) {
|
||||
if (result?.gh_failed) {
|
||||
const details = (result.stderr || result.stdout || `gh exited with status ${result.status}`).trim();
|
||||
const details = (
|
||||
result.stderr ||
|
||||
result.stdout ||
|
||||
`gh exited with status ${result.status}`
|
||||
).trim();
|
||||
fail(`${message}: ${details}`);
|
||||
}
|
||||
if (Array.isArray(result?.errors) && result.errors.length > 0) {
|
||||
@@ -73,9 +77,7 @@ function formatGraphQLAfterClause(cursor) {
|
||||
}
|
||||
|
||||
function findDiscussionCommentNode(nodes, discussionCommentDbId) {
|
||||
return (
|
||||
nodes.find((node) => String(node.databaseId) === String(discussionCommentDbId)) || null
|
||||
);
|
||||
return nodes.find((node) => String(node.databaseId) === String(discussionCommentDbId)) || null;
|
||||
}
|
||||
|
||||
function fetchDiscussionReplyPage(commentNodeId, cursor) {
|
||||
@@ -169,9 +171,13 @@ function fetchDiscussionComment(discussionNumber, discussionCommentDbId) {
|
||||
|
||||
while (!reply && hasMoreReplies) {
|
||||
const replyPage = fetchDiscussionReplyPage(topLevelComment.id, replyCursor);
|
||||
failOnGraphQLFailure(replyPage, `Failed to fetch replies for discussion comment ${topLevelComment.id}`);
|
||||
failOnGraphQLFailure(
|
||||
replyPage,
|
||||
`Failed to fetch replies for discussion comment ${topLevelComment.id}`,
|
||||
);
|
||||
const replies = replyPage?.data?.node?.replies;
|
||||
if (!replies) fail(`Failed to paginate replies for discussion comment ${topLevelComment.id}`);
|
||||
if (!replies)
|
||||
fail(`Failed to paginate replies for discussion comment ${topLevelComment.id}`);
|
||||
|
||||
reply = findDiscussionCommentNode(replies.nodes, discussionCommentDbId);
|
||||
hasMoreReplies = replies.pageInfo.hasNextPage;
|
||||
@@ -189,9 +195,7 @@ function fetchDiscussionComment(discussionNumber, discussionCommentDbId) {
|
||||
}
|
||||
|
||||
function createDiscussionComment(discussionNodeId, body, replyToNodeId) {
|
||||
const replyToClause = replyToNodeId
|
||||
? `, replyToId: "${escapeGraphQLString(replyToNodeId)}"`
|
||||
: "";
|
||||
const replyToClause = replyToNodeId ? `, replyToId: "${escapeGraphQLString(replyToNodeId)}"` : "";
|
||||
const result = ghGraphQL(
|
||||
`mutation { addDiscussionComment(input: { discussionId: "${escapeGraphQLString(discussionNodeId)}"${replyToClause}, body: "${escapeGraphQLString(body)}" }) { comment { id url } } }`,
|
||||
);
|
||||
@@ -261,7 +265,10 @@ function cmdFetchContent(locationJson) {
|
||||
const discussionNumber = urlMatch[1];
|
||||
const discussionCommentDbId = urlMatch[2];
|
||||
|
||||
const { discussionId, comment } = fetchDiscussionComment(discussionNumber, discussionCommentDbId);
|
||||
const { discussionId, comment } = fetchDiscussionComment(
|
||||
discussionNumber,
|
||||
discussionCommentDbId,
|
||||
);
|
||||
if (!comment)
|
||||
fail(
|
||||
`Discussion comment #${discussionCommentDbId} not found in discussion #${discussionNumber}`,
|
||||
|
||||
@@ -82,7 +82,10 @@ html.dark .nav-tabs-underline {
|
||||
border-radius: 8px;
|
||||
background: color-mix(in oklab, rgb(var(--primary)) 4%, transparent);
|
||||
text-decoration: none;
|
||||
transition: transform 0.16s ease, border-color 0.16s ease, background 0.16s ease;
|
||||
transition:
|
||||
transform 0.16s ease,
|
||||
border-color 0.16s ease,
|
||||
background 0.16s ease;
|
||||
}
|
||||
|
||||
.showcase-actions a:first-child {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { resolveEmbeddedFullAccessState } from "../../agents/pi-embedded-runner/
|
||||
import { createOpenClawCodingTools } from "../../agents/pi-tools.js";
|
||||
import { resolveSandboxRuntimeStatus } from "../../agents/sandbox.js";
|
||||
import { buildWorkspaceSkillSnapshot } from "../../agents/skills.js";
|
||||
import { getSkillsSnapshotVersion } from "../../agents/skills/refresh.js";
|
||||
import { getSkillsSnapshotVersion } from "../../agents/skills/refresh-state.js";
|
||||
import { buildSystemPromptParams } from "../../agents/system-prompt-params.js";
|
||||
import { buildAgentSystemPrompt } from "../../agents/system-prompt.js";
|
||||
import type { WorkspaceBootstrapFile } from "../../agents/workspace.js";
|
||||
|
||||
@@ -6,10 +6,10 @@ import { canExecRequestNode } from "../../agents/exec-defaults.js";
|
||||
import { buildWorkspaceSkillSnapshot } from "../../agents/skills.js";
|
||||
import { matchesSkillFilter } from "../../agents/skills/filter.js";
|
||||
import {
|
||||
ensureSkillsWatcher,
|
||||
getSkillsSnapshotVersion,
|
||||
shouldRefreshSnapshotForVersion,
|
||||
} from "../../agents/skills/refresh.js";
|
||||
} from "../../agents/skills/refresh-state.js";
|
||||
import { ensureSkillsWatcher } from "../../agents/skills/refresh.js";
|
||||
import {
|
||||
resolveSessionFilePath,
|
||||
resolveSessionFilePathOptions,
|
||||
|
||||
@@ -121,7 +121,9 @@ This is plain user text`;
|
||||
|
||||
it("strips an active-memory prompt prefix block even when earlier text precedes it", () => {
|
||||
const input = `Queued earlier user turn\n\n${ACTIVE_MEMORY_PREFIX_BLOCK}\n\nWhat should I grab on the way?`;
|
||||
expect(stripInboundMetadata(input)).toBe("Queued earlier user turn\n\nWhat should I grab on the way?");
|
||||
expect(stripInboundMetadata(input)).toBe(
|
||||
"Queued earlier user turn\n\nWhat should I grab on the way?",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not strip active-memory lookalike user text without exact tag lines", () => {
|
||||
|
||||
@@ -682,9 +682,9 @@ export function buildStatusMessage(args: StatusArgs): string {
|
||||
const queueDetails = formatQueueDetails(args.queue);
|
||||
const verboseLabel =
|
||||
verboseLevel === "full" ? "verbose:full" : verboseLevel === "on" ? "verbose" : null;
|
||||
const traceLevel = entry?.traceLevel === "raw" ? "raw" : entry?.traceLevel === "on" ? "on" : "off";
|
||||
const traceLabel =
|
||||
traceLevel === "raw" ? "trace:raw" : traceLevel === "on" ? "trace" : null;
|
||||
const traceLevel =
|
||||
entry?.traceLevel === "raw" ? "raw" : entry?.traceLevel === "on" ? "on" : "off";
|
||||
const traceLabel = traceLevel === "raw" ? "trace:raw" : traceLevel === "on" ? "trace" : null;
|
||||
const pluginStatusLines = verboseLevel !== "off" ? resolveSessionPluginStatusLines(entry) : [];
|
||||
const pluginTraceLines =
|
||||
traceLevel === "on" || traceLevel === "raw" ? resolveSessionPluginTraceLines(entry) : [];
|
||||
|
||||
@@ -5,6 +5,7 @@ import { readCommandSource } from "./command-source.test-helpers.js";
|
||||
const SECRET_TARGET_CALLSITES = [
|
||||
bundledPluginFile("memory-core", "src/cli.runtime.ts"),
|
||||
"src/cli/qr-cli.ts",
|
||||
"src/agents/agent-runtime-config.ts",
|
||||
"src/commands/agent.ts",
|
||||
"src/commands/channels/resolve.ts",
|
||||
"src/commands/channels/shared.ts",
|
||||
@@ -16,6 +17,7 @@ const SECRET_TARGET_CALLSITES = [
|
||||
|
||||
function hasSupportedTargetIdsWiring(source: string): boolean {
|
||||
return (
|
||||
/resolveAgentRuntimeConfig\(/.test(source) ||
|
||||
/targetIds:\s*get[A-Za-z0-9_]+\(\)/m.test(source) ||
|
||||
/targetIds:\s*getAgentRuntimeCommandSecretTargetIds\(/m.test(source) ||
|
||||
/targetIds:\s*scopedTargets\.targetIds/m.test(source) ||
|
||||
@@ -25,6 +27,7 @@ function hasSupportedTargetIdsWiring(source: string): boolean {
|
||||
|
||||
function hasSupportedSecretResolutionWiring(source: string): boolean {
|
||||
return (
|
||||
/resolveAgentRuntimeConfig\(/.test(source) ||
|
||||
/resolveCommandConfigWithSecrets\(/.test(source) ||
|
||||
/resolveCommandSecretRefsViaGateway\(/.test(source) ||
|
||||
/collectStatusScanOverview\(/.test(source)
|
||||
|
||||
47
src/config/sessions/store-entry.ts
Normal file
47
src/config/sessions/store-entry.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
|
||||
import type { SessionEntry } from "./types.js";
|
||||
|
||||
export function normalizeStoreSessionKey(sessionKey: string): string {
|
||||
return normalizeLowercaseStringOrEmpty(sessionKey);
|
||||
}
|
||||
|
||||
export function resolveSessionStoreEntry(params: {
|
||||
store: Record<string, SessionEntry>;
|
||||
sessionKey: string;
|
||||
}): {
|
||||
normalizedKey: string;
|
||||
existing: SessionEntry | undefined;
|
||||
legacyKeys: string[];
|
||||
} {
|
||||
const trimmedKey = params.sessionKey.trim();
|
||||
const normalizedKey = normalizeStoreSessionKey(trimmedKey);
|
||||
const legacyKeySet = new Set<string>();
|
||||
if (
|
||||
trimmedKey !== normalizedKey &&
|
||||
Object.prototype.hasOwnProperty.call(params.store, trimmedKey)
|
||||
) {
|
||||
legacyKeySet.add(trimmedKey);
|
||||
}
|
||||
let existing =
|
||||
params.store[normalizedKey] ?? (legacyKeySet.size > 0 ? params.store[trimmedKey] : undefined);
|
||||
let existingUpdatedAt = existing?.updatedAt ?? 0;
|
||||
for (const [candidateKey, candidateEntry] of Object.entries(params.store)) {
|
||||
if (candidateKey === normalizedKey) {
|
||||
continue;
|
||||
}
|
||||
if (normalizeStoreSessionKey(candidateKey) !== normalizedKey) {
|
||||
continue;
|
||||
}
|
||||
legacyKeySet.add(candidateKey);
|
||||
const candidateUpdatedAt = candidateEntry?.updatedAt ?? 0;
|
||||
if (!existing || candidateUpdatedAt > existingUpdatedAt) {
|
||||
existing = candidateEntry;
|
||||
existingUpdatedAt = candidateUpdatedAt;
|
||||
}
|
||||
}
|
||||
return {
|
||||
normalizedKey,
|
||||
existing,
|
||||
legacyKeys: [...legacyKeySet],
|
||||
};
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
import type { MsgContext } from "../../auto-reply/templating.js";
|
||||
import { writeTextAtomic } from "../../infra/json-files.js";
|
||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
|
||||
import {
|
||||
deliveryContextFromSession,
|
||||
mergeDeliveryContext,
|
||||
@@ -25,6 +24,7 @@ import {
|
||||
setSerializedSessionStore,
|
||||
writeSessionStoreCache,
|
||||
} from "./store-cache.js";
|
||||
import { normalizeStoreSessionKey, resolveSessionStoreEntry } from "./store-entry.js";
|
||||
import { loadSessionStore, normalizeSessionStore } from "./store-load.js";
|
||||
import {
|
||||
clearSessionStoreCacheForTest,
|
||||
@@ -55,6 +55,7 @@ export {
|
||||
getSessionStoreLockQueueSizeForTest,
|
||||
} from "./store-lock-state.js";
|
||||
export { loadSessionStore } from "./store-load.js";
|
||||
export { normalizeStoreSessionKey, resolveSessionStoreEntry } from "./store-entry.js";
|
||||
|
||||
const log = createSubsystemLogger("sessions/store");
|
||||
let sessionArchiveRuntimePromise: Promise<
|
||||
@@ -76,51 +77,6 @@ function removeThreadFromDeliveryContext(context?: DeliveryContext): DeliveryCon
|
||||
return next;
|
||||
}
|
||||
|
||||
export function normalizeStoreSessionKey(sessionKey: string): string {
|
||||
return normalizeLowercaseStringOrEmpty(sessionKey);
|
||||
}
|
||||
|
||||
export function resolveSessionStoreEntry(params: {
|
||||
store: Record<string, SessionEntry>;
|
||||
sessionKey: string;
|
||||
}): {
|
||||
normalizedKey: string;
|
||||
existing: SessionEntry | undefined;
|
||||
legacyKeys: string[];
|
||||
} {
|
||||
const trimmedKey = params.sessionKey.trim();
|
||||
const normalizedKey = normalizeStoreSessionKey(trimmedKey);
|
||||
const legacyKeySet = new Set<string>();
|
||||
if (
|
||||
trimmedKey !== normalizedKey &&
|
||||
Object.prototype.hasOwnProperty.call(params.store, trimmedKey)
|
||||
) {
|
||||
legacyKeySet.add(trimmedKey);
|
||||
}
|
||||
let existing =
|
||||
params.store[normalizedKey] ?? (legacyKeySet.size > 0 ? params.store[trimmedKey] : undefined);
|
||||
let existingUpdatedAt = existing?.updatedAt ?? 0;
|
||||
for (const [candidateKey, candidateEntry] of Object.entries(params.store)) {
|
||||
if (candidateKey === normalizedKey) {
|
||||
continue;
|
||||
}
|
||||
if (normalizeStoreSessionKey(candidateKey) !== normalizedKey) {
|
||||
continue;
|
||||
}
|
||||
legacyKeySet.add(candidateKey);
|
||||
const candidateUpdatedAt = candidateEntry?.updatedAt ?? 0;
|
||||
if (!existing || candidateUpdatedAt > existingUpdatedAt) {
|
||||
existing = candidateEntry;
|
||||
existingUpdatedAt = candidateUpdatedAt;
|
||||
}
|
||||
}
|
||||
return {
|
||||
normalizedKey,
|
||||
existing,
|
||||
legacyKeys: [...legacyKeySet],
|
||||
};
|
||||
}
|
||||
|
||||
export function setSessionWriteLockAcquirerForTests(
|
||||
acquirer: typeof acquireSessionWriteLock | null,
|
||||
): void {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export { canExecRequestNode } from "../../agents/exec-defaults.js";
|
||||
export { resolveAgentSkillsFilter } from "../../agents/agent-scope.js";
|
||||
export { buildWorkspaceSkillSnapshot } from "../../agents/skills.js";
|
||||
export { getSkillsSnapshotVersion } from "../../agents/skills/refresh.js";
|
||||
export { getSkillsSnapshotVersion } from "../../agents/skills/refresh-state.js";
|
||||
export { getRemoteSkillEligibility } from "../../infra/skills-remote.js";
|
||||
|
||||
@@ -8,11 +8,8 @@ import {
|
||||
resolveHooksGmailModel,
|
||||
} from "../agents/model-selection.js";
|
||||
import { formatCliCommand } from "../cli/command-format.js";
|
||||
import {
|
||||
maybeRepairLegacyOAuthProfileIds,
|
||||
noteAuthProfileHealth,
|
||||
noteLegacyCodexProviderOverride,
|
||||
} from "../commands/doctor-auth.js";
|
||||
import { maybeRepairLegacyOAuthProfileIds } from "../commands/doctor-auth-legacy-oauth.js";
|
||||
import { noteAuthProfileHealth, noteLegacyCodexProviderOverride } from "../commands/doctor-auth.js";
|
||||
import { noteBootstrapFileSize } from "../commands/doctor-bootstrap-size.js";
|
||||
import { noteChromeMcpBrowserReadiness } from "../commands/doctor-browser.js";
|
||||
import { maybeRepairBundledPluginRuntimeDeps } from "../commands/doctor-bundled-plugin-runtime-deps.js";
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import chokidar from "chokidar";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { listChannelPlugins } from "../channels/plugins/index.js";
|
||||
import type { ChannelPlugin } from "../channels/plugins/types.js";
|
||||
import {
|
||||
getSkillsSnapshotVersion,
|
||||
resetSkillsRefreshStateForTest,
|
||||
} from "../agents/skills/refresh-state.js";
|
||||
import { listChannelPlugins } from "../channels/plugins/index.js";
|
||||
import type { ChannelPlugin } from "../channels/plugins/types.js";
|
||||
import type { ConfigFileSnapshot, ConfigWriteNotification } from "../config/config.js";
|
||||
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { createTestRegistry } from "../test-utils/channel-plugins.js";
|
||||
@@ -622,16 +622,14 @@ describe("startGatewayConfigReloader", () => {
|
||||
});
|
||||
|
||||
it("does not dedupe when initialInternalWriteHash is null (#67436)", async () => {
|
||||
const readSnapshot = vi
|
||||
.fn<() => Promise<ConfigFileSnapshot>>()
|
||||
.mockResolvedValueOnce(
|
||||
makeSnapshot({
|
||||
config: {
|
||||
gateway: { reload: { debounceMs: 0 }, auth: { mode: "token", token: "startup" } },
|
||||
},
|
||||
hash: "startup-internal-1",
|
||||
}),
|
||||
);
|
||||
const readSnapshot = vi.fn<() => Promise<ConfigFileSnapshot>>().mockResolvedValueOnce(
|
||||
makeSnapshot({
|
||||
config: {
|
||||
gateway: { reload: { debounceMs: 0 }, auth: { mode: "token", token: "startup" } },
|
||||
},
|
||||
hash: "startup-internal-1",
|
||||
}),
|
||||
);
|
||||
const harness = createReloaderHarness(readSnapshot, {
|
||||
initialInternalWriteHash: null,
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { ReplyPayload } from "../../auto-reply/reply-payload.js";
|
||||
import { assertLocalMediaAllowed, LocalMediaAccessError } from "../../media/local-media-access.js";
|
||||
import { assertNoWindowsNetworkPath, safeFileURLToPath } from "../../infra/local-file-access.js";
|
||||
import { assertLocalMediaAllowed, LocalMediaAccessError } from "../../media/local-media-access.js";
|
||||
import { isAudioFileName } from "../../media/mime.js";
|
||||
import { resolveSendableOutboundReplyParts } from "../../plugin-sdk/reply-payload.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
|
||||
|
||||
@@ -3,7 +3,6 @@ import { constants as fsConstants } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import * as tar from "tar";
|
||||
import {
|
||||
buildBackupArchiveBasename,
|
||||
buildBackupArchivePath,
|
||||
@@ -15,6 +14,15 @@ import { isPathWithin } from "../commands/cleanup-utils.js";
|
||||
import { resolveHomeDir, resolveUserPath } from "../utils.js";
|
||||
import { resolveRuntimeServiceVersion } from "../version.js";
|
||||
|
||||
type TarRuntime = typeof import("tar");
|
||||
|
||||
let tarRuntimePromise: Promise<TarRuntime> | undefined;
|
||||
|
||||
function loadTarRuntime(): Promise<TarRuntime> {
|
||||
tarRuntimePromise ??= import("tar");
|
||||
return tarRuntimePromise;
|
||||
}
|
||||
|
||||
export type BackupCreateOptions = {
|
||||
output?: string;
|
||||
dryRun?: boolean;
|
||||
@@ -342,6 +350,7 @@ export async function createBackupArchive(
|
||||
});
|
||||
await fs.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
||||
|
||||
const tar = await loadTarRuntime();
|
||||
await tar.c(
|
||||
{
|
||||
file: tempArchivePath,
|
||||
|
||||
440
src/plugins/provider-auth-input.test.ts
Normal file
440
src/plugins/provider-auth-input.test.ts
Normal file
@@ -0,0 +1,440 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||
import {
|
||||
ensureApiKeyFromEnvOrPrompt,
|
||||
ensureApiKeyFromOptionEnvOrPrompt,
|
||||
maybeApplyApiKeyFromOption,
|
||||
normalizeTokenProviderInput,
|
||||
} from "./provider-auth-input.js";
|
||||
|
||||
const ORIGINAL_MINIMAX_API_KEY = process.env.MINIMAX_API_KEY;
|
||||
const ORIGINAL_MINIMAX_OAUTH_TOKEN = process.env.MINIMAX_OAUTH_TOKEN;
|
||||
|
||||
function restoreMinimaxEnv(): void {
|
||||
if (ORIGINAL_MINIMAX_API_KEY === undefined) {
|
||||
delete process.env.MINIMAX_API_KEY;
|
||||
} else {
|
||||
process.env.MINIMAX_API_KEY = ORIGINAL_MINIMAX_API_KEY;
|
||||
}
|
||||
if (ORIGINAL_MINIMAX_OAUTH_TOKEN === undefined) {
|
||||
delete process.env.MINIMAX_OAUTH_TOKEN;
|
||||
} else {
|
||||
process.env.MINIMAX_OAUTH_TOKEN = ORIGINAL_MINIMAX_OAUTH_TOKEN;
|
||||
}
|
||||
}
|
||||
|
||||
function createPrompter(params?: {
|
||||
confirm?: WizardPrompter["confirm"];
|
||||
note?: WizardPrompter["note"];
|
||||
select?: WizardPrompter["select"];
|
||||
text?: WizardPrompter["text"];
|
||||
}): WizardPrompter {
|
||||
return {
|
||||
confirm: params?.confirm ?? (vi.fn(async () => true) as WizardPrompter["confirm"]),
|
||||
note: params?.note ?? (vi.fn(async () => undefined) as WizardPrompter["note"]),
|
||||
...(params?.select ? { select: params.select } : {}),
|
||||
text: params?.text ?? (vi.fn(async () => "prompt-key") as WizardPrompter["text"]),
|
||||
} as unknown as WizardPrompter;
|
||||
}
|
||||
|
||||
function createPromptSpies(params?: { confirmResult?: boolean; textResult?: string }) {
|
||||
const confirm = vi.fn(async () => params?.confirmResult ?? true);
|
||||
const note = vi.fn(async () => undefined);
|
||||
const text = vi.fn(async () => params?.textResult ?? "prompt-key");
|
||||
return { confirm, note, text };
|
||||
}
|
||||
|
||||
function createPromptAndCredentialSpies(params?: { confirmResult?: boolean; textResult?: string }) {
|
||||
return {
|
||||
...createPromptSpies(params),
|
||||
setCredential: vi.fn(async () => undefined),
|
||||
};
|
||||
}
|
||||
|
||||
function setMinimaxEnv(params: { apiKey?: string; oauthToken?: string } = {}) {
|
||||
if (params.apiKey === undefined) {
|
||||
delete process.env.MINIMAX_API_KEY;
|
||||
} else {
|
||||
process.env.MINIMAX_API_KEY = params.apiKey; // pragma: allowlist secret
|
||||
}
|
||||
if (params.oauthToken === undefined) {
|
||||
delete process.env.MINIMAX_OAUTH_TOKEN;
|
||||
} else {
|
||||
process.env.MINIMAX_OAUTH_TOKEN = params.oauthToken; // pragma: allowlist secret
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureMinimaxApiKey(params: {
|
||||
config?: Parameters<typeof ensureApiKeyFromEnvOrPrompt>[0]["config"];
|
||||
env?: Parameters<typeof ensureApiKeyFromEnvOrPrompt>[0]["env"];
|
||||
confirm: WizardPrompter["confirm"];
|
||||
note?: WizardPrompter["note"];
|
||||
select?: WizardPrompter["select"];
|
||||
text: WizardPrompter["text"];
|
||||
setCredential: Parameters<typeof ensureApiKeyFromEnvOrPrompt>[0]["setCredential"];
|
||||
secretInputMode?: Parameters<typeof ensureApiKeyFromEnvOrPrompt>[0]["secretInputMode"];
|
||||
}) {
|
||||
return await ensureMinimaxApiKeyInternal({
|
||||
config: params.config,
|
||||
env: params.env,
|
||||
prompter: createPrompter({
|
||||
confirm: params.confirm,
|
||||
note: params.note,
|
||||
select: params.select,
|
||||
text: params.text,
|
||||
}),
|
||||
secretInputMode: params.secretInputMode,
|
||||
setCredential: params.setCredential,
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureMinimaxApiKeyInternal(params: {
|
||||
config?: Parameters<typeof ensureApiKeyFromEnvOrPrompt>[0]["config"];
|
||||
env?: Parameters<typeof ensureApiKeyFromEnvOrPrompt>[0]["env"];
|
||||
prompter: WizardPrompter;
|
||||
secretInputMode?: Parameters<typeof ensureApiKeyFromEnvOrPrompt>[0]["secretInputMode"];
|
||||
setCredential: Parameters<typeof ensureApiKeyFromEnvOrPrompt>[0]["setCredential"];
|
||||
}) {
|
||||
return await ensureApiKeyFromEnvOrPrompt({
|
||||
config: params.config ?? {},
|
||||
env: params.env,
|
||||
provider: "minimax",
|
||||
envLabel: "MINIMAX_API_KEY",
|
||||
promptMessage: "Enter key",
|
||||
normalize: (value) => value.trim(),
|
||||
validate: () => undefined,
|
||||
prompter: params.prompter,
|
||||
secretInputMode: params.secretInputMode,
|
||||
setCredential: params.setCredential,
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureMinimaxApiKeyWithEnvRefPrompter(params: {
|
||||
config?: Parameters<typeof ensureApiKeyFromEnvOrPrompt>[0]["config"];
|
||||
env?: Parameters<typeof ensureApiKeyFromEnvOrPrompt>[0]["env"];
|
||||
note: WizardPrompter["note"];
|
||||
select: WizardPrompter["select"];
|
||||
setCredential: Parameters<typeof ensureApiKeyFromEnvOrPrompt>[0]["setCredential"];
|
||||
text: WizardPrompter["text"];
|
||||
}) {
|
||||
return await ensureMinimaxApiKeyInternal({
|
||||
config: params.config,
|
||||
env: params.env,
|
||||
prompter: createPrompter({ select: params.select, text: params.text, note: params.note }),
|
||||
secretInputMode: "ref", // pragma: allowlist secret
|
||||
setCredential: params.setCredential,
|
||||
});
|
||||
}
|
||||
|
||||
async function runEnsureMinimaxApiKeyFlow(params: { confirmResult: boolean; textResult: string }) {
|
||||
setMinimaxEnv({ apiKey: "env-key" });
|
||||
|
||||
const { confirm, text } = createPromptSpies({
|
||||
confirmResult: params.confirmResult,
|
||||
textResult: params.textResult,
|
||||
});
|
||||
const setCredential = vi.fn(async () => undefined);
|
||||
const result = await ensureMinimaxApiKey({
|
||||
confirm,
|
||||
text,
|
||||
setCredential,
|
||||
});
|
||||
|
||||
return { result, setCredential, confirm, text };
|
||||
}
|
||||
|
||||
async function runMaybeApplyDemoToken(tokenProvider: string) {
|
||||
const setCredential = vi.fn(async () => undefined);
|
||||
const result = await maybeApplyApiKeyFromOption({
|
||||
token: " opt-key ",
|
||||
tokenProvider,
|
||||
expectedProviders: ["demo-provider"],
|
||||
normalize: (value) => value.trim(),
|
||||
setCredential,
|
||||
});
|
||||
return { result, setCredential };
|
||||
}
|
||||
|
||||
function expectMinimaxEnvRefCredentialStored(setCredential: ReturnType<typeof vi.fn>) {
|
||||
expect(setCredential).toHaveBeenCalledWith(
|
||||
{ source: "env", provider: "default", id: "MINIMAX_API_KEY" },
|
||||
"ref",
|
||||
);
|
||||
}
|
||||
|
||||
async function ensureWithOptionEnvOrPrompt(params: {
|
||||
token: string;
|
||||
tokenProvider: string;
|
||||
expectedProviders: string[];
|
||||
provider: string;
|
||||
envLabel: string;
|
||||
confirm: WizardPrompter["confirm"];
|
||||
note: WizardPrompter["note"];
|
||||
noteMessage: string;
|
||||
noteTitle: string;
|
||||
setCredential: Parameters<typeof ensureApiKeyFromOptionEnvOrPrompt>[0]["setCredential"];
|
||||
text: WizardPrompter["text"];
|
||||
}) {
|
||||
return await ensureApiKeyFromOptionEnvOrPrompt({
|
||||
token: params.token,
|
||||
tokenProvider: params.tokenProvider,
|
||||
config: {},
|
||||
expectedProviders: params.expectedProviders,
|
||||
provider: params.provider,
|
||||
envLabel: params.envLabel,
|
||||
promptMessage: "Enter key",
|
||||
normalize: (value) => value.trim(),
|
||||
validate: () => undefined,
|
||||
prompter: createPrompter({ confirm: params.confirm, note: params.note, text: params.text }),
|
||||
setCredential: params.setCredential,
|
||||
noteMessage: params.noteMessage,
|
||||
noteTitle: params.noteTitle,
|
||||
});
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
restoreMinimaxEnv();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("normalizeTokenProviderInput", () => {
|
||||
it("trims and lowercases non-empty values", () => {
|
||||
expect(normalizeTokenProviderInput(" DeMo-PrOvIdEr ")).toBe("demo-provider");
|
||||
expect(normalizeTokenProviderInput("")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("maybeApplyApiKeyFromOption", () => {
|
||||
it.each(["demo-provider", " DeMo-PrOvIdEr "])(
|
||||
"stores normalized token when provider %p matches",
|
||||
async (tokenProvider) => {
|
||||
const { result, setCredential } = await runMaybeApplyDemoToken(tokenProvider);
|
||||
|
||||
expect(result).toBe("opt-key");
|
||||
expect(setCredential).toHaveBeenCalledWith("opt-key", undefined);
|
||||
},
|
||||
);
|
||||
|
||||
it("skips when provider does not match", async () => {
|
||||
const setCredential = vi.fn(async () => undefined);
|
||||
|
||||
const result = await maybeApplyApiKeyFromOption({
|
||||
token: "opt-key",
|
||||
tokenProvider: "other-provider",
|
||||
expectedProviders: ["demo-provider"],
|
||||
normalize: (value) => value.trim(),
|
||||
setCredential,
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(setCredential).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ensureApiKeyFromEnvOrPrompt", () => {
|
||||
it("uses env credential when user confirms", async () => {
|
||||
const { result, setCredential, text } = await runEnsureMinimaxApiKeyFlow({
|
||||
confirmResult: true,
|
||||
textResult: "prompt-key",
|
||||
});
|
||||
|
||||
expect(result).toBe("env-key");
|
||||
expect(setCredential).toHaveBeenCalledWith("env-key", "plaintext");
|
||||
expect(text).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to prompt when env is declined", async () => {
|
||||
const { result, setCredential, text } = await runEnsureMinimaxApiKeyFlow({
|
||||
confirmResult: false,
|
||||
textResult: " prompted-key ",
|
||||
});
|
||||
|
||||
expect(result).toBe("prompted-key");
|
||||
expect(setCredential).toHaveBeenCalledWith("prompted-key", "plaintext");
|
||||
expect(text).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: "Enter key",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses explicit inline env ref when secret-input-mode=ref selects existing env key", async () => {
|
||||
setMinimaxEnv({ apiKey: "env-key" });
|
||||
|
||||
const { confirm, text, setCredential } = createPromptAndCredentialSpies({
|
||||
confirmResult: true,
|
||||
textResult: "prompt-key",
|
||||
});
|
||||
|
||||
const result = await ensureMinimaxApiKey({
|
||||
confirm,
|
||||
text,
|
||||
secretInputMode: "ref", // pragma: allowlist secret
|
||||
setCredential,
|
||||
});
|
||||
|
||||
expect(result).toBe("env-key");
|
||||
expectMinimaxEnvRefCredentialStored(setCredential);
|
||||
expect(text).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("fails ref mode without select when fallback env var is missing", async () => {
|
||||
setMinimaxEnv();
|
||||
|
||||
const { confirm, text, setCredential } = createPromptAndCredentialSpies({
|
||||
confirmResult: true,
|
||||
textResult: "prompt-key",
|
||||
});
|
||||
|
||||
await expect(
|
||||
ensureMinimaxApiKey({
|
||||
confirm,
|
||||
text,
|
||||
secretInputMode: "ref", // pragma: allowlist secret
|
||||
setCredential,
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
'Environment variable "MINIMAX_API_KEY" is required for --secret-input-mode ref in non-interactive setup.',
|
||||
);
|
||||
expect(setCredential).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses explicit env for ref fallback instead of host process env", async () => {
|
||||
setMinimaxEnv({ apiKey: "host-key" });
|
||||
const env = { MINIMAX_API_KEY: "explicit-key" } as NodeJS.ProcessEnv;
|
||||
|
||||
const { confirm, text, setCredential } = createPromptAndCredentialSpies({
|
||||
confirmResult: true,
|
||||
textResult: "prompt-key",
|
||||
});
|
||||
|
||||
const result = await ensureMinimaxApiKey({
|
||||
confirm,
|
||||
text,
|
||||
env,
|
||||
secretInputMode: "ref", // pragma: allowlist secret
|
||||
setCredential,
|
||||
});
|
||||
|
||||
expect(result).toBe("explicit-key");
|
||||
expectMinimaxEnvRefCredentialStored(setCredential);
|
||||
});
|
||||
|
||||
it("re-prompts after provider ref validation failure and succeeds with env ref", async () => {
|
||||
setMinimaxEnv({ apiKey: "env-key" });
|
||||
|
||||
const selectValues: Array<"provider" | "env" | "filemain"> = ["provider", "filemain", "env"];
|
||||
const select = vi.fn(async () => selectValues.shift() ?? "env") as WizardPrompter["select"];
|
||||
const text = vi
|
||||
.fn<WizardPrompter["text"]>()
|
||||
.mockResolvedValueOnce("/providers/minimax/apiKey")
|
||||
.mockResolvedValueOnce("MINIMAX_API_KEY");
|
||||
const note = vi.fn(async () => undefined);
|
||||
const setCredential = vi.fn(async () => undefined);
|
||||
|
||||
const result = await ensureMinimaxApiKeyWithEnvRefPrompter({
|
||||
config: {
|
||||
secrets: {
|
||||
providers: {
|
||||
filemain: {
|
||||
source: "file",
|
||||
path: "/tmp/does-not-exist-secrets.json",
|
||||
mode: "json",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
select,
|
||||
text,
|
||||
note,
|
||||
setCredential,
|
||||
});
|
||||
|
||||
expect(result).toBe("env-key");
|
||||
expectMinimaxEnvRefCredentialStored(setCredential);
|
||||
expect(note).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Could not validate provider reference"),
|
||||
"Reference check failed",
|
||||
);
|
||||
});
|
||||
|
||||
it("never includes resolved env secret values in reference validation notes", async () => {
|
||||
setMinimaxEnv({ apiKey: "sk-minimax-redacted-value" });
|
||||
|
||||
const select = vi.fn(async () => "env") as WizardPrompter["select"];
|
||||
const text = vi.fn<WizardPrompter["text"]>().mockResolvedValue("MINIMAX_API_KEY");
|
||||
const note = vi.fn(async () => undefined);
|
||||
const setCredential = vi.fn(async () => undefined);
|
||||
|
||||
const result = await ensureMinimaxApiKeyWithEnvRefPrompter({
|
||||
config: {},
|
||||
select,
|
||||
text,
|
||||
note,
|
||||
setCredential,
|
||||
});
|
||||
|
||||
expect(result).toBe("sk-minimax-redacted-value");
|
||||
const noteMessages = note.mock.calls.map((call) => call.at(0) ?? "").join("\n");
|
||||
expect(noteMessages).toContain("Validated environment variable MINIMAX_API_KEY.");
|
||||
expect(noteMessages).not.toContain("sk-minimax-redacted-value");
|
||||
});
|
||||
});
|
||||
|
||||
describe("ensureApiKeyFromOptionEnvOrPrompt", () => {
|
||||
it("uses opts token and skips note/env/prompt", async () => {
|
||||
const { confirm, note, text, setCredential } = createPromptAndCredentialSpies({
|
||||
confirmResult: true,
|
||||
textResult: "prompt-key",
|
||||
});
|
||||
|
||||
const result = await ensureWithOptionEnvOrPrompt({
|
||||
token: " opts-key ",
|
||||
tokenProvider: " DEMO-PROVIDER ",
|
||||
expectedProviders: ["demo-provider"],
|
||||
provider: "demo-provider",
|
||||
envLabel: "DEMO_TOKEN",
|
||||
confirm,
|
||||
note,
|
||||
noteMessage: "Demo note",
|
||||
noteTitle: "Demo",
|
||||
setCredential,
|
||||
text,
|
||||
});
|
||||
|
||||
expect(result).toBe("opts-key");
|
||||
expect(setCredential).toHaveBeenCalledWith("opts-key", undefined);
|
||||
expect(note).not.toHaveBeenCalled();
|
||||
expect(confirm).not.toHaveBeenCalled();
|
||||
expect(text).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to env flow and shows note when opts provider does not match", async () => {
|
||||
setMinimaxEnv({ apiKey: "env-key" });
|
||||
|
||||
const { confirm, note, text, setCredential } = createPromptAndCredentialSpies({
|
||||
confirmResult: true,
|
||||
textResult: "prompt-key",
|
||||
});
|
||||
|
||||
const result = await ensureWithOptionEnvOrPrompt({
|
||||
token: "opts-key",
|
||||
tokenProvider: "other-provider",
|
||||
expectedProviders: ["minimax"],
|
||||
provider: "minimax",
|
||||
envLabel: "MINIMAX_API_KEY",
|
||||
confirm,
|
||||
note,
|
||||
noteMessage: "Demo provider note",
|
||||
noteTitle: "Demo provider",
|
||||
setCredential,
|
||||
text,
|
||||
});
|
||||
|
||||
expect(result).toBe("env-key");
|
||||
expect(note).toHaveBeenCalledWith("Demo provider note", "Demo provider");
|
||||
expect(confirm).toHaveBeenCalled();
|
||||
expect(text).not.toHaveBeenCalled();
|
||||
expect(setCredential).toHaveBeenCalledWith("env-key", "plaintext");
|
||||
});
|
||||
});
|
||||
@@ -18,9 +18,7 @@ describe("formatDocsLink", () => {
|
||||
});
|
||||
|
||||
it("does not crash when path is undefined (regression: #67076, #67074)", () => {
|
||||
expect(() =>
|
||||
formatDocsLink(undefined as unknown as string, "label"),
|
||||
).not.toThrow();
|
||||
expect(() => formatDocsLink(undefined as unknown as string, "label")).not.toThrow();
|
||||
const out = formatDocsLink(undefined as unknown as string, "label");
|
||||
expect(out).toContain("https://docs.openclaw.ai");
|
||||
});
|
||||
|
||||
@@ -274,7 +274,6 @@ md.linkify.add("www", {
|
||||
break;
|
||||
}
|
||||
return len;
|
||||
|
||||
},
|
||||
normalize(match) {
|
||||
match.url = "http://" + match.url;
|
||||
|
||||
Reference in New Issue
Block a user