Files
AgentHub/server/__tests__/file-manager.test.js
T
nntrivi2001 f2f1cd9b6c fix: per-user Today, native paste and drop on a local machine
- Today's spend in the status bar and the Claude Code statusline is the
  caller's own (`GET /api/pricing/cost?mine=1`, scope.ownSessionWhere), not
  every user's sum for an admin or analyst.
- A screenshot pasted into a local claude pane is sent as Claude Code's own
  image-paste key: Alt+V on Windows (Ctrl+V did nothing there), ^V elsewhere,
  and only when the machine can read its clipboard (wl-paste/xclip + a
  display on Linux), else it is uploaded (GET /api/run/native-paste {key}).
- A file manager's files pasted or dropped on a local browser go in by their
  own paths, read from the OS clipboard or the file manager's selection
  (server/lib/file-manager.js, POST /api/run/clipboard-files and
  /selected-files): no upload, no size limit. A copied image file is pasted
  by path, text that comes with a picture of it (spreadsheet cells) is
  pasted as text, Windows paths go in as `C:/…` mentions.
- Tab titles for Snapshot, Artifacts, Files and Administration.
- An npm tool AgentHub installed under ~/.local is updated there, not by its
  own updater into npm's global prefix.
2026-10-26 11:18:05 +07:00

125 lines
4.4 KiB
JavaScript

/**
* @file file-manager.test.js
* @description server/lib/file-manager.js: the files a file manager's Copy
* put on the OS clipboard (Windows GetFileDropList, macOS NSPasteboard, Linux
* text/uri-list / gnome-copied-files) and the ones selected in it — what a
* drag carries (Windows Shell.Application, macOS Finder) — kept only when
* they exist here, with their sizes; a missing reader is no files.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const { describe, it, before, after, afterEach } = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const {
clipboardFiles,
selectedFiles,
uriListPaths,
__setExecImpl,
} = require("../lib/file-manager");
const TMP = fs.mkdtempSync(path.join(os.tmpdir(), "fm-files-"));
const big = path.join(TMP, "big file.mp4");
const dir = path.join(TMP, "folder");
before(() => {
fs.writeFileSync(big, Buffer.alloc(1234));
fs.mkdirSync(dir);
});
after(() => fs.rmSync(TMP, { recursive: true, force: true }));
afterEach(() => __setExecImpl(async () => ""));
describe("uriListPaths", () => {
it("decodes file URIs and skips a gnome list's verb, comments and other hosts", () => {
assert.deepEqual(
uriListPaths("copy\nfile:///home/u/a%20b.png\r\n# c\nfile://other/x\nhttps://x/y\n"),
["/home/u/a b.png"]
);
});
});
describe("clipboardFiles", () => {
it("reads Windows' file drop list with powershell -Sta", async () => {
let seen;
__setExecImpl(async (cmd, args) => {
seen = [cmd, args];
return `${big}\r\n${dir}\r\n${path.join(TMP, "gone.txt")}\r\n`;
});
assert.deepEqual(await clipboardFiles("win32"), [
{ path: big, size: 1234, dir: false },
{ path: dir, size: null, dir: true },
]);
assert.equal(seen[0], "powershell");
assert.ok(seen[1].includes("-Sta"));
assert.match(seen[1].at(-1), /GetFileDropList/);
});
it("reads macOS file URLs through JXA", async () => {
let seen;
__setExecImpl(async (cmd, args) => {
seen = [cmd, args];
return `${big}\n`;
});
assert.deepEqual(await clipboardFiles("darwin"), [{ path: big, size: 1234, dir: false }]);
assert.equal(seen[0], "osascript");
assert.deepEqual(seen[1].slice(0, 2), ["-l", "JavaScript"]);
});
it("reads Linux text/uri-list with the desktop's display", async () => {
const saved = process.env.DISPLAY;
process.env.DISPLAY = ":0";
try {
const calls = [];
__setExecImpl(async (cmd, args, env) => {
calls.push([cmd, args.join(" "), env.DISPLAY]);
return cmd === "xclip" ? `file://${encodeURI(big)}\n` : "";
});
assert.deepEqual(await clipboardFiles("linux"), [{ path: big, size: 1234, dir: false }]);
assert.deepEqual(calls.at(-1), ["xclip", "-selection clipboard -t text/uri-list -o", ":0"]);
} finally {
if (saved === undefined) delete process.env.DISPLAY;
else process.env.DISPLAY = saved;
}
});
it("answers no files when the reader is missing or prints relative paths", async () => {
__setExecImpl(async () => "");
assert.deepEqual(await clipboardFiles("win32"), []);
__setExecImpl(async () => "relative/x.txt\n");
assert.deepEqual(await clipboardFiles("darwin"), []);
});
it("reads Explorer's selection (and the desktop's) for a drop, once per path", async () => {
let seen;
__setExecImpl(async (cmd, args) => {
seen = [cmd, args];
return `${big}\r\n${big}\r\n`;
});
assert.deepEqual(await selectedFiles("win32"), [{ path: big, size: 1234, dir: false }]);
assert.equal(seen[0], "powershell");
assert.match(seen[1].at(-1), /Shell\.Application[\s\S]*SelectedItems[\s\S]*FindWindowSW/);
});
it("reads Finder's selection on macOS, a folder with its trailing slash", async () => {
let seen;
__setExecImpl(async (cmd, args) => {
seen = [cmd, args];
return `${dir}/\n`;
});
assert.deepEqual(await selectedFiles("darwin"), [{ path: `${dir}/`, size: null, dir: true }]);
assert.equal(seen[0], "osascript");
assert.ok(seen[1].includes("repeat with a in (selection as alias list)"));
});
it("has no selection to read on Linux", async () => {
let called = false;
__setExecImpl(async () => {
called = true;
return big;
});
assert.deepEqual(await selectedFiles("linux"), []);
assert.equal(called, false);
});
});