Files
AgentHub/server/__tests__/native-dialog.test.js
T
nntrivi2001 6c605849d8 feat: workspace aliases, tab groups of split panes, native path picker (v2.1.0)
- /api/workspaces and `agenthub workspace(s)` / `--workspace` as aliases of
  /api/lanes and `agenthub lanes` / `--lane`; old names print one stderr note.
  REPL `help lanes|workspace|workspaces` resolves through the aliases.
- A workspace holds up to ten sessions (slots 0-9); every console tab is a
  group of 1-4 split panes (PaneGroups, agenthub.workspace.paneLayout);
  parseGroups keeps the stored active tab when an earlier one is dropped.
- Browse button beside path fields: POST /api/lanes/pick-path opens the OS
  picker on the server's desktop — loopback only and no forwarding header
  (EREMOTE), a display failure is ENODIALOG not a Cancel, the dialog is
  killed when the request closes, a folder field falls back to the in-app
  browser on any failure.
- Changes rail counts chip, image viewer (/git/raw), status bar session
  counts and per-day cost (`date=` on /api/pricing/cost).
- /agenthub-open and /agenthub-doctor run from the INSTALLED build
  (supersededBy), doctor offers fixes behind two confirmations; plugin-open
  treats a locked bootstrap as non-fatal.
- New-terminal shell waits for the first attach (issue #55).
- repo-onboarding / debug-live-issue skills removed.
- OpenAPI: pick-path, lane/date on /pricing/cost, tool on /run/history,
  /api/workspaces mount.
2026-10-06 11:31:26 +07:00

106 lines
4.1 KiB
JavaScript

/**
* @file native-dialog.test.js
* @description Unit tests for the server-side native folder/file picker: the
* argv each platform gets, the fallback from zenity to kdialog, Cancel versus a
* chosen path, and the "no dialog program" answer the client falls back on.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const { describe, it, afterEach } = require("node:test");
const assert = require("node:assert/strict");
const os = require("node:os");
const nd = require("../lib/native-dialog");
const desktop = require("../lib/desktop-env");
const linux = process.platform === "linux";
describe("native-dialog", () => {
afterEach(() => nd.__setExecImpl(null));
it("starts in the given folder, a file's folder, or home", () => {
assert.equal(nd.startDir(os.tmpdir()), os.tmpdir());
assert.equal(nd.startDir("/definitely/not/here/x"), os.homedir());
assert.equal(nd.startDir("~"), os.homedir());
assert.equal(nd.startDir(undefined), os.homedir());
});
it("builds zenity then kdialog on Linux, the request's folder one argv element", () => {
const [[z, zargs], [k, kargs]] = nd.dialogCommands("dir", "/tmp/a b", "linux");
assert.equal(z, "zenity");
assert.deepEqual(zargs, [
"--file-selection",
"--title=Select a folder",
"--filename=/tmp/a b/",
"--directory",
]);
assert.equal(k, "kdialog");
assert.deepEqual(kargs.slice(0, 2), ["--getexistingdirectory", "/tmp/a b"]);
assert.ok(!nd.dialogCommands("file", "/tmp", "linux")[0][1].includes("--directory"));
});
it("passes the folder to osascript as an argument, never inside the script", () => {
const [[cmd, args]] = nd.dialogCommands("file", '/x"; do shell script "id', "darwin");
assert.equal(cmd, "osascript");
assert.equal(args.at(-1), '/x"; do shell script "id');
assert.ok(!args.slice(0, -2).join(" ").includes("shell script"));
});
it(
"answers the chosen path, null on Cancel, and falls through a missing program",
{
skip: !linux,
},
async () => {
desktop.__setExecImpl(() => "DISPLAY=:0\n");
const tried = [];
nd.__setExecImpl(async (cmd) => {
tried.push(cmd);
return cmd === "zenity" ? { missing: true } : { code: 0, stdout: "/home/u/proj\n" };
});
assert.equal(await nd.pickPath("dir"), "/home/u/proj");
assert.deepEqual(tried, ["zenity", "kdialog"]);
nd.__setExecImpl(async () => ({ code: 1, stdout: "" }));
assert.equal(await nd.pickPath("file"), null);
nd.__setExecImpl(async () => ({ missing: true }));
await assert.rejects(nd.pickPath("dir"), { code: "ENODIALOG" });
}
);
it(
"answers ENODIALOG, not a Cancel, when the program could not reach the desktop",
{ skip: !linux },
async () => {
desktop.__setExecImpl(() => "DISPLAY=:0\n");
nd.__setExecImpl(async () => ({
code: 1,
stdout: "",
stderr: "(zenity:1): Gtk-WARNING **: cannot open display: :0\n",
}));
await assert.rejects(nd.pickPath("dir"), { code: "ENODIALOG" });
}
);
it("passes the abort signal through to the dialog program", { skip: !linux }, async () => {
desktop.__setExecImpl(() => "DISPLAY=:0\n");
const ac = new AbortController();
let seen;
nd.__setExecImpl(async (cmd, args, env, signal) => {
seen = signal;
return { code: 1, stdout: "", stderr: "" };
});
assert.equal(await nd.pickPath("dir", undefined, ac.signal), null);
assert.equal(seen, ac.signal);
});
it("tells a Cancel from a failure per program", () => {
assert.equal(nd.isCancel("zenity", ""), true);
assert.equal(nd.isCancel("zenity", "Gtk-Message: GtkDialog mapped without a transient parent"), true);
assert.equal(nd.isCancel("kdialog", "qt.qpa.xcb: could not connect to display :1"), false);
assert.equal(nd.isCancel("osascript", "execution error: User canceled. (-128)"), true);
assert.equal(nd.isCancel("osascript", "execution error: No user interaction allowed. (-1713)"), false);
assert.equal(nd.isCancel("powershell.exe", ""), true);
assert.equal(nd.isCancel("powershell.exe", "Add-Type : Cannot add type"), false);
});
});