mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
fix(wizard): treat not-directory paths as missing in migration snapshots (#109161)
* fix(wizard): treat not-directory paths as missing in migration snapshots The setup migration snapshot and recovery paths checked for ENOENT only when catching fs errors. ENOTDIR (returned when a path component that should be a directory is a file) was not recognized, so a not-directory migration root made fs.readdir throw instead of resolving to no recovery record, and interrupted the onboarding migration snapshot hash. Migrate both local isMissingPathError helpers to the shared isNotFoundPathError guard (matches ENOENT and ENOTDIR), following the same migration applied to plugins in #107691. * test(wizard): cover ENOTDIR migration snapshots --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -433,6 +433,15 @@ describe("setup migration recovery", () => {
|
||||
await expect(buildSetupMigrationPlanSourceSnapshot(buildPlan(root))).resolves.not.toBe(initial);
|
||||
});
|
||||
|
||||
it("treats source paths beneath a non-directory as missing", async () => {
|
||||
const root = await makeTempRoot();
|
||||
const plan = buildPlan(root);
|
||||
const missingSnapshot = await buildSetupMigrationPlanSourceSnapshot(plan);
|
||||
await fs.writeFile(path.join(root, "hermes"), "not a directory");
|
||||
|
||||
await expect(buildSetupMigrationPlanSourceSnapshot(plan)).resolves.toBe(missingSnapshot);
|
||||
});
|
||||
|
||||
it("snapshots meaningful target changes but ignores migration reports", async () => {
|
||||
const stateDir = await makeTempRoot();
|
||||
const workspaceDir = path.join(stateDir, "workspace");
|
||||
@@ -491,6 +500,21 @@ describe("setup migration recovery", () => {
|
||||
).resolves.not.toBe(workspaceChanged);
|
||||
});
|
||||
|
||||
it("treats target paths beneath a non-directory as missing", async () => {
|
||||
const stateDir = await makeTempRoot();
|
||||
const workspaceDir = path.join(stateDir, "workspace");
|
||||
const missingSnapshot = await buildSetupMigrationTargetSnapshot({
|
||||
config: {},
|
||||
stateDir,
|
||||
workspaceDir,
|
||||
});
|
||||
await fs.writeFile(workspaceDir, "not a directory");
|
||||
|
||||
await expect(
|
||||
buildSetupMigrationTargetSnapshot({ config: {}, stateDir, workspaceDir }),
|
||||
).resolves.toBe(missingSnapshot);
|
||||
});
|
||||
|
||||
it("fails closed when the newest recovery record is malformed", async () => {
|
||||
const stateDir = await makeTempRoot();
|
||||
const reportDir = path.join(stateDir, "migration", "hermes", "2026-07-13T10-00-00Z");
|
||||
@@ -506,4 +530,21 @@ describe("setup migration recovery", () => {
|
||||
}),
|
||||
).rejects.toThrow("Invalid onboarding migration recovery record");
|
||||
});
|
||||
|
||||
it("treats a not-directory migration root as no recovery record", async () => {
|
||||
// A file at the provider report path makes readdir return ENOTDIR; recovery
|
||||
// treats that unavailable child path like a missing report directory.
|
||||
const stateDir = await makeTempRoot();
|
||||
await fs.mkdir(path.join(stateDir, "migration"), { recursive: true });
|
||||
await fs.writeFile(path.join(stateDir, "migration", "hermes"), "not a directory");
|
||||
|
||||
await expect(
|
||||
resolveSetupMigrationRecovery({
|
||||
stateDir,
|
||||
providerId: "hermes",
|
||||
workspaceDir: path.join(stateDir, "workspace"),
|
||||
targetSnapshotHash: BEFORE_HASH,
|
||||
}),
|
||||
).resolves.toEqual({ kind: "none" });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import crypto from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { readDurableJsonFile, writeJsonAtomic } from "../infra/json-files.js";
|
||||
import { isNotFoundPathError } from "../infra/path-guards.js";
|
||||
import { summarizeMigrationItems } from "../plugin-sdk/migration.js";
|
||||
import type { MigrationApplyResult, MigrationItem, MigrationPlan } from "../plugins/types.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
@@ -153,10 +154,6 @@ function isSetupMigrationAttempt(value: unknown): value is SetupMigrationAttempt
|
||||
);
|
||||
}
|
||||
|
||||
function isMissingPathError(error: unknown): boolean {
|
||||
return (error as NodeJS.ErrnoException | undefined)?.code === "ENOENT";
|
||||
}
|
||||
|
||||
export function createSetupMigrationAttempt(
|
||||
params: SetupMigrationIdentity & {
|
||||
plan: MigrationPlan;
|
||||
@@ -285,7 +282,7 @@ async function findLatestSetupMigrationAttempt(params: {
|
||||
try {
|
||||
entries = await fs.readdir(providerReportRoot, { withFileTypes: true });
|
||||
} catch (error) {
|
||||
if (isMissingPathError(error)) {
|
||||
if (isNotFoundPathError(error)) {
|
||||
return undefined;
|
||||
}
|
||||
throw error;
|
||||
|
||||
@@ -5,6 +5,7 @@ import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { withFileLock } from "../infra/file-lock.js";
|
||||
import { isNotFoundPathError } from "../infra/path-guards.js";
|
||||
import type { MigrationPlan } from "../plugins/types.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
|
||||
@@ -25,10 +26,6 @@ const MEANINGFUL_WORKSPACE_ENTRIES = [
|
||||
] as const;
|
||||
const MEANINGFUL_STATE_ENTRIES = ["credentials", "sessions", "agents"] as const;
|
||||
|
||||
function isMissingPathError(error: unknown): boolean {
|
||||
return (error as NodeJS.ErrnoException | undefined)?.code === "ENOENT";
|
||||
}
|
||||
|
||||
function canonicalizeJsonValue(value: unknown): unknown {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(canonicalizeJsonValue);
|
||||
@@ -149,7 +146,7 @@ async function hashTargetPath(
|
||||
try {
|
||||
stat = await fs.lstat(candidate);
|
||||
} catch (error) {
|
||||
if (isMissingPathError(error)) {
|
||||
if (isNotFoundPathError(error)) {
|
||||
hash.update(`missing:${snapshotPath}\0`);
|
||||
return;
|
||||
}
|
||||
@@ -187,7 +184,7 @@ async function hashSourcePath(
|
||||
try {
|
||||
stat = await fs.lstat(candidate);
|
||||
} catch (error) {
|
||||
if (isMissingPathError(error)) {
|
||||
if (isNotFoundPathError(error)) {
|
||||
hash.update(`missing:${snapshotPath}\0`);
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user