Skip to content

Commit 630f1c4

Browse files
committed
test(ui): cover config open failure feedback (#87108)
1 parent ddd1180 commit 630f1c4

2 files changed

Lines changed: 57 additions & 2 deletions

File tree

ui/src/ui/controllers/config.test.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { describe, expect, it, vi } from "vitest";
1+
import { afterEach, describe, expect, it, vi } from "vitest";
22
import {
33
applyConfigSnapshot,
44
applyConfig,
55
ensureAgentConfigEntry,
66
findAgentConfigEntryIndex,
77
loadConfig,
8+
openConfigFile,
89
resetConfigPendingChanges,
910
runUpdate,
1011
saveConfig,
@@ -64,6 +65,10 @@ function requireRequestCall(request: ReturnType<typeof vi.fn>, index = 0): unkno
6465
return call;
6566
}
6667

68+
afterEach(() => {
69+
vi.unstubAllGlobals();
70+
});
71+
6772
describe("applyConfigSnapshot", () => {
6873
it("does not clobber form edits while dirty", () => {
6974
const state = createState();
@@ -271,6 +276,57 @@ describe("loadConfig", () => {
271276
});
272277
});
273278

279+
describe("openConfigFile", () => {
280+
it("surfaces failed open responses and copies the returned config path", async () => {
281+
const request = vi.fn().mockResolvedValue({
282+
ok: false,
283+
path: "/tmp/openclaw.json",
284+
error: "Cannot open file in headless environment.",
285+
});
286+
const writeText = vi.fn().mockResolvedValue(undefined);
287+
vi.stubGlobal("navigator", { clipboard: { writeText } });
288+
289+
const state = createState();
290+
state.connected = true;
291+
state.client = { request } as unknown as ConfigState["client"];
292+
state.lastError = "stale error";
293+
294+
await openConfigFile(state);
295+
296+
expect(request).toHaveBeenCalledWith("config.openFile", {});
297+
expect(writeText).toHaveBeenCalledWith("/tmp/openclaw.json");
298+
expect(state.lastError).toBe(
299+
"Cannot open file in headless environment.\n\nFile path copied to clipboard: /tmp/openclaw.json",
300+
);
301+
});
302+
303+
it("includes the config path in the visible error when clipboard fallback fails", async () => {
304+
const request = vi.fn().mockResolvedValue({
305+
ok: false,
306+
error: "Failed to open config file",
307+
});
308+
const writeText = vi.fn().mockRejectedValue(new Error("clipboard denied"));
309+
vi.stubGlobal("navigator", { clipboard: { writeText } });
310+
311+
const state = createState();
312+
state.connected = true;
313+
state.client = { request } as unknown as ConfigState["client"];
314+
state.configSnapshot = {
315+
config: {},
316+
path: "/tmp/from-snapshot.json",
317+
valid: true,
318+
issues: [],
319+
};
320+
321+
await openConfigFile(state);
322+
323+
expect(writeText).toHaveBeenCalledWith("/tmp/from-snapshot.json");
324+
expect(state.lastError).toBe(
325+
"Failed to open config file\n\nFile path: /tmp/from-snapshot.json",
326+
);
327+
});
328+
});
329+
274330
describe("updateConfigFormValue", () => {
275331
it("seeds from snapshot when form is null", () => {
276332
const state = createState();

ui/src/ui/controllers/config.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,6 @@ export async function openConfigFile(state: ConfigState): Promise<void> {
510510
if (!res.ok) {
511511
const errorMessage = res.error || "Failed to open config file";
512512
state.lastError = errorMessage;
513-
// Copy path to clipboard as fallback
514513
const path = res.path || state.configSnapshot?.path;
515514
if (path) {
516515
try {

0 commit comments

Comments
 (0)