mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-20 09:31:54 -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
@@ -3,6 +3,7 @@ import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { expectDefined } from "@openclaw/normalization-core";
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { expectNoReaddirSyncDuring } from "../test-utils/fs-scan-assertions.js";
|
||||
import { listGitTrackedFiles, toRepoRelativePath } from "../test-utils/repo-files.js";
|
||||
@@ -50,14 +51,6 @@ function readJsonFile(filePath: string): unknown {
|
||||
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
}
|
||||
|
||||
function normalizeText(value: unknown): string | undefined {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed || undefined;
|
||||
}
|
||||
|
||||
function listBundledPluginDirs(): string[] {
|
||||
const externalDirs = listExternalBundledPluginDirs();
|
||||
if (externalDirs) {
|
||||
@@ -146,8 +139,8 @@ function readBundledPluginRecords(): BundledPluginRecord[] {
|
||||
|
||||
const manifest = readJsonFile(manifestPath) as PluginManifestShape;
|
||||
const pkg = readJsonFile(packagePath) as OpenClawPackageShape;
|
||||
const manifestId = normalizeText(manifest.id);
|
||||
const packageName = normalizeText(pkg.name);
|
||||
const manifestId = normalizeOptionalString(manifest.id);
|
||||
const packageName = normalizeOptionalString(pkg.name);
|
||||
if (!manifestId || !packageName) {
|
||||
return [];
|
||||
}
|
||||
@@ -157,8 +150,8 @@ function readBundledPluginRecords(): BundledPluginRecord[] {
|
||||
dirName,
|
||||
packageName,
|
||||
manifestId,
|
||||
installNpmSpec: normalizeText(pkg.openclaw?.install?.npmSpec),
|
||||
channelId: normalizeText(pkg.openclaw?.channel?.id),
|
||||
installNpmSpec: normalizeOptionalString(pkg.openclaw?.install?.npmSpec),
|
||||
channelId: normalizeOptionalString(pkg.openclaw?.channel?.id),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
type PluginManifest,
|
||||
} from "../../manifest.js";
|
||||
import { resolveLoaderPackageRoot } from "../../sdk-alias.js";
|
||||
import { uniqueStrings } from "../shared.js";
|
||||
import { normalizeContractStringValues } from "../shared.js";
|
||||
|
||||
// Build/test inventory only.
|
||||
// Runtime code should prefer manifest/runtime registry queries instead of these snapshots.
|
||||
@@ -111,7 +111,7 @@ function normalizeSetupProviderEnvVars(setup: PluginManifest["setup"]): Record<s
|
||||
(provider) =>
|
||||
[
|
||||
provider.id.trim(),
|
||||
uniqueStrings(provider.envVars ?? [], (value) =>
|
||||
normalizeContractStringValues(provider.envVars ?? [], (value) =>
|
||||
typeof value === "string" ? value.trim() : "",
|
||||
),
|
||||
] as const,
|
||||
@@ -126,57 +126,68 @@ function buildBundledPluginContractSnapshot(
|
||||
): BundledPluginContractSnapshot {
|
||||
return {
|
||||
pluginId: manifest.id,
|
||||
cliBackendIds: uniqueStrings(manifest.cliBackends, (value) => value.trim()),
|
||||
providerIds: uniqueStrings(manifest.providers, (value) => value.trim()),
|
||||
cliBackendIds: normalizeContractStringValues(manifest.cliBackends, (value) => value.trim()),
|
||||
providerIds: normalizeContractStringValues(manifest.providers, (value) => value.trim()),
|
||||
providerEnvVars: normalizeSetupProviderEnvVars(manifest.setup),
|
||||
workerProviderIds: uniqueStrings(manifest.contracts?.workerProviders, (value) => value.trim()),
|
||||
embeddingProviderIds: uniqueStrings(manifest.contracts?.embeddingProviders, (value) =>
|
||||
workerProviderIds: normalizeContractStringValues(manifest.contracts?.workerProviders, (value) =>
|
||||
value.trim(),
|
||||
),
|
||||
speechProviderIds: uniqueStrings(manifest.contracts?.speechProviders, (value) => value.trim()),
|
||||
realtimeTranscriptionProviderIds: uniqueStrings(
|
||||
embeddingProviderIds: normalizeContractStringValues(
|
||||
manifest.contracts?.embeddingProviders,
|
||||
(value) => value.trim(),
|
||||
),
|
||||
speechProviderIds: normalizeContractStringValues(manifest.contracts?.speechProviders, (value) =>
|
||||
value.trim(),
|
||||
),
|
||||
realtimeTranscriptionProviderIds: normalizeContractStringValues(
|
||||
manifest.contracts?.realtimeTranscriptionProviders,
|
||||
(value) => value.trim(),
|
||||
),
|
||||
realtimeVoiceProviderIds: uniqueStrings(manifest.contracts?.realtimeVoiceProviders, (value) =>
|
||||
value.trim(),
|
||||
realtimeVoiceProviderIds: normalizeContractStringValues(
|
||||
manifest.contracts?.realtimeVoiceProviders,
|
||||
(value) => value.trim(),
|
||||
),
|
||||
mediaUnderstandingProviderIds: uniqueStrings(
|
||||
mediaUnderstandingProviderIds: normalizeContractStringValues(
|
||||
manifest.contracts?.mediaUnderstandingProviders,
|
||||
(value) => value.trim(),
|
||||
),
|
||||
transcriptSourceProviderIds: uniqueStrings(
|
||||
transcriptSourceProviderIds: normalizeContractStringValues(
|
||||
manifest.contracts?.transcriptSourceProviders,
|
||||
(value) => value.trim(),
|
||||
),
|
||||
documentExtractorIds: uniqueStrings(manifest.contracts?.documentExtractors, (value) =>
|
||||
value.trim(),
|
||||
documentExtractorIds: normalizeContractStringValues(
|
||||
manifest.contracts?.documentExtractors,
|
||||
(value) => value.trim(),
|
||||
),
|
||||
imageGenerationProviderIds: uniqueStrings(
|
||||
imageGenerationProviderIds: normalizeContractStringValues(
|
||||
manifest.contracts?.imageGenerationProviders,
|
||||
(value) => value.trim(),
|
||||
),
|
||||
videoGenerationProviderIds: uniqueStrings(
|
||||
videoGenerationProviderIds: normalizeContractStringValues(
|
||||
manifest.contracts?.videoGenerationProviders,
|
||||
(value) => value.trim(),
|
||||
),
|
||||
musicGenerationProviderIds: uniqueStrings(
|
||||
musicGenerationProviderIds: normalizeContractStringValues(
|
||||
manifest.contracts?.musicGenerationProviders,
|
||||
(value) => value.trim(),
|
||||
),
|
||||
webContentExtractorIds: uniqueStrings(manifest.contracts?.webContentExtractors, (value) =>
|
||||
value.trim(),
|
||||
webContentExtractorIds: normalizeContractStringValues(
|
||||
manifest.contracts?.webContentExtractors,
|
||||
(value) => value.trim(),
|
||||
),
|
||||
webFetchProviderIds: uniqueStrings(manifest.contracts?.webFetchProviders, (value) =>
|
||||
value.trim(),
|
||||
webFetchProviderIds: normalizeContractStringValues(
|
||||
manifest.contracts?.webFetchProviders,
|
||||
(value) => value.trim(),
|
||||
),
|
||||
webSearchProviderIds: uniqueStrings(manifest.contracts?.webSearchProviders, (value) =>
|
||||
value.trim(),
|
||||
webSearchProviderIds: normalizeContractStringValues(
|
||||
manifest.contracts?.webSearchProviders,
|
||||
(value) => value.trim(),
|
||||
),
|
||||
migrationProviderIds: uniqueStrings(manifest.contracts?.migrationProviders, (value) =>
|
||||
value.trim(),
|
||||
migrationProviderIds: normalizeContractStringValues(
|
||||
manifest.contracts?.migrationProviders,
|
||||
(value) => value.trim(),
|
||||
),
|
||||
toolNames: uniqueStrings(manifest.contracts?.tools, (value) => value.trim()),
|
||||
toolNames: normalizeContractStringValues(manifest.contracts?.tools, (value) => value.trim()),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS,
|
||||
type BundledPluginContractSnapshot,
|
||||
} from "./inventory/bundled-capability-metadata.js";
|
||||
import { uniqueStrings } from "./shared.js";
|
||||
import { normalizeContractStringValues } from "./shared.js";
|
||||
|
||||
type BundledCapabilityRuntimeRegistry = ReturnType<typeof loadBundledCapabilityRuntimeRegistry>;
|
||||
type CapabilityContractEntry<T> = {
|
||||
@@ -34,7 +34,7 @@ function normalizeProviderEnvVars(
|
||||
return Object.fromEntries(
|
||||
Object.entries(providerEnvVars ?? {}).map(([providerId, envVars]) => [
|
||||
providerId,
|
||||
uniqueStrings(envVars),
|
||||
normalizeContractStringValues(envVars),
|
||||
]),
|
||||
);
|
||||
}
|
||||
@@ -44,7 +44,7 @@ function resolvePluginProviderEnvVars(plugin: {
|
||||
}): Record<string, string[]> {
|
||||
const envVars: Record<string, string[]> = {};
|
||||
for (const provider of plugin.setup?.providers ?? []) {
|
||||
envVars[provider.id] = uniqueStrings(provider.envVars ?? []);
|
||||
envVars[provider.id] = normalizeContractStringValues(provider.envVars ?? []);
|
||||
}
|
||||
return normalizeProviderEnvVars(envVars);
|
||||
}
|
||||
@@ -99,29 +99,49 @@ function resolveBundledManifestContracts(): PluginRegistrationContractEntry[] {
|
||||
)
|
||||
.map((plugin) => ({
|
||||
pluginId: plugin.id,
|
||||
cliBackendIds: uniqueStrings(plugin.cliBackends),
|
||||
providerIds: uniqueStrings(plugin.providers),
|
||||
cliBackendIds: normalizeContractStringValues(plugin.cliBackends),
|
||||
providerIds: normalizeContractStringValues(plugin.providers),
|
||||
providerEnvVars: resolvePluginProviderEnvVars(plugin),
|
||||
workerProviderIds: uniqueStrings(plugin.contracts?.workerProviders ?? []),
|
||||
embeddingProviderIds: uniqueStrings(plugin.contracts?.embeddingProviders ?? []),
|
||||
speechProviderIds: uniqueStrings(plugin.contracts?.speechProviders ?? []),
|
||||
realtimeTranscriptionProviderIds: uniqueStrings(
|
||||
workerProviderIds: normalizeContractStringValues(plugin.contracts?.workerProviders ?? []),
|
||||
embeddingProviderIds: normalizeContractStringValues(
|
||||
plugin.contracts?.embeddingProviders ?? [],
|
||||
),
|
||||
speechProviderIds: normalizeContractStringValues(plugin.contracts?.speechProviders ?? []),
|
||||
realtimeTranscriptionProviderIds: normalizeContractStringValues(
|
||||
plugin.contracts?.realtimeTranscriptionProviders ?? [],
|
||||
),
|
||||
realtimeVoiceProviderIds: uniqueStrings(plugin.contracts?.realtimeVoiceProviders ?? []),
|
||||
mediaUnderstandingProviderIds: uniqueStrings(
|
||||
realtimeVoiceProviderIds: normalizeContractStringValues(
|
||||
plugin.contracts?.realtimeVoiceProviders ?? [],
|
||||
),
|
||||
mediaUnderstandingProviderIds: normalizeContractStringValues(
|
||||
plugin.contracts?.mediaUnderstandingProviders ?? [],
|
||||
),
|
||||
transcriptSourceProviderIds: uniqueStrings(plugin.contracts?.transcriptSourceProviders ?? []),
|
||||
documentExtractorIds: uniqueStrings(plugin.contracts?.documentExtractors ?? []),
|
||||
imageGenerationProviderIds: uniqueStrings(plugin.contracts?.imageGenerationProviders ?? []),
|
||||
videoGenerationProviderIds: uniqueStrings(plugin.contracts?.videoGenerationProviders ?? []),
|
||||
musicGenerationProviderIds: uniqueStrings(plugin.contracts?.musicGenerationProviders ?? []),
|
||||
webContentExtractorIds: uniqueStrings(plugin.contracts?.webContentExtractors ?? []),
|
||||
webFetchProviderIds: uniqueStrings(plugin.contracts?.webFetchProviders ?? []),
|
||||
webSearchProviderIds: uniqueStrings(plugin.contracts?.webSearchProviders ?? []),
|
||||
migrationProviderIds: uniqueStrings(plugin.contracts?.migrationProviders ?? []),
|
||||
toolNames: uniqueStrings(plugin.contracts?.tools ?? []),
|
||||
transcriptSourceProviderIds: normalizeContractStringValues(
|
||||
plugin.contracts?.transcriptSourceProviders ?? [],
|
||||
),
|
||||
documentExtractorIds: normalizeContractStringValues(
|
||||
plugin.contracts?.documentExtractors ?? [],
|
||||
),
|
||||
imageGenerationProviderIds: normalizeContractStringValues(
|
||||
plugin.contracts?.imageGenerationProviders ?? [],
|
||||
),
|
||||
videoGenerationProviderIds: normalizeContractStringValues(
|
||||
plugin.contracts?.videoGenerationProviders ?? [],
|
||||
),
|
||||
musicGenerationProviderIds: normalizeContractStringValues(
|
||||
plugin.contracts?.musicGenerationProviders ?? [],
|
||||
),
|
||||
webContentExtractorIds: normalizeContractStringValues(
|
||||
plugin.contracts?.webContentExtractors ?? [],
|
||||
),
|
||||
webFetchProviderIds: normalizeContractStringValues(plugin.contracts?.webFetchProviders ?? []),
|
||||
webSearchProviderIds: normalizeContractStringValues(
|
||||
plugin.contracts?.webSearchProviders ?? [],
|
||||
),
|
||||
migrationProviderIds: normalizeContractStringValues(
|
||||
plugin.contracts?.migrationProviders ?? [],
|
||||
),
|
||||
toolNames: normalizeContractStringValues(plugin.contracts?.tools ?? []),
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Shared upstream model contract tests keep capability flags aligned across bundled catalogs.
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { asOptionalRecord as readRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { listGitTrackedFiles } from "../../test-utils/repo-files.js";
|
||||
|
||||
@@ -57,12 +58,6 @@ function normalizeSharedModelId(modelId: string): string {
|
||||
return (separator === -1 ? modelId : modelId.slice(separator + 1)).toLowerCase();
|
||||
}
|
||||
|
||||
function readRecord(value: unknown): Record<string, unknown> | undefined {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function collectCatalogEntries(): CatalogEntry[] {
|
||||
const entries: CatalogEntry[] = [];
|
||||
for (const manifest of readBundledManifests()) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** Returns unique normalized string values while preserving first-seen order. */
|
||||
export function uniqueStrings(
|
||||
export function normalizeContractStringValues(
|
||||
values: readonly string[] | undefined,
|
||||
normalize: (value: string) => string = (value) => value,
|
||||
): string[] {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** Converts loaded plugin registries into stable plugin records for status and diagnostics. */
|
||||
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
|
||||
import { parseBooleanValue } from "../utils/boolean.js";
|
||||
import type { PluginCompatCode } from "./compat/registry.js";
|
||||
import type { PluginActivationState } from "./config-state.js";
|
||||
import type { PluginBundleFormat, PluginDiagnosticCode, PluginFormat } from "./manifest-types.js";
|
||||
@@ -204,8 +204,7 @@ export function formatPluginFailureSummary(failedPlugins: PluginRecord[]): strin
|
||||
}
|
||||
|
||||
function isPluginLoadDebugEnabled(env: NodeJS.ProcessEnv): boolean {
|
||||
const normalized = normalizeLowercaseStringOrEmpty(env.OPENCLAW_PLUGIN_LOAD_DEBUG);
|
||||
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
||||
return parseBooleanValue(env.OPENCLAW_PLUGIN_LOAD_DEBUG) === true;
|
||||
}
|
||||
|
||||
function describePluginModuleExportShape(
|
||||
|
||||
Reference in New Issue
Block a user