fix(doctor): keep migrated agents on their existing workspace (#123808)

Co-authored-by: scotthuang <scotthuang@tencent.com>
This commit is contained in:
scotthuang
2026-08-15 09:26:45 +08:00
committed by GitHub
parent fd8741b4d8
commit dd67c3bdf0
3 changed files with 93 additions and 2 deletions
+5 -1
View File
@@ -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);
});
});
});
});
+5 -1
View File
@@ -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;