mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-09 12:40:59 +00:00
Merge remote-tracking branch 'origin/master' into dev/4.11.x
# Conflicts: # pyproject.toml # src/langbot/pkg/pipeline/controller.py # uv.lock # web/src/app/home/bots/components/bot-form/BotForm.tsx # web/src/app/home/components/home-sidebar/HomeSidebar.tsx # web/src/app/home/components/home-sidebar/SidebarDataContext.tsx # web/src/app/home/plugin-pages/page.tsx # web/src/app/infra/entities/api/index.ts
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);
|
||||
});
|
||||
@@ -170,7 +170,6 @@ export function makeWorkspaceEntry(
|
||||
'member.remove',
|
||||
'member.update_role',
|
||||
'member.view',
|
||||
'owner.transfer',
|
||||
'provider_secret.manage',
|
||||
'resource.manage',
|
||||
'resource.view',
|
||||
|
||||
@@ -165,3 +165,164 @@ test('an authenticated OSS invitation requires logout before registration', asyn
|
||||
invitation: 'logout-invitation',
|
||||
});
|
||||
});
|
||||
|
||||
test('an authenticated Cloud Account can accept its invitation directly', async ({
|
||||
page,
|
||||
}) => {
|
||||
await installLangBotApiMocks(page, {
|
||||
authenticated: true,
|
||||
storage: {
|
||||
token: 'invited-account-token',
|
||||
userEmail: 'invited@example.com',
|
||||
},
|
||||
});
|
||||
await page.route('**/api/v1/user/account-info', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
code: 0,
|
||||
data: {
|
||||
initialized: true,
|
||||
authenticated_invitation_acceptance_enabled: true,
|
||||
password_login_enabled: false,
|
||||
space_login_enabled: true,
|
||||
},
|
||||
msg: 'ok',
|
||||
}),
|
||||
});
|
||||
});
|
||||
await page.route('**/api/v1/invitations/inspect', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
code: 0,
|
||||
data: {
|
||||
invitation: {
|
||||
uuid: 'cloud-invitation',
|
||||
workspace_uuid: 'workspace-playwright',
|
||||
normalized_email: 'invited@example.com',
|
||||
role: 'viewer',
|
||||
status: 'pending',
|
||||
},
|
||||
workspace: {
|
||||
uuid: 'workspace-playwright',
|
||||
name: 'Playwright Workspace',
|
||||
},
|
||||
},
|
||||
msg: 'ok',
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
let acceptanceAuthorization = '';
|
||||
await page.route('**/api/v1/invitations/accept', async (route) => {
|
||||
acceptanceAuthorization = route.request().headers().authorization ?? '';
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
code: 0,
|
||||
data: {
|
||||
token: 'accepted-cloud-account-token',
|
||||
workspace_uuid: 'workspace-playwright',
|
||||
},
|
||||
msg: 'ok',
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/invitations/accept#token=cloud-invitation');
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Accept Invitation' }),
|
||||
).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Accept Invitation' }).click();
|
||||
await expect(page).toHaveURL(/\/home(?:\/monitoring)?$/);
|
||||
expect(acceptanceAuthorization).toBe('Bearer invited-account-token');
|
||||
});
|
||||
|
||||
test('Space OAuth accepts a pending invitation with the freshly authenticated account', async ({
|
||||
page,
|
||||
}) => {
|
||||
await installLangBotApiMocks(page, {
|
||||
authenticated: false,
|
||||
storage: {
|
||||
token: 'stale-other-account-token',
|
||||
userEmail: 'other@example.com',
|
||||
},
|
||||
});
|
||||
await page.addInitScript(() => {
|
||||
sessionStorage.setItem(
|
||||
'langbot_pending_invitation_token',
|
||||
'matching-invitation',
|
||||
);
|
||||
});
|
||||
|
||||
await page.route('**/api/v1/user/space/callback', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
code: 0,
|
||||
data: {
|
||||
token: 'fresh-invited-account-token',
|
||||
user: 'invited@example.com',
|
||||
},
|
||||
msg: 'ok',
|
||||
}),
|
||||
});
|
||||
});
|
||||
await page.route('**/api/v1/user/info', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
code: 0,
|
||||
data: {
|
||||
account_uuid: 'invited-account',
|
||||
user: 'invited@example.com',
|
||||
account_type: 'space',
|
||||
has_password: false,
|
||||
},
|
||||
msg: 'ok',
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
let acceptanceAuthorization = '';
|
||||
await page.route('**/api/v1/invitations/accept', async (route) => {
|
||||
acceptanceAuthorization = route.request().headers().authorization ?? '';
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
code: 0,
|
||||
data: {
|
||||
token: 'accepted-invited-account-token',
|
||||
workspace_uuid: 'workspace-playwright',
|
||||
},
|
||||
msg: 'ok',
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/auth/space/callback?code=oauth-code&state=oauth-state');
|
||||
|
||||
await expect(page).toHaveURL(/\/home(?:\/monitoring)?$/, {
|
||||
timeout: 5_000,
|
||||
});
|
||||
expect(acceptanceAuthorization).toBe('Bearer fresh-invited-account-token');
|
||||
expect(
|
||||
await page.evaluate(() => ({
|
||||
token: localStorage.getItem('token'),
|
||||
userEmail: localStorage.getItem('userEmail'),
|
||||
invitation: sessionStorage.getItem('langbot_pending_invitation_token'),
|
||||
})),
|
||||
).toEqual({
|
||||
token: 'accepted-invited-account-token',
|
||||
userEmail: 'invited@example.com',
|
||||
invitation: null,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,149 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { installLangBotApiMocks } from './fixtures/langbot-api';
|
||||
|
||||
function wrapped(data: unknown) {
|
||||
return JSON.stringify({
|
||||
code: 0,
|
||||
message: 'ok',
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
async function fulfill(
|
||||
route: Parameters<Parameters<import('@playwright/test').Page['route']>[1]>[0],
|
||||
data: unknown,
|
||||
) {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: wrapped(data),
|
||||
});
|
||||
}
|
||||
|
||||
test('quota-reached create actions are disabled and explain the current limit', async ({
|
||||
page,
|
||||
}) => {
|
||||
await installLangBotApiMocks(page, { authenticated: true });
|
||||
|
||||
await page.route('**/api/v1/system/info', (route) =>
|
||||
fulfill(route, {
|
||||
debug: false,
|
||||
version: 'quota-e2e',
|
||||
edition: 'community',
|
||||
cloud_service_url: 'https://space.langbot.app',
|
||||
enable_marketplace: true,
|
||||
allow_modify_login_info: true,
|
||||
disable_models_service: false,
|
||||
limitation: {
|
||||
max_bots: 2,
|
||||
max_pipelines: 3,
|
||||
max_extensions: 3,
|
||||
max_knowledge_bases: 2,
|
||||
},
|
||||
outbound_ips: [],
|
||||
wizard_status: 'completed',
|
||||
wizard_progress: null,
|
||||
}),
|
||||
);
|
||||
await page.route('**/api/v1/platform/bots**', (route) =>
|
||||
fulfill(route, {
|
||||
bots: Array.from({ length: 2 }, (_, index) => ({
|
||||
uuid: `bot-${index}`,
|
||||
name: `Bot ${index + 1}`,
|
||||
description: '',
|
||||
adapter: 'aiocqhttp',
|
||||
enable: true,
|
||||
updated_at: new Date().toISOString(),
|
||||
})),
|
||||
}),
|
||||
);
|
||||
await page.route('**/api/v1/pipelines**', (route) =>
|
||||
fulfill(route, {
|
||||
pipelines: Array.from({ length: 3 }, (_, index) => ({
|
||||
uuid: `pipeline-${index}`,
|
||||
name: `Pipeline ${index + 1}`,
|
||||
description: '',
|
||||
emoji: '⚙️',
|
||||
updated_at: new Date().toISOString(),
|
||||
})),
|
||||
}),
|
||||
);
|
||||
await page.route('**/api/v1/knowledge/bases**', (route) =>
|
||||
fulfill(route, {
|
||||
bases: Array.from({ length: 2 }, (_, index) => ({
|
||||
uuid: `kb-${index}`,
|
||||
name: `Knowledge ${index + 1}`,
|
||||
description: '',
|
||||
emoji: '📚',
|
||||
updated_at: new Date().toISOString(),
|
||||
})),
|
||||
}),
|
||||
);
|
||||
await page.route('**/api/v1/plugins**', (route) =>
|
||||
fulfill(route, { plugins: [] }),
|
||||
);
|
||||
await page.route('**/api/v1/mcp/servers**', (route) =>
|
||||
fulfill(route, {
|
||||
servers: Array.from({ length: 3 }, (_, index) => ({
|
||||
name: `mcp-${index}`,
|
||||
mode: 'http',
|
||||
enable: true,
|
||||
runtime_info: { status: 'connected' },
|
||||
})),
|
||||
}),
|
||||
);
|
||||
await page.route('**/api/v1/skills**', (route) =>
|
||||
fulfill(route, { skills: [] }),
|
||||
);
|
||||
|
||||
await page.goto('/home/bots');
|
||||
|
||||
const botCreate = page.getByRole('button', {
|
||||
name: 'Create Bots',
|
||||
exact: true,
|
||||
});
|
||||
const pipelineCreate = page.getByRole('button', {
|
||||
name: 'Create Pipelines',
|
||||
exact: true,
|
||||
});
|
||||
const knowledgeCreate = page.getByRole('button', {
|
||||
name: 'Create Knowledge',
|
||||
exact: true,
|
||||
});
|
||||
const addExtension = page.getByRole('button', {
|
||||
name: 'Add Extension',
|
||||
exact: true,
|
||||
});
|
||||
|
||||
await expect(botCreate).toBeDisabled();
|
||||
await expect(pipelineCreate).toBeDisabled();
|
||||
await expect(knowledgeCreate).toBeDisabled();
|
||||
await expect(addExtension).toBeEnabled();
|
||||
|
||||
const botQuotaTrigger = botCreate.locator('..');
|
||||
await botQuotaTrigger.hover();
|
||||
await expect(
|
||||
page.getByText(
|
||||
'The Bots limit (2) for this workspace has been reached. Delete one existing item before creating another.',
|
||||
),
|
||||
).toBeVisible();
|
||||
await botQuotaTrigger.focus();
|
||||
await expect(botQuotaTrigger).toBeFocused();
|
||||
await expect(
|
||||
page.getByText(
|
||||
'The Bots limit (2) for this workspace has been reached. Delete one existing item before creating another.',
|
||||
),
|
||||
).toBeVisible();
|
||||
|
||||
await addExtension.click();
|
||||
await expect(page).toHaveURL(/\/home\/add-extension$/);
|
||||
const manualAdd = page.getByRole('button', { name: 'Manual Add' });
|
||||
await expect(manualAdd).toBeDisabled();
|
||||
await manualAdd.locator('..').hover();
|
||||
await expect(
|
||||
page.getByText(
|
||||
'The Extensions limit (3) for this workspace has been reached. Delete one existing item before creating another.',
|
||||
),
|
||||
).toBeVisible();
|
||||
});
|
||||
Reference in New Issue
Block a user