#!/usr/bin/env node import { createRequire } from 'node:module'; import { execFileSync } from 'node:child_process'; const require = createRequire(import.meta.url); const { chromium } = require('playwright'); const now = new Date().toISOString().replace(/[-:.TZ]/g, '').slice(0, 14); const username = process.env.BEAVER_SMOKE_USERNAME || `smoke${now}`; const password = process.env.BEAVER_SMOKE_PASSWORD || 'TestPass123!'; const email = process.env.BEAVER_SMOKE_EMAIL || `${username}@example.test`; const portalUrl = (process.env.BEAVER_SMOKE_PORTAL_URL || 'http://127.0.0.1:3081').replace(/\/$/, ''); const nextPath = process.env.BEAVER_SMOKE_NEXT_PATH || '/files'; const executablePath = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH || undefined; const hostResolverRules = process.env.BEAVER_SMOKE_HOST_RESOLVER_RULES || ''; const headless = process.env.BEAVER_SMOKE_HEADLESS !== '0'; const verifyMinio = process.env.BEAVER_SMOKE_VERIFY_MINIO === '1'; const launchArgs = []; if (hostResolverRules) { launchArgs.push(`--host-resolver-rules=${hostResolverRules}`); } const failedResponses = []; const failedRequests = []; function remember(list, value) { list.push(value); if (list.length > 30) { list.shift(); } } async function clickFirstVisible(locators) { for (const locator of locators) { if (await locator.first().isVisible().catch(() => false)) { await locator.first().click(); return true; } } return false; } async function main() { const browser = await chromium.launch({ headless, executablePath, args: launchArgs, }); const page = await browser.newPage({ viewport: { width: 1365, height: 900 } }); page.on('response', (response) => { if (response.status() >= 400) { remember(failedResponses, `${response.status()} ${response.url()}`); } }); page.on('requestfailed', (request) => { remember(failedRequests, `${request.url()} ${request.failure()?.errorText || ''}`); }); try { const registerUrl = new URL('/register', portalUrl); registerUrl.searchParams.set('next', nextPath); await page.goto(registerUrl.toString(), { waitUntil: 'domcontentloaded', timeout: 30000 }); await page.locator('#username').fill(username); await page.locator('#email').fill(email); await page.locator('#password').fill(password); await page.locator('#confirmPassword').fill(password); const submitButton = page.locator('form').first().locator('button[type="submit"]'); await submitButton.waitFor({ state: 'visible', timeout: 30000 }); await submitButton.click(); await page.waitForResponse( (response) => response.url().includes('/api/runtime/register'), { timeout: 180000 }, ); await page.getByText(/配置模型|Model Setup/).waitFor({ timeout: 180000 }); const skipped = await clickFirstVisible([ page.getByRole('button', { name: /跳过|Skip/i }), page.locator('button.secondary-button'), ]); if (!skipped) { throw new Error('Model setup skip button was not found'); } await page.waitForURL(new RegExp(`${nextPath.replace('/', '\\/')}$`), { timeout: 60000 }); await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {}); await page.getByText(/文件管理|Files/).waitFor({ timeout: 30000 }); for (const root of ['uploads', 'outputs', 'shared', 'tasks']) { await page.getByText(root, { exact: true }).waitFor({ timeout: 30000 }); } const token = await page.evaluate(() => window.localStorage.getItem('beaver_access_token') || ''); if (!token) { throw new Error('No Beaver access token found after handoff'); } const uploadName = `smoke-${Date.now()}.txt`; const uploadBody = `hello from Beaver smoke ${username}`; const uploadResult = await page.evaluate(async ({ token, uploadName, uploadBody }) => { const form = new FormData(); form.set('path', 'uploads'); form.set('file', new File([uploadBody], uploadName, { type: 'text/plain' })); const response = await fetch('/api/user-files/upload', { method: 'POST', headers: { Authorization: `Bearer ${token}` }, body: form, }); return { status: response.status, body: await response.json().catch(() => ({})) }; }, { token, uploadName, uploadBody }); if (uploadResult.status !== 200 || uploadResult.body.path !== `uploads/${uploadName}`) { throw new Error(`User file upload failed: ${JSON.stringify(uploadResult)}`); } const apiVisibility = await page.evaluate(async ({ token }) => { const headers = { Authorization: `Bearer ${token}` }; const [status, root] = await Promise.all([ fetch('/api/user-files/status', { headers }).then((response) => response.json()), fetch('/api/user-files/browse', { headers }).then((response) => response.json()), ]); return JSON.stringify({ status, root }); }, { token }); for (const forbidden of ['access_key', 'secret_key', 'bucket', 'namespace', 'object_prefix']) { if (apiVisibility.includes(forbidden)) { throw new Error(`User file API leaked storage field: ${forbidden}`); } } const minioVerified = verifyMinio ? verifyUploadedObjectInMinio(username, uploadName) : false; const finalUrl = page.url(); await page.screenshot({ path: process.env.BEAVER_SMOKE_SCREENSHOT || '/tmp/beaver-auth-files-smoke.png', fullPage: true }); console.log(JSON.stringify({ ok: true, username, email, finalUrl, roots: ['uploads', 'outputs', 'shared', 'tasks'], uploadedPath: `uploads/${uploadName}`, minioVerified, failedResponses, failedRequests, }, null, 2)); } catch (error) { await page.screenshot({ path: process.env.BEAVER_SMOKE_ERROR_SCREENSHOT || '/tmp/beaver-auth-files-smoke-error.png', fullPage: true }).catch(() => {}); console.error(JSON.stringify({ ok: false, username, email, currentUrl: page.url(), error: String(error), failedResponses, failedRequests, }, null, 2)); process.exitCode = 1; } finally { await browser.close(); } } function verifyUploadedObjectInMinio(username, uploadName) { const endpoint = process.env.BEAVER_SMOKE_MINIO_ENDPOINT || 'http://127.0.0.1:9000'; const accessKey = process.env.BEAVER_SMOKE_MINIO_ACCESS_KEY || process.env.BEAVER_MINIO_ROOT_USER || ''; const secretKey = process.env.BEAVER_SMOKE_MINIO_SECRET_KEY || process.env.BEAVER_MINIO_ROOT_PASSWORD || ''; const bucket = process.env.BEAVER_SMOKE_MINIO_BUCKET || process.env.BEAVER_USER_FILES_BUCKET || 'beaver-user-files'; const network = process.env.BEAVER_SMOKE_MINIO_NETWORK || ''; if (!accessKey || !secretKey) { throw new Error('MinIO verification requested but access key or secret key is missing'); } const containerArgs = ['run', '--rm', '--entrypoint', '/bin/sh']; if (network) { containerArgs.push('--network', network); } containerArgs.push( 'minio/mc:latest', '-lc', [ `mc alias set beaver ${shellQuote(endpoint)} ${shellQuote(accessKey)} ${shellQuote(secretKey)} >/dev/null`, `mc stat ${shellQuote(`beaver/${bucket}/users/${username}/uploads/${uploadName}`)} >/dev/null`, ].join(' && '), ); execFileSync('docker', containerArgs, { stdio: 'pipe' }); return true; } function shellQuote(value) { return `'${String(value).replace(/'/g, "'\\''")}'`; } await main();