0d986f60bd
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
集成服务器托管模型、自然语言移动与有界抓放、内置 LeKiwi URL 导入和双摄像头;同步部署契约与指定域名 iframe 白名单,保留原有物理安全、会话及调用预算防护。 更新 npm 包及锁文件版本、CHANGELOG 与发布文档。提交前 typecheck、120 项定向前端测试和 44 项后端测试通过(3 项可选跳过);真实 v2 云模型抓放仍待单独验收,不包含运行密钥或构建产物。
121 lines
5.5 KiB
TypeScript
121 lines
5.5 KiB
TypeScript
import { expect, test } from '@playwright/test';
|
||
import { resolve } from 'node:path';
|
||
|
||
test.skip(process.env.CADWORLD_PRODUCTION_TEST !== '1', '真实站点非付费检查须显式启用');
|
||
test('生产 HTTPS:Pyodide 与精简语言界面,不触发推理', async ({ page }) => {
|
||
let inference = 0;
|
||
const errors: string[] = [];
|
||
page.on('pageerror', (e) => errors.push(e.message));
|
||
page.on('request', (r) => {
|
||
if (/\/api\/decision\/v1\/(command|plan|decide|test)$/.test(r.url())) inference++;
|
||
});
|
||
expect((await page.goto('/'))?.status()).toBe(200);
|
||
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: '机器人语言控制', exact: true }).click();
|
||
await expect(page.locator('input[type=password]')).toHaveCount(0);
|
||
await expect(page.getByRole('textbox', { name: '机器人指令' })).toBeEditable();
|
||
await page.getByRole('button', { name: '执行指令', exact: true }).click();
|
||
await expect(page.getByText('请先导入并加载 LeKiwi v1 机器人', { exact: true })).toBeVisible();
|
||
expect(inference).toBe(0);
|
||
expect(errors).toEqual([]);
|
||
});
|
||
test('生产 v2:空工作台载入预设、文字目标及双相机,无付费推理', async ({ page }, info) => {
|
||
let inference = 0;
|
||
const errors: string[] = [];
|
||
page.on('pageerror', (error) => errors.push(error.message));
|
||
await page.route('**/api/decision/v1/**', async (route) => {
|
||
if (/\/(command|plan|decide|test)$/.test(new URL(route.request().url()).pathname)) {
|
||
inference++;
|
||
await route.abort();
|
||
} else await route.continue();
|
||
});
|
||
expect((await page.goto('/'))?.status()).toBe(200);
|
||
await page.getByRole('tab', { name: '控制台', exact: true }).click();
|
||
await page.getByRole('button', { name: '机器人语言控制', exact: true }).click();
|
||
const panel = page.getByLabel('机器人语言控制', { exact: true });
|
||
await expect(panel.getByRole('combobox', { name: '预设场景' })).toBeVisible();
|
||
await expect(panel.locator('input[type=password], details')).toHaveCount(0);
|
||
await panel.getByRole('button', { name: '载入预设场景' }).click();
|
||
await expect(panel.getByRole('status')).toContainText('预设已载入并暂停', { timeout: 120000 });
|
||
await expect(page.getByRole('button', { name: '▶ 播放' })).toBeEnabled();
|
||
await expect(page.getByRole('combobox', { name: '摄像头视角' }).locator('option')).toHaveCount(2);
|
||
await panel.getByRole('textbox', { name: '机器人指令' }).fill('把方块搬到 (0.25,0.50) 米');
|
||
await expect(panel.getByRole('button', { name: '执行指令', exact: true })).toBeEnabled();
|
||
await panel.getByRole('textbox', { name: '机器人指令' }).fill('停止');
|
||
await panel.getByRole('button', { name: '执行指令', exact: true }).click();
|
||
await expect(panel.getByRole('status')).toContainText('用户停止');
|
||
expect(inference).toBe(0);
|
||
expect(errors).toEqual([]);
|
||
await page.screenshot({ path: info.outputPath('language-v2-production.png') });
|
||
});
|
||
|
||
test('生产默认配置受保护、订阅关闭、静态目录无密钥;退出销毁会话,无推理', async ({ page }) => {
|
||
await page.goto('/');
|
||
const result = 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 secured = {
|
||
...headers,
|
||
'X-CSRF-Token': session.csrfToken,
|
||
'X-Config-Version': String(session.configVersion),
|
||
};
|
||
const config = await fetch(prefix + '/configuration', {
|
||
method: 'PUT',
|
||
headers: secured,
|
||
body: '{}',
|
||
});
|
||
const subscription = await fetch(prefix + '/codex/login', {
|
||
method: 'POST',
|
||
headers: secured,
|
||
body: '{}',
|
||
});
|
||
const env = await fetch('/.env');
|
||
const secret = await fetch('/run/secrets/model-keys.env');
|
||
const destroy = await fetch(prefix + '/session', {
|
||
method: 'DELETE',
|
||
headers: secured,
|
||
body: '{}',
|
||
});
|
||
const expired = await fetch(prefix + '/status');
|
||
return {
|
||
ready: session.ready,
|
||
llm: session.configuration.llm.model,
|
||
keyTypes: [typeof session.configuration.llm.hasKey, typeof session.configuration.jev.hasKey],
|
||
locked: config.status,
|
||
subscription: subscription.status,
|
||
env: env.status,
|
||
secret: secret.status,
|
||
destroyed: destroy.status,
|
||
expired: expired.status,
|
||
};
|
||
});
|
||
expect(result).toEqual({
|
||
ready: true,
|
||
llm: 'deepseek-flash',
|
||
keyTypes: ['boolean', 'boolean'],
|
||
locked: 403,
|
||
subscription: 403,
|
||
env: 404,
|
||
secret: 404,
|
||
destroyed: 200,
|
||
expired: 401,
|
||
});
|
||
});
|