test(web): add workspace file-ops, objects, and tree-browse tests

This commit is contained in:
kumarabhirup 2026-03-02 18:34:41 -08:00
parent f23b7133cb
commit 7309d649dd
No known key found for this signature in database
GPG Key ID: DB7CA2289CAB0167
3 changed files with 158 additions and 0 deletions

View File

@ -109,6 +109,20 @@ describe("Workspace File Operations API", () => {
expect(mockWrite).toHaveBeenCalled();
});
it("returns 403 when attempting to modify a system file", async () => {
const { isSystemFile } = await import("@/lib/workspace");
vi.mocked(isSystemFile).mockReturnValueOnce(true);
const { POST } = await import("./file/route.js");
const req = new Request("http://localhost/api/workspace/file", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ path: "IDENTITY.md", content: "# override" }),
});
const res = await POST(req);
expect(res.status).toBe(403);
});
it("returns 400 for missing path", async () => {
const { POST } = await import("./file/route.js");
const req = new Request("http://localhost/api/workspace/file", {

View File

@ -120,6 +120,63 @@ describe("Workspace Objects API", () => {
expect(json.fields).toBeDefined();
});
it("returns saved views and active view from object yaml metadata", async () => {
const {
findDuckDBForObject,
duckdbQueryOnFile,
resolveDuckdbBin,
discoverDuckDBPaths,
getObjectViews,
} = await import("@/lib/workspace");
vi.mocked(findDuckDBForObject).mockReturnValue("/ws/workspace.duckdb");
vi.mocked(resolveDuckdbBin).mockReturnValue("/opt/homebrew/bin/duckdb");
vi.mocked(discoverDuckDBPaths).mockReturnValue(["/ws/workspace.duckdb"]);
vi.mocked(getObjectViews).mockReturnValue({
views: [
{
name: "Important",
filters: {
id: "root",
conjunction: "and",
rules: [
{ id: "rule-1", field: "Status", operator: "is", value: "Important" },
],
},
columns: ["Name", "Status"],
},
],
activeView: "Important",
});
let queryCall = 0;
vi.mocked(duckdbQueryOnFile).mockImplementation(() => {
queryCall += 1;
if (queryCall === 1) {
return [{ id: "obj1", name: "leads", description: "Leads object", icon: "star" }];
}
if (queryCall === 2) {
return [{ id: "f1", name: "name", type: "text", sort_order: 0 }];
}
if (queryCall === 3) {
return [];
}
return [];
});
const { GET } = await import("./objects/[name]/route.js");
const res = await GET(
new Request("http://localhost/api/workspace/objects/leads"),
{ params: Promise.resolve({ name: "leads" }) },
);
expect(res.status).toBe(200);
const json = await res.json();
expect(json.savedViews).toHaveLength(1);
expect(json.savedViews[0].name).toBe("Important");
expect(json.activeView).toBe("Important");
});
it("accepts underscored names", async () => {
const { findDuckDBForObject } = await import("@/lib/workspace");
vi.mocked(findDuckDBForObject).mockReturnValue(null);

View File

@ -120,6 +120,68 @@ describe("Workspace Tree & Browse API", () => {
const json = await res.json();
expect(json.workspaceRoot).toBe("/ws");
});
it("omits root IDENTITY.md from 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).not.toContain("IDENTITY.md");
expect(paths).toContain("notes.md");
});
it("omits managed dench 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 === "/home/testuser/.openclaw/skills" ||
value === "/home/testuser/.openclaw/skills/alpha/SKILL.md" ||
value === "/home/testuser/.openclaw/skills/dench/SKILL.md"
);
});
vi.mocked(mockReaddir).mockImplementation((dir) => {
if (String(dir) === "/ws") {
return [] as unknown as Dirent[];
}
if (String(dir) === "/home/testuser/.openclaw/skills") {
return [
makeDirent("alpha", true),
makeDirent("dench", 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/dench/SKILL.md");
});
});
// ─── GET /api/workspace/browse ──────────────────────────────────
@ -175,6 +237,31 @@ describe("Workspace Tree & Browse API", () => {
const json = await res.json();
expect(json.items).toBeDefined();
});
it("omits root IDENTITY.md from 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).not.toContain("IDENTITY.md");
});
});
// ─── GET /api/workspace/context ──────────────────────────────────