f3a8a38acd
web-platform-ci / Standalone decision service (no cloud credentials) (push) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled
lekiwi-compatibility / cpu-compatibility (push) Has been cancelled
web-platform-ci / Standalone decision service (no cloud credentials) (pull_request) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (pull_request) Has been cancelled
web-platform-ci / Playwright E2E (pull_request) Has been cancelled
lekiwi-compatibility / cpu-compatibility (pull_request) Has been cancelled
集成同源 BYOK 会话隔离、精简模型设置、官方订阅入口和 HTTPS 发布运维;保留本地训练/调参与控制能力。同步 npm 版本及 CHANGELOG,记录公网真实 API 验收仍待用户凭据。
142 lines
6.6 KiB
TypeScript
142 lines
6.6 KiB
TypeScript
import { expect, test } from '@playwright/test';
|
||
import { resolve } from 'node:path';
|
||
import { existsSync } from 'node:fs';
|
||
import { readFile, writeFile } from 'node:fs/promises';
|
||
|
||
test.skip(
|
||
process.env.CADWORLD_PRODUCTION_TEST !== '1',
|
||
'真实站点检查须显式 opt-in;不使用任何现存密钥',
|
||
);
|
||
|
||
test('生产 HTTPS:模型导入、Pyodide、精简设置、安全会话与订阅不可用边界', async ({
|
||
page,
|
||
}, info) => {
|
||
const errors: string[] = [];
|
||
const external: string[] = [];
|
||
page.on('pageerror', (e) => errors.push(e.message));
|
||
page.on('request', (r) => {
|
||
if (
|
||
r.url().startsWith('http') &&
|
||
new URL(r.url()).origin !== 'https://cadworld-sim.robotquan.com'
|
||
)
|
||
external.push(r.url());
|
||
});
|
||
const response = await page.goto('/');
|
||
expect(response?.status()).toBe(200);
|
||
await expect(page).toHaveTitle(/CADWorld Sim/);
|
||
await page
|
||
.locator('input[type=file]')
|
||
.first()
|
||
.setInputFiles(resolve('web_platform/fixtures/python_controller/model.xml'));
|
||
await expect(page.getByRole('button', { name: '▶ 播放' })).toBeEnabled({ timeout: 90000 });
|
||
await page.getByRole('tab', { name: '控制台', exact: true }).click();
|
||
await expect(page.getByRole('button', { name: '强化学习任务', exact: true })).toHaveCount(0);
|
||
await page.getByRole('button', { name: /Python 脚本控制/ }).click();
|
||
await page
|
||
.locator('input[accept=".py,text/x-python"]')
|
||
.setInputFiles(resolve('web_platform/fixtures/python_controller/balance.py'));
|
||
await expect(page.getByText('倒立摆 PD 平衡控制', { exact: true })).toBeVisible({
|
||
timeout: 90000,
|
||
});
|
||
await page.getByRole('button', { name: '启用', exact: true }).click();
|
||
await expect(page.getByRole('button', { name: /Python 脚本控制/ })).toContainText('运行');
|
||
await page.getByRole('button', { name: 'LeKiwi 智能抓放', exact: true }).click();
|
||
await page.getByText('模型设置 · API_KEY / ChatGPT 订阅', { exact: true }).click();
|
||
await expect(page.getByRole('button', { name: '保存设置' })).toBeEnabled({ timeout: 20000 });
|
||
await expect(page.getByLabel('本机服务令牌')).toHaveCount(0);
|
||
const cookie = (await page.context().cookies()).find((c) => c.name === '__Host-cadworld-session');
|
||
expect(cookie?.secure).toBe(true);
|
||
expect(cookie?.httpOnly).toBe(true);
|
||
expect(cookie?.sameSite).toBe('Strict');
|
||
await page.screenshot({ path: info.outputPath('website-api-settings.png') });
|
||
await page.getByRole('button', { name: 'ChatGPT 订阅', exact: true }).click();
|
||
// No login or model invocation. Availability must be explicit; user's login stays interactive.
|
||
await expect(page.getByText(/当前服务器可能无法连接官方服务/)).toBeVisible();
|
||
await expect(page.getByRole('button', { name: '登录 ChatGPT 订阅', exact: true })).toBeVisible();
|
||
await page.screenshot({ path: info.outputPath('website-subscription-entry.png') });
|
||
await page.getByText('连接测试与凭据管理', { exact: true }).click();
|
||
await page.getByRole('button', { name: '清除本次凭据', exact: true }).click();
|
||
await expect(page.getByText(/本次凭据已清除/)).toBeVisible();
|
||
expect(errors).toEqual([]);
|
||
expect(external).toEqual([]);
|
||
});
|
||
|
||
test('官方订阅入口网络探测:不登录账号、不推理,完成后销毁会话', async ({ page }, info) => {
|
||
await page.goto('/');
|
||
const evidence = await page.evaluate(async () => {
|
||
const prefix = '/api/decision/v1';
|
||
const headers = { 'Content-Type': 'application/json' };
|
||
const bootstrap = await fetch(prefix + '/session', { method: 'POST', headers, body: '{}' });
|
||
const session = await bootstrap.json();
|
||
const protectedHeaders = {
|
||
...headers,
|
||
'X-CSRF-Token': session.csrfToken,
|
||
'X-Config-Version': String(session.configVersion),
|
||
};
|
||
const response = await fetch(prefix + '/codex/login', {
|
||
method: 'POST',
|
||
headers: protectedHeaders,
|
||
body: '{}',
|
||
});
|
||
const result = await response.json();
|
||
const cleared = await fetch(prefix + '/session', {
|
||
method: 'DELETE',
|
||
headers: protectedHeaders,
|
||
body: '{}',
|
||
});
|
||
return {
|
||
status: response.status,
|
||
error: typeof result.error === 'string' ? result.error : undefined,
|
||
deviceFlowOffered: Boolean(result.verificationUrl && result.userCode),
|
||
cleared: cleared.status,
|
||
accountLoginPerformed: false,
|
||
inferenceCalls: 0,
|
||
};
|
||
});
|
||
expect(evidence.cleared).toBe(200);
|
||
expect([200, 409, 429, 502, 504]).toContain(evidence.status);
|
||
if (evidence.status !== 200) expect(evidence.error).toMatch(/^[a-z0-9_]+$/);
|
||
await writeFile(
|
||
info.outputPath('subscription-network-status.json'),
|
||
JSON.stringify(evidence, null, 2),
|
||
);
|
||
});
|
||
|
||
test('公网真实 WASM A 抓放(明确 mock,不调用付费 API)', async ({ page }, info) => {
|
||
test.skip(!existsSync('build/lekiwi/lekiwi-v1.zip'), '无本地 A 资产');
|
||
let modelCalls = 0;
|
||
page.on('request', (r) => {
|
||
if (/\/api\/decision\/v1\/(plan|decide|test)$/.test(r.url())) modelCalls++;
|
||
});
|
||
await page.goto('/');
|
||
await page
|
||
.locator('input[type=file]')
|
||
.first()
|
||
.setInputFiles(resolve('build/lekiwi/lekiwi-v1.zip'));
|
||
const dialog = page.getByRole('dialog', { name: '配置 URDF 仿真组件' });
|
||
await expect(dialog).toBeVisible({ timeout: 30000 });
|
||
await dialog.getByLabel('URDF 机器人 profile').selectOption('lekiwi-v1');
|
||
await dialog.getByRole('button', { name: '转换并加载' }).click();
|
||
await expect(page.getByRole('button', { name: '▶ 播放' })).toBeEnabled({ timeout: 90000 });
|
||
await page.getByRole('tab', { name: '控制台', exact: true }).click();
|
||
await page.getByRole('button', { name: 'LeKiwi 智能抓放', exact: true }).click();
|
||
const panel = page.getByLabel('LeKiwi 智能抓放任务', { exact: true });
|
||
await panel.getByRole('button', { name: '开始任务(mock)', exact: true }).click();
|
||
await expect(panel.getByTestId('agent-task-state')).toContainText('物理验收成功', {
|
||
timeout: 180000,
|
||
});
|
||
await panel.getByText('高级操作与证据导出', { exact: true }).click();
|
||
const download = page.waitForEvent('download');
|
||
await panel.getByRole('button', { name: '导出任务证据', exact: true }).click();
|
||
const data = JSON.parse(await readFile((await (await download).path())!, 'utf8'));
|
||
expect(data.succeeded).toBe(true);
|
||
expect(data.transported).toBeGreaterThanOrEqual(0.5);
|
||
expect(data.mode).toBe('mock');
|
||
expect(modelCalls).toBe(0);
|
||
await writeFile(
|
||
info.outputPath('production-mock-physical-evidence.json'),
|
||
JSON.stringify(data, null, 2),
|
||
);
|
||
await page.screenshot({ path: info.outputPath('production-physical-success.png') });
|
||
});
|