diff --git a/extensions/msteams/src/messenger.ts b/extensions/msteams/src/messenger.ts index 44b1e836376..11b04db8eb7 100644 --- a/extensions/msteams/src/messenger.ts +++ b/extensions/msteams/src/messenger.ts @@ -6,6 +6,7 @@ import { type MSTeamsReplyStyle, type ReplyPayload, SILENT_REPLY_TOKEN, + sleep, } from "openclaw/plugin-sdk"; import type { MSTeamsAccessTokenProvider } from "./attachments/types.js"; import type { StoredConversationReference } from "./conversation-store.js"; @@ -166,16 +167,6 @@ function clampMs(value: number, maxMs: number): number { return Math.min(value, maxMs); } -async function sleep(ms: number): Promise { - const delay = Math.max(0, ms); - if (delay === 0) { - return; - } - await new Promise((resolve) => { - setTimeout(resolve, delay); - }); -} - function resolveRetryOptions( retry: false | MSTeamsSendRetryOptions | undefined, ): Required & { enabled: boolean } { diff --git a/extensions/voice-call/src/cli.ts b/extensions/voice-call/src/cli.ts index 207ee546ccd..0707821c465 100644 --- a/extensions/voice-call/src/cli.ts +++ b/extensions/voice-call/src/cli.ts @@ -2,6 +2,7 @@ import type { Command } from "commander"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import { sleep } from "openclaw/plugin-sdk"; import type { VoiceCallConfig } from "./config.js"; import type { VoiceCallRuntime } from "./runtime.js"; import { resolveUserPath } from "./utils.js"; @@ -40,10 +41,6 @@ function resolveDefaultStorePath(config: VoiceCallConfig): string { return path.join(base, "calls.jsonl"); } -function sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - export function registerVoiceCallCli(params: { program: Command; config: VoiceCallConfig; diff --git a/extensions/whatsapp/src/channel.ts b/extensions/whatsapp/src/channel.ts index 3f127e1e1ca..4bce169aa12 100644 --- a/extensions/whatsapp/src/channel.ts +++ b/extensions/whatsapp/src/channel.ts @@ -4,6 +4,7 @@ import { collectWhatsAppStatusIssues, createActionGate, DEFAULT_ACCOUNT_ID, + escapeRegExp, formatPairingApproveHint, getChatChannelMeta, isWhatsAppGroupJid, @@ -33,8 +34,6 @@ import { getWhatsAppRuntime } from "./runtime.js"; const meta = getChatChannelMeta("whatsapp"); -const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - export const whatsappPlugin: ChannelPlugin = { id: "whatsapp", meta: { diff --git a/extensions/zalouser/src/zca.ts b/extensions/zalouser/src/zca.ts index 3e20984acad..841f448a4c1 100644 --- a/extensions/zalouser/src/zca.ts +++ b/extensions/zalouser/src/zca.ts @@ -1,4 +1,5 @@ import { spawn, type SpawnOptions } from "node:child_process"; +import { stripAnsi } from "openclaw/plugin-sdk"; import type { ZcaResult, ZcaRunOptions } from "./types.js"; const ZCA_BINARY = "zca"; @@ -107,11 +108,6 @@ export function runZcaInteractive(args: string[], options?: ZcaRunOptions): Prom }); } -function stripAnsi(str: string): string { - // oxlint-disable-next-line no-control-regex - return str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, ""); -} - export function parseJsonOutput(stdout: string): T | null { try { return JSON.parse(stdout) as T; diff --git a/src/agents/bash-tools.exec.ts b/src/agents/bash-tools.exec.ts index a771f85879e..22af022a7d4 100644 --- a/src/agents/bash-tools.exec.ts +++ b/src/agents/bash-tools.exec.ts @@ -43,7 +43,7 @@ import { buildDockerExecArgs, buildSandboxEnv, chunkString, - clampNumber, + clampWithDefault, coerceEnv, killSession, readEnvInt, @@ -105,13 +105,13 @@ function validateHostEnv(env: Record): void { } } } -const DEFAULT_MAX_OUTPUT = clampNumber( +const DEFAULT_MAX_OUTPUT = clampWithDefault( readEnvInt("PI_BASH_MAX_OUTPUT_CHARS"), 200_000, 1_000, 200_000, ); -const DEFAULT_PENDING_MAX_OUTPUT = clampNumber( +const DEFAULT_PENDING_MAX_OUTPUT = clampWithDefault( readEnvInt("OPENCLAW_BASH_PENDING_MAX_OUTPUT_CHARS"), 200_000, 1_000, @@ -801,7 +801,7 @@ export function createExecTool( defaults?: ExecToolDefaults, // oxlint-disable-next-line typescript/no-explicit-any ): AgentTool { - const defaultBackgroundMs = clampNumber( + const defaultBackgroundMs = clampWithDefault( defaults?.backgroundMs ?? readEnvInt("PI_BASH_YIELD_MS"), 10_000, 10, @@ -860,7 +860,12 @@ export function createExecTool( const yieldWindow = allowBackground ? backgroundRequested ? 0 - : clampNumber(params.yieldMs ?? defaultBackgroundMs, defaultBackgroundMs, 10, 120_000) + : clampWithDefault( + params.yieldMs ?? defaultBackgroundMs, + defaultBackgroundMs, + 10, + 120_000, + ) : null; const elevatedDefaults = defaults?.elevated; const elevatedAllowed = Boolean(elevatedDefaults?.enabled && elevatedDefaults.allowed); diff --git a/src/agents/bash-tools.shared.ts b/src/agents/bash-tools.shared.ts index f0cb672d8fb..99a7a4b792f 100644 --- a/src/agents/bash-tools.shared.ts +++ b/src/agents/bash-tools.shared.ts @@ -146,7 +146,10 @@ function safeCwd() { } } -export function clampNumber( +/** + * Clamp a number within min/max bounds, using defaultValue if undefined or NaN. + */ +export function clampWithDefault( value: number | undefined, defaultValue: number, min: number, diff --git a/src/agents/cli-runner/helpers.ts b/src/agents/cli-runner/helpers.ts index d19885d26e1..0066681a67a 100644 --- a/src/agents/cli-runner/helpers.ts +++ b/src/agents/cli-runner/helpers.ts @@ -10,6 +10,7 @@ import type { CliBackendConfig } from "../../config/types.js"; import type { EmbeddedContextFile } from "../pi-embedded-helpers.js"; import { runExec } from "../../process/exec.js"; import { buildTtsSystemPromptHint } from "../../tts/tts.js"; +import { escapeRegExp } from "../../utils.js"; import { resolveDefaultModelForAgent } from "../model-selection.js"; import { detectRuntimeShell } from "../shell-utils.js"; import { buildSystemPromptParams } from "../system-prompt-params.js"; @@ -17,10 +18,6 @@ import { buildAgentSystemPrompt } from "../system-prompt.js"; const CLI_RUN_QUEUE = new Map>(); -function escapeRegex(value: string): string { - return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -} - export async function cleanupResumeProcesses( backend: CliBackendConfig, sessionId: string, @@ -43,7 +40,7 @@ export async function cleanupResumeProcesses( const resumeTokens = resumeArgs.map((arg) => arg.replaceAll("{sessionId}", sessionId)); const pattern = [commandToken, ...resumeTokens] .filter(Boolean) - .map((token) => escapeRegex(token)) + .map((token) => escapeRegExp(token)) .join(".*"); if (!pattern) { return; @@ -95,9 +92,9 @@ function buildSessionMatchers(backend: CliBackendConfig): RegExp[] { function tokenToRegex(token: string): string { if (!token.includes("{sessionId}")) { - return escapeRegex(token); + return escapeRegExp(token); } - const parts = token.split("{sessionId}").map((part) => escapeRegex(part)); + const parts = token.split("{sessionId}").map((part) => escapeRegExp(part)); return parts.join("\\S+"); } diff --git a/src/agents/pty-keys.ts b/src/agents/pty-keys.ts index 0c6df8ca3ef..d221f3c699e 100644 --- a/src/agents/pty-keys.ts +++ b/src/agents/pty-keys.ts @@ -1,3 +1,5 @@ +import { escapeRegExp } from "../utils.js"; + const ESC = "\x1b"; const CR = "\r"; const TAB = "\t"; @@ -12,10 +14,6 @@ type Modifiers = { shift: boolean; }; -function escapeRegExp(value: string) { - return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -} - const namedKeyMap = new Map([ ["enter", CR], ["return", CR], diff --git a/src/auto-reply/commands-registry.ts b/src/auto-reply/commands-registry.ts index bff1c376455..facd7723d5c 100644 --- a/src/auto-reply/commands-registry.ts +++ b/src/auto-reply/commands-registry.ts @@ -14,6 +14,7 @@ import type { } from "./commands-registry.types.js"; import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js"; import { resolveConfiguredModelRef } from "../agents/model-selection.js"; +import { escapeRegExp } from "../utils.js"; import { getChatCommands, getNativeCommandSurfaces } from "./commands-registry.data.js"; export type { @@ -68,10 +69,6 @@ function getTextAliasMap(): Map { return map; } -function escapeRegExp(value: string) { - return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -} - function buildSkillCommandDefinitions(skillCommands?: SkillCommandSpec[]): ChatCommandDefinition[] { if (!skillCommands || skillCommands.length === 0) { return []; diff --git a/src/auto-reply/model.ts b/src/auto-reply/model.ts index b330d0a9fbb..081070f3f9b 100644 --- a/src/auto-reply/model.ts +++ b/src/auto-reply/model.ts @@ -1,6 +1,4 @@ -function escapeRegExp(value: string) { - return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -} +import { escapeRegExp } from "../utils.js"; export function extractModelDirective( body?: string, diff --git a/src/auto-reply/reply/directives.ts b/src/auto-reply/reply/directives.ts index 43139791564..bb08801b4cc 100644 --- a/src/auto-reply/reply/directives.ts +++ b/src/auto-reply/reply/directives.ts @@ -1,4 +1,5 @@ import type { NoticeLevel, ReasoningLevel } from "../thinking.js"; +import { escapeRegExp } from "../../utils.js"; import { type ElevatedLevel, normalizeElevatedLevel, @@ -17,8 +18,6 @@ type ExtractedLevel = { hasDirective: boolean; }; -const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - const matchLevelDirective = ( body: string, names: string[], diff --git a/src/auto-reply/reply/inbound-sender-meta.ts b/src/auto-reply/reply/inbound-sender-meta.ts index 5e8ce704ff2..df78b15fc41 100644 --- a/src/auto-reply/reply/inbound-sender-meta.ts +++ b/src/auto-reply/reply/inbound-sender-meta.ts @@ -1,6 +1,7 @@ import type { MsgContext } from "../templating.js"; import { normalizeChatType } from "../../channels/chat-type.js"; import { listSenderLabelCandidates, resolveSenderLabel } from "../../channels/sender-label.js"; +import { escapeRegExp } from "../../utils.js"; export function formatInboundBodyWithSenderMeta(params: { body: string; ctx: MsgContext }): string { const body = params.body; @@ -51,7 +52,3 @@ function hasSenderMetaLine(body: string, ctx: MsgContext): boolean { return pattern.test(body); }); } - -function escapeRegExp(value: string): string { - return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -} diff --git a/src/auto-reply/reply/mentions.ts b/src/auto-reply/reply/mentions.ts index 07def8de980..d0a6c253d0d 100644 --- a/src/auto-reply/reply/mentions.ts +++ b/src/auto-reply/reply/mentions.ts @@ -3,10 +3,7 @@ import type { MsgContext } from "../templating.js"; import { resolveAgentConfig } from "../../agents/agent-scope.js"; import { getChannelDock } from "../../channels/dock.js"; import { normalizeChannelId } from "../../channels/plugins/index.js"; - -function escapeRegExp(text: string): string { - return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -} +import { escapeRegExp } from "../../utils.js"; function deriveMentionPatterns(identity?: { name?: string; emoji?: string }) { const patterns: string[] = []; diff --git a/src/auto-reply/tokens.ts b/src/auto-reply/tokens.ts index 62b4f091409..b305391dcd0 100644 --- a/src/auto-reply/tokens.ts +++ b/src/auto-reply/tokens.ts @@ -1,10 +1,8 @@ +import { escapeRegExp } from "../utils.js"; + export const HEARTBEAT_TOKEN = "HEARTBEAT_OK"; export const SILENT_REPLY_TOKEN = "NO_REPLY"; -function escapeRegExp(value: string): string { - return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -} - export function isSilentReplyText( text: string | undefined, token: string = SILENT_REPLY_TOKEN, diff --git a/src/browser/routes/dispatcher.ts b/src/browser/routes/dispatcher.ts index 8610a6138c7..39a6535014e 100644 --- a/src/browser/routes/dispatcher.ts +++ b/src/browser/routes/dispatcher.ts @@ -1,5 +1,6 @@ import type { BrowserRouteContext } from "../server-context.js"; import type { BrowserRequest, BrowserResponse, BrowserRouteRegistrar } from "./types.js"; +import { escapeRegExp } from "../../utils.js"; import { registerBrowserRoutes } from "./index.js"; type BrowserDispatchRequest = { @@ -22,10 +23,6 @@ type RouteEntry = { handler: (req: BrowserRequest, res: BrowserResponse) => void | Promise; }; -function escapeRegex(value: string) { - return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -} - function compileRoute(path: string): { regex: RegExp; paramNames: string[] } { const paramNames: string[] = []; const parts = path.split("/").map((part) => { @@ -34,7 +31,7 @@ function compileRoute(path: string): { regex: RegExp; paramNames: string[] } { paramNames.push(name); return "([^/]+)"; } - return escapeRegex(part); + return escapeRegExp(part); }); return { regex: new RegExp(`^${parts.join("/")}$`), paramNames }; } diff --git a/src/channels/dock.ts b/src/channels/dock.ts index 6451643d1e3..26f19337950 100644 --- a/src/channels/dock.ts +++ b/src/channels/dock.ts @@ -18,7 +18,7 @@ import { resolveSignalAccount } from "../signal/accounts.js"; import { resolveSlackAccount, resolveSlackReplyToMode } from "../slack/accounts.js"; import { buildSlackThreadingToolContext } from "../slack/threading-tool-context.js"; import { resolveTelegramAccount } from "../telegram/accounts.js"; -import { normalizeE164 } from "../utils.js"; +import { escapeRegExp, normalizeE164 } from "../utils.js"; import { resolveWhatsAppAccount } from "../web/accounts.js"; import { normalizeWhatsAppTarget } from "../whatsapp/normalize.js"; import { @@ -76,8 +76,6 @@ const formatLower = (allowFrom: Array) => .filter(Boolean) .map((entry) => entry.toLowerCase()); -const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - // Channel docks: lightweight channel metadata/behavior for shared code paths. // // Rules: diff --git a/src/cli/cli-utils.ts b/src/cli/cli-utils.ts index 72cd3e11bdf..d91b9a3331b 100644 --- a/src/cli/cli-utils.ts +++ b/src/cli/cli-utils.ts @@ -1,14 +1,13 @@ import type { Command } from "commander"; +import { formatErrorMessage } from "../infra/errors.js"; + +export { formatErrorMessage }; export type ManagerLookupResult = { manager: T | null; error?: string; }; -export function formatErrorMessage(err: unknown): string { - return err instanceof Error ? err.message : String(err); -} - export async function withManager(params: { getManager: () => Promise>; onMissing: (error?: string) => void; diff --git a/src/commands/health-format.test.ts b/src/commands/health-format.test.ts index bc3a732fd50..7381743f1f2 100644 --- a/src/commands/health-format.test.ts +++ b/src/commands/health-format.test.ts @@ -1,10 +1,7 @@ import { describe, expect, it } from "vitest"; +import { stripAnsi } from "../terminal/ansi.js"; import { formatHealthCheckFailure } from "./health-format.js"; -const ansiEscape = String.fromCharCode(27); -const ansiRegex = new RegExp(`${ansiEscape}\\[[0-9;]*m`, "g"); -const stripAnsi = (input: string) => input.replace(ansiRegex, ""); - describe("formatHealthCheckFailure", () => { it("keeps non-rich output stable", () => { const err = new Error("gateway closed (1006 abnormal closure): no close reason"); diff --git a/src/gateway/probe.ts b/src/gateway/probe.ts index c2593e0410d..42a10f1cb9c 100644 --- a/src/gateway/probe.ts +++ b/src/gateway/probe.ts @@ -1,5 +1,6 @@ import { randomUUID } from "node:crypto"; import type { SystemPresence } from "../infra/system-presence.js"; +import { formatErrorMessage } from "../infra/errors.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js"; import { GatewayClient } from "./client.js"; @@ -26,13 +27,6 @@ export type GatewayProbeResult = { configSnapshot: unknown; }; -function formatError(err: unknown): string { - if (err instanceof Error) { - return err.message; - } - return String(err); -} - export async function probeGateway(opts: { url: string; auth?: GatewayProbeAuth; @@ -65,7 +59,7 @@ export async function probeGateway(opts: { mode: GATEWAY_CLIENT_MODES.PROBE, instanceId, onConnectError: (err) => { - connectError = formatError(err); + connectError = formatErrorMessage(err); }, onClose: (code, reason) => { close = { code, reason }; @@ -93,7 +87,7 @@ export async function probeGateway(opts: { settle({ ok: false, connectLatencyMs, - error: formatError(err), + error: formatErrorMessage(err), close, health: null, status: null, diff --git a/src/gateway/server-methods/logs.ts b/src/gateway/server-methods/logs.ts index e3c1af75f31..aebd6efa9d3 100644 --- a/src/gateway/server-methods/logs.ts +++ b/src/gateway/server-methods/logs.ts @@ -2,6 +2,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import type { GatewayRequestHandlers } from "./types.js"; import { getResolvedLoggerSettings } from "../../logging.js"; +import { clamp } from "../../utils.js"; import { ErrorCodes, errorShape, @@ -15,10 +16,6 @@ const MAX_LIMIT = 5000; const MAX_BYTES = 1_000_000; const ROLLING_LOG_RE = /^openclaw-\d{4}-\d{2}-\d{2}\.log$/; -function clamp(value: number, min: number, max: number) { - return Math.max(min, Math.min(max, value)); -} - function isRollingLogFile(file: string): boolean { return ROLLING_LOG_RE.test(path.basename(file)); } diff --git a/src/infra/env-file.ts b/src/infra/env-file.ts index c20222a6cc0..525af40bbae 100644 --- a/src/infra/env-file.ts +++ b/src/infra/env-file.ts @@ -1,10 +1,6 @@ import fs from "node:fs"; import path from "node:path"; -import { resolveConfigDir } from "../utils.js"; - -function escapeRegExp(value: string): string { - return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -} +import { escapeRegExp, resolveConfigDir } from "../utils.js"; export function upsertSharedEnvVar(params: { key: string; diff --git a/src/memory/embeddings.ts b/src/memory/embeddings.ts index 6b78c3d738a..e87b491f6f3 100644 --- a/src/memory/embeddings.ts +++ b/src/memory/embeddings.ts @@ -1,6 +1,7 @@ import type { Llama, LlamaEmbeddingContext, LlamaModel } from "node-llama-cpp"; import fsSync from "node:fs"; import type { OpenClawConfig } from "../config/config.js"; +import { formatErrorMessage } from "../infra/errors.js"; import { resolveUserPath } from "../utils.js"; import { createGeminiEmbeddingProvider, type GeminiEmbeddingClient } from "./embeddings-gemini.js"; import { createOpenAiEmbeddingProvider, type OpenAiEmbeddingClient } from "./embeddings-openai.js"; @@ -73,7 +74,7 @@ function canAutoSelectLocal(options: EmbeddingProviderOptions): boolean { } function isMissingApiKeyError(err: unknown): boolean { - const message = formatError(err); + const message = formatErrorMessage(err); return message.includes("No API key found for provider"); } @@ -149,7 +150,7 @@ export async function createEmbeddingProvider( }; const formatPrimaryError = (err: unknown, provider: "openai" | "local" | "gemini" | "voyage") => - provider === "local" ? formatLocalSetupError(err) : formatError(err); + provider === "local" ? formatLocalSetupError(err) : formatErrorMessage(err); if (requestedProvider === "auto") { const missingKeyErrors: string[] = []; @@ -202,7 +203,7 @@ export async function createEmbeddingProvider( } catch (fallbackErr) { // oxlint-disable-next-line preserve-caught-error throw new Error( - `${reason}\n\nFallback to ${fallback} failed: ${formatError(fallbackErr)}`, + `${reason}\n\nFallback to ${fallback} failed: ${formatErrorMessage(fallbackErr)}`, { cause: fallbackErr }, ); } @@ -211,13 +212,6 @@ export async function createEmbeddingProvider( } } -function formatError(err: unknown): string { - if (err instanceof Error) { - return err.message; - } - return String(err); -} - function isNodeLlamaCppMissing(err: unknown): boolean { if (!(err instanceof Error)) { return false; @@ -230,7 +224,7 @@ function isNodeLlamaCppMissing(err: unknown): boolean { } function formatLocalSetupError(err: unknown): string { - const detail = formatError(err); + const detail = formatErrorMessage(err); const missing = isNodeLlamaCppMissing(err); return [ "Local embeddings unavailable.", diff --git a/src/plugin-sdk/index.ts b/src/plugin-sdk/index.ts index abc93716a02..67c95c13c8d 100644 --- a/src/plugin-sdk/index.ts +++ b/src/plugin-sdk/index.ts @@ -229,7 +229,8 @@ export { } from "../agents/tools/common.js"; export { formatDocsLink } from "../terminal/links.js"; export type { HookEntry } from "../hooks/types.js"; -export { normalizeE164 } from "../utils.js"; +export { clamp, escapeRegExp, normalizeE164, sleep } from "../utils.js"; +export { stripAnsi } from "../terminal/ansi.js"; export { missingTargetError } from "../infra/outbound/target-errors.js"; export { registerLogTransport } from "../logging/logger.js"; export type { LogTransport, LogTransportRecord } from "../logging/logger.js"; diff --git a/src/utils.ts b/src/utils.ts index a30264df2d0..17fa7a3d327 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -21,6 +21,16 @@ export function clampInt(value: number, min: number, max: number): number { return clampNumber(Math.floor(value), min, max); } +/** Alias for clampNumber (shorter, more common name) */ +export const clamp = clampNumber; + +/** + * Escapes special regex characters in a string so it can be used in a RegExp constructor. + */ +export function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + export type WebChannel = "web"; export function assertWebChannel(input: string): asserts input is WebChannel { diff --git a/src/web/reconnect.ts b/src/web/reconnect.ts index a0024810670..eec6f4689e3 100644 --- a/src/web/reconnect.ts +++ b/src/web/reconnect.ts @@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto"; import type { OpenClawConfig } from "../config/config.js"; import type { BackoffPolicy } from "../infra/backoff.js"; import { computeBackoff, sleepWithAbort } from "../infra/backoff.js"; +import { clamp } from "../utils.js"; export type ReconnectPolicy = BackoffPolicy & { maxAttempts: number; @@ -16,8 +17,6 @@ export const DEFAULT_RECONNECT_POLICY: ReconnectPolicy = { maxAttempts: 12, }; -const clamp = (val: number, min: number, max: number) => Math.max(min, Math.min(max, val)); - export function resolveHeartbeatSeconds(cfg: OpenClawConfig, overrideSeconds?: number): number { const candidate = overrideSeconds ?? cfg.web?.heartbeatSeconds; if (typeof candidate === "number" && candidate > 0) { diff --git a/test/gateway.multi.e2e.test.ts b/test/gateway.multi.e2e.test.ts index 7bbe7ecc305..5e6d7cb390d 100644 --- a/test/gateway.multi.e2e.test.ts +++ b/test/gateway.multi.e2e.test.ts @@ -8,6 +8,7 @@ import path from "node:path"; import { afterAll, describe, expect, it } from "vitest"; import { GatewayClient } from "../src/gateway/client.js"; import { loadOrCreateDeviceIdentity } from "../src/infra/device-identity.js"; +import { sleep } from "../src/utils.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../src/utils/message-channel.js"; type GatewayInstance = { @@ -32,8 +33,6 @@ type HealthPayload = { ok?: boolean }; const GATEWAY_START_TIMEOUT_MS = 45_000; const E2E_TIMEOUT_MS = 120_000; -const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); - const getFreePort = async () => { const srv = net.createServer(); await new Promise((resolve) => srv.listen(0, "127.0.0.1", resolve)); diff --git a/test/helpers/envelope-timestamp.ts b/test/helpers/envelope-timestamp.ts index 22aa3580d10..f86c90d7660 100644 --- a/test/helpers/envelope-timestamp.ts +++ b/test/helpers/envelope-timestamp.ts @@ -3,6 +3,8 @@ import { formatZonedTimestamp, } from "../../src/infra/format-time/format-datetime.js"; +export { escapeRegExp } from "../../src/utils.js"; + type EnvelopeTimestampZone = string; export function formatEnvelopeTimestamp(date: Date, zone: EnvelopeTimestampZone = "utc"): string { @@ -36,7 +38,3 @@ export function formatEnvelopeTimestamp(date: Date, zone: EnvelopeTimestampZone export function formatLocalEnvelopeTimestamp(date: Date): string { return formatEnvelopeTimestamp(date, "local"); } - -export function escapeRegExp(value: string): string { - return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -} diff --git a/test/helpers/normalize-text.ts b/test/helpers/normalize-text.ts index d81be0106cf..a5134255ffd 100644 --- a/test/helpers/normalize-text.ts +++ b/test/helpers/normalize-text.ts @@ -1,32 +1,4 @@ -function stripAnsi(input: string): string { - let out = ""; - for (let i = 0; i < input.length; i++) { - const code = input.charCodeAt(i); - if (code !== 27) { - out += input[i]; - continue; - } - - const next = input[i + 1]; - if (next !== "[") { - continue; - } - i += 1; - - while (i + 1 < input.length) { - i += 1; - const c = input[i]; - if (!c) { - break; - } - const isLetter = (c >= "A" && c <= "Z") || (c >= "a" && c <= "z") || c === "~"; - if (isLetter) { - break; - } - } - } - return out; -} +import { stripAnsi } from "../../src/terminal/ansi.js"; export function normalizeTestText(input: string): string { return stripAnsi(input) diff --git a/test/helpers/poll.ts b/test/helpers/poll.ts index 0b1a212e937..5704965cbc6 100644 --- a/test/helpers/poll.ts +++ b/test/helpers/poll.ts @@ -1,12 +1,10 @@ +import { sleep } from "../../src/utils.js"; + export type PollOptions = { timeoutMs?: number; intervalMs?: number; }; -function sleep(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - export async function pollUntil( fn: () => Promise, opts: PollOptions = {},