mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:10:45 +00:00
refactor(migration): share cached config runtime helper
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
4266568b93a03ce07e7811285ebc67ec90ebf7519ee892bbeb618b4a527f81e9 plugin-sdk-api-baseline.json
|
||||
545c2ea8874ef1332c7710b74291f391231c861ac296afa46ddcc91fc1c2ed1c plugin-sdk-api-baseline.jsonl
|
||||
cb1975fe65fcab0d50f4bf368118e61640d870a13bb8d9a44a9abb0f79f3c729 plugin-sdk-api-baseline.json
|
||||
c8e2ebe7dc13d170b83b96109dd46fc33057e6f4200f981dc5ea9623e73affab plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -151,7 +151,7 @@ Migration sources are plugins. A plugin declares its provider ids in `openclaw.p
|
||||
|
||||
At runtime the plugin calls `api.registerMigrationProvider(...)`. The provider implements `detect`, `plan`, and `apply`. Core owns CLI orchestration, backup policy, prompts, JSON output, and conflict preflight. Core passes the reviewed plan into `apply(ctx, plan)`, and providers may rebuild the plan only when that argument is absent for compatibility.
|
||||
|
||||
Provider plugins can use `openclaw/plugin-sdk/migration` for item construction and summary counts, plus `openclaw/plugin-sdk/migration-runtime` for conflict-aware file copies, archive-only report copies, and migration reports.
|
||||
Provider plugins can use `openclaw/plugin-sdk/migration` for item construction and summary counts, plus `openclaw/plugin-sdk/migration-runtime` for conflict-aware file copies, archive-only report copies, cached config-runtime wrappers, and migration reports.
|
||||
|
||||
## Onboarding integration
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ For the plugin authoring guide, see [Plugin SDK overview](/plugins/sdk-overview)
|
||||
| `plugin-sdk/test-fixtures` | Generic CLI, sandbox, skill, agent-message, system-event, module reload, bundled plugin path, terminal, chunking, auth-token, and typed-case test fixtures |
|
||||
| `plugin-sdk/test-node-mocks` | Focused Node builtin mock helpers for use inside Vitest `vi.mock("node:*")` factories |
|
||||
| `plugin-sdk/migration` | Migration provider item helpers such as `createMigrationItem`, reason constants, item status markers, redaction helpers, and `summarizeMigrationItems` |
|
||||
| `plugin-sdk/migration-runtime` | Runtime migration helpers such as `copyMigrationFileItem` and `writeMigrationReport` |
|
||||
| `plugin-sdk/migration-runtime` | Runtime migration helpers such as `copyMigrationFileItem`, `withCachedMigrationConfigRuntime`, and `writeMigrationReport` |
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Channel subpaths">
|
||||
|
||||
@@ -3,6 +3,7 @@ import { summarizeMigrationItems } from "openclaw/plugin-sdk/migration";
|
||||
import {
|
||||
archiveMigrationItem,
|
||||
copyMigrationFileItem,
|
||||
withCachedMigrationConfigRuntime,
|
||||
writeMigrationReport,
|
||||
} from "openclaw/plugin-sdk/migration-runtime";
|
||||
import type {
|
||||
@@ -16,54 +17,6 @@ import { appendItem } from "./helpers.js";
|
||||
import { buildClaudePlan } from "./plan.js";
|
||||
import { applyGeneratedSkillItem } from "./skills.js";
|
||||
|
||||
function withCachedConfigRuntime(
|
||||
runtime: MigrationProviderContext["runtime"] | undefined,
|
||||
fallbackConfig: MigrationProviderContext["config"],
|
||||
): MigrationProviderContext["runtime"] | undefined {
|
||||
if (!runtime) {
|
||||
return undefined;
|
||||
}
|
||||
const configApi = runtime.config;
|
||||
if (!configApi?.current || !configApi.mutateConfigFile) {
|
||||
return runtime;
|
||||
}
|
||||
let cachedConfig: MigrationProviderContext["config"] | undefined;
|
||||
const current = (): ReturnType<typeof configApi.current> => {
|
||||
cachedConfig ??= structuredClone(
|
||||
(configApi.current() ?? fallbackConfig) as MigrationProviderContext["config"],
|
||||
);
|
||||
return cachedConfig;
|
||||
};
|
||||
return {
|
||||
...runtime,
|
||||
config: {
|
||||
...runtime.config,
|
||||
current,
|
||||
mutateConfigFile: async (params) => {
|
||||
const result = await configApi.mutateConfigFile({
|
||||
...params,
|
||||
mutate: async (draft, context) => {
|
||||
const mutationResult = await params.mutate(draft, context);
|
||||
cachedConfig = structuredClone(draft);
|
||||
return mutationResult;
|
||||
},
|
||||
});
|
||||
cachedConfig = structuredClone(result.nextConfig);
|
||||
return result;
|
||||
},
|
||||
...(configApi.replaceConfigFile
|
||||
? {
|
||||
replaceConfigFile: async (params) => {
|
||||
const result = await configApi.replaceConfigFile(params);
|
||||
cachedConfig = structuredClone(result.nextConfig);
|
||||
return result;
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function applyClaudePlan(params: {
|
||||
ctx: MigrationProviderContext;
|
||||
plan?: MigrationPlan;
|
||||
@@ -71,7 +24,10 @@ export async function applyClaudePlan(params: {
|
||||
}): Promise<MigrationApplyResult> {
|
||||
const plan = params.plan ?? (await buildClaudePlan(params.ctx));
|
||||
const reportDir = params.ctx.reportDir ?? path.join(params.ctx.stateDir, "migration", "claude");
|
||||
const runtime = withCachedConfigRuntime(params.ctx.runtime ?? params.runtime, params.ctx.config);
|
||||
const runtime = withCachedMigrationConfigRuntime(
|
||||
params.ctx.runtime ?? params.runtime,
|
||||
params.ctx.config,
|
||||
);
|
||||
const applyCtx = { ...params.ctx, runtime };
|
||||
const items: MigrationItem[] = [];
|
||||
for (const item of plan.items) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { markMigrationItemSkipped, summarizeMigrationItems } from "openclaw/plug
|
||||
import {
|
||||
archiveMigrationItem,
|
||||
copyMigrationFileItem,
|
||||
withCachedMigrationConfigRuntime,
|
||||
writeMigrationReport,
|
||||
} from "openclaw/plugin-sdk/migration-runtime";
|
||||
import type {
|
||||
@@ -20,54 +21,6 @@ import { resolveTargets } from "./targets.js";
|
||||
|
||||
const HERMES_REASON_BLOCKED_BY_APPLY_CONFLICT = "blocked by earlier apply conflict";
|
||||
|
||||
function withCachedConfigRuntime(
|
||||
runtime: MigrationProviderContext["runtime"] | undefined,
|
||||
fallbackConfig: MigrationProviderContext["config"],
|
||||
): MigrationProviderContext["runtime"] | undefined {
|
||||
if (!runtime) {
|
||||
return undefined;
|
||||
}
|
||||
const configApi = runtime.config;
|
||||
if (!configApi?.current || !configApi.mutateConfigFile) {
|
||||
return runtime;
|
||||
}
|
||||
let cachedConfig: MigrationProviderContext["config"] | undefined;
|
||||
const current = (): ReturnType<typeof configApi.current> => {
|
||||
cachedConfig ??= structuredClone(
|
||||
(configApi.current() ?? fallbackConfig) as MigrationProviderContext["config"],
|
||||
);
|
||||
return cachedConfig;
|
||||
};
|
||||
return {
|
||||
...runtime,
|
||||
config: {
|
||||
...runtime.config,
|
||||
current,
|
||||
mutateConfigFile: async (params) => {
|
||||
const result = await configApi.mutateConfigFile({
|
||||
...params,
|
||||
mutate: async (draft, context) => {
|
||||
const mutationResult = await params.mutate(draft, context);
|
||||
cachedConfig = structuredClone(draft);
|
||||
return mutationResult;
|
||||
},
|
||||
});
|
||||
cachedConfig = structuredClone(result.nextConfig);
|
||||
return result;
|
||||
},
|
||||
...(configApi.replaceConfigFile
|
||||
? {
|
||||
replaceConfigFile: async (params) => {
|
||||
const result = await configApi.replaceConfigFile(params);
|
||||
cachedConfig = structuredClone(result.nextConfig);
|
||||
return result;
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function applyHermesPlan(params: {
|
||||
ctx: MigrationProviderContext;
|
||||
plan?: MigrationPlan;
|
||||
@@ -77,7 +30,10 @@ export async function applyHermesPlan(params: {
|
||||
const reportDir = params.ctx.reportDir ?? path.join(params.ctx.stateDir, "migration", "hermes");
|
||||
const targets = resolveTargets(params.ctx);
|
||||
const items: MigrationItem[] = [];
|
||||
const runtime = withCachedConfigRuntime(params.ctx.runtime ?? params.runtime, params.ctx.config);
|
||||
const runtime = withCachedMigrationConfigRuntime(
|
||||
params.ctx.runtime ?? params.runtime,
|
||||
params.ctx.config,
|
||||
);
|
||||
const applyCtx = { ...params.ctx, runtime };
|
||||
let blockedByApplyConflict = false;
|
||||
for (const item of plan.items) {
|
||||
|
||||
@@ -2,14 +2,99 @@ import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { copyMigrationFileItem, writeMigrationReport } from "./migration-runtime.js";
|
||||
import {
|
||||
copyMigrationFileItem,
|
||||
withCachedMigrationConfigRuntime,
|
||||
writeMigrationReport,
|
||||
} from "./migration-runtime.js";
|
||||
import { createMigrationItem } from "./migration.js";
|
||||
import type { MigrationProviderContext } from "./plugin-entry.js";
|
||||
|
||||
async function writeFile(filePath: string, contents: string): Promise<void> {
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
||||
await fs.writeFile(filePath, contents, "utf8");
|
||||
}
|
||||
|
||||
describe("withCachedMigrationConfigRuntime", () => {
|
||||
it("serves later config mutations from the same cached runtime snapshot", async () => {
|
||||
type Runtime = NonNullable<MigrationProviderContext["runtime"]>;
|
||||
type RuntimeConfig = MigrationProviderContext["config"];
|
||||
type MutateConfigFileParams = Parameters<Runtime["config"]["mutateConfigFile"]>[0];
|
||||
type ReplaceConfigFileParams = Parameters<Runtime["config"]["replaceConfigFile"]>[0];
|
||||
type MutateConfigFileResult = Awaited<ReturnType<Runtime["config"]["mutateConfigFile"]>>;
|
||||
type ReplaceConfigFileResult = Awaited<ReturnType<Runtime["config"]["replaceConfigFile"]>>;
|
||||
|
||||
const fallbackConfig = { agents: { defaults: { model: { primary: "openai/base" } } } };
|
||||
let runtimeConfig: RuntimeConfig = structuredClone(fallbackConfig);
|
||||
const current = vi.fn(() => runtimeConfig);
|
||||
const mutateConfigFile = vi.fn(
|
||||
async (params: MutateConfigFileParams): Promise<MutateConfigFileResult> => {
|
||||
const draft = structuredClone(runtimeConfig) as RuntimeConfig;
|
||||
const result = await params.mutate(draft, {
|
||||
snapshot: {} as never,
|
||||
previousHash: null,
|
||||
});
|
||||
runtimeConfig = structuredClone(draft);
|
||||
return {
|
||||
path: "/tmp/openclaw.json",
|
||||
previousHash: null,
|
||||
snapshot: {} as never,
|
||||
nextConfig: runtimeConfig,
|
||||
afterWrite: { mode: "auto" },
|
||||
followUp: { mode: "auto", requiresRestart: false },
|
||||
result,
|
||||
};
|
||||
},
|
||||
);
|
||||
const replaceConfigFile = vi.fn(
|
||||
async (params: ReplaceConfigFileParams): Promise<ReplaceConfigFileResult> => {
|
||||
runtimeConfig = structuredClone(params.nextConfig);
|
||||
return {
|
||||
path: "/tmp/openclaw.json",
|
||||
previousHash: null,
|
||||
snapshot: {} as never,
|
||||
nextConfig: runtimeConfig,
|
||||
afterWrite: { mode: "auto" },
|
||||
followUp: { mode: "auto", requiresRestart: false },
|
||||
};
|
||||
},
|
||||
);
|
||||
const runtime = {
|
||||
config: {
|
||||
current,
|
||||
mutateConfigFile,
|
||||
replaceConfigFile,
|
||||
},
|
||||
} as unknown as Runtime;
|
||||
|
||||
const wrapped = withCachedMigrationConfigRuntime(runtime, fallbackConfig);
|
||||
expect(wrapped?.config.current()).toEqual(fallbackConfig);
|
||||
runtimeConfig = { agents: { defaults: { model: { primary: "openai/external" } } } };
|
||||
|
||||
await wrapped?.config.mutateConfigFile({
|
||||
base: "runtime",
|
||||
afterWrite: { mode: "auto" },
|
||||
mutate(draft) {
|
||||
draft.agents ??= {};
|
||||
draft.agents.defaults ??= {};
|
||||
draft.agents.defaults.model = { primary: "openai/mutated" };
|
||||
},
|
||||
});
|
||||
expect(wrapped?.config.current()).toEqual({
|
||||
agents: { defaults: { model: { primary: "openai/mutated" } } },
|
||||
});
|
||||
|
||||
await wrapped?.config.replaceConfigFile({
|
||||
nextConfig: { agents: { defaults: { model: { primary: "openai/replaced" } } } },
|
||||
afterWrite: { mode: "auto" },
|
||||
});
|
||||
expect(wrapped?.config.current()).toEqual({
|
||||
agents: { defaults: { model: { primary: "openai/replaced" } } },
|
||||
});
|
||||
expect(current).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("copyMigrationFileItem", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { MigrationApplyResult, MigrationItem } from "../plugins/types.js";
|
||||
import type {
|
||||
MigrationApplyResult,
|
||||
MigrationItem,
|
||||
MigrationProviderContext,
|
||||
} from "../plugins/types.js";
|
||||
import {
|
||||
MIGRATION_REASON_MISSING_SOURCE_OR_TARGET,
|
||||
MIGRATION_REASON_TARGET_EXISTS,
|
||||
@@ -14,6 +18,54 @@ import {
|
||||
|
||||
export type { MigrationApplyResult, MigrationItem } from "../plugins/types.js";
|
||||
|
||||
export function withCachedMigrationConfigRuntime(
|
||||
runtime: MigrationProviderContext["runtime"] | undefined,
|
||||
fallbackConfig: MigrationProviderContext["config"],
|
||||
): MigrationProviderContext["runtime"] | undefined {
|
||||
if (!runtime) {
|
||||
return undefined;
|
||||
}
|
||||
const configApi = runtime.config;
|
||||
if (!configApi?.current || !configApi.mutateConfigFile) {
|
||||
return runtime;
|
||||
}
|
||||
let cachedConfig: MigrationProviderContext["config"] | undefined;
|
||||
const current = (): ReturnType<typeof configApi.current> => {
|
||||
cachedConfig ??= structuredClone(
|
||||
(configApi.current() ?? fallbackConfig) as MigrationProviderContext["config"],
|
||||
);
|
||||
return cachedConfig;
|
||||
};
|
||||
return {
|
||||
...runtime,
|
||||
config: {
|
||||
...runtime.config,
|
||||
current,
|
||||
mutateConfigFile: async (params) => {
|
||||
const result = await configApi.mutateConfigFile({
|
||||
...params,
|
||||
mutate: async (draft, context) => {
|
||||
const mutationResult = await params.mutate(draft, context);
|
||||
cachedConfig = structuredClone(draft);
|
||||
return mutationResult;
|
||||
},
|
||||
});
|
||||
cachedConfig = structuredClone(result.nextConfig);
|
||||
return result;
|
||||
},
|
||||
...(configApi.replaceConfigFile
|
||||
? {
|
||||
replaceConfigFile: async (params) => {
|
||||
const result = await configApi.replaceConfigFile(params);
|
||||
cachedConfig = structuredClone(result.nextConfig);
|
||||
return result;
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function exists(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
|
||||
Reference in New Issue
Block a user