





















@@ -1,4 +1,5 @@
11import * as fs from "node:fs";
2+import fsPromises from "node:fs/promises";
23import os from "node:os";
34import path from "node:path";
45import { afterEach, describe, expect, it, vi } from "vitest";
@@ -7,6 +8,7 @@ import { withMockedPlatform } from "../test-utils/vitest-spies.js";
78import {
89formatControlUiSshHint,
910handleReset,
11+moveToTrash,
1012normalizeGatewayTokenInput,
1113openUrl,
1214probeGatewayReachable,
@@ -17,6 +19,7 @@ import {
1719} from "./onboard-helpers.js";
18201921const mocks = vi.hoisted(() => ({
22+movePathToTrash: vi.fn(async (targetPath: string) => `${targetPath}.trashed`),
2023runCommandWithTimeout: vi.fn<
2124(
2225argv: string[],
@@ -33,6 +36,10 @@ const mocks = vi.hoisted(() => ({
3336probeGateway: vi.fn(),
3437}));
353839+vi.mock("../infra/fs-safe.js", () => ({
40+movePathToTrash: mocks.movePathToTrash,
41+}));
42+3643vi.mock("../process/exec.js", () => ({
3744runCommandWithTimeout: mocks.runCommandWithTimeout,
3845}));
@@ -89,9 +96,13 @@ describe("handleReset", () => {
89969097const runtime = { log: vi.fn() } as unknown as RuntimeEnv;
919892-await handleReset("full", workspaceDir, runtime);
99+try {
100+await handleReset("full", workspaceDir, runtime);
101+} finally {
102+fs.rmSync(homeDir, { recursive: true, force: true });
103+}
9310494-const trashedPaths = mocks.runCommandWithTimeout.mock.calls.map(([argv]) => argv[1]);
105+const trashedPaths = mocks.movePathToTrash.mock.calls.map(([targetPath]) => targetPath);
95106expect(trashedPaths).toEqual([
96107profileConfigPath,
97108profileCredentialsDir,
@@ -102,6 +113,78 @@ describe("handleReset", () => {
102113});
103114});
104115116+describe("moveToTrash", () => {
117+it("uses fs-safe trash instead of resolving a PATH trash command", async () => {
118+const testRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-trash-helper-"));
119+const targetPath = path.join(testRoot, "target");
120+fs.mkdirSync(targetPath, { recursive: true });
121+const runtime = { log: vi.fn() } as unknown as RuntimeEnv;
122+123+try {
124+await moveToTrash(targetPath, runtime);
125+} finally {
126+fs.rmSync(testRoot, { recursive: true, force: true });
127+}
128+129+expect(mocks.movePathToTrash).toHaveBeenCalledWith(targetPath, {
130+allowedRoots: [path.dirname(targetPath)],
131+});
132+expect(mocks.runCommandWithTimeout).not.toHaveBeenCalled();
133+expect(runtime.log).toHaveBeenCalledWith(`Moved to Trash: ${targetPath}`);
134+});
135+136+it("allows fs-safe trash to move a symlink whose target resolves outside the parent", async () => {
137+const testRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-trash-symlink-"));
138+const targetPath = path.join(testRoot, "target-link");
139+const outsideTarget = path.join(os.tmpdir(), "openclaw-trash-symlink-target");
140+fs.writeFileSync(targetPath, "link placeholder");
141+vi.spyOn(fsPromises, "lstat").mockResolvedValue({
142+isSymbolicLink: () => true,
143+} as fs.Stats);
144+vi.spyOn(fsPromises, "realpath").mockImplementation(async (candidate) =>
145+String(candidate) === path.dirname(targetPath) ? path.dirname(targetPath) : outsideTarget,
146+);
147+const runtime = { log: vi.fn() } as unknown as RuntimeEnv;
148+149+try {
150+await moveToTrash(targetPath, runtime);
151+} finally {
152+fs.rmSync(testRoot, { recursive: true, force: true });
153+}
154+155+expect(mocks.movePathToTrash).toHaveBeenCalledWith(targetPath, {
156+allowedRoots: [path.dirname(targetPath), path.dirname(outsideTarget)],
157+});
158+});
159+160+it("canonicalizes a symlinked parent before calling fs-safe trash", async () => {
161+const testRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-trash-parent-link-"));
162+const lexicalParent = path.join(testRoot, "state-link");
163+const realParent = path.join(testRoot, "state-real");
164+const targetPath = path.join(lexicalParent, "openclaw.json");
165+const sourcePath = path.join(realParent, "openclaw.json");
166+fs.mkdirSync(lexicalParent, { recursive: true });
167+fs.writeFileSync(targetPath, "{}\n");
168+vi.spyOn(fsPromises, "realpath").mockImplementation(async (candidate) =>
169+String(candidate) === lexicalParent ? realParent : String(candidate),
170+);
171+vi.spyOn(fsPromises, "lstat").mockResolvedValue({
172+isSymbolicLink: () => false,
173+} as fs.Stats);
174+const runtime = { log: vi.fn() } as unknown as RuntimeEnv;
175+176+try {
177+await moveToTrash(targetPath, runtime);
178+} finally {
179+fs.rmSync(testRoot, { recursive: true, force: true });
180+}
181+182+expect(mocks.movePathToTrash).toHaveBeenCalledWith(sourcePath, {
183+allowedRoots: [realParent],
184+});
185+});
186+});
187+105188describe("openUrl", () => {
106189it("passes OAuth URLs to Windows FileProtocolHandler without cmd parsing", async () => {
107190vi.stubEnv("VITEST", "");
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。