From 1220a7609a02c6a9acfbfd26bfa4bfc952a78457 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 10 Aug 2026 22:50:48 -0700 Subject: [PATCH] refactor: consolidate promise-like guards (#121861) * feat(normalization-core): add promise-like guard * refactor: consolidate promise-like guards * fix(normalization-core): keep isPromiseLike non-throwing on hostile then getters ClawSweeper finding on #121861: the diagnostics-path local guard caught throwing then getters; the canonical guard must classify, never throw. * test(normalization-core): annotate intentional hostile-thenable fixture --- packages/normalization-core/package.json | 7 ++++- .../src/promise-like.test.ts | 28 +++++++++++++++++++ .../normalization-core/src/promise-like.ts | 13 +++++++++ src/agents/agent-tools.ring-zero-context.ts | 8 +----- src/agents/cron-creator-authority-context.ts | 8 +----- .../run/attempt.model-diagnostic-events.ts | 12 +------- .../embedded-agent-subscribe.callback.ts | 4 +-- ...dded-agent-subscribe.handlers.lifecycle.ts | 2 +- ...edded-agent-subscribe.handlers.messages.ts | 2 +- ...embedded-agent-subscribe.handlers.tools.ts | 2 +- .../embedded-agent-subscribe.handlers.ts | 2 +- .../embedded-agent-subscribe.promise.ts | 9 ------ src/agents/embedded-agent-subscribe.ts | 2 +- src/channels/inbound-event/context.ts | 5 +--- src/gateway/server-methods/approval-shared.ts | 5 +--- src/infra/sqlite-transaction.ts | 5 +--- src/llm/providers/stream-wrappers/openai.ts | 9 +----- src/media-understanding/image.ts | 5 +--- src/plugins/hooks.ts | 8 +----- src/plugins/host-hook-state.ts | 5 +--- src/plugins/loader-module-runtime.ts | 9 +----- src/test-utils/vitest-spies.ts | 1 + test/vitest/vitest.shared.config.ts | 10 +++++++ tsconfig.json | 3 ++ 24 files changed, 79 insertions(+), 85 deletions(-) create mode 100644 packages/normalization-core/src/promise-like.test.ts create mode 100644 packages/normalization-core/src/promise-like.ts delete mode 100644 src/agents/embedded-agent-subscribe.promise.ts diff --git a/packages/normalization-core/package.json b/packages/normalization-core/package.json index efdda1d3c45b..1760e0e4a507 100644 --- a/packages/normalization-core/package.json +++ b/packages/normalization-core/package.json @@ -54,6 +54,11 @@ "import": "./dist/phone-presentation.mjs", "default": "./dist/phone-presentation.mjs" }, + "./promise-like": { + "types": "./dist/promise-like.d.mts", + "import": "./dist/promise-like.mjs", + "default": "./dist/promise-like.mjs" + }, "./record-coerce": { "types": "./dist/record-coerce.d.mts", "import": "./dist/record-coerce.mjs", @@ -91,7 +96,7 @@ } }, "scripts": { - "build": "tsdown src/index.ts src/agent-id.ts src/boolean-coercion.ts src/cjk-chars.ts src/error-coercion.ts src/expect.ts src/json-schema.ts src/number-coercion.ts src/phone-presentation.ts src/record-coerce.ts src/result.ts src/stable-node-path.ts src/stable-stringify.ts src/string-coerce.ts src/string-normalization.ts src/utf16-slice.ts --no-config --platform node --format esm --dts --out-dir dist --clean" + "build": "tsdown src/index.ts src/agent-id.ts src/boolean-coercion.ts src/cjk-chars.ts src/error-coercion.ts src/expect.ts src/json-schema.ts src/number-coercion.ts src/phone-presentation.ts src/promise-like.ts src/record-coerce.ts src/result.ts src/stable-node-path.ts src/stable-stringify.ts src/string-coerce.ts src/string-normalization.ts src/utf16-slice.ts --no-config --platform node --format esm --dts --out-dir dist --clean" }, "dependencies": { "libphonenumber-js": "1.13.9", diff --git a/packages/normalization-core/src/promise-like.test.ts b/packages/normalization-core/src/promise-like.test.ts new file mode 100644 index 000000000000..7e78b8c1a3fb --- /dev/null +++ b/packages/normalization-core/src/promise-like.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "vitest"; +import { isPromiseLike } from "./promise-like.js"; + +describe("isPromiseLike", () => { + it("accepts thenables and rejects values without a callable then", () => { + const thenable = {}; + // oxlint-disable-next-line unicorn/no-thenable -- An explicit thenable is the contract under test. + Reflect.defineProperty(thenable, "then", { value: () => {} }); + const nonCallableThen = {}; + // oxlint-disable-next-line unicorn/no-thenable -- The guard must reject a non-callable then field. + Reflect.defineProperty(nonCallableThen, "then", { value: true }); + + expect(isPromiseLike(Promise.resolve())).toBe(true); + expect(isPromiseLike(thenable)).toBe(true); + expect(isPromiseLike(nonCallableThen)).toBe(false); + expect(isPromiseLike(null)).toBe(false); + }); +}); + +it("classifies objects with throwing then getters as non-thenable", () => { + // oxlint-disable-next-line unicorn/no-thenable -- intentionally hostile thenable fixture + const hostile = Object.defineProperty({}, "then", { + get() { + throw new Error("trap"); + }, + }); + expect(isPromiseLike(hostile)).toBe(false); +}); diff --git a/packages/normalization-core/src/promise-like.ts b/packages/normalization-core/src/promise-like.ts new file mode 100644 index 000000000000..a3e79119633e --- /dev/null +++ b/packages/normalization-core/src/promise-like.ts @@ -0,0 +1,13 @@ +/** Canonical thenable guard; use instead of local isPromiseLike copies. */ +export function isPromiseLike(value: unknown): value is PromiseLike { + if (value === null || (typeof value !== "object" && typeof value !== "function")) { + return false; + } + // A hostile/exotic object can throw from its `then` getter; a guard must + // classify, never throw (diagnostics streams rely on this). + try { + return typeof (value as { then?: unknown }).then === "function"; + } catch { + return false; + } +} diff --git a/src/agents/agent-tools.ring-zero-context.ts b/src/agents/agent-tools.ring-zero-context.ts index af770819fbd5..1375b54c94f9 100644 --- a/src/agents/agent-tools.ring-zero-context.ts +++ b/src/agents/agent-tools.ring-zero-context.ts @@ -1,4 +1,5 @@ import { AsyncLocalStorage } from "node:async_hooks"; +import { isPromiseLike } from "@openclaw/normalization-core/promise-like"; import type { AnyAgentTool } from "./tools/common.js"; type AgentRingZeroToolScope = { @@ -8,13 +9,6 @@ type AgentRingZeroToolScope = { const activeRingZeroTools = new AsyncLocalStorage(); -function isPromiseLike(value: unknown): value is PromiseLike { - if ((typeof value !== "object" || value === null) && typeof value !== "function") { - return false; - } - return "then" in value && typeof value.then === "function"; -} - class HostScopedAgentToolAuthorizationError extends Error { readonly status = 403; diff --git a/src/agents/cron-creator-authority-context.ts b/src/agents/cron-creator-authority-context.ts index 9818198d2077..8c46d0012577 100644 --- a/src/agents/cron-creator-authority-context.ts +++ b/src/agents/cron-creator-authority-context.ts @@ -1,4 +1,5 @@ import { AsyncLocalStorage } from "node:async_hooks"; +import { isPromiseLike } from "@openclaw/normalization-core/promise-like"; import { createCronCreatorAuthorityRunScope, mintCronCreatorAuthorityGrant, @@ -21,13 +22,6 @@ const activeCronCreatorAuthority = new AsyncLocalStorage(); -function isPromiseLike(value: unknown): value is PromiseLike { - if ((typeof value !== "object" || value === null) && typeof value !== "function") { - return false; - } - return "then" in value && typeof value.then === "function"; -} - /** Keeps fresh cron reauthorization within one admitted Gateway agent run. */ export function runWithCronCreatorAuthority( runId: string, diff --git a/src/agents/embedded-agent-runner/run/attempt.model-diagnostic-events.ts b/src/agents/embedded-agent-runner/run/attempt.model-diagnostic-events.ts index 64b8a0607b35..1a2dac6183a3 100644 --- a/src/agents/embedded-agent-runner/run/attempt.model-diagnostic-events.ts +++ b/src/agents/embedded-agent-runner/run/attempt.model-diagnostic-events.ts @@ -1,3 +1,4 @@ +import { isPromiseLike } from "@openclaw/normalization-core/promise-like"; import { isRecord } from "@openclaw/normalization-core/record-coerce"; import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; /** @@ -417,17 +418,6 @@ function modelCallSizeTimingFields(state: ModelCallObservationState): ModelCallS }; } -function isPromiseLike(value: unknown): value is PromiseLike { - if (value === null || (typeof value !== "object" && typeof value !== "function")) { - return false; - } - try { - return typeof (value as { then?: unknown }).then === "function"; - } catch { - return false; - } -} - function asyncIteratorFactory(value: unknown): (() => AsyncIterator) | undefined { if (value === null || typeof value !== "object") { return undefined; diff --git a/src/agents/embedded-agent-subscribe.callback.ts b/src/agents/embedded-agent-subscribe.callback.ts index 79b79b747338..e6c491b9e021 100644 --- a/src/agents/embedded-agent-subscribe.callback.ts +++ b/src/agents/embedded-agent-subscribe.callback.ts @@ -1,4 +1,4 @@ -import { isPromiseLike } from "./embedded-agent-subscribe.promise.js"; +import { isPromiseLike } from "@openclaw/normalization-core/promise-like"; type CallbackLogger = { warn(message: string): void; @@ -12,7 +12,7 @@ export function runBestEffortCallback(params: { }): void { try { const result = params.callback(); - if (isPromiseLike(result)) { + if (isPromiseLike(result)) { void Promise.resolve(result).catch((error: unknown) => { params.log.warn(`${params.label} callback failed: ${String(error)}`); }); diff --git a/src/agents/embedded-agent-subscribe.handlers.lifecycle.ts b/src/agents/embedded-agent-subscribe.handlers.lifecycle.ts index 999cf84b1e76..c086150c49db 100644 --- a/src/agents/embedded-agent-subscribe.handlers.lifecycle.ts +++ b/src/agents/embedded-agent-subscribe.handlers.lifecycle.ts @@ -1,6 +1,7 @@ /** * Handles lifecycle and compaction events from subscribed embedded-agent sessions. */ +import { isPromiseLike } from "@openclaw/normalization-core/promise-like"; import { createInlineCodeState } from "../../packages/markdown-core/src/code-spans.js"; import { emitAgentEvent } from "../infra/agent-events.js"; import { hasAcceptedSessionSpawn } from "./accepted-session-spawn.js"; @@ -26,7 +27,6 @@ import { hasAssistantVisibleReply, } from "./embedded-agent-subscribe.handlers.messages.js"; import type { EmbeddedAgentSubscribeContext } from "./embedded-agent-subscribe.handlers.types.js"; -import { isPromiseLike } from "./embedded-agent-subscribe.promise.js"; import { isAssistantMessage } from "./embedded-agent-utils.js"; import type { AgentSessionEvent } from "./sessions/index.js"; import { summarizeToolValidationError } from "./tool-error-summary.js"; diff --git a/src/agents/embedded-agent-subscribe.handlers.messages.ts b/src/agents/embedded-agent-subscribe.handlers.messages.ts index 8331bcbb4890..9762ff221038 100644 --- a/src/agents/embedded-agent-subscribe.handlers.messages.ts +++ b/src/agents/embedded-agent-subscribe.handlers.messages.ts @@ -1,3 +1,4 @@ +import { isPromiseLike } from "@openclaw/normalization-core/promise-like"; /** * Handles embedded-agent assistant message events, block replies, reasoning * streams, reply directives, and pending tool media attachment handoff. @@ -34,7 +35,6 @@ import type { EmbeddedAgentSubscribeContext, EmbeddedAgentSubscribeState, } from "./embedded-agent-subscribe.handlers.types.js"; -import { isPromiseLike } from "./embedded-agent-subscribe.promise.js"; import { appendRawStream } from "./embedded-agent-subscribe.raw-stream.js"; import { warnIfAssistantEmittedSuspiciousText } from "./embedded-agent-subscribe.tool-text-diagnostics.js"; import { diff --git a/src/agents/embedded-agent-subscribe.handlers.tools.ts b/src/agents/embedded-agent-subscribe.handlers.tools.ts index 3671a61e4145..266132c8a557 100644 --- a/src/agents/embedded-agent-subscribe.handlers.tools.ts +++ b/src/agents/embedded-agent-subscribe.handlers.tools.ts @@ -1,3 +1,4 @@ +import { isPromiseLike } from "@openclaw/normalization-core/promise-like"; /** * Handles embedded-agent tool execution events and turns them into channel UI, * replay state, hook calls, approval prompts, media queues, and agent-event @@ -69,7 +70,6 @@ import type { ToolCallSummary, ToolHandlerContext, } from "./embedded-agent-subscribe.handlers.types.js"; -import { isPromiseLike } from "./embedded-agent-subscribe.promise.js"; import { collectMessagingMediaUrlsFromRecord, collectMessagingMediaUrlsFromToolResult, diff --git a/src/agents/embedded-agent-subscribe.handlers.ts b/src/agents/embedded-agent-subscribe.handlers.ts index 5ef4dd739470..efdbc343dbc8 100644 --- a/src/agents/embedded-agent-subscribe.handlers.ts +++ b/src/agents/embedded-agent-subscribe.handlers.ts @@ -1,6 +1,7 @@ /** * Dispatches serialized embedded-agent subscription events to specific handlers. */ +import { isPromiseLike } from "@openclaw/normalization-core/promise-like"; import { handleAgentEnd, handleAgentStart, @@ -24,7 +25,6 @@ import type { EmbeddedAgentSubscribeContext, EmbeddedAgentSubscribeEvent, } from "./embedded-agent-subscribe.handlers.types.js"; -import { isPromiseLike } from "./embedded-agent-subscribe.promise.js"; import type { AgentMessage } from "./runtime/index.js"; /** Create the serialized event dispatcher for subscribed embedded-agent sessions. */ diff --git a/src/agents/embedded-agent-subscribe.promise.ts b/src/agents/embedded-agent-subscribe.promise.ts deleted file mode 100644 index 272f09e5c124..000000000000 --- a/src/agents/embedded-agent-subscribe.promise.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** Narrow unknown values to PromiseLike without requiring a concrete Promise. */ -export function isPromiseLike(value: unknown): value is PromiseLike { - return Boolean( - value && - (typeof value === "object" || typeof value === "function") && - "then" in value && - typeof (value as { then?: unknown }).then === "function", - ); -} diff --git a/src/agents/embedded-agent-subscribe.ts b/src/agents/embedded-agent-subscribe.ts index b502cf9affeb..6c4436a9424a 100644 --- a/src/agents/embedded-agent-subscribe.ts +++ b/src/agents/embedded-agent-subscribe.ts @@ -1,6 +1,7 @@ /** * Subscribes to embedded-agent sessions and streams formatted replies/events. */ +import { isPromiseLike } from "@openclaw/normalization-core/promise-like"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import type { InlineCodeState } from "../../packages/markdown-core/src/code-spans.js"; @@ -51,7 +52,6 @@ import type { EmbeddedAgentSubscribeContext, EmbeddedAgentSubscribeState, } from "./embedded-agent-subscribe.handlers.types.js"; -import { isPromiseLike } from "./embedded-agent-subscribe.promise.js"; import { buildToolLifecycleErrorResult, extractToolResultMediaArtifact, diff --git a/src/channels/inbound-event/context.ts b/src/channels/inbound-event/context.ts index b8618f5cb02c..7f78ebeb0ece 100644 --- a/src/channels/inbound-event/context.ts +++ b/src/channels/inbound-event/context.ts @@ -3,6 +3,7 @@ * * Converts route, sender, command, media, and supplemental facts into finalized message context. */ +import { isPromiseLike } from "@openclaw/normalization-core/promise-like"; import { commandTurnKindToSource, createCommandTurnContext, @@ -251,10 +252,6 @@ function definedFields>(fields: T): Partial ) as Partial; } -function isPromiseLike(value: MaybePromise): value is Promise { - return Boolean(value) && typeof (value as { then?: unknown }).then === "function"; -} - function stripQuoteRuntimeFields( quote: ChannelInboundSupplementalQuoteFacts, ): NonNullable { diff --git a/src/gateway/server-methods/approval-shared.ts b/src/gateway/server-methods/approval-shared.ts index be644c2220be..9af5c63bb1a2 100644 --- a/src/gateway/server-methods/approval-shared.ts +++ b/src/gateway/server-methods/approval-shared.ts @@ -1,5 +1,6 @@ // Approval shared helpers normalize pending exec/plugin approval lookups, // decision payloads, turn-source routing, and gateway error responses. +import { isPromiseLike } from "@openclaw/normalization-core/promise-like"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { ErrorCodes, errorShape } from "../../../packages/gateway-protocol/src/index.js"; import type { @@ -76,10 +77,6 @@ type ApprovalResolveParamsValidator = (( errors?: ValidationError[] | null; }; -function isPromiseLike(value: T | Promise): value is Promise { - return typeof value === "object" && value !== null && "then" in value; -} - function isApprovalDecision(value: string): value is ExecApprovalDecision { return value === "allow-once" || value === "allow-always" || value === "deny"; } diff --git a/src/infra/sqlite-transaction.ts b/src/infra/sqlite-transaction.ts index c9565bd1b63c..634adbd8789c 100644 --- a/src/infra/sqlite-transaction.ts +++ b/src/infra/sqlite-transaction.ts @@ -1,5 +1,6 @@ // Provides SQLite transaction helpers with nested savepoints. import type { DatabaseSync } from "node:sqlite"; +import { isPromiseLike } from "@openclaw/normalization-core/promise-like"; import { createSubsystemLogger, type SubsystemLogger } from "../logging/subsystem.js"; // The cache-state module keeps this lifecycle edge off the kysely value graph // so cold control-plane paths using transactions do not load kysely. @@ -37,10 +38,6 @@ function nextSavepointName(): string { return `openclaw_tx_${nextSavepointId}`; } -function isPromiseLike(value: unknown): value is PromiseLike { - return Boolean(value && typeof (value as { then?: unknown }).then === "function"); -} - function assertSyncTransactionResult(value: unknown): void { if (isPromiseLike(value)) { throw new Error( diff --git a/src/llm/providers/stream-wrappers/openai.ts b/src/llm/providers/stream-wrappers/openai.ts index db620ce773d6..a5e3c23c744b 100644 --- a/src/llm/providers/stream-wrappers/openai.ts +++ b/src/llm/providers/stream-wrappers/openai.ts @@ -15,6 +15,7 @@ import { applyOpenAIResponsesPayloadPolicy, resolveOpenAIResponsesPayloadPolicy, } from "@openclaw/ai/transports"; +import { isPromiseLike } from "@openclaw/normalization-core/promise-like"; import { isRecord } from "@openclaw/normalization-core/record-coerce"; // OpenAI stream wrapper normalizes OpenAI-compatible streamed tool and text events. import { @@ -134,14 +135,6 @@ function isCodeModeEnabled(config?: OpenClawConfig): boolean { ); } -function isPromiseLike(value: unknown): value is PromiseLike { - return ( - value !== null && - (typeof value === "object" || typeof value === "function") && - typeof (value as { then?: unknown }).then === "function" - ); -} - function filterCodeModePayloadHookResult( payload: unknown, nextPayload: unknown, diff --git a/src/media-understanding/image.ts b/src/media-understanding/image.ts index b1ca9145350f..e249d59acb58 100644 --- a/src/media-understanding/image.ts +++ b/src/media-understanding/image.ts @@ -1,6 +1,7 @@ // Model-backed image understanding runtime for providers without a native media // provider hook. import { clampPositiveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion"; +import { isPromiseLike } from "@openclaw/normalization-core/promise-like"; import { isRecord } from "@openclaw/normalization-core/record-coerce"; import { isMinimaxVlmModel, minimaxUnderstandImage } from "../agents/minimax-vlm.js"; import { requireApiKey, resolveApiKeyForProvider } from "../agents/model-auth.js"; @@ -93,10 +94,6 @@ function isImageModelNoTextError(err: unknown): boolean { return err instanceof Error && /^Image model returned no text\b/.test(err.message); } -function isPromiseLike(value: unknown): value is PromiseLike { - return Boolean(value) && typeof (value as { then?: unknown }).then === "function"; -} - function composeImageDescriptionPayloadHandlers( first: ProviderStreamOptions["onPayload"] | undefined, second: ProviderStreamOptions["onPayload"] | undefined, diff --git a/src/plugins/hooks.ts b/src/plugins/hooks.ts index 89eea17f6ad8..fbb820b6a1f5 100644 --- a/src/plugins/hooks.ts +++ b/src/plugins/hooks.ts @@ -7,6 +7,7 @@ import { AsyncLocalStorage } from "node:async_hooks"; import { clampPositiveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion"; +import { isPromiseLike } from "@openclaw/normalization-core/promise-like"; import { isToolAllowedByPolicyName } from "../agents/tool-policy-match.js"; import { attachToolAllowlistIntersection, @@ -615,13 +616,6 @@ export function createHookRunner( const getPluginPackageVersion = (pluginId: string): string | undefined => registry.plugins.find((plugin) => plugin.id === pluginId)?.packageVersion; - const isPromiseLike = (value: unknown): value is PromiseLike => { - if ((typeof value !== "object" && typeof value !== "function") || value === null) { - return false; - } - return typeof (value as { then?: unknown }).then === "function"; - }; - const normalizePositiveTimeoutMs = (timeoutMs: number | undefined): number | undefined => { return clampPositiveTimerTimeoutMs(timeoutMs); }; diff --git a/src/plugins/host-hook-state.ts b/src/plugins/host-hook-state.ts index 4e8dea79ed3c..6f997de5c990 100644 --- a/src/plugins/host-hook-state.ts +++ b/src/plugins/host-hook-state.ts @@ -1,5 +1,6 @@ // Tracks host hook state and scheduled turn identifiers. import { randomUUID } from "node:crypto"; +import { isPromiseLike } from "@openclaw/normalization-core/promise-like"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import type { SessionEntry } from "../config/sessions.js"; import { @@ -464,10 +465,6 @@ function collectPluginSessionExtensionProjections(params: { return projections; } -function isPromiseLike(value: unknown): value is PromiseLike { - return Boolean(value && typeof (value as { then?: unknown }).then === "function"); -} - function discardUnexpectedPromiseProjection(value: PromiseLike): void { void Promise.resolve(value).catch(() => undefined); } diff --git a/src/plugins/loader-module-runtime.ts b/src/plugins/loader-module-runtime.ts index a1e8cda2f572..a4d2dc989bb8 100644 --- a/src/plugins/loader-module-runtime.ts +++ b/src/plugins/loader-module-runtime.ts @@ -1,3 +1,4 @@ +import { isPromiseLike } from "@openclaw/normalization-core/promise-like"; import { toSafeImportPath } from "../shared/import-specifier.js"; import { attachPluginApiFacades } from "./api-facades.js"; import { isLateCallablePluginApiMethod } from "./api-lifecycle.js"; @@ -41,14 +42,6 @@ const LAZY_RUNTIME_REFLECTION_KEYS = [ "llm", ] as const satisfies readonly (keyof PluginRuntime)[]; -function isPromiseLike(value: unknown): value is PromiseLike { - return ( - (typeof value === "object" || typeof value === "function") && - value !== null && - typeof (value as { then?: unknown }).then === "function" - ); -} - function createGuardedPluginRegistrationApi(api: OpenClawPluginApi): { api: OpenClawPluginApi; close: () => void; diff --git a/src/test-utils/vitest-spies.ts b/src/test-utils/vitest-spies.ts index 05384f9c5b18..66c34b4d5d51 100644 --- a/src/test-utils/vitest-spies.ts +++ b/src/test-utils/vitest-spies.ts @@ -12,6 +12,7 @@ function restoreMocks(mocks: readonly RestorableMock[]): void { } } +// This guard requires concrete Promise.finally narrowing for synchronous cleanup overloads. function isPromiseLike(value: T | Promise): value is Promise { return ( typeof value === "object" && diff --git a/test/vitest/vitest.shared.config.ts b/test/vitest/vitest.shared.config.ts index fe6f7492f971..f98fa5ad49cc 100644 --- a/test/vitest/vitest.shared.config.ts +++ b/test/vitest/vitest.shared.config.ts @@ -482,6 +482,16 @@ export const sharedVitestConfig = { "phone-presentation.ts", ), }, + { + find: "@openclaw/normalization-core/promise-like", + replacement: path.join( + repoRoot, + "packages", + "normalization-core", + "src", + "promise-like.ts", + ), + }, { find: "@openclaw/normalization-core/record-coerce", replacement: path.join( diff --git a/tsconfig.json b/tsconfig.json index 5eead2bc5603..92c08cfab01a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -164,6 +164,9 @@ "@openclaw/normalization-core/phone-presentation": [ "./packages/normalization-core/src/phone-presentation.ts" ], + "@openclaw/normalization-core/promise-like": [ + "./packages/normalization-core/src/promise-like.ts" + ], "@openclaw/normalization-core/record-coerce": [ "./packages/normalization-core/src/record-coerce.ts" ],