mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-09 04:40:57 +00:00
[verified] fix: harden OSS and Cloud workspace UI (#2387)
Co-authored-by: dadachann <185672915+dadachann@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import {
|
||||
installLangBotApiMocks,
|
||||
makeWorkspaceEntry,
|
||||
} from './fixtures/langbot-api';
|
||||
|
||||
function wrapped(data: unknown) {
|
||||
return JSON.stringify({
|
||||
code: 0,
|
||||
message: 'ok',
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
test('Cloud never exposes or requests storage analysis', async ({ page }) => {
|
||||
const workspace = makeWorkspaceEntry(
|
||||
'workspace-cloud',
|
||||
'Cloud Workspace',
|
||||
'cloud_projection',
|
||||
);
|
||||
await installLangBotApiMocks(page, {
|
||||
authenticated: true,
|
||||
workspaces: [workspace],
|
||||
});
|
||||
await page.route(
|
||||
/\/api\/v1\/workspaces\/workspace-cloud\/(members|invitations)$/,
|
||||
async (route) => {
|
||||
const collection = route.request().url().endsWith('/members')
|
||||
? 'members'
|
||||
: 'invitations';
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: wrapped({ [collection]: [] }),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let storageAnalysisRequests = 0;
|
||||
await page.route('**/api/v1/system/storage-analysis', async (route) => {
|
||||
storageAnalysisRequests += 1;
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: wrapped({}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/home/bots');
|
||||
await page.getByRole('button', { name: /admin@example\.com/i }).click();
|
||||
await expect(page.getByText('Storage Analysis', { exact: true })).toHaveCount(
|
||||
0,
|
||||
);
|
||||
|
||||
await page.goto('/home/bots?action=showStorageAnalysis');
|
||||
await expect(page.getByRole('dialog')).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Workspace' })).toBeVisible();
|
||||
await expect(page.getByText('Storage Analysis', { exact: true })).toHaveCount(
|
||||
0,
|
||||
);
|
||||
expect(storageAnalysisRequests).toBe(0);
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import {
|
||||
installLangBotApiMocks,
|
||||
makeWorkspaceEntry,
|
||||
} from './fixtures/langbot-api';
|
||||
|
||||
function wrapped(data: unknown) {
|
||||
return JSON.stringify({
|
||||
code: 0,
|
||||
message: 'ok',
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
test('loads a Cloud plugin page through the authenticated asset route', async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = makeWorkspaceEntry(
|
||||
'workspace-cloud',
|
||||
'Cloud Workspace',
|
||||
'cloud_projection',
|
||||
);
|
||||
await installLangBotApiMocks(page, {
|
||||
authenticated: true,
|
||||
workspaces: [workspace],
|
||||
});
|
||||
|
||||
await page.route('**/api/v1/plugins', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: wrapped({
|
||||
plugins: [
|
||||
{
|
||||
install_source: 'marketplace',
|
||||
install_info: {},
|
||||
debug: false,
|
||||
manifest: {
|
||||
manifest: {
|
||||
metadata: {
|
||||
author: 'langbot-team',
|
||||
name: 'LangRAG',
|
||||
version: '0.1.9',
|
||||
label: { en_US: 'LangRAG', zh_Hans: 'LangRAG' },
|
||||
},
|
||||
spec: {
|
||||
pages: [
|
||||
{
|
||||
id: 'observability',
|
||||
path: 'components/pages/observability.html',
|
||||
label: { en_US: 'Observability', zh_Hans: '观测面板' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
let authenticatedAssetRequests = 0;
|
||||
await page.route(
|
||||
'**/api/v1/plugins/langbot-team/LangRAG/authenticated-assets/**',
|
||||
async (route) => {
|
||||
authenticatedAssetRequests += 1;
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'text/html',
|
||||
body: '<!doctype html><html><body><h1>LangRAG Observability</h1></body></html>',
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
await page.goto(
|
||||
'/home/plugin-pages?id=langbot-team%2FLangRAG%2Fobservability',
|
||||
);
|
||||
|
||||
await expect(
|
||||
page
|
||||
.frameLocator('iframe')
|
||||
.getByRole('heading', { name: 'LangRAG Observability' }),
|
||||
).toBeVisible();
|
||||
expect(authenticatedAssetRequests).toBeGreaterThan(0);
|
||||
await expect(page.getByText('Loading...')).toHaveCount(0);
|
||||
});
|
||||
@@ -0,0 +1,100 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const currentDirectory = path.dirname(fileURLToPath(import.meta.url));
|
||||
const webRoot = path.resolve(currentDirectory, '../..');
|
||||
|
||||
function readSource(relativePath) {
|
||||
return fs.readFileSync(path.join(webRoot, relativePath), 'utf8');
|
||||
}
|
||||
|
||||
const homeSidebarSource = readSource(
|
||||
'src/app/home/components/home-sidebar/HomeSidebar.tsx',
|
||||
);
|
||||
const botFormSource = readSource(
|
||||
'src/app/home/bots/components/bot-form/BotForm.tsx',
|
||||
);
|
||||
const kbFormSource = readSource(
|
||||
'src/app/home/knowledge/components/kb-form/KBForm.tsx',
|
||||
);
|
||||
const settingsDialogSource = readSource(
|
||||
'src/app/home/components/settings-dialog/SettingsDialog.tsx',
|
||||
);
|
||||
const pluginPageSource = readSource('src/app/home/plugin-pages/page.tsx');
|
||||
const authenticatedPluginResourceSource = readSource(
|
||||
'src/hooks/useAuthenticatedPluginResource.ts',
|
||||
);
|
||||
|
||||
test('hides the entire workspace switcher slot for a singleton local workspace', () => {
|
||||
assert.match(homeSidebarSource, /useWorkspaceBootstrap/);
|
||||
assert.match(
|
||||
homeSidebarSource,
|
||||
/const showWorkspaceSwitcher\s*=\s*workspaces\.length\s*>\s*1\s*\|\|\s*currentWorkspace\?\.workspace\.source\s*===\s*'cloud_projection'/,
|
||||
);
|
||||
assert.match(
|
||||
homeSidebarSource,
|
||||
/\{showWorkspaceSwitcher\s*&&\s*\(\s*<div className="px-2[^>]*>\s*<WorkspaceSwitcher/,
|
||||
);
|
||||
});
|
||||
|
||||
test('keeps bot cards at the same vertical spacing as knowledge-base cards', () => {
|
||||
assert.match(
|
||||
botFormSource,
|
||||
/<fieldset className="space-y-6" disabled=\{isLoading\}>/,
|
||||
);
|
||||
assert.match(kbFormSource, /<form[\s\S]*?className="space-y-6"/);
|
||||
});
|
||||
|
||||
test('does not expose storage analysis in Cloud settings or via a deep link', () => {
|
||||
assert.match(
|
||||
homeSidebarSource,
|
||||
/canViewStorageAnalysis\s*&&\s*\(\s*<DropdownMenuItem[\s\S]*?openSettings\('storageAnalysis'\)/,
|
||||
);
|
||||
assert.match(
|
||||
settingsDialogSource,
|
||||
/const canViewStorageAnalysis\s*=\s*currentWorkspace\?\.workspace\.source\s*!==\s*'cloud_projection'\s*&&\s*canViewAudit/,
|
||||
);
|
||||
assert.match(
|
||||
settingsDialogSource,
|
||||
/item\.id === 'storageAnalysis'[\s\S]*?return canViewStorageAnalysis/,
|
||||
);
|
||||
assert.match(
|
||||
settingsDialogSource,
|
||||
/section === 'storageAnalysis' && !canViewStorageAnalysis/,
|
||||
);
|
||||
assert.match(
|
||||
settingsDialogSource,
|
||||
/section === 'storageAnalysis' &&\s*canViewStorageAnalysis &&\s*\(\s*<StorageAnalysisPanel/,
|
||||
);
|
||||
});
|
||||
|
||||
test('loads plugin pages through the authenticated Workspace-scoped asset route', () => {
|
||||
assert.match(pluginPageSource, /useAuthenticatedPluginAsset/);
|
||||
assert.match(
|
||||
pluginPageSource,
|
||||
/useAuthenticatedPluginAsset\(\s*author,\s*pluginName,\s*pagePath,?\s*\)/,
|
||||
);
|
||||
assert.match(pluginPageSource, /src=\{assetUrl\}/);
|
||||
assert.doesNotMatch(pluginPageSource, /getPluginAssetURL\(/);
|
||||
assert.match(pluginPageSource, /plugins\.loadFailed/);
|
||||
assert.match(pluginPageSource, /loadedAssetUrl !== assetUrl/);
|
||||
});
|
||||
|
||||
test('revokes and reloads authenticated plugin resources when the Workspace changes', () => {
|
||||
assert.match(authenticatedPluginResourceSource, /useCurrentWorkspace/);
|
||||
assert.match(
|
||||
authenticatedPluginResourceSource,
|
||||
/const workspaceUuid = currentWorkspace\?\.workspace\.uuid;/,
|
||||
);
|
||||
assert.match(
|
||||
authenticatedPluginResourceSource,
|
||||
/\[author, name, filepath, resourceKey\]/,
|
||||
);
|
||||
assert.match(
|
||||
authenticatedPluginResourceSource,
|
||||
/resource\.key === resourceKey \? resource\.url : ''/,
|
||||
);
|
||||
});
|
||||
@@ -50,15 +50,22 @@ test('places WorkspaceSwitcher between the sidebar header and Home navigation',
|
||||
);
|
||||
});
|
||||
|
||||
test('shows WorkspaceSwitcher for a current Cloud or OSS workspace even when it is the only workspace', () => {
|
||||
test('hides WorkspaceSwitcher for the singleton local OSS workspace and keeps it for Cloud or multiple workspaces', () => {
|
||||
assert.match(
|
||||
workspaceSwitcherSource,
|
||||
/if \(!currentWorkspace\) return null;/,
|
||||
);
|
||||
assert.doesNotMatch(workspaceSwitcherSource, /workspaces\.length\s*<=\s*1/);
|
||||
assert.doesNotMatch(
|
||||
assert.match(
|
||||
homeSidebarSource,
|
||||
/currentWorkspace\?\.workspace\.source\s*===\s*'cloud_projection'[\s\S]{0,200}<WorkspaceSwitcher/,
|
||||
/const workspaces = useWorkspaceBootstrap\(\);/,
|
||||
);
|
||||
assert.match(
|
||||
homeSidebarSource,
|
||||
/const showWorkspaceSwitcher\s*=\s*workspaces\.length\s*>\s*1\s*\|\|\s*currentWorkspace\?\.workspace\.source\s*===\s*'cloud_projection'/,
|
||||
);
|
||||
assert.match(
|
||||
homeSidebarSource,
|
||||
/\{showWorkspaceSwitcher\s*&&\s*\(\s*<div className="px-2[^>]*>[\s\S]*?<WorkspaceSwitcher/,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user