feat(webui): add project workspaces and access controls (#4007)
* feat(webui): add project workspaces and access controls * feat(webui): add project workspaces and access controls * refactor(tools): centralize workspace access resolution * refactor(webui): remove unused workspace host state * fix(webui): hide estimated file edit label * fix(webui): clarify file edit deletion feedback * fix(webui): label deleted file activity * fix(webui): flatten file edit activity rows * fix(core): remove path-only patch deletion * fix(core): keep apply patch non-destructive * refactor(webui): trim workspace host plumbing * fix(tools): register exec with tools config
This commit is contained in:
@@ -12,6 +12,8 @@ const updateUrlSpy = vi.fn();
|
||||
const attachSpy = vi.fn();
|
||||
const runStatusHandlers = new Set<(chatId: string, startedAt: number | null) => void>();
|
||||
let mockSessions: ChatSummary[] = [];
|
||||
const HERO_GREETING_PATTERN =
|
||||
/What should we work on\?|Where should we start\?|What are we building today\?|What should we tackle together\?/;
|
||||
|
||||
function jsonResponse(body: unknown): Response {
|
||||
return {
|
||||
@@ -97,6 +99,9 @@ function baseSettingsPayload() {
|
||||
},
|
||||
advanced: {
|
||||
restrict_to_workspace: false,
|
||||
webui_allow_local_service_access: true,
|
||||
webui_default_access_mode: "default",
|
||||
private_service_protection_enabled: true,
|
||||
ssrf_whitelist_count: 0,
|
||||
mcp_server_count: 0,
|
||||
exec_enabled: true,
|
||||
@@ -412,29 +417,7 @@ describe("App layout", () => {
|
||||
const encoded = new URLSearchParams(updateUrl?.split("?", 2)[1]).get("state");
|
||||
expect(JSON.parse(encoded ?? "{}").view.show_archived).toBe(true);
|
||||
|
||||
fireEvent.pointerDown(within(sidebar).getByRole("button", { name: "View" }), {
|
||||
button: 0,
|
||||
ctrlKey: false,
|
||||
});
|
||||
fireEvent.click(await screen.findByText("Compact list"));
|
||||
await waitFor(() => {
|
||||
const lastUpdateUrl = vi.mocked(fetch).mock.calls
|
||||
.map(([url]) => String(url))
|
||||
.filter((url) => url.startsWith("/api/webui/sidebar-state/update?"))
|
||||
.at(-1);
|
||||
const lastEncoded = new URLSearchParams(lastUpdateUrl?.split("?", 2)[1]).get("state");
|
||||
expect(JSON.parse(lastEncoded ?? "{}").view.density).toBe("compact");
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText("Title A-Z"));
|
||||
await waitFor(() => {
|
||||
const lastUpdateUrl = vi.mocked(fetch).mock.calls
|
||||
.map(([url]) => String(url))
|
||||
.filter((url) => url.startsWith("/api/webui/sidebar-state/update?"))
|
||||
.at(-1);
|
||||
const lastEncoded = new URLSearchParams(lastUpdateUrl?.split("?", 2)[1]).get("state");
|
||||
expect(JSON.parse(lastEncoded ?? "{}").view.sort).toBe("title_asc");
|
||||
});
|
||||
expect(within(sidebar).queryByRole("button", { name: "View" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("sorts chats by displayed title when A-Z is persisted", async () => {
|
||||
@@ -785,6 +768,9 @@ describe("App layout", () => {
|
||||
},
|
||||
advanced: {
|
||||
restrict_to_workspace: false,
|
||||
webui_allow_local_service_access: true,
|
||||
webui_default_access_mode: "default",
|
||||
private_service_protection_enabled: true,
|
||||
ssrf_whitelist_count: 0,
|
||||
mcp_server_count: 0,
|
||||
exec_enabled: true,
|
||||
@@ -828,8 +814,8 @@ describe("App layout", () => {
|
||||
expect(within(settingsNav).queryByRole("button", { name: "Providers" })).not.toBeInTheDocument();
|
||||
expect(within(settingsNav).getByRole("button", { name: "Image" })).toBeInTheDocument();
|
||||
expect(within(settingsNav).getByRole("button", { name: "Web" })).toBeInTheDocument();
|
||||
expect(within(settingsNav).getByRole("button", { name: "Apps" })).toBeInTheDocument();
|
||||
expect(within(settingsNav).getByRole("button", { name: "Advanced" })).toBeInTheDocument();
|
||||
expect(within(settingsNav).queryByRole("button", { name: "Apps" })).not.toBeInTheDocument();
|
||||
expect(within(settingsNav).getByRole("button", { name: "Security" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Sign out" })).toBeInTheDocument();
|
||||
fireEvent.click(within(settingsNav).getByRole("button", { name: "Appearance" }));
|
||||
expect(screen.getByText("Brand logos")).toBeInTheDocument();
|
||||
@@ -906,9 +892,13 @@ describe("App layout", () => {
|
||||
expect(screen.getByText("BSAo••••ew20")).toBeInTheDocument();
|
||||
expect(screen.queryByDisplayValue("unsaved-brave-key")).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(within(settingsNav).getByRole("button", { name: "Runtime" }));
|
||||
fireEvent.click(within(settingsNav).getByRole("button", { name: "System" }));
|
||||
expect(screen.getByText("Bot name")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Tool hint length")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Heartbeat")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Dream")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Unified session")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("Default workspace")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Save" })).toBeDisabled();
|
||||
fireEvent.pointerDown(screen.getByRole("button", { name: "UTC" }));
|
||||
expect(screen.getByPlaceholderText("Search timezone")).toBeInTheDocument();
|
||||
@@ -1071,6 +1061,9 @@ describe("App layout", () => {
|
||||
},
|
||||
advanced: {
|
||||
restrict_to_workspace: false,
|
||||
webui_allow_local_service_access: true,
|
||||
webui_default_access_mode: "default",
|
||||
private_service_protection_enabled: true,
|
||||
ssrf_whitelist_count: 0,
|
||||
mcp_server_count: 0,
|
||||
exec_enabled: true,
|
||||
@@ -1097,7 +1090,7 @@ describe("App layout", () => {
|
||||
fireEvent.click(screen.getByRole("button", { name: "Back to chat" }));
|
||||
|
||||
await waitFor(() => expect(document.title).toBe("nanobot"));
|
||||
expect(screen.getByText("What can I do for you?")).toBeInTheDocument();
|
||||
expect(screen.getByText(HERO_GREETING_PATTERN)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("filters sessions in the centered search dialog", async () => {
|
||||
@@ -1266,23 +1259,23 @@ describe("App layout", () => {
|
||||
expect(toggleThemeSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Collapse sidebar" }));
|
||||
const desktopAside = container.querySelector("aside.lg\\:block") as HTMLElement;
|
||||
await waitFor(() => expect(desktopAside.style.width).toBe("56px"));
|
||||
const sidebarAside = container.querySelector("aside.lg\\:block") as HTMLElement;
|
||||
await waitFor(() => expect(sidebarAside.style.width).toBe("56px"));
|
||||
|
||||
expect(screen.queryByRole("button", { name: "Start a new chat" })).not.toBeInTheDocument();
|
||||
const rail = screen.getByRole("navigation", { name: "Sidebar navigation" });
|
||||
expect(within(rail).getByRole("button", { name: "New chat" })).toBeInTheDocument();
|
||||
expect(within(rail).getByRole("button", { name: "Search" })).toBeInTheDocument();
|
||||
expect(within(rail).getByRole("button", { name: "View" })).toBeInTheDocument();
|
||||
expect(within(rail).queryByRole("button", { name: "View" })).not.toBeInTheDocument();
|
||||
expect(within(rail).queryByText("Existing chat")).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(within(rail).getByRole("button", { name: "Toggle sidebar" }));
|
||||
await waitFor(() => expect(desktopAside.style.width).toBe("272px"));
|
||||
await waitFor(() => expect(sidebarAside.style.width).toBe("272px"));
|
||||
|
||||
const sidebar = screen.getByRole("navigation", { name: "Sidebar navigation" });
|
||||
fireEvent.click(within(sidebar).getByRole("button", { name: "New chat" }));
|
||||
expect(createChatSpy).not.toHaveBeenCalled();
|
||||
expect(screen.getByText("What can I do for you?")).toBeInTheDocument();
|
||||
expect(screen.getByText(HERO_GREETING_PATTERN)).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Start a new chat" })).not.toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Toggle theme from header" })).toBeInTheDocument();
|
||||
expect(within(sidebar).getByRole("button", { name: "Settings" })).toBeInTheDocument();
|
||||
|
||||
Reference in New Issue
Block a user