diff --git a/web/tests/e2e/account-oauth.spec.ts b/web/tests/e2e/account-oauth.spec.ts
new file mode 100644
index 000000000..927fb0fda
--- /dev/null
+++ b/web/tests/e2e/account-oauth.spec.ts
@@ -0,0 +1,103 @@
+import { expect, test } from '@playwright/test';
+import { installLangBotApiMocks } from './fixtures/langbot-api';
+
+for (const invitation of [false, true]) {
+ test(`Cloud login starts standard OAuth, invitation=${invitation}`, async ({
+ page,
+ }) => {
+ await installLangBotApiMocks(page);
+ await page.route('**/api/v1/system/info', (route) =>
+ route.fulfill({
+ json: {
+ code: 0,
+ data: {
+ edition: 'cloud',
+ deployment_mode: 'cloud',
+ cloud_service_url: 'https://space.langbot.app',
+ wizard_status: 'completed',
+ },
+ },
+ }),
+ );
+ await page.route('**/api/v1/user/account-info', (route) =>
+ route.fulfill({
+ json: {
+ code: 0,
+ data: {
+ initialized: true,
+ password_login_enabled: false,
+ space_login_enabled: true,
+ authenticated_invitation_acceptance_enabled: true,
+ },
+ },
+ }),
+ );
+ if (invitation)
+ await page.addInitScript(() =>
+ sessionStorage.setItem(
+ 'langbot_pending_invitation_token',
+ 'invited-token',
+ ),
+ );
+ let redirectUri: string | null = null;
+ await page.route('**/api/v1/user/space/authorize-url**', async (route) => {
+ redirectUri = new URL(route.request().url()).searchParams.get(
+ 'redirect_uri',
+ );
+ await route.fulfill({
+ json: { code: 0, data: { authorize_url: '/oauth-provider' } },
+ });
+ });
+ await page.route('**/oauth-provider', (route) =>
+ route.fulfill({
+ contentType: 'text/html',
+ body: '
OAuth provider
',
+ }),
+ );
+ await page.goto(invitation ? '/login?invitation=1' : '/login');
+ await page
+ .getByRole('button', { name: 'Login with LangBot Account', exact: true })
+ .click();
+ await expect(page).toHaveURL(/\/oauth-provider$/);
+ expect(redirectUri).toBe(new URL('/auth/space/callback', page.url()).href);
+ expect(
+ await page.evaluate(() =>
+ sessionStorage.getItem('langbot_pending_invitation_token'),
+ ),
+ ).toBe(invitation ? 'invited-token' : null);
+ });
+}
+
+for (const [code, title, message] of [
+ [
+ 'space_account_not_registered',
+ 'Account not registered',
+ 'No local account is registered for this LangBot Account email. Ask the Workspace owner for an invitation.',
+ ],
+ [
+ 'space_account_binding_required',
+ 'LangBot Account connection required',
+ 'This local account must connect a LangBot Account from Account settings before using LangBot Account login.',
+ ],
+]) {
+ test(`OAuth callback explains ${code}`, async ({ page }) => {
+ await installLangBotApiMocks(page);
+ await page.route('**/api/v1/user/space/callback', (route) =>
+ route.fulfill({
+ status: 403,
+ json: { code, msg: 'Generic upstream message' },
+ }),
+ );
+ await page.goto('/auth/space/callback?code=code&state=state');
+ await expect(page.getByText(title, { exact: true })).toBeVisible();
+ await expect(page.getByText(message, { exact: true })).toBeVisible();
+ await expect(
+ page.getByText('Generic upstream message', { exact: true }),
+ ).toHaveCount(0);
+ await page
+ .getByRole('button', { name: 'Back to Login', exact: true })
+ .click();
+ await expect(page).toHaveURL(/\/login$/);
+ expect(await page.evaluate(() => localStorage.getItem('token'))).toBeNull();
+ });
+}
diff --git a/web/tests/e2e/bot-form-layout.spec.ts b/web/tests/e2e/bot-form-layout.spec.ts
new file mode 100644
index 000000000..978aad807
--- /dev/null
+++ b/web/tests/e2e/bot-form-layout.spec.ts
@@ -0,0 +1,133 @@
+import { expect, test } from '@playwright/test';
+import { installLangBotApiMocks } from './fixtures/langbot-api';
+
+for (const width of [1440, 390]) {
+ test(`bot setup and detail columns stay separated at ${width}px`, async ({
+ page,
+ }) => {
+ await page.setViewportSize({ width, height: 1000 });
+ await installLangBotApiMocks(page, { authenticated: true });
+ await page.goto('/home/bots?id=new');
+ const layout = page.locator('#bot-form > fieldset');
+ const basic = layout.locator('[data-guide="bot-basic"]');
+ const adapter = page.getByTestId('adapter-gallery');
+ await expect(basic).toBeVisible();
+ await expect(adapter).toBeVisible();
+ const basicBox = (await basic.boundingBox())!;
+ const adapterBox = (await adapter.boundingBox())!;
+ if (width >= 1024)
+ expect(adapterBox.x).toBeGreaterThan(basicBox.x + basicBox.width);
+ else expect(adapterBox.y).toBeGreaterThan(basicBox.y + basicBox.height);
+ await adapter.getByRole('button', { name: /Playwright Adapter/ }).click();
+ await page.locator('input[name="name"]').fill('Layout bot');
+ await page.getByRole('button', { name: /^Submit$/ }).click();
+ await expect(page).toHaveURL(/\/home\/bots\?id=bot-1$/);
+ const routing = page.locator('[data-guide="bot-routing"]');
+ await expect(routing).toBeVisible();
+ const cards = await layout.locator(':scope > [data-slot="card"]').all();
+ expect(cards).toHaveLength(2);
+ const left = (await cards[0].boundingBox())!;
+ const right = (await cards[1].boundingBox())!;
+ if (width >= 1024) expect(right.x).toBeGreaterThan(left.x + left.width);
+ else expect(right.y).toBeGreaterThan(left.y + left.height);
+ expect(
+ await page.evaluate(
+ () => document.documentElement.scrollWidth <= innerWidth,
+ ),
+ ).toBe(true);
+ await page.goto('/home/knowledge?id=new');
+ const form = page.locator('#kb-form');
+ await expect(form).toBeVisible();
+ const children = await form.locator(':scope > *').all();
+ expect(children.length).toBeGreaterThan(1);
+ for (let i = 1; i < children.length; i++) {
+ const previous = await children[i - 1].boundingBox();
+ const current = await children[i].boundingBox();
+ if (previous && current)
+ expect(current.y - previous.y - previous.height).toBeGreaterThanOrEqual(
+ 20,
+ );
+ }
+ });
+}
+
+test('the dedicated connection selector hides its manifest controller and persists its value', async ({
+ page,
+}) => {
+ await installLangBotApiMocks(page, { authenticated: true });
+ await page.route('**/api/v1/platform/adapters', (route) =>
+ route.fulfill({
+ json: {
+ code: 0,
+ data: {
+ adapters: [
+ {
+ name: 'connection-adapter',
+ label: { en_US: 'Connection adapter' },
+ description: { en_US: 'Both connection modes' },
+ spec: {
+ categories: ['testing'],
+ config: [
+ {
+ name: 'transport',
+ label: { en_US: 'Raw transport controller' },
+ type: 'select',
+ required: true,
+ default: 'persistent',
+ options: [
+ { name: 'persistent', label: { en_US: 'Persistent' } },
+ {
+ name: 'webhook',
+ label: { en_US: 'Webhook transport' },
+ },
+ ],
+ },
+ {
+ name: 'webhook_url',
+ label: { en_US: 'Webhook address' },
+ type: 'webhook-url',
+ show_if: {
+ field: 'transport',
+ operator: 'eq',
+ value: 'webhook',
+ },
+ },
+ {
+ name: 'credential',
+ label: { en_US: 'Adapter credential' },
+ type: 'string',
+ required: true,
+ default: '',
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ }),
+ );
+ await page.goto('/home/bots?id=new');
+ await page
+ .getByTestId('adapter-gallery')
+ .getByRole('button', { name: /Connection adapter/ })
+ .click();
+ await page.getByRole('button', { name: /^Submit$/ }).click();
+ await expect(page).toHaveURL(/\/home\/bots\?id=bot-1$/);
+ await page.getByRole('radio', { name: /^Webhook/ }).click();
+ await expect(page.getByText(/^Raw transport controller/)).toHaveCount(0);
+ await expect(
+ page.getByText('Webhook address', { exact: true }),
+ ).toBeVisible();
+ await page.locator('input[name="credential"]').fill('persist-me');
+ const save = page.waitForRequest(
+ (request) =>
+ request.method() === 'PUT' &&
+ request.url().endsWith('/api/v1/platform/bots/bot-1'),
+ );
+ await page.getByRole('button', { name: /^Save and enable$/ }).click();
+ expect((await save).postDataJSON().adapter_config).toEqual({
+ transport: 'webhook',
+ credential: 'persist-me',
+ });
+});
diff --git a/web/tests/e2e/cloud-storage-privacy.spec.ts b/web/tests/e2e/cloud-storage-privacy.spec.ts
index c28cf24a0..d9c69edff 100644
--- a/web/tests/e2e/cloud-storage-privacy.spec.ts
+++ b/web/tests/e2e/cloud-storage-privacy.spec.ts
@@ -61,3 +61,52 @@ test('Cloud never exposes or requests storage analysis', async ({ page }) => {
);
expect(storageAnalysisRequests).toBe(0);
});
+
+test('an OSS owner opens storage analysis from its storage-icon menu item', async ({
+ page,
+}) => {
+ await installLangBotApiMocks(page, { authenticated: true });
+ let requests = 0;
+ await page.route('**/api/v1/system/storage-analysis', async (route) => {
+ requests++;
+ await route.fulfill({
+ json: {
+ code: 0,
+ data: {
+ generated_at: '2026-10-17T00:00:00Z',
+ total_size_bytes: 123,
+ sections: [],
+ cleanup_policy: {
+ uploaded_file_retention_days: 7,
+ log_retention_days: 14,
+ },
+ database: {
+ type: 'sqlite',
+ monitoring_counts: {},
+ binary_storage: { count: 0, size_bytes: 0 },
+ },
+ cleanup_candidates: { uploaded_files: [], log_files: [] },
+ tasks: {},
+ },
+ },
+ });
+ });
+ await page.goto('/home/bots');
+ await page.getByRole('button', { name: /admin@example\.com/i }).click();
+ const menuItem = page.getByRole('menuitem', {
+ name: 'Storage Analysis',
+ exact: true,
+ });
+ await expect(menuItem).toBeVisible();
+ await expect(menuItem.locator('svg.lucide-hard-drive')).toBeVisible();
+ await menuItem.click();
+ await expect(
+ page
+ .getByRole('dialog')
+ .getByRole('button', { name: 'Refresh', exact: true }),
+ ).toBeVisible();
+ await expect(
+ page.getByRole('dialog').getByText('123 B', { exact: true }),
+ ).toBeVisible();
+ await expect.poll(() => requests).toBeGreaterThan(0);
+});
diff --git a/web/tests/e2e/invitations.spec.ts b/web/tests/e2e/invitations.spec.ts
index b9f77cfb4..ccf3908f8 100644
--- a/web/tests/e2e/invitations.spec.ts
+++ b/web/tests/e2e/invitations.spec.ts
@@ -214,6 +214,7 @@ test('an OAuth-only OSS instance registers the invited email with a local passwo
email: 'invited@example.com',
password: 'invite-password-123',
});
+ expect(await page.evaluate(() => localStorage.getItem('token'))).toBeNull();
});
test('an authenticated OSS invitation requires logout before registration', async ({
@@ -402,8 +403,10 @@ test('Space OAuth accepts a pending invitation with the freshly authenticated ac
});
let acceptanceAuthorization = '';
+ let acceptanceBody: unknown;
await page.route('**/api/v1/invitations/accept', async (route) => {
acceptanceAuthorization = route.request().headers().authorization ?? '';
+ acceptanceBody = route.request().postDataJSON();
await route.fulfill({
status: 200,
contentType: 'application/json',
@@ -424,6 +427,7 @@ test('Space OAuth accepts a pending invitation with the freshly authenticated ac
timeout: 5_000,
});
expect(acceptanceAuthorization).toBe('Bearer fresh-invited-account-token');
+ expect(acceptanceBody).toEqual({ token: 'matching-invitation' });
expect(
await page.evaluate(() => ({
token: localStorage.getItem('token'),
diff --git a/web/tests/e2e/owner-space-billing.spec.ts b/web/tests/e2e/owner-space-billing.spec.ts
index 4db78e3f6..60be5d490 100644
--- a/web/tests/e2e/owner-space-billing.spec.ts
+++ b/web/tests/e2e/owner-space-billing.spec.ts
@@ -1,64 +1,98 @@
import { expect, test } from '@playwright/test';
+import {
+ installLangBotApiMocks,
+ makeWorkspaceEntry,
+} from './fixtures/langbot-api';
-import { installLangBotApiMocks } from './fixtures/langbot-api';
-
-test('an OSS local-only owner is prompted to bind before using LangBot Models', async ({
- page,
-}) => {
- await installLangBotApiMocks(page, { authenticated: true });
- await page.route('**/api/v1/user/space-credits', (route) =>
- route.fulfill({
- contentType: 'application/json',
- body: JSON.stringify({
- code: 0,
- data: {
- credits: null,
- owner_space_bound: false,
- is_workspace_owner: true,
- },
- msg: 'ok',
- }),
- }),
- );
- await page.route('**/api/v1/provider/providers', (route) =>
- route.fulfill({
- contentType: 'application/json',
- body: JSON.stringify({
- code: 0,
- data: {
- providers: [
- {
- uuid: 'langbot-models-provider',
- name: 'LangBot Models',
- requester: 'space-chat-completions',
- base_url: '',
- api_keys: [],
+for (const role of ['owner', 'viewer'] as const) {
+ for (const bound of [false, true]) {
+ test(`LangBot Models uses Workspace billing for ${role}, owner bound=${bound}`, async ({
+ page,
+ }) => {
+ const workspace = makeWorkspaceEntry(
+ 'billing-workspace',
+ 'Billing Workspace',
+ 'local',
+ );
+ workspace.membership.role = role;
+ await installLangBotApiMocks(page, {
+ authenticated: true,
+ workspaces: [workspace],
+ });
+ const billingHeaders: Array> = [];
+ await page.route('**/api/v1/user/space-credits', async (route) => {
+ billingHeaders.push(route.request().headers());
+ await route.fulfill({
+ json: {
+ code: 0,
+ data: {
+ credits: bound ? 12500 : null,
+ owner_space_bound: bound,
+ is_workspace_owner: role === 'owner',
},
- ],
- },
- msg: 'ok',
- }),
- }),
- );
- await page.route('**/api/v1/provider/requesters**', (route) =>
- route.fulfill({
- contentType: 'application/json',
- body: JSON.stringify({ code: 0, data: { requesters: [] }, msg: 'ok' }),
- }),
- );
- await page.route('**/api/v1/provider/models/**', (route) =>
- route.fulfill({
- contentType: 'application/json',
- body: JSON.stringify({ code: 0, data: { models: [] }, msg: 'ok' }),
- }),
- );
-
- await page.goto('/home?action=showModelSettings');
-
- await expect(
- page.getByText(
- 'The Workspace owner must connect a LangBot Account for LangBot Models.',
- { exact: true },
- ),
- ).toBeVisible();
-});
+ },
+ });
+ });
+ await page.route('**/api/v1/provider/providers', (route) =>
+ route.fulfill({
+ json: {
+ code: 0,
+ data: {
+ providers: [
+ {
+ uuid: 'langbot-models-provider',
+ name: 'LangBot Models',
+ requester: 'space-chat-completions',
+ base_url: '',
+ api_keys: [],
+ },
+ ],
+ },
+ },
+ }),
+ );
+ await page.route('**/api/v1/provider/requesters**', (route) =>
+ route.fulfill({ json: { code: 0, data: { requesters: [] } } }),
+ );
+ await page.route('**/api/v1/provider/models/**', (route) =>
+ route.fulfill({ json: { code: 0, data: { models: [] } } }),
+ );
+ await page.goto('/home?action=showModelSettings');
+ await expect.poll(() => billingHeaders.length).toBeGreaterThan(0);
+ expect(billingHeaders[0]).toMatchObject({
+ authorization: 'Bearer playwright-token',
+ 'x-workspace-id': 'billing-workspace',
+ });
+ const dialog = page.getByRole('dialog');
+ await expect(
+ dialog.getByText(
+ 'The Workspace owner must connect a LangBot Account for LangBot Models.',
+ { exact: true },
+ ),
+ ).toHaveCount(bound ? 0 : 1);
+ await expect(
+ dialog.getByRole('button', {
+ name: 'Login with LangBot Account',
+ exact: true,
+ }),
+ ).toHaveCount(role === 'owner' && !bound ? 1 : 0);
+ await expect(
+ dialog.getByText('2.50 Credits', { exact: true }),
+ ).toHaveCount(bound ? 1 : 0);
+ if (bound) {
+ await expect(
+ dialog
+ .getByText('2.50 Credits', { exact: true })
+ .locator('..')
+ .getByRole('button'),
+ ).toHaveCount(role === 'owner' ? 1 : 0);
+ }
+ await expect(
+ dialog.getByText(
+ "Uses the Workspace owner's LangBot Account billing and credits.",
+ { exact: true },
+ ),
+ ).toHaveCount(role === 'viewer' && bound ? 1 : 0);
+ });
+ }
+}
diff --git a/web/tests/e2e/plugin-page-auth.spec.ts b/web/tests/e2e/plugin-page-auth.spec.ts
index 384567120..7cc477cc7 100644
--- a/web/tests/e2e/plugin-page-auth.spec.ts
+++ b/web/tests/e2e/plugin-page-auth.spec.ts
@@ -62,6 +62,7 @@ test('loads a Cloud plugin page through the authenticated asset route', async ({
});
let authenticatedAssetRequests = 0;
+ const assetHeaders: Array> = [];
let pageSdkRequests = 0;
let pageApiRequests = 0;
await page.route('**/api/v1/plugins/_sdk/page-sdk.js', async (route) => {
@@ -91,6 +92,7 @@ test('loads a Cloud plugin page through the authenticated asset route', async ({
'**/api/v1/plugins/langbot-team/LangRAG/authenticated-assets/**',
async (route) => {
authenticatedAssetRequests += 1;
+ assetHeaders.push(route.request().headers());
await route.fulfill({
status: 200,
contentType: 'text/html',
@@ -135,7 +137,125 @@ test('loads a Cloud plugin page through the authenticated asset route', async ({
'true',
);
expect(authenticatedAssetRequests).toBeGreaterThan(0);
+ expect(assetHeaders[0]).toMatchObject({
+ authorization: 'Bearer playwright-token',
+ 'x-workspace-id': 'workspace-cloud',
+ });
expect(pageSdkRequests).toBe(1);
expect(pageApiRequests).toBe(1);
await expect(page.getByText('Loading...')).toHaveCount(0);
});
+
+test('plugin asset hook replaces and revokes URLs on a same-document Workspace change', async ({
+ page,
+}) => {
+ const first = makeWorkspaceEntry('asset-first', 'First', 'local');
+ const second = makeWorkspaceEntry('asset-second', 'Second', 'local');
+ await installLangBotApiMocks(page, {
+ authenticated: true,
+ workspaces: [first],
+ });
+ const headers: Array> = [];
+ await page.route(
+ '**/api/v1/plugins/test/asset/authenticated-assets/page.html',
+ async (route) => {
+ headers.push(route.request().headers());
+ await route.fulfill({
+ contentType: 'text/html',
+ body: `${headers.at(-1)?.['x-workspace-id']}
`,
+ });
+ },
+ );
+ await page.goto('/home/bots');
+ await expect
+ .poll(() =>
+ page.evaluate(async () => {
+ const path = '/src/app/infra/http/currentWorkspaceStore.ts';
+ return (await import(path)).getCurrentWorkspaceSnapshot()?.workspace
+ .uuid;
+ }),
+ )
+ .toBe('asset-first');
+ await page.evaluate(async () => {
+ const resources = performance
+ .getEntriesByType('resource')
+ .map((entry) => entry.name);
+ const reactPath = resources.find((url) =>
+ new URL(url).pathname.endsWith('/react.js'),
+ )!;
+ const domPath = resources.find((url) =>
+ new URL(url).pathname.endsWith('/react-dom_client.js'),
+ )!;
+ const React = (await import(reactPath)).default;
+ const domModule = await import(domPath);
+ const { createRoot } = domModule.default || domModule;
+ const hookPath = '/src/hooks/useAuthenticatedPluginResource.ts';
+ const { useAuthenticatedPluginAsset } = await import(hookPath);
+ const host = document.createElement('main');
+ host.id = 'asset-hook';
+ document.getElementById('root')!.style.display = 'none';
+ document.body.appendChild(host);
+ const revoke = URL.revokeObjectURL.bind(URL);
+ URL.revokeObjectURL = (url) => {
+ host.dataset.revoked = JSON.stringify([
+ ...JSON.parse(host.dataset.revoked || '[]'),
+ url,
+ ]);
+ revoke(url);
+ };
+ function Resource() {
+ const resource = useAuthenticatedPluginAsset(
+ 'test',
+ 'asset',
+ 'page.html',
+ );
+ return React.createElement(
+ 'output',
+ { 'data-testid': 'asset-url' },
+ resource.url,
+ );
+ }
+ function Harness() {
+ const [mounted, setMounted] = React.useState(true);
+ return React.createElement(
+ React.Fragment,
+ null,
+ mounted && React.createElement(Resource),
+ React.createElement(
+ 'button',
+ { onClick: () => setMounted(false) },
+ 'Unmount resource',
+ ),
+ );
+ }
+ createRoot(host).render(React.createElement(Harness));
+ });
+ const output = page.getByTestId('asset-url');
+ await expect(output).toHaveText(/^blob:/);
+ const oldURL = await output.textContent();
+ await page.evaluate(async (workspace) => {
+ const contextPath = '/src/app/infra/http/workspaceContext.ts';
+ const storePath = '/src/app/infra/http/currentWorkspaceStore.ts';
+ const context = await import(contextPath);
+ const store = await import(storePath);
+ context.setActiveWorkspaceUuid(workspace.workspace.uuid);
+ store.setCurrentWorkspaceSnapshot(workspace);
+ }, second);
+ await expect(output).toHaveText(/^blob:/);
+ await expect(output).not.toHaveText(oldURL!);
+ await expect
+ .poll(() => headers.map((item) => item['x-workspace-id']))
+ .toEqual(['asset-first', 'asset-second']);
+ expect(
+ headers.every((item) => item.authorization === 'Bearer playwright-token'),
+ ).toBe(true);
+ const newURL = await output.textContent();
+ await expect
+ .poll(() => page.locator('#asset-hook').getAttribute('data-revoked'))
+ .toBe(JSON.stringify([oldURL]));
+ await page.getByRole('button', { name: 'Unmount resource' }).click();
+ await expect(output).toHaveCount(0);
+ await expect
+ .poll(() => page.locator('#asset-hook').getAttribute('data-revoked'))
+ .toBe(JSON.stringify([oldURL, newURL]));
+});
diff --git a/web/tests/unit/cloud-new-account-entry.test.mjs b/web/tests/unit/cloud-new-account-entry.test.mjs
deleted file mode 100644
index 8a096be33..000000000
--- a/web/tests/unit/cloud-new-account-entry.test.mjs
+++ /dev/null
@@ -1,19 +0,0 @@
-import assert from 'node:assert/strict';
-import fs from 'node:fs';
-import test from 'node:test';
-
-const source = fs.readFileSync(
- new URL('../../src/app/login/page.tsx', import.meta.url),
- 'utf8',
-);
-
-test('normal Cloud login uses the standard Space OAuth callback path', () => {
- assert.doesNotMatch(source, /cloudEntry/);
- assert.match(source, /getSpaceAuthorizeUrl\(redirectUri\)/);
-});
-
-test('invitation login uses the same OAuth callback before accepting the invitation', () => {
- assert.doesNotMatch(source, /cloudEntry/);
- assert.match(source, /const invitationToken = getPendingInvitationToken\(\)/);
- assert.match(source, /acceptWorkspaceInvitation\(invitationToken\)/);
-});
diff --git a/web/tests/unit/oss-account-space-billing.test.mjs b/web/tests/unit/oss-account-space-billing.test.mjs
index e49cf43cb..e5c683223 100644
--- a/web/tests/unit/oss-account-space-billing.test.mjs
+++ b/web/tests/unit/oss-account-space-billing.test.mjs
@@ -10,45 +10,6 @@ const root = path.resolve(
);
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
-test('invited local registration returns to login instead of authenticating', () => {
- const source = read('src/app/invitations/accept/page.tsx');
- assert.doesNotMatch(
- source,
- /beginAuthenticatedSession\([\s\S]{0,120}response\.token/,
- );
- assert.match(source, /navigate\('\/login\?invitation=1'/);
-});
-
-test('authenticated invitation page offers logout while retaining invitation', () => {
- const source = read('src/app/invitations/accept/page.tsx');
- assert.match(source, /workspace\.logoutAndReturn/);
- assert.match(source, /setPendingInvitationToken\(token\)/);
-});
-
-test('Space OAuth callback distinguishes unknown and unbound accounts by stable codes', () => {
- const source = read('src/app/auth/space/callback/page.tsx');
- assert.match(source, /space_account_not_registered/);
- assert.match(source, /space_account_binding_required/);
-});
-
-test('models panel derives LangBot Models billing state from workspace owner', () => {
- const source = read('src/app/home/components/models-dialog/ModelsPanel.tsx');
- assert.match(source, /getWorkspaceSpaceBilling/);
- assert.doesNotMatch(source, /getSpaceCredits\(\)/);
- assert.match(source, /membership\.role === 'owner'/);
-});
-
-test('provider card represents owner and member owner-bound states explicitly', () => {
- const source = read(
- 'src/app/home/components/models-dialog/components/ProviderCard.tsx',
- );
- assert.match(source, /isWorkspaceOwner/);
- assert.match(source, /ownerSpaceBound/);
- assert.match(source, /models\.ownerMustBindSpace/);
- assert.match(source, /models\.usesOwnerSpaceBilling/);
- assert.match(source, /isWorkspaceOwner && \(\s*