Files
openclaw/extensions/file-transfer/src/tools/dir-list-tool.test.ts
2026-08-20 23:28:31 -07:00

134 lines
3.9 KiB
TypeScript

// File Transfer tests cover dir list tool plugin behavior.
import {
callGatewayTool,
listNodes,
resolveNodeIdFromList,
} from "openclaw/plugin-sdk/agent-harness-runtime";
import { afterEach, describe, expect, it, vi } from "vitest";
import { createDirListTool } from "./dir-list-tool.js";
vi.mock("openclaw/plugin-sdk/agent-harness-runtime", () => ({
callGatewayTool: vi.fn(),
listNodes: vi.fn(),
resolveNodeIdFromList: vi.fn(),
}));
vi.mock("../shared/audit.js", () => ({
appendFileTransferAudit: vi.fn(),
}));
afterEach(() => {
vi.mocked(callGatewayTool).mockReset();
vi.mocked(listNodes).mockReset();
vi.mocked(resolveNodeIdFromList).mockReset();
});
describe("dir_list tool", () => {
it("exposes the next page token to the model and forwards the current page token", async () => {
const entries = [
{ name: "report.txt", isDir: false },
{ name: "nested", isDir: true },
];
vi.mocked(listNodes).mockResolvedValue([{ nodeId: "node-1", displayName: "Node One" }]);
vi.mocked(resolveNodeIdFromList).mockReturnValue("node-1");
vi.mocked(callGatewayTool).mockResolvedValue({
payload: {
ok: true,
path: "/tmp/project",
entries,
nextPageToken: "3",
truncated: true,
},
});
const result = await createDirListTool().execute("tool-call-1", {
node: "node-1",
path: "/tmp/project",
pageToken: "+01",
maxEntries: 2,
});
expect(result.content).toEqual([
{
type: "text",
text: 'Listed /tmp/project: 1 file, 1 subdir (more entries available). Call dir_list again with pageToken="3".',
},
]);
expect(result.details).toEqual({
path: "/tmp/project",
entries,
nextPageToken: "3",
truncated: true,
});
expect(callGatewayTool).toHaveBeenCalledWith(
"node.invoke",
expect.anything(),
expect.objectContaining({
nodeId: "node-1",
command: "dir.list",
params: {
path: "/tmp/project",
pageToken: "+01",
maxEntries: 2,
},
}),
);
});
it.each([undefined, ""])(
"reports truncation without inventing an unavailable page token (%s)",
async (nextPageToken) => {
vi.mocked(listNodes).mockResolvedValue([{ nodeId: "node-1", displayName: "Node One" }]);
vi.mocked(resolveNodeIdFromList).mockReturnValue("node-1");
vi.mocked(callGatewayTool).mockResolvedValue({
payload: {
ok: true,
path: "/tmp/project",
entries: [],
nextPageToken,
truncated: true,
},
});
const result = await createDirListTool().execute("tool-call-1", {
node: "node-1",
path: "/tmp/project",
});
expect(result.content).toEqual([
{ type: "text", text: "Listed /tmp/project: 0 files, 0 subdirs (more entries available)" },
]);
expect(result.details).toEqual({
path: "/tmp/project",
entries: [],
nextPageToken,
truncated: true,
});
},
);
it("reports missing paired nodes before retrying guessed local node names", async () => {
vi.mocked(listNodes).mockResolvedValue([]);
await expect(
createDirListTool().execute("tool-call-1", {
node: "local",
path: "/tmp/project",
}),
).rejects.toThrow(
"no paired nodes available; file-transfer tools require a paired node from nodes status. Use local file/exec tools for local workspace paths.",
);
expect(resolveNodeIdFromList).not.toHaveBeenCalled();
expect(callGatewayTool).not.toHaveBeenCalled();
});
it("describes node as a paired-node reference, not a local alias", () => {
const schema = JSON.stringify(createDirListTool().parameters);
expect(schema).toContain("Existing paired node id");
expect(schema).toContain("nodes status");
expect(schema).toContain("local, host, gateway, or auto");
});
});