mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:30:44 +00:00
fix(onboard): run noninteractive migration imports
This commit is contained in:
@@ -32,6 +32,7 @@ Docs: https://docs.openclaw.ai
|
||||
- WhatsApp: stage `qrcode` through root mirrored runtime dependencies so packaged QR pairing can render from staged plugin-runtime-deps installs. Fixes #75394. Thanks @FelipeX2001.
|
||||
- Discord/voice: apply per-channel Discord `systemPrompt` overrides to voice transcript turns by forwarding the trusted channel prompt through the voice agent run. Fixes #47095. Thanks @qearlyao.
|
||||
- Gateway/agent: reject strict `openclaw agent --deliver` requests with missing delivery targets before starting the agent run, so users do not wait for a completed turn that cannot send anywhere. Thanks @vincentkoc.
|
||||
- Setup/import: honor non-interactive `--import-from` onboarding flags by running the migration import path instead of silently completing normal setup without importing anything. Thanks @vincentkoc.
|
||||
- Discord/voice: run voice-channel turns under a voice-output policy that hides the agent `tts` tool and asks for spoken reply text, so `/vc join` sessions synthesize and play agent replies instead of ending with `NO_REPLY`. Fixes #61536. Thanks @aounakram.
|
||||
- Plugins/runtime-deps: include packaged OpenClaw identity in bundled plugin loader cache keys, so same-path package upgrades stop reusing stale versioned runtime-deps mirrors. Fixes #75045. Thanks @sahilsatralkar.
|
||||
- Plugin SDK: restore reply-prefix and reply-pipeline helpers on the deprecated root/compat SDK surface so external plugins still using `openclaw/plugin-sdk` do not fail message dispatch after update. Fixes #75171. Thanks @zhangxiliang.
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { MigrationApplyResult, MigrationPlan } from "../plugins/types.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { makeTempWorkspace } from "../test-helpers/workspace.js";
|
||||
import { captureEnv } from "../test-utils/env.js";
|
||||
@@ -15,6 +16,14 @@ type InstallGatewayDaemonResult = Awaited<ReturnType<typeof installGatewayDaemon
|
||||
const installGatewayDaemonNonInteractiveMock = vi.hoisted(() =>
|
||||
vi.fn(async (): Promise<InstallGatewayDaemonResult> => ({ installed: true })),
|
||||
);
|
||||
const createPreMigrationBackupMock = vi.hoisted(() => vi.fn(async () => undefined));
|
||||
const migrationProviderMock = vi.hoisted(() => ({
|
||||
id: "hermes",
|
||||
label: "Hermes",
|
||||
description: "Hermes migration provider",
|
||||
plan: vi.fn(),
|
||||
apply: vi.fn(),
|
||||
}));
|
||||
const healthCommandMock = vi.hoisted(() => vi.fn(async () => {}));
|
||||
const gatewayServiceMock = vi.hoisted(() => ({
|
||||
label: "LaunchAgent",
|
||||
@@ -136,6 +145,20 @@ vi.mock("./post-config-runtime-deps.js", () => ({
|
||||
preparePostConfigBundledRuntimeDeps: preparePostConfigBundledRuntimeDepsMock,
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/migration-provider-runtime.js", () => ({
|
||||
resolvePluginMigrationProviders: () => [migrationProviderMock],
|
||||
resolvePluginMigrationProvider: ({ providerId }: { providerId: string }) =>
|
||||
providerId === migrationProviderMock.id ? migrationProviderMock : undefined,
|
||||
}));
|
||||
|
||||
vi.mock("./migrate/apply.js", async (importActual) => {
|
||||
const actual = await importActual<typeof import("./migrate/apply.js")>();
|
||||
return {
|
||||
...actual,
|
||||
createPreMigrationBackup: createPreMigrationBackupMock,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../daemon/service.js", () => ({
|
||||
resolveGatewayService: () => gatewayServiceMock,
|
||||
}));
|
||||
@@ -316,7 +339,11 @@ describe("onboard (non-interactive): gateway and remote auth", () => {
|
||||
afterEach(() => {
|
||||
waitForGatewayReachableMock = undefined;
|
||||
testConfigStore.clear();
|
||||
ensureWorkspaceAndSessionsMock.mockClear();
|
||||
installGatewayDaemonNonInteractiveMock.mockClear();
|
||||
createPreMigrationBackupMock.mockClear();
|
||||
migrationProviderMock.plan.mockReset();
|
||||
migrationProviderMock.apply.mockReset();
|
||||
healthCommandMock.mockClear();
|
||||
gatewayServiceMock.isLoaded.mockClear();
|
||||
gatewayServiceMock.readRuntime.mockClear();
|
||||
@@ -399,6 +426,85 @@ describe("onboard (non-interactive): gateway and remote auth", () => {
|
||||
});
|
||||
}, 60_000);
|
||||
|
||||
it("applies non-interactive migration imports instead of ignoring import flags", async () => {
|
||||
await withStateDir("state-noninteractive-import-", async (stateDir) => {
|
||||
const source = path.join(stateDir, "hermes-home");
|
||||
const workspace = path.join(stateDir, "openclaw");
|
||||
const planned: MigrationPlan = {
|
||||
providerId: "hermes",
|
||||
source,
|
||||
target: workspace,
|
||||
summary: {
|
||||
total: 1,
|
||||
planned: 1,
|
||||
migrated: 0,
|
||||
skipped: 0,
|
||||
conflicts: 0,
|
||||
errors: 0,
|
||||
sensitive: 0,
|
||||
},
|
||||
items: [
|
||||
{
|
||||
id: "workspace:AGENTS.md",
|
||||
kind: "workspace",
|
||||
action: "copy",
|
||||
status: "planned",
|
||||
source: path.join(source, "AGENTS.md"),
|
||||
target: path.join(workspace, "AGENTS.md"),
|
||||
},
|
||||
],
|
||||
};
|
||||
const applied: MigrationApplyResult = {
|
||||
...planned,
|
||||
summary: {
|
||||
...planned.summary,
|
||||
planned: 0,
|
||||
migrated: 1,
|
||||
},
|
||||
items: planned.items.map((item) => ({ ...item, status: "migrated" as const })),
|
||||
};
|
||||
migrationProviderMock.plan.mockResolvedValueOnce(planned);
|
||||
migrationProviderMock.apply.mockResolvedValueOnce(applied);
|
||||
|
||||
await runNonInteractiveSetup(
|
||||
{
|
||||
nonInteractive: true,
|
||||
mode: "local",
|
||||
workspace,
|
||||
authChoice: "skip",
|
||||
skipHealth: true,
|
||||
importFrom: "hermes",
|
||||
importSource: source,
|
||||
},
|
||||
runtime,
|
||||
);
|
||||
|
||||
expect(migrationProviderMock.plan).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
source,
|
||||
includeSecrets: false,
|
||||
overwrite: false,
|
||||
config: expect.objectContaining({
|
||||
agents: expect.objectContaining({
|
||||
defaults: expect.objectContaining({ workspace }),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(migrationProviderMock.apply).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
source,
|
||||
reportDir: expect.stringContaining(path.join(stateDir, "migration", "hermes")),
|
||||
}),
|
||||
planned,
|
||||
);
|
||||
expect(readTestConfig().agents?.defaults?.workspace).toBe(workspace);
|
||||
expect(ensureWorkspaceAndSessionsMock).not.toHaveBeenCalled();
|
||||
expect(preparePostConfigBundledRuntimeDepsMock).not.toHaveBeenCalled();
|
||||
expect(healthCommandMock).not.toHaveBeenCalled();
|
||||
});
|
||||
}, 60_000);
|
||||
|
||||
it("writes gateway.remote url/token", async () => {
|
||||
await withStateDir("state-remote-", async (_stateDir) => {
|
||||
const port = getPseudoPort(30_000);
|
||||
|
||||
@@ -1,12 +1,94 @@
|
||||
import { formatCliCommand } from "../cli/command-format.js";
|
||||
import { replaceConfigFile } from "../config/config.js";
|
||||
import { readConfigFileSnapshot } from "../config/io.js";
|
||||
import { logConfigUpdated } from "../config/logging.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||
import { runNonInteractiveLocalSetup } from "./onboard-non-interactive/local.js";
|
||||
import { runNonInteractiveRemoteSetup } from "./onboard-non-interactive/remote.js";
|
||||
import type { OnboardOptions } from "./onboard-types.js";
|
||||
|
||||
function createNonInteractiveMigrationPrompter(runtime: RuntimeEnv): WizardPrompter {
|
||||
const unavailable = (message: string): never => {
|
||||
throw new Error(
|
||||
`Non-interactive migration import needs explicit flags before prompting: ${message}`,
|
||||
);
|
||||
};
|
||||
return {
|
||||
async intro(title) {
|
||||
runtime.log(title);
|
||||
},
|
||||
async outro(message) {
|
||||
runtime.log(message);
|
||||
},
|
||||
async note(message, title) {
|
||||
runtime.log(title ? `${title}\n${message}` : message);
|
||||
},
|
||||
async select(params) {
|
||||
unavailable(params.message);
|
||||
},
|
||||
async multiselect(params) {
|
||||
unavailable(params.message);
|
||||
},
|
||||
async text(params) {
|
||||
unavailable(params.message);
|
||||
},
|
||||
async confirm(params) {
|
||||
unavailable(params.message);
|
||||
},
|
||||
progress(label) {
|
||||
runtime.log(label);
|
||||
return {
|
||||
update(message) {
|
||||
runtime.log(message);
|
||||
},
|
||||
stop(message) {
|
||||
if (message) {
|
||||
runtime.log(message);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function runNonInteractiveMigrationImport(params: {
|
||||
opts: OnboardOptions;
|
||||
runtime: RuntimeEnv;
|
||||
baseConfig: OpenClawConfig;
|
||||
baseHash?: string;
|
||||
}) {
|
||||
const providerId = params.opts.importFrom?.trim();
|
||||
if (!providerId) {
|
||||
params.runtime.error("--import-from is required for non-interactive migration import.");
|
||||
params.runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
const { detectSetupMigrationSources, runSetupMigrationImport } =
|
||||
await import("../wizard/setup.migration-import.js");
|
||||
const detections = await detectSetupMigrationSources({
|
||||
config: params.baseConfig,
|
||||
runtime: params.runtime,
|
||||
});
|
||||
await runSetupMigrationImport({
|
||||
opts: { ...params.opts, importFrom: providerId, nonInteractive: true },
|
||||
baseConfig: params.baseConfig,
|
||||
detections,
|
||||
prompter: createNonInteractiveMigrationPrompter(params.runtime),
|
||||
runtime: params.runtime,
|
||||
async commitConfigFile(config) {
|
||||
await replaceConfigFile({
|
||||
nextConfig: config,
|
||||
...(params.baseHash !== undefined ? { baseHash: params.baseHash } : {}),
|
||||
});
|
||||
logConfigUpdated(params.runtime);
|
||||
return config;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function runNonInteractiveSetup(
|
||||
opts: OnboardOptions,
|
||||
runtime: RuntimeEnv = defaultRuntime,
|
||||
@@ -32,6 +114,11 @@ export async function runNonInteractiveSetup(
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.importFrom || opts.importSource || opts.importSecrets || opts.flow === "import") {
|
||||
await runNonInteractiveMigrationImport({ opts, runtime, baseConfig, baseHash: snapshot.hash });
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode === "remote") {
|
||||
await runNonInteractiveRemoteSetup({ opts, runtime, baseConfig, baseHash: snapshot.hash });
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user