Merge pull request #2670 from langbot-app/test/workspace-quota-boundaries

test(web): verify workspace switching and quota failure boundaries
This commit is contained in:
huanghuoguoguo
2026-10-17 20:12:51 +08:00
committed by GitHub
3 changed files with 339 additions and 15 deletions
+150
View File
@@ -139,3 +139,153 @@ test('loads a Cloud plugin page through the authenticated asset route', async ({
expect(pageApiRequests).toBe(1);
await expect(page.getByText('Loading...')).toHaveCount(0);
});
for (const pendingAsset of [false, true]) {
test(`Workspace switch discards ${pendingAsset ? 'late' : 'loaded'} plugin assets`, async ({
page,
}) => {
await installLangBotApiMocks(page, {
authenticated: true,
workspaces: [
makeWorkspaceEntry('workspace-alpha', 'Alpha Workspace'),
makeWorkspaceEntry('workspace-beta', 'Beta Workspace'),
],
});
await page.route('**/api/v1/plugins', async (route) => {
const scope = route.request().headers()['x-workspace-id'];
expect(['workspace-alpha', 'workspace-beta']).toContain(scope);
const label = scope === 'workspace-alpha' ? 'Alpha page' : 'Beta page';
await route.fulfill({
json: {
code: 0,
data: {
plugins: [
{
install_source: 'marketplace',
install_info: {},
debug: false,
manifest: {
manifest: {
metadata: {
author: 'qa',
name: 'scoped-page',
version: '1.0.0',
label: { en_US: label },
},
spec: {
pages: [
{
id: 'overview',
path: 'overview.html',
label: { en_US: label },
},
],
},
},
},
},
],
},
},
});
});
const assetHeaders: Record<string, string>[] = [];
let releaseAsset = () => {};
const assetGate = new Promise<void>((resolve) => {
releaseAsset = resolve;
});
let assetSettled = () => {};
const assetFinished = new Promise<void>((resolve) => {
assetSettled = resolve;
});
await page.route(
'**/api/v1/plugins/qa/scoped-page/authenticated-assets/overview.html',
async (route) => {
const headers = route.request().headers();
assetHeaders.push(headers);
expect(['workspace-alpha', 'workspace-beta']).toContain(
headers['x-workspace-id'],
);
if (pendingAsset && headers['x-workspace-id'] === 'workspace-alpha') {
await assetGate;
}
const label =
headers['x-workspace-id'] === 'workspace-alpha' ? 'Alpha' : 'Beta';
await route.fulfill({
contentType: 'text/html',
body: `<!doctype html><html><body><h1>${label} private page</h1></body></html>`,
});
if (headers['x-workspace-id'] === 'workspace-alpha') assetSettled();
},
);
try {
await page.goto('/home/plugin-pages?id=qa%2Fscoped-page%2Foverview');
await page.getByRole('button', { name: /Alpha Workspace/ }).click();
await expect(
page.getByRole('button', { name: 'Alpha page' }),
).toBeVisible();
await expect.poll(() => assetHeaders.length).toBeGreaterThan(0);
let previousAssetUrl: string | null = null;
if (!pendingAsset) {
await expect(
page.frameLocator('iframe').getByRole('heading', {
name: 'Alpha private page',
}),
).toBeVisible();
previousAssetUrl = await page.locator('iframe').getAttribute('src');
}
expect(assetHeaders[0]['x-workspace-id']).toBe('workspace-alpha');
expect(assetHeaders[0].authorization).toBe('Bearer playwright-token');
await page.getByRole('button', { name: 'Switch Workspace' }).click();
await page.getByRole('menuitem', { name: /Beta Workspace/ }).click();
const switcher = page.getByRole('button', { name: 'Switch Workspace' });
await expect(page).toHaveURL('/home/monitoring');
await expect(switcher).toContainText('Beta Workspace');
await expect(
page.getByRole('button', { name: 'Beta page' }),
).toBeVisible();
await expect(
page.getByRole('button', { name: 'Alpha page' }),
).toHaveCount(0);
await expect(page.locator('iframe')).toHaveCount(0);
if (previousAssetUrl) {
expect(
await page.evaluate(async (url) => {
try {
await fetch(url);
return true;
} catch {
return false;
}
}, previousAssetUrl),
).toBe(false);
}
const assetsBeforeBeta = assetHeaders.length;
await page.getByRole('button', { name: 'Beta page' }).click();
await expect
.poll(() => assetHeaders.length)
.toBeGreaterThan(assetsBeforeBeta);
expect(assetHeaders.at(-1)?.['x-workspace-id']).toBe('workspace-beta');
expect(assetHeaders.at(-1)?.authorization).toBe(
'Bearer playwright-token',
);
const pluginFrame = page.frameLocator('iframe');
await expect(
pluginFrame.getByRole('heading', { name: 'Beta private page' }),
).toBeVisible();
releaseAsset();
await assetFinished;
await expect(
pluginFrame.getByRole('heading', { name: 'Beta private page' }),
).toBeVisible();
await expect(
pluginFrame.getByRole('heading', { name: 'Alpha private page' }),
).toHaveCount(0);
} finally {
releaseAsset();
}
});
}
+189
View File
@@ -148,3 +148,192 @@ test('quota-reached create actions are disabled and explain the current limit',
),
).toBeVisible();
});
for (const kind of ['plugin', 'skill'] as const) {
test(`GitHub ${kind} confirmation rejects a failed quota check and can retry`, 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_extensions: 3 },
outbound_ips: [],
wizard_status: 'completed',
wizard_progress: null,
}),
);
let checkingQuota = false;
let quotaChecks = 0;
let releaseQuotaCheck!: () => void;
const quotaCheckGate = new Promise<void>((resolve) => {
releaseQuotaCheck = resolve;
});
await page.route('**/api/v1/plugins', async (route) => {
if (checkingQuota) {
quotaChecks += 1;
await quotaCheckGate;
checkingQuota = false;
await route.fulfill({
status: 503,
json: { code: 503, message: 'Quota service unavailable' },
});
return;
}
await fulfill(route, { plugins: [] });
});
const skillUrl =
'https://github.com/quota-fixture/extension/blob/main/SKILL.md';
await page.route('**/api/v1/plugins/github/releases', (route) =>
fulfill(route, {
owner: 'quota-fixture',
repo: 'extension',
releases: [
{
id: 7,
tag_name: 'v1.0.0',
name: 'Quota Test Release',
published_at: '2026-01-01T00:00:00Z',
prerelease: false,
draft: false,
},
],
}),
);
await page.route('**/api/v1/plugins/github/release-assets', (route) =>
fulfill(route, {
assets: [
{
id: 42,
name: 'quota-test.lbpkg',
size: 1024,
download_url: 'https://example.com/quota-test.lbpkg',
content_type: 'application/octet-stream',
},
],
}),
);
await page.route('**/api/v1/skills/install/github/preview', (route) =>
fulfill(route, {
skills: [
{
name: 'quota-test-skill',
display_name: 'Quota Test Skill',
description: 'Skill used to verify quota confirmation',
instructions: '# Quota Test Skill',
package_root: '.',
},
],
}),
);
const installRequests: { path: string; method: string; body: unknown }[] =
[];
await page.route(
/\/api\/v1\/(plugins|skills)\/install\/github$/,
(route) => {
const request = route.request();
installRequests.push({
path: new URL(request.url()).pathname,
method: request.method(),
body: request.postDataJSON(),
});
return fulfill(
route,
kind === 'plugin' ? { task_id: 'quota-test-task' } : { skills: [] },
);
},
);
await page.goto('/home/add-extension');
await page.getByRole('button', { name: 'Manual Add', exact: true }).click();
await page.getByRole('button', { name: 'Install from GitHub' }).click();
await page
.getByPlaceholder('GitHub repository, Release, or SKILL.md link')
.fill(
kind === 'plugin'
? 'https://github.com/quota-fixture/extension'
: skillUrl,
);
const confirm = page.getByRole('button', { name: 'Confirm', exact: true });
await confirm.click();
if (kind === 'plugin') {
await page.getByText('Quota Test Release', { exact: true }).click();
await page.getByText('quota-test.lbpkg', { exact: true }).click();
} else {
await expect(
page.getByText('Quota Test Skill', { exact: true }),
).toBeVisible();
}
checkingQuota = true;
await confirm.click();
try {
await expect.poll(() => quotaChecks).toBe(1);
await expect(
page.getByText(`Installing ${kind}...`, { exact: true }),
).toBeVisible();
await expect(confirm).toHaveCount(0);
await page.keyboard.press('Enter');
expect(quotaChecks).toBe(1);
expect(installRequests).toEqual([]);
} finally {
releaseQuotaCheck();
}
await expect(
page.getByText(
'Unable to verify the current workspace quota. Please try again.',
),
).toBeVisible();
expect(installRequests).toEqual([]);
await expect(confirm).toBeEnabled();
await confirm.click();
await expect.poll(() => installRequests.length).toBe(1);
await expect(
page.getByRole('button', {
name: 'Manual Add',
exact: true,
includeHidden: true,
}),
).toHaveAttribute('aria-expanded', 'false');
await expect(
page.getByPlaceholder('GitHub repository, Release, or SKILL.md link'),
).toBeHidden();
expect(installRequests).toEqual([
{
path: `/api/v1/${kind === 'plugin' ? 'plugins' : 'skills'}/install/github`,
method: 'POST',
body:
kind === 'plugin'
? {
asset_id: 42,
release_id: 7,
owner: 'quota-fixture',
repo: 'extension',
release_tag: 'v1.0.0',
}
: {
asset_url: skillUrl,
owner: 'quota-fixture',
repo: 'extension',
release_tag: 'main',
},
},
]);
if (kind === 'skill') {
await expect(
page.getByText('Skill installed successfully', { exact: true }),
).toBeVisible();
}
});
}
@@ -99,21 +99,6 @@ test('add-extension page disables all install entry points at the quota', () =>
assert.match(page, /quota=\{extensionQuota\}/);
});
test('extension confirmation checks fail closed and enter an in-flight state first', () => {
const page = fs.readFileSync(addExtensionPath, 'utf8');
assert.match(page, /limitation\.quotaCheckFailed/);
assert.doesNotMatch(page, /If we can't check, let backend handle it/);
assert.match(
page,
/setGithubInstallStatus\(GithubInstallStatus\.INSTALLING\);\s+if \(!\(await checkExtensionsLimit\(\)\)\)/,
);
assert.match(
page,
/setGithubInstallStatus\(GithubInstallStatus\.SKILL_INSTALLING\);\s+if \(!\(await checkExtensionsLimit\(\)\)\)/,
);
});
test('quota tooltip copy is localized in Simplified Chinese', () => {
const source = fs.readFileSync(zhPath, 'utf8');
assert.match(source, /createDisabledTooltip/);