const { test, expect } = require('@playwright/test'); const { browserJson, createAgent, getCurrentWorkspace, login, localized, uniqueName } = require('./helpers'); test('api keys page opens create key flow', async ({ page }) => { await page.context().grantPermissions(['clipboard-read', 'clipboard-write']); await login(page); const workspace = await getCurrentWorkspace(page); await createAgent(page, workspace.id, { slug: uniqueName('playwright_keys_agent'), display_name: 'Playwright Keys Agent', description: 'Agent for API key page smoke test.', instructions: {}, tool_selection_policy: {}, }); await page.goto('/api-keys'); await expect(page.locator('.page-title')).toHaveText(localized('Agent Keys', 'Ключи агентов')); await expect(page.locator('[data-testid="machine-access-summary"]')).toHaveCount(0); await expect(page.locator('#machine-access-note')).toHaveCount(0); await expect(page.locator('[data-i18n="apikeys.agent.subtitle"]')).toContainText( localized('Select the AI agent this key is issued for', 'Выберите AI-агента') ); await expect(page.locator('#agent-select')).not.toBeDisabled(); await expect(page.locator('#btn-create-key')).toBeEnabled(); await page.locator('#btn-create-key').click(); await expect(page.locator('#modal-create')).toHaveClass(/open/); await expect(page.locator('.modal-title')).toHaveText(localized('Create MCP client key', 'Создать ключ MCP-клиента')); await expect(page.locator('[data-scope="read"]')).toBeChecked(); await expect(page.locator('[data-scope="write"]')).toBeChecked(); await page.locator('#new-key-name').fill(`playwright-${Date.now()}`); await page.locator('#modal-confirm-btn').click(); await expect(page.locator('#modal-reveal-body')).toContainText(localized('Copy this key now', 'Скопируйте этот ключ сейчас')); await expect(page.locator('#reveal-key-value')).toContainText('crk_'); const config = page.getByTestId('onboarding-connection-config'); await expect(config).toBeVisible(); await config.getByRole('button', { name: localized('Copy configuration', 'Скопировать конфигурацию') }).first().click(); await expect(page.locator('#reveal-key-value')).toHaveText(''); await expect(config).toBeHidden(); await expect(page.locator('#onboarding-connection-clients')).toBeEmpty(); await page.locator('#modal-done-btn').click(); await expect(page.locator('#modal-create')).not.toHaveClass(/open/); await expect(page.locator('#reveal-key-value')).toHaveText(''); await page.locator('#key-kind-approval').click(); await expect(page.locator('#key-kind-hint')).toContainText( localized('human confirmation interface', 'человек подтверждает действие') ); await expect(page.locator('#btn-create-key')).toContainText( localized('Create approval key', 'Создать ключ подтверждения') ); await page.locator('#btn-create-key').click(); await expect(page.locator('.modal-title')).toHaveText(localized('Create approval key', 'Создать ключ подтверждения')); await expect(page.locator('#approval-key-warning')).toContainText( localized('Do not pass this key to an LLM', 'Не передавайте этот ключ LLM') ); await page.locator('#new-key-name').fill(`playwright-approval-${Date.now()}`); await page.locator('#modal-confirm-btn').click(); await expect(page.locator('#reveal-key-value')).toContainText('crk_appr_'); await expect(page.locator('#onboarding-connection-config')).toBeHidden(); const approvalRawKey = await page.locator('#reveal-key-value').textContent(); await page.goto('/agents'); await page.goBack(); await expect(page.locator('#reveal-key-value')).not.toContainText(approvalRawKey || 'crk_appr_'); }); test('failed clipboard write preserves one-time key material', async ({ page }) => { // community-scope: allow=one-time-token await login(page); const workspace = await getCurrentWorkspace(page); await createAgent(page, workspace.id, { slug: uniqueName('playwright_clipboard_agent'), display_name: 'Playwright Clipboard Agent', description: 'Agent for clipboard failure recovery.', instructions: {}, tool_selection_policy: {}, }); await page.addInitScript(() => { Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText: () => Promise.reject(new Error('clipboard denied')) }, }); }); await page.goto('/api-keys'); await page.locator('#btn-create-key').click(); await page.locator('#new-key-name').fill(uniqueName('clipboard_failure')); await page.locator('#modal-confirm-btn').click(); await expect(page.locator('#reveal-key-value')).toHaveText(/^crk_/); const rawKey = await page.locator('#reveal-key-value').textContent(); expect(rawKey).toMatch(/^crk_/); await page.locator('#copy-key-btn').click(); await expect(page.locator('#reveal-key-value')).toHaveText(rawKey); await expect(page.locator('#modal-reveal-body')).toContainText( localized('Copy this key now', 'Скопируйте ключ сейчас') ); const config = page.getByTestId('onboarding-connection-config'); await expect(config).toBeVisible(); await config.getByRole('button', { name: localized('Copy configuration', 'Скопировать конфигурацию') }).first().click(); await expect(page.locator('#reveal-key-value')).toHaveText(rawKey); await expect(config).toBeVisible(); await expect(page.locator('#onboarding-connection-clients')).not.toBeEmpty(); }); test('ambiguous create failure reconciles metadata before deliberate retry', async ({ page }) => { await login(page); const workspace = await getCurrentWorkspace(page); await createAgent(page, workspace.id, { slug: uniqueName('playwright_ambiguous_agent'), display_name: 'Playwright Ambiguous Agent', description: 'Agent for ambiguous key creation recovery.', instructions: {}, tool_selection_policy: {}, }); let failCreate = true; await page.route(/\/api\/admin\/workspaces\/[^/]+\/agents\/[^/]+\/platform-api-keys$/, async (route) => { if (route.request().method() === 'POST' && failCreate) { failCreate = false; await route.fulfill({ status: 503, contentType: 'application/json', body: JSON.stringify({ error: { code: 'key_create_outcome_unknown', message: 'unknown' } }), }); return; } await route.continue(); }); await page.goto('/api-keys'); await page.locator('#btn-create-key').click(); await page.locator('#new-key-name').fill(uniqueName('ambiguous_key')); await page.locator('#modal-confirm-btn').click(); await expect(page.getByTestId('ambiguous-create-warning')).toBeVisible(); await expect(page.locator('#modal-confirm-btn')).toBeDisabled(); await page.locator('#ambiguous-create-retry-btn').click(); await expect(page.getByTestId('ambiguous-create-warning')).toBeHidden(); await expect(page.locator('#modal-confirm-btn')).toBeEnabled(); }); test('stale create response reconciles metadata without changing a newer modal generation', async ({ page }) => { await login(page); const workspace = await getCurrentWorkspace(page); const agent = await createAgent(page, workspace.id, { slug: uniqueName('playwright_keys_stale_agent'), display_name: 'Playwright Keys Stale Agent', description: 'Agent for API key stale response test.', instructions: {}, tool_selection_policy: {}, }); let releaseFirstCreate; let releaseSecondCreate; const firstCreateReleased = new Promise((resolve) => { releaseFirstCreate = resolve; }); const secondCreateReleased = new Promise((resolve) => { releaseSecondCreate = resolve; }); let createCount = 0; await page.route(/\/api\/admin\/workspaces\/[^/]+\/agents\/[^/]+\/platform-api-keys$/, async (route) => { if (route.request().method() !== 'POST') { await route.continue(); return; } const requestNumber = ++createCount; await (requestNumber === 1 ? firstCreateReleased : secondCreateReleased); await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ secret: requestNumber === 1 ? 'crk_ui_stale_canary_should_not_render' : 'crk_ui_current_canary_may_render', api_key: { api_key: { id: requestNumber === 1 ? 'pk_ui_stale' : 'pk_ui_current', workspace_id: workspace.id, agent_id: agent.agent_id, key_kind: 'mcp_client', name: requestNumber === 1 ? 'ui-stale-key' : 'ui-current-key', prefix: requestNumber === 1 ? 'crk_ui_stale' : 'crk_ui_current', scopes: ['read'], status: 'active', created_at: '2026-08-15T00:00:00Z', last_used_at: null, expires_at: null, allowed_origins: [], }, }, }), }); }); await page.goto('/api-keys'); await page.locator('#btn-create-key').click(); await page.locator('#new-key-name').fill(uniqueName('ui_stale_key')); await page.locator('#modal-confirm-btn').click(); await expect(page.locator('#modal-confirm-btn')).toBeDisabled(); await page.locator('#modal-cancel-btn').click(); await expect(page.locator('#modal-create')).not.toHaveClass(/open/); await page.locator('#btn-create-key').click(); await page.locator('#new-key-name').fill(uniqueName('ui_current_key')); await page.locator('#modal-confirm-btn').click(); const confirm = page.locator('#modal-confirm-btn'); await expect(confirm).toBeDisabled(); await expect(confirm).toContainText(localized('Creating', 'Создание')); releaseFirstCreate(); await expect(page.getByTestId('onboarding-key-lost')).toBeVisible(); await expect(confirm).toBeDisabled(); await expect(confirm).toContainText(localized('Creating', 'Создание')); releaseSecondCreate(); await expect(page.locator('#modal-reveal-body')).toBeVisible(); await expect(page.locator('#reveal-key-value')).not.toContainText('crk_ui_stale_canary_should_not_render'); await expect(page.locator('#reveal-key-value')).toContainText('crk_ui_current_canary_may_render'); }); test('duplicate key revoke and delete clicks send one request while the first request is pending', async ({ page }) => { await page.addInitScript(() => { window.confirm = () => true; }); await login(page); const workspace = await getCurrentWorkspace(page); const agent = await createAgent(page, workspace.id, { slug: uniqueName('playwright_key_mutation_agent'), display_name: 'Playwright Key Mutation Agent', description: 'Agent for duplicate key mutation guards.', instructions: {}, tool_selection_policy: {}, }); const revokeName = uniqueName('revoke_once'); const deleteName = uniqueName('delete_once'); await browserJson( page, 'POST', `/api/admin/workspaces/${encodeURIComponent(workspace.id)}/agents/${encodeURIComponent(agent.agent_id)}/platform-api-keys`, { name: revokeName, key_kind: 'mcp_client', scopes: ['read'] }, ); const revokedKey = await browserJson( page, 'POST', `/api/admin/workspaces/${encodeURIComponent(workspace.id)}/agents/${encodeURIComponent(agent.agent_id)}/platform-api-keys`, { name: deleteName, key_kind: 'mcp_client', scopes: ['read'] }, ); const revokedKeyId = revokedKey.api_key.api_key.id; await browserJson( page, 'POST', `/api/admin/workspaces/${encodeURIComponent(workspace.id)}/agents/${encodeURIComponent(agent.agent_id)}/platform-api-keys/${encodeURIComponent(revokedKeyId)}/revoke`, {}, [204], ); let releaseRevoke; const revokeReleased = new Promise((resolve) => { releaseRevoke = resolve; }); let revokeRequests = 0; let releaseDelete; const deleteReleased = new Promise((resolve) => { releaseDelete = resolve; }); let deleteRequests = 0; await page.route(/\/api\/admin\/workspaces\/[^/]+\/agents\/[^/]+\/platform-api-keys\/[^/]+(?:\/revoke)?$/, async (route) => { const method = route.request().method(); if (method === 'POST' && /\/revoke$/.test(route.request().url())) { revokeRequests += 1; await revokeReleased; await route.fulfill({ status: 204 }); return; } if (method === 'DELETE') { deleteRequests += 1; await deleteReleased; await route.fulfill({ status: 204 }); return; } await route.continue(); }); await page.goto('/api-keys'); await page.locator('#agent-select').selectOption(agent.agent_id); await expect(page.locator('#keys-tbody')).toContainText(revokeName); await expect(page.locator('#keys-tbody')).toContainText(deleteName); await page.evaluate((name) => { const row = Array.from(document.querySelectorAll('#keys-tbody tr')) .find((candidate) => candidate.textContent.includes(name)); const button = row.querySelector('[title="Revoke key"]'); button.click(); button.click(); }, revokeName); await expect.poll(() => revokeRequests).toBe(1); releaseRevoke(); await expect(page.locator('#keys-tbody')).toContainText(revokeName); const deleteRow = page.locator('#keys-tbody tr').filter({ hasText: deleteName }).first(); const deleteButton = deleteRow.locator('[title="Delete"]'); await expect(deleteRow).toBeVisible(); await expect(deleteButton).toBeEnabled(); await deleteButton.evaluate((button) => { button.click(); button.click(); }); await expect.poll(() => deleteRequests).toBe(1); releaseDelete(); await expect(page.locator('#keys-tbody')).toContainText(deleteName); });