mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:10:45 +00:00
refactor(plugins): enforce config API deprecations
This commit is contained in:
@@ -1336,7 +1336,7 @@
|
||||
"check:base-config-schema": "node --import tsx scripts/generate-base-config-schema.ts --check",
|
||||
"check:bundled-channel-config-metadata": "node --import tsx scripts/generate-bundled-channel-config-metadata.ts --check",
|
||||
"check:changed": "node scripts/check-changed.mjs",
|
||||
"check:deprecated-internal-config-api": "pnpm test src/plugins/contracts/deprecated-internal-config-api.test.ts -- --reporter=verbose",
|
||||
"check:deprecated-internal-config-api": "node scripts/check-deprecated-internal-config-api.mjs",
|
||||
"check:docs": "pnpm format:docs:check && pnpm lint:docs && pnpm docs:check-mdx && pnpm docs:check-i18n-glossary && pnpm docs:check-links",
|
||||
"check:host-env-policy:swift": "node scripts/generate-host-env-security-policy-swift.mjs --check",
|
||||
"check:import-cycles": "node --import tsx scripts/check-import-cycles.ts",
|
||||
|
||||
20
scripts/check-deprecated-internal-config-api.mjs
Normal file
20
scripts/check-deprecated-internal-config-api.mjs
Normal file
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env node
|
||||
import { collectDeprecatedInternalConfigApiViolations } from "./lib/deprecated-config-api-guard.mjs";
|
||||
|
||||
export function main() {
|
||||
const violations = collectDeprecatedInternalConfigApiViolations();
|
||||
if (violations.length === 0) {
|
||||
console.log("deprecated internal config API guard passed");
|
||||
return 0;
|
||||
}
|
||||
|
||||
console.error("Deprecated internal config API guard failed:");
|
||||
for (const violation of violations) {
|
||||
console.error(`- ${violation}`);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
process.exitCode = main();
|
||||
}
|
||||
3
scripts/lib/deprecated-config-api-guard.d.mts
Normal file
3
scripts/lib/deprecated-config-api-guard.d.mts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function collectDeprecatedInternalConfigApiViolations(options?: {
|
||||
repoRoot?: string;
|
||||
}): string[];
|
||||
325
scripts/lib/deprecated-config-api-guard.mjs
Normal file
325
scripts/lib/deprecated-config-api-guard.mjs
Normal file
@@ -0,0 +1,325 @@
|
||||
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
||||
import { dirname, relative, resolve, sep } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const DEFAULT_REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
|
||||
const COMPAT_CONFIG_API_FILES = new Set([
|
||||
"src/config/config.ts",
|
||||
"src/config/io.ts",
|
||||
"src/config/mutate.ts",
|
||||
"src/memory-host-sdk/runtime-core.ts",
|
||||
"src/plugin-sdk/browser-config-runtime.ts",
|
||||
"src/plugin-sdk/config-runtime.ts",
|
||||
"src/plugin-sdk/memory-core.ts",
|
||||
"src/plugin-sdk/memory-core-host-runtime-core.ts",
|
||||
"src/plugins/contracts/deprecated-internal-config-api.test.ts",
|
||||
"src/plugins/runtime/runtime-config.test.ts",
|
||||
"src/plugins/runtime/runtime-config.ts",
|
||||
"src/plugins/runtime/types-core.ts",
|
||||
]);
|
||||
|
||||
const AMBIENT_RUNTIME_LOAD_CONFIG_COMPAT_FILES = new Set([
|
||||
"src/plugins/runtime/load-context.ts",
|
||||
"src/plugins/runtime/runtime-config.ts",
|
||||
"src/plugins/runtime/runtime-plugin-boundary.ts",
|
||||
]);
|
||||
|
||||
const PROCESS_BOUNDARY_DIRECT_CONFIG_LOAD_FILES = new Set([
|
||||
"src/cli/banner-config-lite.ts",
|
||||
"src/cli/daemon-cli/status.gather.ts",
|
||||
]);
|
||||
|
||||
function collectTypeScriptFiles(dir) {
|
||||
if (!existsSync(dir)) {
|
||||
return [];
|
||||
}
|
||||
const entries = readdirSync(dir, { withFileTypes: true });
|
||||
const files = [];
|
||||
for (const entry of entries) {
|
||||
const fullPath = resolve(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
if (entry.name === "dist" || entry.name === "node_modules") {
|
||||
continue;
|
||||
}
|
||||
files.push(...collectTypeScriptFiles(fullPath));
|
||||
continue;
|
||||
}
|
||||
if (entry.isFile() && entry.name.endsWith(".ts")) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function repoRelative(repoRoot, filePath) {
|
||||
return relative(repoRoot, filePath).split(sep).join("/");
|
||||
}
|
||||
|
||||
function isProductionExtensionFile(relPath) {
|
||||
if (
|
||||
relPath.includes("/test-support/") ||
|
||||
relPath.includes(".test.") ||
|
||||
relPath.includes(".live.test.") ||
|
||||
relPath.includes(".test-d.") ||
|
||||
relPath.includes(".test-harness.") ||
|
||||
relPath.includes(".test-shared.") ||
|
||||
relPath.endsWith("-test-helpers.ts") ||
|
||||
relPath.endsWith("-test-support.ts")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function isTestOrHarnessFile(relPath) {
|
||||
return (
|
||||
relPath.includes("test-support") ||
|
||||
relPath.includes("/test-support/") ||
|
||||
relPath.includes("/test-helpers/") ||
|
||||
relPath.includes(".test.") ||
|
||||
relPath.includes(".live.test.") ||
|
||||
relPath.includes(".test-d.") ||
|
||||
relPath.includes(".test-harness.") ||
|
||||
relPath.includes(".test-shared.") ||
|
||||
relPath.endsWith(".test-helpers.ts") ||
|
||||
relPath.endsWith(".test-support.ts") ||
|
||||
relPath.endsWith("-test-helpers.ts") ||
|
||||
relPath.endsWith("-test-support.ts")
|
||||
);
|
||||
}
|
||||
|
||||
function isCompatConfigApiFile(relPath) {
|
||||
return COMPAT_CONFIG_API_FILES.has(relPath);
|
||||
}
|
||||
|
||||
function isAmbientRuntimeConfigCompatFile(relPath) {
|
||||
return AMBIENT_RUNTIME_LOAD_CONFIG_COMPAT_FILES.has(relPath);
|
||||
}
|
||||
|
||||
function findLineNumbers(source, pattern) {
|
||||
const lines = source.split(/\r?\n/);
|
||||
return lines.flatMap((line, index) => (pattern.test(line) ? [index + 1] : []));
|
||||
}
|
||||
|
||||
function findMatchLineNumbers(source, pattern) {
|
||||
const flags = pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`;
|
||||
const regex = new RegExp(pattern.source, flags);
|
||||
const lines = [];
|
||||
for (let match = regex.exec(source); match; match = regex.exec(source)) {
|
||||
lines.push(source.slice(0, match.index).split(/\r?\n/).length);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
function findNonCommentLineNumbers(source, pattern) {
|
||||
return source.split(/\r?\n/).flatMap((line, index) => {
|
||||
const trimmed = line.trimStart();
|
||||
if (trimmed.startsWith("//") || trimmed.startsWith("*")) {
|
||||
return [];
|
||||
}
|
||||
return pattern.test(line) ? [index + 1] : [];
|
||||
});
|
||||
}
|
||||
|
||||
function repoCodeRoots(repoRoot) {
|
||||
return ["src", "extensions", "packages", "test", "scripts"].map((entry) =>
|
||||
resolve(repoRoot, entry),
|
||||
);
|
||||
}
|
||||
|
||||
function pushDeprecatedRuntimeApiViolations(violations, files) {
|
||||
const guards = [
|
||||
{
|
||||
pattern:
|
||||
/(?:api\.runtime\.config|core\.config|runtime\.config|get[A-Za-z0-9]+Runtime\(\)\.config|rt\.config|configApi)\??\.loadConfig\b/,
|
||||
replacement: "use runtime.config.current() or pass the already loaded config",
|
||||
},
|
||||
{
|
||||
pattern:
|
||||
/(?:api\.runtime\.config|core\.config|runtime\.config|get[A-Za-z0-9]+Runtime\(\)\.config|rt\.config|configApi)\??\.writeConfigFile\b/,
|
||||
replacement:
|
||||
"use runtime.config.mutateConfigFile(...) or replaceConfigFile(...) with afterWrite",
|
||||
},
|
||||
];
|
||||
|
||||
for (const { filePath, relPath } of files) {
|
||||
const source = readFileSync(filePath, "utf8");
|
||||
for (const guard of guards) {
|
||||
for (const line of findMatchLineNumbers(source, guard.pattern)) {
|
||||
violations.push(`${relPath}:${line} ${guard.replacement}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function collectDeprecatedInternalConfigApiViolations({
|
||||
repoRoot = DEFAULT_REPO_ROOT,
|
||||
} = {}) {
|
||||
const srcRoot = resolve(repoRoot, "src");
|
||||
const extensionsRoot = resolve(repoRoot, "extensions");
|
||||
const gatewayServerMethodsRoot = resolve(srcRoot, "gateway/server-methods");
|
||||
const ambientRuntimeConfigRoots = [
|
||||
"src/gateway",
|
||||
"src/auto-reply",
|
||||
"src/agents",
|
||||
"src/infra",
|
||||
"src/mcp",
|
||||
"src/plugins/runtime",
|
||||
"src/config/sessions",
|
||||
].map((entry) => resolve(repoRoot, entry));
|
||||
|
||||
const violations = [];
|
||||
|
||||
const productionExtensionFiles = collectTypeScriptFiles(extensionsRoot)
|
||||
.map((filePath) => ({ filePath, relPath: repoRelative(repoRoot, filePath) }))
|
||||
.filter(({ relPath }) => isProductionExtensionFile(relPath));
|
||||
pushDeprecatedRuntimeApiViolations(violations, productionExtensionFiles);
|
||||
|
||||
for (const { filePath, relPath } of productionExtensionFiles) {
|
||||
const source = readFileSync(filePath, "utf8");
|
||||
const guards = [
|
||||
{
|
||||
pattern:
|
||||
/\b(?:import|export)\s+(?:type\s+)?\{[^}]*\bloadConfig\b[^}]*\}\s+from\s+["']openclaw\/plugin-sdk\/(?:browser-config-runtime|config-runtime|memory-core-host-runtime-core)["']/,
|
||||
replacement:
|
||||
"use getRuntimeConfig(), runtime.config.current(), or pass the already loaded config",
|
||||
},
|
||||
{
|
||||
pattern: /(?<!\.)\bloadConfig\s*\(/,
|
||||
replacement: "use getRuntimeConfig(), runtime.config.current(), or passed config",
|
||||
},
|
||||
{
|
||||
pattern: /\bcreateConfigIO\b|\.\s*loadConfig\s*\(/,
|
||||
replacement: "use runtime.config.current(), getRuntimeConfig(), or passed config",
|
||||
},
|
||||
{
|
||||
pattern: /\bwriteConfigFile\s*\(/,
|
||||
replacement: "use mutateConfigFile(...) or replaceConfigFile(...) with afterWrite",
|
||||
},
|
||||
];
|
||||
for (const guard of guards) {
|
||||
for (const line of findLineNumbers(source, guard.pattern)) {
|
||||
violations.push(`${relPath}:${line} ${guard.replacement}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const repoFiles = repoCodeRoots(repoRoot)
|
||||
.flatMap(collectTypeScriptFiles)
|
||||
.map((filePath) => ({ filePath, relPath: repoRelative(repoRoot, filePath) }));
|
||||
|
||||
pushDeprecatedRuntimeApiViolations(
|
||||
violations,
|
||||
repoFiles.filter(({ relPath }) => !isCompatConfigApiFile(relPath)),
|
||||
);
|
||||
|
||||
for (const { filePath, relPath } of repoFiles.filter(
|
||||
({ relPath }) => !isCompatConfigApiFile(relPath),
|
||||
)) {
|
||||
const source = readFileSync(filePath, "utf8");
|
||||
const guards = [
|
||||
{
|
||||
pattern:
|
||||
/\b(?:import|export)\s+(?:type\s+)?\{[\s\S]*?\b(?:loadConfig|writeConfigFile)\b[\s\S]*?\}\s+from\s+["']openclaw\/plugin-sdk\/(?:browser-config-runtime|config-runtime|memory-core-host-runtime-core|memory-core)["']/,
|
||||
replacement:
|
||||
"use getRuntimeConfig(), runtime.config.current(), or mutation helpers with afterWrite",
|
||||
},
|
||||
{
|
||||
pattern:
|
||||
/ReturnType<typeof import\(["']openclaw\/plugin-sdk\/(?:browser-config-runtime|config-runtime|memory-core-host-runtime-core|memory-core)["']\)\.(?:loadConfig|writeConfigFile)>/,
|
||||
replacement: "use OpenClawConfig or the explicit mutation helper type",
|
||||
},
|
||||
];
|
||||
for (const guard of guards) {
|
||||
for (const line of findMatchLineNumbers(source, guard.pattern)) {
|
||||
violations.push(`${relPath}:${line} ${guard.replacement}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const { filePath, relPath } of repoFiles.filter(
|
||||
({ relPath }) =>
|
||||
!isTestOrHarnessFile(relPath) &&
|
||||
!isCompatConfigApiFile(relPath) &&
|
||||
!relPath.startsWith("test/"),
|
||||
)) {
|
||||
const source = readFileSync(filePath, "utf8");
|
||||
const importPattern =
|
||||
/\bimport\s+\{[\s\S]*?\bwriteConfigFile\b[\s\S]*?\}\s+from\s+["'][^"']*(?:config\/config|config\/io)\.js["']/;
|
||||
const dynamicImportPattern =
|
||||
/\bconst\s+\{[\s\S]*?\bwriteConfigFile\b[\s\S]*?\}\s*=\s*await\s+import\(["'][^"']*(?:config\/config|config\/io)\.js["']\)/;
|
||||
const directMethodPattern = /\.\s*writeConfigFile\s*\(/;
|
||||
for (const pattern of [importPattern, dynamicImportPattern]) {
|
||||
for (const line of findMatchLineNumbers(source, pattern)) {
|
||||
violations.push(
|
||||
`${relPath}:${line} use replaceConfigFile(...) or mutateConfigFile(...) with afterWrite`,
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const line of findNonCommentLineNumbers(source, directMethodPattern)) {
|
||||
violations.push(
|
||||
`${relPath}:${line} use replaceConfigFile(...) or mutateConfigFile(...) with afterWrite`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const { filePath, relPath } of repoFiles.filter(
|
||||
({ relPath }) =>
|
||||
!isTestOrHarnessFile(relPath) &&
|
||||
!isCompatConfigApiFile(relPath) &&
|
||||
!PROCESS_BOUNDARY_DIRECT_CONFIG_LOAD_FILES.has(relPath) &&
|
||||
!relPath.startsWith("test/"),
|
||||
)) {
|
||||
const source = readFileSync(filePath, "utf8");
|
||||
for (const line of findNonCommentLineNumbers(source, /(?<!\.)\bloadConfig\s*\(/)) {
|
||||
violations.push(
|
||||
`${relPath}:${line} use a passed cfg, context.getRuntimeConfig(), or getRuntimeConfig() at an explicit process boundary`,
|
||||
);
|
||||
}
|
||||
for (const line of findNonCommentLineNumbers(source, /\.\s*loadConfig\s*\(/)) {
|
||||
violations.push(
|
||||
`${relPath}:${line} use a passed cfg, context.getRuntimeConfig(), or getRuntimeConfig() at an explicit process boundary`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const { filePath, relPath } of collectTypeScriptFiles(gatewayServerMethodsRoot)
|
||||
.map((filePath) => ({ filePath, relPath: repoRelative(repoRoot, filePath) }))
|
||||
.filter(({ relPath }) => !isTestOrHarnessFile(relPath))) {
|
||||
const source = readFileSync(filePath, "utf8");
|
||||
const importPattern =
|
||||
/\bimport\s+\{[\s\S]*?\bloadConfig\b[\s\S]*?\}\s+from\s+["'][^"']*(?:config\/config|config\/io)\.js["']/;
|
||||
for (const line of findMatchLineNumbers(source, importPattern)) {
|
||||
violations.push(
|
||||
`${relPath}:${line} use context.getRuntimeConfig() in gateway request handlers`,
|
||||
);
|
||||
}
|
||||
for (const line of findNonCommentLineNumbers(source, /(?<!\.)\bloadConfig\s*\(/)) {
|
||||
violations.push(
|
||||
`${relPath}:${line} use context.getRuntimeConfig() in gateway request handlers`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const { filePath, relPath } of ambientRuntimeConfigRoots
|
||||
.flatMap(collectTypeScriptFiles)
|
||||
.map((filePath) => ({ filePath, relPath: repoRelative(repoRoot, filePath) }))
|
||||
.filter(
|
||||
({ relPath }) =>
|
||||
!isTestOrHarnessFile(relPath) &&
|
||||
!isCompatConfigApiFile(relPath) &&
|
||||
!isAmbientRuntimeConfigCompatFile(relPath),
|
||||
)) {
|
||||
const source = readFileSync(filePath, "utf8");
|
||||
const loadConfigLines = findNonCommentLineNumbers(source, /(?<!\.)\bloadConfig\s*\(/);
|
||||
if (loadConfigLines.length === 0) {
|
||||
continue;
|
||||
}
|
||||
violations.push(
|
||||
`${relPath}:${loadConfigLines.join(",")} has ${loadConfigLines.length} ambient loadConfig() calls. Pass cfg through the call path, use context.getRuntimeConfig(), or use getRuntimeConfig() at a process boundary.`,
|
||||
);
|
||||
}
|
||||
|
||||
return violations;
|
||||
}
|
||||
@@ -1,355 +1,8 @@
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import { dirname, relative, resolve, sep } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const SRC_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const REPO_ROOT = resolve(SRC_ROOT, "..");
|
||||
const EXTENSIONS_ROOT = resolve(REPO_ROOT, "extensions");
|
||||
const REPO_CODE_ROOTS = ["src", "extensions", "packages", "test", "scripts"].map((entry) =>
|
||||
resolve(REPO_ROOT, entry),
|
||||
);
|
||||
const GATEWAY_SERVER_METHODS_ROOT = resolve(SRC_ROOT, "gateway/server-methods");
|
||||
const AMBIENT_RUNTIME_CONFIG_ROOTS = [
|
||||
"src/gateway",
|
||||
"src/auto-reply",
|
||||
"src/agents",
|
||||
"src/infra",
|
||||
"src/mcp",
|
||||
"src/plugins/runtime",
|
||||
"src/config/sessions",
|
||||
].map((entry) => resolve(REPO_ROOT, entry));
|
||||
|
||||
const COMPAT_CONFIG_API_FILES = new Set([
|
||||
"src/config/config.ts",
|
||||
"src/config/io.ts",
|
||||
"src/config/mutate.ts",
|
||||
"src/memory-host-sdk/runtime-core.ts",
|
||||
"src/plugin-sdk/browser-config-runtime.ts",
|
||||
"src/plugin-sdk/config-runtime.ts",
|
||||
"src/plugin-sdk/memory-core.ts",
|
||||
"src/plugin-sdk/memory-core-host-runtime-core.ts",
|
||||
"src/plugins/contracts/deprecated-internal-config-api.test.ts",
|
||||
"src/plugins/runtime/runtime-config.test.ts",
|
||||
"src/plugins/runtime/runtime-config.ts",
|
||||
"src/plugins/runtime/types-core.ts",
|
||||
]);
|
||||
const AMBIENT_RUNTIME_LOAD_CONFIG_COMPAT_FILES = new Set([
|
||||
"src/plugins/runtime/load-context.ts",
|
||||
"src/plugins/runtime/runtime-config.ts",
|
||||
"src/plugins/runtime/runtime-plugin-boundary.ts",
|
||||
]);
|
||||
const PROCESS_BOUNDARY_DIRECT_CONFIG_LOAD_FILES = new Set([
|
||||
"src/cli/banner-config-lite.ts",
|
||||
"src/cli/daemon-cli/status.gather.ts",
|
||||
]);
|
||||
|
||||
function collectTypeScriptFiles(dir: string): string[] {
|
||||
const entries = readdirSync(dir, { withFileTypes: true });
|
||||
const files: string[] = [];
|
||||
for (const entry of entries) {
|
||||
const fullPath = resolve(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
if (entry.name === "dist" || entry.name === "node_modules") {
|
||||
continue;
|
||||
}
|
||||
files.push(...collectTypeScriptFiles(fullPath));
|
||||
continue;
|
||||
}
|
||||
if (entry.isFile() && entry.name.endsWith(".ts")) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function repoRelative(filePath: string): string {
|
||||
return relative(REPO_ROOT, filePath).split(sep).join("/");
|
||||
}
|
||||
|
||||
function isProductionExtensionFile(relPath: string): boolean {
|
||||
if (
|
||||
relPath.includes("/test-support/") ||
|
||||
relPath.includes(".test.") ||
|
||||
relPath.includes(".live.test.") ||
|
||||
relPath.includes(".test-d.") ||
|
||||
relPath.includes(".test-harness.") ||
|
||||
relPath.includes(".test-shared.") ||
|
||||
relPath.endsWith("-test-helpers.ts") ||
|
||||
relPath.endsWith("-test-support.ts")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function isTestOrHarnessFile(relPath: string): boolean {
|
||||
return (
|
||||
relPath.includes("test-support") ||
|
||||
relPath.includes("/test-support/") ||
|
||||
relPath.includes("/test-helpers/") ||
|
||||
relPath.includes(".test.") ||
|
||||
relPath.includes(".live.test.") ||
|
||||
relPath.includes(".test-d.") ||
|
||||
relPath.includes(".test-harness.") ||
|
||||
relPath.includes(".test-shared.") ||
|
||||
relPath.endsWith(".test-helpers.ts") ||
|
||||
relPath.endsWith(".test-support.ts") ||
|
||||
relPath.endsWith("-test-helpers.ts") ||
|
||||
relPath.endsWith("-test-support.ts")
|
||||
);
|
||||
}
|
||||
|
||||
function isCompatConfigApiFile(relPath: string): boolean {
|
||||
return COMPAT_CONFIG_API_FILES.has(relPath);
|
||||
}
|
||||
|
||||
function isAmbientRuntimeConfigCompatFile(relPath: string): boolean {
|
||||
return AMBIENT_RUNTIME_LOAD_CONFIG_COMPAT_FILES.has(relPath);
|
||||
}
|
||||
|
||||
function findLineNumbers(source: string, pattern: RegExp): number[] {
|
||||
const lines = source.split(/\r?\n/);
|
||||
return lines.flatMap((line, index) => (pattern.test(line) ? [index + 1] : []));
|
||||
}
|
||||
|
||||
function findMatchLineNumbers(source: string, pattern: RegExp): number[] {
|
||||
const flags = pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`;
|
||||
const regex = new RegExp(pattern.source, flags);
|
||||
const lines: number[] = [];
|
||||
for (let match = regex.exec(source); match; match = regex.exec(source)) {
|
||||
lines.push(source.slice(0, match.index).split(/\r?\n/).length);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
function findNonCommentLineNumbers(source: string, pattern: RegExp): number[] {
|
||||
return source.split(/\r?\n/).flatMap((line, index) => {
|
||||
const trimmed = line.trimStart();
|
||||
if (trimmed.startsWith("//") || trimmed.startsWith("*")) {
|
||||
return [];
|
||||
}
|
||||
return pattern.test(line) ? [index + 1] : [];
|
||||
});
|
||||
}
|
||||
import { collectDeprecatedInternalConfigApiViolations } from "../../../scripts/lib/deprecated-config-api-guard.mjs";
|
||||
|
||||
describe("deprecated internal config API guardrails", () => {
|
||||
it("keeps bundled plugin production code off direct runtime config load/write APIs", () => {
|
||||
const violations: string[] = [];
|
||||
const files = collectTypeScriptFiles(EXTENSIONS_ROOT)
|
||||
.map((filePath) => ({ filePath, relPath: repoRelative(filePath) }))
|
||||
.filter(({ relPath }) => isProductionExtensionFile(relPath));
|
||||
|
||||
for (const { filePath, relPath } of files) {
|
||||
const source = readFileSync(filePath, "utf8");
|
||||
const guards = [
|
||||
{
|
||||
pattern:
|
||||
/(?:api\.runtime\.config|core\.config|runtime\.config|get[A-Za-z0-9]+Runtime\(\)\.config|rt\.config|configApi)\??\.loadConfig\b/,
|
||||
replacement: "use runtime.config.current() or pass the already loaded config",
|
||||
},
|
||||
{
|
||||
pattern:
|
||||
/(?:api\.runtime\.config|core\.config|runtime\.config|get[A-Za-z0-9]+Runtime\(\)\.config|rt\.config|configApi)\??\.writeConfigFile\b/,
|
||||
replacement:
|
||||
"use runtime.config.mutateConfigFile(...) or replaceConfigFile(...) with afterWrite",
|
||||
},
|
||||
{
|
||||
pattern:
|
||||
/\b(?:import|export)\s+(?:type\s+)?\{[^}]*\bloadConfig\b[^}]*\}\s+from\s+["']openclaw\/plugin-sdk\/(?:browser-config-runtime|config-runtime|memory-core-host-runtime-core)["']/,
|
||||
replacement:
|
||||
"use getRuntimeConfig(), runtime.config.current(), or pass the already loaded config",
|
||||
},
|
||||
{
|
||||
pattern: /(?<!\.)\bloadConfig\s*\(/,
|
||||
replacement: "use getRuntimeConfig(), runtime.config.current(), or passed config",
|
||||
},
|
||||
{
|
||||
pattern: /\bcreateConfigIO\b|\.\s*loadConfig\s*\(/,
|
||||
replacement: "use runtime.config.current(), getRuntimeConfig(), or passed config",
|
||||
},
|
||||
{
|
||||
pattern: /\bwriteConfigFile\s*\(/,
|
||||
replacement: "use mutateConfigFile(...) or replaceConfigFile(...) with afterWrite",
|
||||
},
|
||||
];
|
||||
for (const guard of guards) {
|
||||
for (const line of findLineNumbers(source, guard.pattern)) {
|
||||
violations.push(`${relPath}:${line} ${guard.replacement}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(violations).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps repo code off deprecated plugin runtime config load/write APIs", () => {
|
||||
const violations: string[] = [];
|
||||
const files = REPO_CODE_ROOTS.flatMap(collectTypeScriptFiles)
|
||||
.map((filePath) => ({ filePath, relPath: repoRelative(filePath) }))
|
||||
.filter(({ relPath }) => !isCompatConfigApiFile(relPath));
|
||||
|
||||
const guards = [
|
||||
{
|
||||
pattern:
|
||||
/(?:api\.runtime\.config|core\.config|runtime\.config|get[A-Za-z0-9]+Runtime\(\)\.config|rt\.config|configApi)\??\.loadConfig\b/,
|
||||
replacement: "use runtime.config.current() or pass the already loaded config",
|
||||
},
|
||||
{
|
||||
pattern:
|
||||
/(?:api\.runtime\.config|core\.config|runtime\.config|get[A-Za-z0-9]+Runtime\(\)\.config|rt\.config|configApi)\??\.writeConfigFile\b/,
|
||||
replacement:
|
||||
"use runtime.config.mutateConfigFile(...) or replaceConfigFile(...) with afterWrite",
|
||||
},
|
||||
{
|
||||
pattern:
|
||||
/\b(?:import|export)\s+(?:type\s+)?\{[\s\S]*?\b(?:loadConfig|writeConfigFile)\b[\s\S]*?\}\s+from\s+["']openclaw\/plugin-sdk\/(?:browser-config-runtime|config-runtime|memory-core-host-runtime-core|memory-core)["']/,
|
||||
replacement:
|
||||
"use getRuntimeConfig(), runtime.config.current(), or mutation helpers with afterWrite",
|
||||
},
|
||||
{
|
||||
pattern:
|
||||
/ReturnType<typeof import\(["']openclaw\/plugin-sdk\/(?:browser-config-runtime|config-runtime|memory-core-host-runtime-core|memory-core)["']\)\.(?:loadConfig|writeConfigFile)>/,
|
||||
replacement: "use OpenClawConfig or the explicit mutation helper type",
|
||||
},
|
||||
];
|
||||
|
||||
for (const { filePath, relPath } of files) {
|
||||
const source = readFileSync(filePath, "utf8");
|
||||
for (const guard of guards) {
|
||||
for (const line of findMatchLineNumbers(source, guard.pattern)) {
|
||||
violations.push(`${relPath}:${line} ${guard.replacement}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(violations).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps production config writes on mutation helpers", () => {
|
||||
const violations: string[] = [];
|
||||
const files = REPO_CODE_ROOTS.flatMap(collectTypeScriptFiles)
|
||||
.map((filePath) => ({ filePath, relPath: repoRelative(filePath) }))
|
||||
.filter(
|
||||
({ relPath }) =>
|
||||
!isTestOrHarnessFile(relPath) &&
|
||||
!isCompatConfigApiFile(relPath) &&
|
||||
!relPath.startsWith("test/"),
|
||||
);
|
||||
|
||||
const importPattern =
|
||||
/\bimport\s+\{[\s\S]*?\bwriteConfigFile\b[\s\S]*?\}\s+from\s+["'][^"']*(?:config\/config|config\/io)\.js["']/;
|
||||
const dynamicImportPattern =
|
||||
/\bconst\s+\{[\s\S]*?\bwriteConfigFile\b[\s\S]*?\}\s*=\s*await\s+import\(["'][^"']*(?:config\/config|config\/io)\.js["']\)/;
|
||||
const directMethodPattern = /\.\s*writeConfigFile\s*\(/;
|
||||
|
||||
for (const { filePath, relPath } of files) {
|
||||
const source = readFileSync(filePath, "utf8");
|
||||
for (const pattern of [importPattern, dynamicImportPattern]) {
|
||||
for (const line of findMatchLineNumbers(source, pattern)) {
|
||||
violations.push(
|
||||
`${relPath}:${line} use replaceConfigFile(...) or mutateConfigFile(...) with afterWrite`,
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const line of findNonCommentLineNumbers(source, directMethodPattern)) {
|
||||
violations.push(
|
||||
`${relPath}:${line} use replaceConfigFile(...) or mutateConfigFile(...) with afterWrite`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
expect(violations).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps production code off direct config loads outside explicit process boundaries", () => {
|
||||
const violations: string[] = [];
|
||||
const files = REPO_CODE_ROOTS.flatMap(collectTypeScriptFiles)
|
||||
.map((filePath) => ({ filePath, relPath: repoRelative(filePath) }))
|
||||
.filter(
|
||||
({ relPath }) =>
|
||||
!isTestOrHarnessFile(relPath) &&
|
||||
!isCompatConfigApiFile(relPath) &&
|
||||
!PROCESS_BOUNDARY_DIRECT_CONFIG_LOAD_FILES.has(relPath) &&
|
||||
!relPath.startsWith("test/"),
|
||||
);
|
||||
|
||||
const directCallPattern = /(?<!\.)\bloadConfig\s*\(/;
|
||||
const directMethodPattern = /\.\s*loadConfig\s*\(/;
|
||||
|
||||
for (const { filePath, relPath } of files) {
|
||||
const source = readFileSync(filePath, "utf8");
|
||||
for (const line of findNonCommentLineNumbers(source, directCallPattern)) {
|
||||
violations.push(
|
||||
`${relPath}:${line} use a passed cfg, context.getRuntimeConfig(), or getRuntimeConfig() at an explicit process boundary`,
|
||||
);
|
||||
}
|
||||
for (const line of findNonCommentLineNumbers(source, directMethodPattern)) {
|
||||
violations.push(
|
||||
`${relPath}:${line} use a passed cfg, context.getRuntimeConfig(), or getRuntimeConfig() at an explicit process boundary`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
expect(violations).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps gateway server method handlers on the request runtime config snapshot", () => {
|
||||
const violations: string[] = [];
|
||||
const files = collectTypeScriptFiles(GATEWAY_SERVER_METHODS_ROOT)
|
||||
.map((filePath) => ({ filePath, relPath: repoRelative(filePath) }))
|
||||
.filter(({ relPath }) => !isTestOrHarnessFile(relPath));
|
||||
|
||||
const guards = [
|
||||
{
|
||||
pattern:
|
||||
/\bimport\s+\{[\s\S]*?\bloadConfig\b[\s\S]*?\}\s+from\s+["'][^"']*(?:config\/config|config\/io)\.js["']/,
|
||||
replacement: "use context.getRuntimeConfig() in gateway request handlers",
|
||||
},
|
||||
{
|
||||
pattern: /(?<!\.)\bloadConfig\s*\(/,
|
||||
replacement: "use context.getRuntimeConfig() in gateway request handlers",
|
||||
},
|
||||
];
|
||||
|
||||
for (const { filePath, relPath } of files) {
|
||||
const source = readFileSync(filePath, "utf8");
|
||||
for (const guard of guards) {
|
||||
const lines = guard.pattern.source.includes("import\\s+")
|
||||
? findMatchLineNumbers(source, guard.pattern)
|
||||
: findNonCommentLineNumbers(source, guard.pattern);
|
||||
for (const line of lines) {
|
||||
violations.push(`${relPath}:${line} ${guard.replacement}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(violations).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps long-lived runtime code off ambient loadConfig calls", () => {
|
||||
const violations: string[] = [];
|
||||
const files = AMBIENT_RUNTIME_CONFIG_ROOTS.flatMap(collectTypeScriptFiles)
|
||||
.map((filePath) => ({ filePath, relPath: repoRelative(filePath) }))
|
||||
.filter(
|
||||
({ relPath }) =>
|
||||
!isTestOrHarnessFile(relPath) &&
|
||||
!isCompatConfigApiFile(relPath) &&
|
||||
!isAmbientRuntimeConfigCompatFile(relPath),
|
||||
);
|
||||
|
||||
for (const { filePath, relPath } of files) {
|
||||
const source = readFileSync(filePath, "utf8");
|
||||
const loadConfigLines = findNonCommentLineNumbers(source, /(?<!\.)\bloadConfig\s*\(/);
|
||||
if (loadConfigLines.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
violations.push(
|
||||
`${relPath}:${loadConfigLines.join(",")} has ${loadConfigLines.length} ambient loadConfig() calls. Pass cfg through the call path, use context.getRuntimeConfig(), or use getRuntimeConfig() at a process boundary.`,
|
||||
);
|
||||
}
|
||||
|
||||
expect(violations).toEqual([]);
|
||||
it("keeps production code off deprecated config load/write seams", () => {
|
||||
expect(collectDeprecatedInternalConfigApiViolations()).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
const getRuntimeConfigMock = vi.fn();
|
||||
const mutateConfigFileMock = vi.fn();
|
||||
const replaceConfigFileMock = vi.fn();
|
||||
const logWarnMock = vi.fn();
|
||||
|
||||
vi.mock("../../config/config.js", () => ({
|
||||
getRuntimeConfig: () => getRuntimeConfigMock(),
|
||||
@@ -14,6 +15,10 @@ vi.mock("../../config/mutate.js", () => ({
|
||||
replaceConfigFile: (...args: unknown[]) => replaceConfigFileMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../../logger.js", () => ({
|
||||
logWarn: (...args: unknown[]) => logWarnMock(...args),
|
||||
}));
|
||||
|
||||
const { createRuntimeConfig } = await import("./runtime-config.js");
|
||||
|
||||
describe("createRuntimeConfig", () => {
|
||||
@@ -21,6 +26,7 @@ describe("createRuntimeConfig", () => {
|
||||
getRuntimeConfigMock.mockReset();
|
||||
mutateConfigFileMock.mockReset();
|
||||
replaceConfigFileMock.mockReset();
|
||||
logWarnMock.mockClear();
|
||||
getRuntimeConfigMock.mockReturnValue({ plugins: {} });
|
||||
mutateConfigFileMock.mockResolvedValue({ previousHash: null, nextHash: "next" });
|
||||
replaceConfigFileMock.mockResolvedValue({ previousHash: null, nextHash: "next" });
|
||||
@@ -34,6 +40,9 @@ describe("createRuntimeConfig", () => {
|
||||
expect(configApi.current()).toBe(runtimeConfig);
|
||||
expect(configApi.loadConfig()).toBe(runtimeConfig);
|
||||
expect(getRuntimeConfigMock).toHaveBeenCalledTimes(2);
|
||||
expect(logWarnMock).toHaveBeenCalledWith(
|
||||
"plugin runtime config.loadConfig() is deprecated; use config.current().",
|
||||
);
|
||||
});
|
||||
|
||||
it("routes deprecated writeConfigFile through replaceConfigFile with afterWrite", async () => {
|
||||
@@ -42,6 +51,9 @@ describe("createRuntimeConfig", () => {
|
||||
|
||||
await configApi.writeConfigFile(nextConfig);
|
||||
|
||||
expect(logWarnMock).toHaveBeenCalledWith(
|
||||
"plugin runtime config.writeConfigFile() is deprecated; use config.mutateConfigFile(...) or config.replaceConfigFile(...).",
|
||||
);
|
||||
expect(replaceConfigFileMock).toHaveBeenCalledWith({
|
||||
nextConfig,
|
||||
afterWrite: { mode: "auto" },
|
||||
|
||||
@@ -3,8 +3,22 @@ import {
|
||||
mutateConfigFile as mutateConfigFileInternal,
|
||||
replaceConfigFile as replaceConfigFileInternal,
|
||||
} from "../../config/mutate.js";
|
||||
import { logWarn } from "../../logger.js";
|
||||
import type { PluginRuntime } from "./types.js";
|
||||
|
||||
const warnedDeprecatedConfigApis = new Set<string>();
|
||||
|
||||
function warnDeprecatedConfigApiOnce(
|
||||
name: "loadConfig" | "writeConfigFile",
|
||||
replacement: string,
|
||||
): void {
|
||||
if (warnedDeprecatedConfigApis.has(name)) {
|
||||
return;
|
||||
}
|
||||
warnedDeprecatedConfigApis.add(name);
|
||||
logWarn(`plugin runtime config.${name}() is deprecated; use ${replacement}.`);
|
||||
}
|
||||
|
||||
export function createRuntimeConfig(): PluginRuntime["config"] {
|
||||
return {
|
||||
current: getRuntimeConfig,
|
||||
@@ -18,8 +32,15 @@ export function createRuntimeConfig(): PluginRuntime["config"] {
|
||||
...params,
|
||||
writeOptions: params.writeOptions,
|
||||
}),
|
||||
loadConfig: getRuntimeConfig,
|
||||
loadConfig: () => {
|
||||
warnDeprecatedConfigApiOnce("loadConfig", "config.current()");
|
||||
return getRuntimeConfig();
|
||||
},
|
||||
writeConfigFile: async (cfg, options) => {
|
||||
warnDeprecatedConfigApiOnce(
|
||||
"writeConfigFile",
|
||||
"config.mutateConfigFile(...) or config.replaceConfigFile(...)",
|
||||
);
|
||||
await replaceConfigFileInternal({
|
||||
nextConfig: cfg,
|
||||
afterWrite: options?.afterWrite ?? { mode: "auto" },
|
||||
|
||||
@@ -63,6 +63,12 @@ function createTaskFlowSessionMock() {
|
||||
};
|
||||
}
|
||||
|
||||
function createDeprecatedRuntimeConfigError(name: "loadConfig" | "writeConfigFile"): Error {
|
||||
return new Error(
|
||||
`Plugin runtime config.${name}() is deprecated in tests; pass cfg/current() or use mutateConfigFile()/replaceConfigFile().`,
|
||||
);
|
||||
}
|
||||
|
||||
export function createPluginRuntimeMock(overrides: DeepPartial<PluginRuntime> = {}): PluginRuntime {
|
||||
const taskFlow = {
|
||||
bindSession: vi.fn(
|
||||
@@ -93,8 +99,12 @@ export function createPluginRuntimeMock(overrides: DeepPartial<PluginRuntime> =
|
||||
afterWrite: { mode: "auto" },
|
||||
followUp: { mode: "auto", requiresRestart: false },
|
||||
})) as unknown as PluginRuntime["config"]["replaceConfigFile"],
|
||||
loadConfig: vi.fn(() => ({})) as unknown as PluginRuntime["config"]["loadConfig"],
|
||||
writeConfigFile: vi.fn() as unknown as PluginRuntime["config"]["writeConfigFile"],
|
||||
loadConfig: vi.fn(() => {
|
||||
throw createDeprecatedRuntimeConfigError("loadConfig");
|
||||
}) as unknown as PluginRuntime["config"]["loadConfig"],
|
||||
writeConfigFile: vi.fn(async () => {
|
||||
throw createDeprecatedRuntimeConfigError("writeConfigFile");
|
||||
}) as unknown as PluginRuntime["config"]["writeConfigFile"],
|
||||
},
|
||||
agent: {
|
||||
defaults: {
|
||||
|
||||
Reference in New Issue
Block a user