mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-07 22:44:16 +00:00
fix(doctor): suppress repeated legacy state migration warnings (#11709)
* fix(doctor): suppress repeated state migration warning * fix: harden state-dir mirror detection + warnings (#11709) (thanks @gumadeiras) * test: cover mirror hardening edge cases (#11709) (thanks @gumadeiras)
This commit is contained in:
committed by
GitHub
parent
e02d144af9
commit
b75d618080
@@ -21,6 +21,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Memory/QMD: run boot refresh in background by default, add configurable QMD maintenance timeouts, and retry QMD after fallback failures. (#9690, #9705)
|
||||
- Media understanding: recognize `.caf` audio attachments for transcription. (#10982) Thanks @succ985.
|
||||
- State dir: honor `OPENCLAW_STATE_DIR` for default device identity and canvas storage paths. (#4824) Thanks @kossoy.
|
||||
- Doctor/State dir: suppress repeated legacy migration warnings only for valid symlink mirrors, while keeping warnings for empty or invalid legacy trees. (#11709) Thanks @gumadeiras.
|
||||
- Tests: harden flaky hotspots by removing timer sleeps, consolidating onboarding provider-auth coverage, and improving memory test realism. (#11598) Thanks @gumadeiras.
|
||||
|
||||
## 2026.2.6
|
||||
|
||||
@@ -352,4 +352,157 @@ describe("doctor legacy state migrations", () => {
|
||||
expect(result.skipped).toBe(true);
|
||||
expect(result.migrated).toBe(false);
|
||||
});
|
||||
|
||||
it("does not warn when legacy state dir is an already-migrated symlink mirror", async () => {
|
||||
const root = await makeTempRoot();
|
||||
const targetDir = path.join(root, ".openclaw");
|
||||
const legacyDir = path.join(root, ".moltbot");
|
||||
fs.mkdirSync(path.join(targetDir, "sessions"), { recursive: true });
|
||||
fs.mkdirSync(path.join(targetDir, "agent"), { recursive: true });
|
||||
fs.mkdirSync(legacyDir, { recursive: true });
|
||||
|
||||
const dirLinkType = process.platform === "win32" ? "junction" : "dir";
|
||||
fs.symlinkSync(path.join(targetDir, "sessions"), path.join(legacyDir, "sessions"), dirLinkType);
|
||||
fs.symlinkSync(path.join(targetDir, "agent"), path.join(legacyDir, "agent"), dirLinkType);
|
||||
|
||||
const result = await autoMigrateLegacyStateDir({
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
homedir: () => root,
|
||||
});
|
||||
|
||||
expect(result.migrated).toBe(false);
|
||||
expect(result.warnings).toEqual([]);
|
||||
});
|
||||
|
||||
it("warns when legacy state dir is empty and target already exists", async () => {
|
||||
const root = await makeTempRoot();
|
||||
const targetDir = path.join(root, ".openclaw");
|
||||
const legacyDir = path.join(root, ".moltbot");
|
||||
fs.mkdirSync(targetDir, { recursive: true });
|
||||
fs.mkdirSync(legacyDir, { recursive: true });
|
||||
|
||||
const result = await autoMigrateLegacyStateDir({
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
homedir: () => root,
|
||||
});
|
||||
|
||||
expect(result.migrated).toBe(false);
|
||||
expect(result.warnings).toEqual([
|
||||
`State dir migration skipped: target already exists (${targetDir}). Remove or merge manually.`,
|
||||
]);
|
||||
});
|
||||
|
||||
it("warns when legacy state dir contains non-symlink entries and target already exists", async () => {
|
||||
const root = await makeTempRoot();
|
||||
const targetDir = path.join(root, ".openclaw");
|
||||
const legacyDir = path.join(root, ".moltbot");
|
||||
fs.mkdirSync(targetDir, { recursive: true });
|
||||
fs.mkdirSync(legacyDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(legacyDir, "sessions.json"), "{}", "utf-8");
|
||||
|
||||
const result = await autoMigrateLegacyStateDir({
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
homedir: () => root,
|
||||
});
|
||||
|
||||
expect(result.migrated).toBe(false);
|
||||
expect(result.warnings).toEqual([
|
||||
`State dir migration skipped: target already exists (${targetDir}). Remove or merge manually.`,
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not warn when legacy state dir contains nested symlink mirrors", async () => {
|
||||
const root = await makeTempRoot();
|
||||
const targetDir = path.join(root, ".openclaw");
|
||||
const legacyDir = path.join(root, ".moltbot");
|
||||
fs.mkdirSync(path.join(targetDir, "agents", "main"), { recursive: true });
|
||||
fs.mkdirSync(legacyDir, { recursive: true });
|
||||
fs.mkdirSync(path.join(legacyDir, "agents"), { recursive: true });
|
||||
|
||||
const dirLinkType = process.platform === "win32" ? "junction" : "dir";
|
||||
fs.symlinkSync(
|
||||
path.join(targetDir, "agents", "main"),
|
||||
path.join(legacyDir, "agents", "main"),
|
||||
dirLinkType,
|
||||
);
|
||||
|
||||
const result = await autoMigrateLegacyStateDir({
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
homedir: () => root,
|
||||
});
|
||||
|
||||
expect(result.migrated).toBe(false);
|
||||
expect(result.warnings).toEqual([]);
|
||||
});
|
||||
|
||||
it("warns when legacy state dir symlink points outside the target tree", async () => {
|
||||
const root = await makeTempRoot();
|
||||
const targetDir = path.join(root, ".openclaw");
|
||||
const legacyDir = path.join(root, ".moltbot");
|
||||
const outsideDir = path.join(root, ".outside-state");
|
||||
fs.mkdirSync(path.join(targetDir, "sessions"), { recursive: true });
|
||||
fs.mkdirSync(legacyDir, { recursive: true });
|
||||
fs.mkdirSync(outsideDir, { recursive: true });
|
||||
|
||||
const dirLinkType = process.platform === "win32" ? "junction" : "dir";
|
||||
fs.symlinkSync(path.join(outsideDir), path.join(legacyDir, "sessions"), dirLinkType);
|
||||
|
||||
const result = await autoMigrateLegacyStateDir({
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
homedir: () => root,
|
||||
});
|
||||
|
||||
expect(result.migrated).toBe(false);
|
||||
expect(result.warnings).toEqual([
|
||||
`State dir migration skipped: target already exists (${targetDir}). Remove or merge manually.`,
|
||||
]);
|
||||
});
|
||||
|
||||
it("warns when legacy state dir contains a broken symlink target", async () => {
|
||||
const root = await makeTempRoot();
|
||||
const targetDir = path.join(root, ".openclaw");
|
||||
const legacyDir = path.join(root, ".moltbot");
|
||||
fs.mkdirSync(path.join(targetDir, "sessions"), { recursive: true });
|
||||
fs.mkdirSync(legacyDir, { recursive: true });
|
||||
|
||||
const dirLinkType = process.platform === "win32" ? "junction" : "dir";
|
||||
const targetSessionDir = path.join(targetDir, "sessions");
|
||||
fs.symlinkSync(targetSessionDir, path.join(legacyDir, "sessions"), dirLinkType);
|
||||
fs.rmSync(targetSessionDir, { recursive: true, force: true });
|
||||
|
||||
const result = await autoMigrateLegacyStateDir({
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
homedir: () => root,
|
||||
});
|
||||
|
||||
expect(result.migrated).toBe(false);
|
||||
expect(result.warnings).toEqual([
|
||||
`State dir migration skipped: target already exists (${targetDir}). Remove or merge manually.`,
|
||||
]);
|
||||
});
|
||||
|
||||
it("warns when legacy symlink escapes target tree through second-hop symlink", async () => {
|
||||
const root = await makeTempRoot();
|
||||
const targetDir = path.join(root, ".openclaw");
|
||||
const legacyDir = path.join(root, ".moltbot");
|
||||
const outsideDir = path.join(root, ".outside-state");
|
||||
fs.mkdirSync(targetDir, { recursive: true });
|
||||
fs.mkdirSync(legacyDir, { recursive: true });
|
||||
fs.mkdirSync(outsideDir, { recursive: true });
|
||||
|
||||
const dirLinkType = process.platform === "win32" ? "junction" : "dir";
|
||||
const targetHop = path.join(targetDir, "hop");
|
||||
fs.symlinkSync(outsideDir, targetHop, dirLinkType);
|
||||
fs.symlinkSync(targetHop, path.join(legacyDir, "sessions"), dirLinkType);
|
||||
|
||||
const result = await autoMigrateLegacyStateDir({
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
homedir: () => root,
|
||||
});
|
||||
|
||||
expect(result.migrated).toBe(false);
|
||||
expect(result.warnings).toEqual([
|
||||
`State dir migration skipped: target already exists (${targetDir}). Remove or merge manually.`,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -360,6 +360,68 @@ function isDirPath(filePath: string): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
function isWithinDir(targetPath: string, rootDir: string): boolean {
|
||||
const relative = path.relative(path.resolve(rootDir), path.resolve(targetPath));
|
||||
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
||||
}
|
||||
|
||||
function isLegacyTreeSymlinkMirror(currentDir: string, realTargetDir: string): boolean {
|
||||
let entries: fs.Dirent[];
|
||||
try {
|
||||
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
if (entries.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
const entryPath = path.join(currentDir, entry.name);
|
||||
let stat: fs.Stats;
|
||||
try {
|
||||
stat = fs.lstatSync(entryPath);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
if (stat.isSymbolicLink()) {
|
||||
const resolvedTarget = resolveSymlinkTarget(entryPath);
|
||||
if (!resolvedTarget) {
|
||||
return false;
|
||||
}
|
||||
let resolvedRealTarget: string;
|
||||
try {
|
||||
resolvedRealTarget = fs.realpathSync(resolvedTarget);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
if (!isWithinDir(resolvedRealTarget, realTargetDir)) {
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (stat.isDirectory()) {
|
||||
if (!isLegacyTreeSymlinkMirror(entryPath, realTargetDir)) {
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function isLegacyDirSymlinkMirror(legacyDir: string, targetDir: string): boolean {
|
||||
let realTargetDir: string;
|
||||
try {
|
||||
realTargetDir = fs.realpathSync(targetDir);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return isLegacyTreeSymlinkMirror(legacyDir, realTargetDir);
|
||||
}
|
||||
|
||||
export async function autoMigrateLegacyStateDir(params: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
homedir?: () => string;
|
||||
@@ -443,6 +505,9 @@ export async function autoMigrateLegacyStateDir(params: {
|
||||
}
|
||||
|
||||
if (isDirPath(targetDir)) {
|
||||
if (legacyDir && isLegacyDirSymlinkMirror(legacyDir, targetDir)) {
|
||||
return { migrated: false, skipped: false, changes, warnings };
|
||||
}
|
||||
warnings.push(
|
||||
`State dir migration skipped: target already exists (${targetDir}). Remove or merge manually.`,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user