feat: release v1.0.2 LeKiwi 语言控制与网站嵌入
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) (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 云模型抓放仍待单独验收,不包含运行密钥或构建产物。
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
const site = 'https://cadworld-sim.robotquan.com';
|
||||
const approvedParent = 'https://cadworld.robotquan.com';
|
||||
|
||||
test.skip(process.env.CADWORLD_PRODUCTION_TEST !== '1', '真实站点非付费检查须显式启用');
|
||||
|
||||
test('生产嵌入响应头:仅允许自身与指定同事域名,API 仍拒绝父页面来源', async ({ request }) => {
|
||||
const response = await request.get(site);
|
||||
expect(response.status()).toBe(200);
|
||||
const headers = response.headers();
|
||||
expect(headers['x-frame-options']).toBeUndefined();
|
||||
expect(headers['content-security-policy']).toContain(`frame-ancestors 'self' ${approvedParent};`);
|
||||
expect(headers['content-security-policy']).toContain("object-src 'none'");
|
||||
expect(headers['x-content-type-options']).toBe('nosniff');
|
||||
expect(headers['strict-transport-security']).toContain('max-age=31536000');
|
||||
const forbidden = await request.post(`${site}/api/decision/v1/session`, {
|
||||
headers: { Origin: approvedParent, 'Sec-Fetch-Site': 'same-site' },
|
||||
data: {},
|
||||
});
|
||||
expect(forbidden.status()).toBe(403);
|
||||
expect((await forbidden.json()).error).toBe('origin_forbidden');
|
||||
expect(forbidden.headers()['access-control-allow-origin']).toBeUndefined();
|
||||
});
|
||||
|
||||
test('生产嵌入:允许来源真实加载应用、Strict Cookie 会话和 CSRF 正常,无推理', async ({
|
||||
page,
|
||||
context,
|
||||
}, info) => {
|
||||
let inference = 0;
|
||||
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();
|
||||
});
|
||||
// Only the parent HTML is synthetic. The iframe, assets and session API are live.
|
||||
const parentUrl = `${approvedParent}/__cadworld_embed_acceptance__`;
|
||||
await page.route(parentUrl, (route) =>
|
||||
route.fulfill({
|
||||
contentType: 'text/html',
|
||||
body: `<html><body style="margin:0"><iframe title="CADWorld Sim" src="${site}/" style="width:100%;height:95vh;border:0" allow="fullscreen"></iframe></body></html>`,
|
||||
}),
|
||||
);
|
||||
await page.goto(parentUrl);
|
||||
const frame = page.frameLocator('iframe');
|
||||
await frame.getByRole('tab', { name: '控制台', exact: true }).click();
|
||||
await frame.getByRole('button', { name: '机器人语言控制', exact: true }).click();
|
||||
await expect(frame.getByRole('textbox', { name: '机器人指令' })).toBeEditable();
|
||||
const embedded = page.frames().find((f) => f.url() === `${site}/`);
|
||||
expect(embedded).toBeTruthy();
|
||||
const sessionResult = await embedded!.evaluate(async () => {
|
||||
const prefix = '/api/decision/v1';
|
||||
const bootstrap = await fetch(prefix + '/session', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: '{}',
|
||||
});
|
||||
const session = await bootstrap.json();
|
||||
const status = await fetch(prefix + '/status');
|
||||
const csrfGuard = await fetch(prefix + '/cancel', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: '{}',
|
||||
});
|
||||
const destroyed = await fetch(prefix + '/session', {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': session.csrfToken },
|
||||
body: '{}',
|
||||
});
|
||||
const expired = await fetch(prefix + '/status');
|
||||
return {
|
||||
bootstrap: bootstrap.status,
|
||||
ready: session.ready,
|
||||
status: status.status,
|
||||
csrfGuard: csrfGuard.status,
|
||||
destroyed: destroyed.status,
|
||||
expired: expired.status,
|
||||
};
|
||||
});
|
||||
expect(sessionResult).toEqual({
|
||||
bootstrap: 200,
|
||||
ready: true,
|
||||
status: 200,
|
||||
csrfGuard: 403,
|
||||
destroyed: 200,
|
||||
expired: 401,
|
||||
});
|
||||
expect(
|
||||
(await context.cookies(site)).filter((c) => c.name.includes('session')).map((c) => c.name),
|
||||
).toHaveLength(0);
|
||||
expect(inference).toBe(0);
|
||||
await page.screenshot({ path: info.outputPath('approved-iframe.png') });
|
||||
});
|
||||
|
||||
test('生产嵌入:未授权同站子域名仍被 CSP 拒绝', async ({ page }) => {
|
||||
const violations: string[] = [];
|
||||
page.on('console', (message) => {
|
||||
if (message.type() === 'error') violations.push(message.text());
|
||||
});
|
||||
const parentUrl = 'https://unapproved.robotquan.com/__cadworld_embed_acceptance__';
|
||||
await page.route(parentUrl, (route) =>
|
||||
route.fulfill({
|
||||
contentType: 'text/html',
|
||||
body: `<iframe src="${site}/" title="forbidden"></iframe>`,
|
||||
}),
|
||||
);
|
||||
await page.goto(parentUrl);
|
||||
await expect.poll(() => violations.some((text) => text.includes('frame-ancestors'))).toBe(true);
|
||||
await expect(
|
||||
page.frameLocator('iframe').getByRole('tab', { name: '控制台', exact: true }),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
Reference in New Issue
Block a user