const { test, expect } = require('@playwright/test'); const { browserJson, createAgent, getCurrentWorkspace, login, uniqueName, } = require('./helpers'); function onboardingSnapshot(workspaceId, overrides = {}) { return { schema_version: 1, workspace_id: workspaceId, revision: 1, status: 'in_progress', dismissed: false, eligible_since: '2026-08-23T08:00:00Z', started_at: '2026-08-23T08:01:00Z', completed_at: null, steps: [ { kind: 'operation', status: 'complete', action: 'open_operation' }, { kind: 'test', status: 'complete', action: 'open_operation' }, { kind: 'publish', status: 'complete', action: 'open_operation' }, { kind: 'agent', status: 'current', action: 'create_agent' }, { kind: 'key', status: 'pending', action: 'create_key' }, { kind: 'connection', status: 'pending', action: 'show_connection' }, { kind: 'first_call', status: 'pending', action: 'open_logs' }, ], operation: { id: 'op_onboarding_contract', published_version: 1, last_test: { status: 'ok', request_id: 'req_onboarding_test', trace_id: '0123456789abcdef0123456789abcdef', occurred_at: '2026-08-23T08:02:00Z', }, }, agent: null, key: null, connection: { status: 'pending', reason_code: 'key_required' }, first_call: null, ...overrides, }; } async function routeOnboarding(page, responder) { await page.route(/\/api\/admin\/workspaces\/[^/]+\/onboarding(?:\/events)?(?:\?.*)?$/, responder); } async function parseMcpResponse(response) { const text = await response.text(); const dataLine = text.split(/\r?\n/).find((line) => line.startsWith('data:')); return JSON.parse(dataLine ? dataLine.slice(5).trim() : text); } async function mcpPost(request, endpoint, key, payload, sessionId) { const headers = { Authorization: `Bearer ${key}`, Accept: 'application/json, text/event-stream', 'Content-Type': 'application/json', }; if (sessionId) { headers['MCP-Session-Id'] = sessionId; headers['MCP-Protocol-Version'] = '2025-06-18'; } return request.post(endpoint, { headers, data: payload }); } test('optional Getting Started checklist is server-derived, resumable, bilingual and accessible', async ({ page }) => { await login(page); const workspace = await getCurrentWorkspace(page); let snapshotRequests = 0; const presentationEvents = []; await routeOnboarding(page, async (route) => { snapshotRequests += 1; if (route.request().method() === 'POST') { presentationEvents.push((await route.request().postDataJSON()).event); } await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(onboardingSnapshot(workspace.id)), }); }); await page.goto('/'); const trigger = page.getByTestId('onboarding-trigger'); await expect(trigger).toContainText(/Getting Started|Начало работы/i); const checklist = page.getByTestId('onboarding-checklist'); if (!await checklist.isVisible()) await trigger.click(); await expect(checklist).toBeVisible(); await expect.poll(() => presentationEvents).toContain('started'); await expect(checklist.locator('ol > li')).toHaveCount(7); await expect(checklist.locator('[aria-current="step"]')).toContainText(/Agent|Агент/i); await expect(checklist.getByRole('status')).toContainText(/3\s*\/\s*7/); await expect(checklist.getByRole('button', { name: /Create agent|Создать агента/i })).toBeFocused(); await page.getByTestId('onboarding-collapse').click(); await expect(checklist).toBeHidden(); await page.reload(); await expect(page.getByTestId('onboarding-trigger')).toBeVisible(); expect(snapshotRequests).toBeGreaterThanOrEqual(2); await page.getByTestId('onboarding-trigger').click(); await page.getByTestId('onboarding-dismiss').click(); await expect(checklist).toBeHidden(); await expect(page.getByTestId('onboarding-trigger')).toContainText(/Resume Getting Started|Продолжить начало работы/i); await expect.poll(() => presentationEvents).toEqual(expect.arrayContaining(['dismissed', 'abandoned'])); await page.reload(); await expect(page.getByTestId('onboarding-trigger')).toContainText(/Resume Getting Started|Продолжить начало работы/i); await page.getByTestId('onboarding-trigger').click(); await expect(checklist).toBeVisible(); await expect(page.getByTestId('onboarding-trigger')).toContainText(/Getting Started|Начало работы/i); await page.evaluate(() => localStorage.setItem('crank_lang', 'ru')); await page.reload(); await expect(page.getByTestId('onboarding-trigger')).toContainText('Начало работы'); await page.getByTestId('onboarding-trigger').click(); await expect(page.getByTestId('onboarding-checklist')).toContainText(/агент/i); await page.evaluate(() => window.setLang('en')); await expect(page.getByTestId('onboarding-checklist').locator('.onboarding-title')).toHaveText('Getting Started'); await expect(page.getByTestId('onboarding-collapse')).toHaveAttribute('aria-label', /Collapse Getting Started/i); await page.keyboard.press('Escape'); await expect(page.getByTestId('onboarding-checklist')).toBeHidden(); await expect(page.getByTestId('onboarding-trigger')).toBeFocused(); await page.emulateMedia({ reducedMotion: 'reduce' }); await page.getByTestId('onboarding-trigger').click(); await expect(page.getByTestId('onboarding-checklist')).toHaveCSS('scroll-behavior', 'auto'); }); test('onboarding error exposes safe support correlation and retries without a client completion bypass', async ({ page }) => { await login(page); const workspace = await getCurrentWorkspace(page); let fail = false; let calls = 0; await routeOnboarding(page, async (route) => { calls += 1; if (fail) { await route.fulfill({ status: 503, contentType: 'application/json', headers: { 'x-request-id': 'req_onboarding_retry', 'x-trace-id': '0123456789abcdef0123456789abcdef', }, body: JSON.stringify({ error: { code: 'onboarding_unavailable', message: 'unavailable' } }), }); return; } await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(onboardingSnapshot(workspace.id, { revision: 2 })) }); }); await page.goto('/'); await page.getByTestId('onboarding-trigger').click(); fail = true; await page.getByTestId('onboarding-refresh').click(); const alert = page.getByRole('alert'); await expect(alert).toContainText('onboarding_unavailable'); await expect(alert).toContainText('req_onboarding_retry'); await expect(alert).toContainText('0123456789abcdef0123456789abcdef'); await expect(page.getByTestId('onboarding-retry')).toBeFocused(); fail = false; await page.getByTestId('onboarding-retry').click(); await expect(page.getByTestId('onboarding-checklist').locator('ol > li')).toHaveCount(7); expect(calls).toBeGreaterThanOrEqual(2); }); test('validation, upstream and compatibility errors recover from the last authoritative onboarding step', async ({ page }) => { await login(page); const workspace = await getCurrentWorkspace(page); const failures = [ { status: 422, code: 'onboarding_validation_failed' }, { status: 502, code: 'onboarding_upstream_unavailable' }, { status: 426, code: 'onboarding_client_incompatible' }, ]; let failureIndex = -1; await routeOnboarding(page, async (route) => { if (failureIndex >= 0) { const failure = failures[failureIndex]; await route.fulfill({ status: failure.status, contentType: 'application/json', headers: { 'x-request-id': `req_onboarding_${failure.status}`, 'x-trace-id': '0123456789abcdef0123456789abcdef', }, body: JSON.stringify({ error: { code: failure.code, message: 'safe failure' } }), }); failureIndex = -1; return; } await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(onboardingSnapshot(workspace.id, { revision: 3 })) }); }); await page.goto('/'); await page.getByTestId('onboarding-trigger').click(); for (let index = 0; index < failures.length; index += 1) { failureIndex = index; await page.getByTestId('onboarding-refresh').click(); const alert = page.getByRole('alert'); await expect(alert).toContainText(failures[index].code); await expect(alert).toContainText(`req_onboarding_${failures[index].status}`); await expect(alert).toContainText('0123456789abcdef0123456789abcdef'); await page.getByTestId('onboarding-retry').click(); await expect(page.getByTestId('onboarding-trigger')).toContainText(/3\s*\/\s*7/); await expect(page.getByTestId('onboarding-checklist').locator('ol > li')).toHaveCount(7); await expect(page.getByTestId('onboarding-completion')).toHaveCount(0); } }); test('authentication recovery returns to an incomplete authoritative onboarding projection', async ({ page }) => { await login(page); const workspace = await getCurrentWorkspace(page); let rejectOnce = false; await routeOnboarding(page, async (route) => { if (rejectOnce) { rejectOnce = false; await route.fulfill({ status: 401, contentType: 'application/json', body: JSON.stringify({ error: { code: 'onboarding_auth_required', message: 'sign in again' } }), }); return; } await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(onboardingSnapshot(workspace.id, { revision: 4 })) }); }); await page.goto('/'); await page.getByTestId('onboarding-trigger').click(); rejectOnce = true; await page.getByTestId('onboarding-refresh').click(); await expect(page).toHaveURL(/\/login/); await page.context().clearCookies(); await login(page); await expect(page.getByTestId('onboarding-trigger')).toContainText(/3\s*\/\s*7/); await expect(page.getByTestId('onboarding-completion')).toHaveCount(0); }); test('a BroadcastChannel refresh updates a second open tab from server progress', async ({ page, context }) => { await login(page); const workspace = await getCurrentWorkspace(page); var revision = 1; const respond = async (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(onboardingSnapshot(workspace.id, { revision, steps: revision === 1 ? onboardingSnapshot(workspace.id).steps : onboardingSnapshot(workspace.id).steps.map((step) => ({ ...step, status: 'complete' })), })), }); await routeOnboarding(page, respond); await page.goto('/'); await expect(page.getByTestId('onboarding-trigger')).toContainText(/3\s*\/\s*7/); const other = await context.newPage(); await routeOnboarding(other, respond); await other.goto('/'); await expect(other.locator('.page-title, .page-heading').first()).toBeVisible(); revision = 2; await other.evaluate(() => window.CrankOnboarding.signalRefresh()); await expect(page.getByTestId('onboarding-trigger')).toContainText(/7\s*\/\s*7/); await other.close(); }); test('bursty browser refresh signals coalesce into one request plus one trailing refresh', async ({ page }) => { await login(page); const workspace = await getCurrentWorkspace(page); let requestNumber = 0; let releaseRefresh; const refreshReleased = new Promise((resolve) => { releaseRefresh = resolve; }); await routeOnboarding(page, async (route) => { if (route.request().method() !== 'GET') { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(onboardingSnapshot(workspace.id, { revision: 1 })) }); return; } requestNumber += 1; if (requestNumber === 1) { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(onboardingSnapshot(workspace.id, { revision: 1 })), }); return; } if (requestNumber === 2) await refreshReleased; await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(onboardingSnapshot(workspace.id, { revision: 2, steps: onboardingSnapshot(workspace.id).steps.map((step) => ({ ...step, status: 'complete' })), status: 'complete', completed_at: '2026-08-23T08:10:00Z', first_call: { log_id: 'log_newest', tool_name: 'frankfurter_latest_rate', request_id: 'req_newest', trace_id: 'abcdefabcdefabcdefabcdefabcdefab', occurred_at: '2026-08-23T08:10:00Z', }, })), }); }); await page.goto('/'); if (!await page.getByTestId('onboarding-checklist').isVisible()) { await page.getByTestId('onboarding-trigger').click(); } await page.evaluate(() => { window.CrankOnboarding.refresh(); window.CrankOnboarding.refresh(); window.CrankOnboarding.refresh(); window.CrankOnboarding.refresh(); }); await expect.poll(() => requestNumber).toBe(2); releaseRefresh(); await expect(page.getByTestId('onboarding-trigger')).toContainText(/7\s*\/\s*7/); await expect(page.getByTestId('onboarding-completion')).toContainText('req_newest'); await expect(page.getByTestId('onboarding-completion')).toContainText('frankfurter_latest_rate'); await expect(page.getByTestId('onboarding-completion')).toContainText('2026-08-23T08:10:00Z'); await expect.poll(() => requestNumber).toBe(3); }); test('stale onboarding deep link requires server reset before explicit reselection', async ({ page }) => { await login(page); const workspace = await getCurrentWorkspace(page); let resetPayload = null; await routeOnboarding(page, async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(onboardingSnapshot(workspace.id, { revision: 17, operation_id: 'op_current', operation_version: 4, agent_id: 'agent_current', catalog_revision: 9, })), }); }); await page.route(/\/api\/admin\/workspaces\/[^/]+\/onboarding\/reset-selection$/, async (route) => { resetPayload = await route.request().postDataJSON(); await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(onboardingSnapshot(workspace.id, { revision: 18 })), }); }); await page.goto('/agents?onboarding=1&action=create&operationId=op_stale&operationVersion=3'); const recovery = page.getByTestId('onboarding-deep-link-stale'); await expect(recovery).toBeVisible(); await expect(recovery).toContainText(/changed Operation|изменённой операции/i); await page.getByTestId('onboarding-reselect').click(); await expect.poll(() => resetPayload).toEqual({ expected_revision: 17 }); await expect(page).toHaveURL('/agents'); }); test('one-time key connection config is never recoverable after the reveal is lost', async ({ page }) => { // community-scope: allow=one-time-token await login(page); const workspace = await getCurrentWorkspace(page); const agent = await createAgent(page, workspace.id, { slug: uniqueName('onboarding_lost_key'), display_name: 'Onboarding lost key Agent', description: 'Proves one-time onboarding key handling.', // community-scope: allow=one-time-token instructions: {}, tool_selection_policy: { mode: 'direct', groups: [], search: { max_results: 8 } }, }); await page.goto(`/api-keys?onboarding=1&action=create&agentId=${encodeURIComponent(agent.agent_id)}`); await page.waitForFunction(() => Boolean(window.CrankAuth && window.CrankAuth.getCsrfToken())); await expect(page.locator('#agent-select')).toHaveValue(agent.agent_id); await expect(page.locator('#modal-create')).toHaveClass(/open/); await page.locator('#new-key-name').fill(uniqueName('onboarding_client')); await page.locator('#modal-confirm-btn').click(); const revealKey = page.locator('#reveal-key-value'); await expect(revealKey).toHaveText(/^crk_/); const rawKey = await revealKey.textContent(); await expect(page.getByTestId('onboarding-connection-config')).toContainText('/mcp/v1/'); await expect(page.getByTestId('onboarding-clipboard-warning')).toBeVisible(); const leakedBeforeNavigation = await page.evaluate((secret) => ({ local: Object.values(localStorage).some((value) => String(value).includes(secret)), session: Object.values(sessionStorage).some((value) => String(value).includes(secret)), url: location.href.includes(secret), }), rawKey); expect(leakedBeforeNavigation).toEqual({ local: false, session: false, url: false }); await page.goto('/agents'); await page.goBack(); await expect(page.locator('body')).not.toContainText(rawKey); await expect(page.getByTestId('onboarding-key-lost')).toContainText(/create|rotate|созда|ротац/i); await expect(page.getByTestId('onboarding-connection-config')).toBeHidden(); await expect(page.locator('#onboarding-connection-clients')).toBeEmpty(); }); test('first value is proven only by a real tools/call through the public MCP endpoint', async ({ page, request }) => { test.setTimeout(90_000); await login(page); const workspace = await getCurrentWorkspace(page); await page.waitForFunction(() => Boolean(window.CrankAuth && window.CrankAuth.getCsrfToken())); const suffix = uniqueName('mcp').replace(/_/g, '-'); const operationName = uniqueName('onboarding_real_rest'); const agentSlug = `onboarding-real-${suffix}`.toLowerCase().replace(/[^a-z0-9-]/g, '-'); // Exercise the actual wizard controls and lifecycle buttons. This is not an // API provisioning shortcut: every mutation below is initiated by visible UI. await page.goto('/wizard/?onboarding=1'); await page.waitForFunction(() => window.CrankWizardReady === true && window.currentStep === 1); await page.locator('[data-testid="wizard-protocol-rest"]').click(); await page.locator('#btn-continue').click(); await expect(page.locator('#upstream-new-trigger')).toBeVisible(); await page.locator('#upstream-new-trigger').click(); await page.locator('#new-upstream-name').fill(`onboarding-${suffix}`); await page.locator('#new-upstream-url').fill('http://127.0.0.1:3310'); await page.locator('#new-upstream-static-headers').fill('{"Accept":"application/json"}'); await page.locator('[data-wizard-action="save-upstream"]').click(); await expect(page.locator('#upstream-preview-url')).toHaveText('http://127.0.0.1:3310'); await page.locator('#endpoint-path').fill('/rates'); await page.locator('#btn-continue').click(); await page.locator('.method-card[data-method="GET"]').click(); await page.locator('#btn-continue').click(); await page.locator('#tool-name').fill(operationName); await page.locator('#tool-display-name').fill('Deterministic onboarding REST Operation'); await page.locator('#tool-title').fill('Deterministic onboarding REST Operation'); await page.locator('#tool-description').fill( 'A local deterministic REST target exercised through the complete onboarding UI flow.', ); await page.locator('#tool-input-schema').fill(JSON.stringify({ type: 'object', properties: { base: { type: 'string', description: 'Base currency.' }, quote: { type: 'string', description: 'Quote currency.' }, }, required: ['base', 'quote'], })); await page.locator('#tool-output-schema').fill(JSON.stringify({ type: 'object', properties: { base: { type: 'string', description: 'Returned base currency.' } }, required: ['base'], })); await page.locator('#btn-continue').click(); await page.locator('details.advanced-mapping-details').filter({ has: page.locator('#tool-input-mapping') }).locator('summary').click(); await page.locator('#tool-input-mapping').fill(JSON.stringify({ 'query.base': '$.input.base', 'query.quote': '$.input.quote', })); await page.locator('details.advanced-mapping-details').filter({ has: page.locator('#tool-output-mapping') }).locator('summary').click(); await page.locator('#tool-output-mapping').fill(JSON.stringify({ base: '$.response.body.base', })); await page.evaluate(() => window.CrankWizardMapping.renderFromEditors()); await page.locator('#tool-exec-config').fill('{"timeout_ms":2000,"headers":{}}'); await page.locator('#wizard-test-input').fill('{"base":"USD","quote":"EUR"}'); const createOperationResponse = page.waitForResponse((response) => ( response.request().method() === 'POST' && /\/api\/admin\/workspaces\/[^/]+\/operations$/.test(response.url()) )); await page.locator('.btn-save-draft').click(); const createdOperation = await (await createOperationResponse).json(); const operationId = createdOperation.operation_id; await expect(page).toHaveURL(new RegExp(`operationId=${operationId}`)); const testRunResponse = page.waitForResponse((response) => ( response.request().method() === 'POST' && response.url().includes(`/operations/${operationId}/test-runs`) )); await page.locator('#wizard-run-test').click(); const testRun = await (await testRunResponse).json(); expect(testRun.ok).toBe(true); await expect(page.locator('#wizard-test-request-id')).not.toHaveText(''); page.once('dialog', (dialog) => dialog.accept()); const publishOperationResponse = page.waitForResponse((response) => ( response.request().method() === 'POST' && response.url().includes(`/operations/${operationId}/publish`) )); await page.locator('#wizard-publish-operation').click(); const published = await (await publishOperationResponse).json(); expect(published.published_version).toBe(1); // Continue through the actual onboarding Agent drawer. The deep link selects // the exact published Operation/version; the visible Create action publishes it. await page.goto(`/agents?onboarding=1&action=create&operationId=${encodeURIComponent(operationId)}&operationVersion=1`); await expect(page.locator('.drawer')).toHaveClass(/open/); const identityInputs = page.locator('.drawer-section').first().locator('input.form-input'); await identityInputs.nth(0).fill('Onboarding real public MCP Agent'); await identityInputs.nth(1).fill(agentSlug); await page.locator('.drawer-section').first().locator('textarea').fill( 'Real public MCP onboarding acceptance flow.', ); await expect(page.locator('.ops-picker-item.selected')).toContainText(operationName); const createAgentResponse = page.waitForResponse((response) => ( response.request().method() === 'POST' && /\/api\/admin\/workspaces\/[^/]+\/agents$/.test(response.url()) )); const bindAgentResponse = page.waitForResponse((response) => ( response.request().method() === 'POST' && /\/agents\/[^/]+\/bindings$/.test(response.url()) )); const publishAgentResponse = page.waitForResponse((response) => ( response.request().method() === 'POST' && /\/agents\/[^/]+\/publish$/.test(response.url()) )); await page.locator('.drawer-footer .btn-primary-sm').click(); const createdResponse = await createAgentResponse; const bindingResponse = await bindAgentResponse; expect(bindingResponse.ok(), await bindingResponse.text()).toBe(true); const agentPublishResponse = await publishAgentResponse; expect(agentPublishResponse.ok(), await agentPublishResponse.text()).toBe(true); const createdAgent = await createdResponse.json(); const provisioned = { agentId: createdAgent.agent_id, operationId, operationVersion: 1, toolName: operationName, }; await expect(page.locator('.drawer')).not.toHaveClass(/open/); // The key is likewise created through the visible onboarding key flow. await page.goto(`/api-keys?onboarding=1&action=create&agentId=${encodeURIComponent(provisioned.agentId)}`); await expect(page.locator('#agent-select')).toHaveValue(provisioned.agentId); await expect(page.locator('#modal-create')).toHaveClass(/open/); await expect(page.locator('[data-scope="read"]')).toBeChecked(); await expect(page.locator('[data-scope="write"]')).toBeChecked(); await page.locator('#new-key-name').fill(uniqueName('onboarding_real_key')); const keyResponsePromise = page.waitForResponse((response) => ( response.request().method() === 'POST' && /\/platform-api-keys$/.test(response.url()) )); await page.locator('#modal-confirm-btn').click(); const keyResponse = await keyResponsePromise; expect(keyResponse.ok()).toBe(true); const keyBody = await keyResponse.json(); provisioned.key = await page.locator('#reveal-key-value').textContent(); provisioned.keyId = keyBody.api_key.id || keyBody.api_key.api_key.id; provisioned.endpoint = await page.locator('#onboarding-connection-clients > code').textContent(); expect(provisioned.key).toMatch(/^crk_/); await expect(page.getByTestId('onboarding-connection-config')).toContainText(provisioned.endpoint); expect(provisioned.endpoint).toContain('/mcp/v1/'); const initialize = await mcpPost(request, provisioned.endpoint, provisioned.key, { jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'crank-onboarding-playwright', version: '1.0.0' }, }, }); const initializeBody = await parseMcpResponse(initialize); expect(initialize.ok(), JSON.stringify(initializeBody)).toBe(true); expect(initializeBody.result.protocolVersion).toBe('2025-06-18'); const sessionId = initialize.headers()['mcp-session-id']; expect(sessionId).toBeTruthy(); const initialized = await mcpPost(request, provisioned.endpoint, provisioned.key, { jsonrpc: '2.0', method: 'notifications/initialized', params: {}, }, sessionId); expect([200, 202]).toContain(initialized.status()); const listed = await mcpPost(request, provisioned.endpoint, provisioned.key, { jsonrpc: '2.0', id: 2, method: 'tools/list', params: {}, }, sessionId); expect(listed.ok()).toBe(true); const listedBody = await parseMcpResponse(listed); expect(listedBody.result.tools.map((tool) => tool.name)).toContain(provisioned.toolName); const called = await mcpPost(request, provisioned.endpoint, provisioned.key, { jsonrpc: '2.0', id: 3, method: 'tools/call', params: { name: provisioned.toolName, arguments: { base: 'USD', quote: 'EUR' }, }, }, sessionId); expect(called.ok()).toBe(true); const calledBody = await parseMcpResponse(called); expect(calledBody.error).toBeUndefined(); expect(calledBody.result.isError).not.toBe(true); // This is deliberately the real authoritative endpoint. The test never posts a // browser-only "complete" flag and never calls a test-only onboarding bypass. const progress = await browserJson( page, 'GET', `/api/admin/workspaces/${encodeURIComponent(workspace.id)}/onboarding`, ); expect(progress.status).toBe('complete'); expect(progress.agent_id).toBe(provisioned.agentId); expect(progress.platform_api_key_id).toBe(provisioned.keyId); expect(progress.operation_id).toBe(provisioned.operationId); expect(progress.operation_version).toBe(provisioned.operationVersion); expect(progress.first_call.agent_id).toBe(provisioned.agentId); expect(progress.first_call.key_id).toBe(provisioned.keyId); expect(progress.first_call.operation_id).toBe(provisioned.operationId); expect(progress.first_call.operation_version).toBe(provisioned.operationVersion); expect(progress.first_call.tool_name).toBe(provisioned.toolName); expect(progress.first_call.request_id).toBeTruthy(); expect(progress.first_call.trace_id).toMatch(/^[0-9a-f]{32}$/); await page.goto('/'); await expect(page.getByTestId('onboarding-trigger')).toContainText(/7\s*\/\s*7/); await page.getByTestId('onboarding-trigger').click(); const completion = page.getByTestId('onboarding-completion'); await expect(completion).toContainText(provisioned.toolName); await expect(completion).toContainText(progress.first_call.occurred_at); await expect(completion).toContainText(progress.first_call.request_id); await expect(completion).toContainText(progress.first_call.trace_id); await page.goto(`/logs?log_id=${encodeURIComponent(progress.first_call.log_id)}`); await expect(page.locator(`[data-id="${progress.first_call.log_id}"]`)).toBeVisible(); await expect(page.locator(`[data-exp="${progress.first_call.log_id}"]`)).toContainText(progress.first_call.trace_id); await page.waitForFunction(() => Boolean(window.CrankAuth && window.CrankAuth.getCsrfToken())); const revokeEvidence = await page.evaluate(async ({ workspaceId, agentId, keyId }) => { const before = await window.CrankApi.listAgentPlatformApiKeys(workspaceId, agentId); await window.CrankApi.revokeAgentPlatformApiKey(workspaceId, agentId, keyId); const after = await window.CrankApi.listAgentPlatformApiKeys(workspaceId, agentId); const onboarding = await window.CrankApi.getOnboarding(workspaceId); return { beforeCount: before.items.length, afterCount: after.items.length, revoked: after.items.find((item) => (item.api_key || item).id === keyId), onboarding, }; }, { workspaceId: workspace.id, agentId: provisioned.agentId, keyId: provisioned.keyId }); expect(revokeEvidence.afterCount).toBe(revokeEvidence.beforeCount); expect((revokeEvidence.revoked.api_key || revokeEvidence.revoked).status).toBe('revoked'); expect(revokeEvidence.onboarding.status).not.toBe('complete'); const keyStep = revokeEvidence.onboarding.steps.find((step) => step.id === 'key'); expect(keyStep.completed).toBe(false); expect(keyStep.status).toBe('regressed'); });