All test assertions updated to reference crm instead of dench, and IDENTITY.md is now expected to be visible in the workspace tree.
331 lines
13 KiB
TypeScript
331 lines
13 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import type { Dirent } from "node:fs";
|
|
|
|
// Mock node:fs
|
|
vi.mock("node:fs", () => ({
|
|
readdirSync: vi.fn(() => []),
|
|
readFileSync: vi.fn(() => ""),
|
|
existsSync: vi.fn(() => false),
|
|
statSync: vi.fn(() => ({ isDirectory: () => false, size: 100 })),
|
|
}));
|
|
|
|
// Mock node:os
|
|
vi.mock("node:os", () => ({
|
|
homedir: vi.fn(() => "/home/testuser"),
|
|
}));
|
|
|
|
// Mock workspace
|
|
vi.mock("@/lib/workspace", () => ({
|
|
resolveWorkspaceRoot: vi.fn(() => null),
|
|
resolveOpenClawStateDir: vi.fn(() => "/home/testuser/.openclaw-ironclaw"),
|
|
getActiveWorkspaceName: vi.fn(() => null),
|
|
parseSimpleYaml: vi.fn(() => ({})),
|
|
duckdbQueryAll: vi.fn(() => []),
|
|
duckdbQueryAllAsync: vi.fn(async () => []),
|
|
isDatabaseFile: vi.fn(() => false),
|
|
discoverDuckDBPaths: vi.fn(() => []),
|
|
resolveDuckdbBin: vi.fn(() => null),
|
|
safeResolvePath: vi.fn(() => null),
|
|
}));
|
|
|
|
function makeDirent(name: string, isDir: boolean): Dirent {
|
|
return {
|
|
name,
|
|
isDirectory: () => isDir,
|
|
isFile: () => !isDir,
|
|
isBlockDevice: () => false,
|
|
isCharacterDevice: () => false,
|
|
isFIFO: () => false,
|
|
isSocket: () => false,
|
|
isSymbolicLink: () => false,
|
|
path: "",
|
|
parentPath: "",
|
|
} as Dirent;
|
|
}
|
|
|
|
describe("Workspace Tree & Browse API", () => {
|
|
beforeEach(() => {
|
|
vi.resetModules();
|
|
vi.mock("node:fs", () => ({
|
|
readdirSync: vi.fn(() => []),
|
|
readFileSync: vi.fn(() => ""),
|
|
existsSync: vi.fn(() => false),
|
|
statSync: vi.fn(() => ({ isDirectory: () => false, size: 100 })),
|
|
}));
|
|
vi.mock("node:os", () => ({
|
|
homedir: vi.fn(() => "/home/testuser"),
|
|
}));
|
|
vi.mock("@/lib/workspace", () => ({
|
|
resolveWorkspaceRoot: vi.fn(() => null),
|
|
resolveOpenClawStateDir: vi.fn(() => "/home/testuser/.openclaw-ironclaw"),
|
|
getActiveWorkspaceName: vi.fn(() => null),
|
|
parseSimpleYaml: vi.fn(() => ({})),
|
|
duckdbQueryAll: vi.fn(() => []),
|
|
duckdbQueryAllAsync: vi.fn(async () => []),
|
|
isDatabaseFile: vi.fn(() => false),
|
|
discoverDuckDBPaths: vi.fn(() => []),
|
|
resolveDuckdbBin: vi.fn(() => null),
|
|
safeResolvePath: vi.fn(() => null),
|
|
}));
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
// ─── GET /api/workspace/tree ────────────────────────────────────
|
|
|
|
describe("GET /api/workspace/tree", () => {
|
|
it("returns tree with exists=false when no workspace root", async () => {
|
|
const { GET } = await import("./tree/route.js");
|
|
const req = new Request("http://localhost/api/workspace/tree");
|
|
const res = await GET(req);
|
|
const json = await res.json();
|
|
expect(json.exists).toBe(false);
|
|
expect(json.tree).toEqual([]);
|
|
expect(json.workspace).toBeNull();
|
|
});
|
|
|
|
it("returns tree with workspace files", async () => {
|
|
const { resolveWorkspaceRoot, getActiveWorkspaceName } = await import("@/lib/workspace");
|
|
vi.mocked(resolveWorkspaceRoot).mockReturnValue("/ws");
|
|
vi.mocked(getActiveWorkspaceName).mockReturnValue("default");
|
|
const { readdirSync: mockReaddir, existsSync: mockExists } = await import("node:fs");
|
|
vi.mocked(mockExists).mockReturnValue(true);
|
|
vi.mocked(mockReaddir).mockImplementation((dir) => {
|
|
if (String(dir) === "/ws") {
|
|
return [
|
|
makeDirent("knowledge", true),
|
|
makeDirent("readme.md", false),
|
|
] as unknown as Dirent[];
|
|
}
|
|
return [] as unknown as Dirent[];
|
|
});
|
|
|
|
const { GET } = await import("./tree/route.js");
|
|
const req = new Request("http://localhost/api/workspace/tree");
|
|
const res = await GET(req);
|
|
const json = await res.json();
|
|
expect(json.exists).toBe(true);
|
|
expect(json.tree.length).toBeGreaterThan(0);
|
|
expect(json.workspace).toBe("default");
|
|
});
|
|
|
|
it("includes workspaceRoot in response", async () => {
|
|
const { resolveWorkspaceRoot } = await import("@/lib/workspace");
|
|
vi.mocked(resolveWorkspaceRoot).mockReturnValue("/ws");
|
|
const { existsSync: mockExists } = await import("node:fs");
|
|
vi.mocked(mockExists).mockReturnValue(true);
|
|
|
|
const { GET } = await import("./tree/route.js");
|
|
const req = new Request("http://localhost/api/workspace/tree");
|
|
const res = await GET(req);
|
|
const json = await res.json();
|
|
expect(json.workspaceRoot).toBe("/ws");
|
|
});
|
|
|
|
it("includes root IDENTITY.md in the workspace tree", async () => {
|
|
const { resolveWorkspaceRoot } = await import("@/lib/workspace");
|
|
vi.mocked(resolveWorkspaceRoot).mockReturnValue("/ws");
|
|
const { readdirSync: mockReaddir, existsSync: mockExists } = await import("node:fs");
|
|
vi.mocked(mockExists).mockImplementation((p) => String(p) === "/ws");
|
|
vi.mocked(mockReaddir).mockImplementation((dir) => {
|
|
if (String(dir) === "/ws") {
|
|
return [
|
|
makeDirent("IDENTITY.md", false),
|
|
makeDirent("notes.md", false),
|
|
] as unknown as Dirent[];
|
|
}
|
|
return [] as unknown as Dirent[];
|
|
});
|
|
|
|
const { GET } = await import("./tree/route.js");
|
|
const req = new Request("http://localhost/api/workspace/tree");
|
|
const res = await GET(req);
|
|
const json = await res.json();
|
|
const paths = (json.tree as Array<{ path: string }>).map((n) => n.path);
|
|
expect(paths).toContain("IDENTITY.md");
|
|
expect(paths).toContain("notes.md");
|
|
});
|
|
|
|
it("omits managed crm skill from the virtual skills folder", async () => {
|
|
const { resolveWorkspaceRoot } = await import("@/lib/workspace");
|
|
vi.mocked(resolveWorkspaceRoot).mockReturnValue("/ws");
|
|
const { readdirSync: mockReaddir, existsSync: mockExists } = await import("node:fs");
|
|
vi.mocked(mockExists).mockImplementation((p) => {
|
|
const value = String(p);
|
|
return (
|
|
value === "/ws" ||
|
|
value === "/ws/skills" ||
|
|
value === "/ws/skills/alpha/SKILL.md" ||
|
|
value === "/ws/skills/crm/SKILL.md"
|
|
);
|
|
});
|
|
vi.mocked(mockReaddir).mockImplementation((dir) => {
|
|
if (String(dir) === "/ws") {
|
|
return [] as unknown as Dirent[];
|
|
}
|
|
if (String(dir) === "/ws/skills") {
|
|
return [
|
|
makeDirent("alpha", true),
|
|
makeDirent("crm", true),
|
|
] as unknown as Dirent[];
|
|
}
|
|
return [] as unknown as Dirent[];
|
|
});
|
|
|
|
const { GET } = await import("./tree/route.js");
|
|
const req = new Request("http://localhost/api/workspace/tree");
|
|
const res = await GET(req);
|
|
const json = await res.json();
|
|
const skillsFolder = (json.tree as Array<{ path: string; children?: Array<{ path: string }> }>).find(
|
|
(node) => node.path === "~skills",
|
|
);
|
|
const skillPaths = (skillsFolder?.children ?? []).map((child) => child.path);
|
|
expect(skillPaths).toContain("~skills/alpha/SKILL.md");
|
|
expect(skillPaths).not.toContain("~skills/crm/SKILL.md");
|
|
});
|
|
});
|
|
|
|
// ─── GET /api/workspace/browse ──────────────────────────────────
|
|
|
|
describe("GET /api/workspace/browse", () => {
|
|
it("returns directory listing", async () => {
|
|
const { existsSync: mockExists, readdirSync: mockReaddir, statSync: mockStat } = await import("node:fs");
|
|
vi.mocked(mockExists).mockReturnValue(true);
|
|
vi.mocked(mockReaddir).mockReturnValue([
|
|
makeDirent("file.txt", false),
|
|
makeDirent("subfolder", true),
|
|
] as unknown as Dirent[]);
|
|
vi.mocked(mockStat).mockReturnValue({ isDirectory: () => false, size: 100 } as never);
|
|
|
|
const { GET } = await import("./browse/route.js");
|
|
const req = new Request("http://localhost/api/workspace/browse?dir=/tmp/test");
|
|
const res = await GET(req);
|
|
const json = await res.json();
|
|
expect(json.entries).toBeDefined();
|
|
expect(json.currentDir).toBeDefined();
|
|
});
|
|
|
|
it("returns parentDir for nested directories", async () => {
|
|
const { existsSync: mockExists, readdirSync: mockReaddir, statSync: mockStat } = await import("node:fs");
|
|
vi.mocked(mockExists).mockReturnValue(true);
|
|
vi.mocked(mockReaddir).mockReturnValue([]);
|
|
vi.mocked(mockStat).mockReturnValue({ isDirectory: () => true, size: 0 } as never);
|
|
|
|
const { GET } = await import("./browse/route.js");
|
|
const req = new Request("http://localhost/api/workspace/browse?dir=/tmp/test/sub");
|
|
const res = await GET(req);
|
|
const json = await res.json();
|
|
expect(json.parentDir).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ─── GET /api/workspace/suggest-files ────────────────────────────
|
|
|
|
describe("GET /api/workspace/suggest-files", () => {
|
|
it("returns suggestions when workspace exists", async () => {
|
|
const { resolveWorkspaceRoot } = await import("@/lib/workspace");
|
|
vi.mocked(resolveWorkspaceRoot).mockReturnValue("/ws");
|
|
const { existsSync: mockExists, readdirSync: mockReaddir } = await import("node:fs");
|
|
vi.mocked(mockExists).mockReturnValue(true);
|
|
vi.mocked(mockReaddir).mockReturnValue([
|
|
makeDirent("doc.md", false),
|
|
] as unknown as Dirent[]);
|
|
|
|
const { GET } = await import("./suggest-files/route.js");
|
|
const req = new Request("http://localhost/api/workspace/suggest-files?q=doc");
|
|
const res = await GET(req);
|
|
expect(res.status).toBe(200);
|
|
const json = await res.json();
|
|
expect(json.items).toBeDefined();
|
|
});
|
|
|
|
it("includes root IDENTITY.md in sidebar file suggestions", async () => {
|
|
const { resolveWorkspaceRoot } = await import("@/lib/workspace");
|
|
vi.mocked(resolveWorkspaceRoot).mockReturnValue("/ws");
|
|
const { existsSync: mockExists, readdirSync: mockReaddir } = await import("node:fs");
|
|
vi.mocked(mockExists).mockReturnValue(true);
|
|
vi.mocked(mockReaddir).mockImplementation((dir) => {
|
|
if (String(dir) === "/ws") {
|
|
return [
|
|
makeDirent("IDENTITY.md", false),
|
|
makeDirent("doc.md", false),
|
|
] as unknown as Dirent[];
|
|
}
|
|
return [] as unknown as Dirent[];
|
|
});
|
|
|
|
const { GET } = await import("./suggest-files/route.js");
|
|
const req = new Request("http://localhost/api/workspace/suggest-files");
|
|
const res = await GET(req);
|
|
expect(res.status).toBe(200);
|
|
const json = await res.json();
|
|
const names = (json.items as Array<{ name: string }>).map((item) => item.name);
|
|
expect(names).toContain("doc.md");
|
|
expect(names).toContain("IDENTITY.md");
|
|
});
|
|
});
|
|
|
|
// ─── GET /api/workspace/context ──────────────────────────────────
|
|
|
|
describe("GET /api/workspace/context", () => {
|
|
it("returns exists=false when no workspace root", async () => {
|
|
const { resolveWorkspaceRoot } = await import("@/lib/workspace");
|
|
vi.mocked(resolveWorkspaceRoot).mockReturnValue(null);
|
|
|
|
const { GET } = await import("./context/route.js");
|
|
const res = await GET();
|
|
const json = await res.json();
|
|
expect(json.exists).toBe(false);
|
|
});
|
|
|
|
it("returns context when workspace_context.yaml exists", async () => {
|
|
const { resolveWorkspaceRoot, parseSimpleYaml } = await import("@/lib/workspace");
|
|
vi.mocked(resolveWorkspaceRoot).mockReturnValue("/ws");
|
|
vi.mocked(parseSimpleYaml).mockReturnValue({ org_name: "Acme", org_slug: "acme" });
|
|
const { existsSync: mockExists, readFileSync: mockReadFile } = await import("node:fs");
|
|
vi.mocked(mockExists).mockReturnValue(true);
|
|
vi.mocked(mockReadFile).mockReturnValue("org_name: Acme" as never);
|
|
|
|
const { GET } = await import("./context/route.js");
|
|
const res = await GET();
|
|
const json = await res.json();
|
|
expect(json.exists).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ─── GET /api/workspace/search-index ─────────────────────────────
|
|
|
|
describe("GET /api/workspace/search-index", () => {
|
|
it("returns empty items when no workspace", async () => {
|
|
const { resolveWorkspaceRoot } = await import("@/lib/workspace");
|
|
vi.mocked(resolveWorkspaceRoot).mockReturnValue(null);
|
|
|
|
const { GET } = await import("./search-index/route.js");
|
|
const res = await GET();
|
|
const json = await res.json();
|
|
expect(json.items).toEqual([]);
|
|
});
|
|
|
|
it("returns file items from workspace tree", async () => {
|
|
const { resolveWorkspaceRoot, duckdbQueryAll } = await import("@/lib/workspace");
|
|
vi.mocked(resolveWorkspaceRoot).mockReturnValue("/ws");
|
|
vi.mocked(duckdbQueryAll).mockReturnValue([]);
|
|
const { existsSync: mockExists, readdirSync: mockReaddir } = await import("node:fs");
|
|
vi.mocked(mockExists).mockReturnValue(true);
|
|
vi.mocked(mockReaddir).mockImplementation((dir) => {
|
|
if (String(dir) === "/ws") {
|
|
return [makeDirent("readme.md", false)] as unknown as Dirent[];
|
|
}
|
|
return [] as unknown as Dirent[];
|
|
});
|
|
|
|
const { GET } = await import("./search-index/route.js");
|
|
const res = await GET();
|
|
const json = await res.json();
|
|
expect(json.items.length).toBeGreaterThanOrEqual(0);
|
|
});
|
|
});
|
|
});
|