mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-23 10:55:31 -06:00
refactor: consolidate coercion contracts (#122458)
* refactor: consolidate coercion contracts Centralize exact string, record, numeric, date, Boolean, argument, and structured-error coercions while preserving call-site semantics. Migrate canonical-name collisions and deprecated internal SDK bypasses, deleting 55 net production/tooling lines. Expand declaration ownership enforcement to 101 allowed helpers and add a narrow export-completeness audit. * fix: preserve standalone script coercions Keep copied Control UI tooling self-contained and retain the trusted release harness module-relative source seam when the harness runs against an old target cwd.
This commit is contained in:
committed by
GitHub
parent
66fe424590
commit
b080dd1e76
@@ -1,6 +1,6 @@
|
||||
/** Tests live model switching behavior in active agent command sessions. */
|
||||
|
||||
import { expectDefined } from "@openclaw/normalization-core";
|
||||
import { expectDefined, toStringifiedError } from "@openclaw/normalization-core";
|
||||
import { createRequireRecord } from "openclaw/plugin-sdk/test-fixtures";
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { SessionEntry } from "../config/sessions.js";
|
||||
@@ -274,8 +274,7 @@ vi.mock("../acp/policy.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../acp/runtime/errors.js", () => ({
|
||||
toAcpRuntimeError: ({ error }: { error: unknown }) =>
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
toAcpRuntimeError: ({ error }: { error: unknown }) => toStringifiedError(error),
|
||||
}));
|
||||
|
||||
vi.mock("@openclaw/acp-core/runtime/session-identifiers", () => ({
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
|
||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { readNonBlankString } from "@openclaw/normalization-core/string-coerce";
|
||||
import { uniqueStrings } from "@openclaw/normalization-core/string-normalization";
|
||||
import { coerceSecretRef } from "../../config/types.secrets.js";
|
||||
import type { OpenClawAgentDatabase } from "../../state/openclaw-agent-db.js";
|
||||
@@ -56,11 +57,7 @@ function isRetainedUsageStatsId(
|
||||
// Persisted credential normalization accepts old field names and SecretRef-ish
|
||||
// values, then emits the current credential discriminated union.
|
||||
function normalizeOptionalCredentialString(value: unknown): string | undefined {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed ? value : undefined;
|
||||
return readNonBlankString(value);
|
||||
}
|
||||
|
||||
function normalizeExpiryField(value: unknown): number | undefined {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/** Prepares exec workdir and environment facts before policy and host dispatch. */
|
||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
import { normalizeChatChannelId } from "../channels/ids.js";
|
||||
import type { ExecHost } from "../infra/exec-approvals.js";
|
||||
@@ -96,7 +97,7 @@ function buildChannelContextEnv(
|
||||
}
|
||||
|
||||
function isExecToolArgsObject(value: unknown): value is ExecToolArgs {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
return isRecord(value);
|
||||
}
|
||||
|
||||
function filterPluginExecEnv(rawEnv: Record<string, string>): Record<string, string> | undefined {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { asPositiveFiniteNumber } from "@openclaw/normalization-core/number-coercion";
|
||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import {
|
||||
createReasoningTagTextPartitioner,
|
||||
@@ -470,11 +471,7 @@ function readThinkingProgressTokens(delta: Record<string, unknown>): number | un
|
||||
if (delta.type !== "thinking_delta" || delta.thinking !== "") {
|
||||
return undefined;
|
||||
}
|
||||
const estimatedTokens = delta.estimated_tokens;
|
||||
if (typeof estimatedTokens !== "number" || !Number.isFinite(estimatedTokens)) {
|
||||
return undefined;
|
||||
}
|
||||
return estimatedTokens > 0 ? estimatedTokens : undefined;
|
||||
return asPositiveFiniteNumber(delta.estimated_tokens);
|
||||
}
|
||||
|
||||
function emitClaudeThinkingProgress(
|
||||
|
||||
@@ -11,6 +11,7 @@ import { stableStringify } from "@openclaw/normalization-core";
|
||||
import {
|
||||
asDateTimestampMs,
|
||||
isFutureDateTimestampMs,
|
||||
parseDateStringTimestampMs,
|
||||
resolveExpiresAtMsFromDurationMs,
|
||||
} from "@openclaw/normalization-core/number-coercion";
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
@@ -203,10 +204,7 @@ async function appendGooglePromptCacheEntry(
|
||||
}
|
||||
|
||||
function parseExpireTimeMs(expireTime: string | undefined): number | null {
|
||||
if (!expireTime) {
|
||||
return null;
|
||||
}
|
||||
return asDateTimestampMs(Date.parse(expireTime)) ?? null;
|
||||
return parseDateStringTimestampMs(expireTime) ?? null;
|
||||
}
|
||||
|
||||
function convertManagedGoogleTools(tools: NonNullable<GooglePromptCacheContext["tools"]>) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* Reads normalized context-token metadata from resolved model definitions.
|
||||
*/
|
||||
import { asFiniteNumber } from "@openclaw/normalization-core/number-coercion";
|
||||
import type { Model } from "../../llm/types.js";
|
||||
|
||||
/**
|
||||
@@ -14,5 +15,5 @@ type AgentModelWithOptionalContextTokens = Model & {
|
||||
/** Prefer contextTokens, then contextWindow, when present on model metadata. */
|
||||
export function readAgentModelContextTokens(model: Model | null | undefined): number | undefined {
|
||||
const value = (model as AgentModelWithOptionalContextTokens | null | undefined)?.contextTokens;
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
||||
return asFiniteNumber(value);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { finiteSecondsToTimerSafeMilliseconds } from "@openclaw/normalization-core/number-coercion";
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import type { Api, Model } from "../../llm/types.js";
|
||||
import type { ProviderRuntimeModel } from "../../plugins/provider-runtime-model.types.js";
|
||||
@@ -272,11 +273,7 @@ export function resolveProviderTransport(params: {
|
||||
}
|
||||
|
||||
export function normalizeTransportBaseUrl(baseUrl: unknown): string | undefined {
|
||||
if (typeof baseUrl !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = baseUrl.trim();
|
||||
return trimmed ? trimmed : undefined;
|
||||
return normalizeOptionalString(baseUrl);
|
||||
}
|
||||
|
||||
export function resolveProviderRequestTimeoutMs(timeoutSeconds: unknown): number | undefined {
|
||||
|
||||
@@ -71,7 +71,7 @@ function hasNonEmptyAssistantText(texts: string[]): boolean {
|
||||
return texts.some((text) => text.trim().length > 0);
|
||||
}
|
||||
|
||||
function hasNonEmptyString(values: string[]): boolean {
|
||||
function hasAnyNonBlankString(values: string[]): boolean {
|
||||
return values.some((value) => value.trim().length > 0);
|
||||
}
|
||||
|
||||
@@ -82,8 +82,8 @@ function hasCommittedMessagingDeliveryEvidence(
|
||||
>,
|
||||
): boolean {
|
||||
return (
|
||||
hasNonEmptyString(params.messagingToolSentTexts) ||
|
||||
hasNonEmptyString(params.messagingToolSentMediaUrls) ||
|
||||
hasAnyNonBlankString(params.messagingToolSentTexts) ||
|
||||
hasAnyNonBlankString(params.messagingToolSentMediaUrls) ||
|
||||
params.messagingToolSentTargets.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* before-finalize retry/finalize decisions with bounded retry accounting.
|
||||
*/
|
||||
import { createHash } from "node:crypto";
|
||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { normalizeOptionalString as normalizeTrimmedString } from "@openclaw/normalization-core/string-coerce";
|
||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
|
||||
@@ -240,5 +241,5 @@ function readBeforeAgentFinalizeRetryCandidates(
|
||||
function isBeforeAgentFinalizeRetry(
|
||||
value: unknown,
|
||||
): value is NonNullable<PluginHookBeforeAgentFinalizeResult["retry"]> {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
return isRecord(value);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import path from "node:path";
|
||||
import { asFiniteNumber } from "@openclaw/normalization-core/number-coercion";
|
||||
import { resolveStateDir } from "../../config/paths.js";
|
||||
import {
|
||||
listConfiguredSessionStoreAgentIds,
|
||||
@@ -64,9 +65,7 @@ export function normalizeStringSet(values: Iterable<string> | undefined): Set<st
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export function normalizeFiniteTimestamp(value: unknown): number | undefined {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
||||
}
|
||||
export const normalizeFiniteTimestamp = asFiniteNumber;
|
||||
|
||||
export function hasCurrentProcessOwner(params: {
|
||||
activeSessionIds: Set<string>;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Resolves MCP transport command, environment, and timeout configuration.
|
||||
*/
|
||||
import {
|
||||
asPositiveFiniteNumber,
|
||||
clampPositiveTimerTimeoutMs,
|
||||
resolvePositiveTimerTimeoutMs,
|
||||
} from "@openclaw/normalization-core/number-coercion";
|
||||
@@ -68,8 +69,8 @@ function getPositiveNumber(rawServer: unknown, keys: readonly string[]): number
|
||||
}
|
||||
const record = rawServer as Record<string, unknown>;
|
||||
for (const key of keys) {
|
||||
const value = record[key];
|
||||
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
||||
const value = asPositiveFiniteNumber(record[key]);
|
||||
if (value !== undefined) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { toStringifiedError } from "@openclaw/normalization-core/error-coercion";
|
||||
|
||||
export class PreparedModelRuntimeOwnerNotPublishedError extends Error {}
|
||||
|
||||
export class PreparedModelRuntimePublicationSupersededError extends PreparedModelRuntimeOwnerNotPublishedError {}
|
||||
|
||||
export function toPreparedModelRuntimeError(error: unknown): Error {
|
||||
return error instanceof Error ? error : new Error(String(error));
|
||||
return toStringifiedError(error);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
|
||||
import type { KeyId } from "@earendil-works/pi-tui";
|
||||
import { coerceErrorMessage } from "@openclaw/normalization-core/error-coercion";
|
||||
import type { ImageContent, Model } from "../../../llm/types.js";
|
||||
import { interactiveAgentTheme as theme, type Theme } from "../../modes/interactive/theme/theme.js";
|
||||
import type { AgentMessage } from "../../runtime/index.js";
|
||||
@@ -335,7 +336,7 @@ export class ExtensionRunner {
|
||||
this.emitError({
|
||||
extensionPath,
|
||||
event: "register_provider",
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
error: coerceErrorMessage(err),
|
||||
stack: err instanceof Error ? err.stack : undefined,
|
||||
});
|
||||
}
|
||||
@@ -734,7 +735,7 @@ export class ExtensionRunner {
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const message = coerceErrorMessage(err);
|
||||
const stack = err instanceof Error ? err.stack : undefined;
|
||||
this.emitError({
|
||||
extensionPath: ext.path,
|
||||
@@ -782,7 +783,7 @@ export class ExtensionRunner {
|
||||
currentMessage = handlerResult.message;
|
||||
modified = true;
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const message = coerceErrorMessage(err);
|
||||
const stack = err instanceof Error ? err.stack : undefined;
|
||||
this.emitError({
|
||||
extensionPath: ext.path,
|
||||
@@ -830,7 +831,7 @@ export class ExtensionRunner {
|
||||
modified = true;
|
||||
}
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const message = coerceErrorMessage(err);
|
||||
const stack = err instanceof Error ? err.stack : undefined;
|
||||
this.emitError({
|
||||
extensionPath: ext.path,
|
||||
@@ -894,7 +895,7 @@ export class ExtensionRunner {
|
||||
return handlerResult as UserBashEventResult;
|
||||
}
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const message = coerceErrorMessage(err);
|
||||
const stack = err instanceof Error ? err.stack : undefined;
|
||||
this.emitError({
|
||||
extensionPath: ext.path,
|
||||
@@ -934,7 +935,7 @@ export class ExtensionRunner {
|
||||
currentMessages = (handlerResult as ContextEventResult).messages!;
|
||||
}
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const message = coerceErrorMessage(err);
|
||||
const stack = err instanceof Error ? err.stack : undefined;
|
||||
this.emitError({
|
||||
extensionPath: ext.path,
|
||||
@@ -970,7 +971,7 @@ export class ExtensionRunner {
|
||||
currentPayload = handlerResult;
|
||||
}
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const message = coerceErrorMessage(err);
|
||||
const stack = err instanceof Error ? err.stack : undefined;
|
||||
this.emitError({
|
||||
extensionPath: ext.path,
|
||||
@@ -1031,7 +1032,7 @@ export class ExtensionRunner {
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const message = coerceErrorMessage(err);
|
||||
const stack = err instanceof Error ? err.stack : undefined;
|
||||
this.emitError({
|
||||
extensionPath: ext.path,
|
||||
@@ -1094,7 +1095,7 @@ export class ExtensionRunner {
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const message = coerceErrorMessage(err);
|
||||
const stack = err instanceof Error ? err.stack : undefined;
|
||||
this.emitError({
|
||||
extensionPath: ext.path,
|
||||
@@ -1140,7 +1141,7 @@ export class ExtensionRunner {
|
||||
this.emitError({
|
||||
extensionPath: ext.path,
|
||||
event: "input",
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
error: coerceErrorMessage(err),
|
||||
stack: err instanceof Error ? err.stack : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
asSafeIntegerInRange,
|
||||
parseStrictFiniteNumber,
|
||||
} from "@openclaw/normalization-core/number-coercion";
|
||||
import { asNonArrayRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { normalizeStringEntries } from "@openclaw/normalization-core/string-normalization";
|
||||
import type { TSchema } from "typebox";
|
||||
import { readLocalFileSafely } from "../../infra/fs-safe.js";
|
||||
@@ -70,9 +71,7 @@ export type AnyAgentTool = Omit<AgentTool, "execute"> &
|
||||
};
|
||||
|
||||
export function asToolParamsRecord(params: unknown): Record<string, unknown> {
|
||||
return params && typeof params === "object" && !Array.isArray(params)
|
||||
? (params as Record<string, unknown>)
|
||||
: {};
|
||||
return asNonArrayRecord(params);
|
||||
}
|
||||
|
||||
type StringParamOptions = {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*
|
||||
* Implements only the Gateway calls needed by session tools and rejects unsupported methods.
|
||||
*/
|
||||
import { asPositiveSafeInteger } from "@openclaw/normalization-core/number-coercion";
|
||||
import { normalizeFastMode, type FastMode } from "@openclaw/normalization-core/string-coerce";
|
||||
import type {
|
||||
SessionsListParams,
|
||||
@@ -145,7 +146,7 @@ function readChatHistoryMessageSeq(message: unknown): number | undefined {
|
||||
return undefined;
|
||||
}
|
||||
const seq = (metadata as Record<string, unknown>).seq;
|
||||
return typeof seq === "number" && Number.isSafeInteger(seq) && seq > 0 ? seq : undefined;
|
||||
return asPositiveSafeInteger(seq);
|
||||
}
|
||||
|
||||
function resolveChatHistoryNextOffset(params: {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { expectDefined } from "@openclaw/normalization-core";
|
||||
import { coerceErrorMessage as formatLiveError, expectDefined } from "@openclaw/normalization-core";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import type { ModelApi } from "../../config/types.models.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
@@ -110,10 +110,6 @@ function readJpegDimensions(buffer: Buffer): { width: number; height: number } {
|
||||
throw new Error("JPEG dimensions not found");
|
||||
}
|
||||
|
||||
function formatLiveError(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
function isSkippableLiveError(error: unknown): boolean {
|
||||
const message = formatLiveError(error);
|
||||
return (
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*
|
||||
* Reads bounded, redacted session transcript history after session visibility filtering.
|
||||
*/
|
||||
import { asPositiveSafeInteger } from "@openclaw/normalization-core/number-coercion";
|
||||
import { Type } from "typebox";
|
||||
import { getRuntimeConfig } from "../../config/config.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
@@ -216,7 +217,7 @@ function readHistoryMessageSeq(message: unknown): number | undefined {
|
||||
return undefined;
|
||||
}
|
||||
const seq = (meta as Record<string, unknown>).seq;
|
||||
return typeof seq === "number" && Number.isSafeInteger(seq) && seq > 0 ? seq : undefined;
|
||||
return asPositiveSafeInteger(seq);
|
||||
}
|
||||
|
||||
function readHistoryMessageId(message: unknown): string | undefined {
|
||||
|
||||
Reference in New Issue
Block a user