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:
@@ -7,15 +7,20 @@ import {
|
||||
fetchMcpPresets,
|
||||
fetchSidebarState,
|
||||
fetchWebuiThread,
|
||||
fetchWorkspaces,
|
||||
importMcpConfig,
|
||||
listSessions,
|
||||
listSlashCommands,
|
||||
loginProviderOAuth,
|
||||
logoutProviderOAuth,
|
||||
runCliAppAction,
|
||||
runMcpPresetAction,
|
||||
saveCustomMcpServer,
|
||||
updateSidebarState,
|
||||
updateImageGenerationSettings,
|
||||
updateModelConfiguration,
|
||||
updateMcpServerTools,
|
||||
updateNetworkSafetySettings,
|
||||
updateProviderSettings,
|
||||
updateSettings,
|
||||
updateWebSearchSettings,
|
||||
@@ -89,6 +94,44 @@ describe("webui API helpers", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("serializes model configuration updates", async () => {
|
||||
await updateModelConfiguration("tok", {
|
||||
name: "codex",
|
||||
label: "Codex",
|
||||
provider: "openai_codex",
|
||||
model: "openai-codex/gpt-5.5",
|
||||
});
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
"/api/settings/model-configurations/update?name=codex&label=Codex&provider=openai_codex&model=openai-codex%2Fgpt-5.5",
|
||||
expect.objectContaining({
|
||||
headers: { Authorization: "Bearer tok" },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("reports HTML API fallbacks as gateway mismatch errors", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
headers: new Headers({ "content-type": "text/html; charset=utf-8" }),
|
||||
text: async () => "<!doctype html><html></html>",
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(
|
||||
updateModelConfiguration("tok", {
|
||||
name: "codex",
|
||||
model: "openai-codex/gpt-5.5",
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
status: 200,
|
||||
message: "Gateway returned WebUI HTML instead of JSON. Restart nanobot gateway and try again.",
|
||||
});
|
||||
});
|
||||
|
||||
it("serializes provider settings updates without returning secrets", async () => {
|
||||
await updateProviderSettings("tok", {
|
||||
provider: "openrouter",
|
||||
@@ -104,6 +147,24 @@ describe("webui API helpers", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("serializes provider OAuth login and logout actions", async () => {
|
||||
await loginProviderOAuth("tok", "openai_codex");
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
"/api/settings/provider/oauth-login?provider=openai_codex",
|
||||
expect.objectContaining({
|
||||
headers: { Authorization: "Bearer tok" },
|
||||
}),
|
||||
);
|
||||
|
||||
await logoutProviderOAuth("tok", "openai_codex");
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
"/api/settings/provider/oauth-logout?provider=openai_codex",
|
||||
expect.objectContaining({
|
||||
headers: { Authorization: "Bearer tok" },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("serializes web search settings updates", async () => {
|
||||
await updateWebSearchSettings("tok", {
|
||||
provider: "searxng",
|
||||
@@ -121,6 +182,20 @@ describe("webui API helpers", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("serializes network safety settings updates", async () => {
|
||||
await updateNetworkSafetySettings("tok", {
|
||||
webuiAllowLocalServiceAccess: false,
|
||||
webuiDefaultAccessMode: "full",
|
||||
});
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
"/api/settings/network-safety/update?webui_allow_local_service_access=false&webui_default_access_mode=full",
|
||||
expect.objectContaining({
|
||||
headers: { Authorization: "Bearer tok" },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("serializes image generation settings updates", async () => {
|
||||
await updateImageGenerationSettings("tok", {
|
||||
enabled: true,
|
||||
@@ -257,6 +332,7 @@ describe("webui API helpers", () => {
|
||||
pinned_keys: ["websocket:chat-1"],
|
||||
archived_keys: ["websocket:old"],
|
||||
title_overrides: { "websocket:chat-1": "Release" },
|
||||
project_name_overrides: { "/Users/me/nanobot": "Core" },
|
||||
tags_by_key: {},
|
||||
collapsed_groups: {},
|
||||
view: {
|
||||
@@ -292,9 +368,39 @@ describe("webui API helpers", () => {
|
||||
expect(JSON.parse(encodedState ?? "{}")).toMatchObject({
|
||||
pinned_keys: ["websocket:chat-1"],
|
||||
title_overrides: { "websocket:chat-1": "Release" },
|
||||
project_name_overrides: { "/Users/me/nanobot": "Core" },
|
||||
});
|
||||
});
|
||||
|
||||
it("fetches workspace project state", async () => {
|
||||
const payload = {
|
||||
schema_version: 1,
|
||||
default_access_mode: "default" as const,
|
||||
default_scope: {
|
||||
project_path: "/tmp/workspace",
|
||||
project_name: "workspace",
|
||||
access_mode: "restricted" as const,
|
||||
restrict_to_workspace: true,
|
||||
},
|
||||
controls: {
|
||||
can_change_project: true,
|
||||
can_use_full_access: true,
|
||||
},
|
||||
};
|
||||
vi.mocked(fetch).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => payload,
|
||||
} as Response);
|
||||
|
||||
await expect(fetchWorkspaces("tok")).resolves.toEqual(payload);
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
"/api/workspaces",
|
||||
expect.objectContaining({
|
||||
headers: { Authorization: "Bearer tok" },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("maps generated session titles from the sessions list", async () => {
|
||||
vi.mocked(fetch).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
|
||||
Reference in New Issue
Block a user