refactor(normalization): reuse trimmed list helpers (#113295)

This commit is contained in:
Vincent Koc
2026-07-24 17:43:22 +08:00
committed by GitHub
parent 89643f6401
commit e3636852ba
7 changed files with 33 additions and 58 deletions
-1
View File
@@ -352,7 +352,6 @@ packages/gateway-protocol/src/schema/agents-models-skills.ts
packages/gateway-protocol/src/schema/protocol-schemas.ts
packages/markdown-core/src/ir.ts
packages/memory-host-sdk/src/host/session-files.ts
packages/model-catalog-core/src/model-catalog-normalize.ts
packages/sdk/src/client.ts
packages/sdk/src/index.test.ts
packages/speech-core/src/tts.test.ts
+3 -12
View File
@@ -3,6 +3,7 @@ import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
normalizeTrimmedStringList,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import type {
SlackBasePostMessagePayload,
@@ -24,16 +25,6 @@ type SlackWebApiErrorData = {
};
};
function normalizeSlackScopeList(value: unknown): string[] {
if (!Array.isArray(value)) {
return [];
}
return value.flatMap((scope) => {
const normalized = normalizeOptionalString(scope);
return normalized ? [normalized] : [];
});
}
function getSlackWebApiErrorData(err: unknown): SlackWebApiErrorData | undefined {
if (!(err instanceof Error)) {
return undefined;
@@ -53,8 +44,8 @@ function isSlackCustomizeScopeError(err: unknown): boolean {
return true;
}
const scopes = [
...normalizeSlackScopeList(data?.response_metadata?.scopes),
...normalizeSlackScopeList(data?.response_metadata?.acceptedScopes),
...normalizeTrimmedStringList(data?.response_metadata?.scopes),
...normalizeTrimmedStringList(data?.response_metadata?.acceptedScopes),
].map((scope) => normalizeLowercaseStringOrEmpty(scope));
return scopes.includes("chat:write.customize");
}
@@ -227,7 +227,9 @@ describe("sendMessageSlack customize-scope fallback", () => {
const client = createSlackSendTestClient();
vi.mocked(client.chat.postMessage)
.mockRejectedValueOnce(
buildMissingScopeError({ acceptedScopes: ["chat:write", "chat:write.customize"] }),
buildMissingScopeError({
acceptedScopes: [" chat:write ", "", " chat:write.customize "],
}),
)
.mockResolvedValueOnce({ ts: "171234.567" });
@@ -383,8 +385,8 @@ describe("sendMessageSlack customize-scope fallback", () => {
vi.mocked(client.chat.postMessage).mockRejectedValueOnce(
buildMissingScopeError({
needed: "im:write",
scopes: ["chat:write", "users:read"],
acceptedScopes: ["im:write", "mpim:write"],
scopes: [" chat:write ", "", " users:read "],
acceptedScopes: [" im:write ", " mpim:write "],
}),
);
+3 -12
View File
@@ -26,6 +26,7 @@ import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
import {
normalizeOptionalString,
normalizeOptionalString as normalizeSlackApiString,
normalizeTrimmedStringList,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { sliceUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
import type { SlackTokenSource } from "./accounts.js";
@@ -195,16 +196,6 @@ function resolveSlackSendIdentity(params: {
);
}
function normalizeSlackScopeList(value: unknown): string[] {
if (!Array.isArray(value)) {
return [];
}
return value.flatMap((scope) => {
const normalized = normalizeSlackApiString(scope);
return normalized ? [normalized] : [];
});
}
function getSlackWebApiErrorData(err: unknown): SlackWebApiErrorData | undefined {
if (!(err instanceof Error)) {
return undefined;
@@ -230,11 +221,11 @@ function formatSlackWebApiErrorMessage(err: unknown): string | undefined {
if (needed) {
details.push(`needed: ${needed}`);
}
const scopes = normalizeSlackScopeList(data?.response_metadata?.scopes);
const scopes = normalizeTrimmedStringList(data?.response_metadata?.scopes);
if (scopes.length) {
details.push(`granted: ${scopes.join(", ")}`);
}
const acceptedScopes = normalizeSlackScopeList(data?.response_metadata?.acceptedScopes);
const acceptedScopes = normalizeTrimmedStringList(data?.response_metadata?.acceptedScopes);
if (acceptedScopes.length) {
details.push(`accepted: ${acceptedScopes.join(", ")}`);
}
@@ -57,11 +57,14 @@ describe("model catalog normalization", () => {
compat: {
supportsTools: true,
openRouterRouting: {
only: ["anthropic", 1],
only: [" anthropic ", "", 1],
allow_fallbacks: false,
require_parameters: "no",
},
vercelGatewayRouting: { order: ["anthropic", 1], only: "openai" },
vercelGatewayRouting: {
order: [" anthropic ", "", 1],
only: "openai",
},
zaiToolStream: true,
cacheControlFormat: "anthropic",
sendSessionAffinityHeaders: true,
@@ -75,9 +78,9 @@ describe("model catalog normalization", () => {
},
status: "preview",
statusReason: "rolling out",
replaces: ["gpt-5.3"],
replaces: [" gpt-5.3 ", ""],
replacedBy: "gpt-5.5",
tags: ["default"],
tags: [" default ", ""],
},
{
id: "",
@@ -1,4 +1,9 @@
// Model Catalog Core helper module supports model catalog normalize behavior.
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import {
normalizeOptionalTrimmedStringList,
normalizeTrimmedStringList,
} from "@openclaw/normalization-core/string-normalization";
import {
buildModelCatalogMergeKey,
buildModelCatalogRef,
@@ -48,31 +53,6 @@ function isBlockedObjectKey(key: string): boolean {
return key === "__proto__" || key === "prototype" || key === "constructor";
}
/** Normalize optional catalog strings. */
function normalizeOptionalString(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed ? trimmed : undefined;
}
/** Normalize arrays of trimmed strings, dropping invalid entries. */
function normalizeTrimmedStringList(value: unknown): string[] {
if (!Array.isArray(value)) {
return [];
}
return value.flatMap((entry) => {
const normalized = normalizeOptionalString(entry);
return normalized ? [normalized] : [];
});
}
function normalizeOptionalTrimmedStringList(value: unknown): string[] | undefined {
const normalized = normalizeTrimmedStringList(value);
return normalized.length > 0 ? normalized : undefined;
}
function normalizeModelCatalogThinkingLevelMap(
value: unknown,
): ModelCatalogThinkingLevelMap | undefined {
@@ -761,4 +741,3 @@ export function normalizeModelCatalogProviderRows(params: {
return rows.toSorted((a, b) => a.provider.localeCompare(b.provider) || a.id.localeCompare(b.id));
}
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */
@@ -3,10 +3,12 @@ import { describe, expect, it } from "vitest";
import {
normalizeAtHashSlug,
normalizeHyphenSlug,
normalizeOptionalTrimmedStringList,
normalizeSortedUniqueStringEntries,
normalizeSortedUniqueTrimmedStringList,
normalizeStringEntries,
normalizeStringEntriesLower,
normalizeTrimmedStringList,
normalizeUniqueSingleOrTrimmedStringList,
normalizeUniqueStringEntries,
normalizeUniqueStringEntriesLower,
@@ -59,6 +61,14 @@ describe("normalization-core/string-normalization", () => {
expect(normalizeUniqueTrimmedStringList("b")).toEqual([]);
});
it("normalizes array-backed trimmed string lists", () => {
const values = [" first ", "", 42, " second ", null];
expect(normalizeTrimmedStringList(values)).toEqual(["first", "second"]);
expect(normalizeTrimmedStringList("first")).toEqual([]);
expect(normalizeOptionalTrimmedStringList(values)).toEqual(["first", "second"]);
expect(normalizeOptionalTrimmedStringList(["", 42])).toBeUndefined();
});
it("normalizes sorted unique trimmed string lists", () => {
expect(normalizeSortedUniqueTrimmedStringList([" b ", "a", "b", "", "a"])).toEqual(["a", "b"]);
expect(normalizeSortedUniqueTrimmedStringList(["z", 1, " a "] as unknown[])).toEqual([