diff --git a/apps/web/app/api/workspace/file-ops.test.ts b/apps/web/app/api/workspace/file-ops.test.ts index 5a6bcdb94d6..41d66191232 100644 --- a/apps/web/app/api/workspace/file-ops.test.ts +++ b/apps/web/app/api/workspace/file-ops.test.ts @@ -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", { diff --git a/apps/web/app/api/workspace/objects.test.ts b/apps/web/app/api/workspace/objects.test.ts index 6b97b35a154..0c4ea27385f 100644 --- a/apps/web/app/api/workspace/objects.test.ts +++ b/apps/web/app/api/workspace/objects.test.ts @@ -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); diff --git a/apps/web/app/api/workspace/tree-browse.test.ts b/apps/web/app/api/workspace/tree-browse.test.ts index 0f59f1a4c12..e6a5e3c0e21 100644 --- a/apps/web/app/api/workspace/tree-browse.test.ts +++ b/apps/web/app/api/workspace/tree-browse.test.ts @@ -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 ──────────────────────────────────