mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
fix(doctor): honor explicit plugin-only migration authority (#118700)
This commit is contained in:
committed by
GitHub
parent
a1a73cf421
commit
395c772ba7
@@ -731,7 +731,7 @@ describe("runDoctorConfigPreflight state migration", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("retains the prepared core-state fact after runtime files appear", async () => {
|
||||
it("retains the prepared core-state fact and explicit Doctor repair authority", async () => {
|
||||
needsStartupMigrationCheckpoint.mockReturnValue(true);
|
||||
|
||||
await runDoctorConfigPreflight({
|
||||
@@ -739,10 +739,15 @@ describe("runDoctorConfigPreflight state migration", () => {
|
||||
invalidConfigNote: false,
|
||||
requireStartupMigrationCheckpoint: true,
|
||||
skipPristineCoreStateMigrations: true,
|
||||
doctorOnlyStateMigrations: true,
|
||||
});
|
||||
|
||||
expect(autoMigrateLegacyState).not.toHaveBeenCalled();
|
||||
expect(autoMigrateLegacyPluginDoctorState).toHaveBeenCalledOnce();
|
||||
expect(autoMigrateLegacyPluginDoctorState).toHaveBeenCalledWith({
|
||||
config: { gateway: { mode: "local", port: 19091 } },
|
||||
env: process.env,
|
||||
doctorOnlyStateMigrations: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("blocks gateway readiness when startup migrations leave warnings", async () => {
|
||||
@@ -1003,7 +1008,7 @@ describe("runDoctorConfigPreflight state migration", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps plugin state migrations for partially valid legacy config repairs", async () => {
|
||||
it("keeps explicit Doctor repair authority for partially valid legacy config", async () => {
|
||||
const resolvedConfig = {
|
||||
gateway: { mode: "local", port: "not-a-port" },
|
||||
memory: {
|
||||
@@ -1040,6 +1045,7 @@ describe("runDoctorConfigPreflight state migration", () => {
|
||||
await runDoctorConfigPreflight({
|
||||
migrateLegacyConfig: false,
|
||||
invalidConfigNote: false,
|
||||
doctorOnlyStateMigrations: true,
|
||||
});
|
||||
|
||||
expect(repairLegacyCronStoreWithoutPrompt).not.toHaveBeenCalled();
|
||||
@@ -1047,6 +1053,7 @@ describe("runDoctorConfigPreflight state migration", () => {
|
||||
expect(autoMigrateLegacyPluginDoctorState).toHaveBeenCalledWith({
|
||||
config: resolvedConfig,
|
||||
env: process.env,
|
||||
doctorOnlyStateMigrations: true,
|
||||
});
|
||||
expect(autoMigrateLegacyTaskStateSidecars).toHaveBeenCalledWith({ env: process.env });
|
||||
expect(note).toHaveBeenCalledWith("- plugin-imported", "Doctor changes");
|
||||
|
||||
@@ -388,6 +388,9 @@ export async function runDoctorConfigPreflight(
|
||||
autoMigrateLegacyPluginDoctorState({
|
||||
config: pluginDoctorOnlyConfig,
|
||||
env: process.env,
|
||||
...(options.doctorOnlyStateMigrations === true
|
||||
? { doctorOnlyStateMigrations: true }
|
||||
: {}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
@@ -432,6 +435,9 @@ export async function runDoctorConfigPreflight(
|
||||
autoMigrateLegacyPluginDoctorState({
|
||||
config: pluginDoctorConfig,
|
||||
env: process.env,
|
||||
...(options.doctorOnlyStateMigrations === true
|
||||
? { doctorOnlyStateMigrations: true }
|
||||
: {}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -900,6 +900,7 @@ export async function autoMigrateLegacyPluginDoctorState(params: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
homedir?: () => string;
|
||||
log?: MigrationLogger;
|
||||
doctorOnlyStateMigrations?: boolean;
|
||||
}): Promise<{
|
||||
migrated: boolean;
|
||||
skipped: boolean;
|
||||
@@ -935,6 +936,7 @@ export async function autoMigrateLegacyPluginDoctorState(params: {
|
||||
env,
|
||||
stateDir,
|
||||
oauthDir,
|
||||
includeDoctorOnly: params.doctorOnlyStateMigrations === true,
|
||||
warnings,
|
||||
});
|
||||
const migrated = await migratePluginDoctorStatePlans({
|
||||
|
||||
@@ -6,9 +6,13 @@ import { DatabaseSync } from "node:sqlite";
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { readAcpSessionMetaForEntry } from "../acp/runtime/session-meta.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { readMemoryHostEventRecords } from "../memory-host-sdk/events.js";
|
||||
import { loadNodeHostConfig } from "../node-host/config.js";
|
||||
import { readChannelPairingStateSnapshot } from "../pairing/pairing-store-sqlite.test-helpers.js";
|
||||
import type { PluginDoctorStateMigrationContext } from "../plugins/doctor-contract-registry.js";
|
||||
import type {
|
||||
PluginDoctorStateMigration,
|
||||
PluginDoctorStateMigrationContext,
|
||||
} from "../plugins/doctor-contract-registry.js";
|
||||
import type { DB as OpenClawStateKyselyDatabase } from "../state/openclaw-state-db.generated.js";
|
||||
import {
|
||||
closeOpenClawStateDatabaseForTest,
|
||||
@@ -883,6 +887,88 @@ describe("state migrations", () => {
|
||||
expect(migrateLegacyState).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("restores retained Memory Core host events only for explicit plugin-only Doctor repair", async () => {
|
||||
const root = await fs.realpath(await createTempDir());
|
||||
const stateDir = path.join(root, ".openclaw");
|
||||
const workspaceDir = path.join(root, "workspace");
|
||||
const eventPath = path.join(workspaceDir, "memory", ".dreams", "events.jsonl");
|
||||
const env = createEnv(stateDir);
|
||||
const cfg = {
|
||||
agents: { list: [{ id: "main", default: true, workspace: workspaceDir }] },
|
||||
} as OpenClawConfig;
|
||||
const event = {
|
||||
type: "memory.recall.recorded",
|
||||
timestamp: "2026-07-01T00:00:00.000Z",
|
||||
query: "retained before upgrade",
|
||||
resultCount: 0,
|
||||
results: [],
|
||||
} as const;
|
||||
await fs.mkdir(path.dirname(eventPath), { recursive: true });
|
||||
await fs.writeFile(eventPath, `${JSON.stringify(event)}\n`, "utf8");
|
||||
|
||||
const { stateMigrations } = (await import(
|
||||
/* @vite-ignore */ new URL(
|
||||
"../../extensions/memory-core/doctor-contract-api.ts",
|
||||
import.meta.url,
|
||||
).href
|
||||
)) as { stateMigrations: PluginDoctorStateMigration[] };
|
||||
const migration = stateMigrations.find(
|
||||
(candidate) => candidate.id === "memory-core-host-events-jsonl-to-sqlite",
|
||||
);
|
||||
expect(migration).toBeDefined();
|
||||
if (!migration) {
|
||||
throw new Error("Expected the bundled Memory Core host-event Doctor migration");
|
||||
}
|
||||
pluginDoctorStateMigrationEntries.entries = [
|
||||
{
|
||||
pluginId: "memory-core",
|
||||
migration: {
|
||||
id: migration.id,
|
||||
label: migration.label,
|
||||
doctorOnly: migration.doctorOnly,
|
||||
detectLegacyState: (params) =>
|
||||
migration.detectLegacyState({
|
||||
...params,
|
||||
context: params.context as PluginDoctorStateMigrationContext,
|
||||
}),
|
||||
migrateLegacyState: async (params) => {
|
||||
const result = await migration.migrateLegacyState({
|
||||
...params,
|
||||
context: params.context as PluginDoctorStateMigrationContext,
|
||||
});
|
||||
return { changes: result.changes, warnings: result.warnings };
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const automatic = await autoMigrateLegacyPluginDoctorState({
|
||||
config: cfg,
|
||||
env,
|
||||
homedir: () => root,
|
||||
});
|
||||
expect(automatic.warnings).toEqual([]);
|
||||
expect(automatic.changes).not.toContain(
|
||||
"Migrated Memory Core host events -> SQLite plugin state (1 new row(s))",
|
||||
);
|
||||
await expect(readMemoryHostEventRecords({ workspaceDir, env })).resolves.toEqual([]);
|
||||
await expect(fs.stat(eventPath)).resolves.toBeDefined();
|
||||
|
||||
const repaired = await autoMigrateLegacyPluginDoctorState({
|
||||
config: cfg,
|
||||
env,
|
||||
homedir: () => root,
|
||||
doctorOnlyStateMigrations: true,
|
||||
});
|
||||
expect(repaired.warnings).toEqual([]);
|
||||
expect(repaired.changes).toContain(
|
||||
"Migrated Memory Core host events -> SQLite plugin state (1 new row(s))",
|
||||
);
|
||||
await expect(readMemoryHostEventRecords({ workspaceDir, env })).resolves.toEqual([event]);
|
||||
await expectMissingPath(eventPath);
|
||||
await expect(fs.stat(`${eventPath}.migrated`)).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
it("runs doctor-only repairs after the automatic migration check", async () => {
|
||||
const root = await createTempDir();
|
||||
const stateDir = path.join(root, ".openclaw");
|
||||
|
||||
Reference in New Issue
Block a user