fix(files): preserve relative symlinks when duplicating entries (#6771)

* fix(files): preserve relative symlinks when duplicating entries

* test(files): verify duplicated symlink targets through daemon RPC
This commit is contained in:
Elowen
2026-10-13 10:56:43 +02:00
committed by GitHub
parent b923cde6cf
commit 337e81a9c6
2 changed files with 88 additions and 1 deletions
@@ -1,5 +1,13 @@
import { describe, test, expect, beforeEach, afterEach } from "vitest";
import { mkdtempSync, writeFileSync, rmSync, mkdirSync, symlinkSync, readdirSync } from "fs";
import {
mkdtempSync,
writeFileSync,
rmSync,
mkdirSync,
symlinkSync,
readdirSync,
readlinkSync,
} from "fs";
import { tmpdir } from "os";
import path from "path";
import { createDaemonTestContext, type DaemonTestContext } from "../test-utils/index.js";
@@ -387,6 +395,84 @@ describe("file explorer", () => {
}
});
test.skipIf(process.platform === "win32")(
"duplicating a folder keeps relative symlinks inside the copy",
async () => {
const cwd = tmpCwd();
try {
mkdirSync(path.join(cwd, "template"));
writeFileSync(path.join(cwd, "template", "data.txt"), "original payload\n");
symlinkSync("data.txt", path.join(cwd, "template", "alias.txt"));
const duplicated = await ctx.client.duplicateFileEntry({ cwd, path: "template" });
expect(duplicated).toMatchObject({
duplicatedPath: "template copy",
success: true,
error: null,
});
expect(readlinkSync(path.join(cwd, "template copy", "alias.txt"))).toBe("data.txt");
const copiedTarget = await ctx.client.readFile(cwd, "template copy/data.txt");
const targetWrite = await ctx.client.writeFile({
cwd,
path: "template copy/data.txt",
content: "copy payload\n",
expectedModifiedAt: copiedTarget.modifiedAt,
expectedRevision: copiedTarget.revision,
});
expect(targetWrite.status).toBe("written");
const copiedAlias = await ctx.client.readFile(cwd, "template copy/alias.txt");
expect(new TextDecoder().decode(copiedAlias.bytes)).toBe("copy payload\n");
const aliasWrite = await ctx.client.writeFile({
cwd,
path: "template copy/alias.txt",
content: "edited through copied alias\n",
expectedModifiedAt: copiedAlias.modifiedAt,
expectedRevision: copiedAlias.revision,
});
expect(aliasWrite.status).toBe("written");
const original = await ctx.client.readFile(cwd, "template/data.txt");
expect(new TextDecoder().decode(original.bytes)).toBe("original payload\n");
const editedCopy = await ctx.client.readFile(cwd, "template copy/data.txt");
expect(new TextDecoder().decode(editedCopy.bytes)).toBe("edited through copied alias\n");
const deleted = await ctx.client.deleteFileEntry({ cwd, path: "template" });
expect(deleted).toMatchObject({ success: true, error: null });
const survivingAlias = await ctx.client.readFile(cwd, "template copy/alias.txt");
expect(new TextDecoder().decode(survivingAlias.bytes)).toBe(
"edited through copied alias\n",
);
} finally {
rmSync(cwd, { recursive: true, force: true });
}
},
);
test.skipIf(process.platform === "win32")(
"duplicating a file symlink preserves an explicit absolute target",
async () => {
const cwd = tmpCwd();
const targetPath = path.join(cwd, "data.txt");
try {
writeFileSync(targetPath, "absolute target payload\n");
symlinkSync(targetPath, path.join(cwd, "alias.txt"));
const duplicated = await ctx.client.duplicateFileEntry({ cwd, path: "alias.txt" });
expect(duplicated).toMatchObject({
duplicatedPath: "alias copy.txt",
success: true,
error: null,
});
expect(readlinkSync(path.join(cwd, "alias copy.txt"))).toBe(targetPath);
const copy = await ctx.client.readFile(cwd, "alias copy.txt");
expect(new TextDecoder().decode(copy.bytes)).toBe("absolute target payload\n");
} finally {
rmSync(cwd, { recursive: true, force: true });
}
},
);
test("lists directory contents", async () => {
const cwd = tmpCwd();
@@ -669,6 +669,7 @@ export async function duplicateExplorerEntry({
errorOnExist: true,
force: false,
preserveTimestamps: true,
verbatimSymlinks: true,
});
return { status: "ok", path: normalizeRelativePath({ root, targetPath }) };
} catch (error) {