mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-24 11:25:50 -06:00
Merge remote-tracking branch 'origin/main' into fix/inline-slash-skills
This commit is contained in:
@@ -514,6 +514,30 @@ describe("config mutate helpers", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it.runIf(process.platform !== "win32")(
|
||||
"diagnoses config lock failures through a symlinked config directory",
|
||||
async () => {
|
||||
const root = await suiteRootTracker.make("lock-permission-symlink");
|
||||
const realConfigDir = path.join(root, "real");
|
||||
const configuredDir = path.join(root, "configured");
|
||||
await fs.mkdir(realConfigDir);
|
||||
await fs.symlink(realConfigDir, configuredDir);
|
||||
const configPath = path.join(configuredDir, "openclaw.json");
|
||||
const lockPath = path.join(realConfigDir, "openclaw.json.lock");
|
||||
const failure = Object.assign(new Error(`EACCES: permission denied, open '${lockPath}'`), {
|
||||
code: "EACCES",
|
||||
path: lockPath,
|
||||
});
|
||||
fileLockMocks.withFileLock.mockRejectedValueOnce(failure);
|
||||
const snapshot = createSnapshot({ hash: "hash-1", path: configPath, sourceConfig: {} });
|
||||
|
||||
await expect(replaceConfigFile({ snapshot, nextConfig: {} })).rejects.toMatchObject({
|
||||
message: `OpenClaw cannot write to the config directory ${configuredDir}. Fix its ownership or permissions, then try again. Underlying error: ${failure.message}`,
|
||||
cause: failure,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("preserves a permission failure raised outside the config directory", async () => {
|
||||
const configDir = await suiteRootTracker.make("lock-unrelated-permission");
|
||||
const configPath = path.join(configDir, "openclaw.json");
|
||||
|
||||
+15
-4
@@ -225,10 +225,10 @@ async function withConfigMutationLock<T>(
|
||||
async () => await withFileLock(configPath, CONFIG_MUTATION_LOCK_OPTIONS, fn),
|
||||
),
|
||||
)
|
||||
.catch((error: unknown) => {
|
||||
.catch(async (error: unknown) => {
|
||||
// Only relabel a permission failure on the config directory itself. The caller's mutation
|
||||
// runs inside this scope, so an unrelated EACCES from its own work must not be misdiagnosed.
|
||||
if (!isPermissionErrorInDirectory(error, configDir)) {
|
||||
if (!(await isPermissionErrorInDirectory(error, configDir))) {
|
||||
throw error;
|
||||
}
|
||||
throw new Error(
|
||||
@@ -238,7 +238,7 @@ async function withConfigMutationLock<T>(
|
||||
});
|
||||
}
|
||||
|
||||
function isPermissionErrorInDirectory(error: unknown, directory: string): boolean {
|
||||
async function isPermissionErrorInDirectory(error: unknown, directory: string): Promise<boolean> {
|
||||
if (
|
||||
!isErrno(error) ||
|
||||
(error.code !== "EACCES" && error.code !== "EPERM" && error.code !== "EROFS")
|
||||
@@ -246,7 +246,18 @@ function isPermissionErrorInDirectory(error: unknown, directory: string): boolea
|
||||
return false;
|
||||
}
|
||||
const failedPath = error.path;
|
||||
return typeof failedPath === "string" && path.dirname(path.resolve(failedPath)) === directory;
|
||||
if (typeof failedPath !== "string") {
|
||||
return false;
|
||||
}
|
||||
const failedDir = path.dirname(path.resolve(failedPath));
|
||||
if (failedDir === directory) {
|
||||
return true;
|
||||
}
|
||||
// Node reports the canonical path, so a config directory reached through a symlink (a macOS
|
||||
// /var -> /private/var home, for one) never matches the raw string. Resolve only on mismatch to
|
||||
// keep the successful write path free of an extra syscall.
|
||||
const canonicalDirectory = await fs.realpath(directory).catch(() => undefined);
|
||||
return canonicalDirectory !== undefined && failedDir === canonicalDirectory;
|
||||
}
|
||||
|
||||
function markActiveConfigMutationPath(configPath: string): void {
|
||||
|
||||
Reference in New Issue
Block a user