feat: complete Epic 1 production foundation
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { createAgent, getCurrentWorkspace, login, localized, uniqueName } = require('./helpers');
|
||||
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, {
|
||||
@@ -23,10 +24,21 @@ test('api keys page opens create key flow', async ({ page }) => {
|
||||
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(
|
||||
@@ -43,4 +55,253 @@ test('api keys page opens create key flow', async ({ page }) => {
|
||||
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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user