mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-23 19:08:22 -06:00
fix(doctor): keep migrated agents on their existing workspace (#123808)
Co-authored-by: scotthuang <scotthuang@tencent.com>
This commit is contained in:
@@ -238,7 +238,11 @@ export async function loadAndMaybeMigrateDoctorConfig(params: {
|
||||
const includeOwnsRoster = configIncludeOwnsAgentRoster(snapshot);
|
||||
if (snapshot.exists && rosterMigrationNeeded && !includeOwnsRoster) {
|
||||
// Runtime roster normalization is read-only; doctor --fix owns persistence.
|
||||
const migrated = migratePersistedImplicitMainRoster(state.candidate).config as OpenClawConfig;
|
||||
// Persist the legacy owner's workspace in doctor's canonical candidate. The writer may run
|
||||
// again after health repairs, when the retired owner marker is no longer available to recover it.
|
||||
const migrated = migratePersistedImplicitMainRoster(state.candidate, {
|
||||
materializeWorkspace: true,
|
||||
}).config as OpenClawConfig;
|
||||
const migratedRoster = readAgentRosterProperty(migrated);
|
||||
const migratedEntries = migratedRoster?.kind === "entries" ? migratedRoster.value : undefined;
|
||||
const { list: _legacyList, ...candidateAgents } = migrated.agents ?? {};
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { readConfigFileSnapshot } from "../config/config.js";
|
||||
import { withEnvOverride, withTempHome, writeOpenClawConfig } from "../config/test-helpers.js";
|
||||
import {
|
||||
runInitialConfigWriteHealth,
|
||||
runWriteConfigHealth,
|
||||
} from "../flows/doctor-health-contribution-runners.config.js";
|
||||
import type { DoctorHealthFlowContext } from "../flows/doctor-health-contribution-types.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js";
|
||||
import { loadAndMaybeMigrateDoctorConfig } from "./doctor-config-flow.js";
|
||||
import { createDoctorPrompter, type DoctorOptions } from "./doctor-prompter.js";
|
||||
|
||||
describe("Doctor workspace persistence", () => {
|
||||
afterEach(() => {
|
||||
closeOpenClawStateDatabaseForTest();
|
||||
});
|
||||
|
||||
it("keeps the legacy owner on the shared workspace across later health writes", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
await withEnvOverride({ OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1" }, async () => {
|
||||
const workspace = path.join(home, "shared-workspace");
|
||||
const configPath = await writeOpenClawConfig(home, {
|
||||
agents: {
|
||||
defaults: { workspace },
|
||||
entries: {
|
||||
main: { default: true },
|
||||
cursor: { workspace },
|
||||
},
|
||||
},
|
||||
gateway: { mode: "local" },
|
||||
});
|
||||
const runtime: RuntimeEnv = {
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
log: vi.fn(),
|
||||
};
|
||||
const options: DoctorOptions = { nonInteractive: true, repair: true };
|
||||
const prompter = createDoctorPrompter({ runtime, options });
|
||||
const configResult = await loadAndMaybeMigrateDoctorConfig({
|
||||
options,
|
||||
confirm: (params) => prompter.confirm(params),
|
||||
runtime,
|
||||
prompter,
|
||||
});
|
||||
const ctx: DoctorHealthFlowContext = {
|
||||
runtime,
|
||||
options,
|
||||
prompter,
|
||||
configResult,
|
||||
cfg: configResult.cfg,
|
||||
cfgForPersistence: structuredClone(configResult.cfg),
|
||||
sourceConfigValid: configResult.sourceConfigValid ?? true,
|
||||
configPath,
|
||||
stateDirExistedAtStart: true,
|
||||
...(configResult.runWithPluginMetadataSnapshot
|
||||
? { runWithPluginMetadataSnapshot: configResult.runWithPluginMetadataSnapshot }
|
||||
: {}),
|
||||
...(configResult.invalidatePluginMetadataSnapshot
|
||||
? { invalidatePluginMetadataSnapshot: configResult.invalidatePluginMetadataSnapshot }
|
||||
: {}),
|
||||
};
|
||||
|
||||
await runInitialConfigWriteHealth(ctx);
|
||||
expect((await readConfigFileSnapshot()).config.agents?.entries?.main?.workspace).toBe(
|
||||
workspace,
|
||||
);
|
||||
|
||||
ctx.cfg = {
|
||||
...ctx.cfg,
|
||||
gateway: { ...ctx.cfg.gateway, bind: "lan" },
|
||||
};
|
||||
await runWriteConfigHealth(ctx);
|
||||
|
||||
const snapshot = await readConfigFileSnapshot();
|
||||
expect(snapshot.valid).toBe(true);
|
||||
expect(snapshot.config.agents?.ownership).toBe("explicit");
|
||||
expect(snapshot.config.agents?.entries?.main?.workspace).toBe(workspace);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -15,7 +15,10 @@ type MigrationResult = {
|
||||
retainedLegacyDefaultAgentId?: string;
|
||||
};
|
||||
|
||||
export function migratePersistedImplicitMainRoster(raw: unknown): MigrationResult {
|
||||
export function migratePersistedImplicitMainRoster(
|
||||
raw: unknown,
|
||||
options: { materializeWorkspace?: boolean } = {},
|
||||
): MigrationResult {
|
||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
||||
return { config: raw, changed: false, diagnostics: [] };
|
||||
}
|
||||
@@ -124,6 +127,7 @@ export function migratePersistedImplicitMainRoster(raw: unknown): MigrationResul
|
||||
const materialized = materializeLegacyDefaultAgentRoles(
|
||||
nextRoot as OpenClawConfig,
|
||||
legacyDefaultAgentId,
|
||||
options,
|
||||
);
|
||||
nextRoot = materialized.config as Record<string, unknown>;
|
||||
insertedPaths = materialized.insertedPaths;
|
||||
|
||||
Reference in New Issue
Block a user