Files
Genarrative/apps/ai-game-creator-shell/tests/gameRunBridge.test.ts
menghao 752505547d
Project CI / Frontend tests (pull_request) Failing after 23s
Project CI / Repository checks (pull_request) Failing after 1m4s
Project CI / Backend tests (pull_request) Successful in 4m59s
Project CI / Native shell tests (pull_request) Successful in 13m17s
实现运行工作台正式测试切片与悬停信息
新增正式测试切片合同、状态机与上一项/播放暂停/下一项控制
新增可信悬停资源解析与资源管理画布定位
扩展 Tauri 版本登记、运行 Bridge 和 Agent 生成合同
补齐 TypeScript、Rust 与 AppSurface 回归测试
同步工作台 PRD、技术方案与共享决策记录
保持 P2 资源替换、参数写入和下一版本能力未接线
2026-08-04 17:06:24 +08:00

238 lines
7.1 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import type { GameRuntimeMessage } from '../../../../packages/shared/src/contracts/gameCreationApp';
import {
createGameHostMessage,
createGameRunSession,
type GameRunBridgeIdentity,
GameRunBridgeRuntimeGuard,
gameRunSessionFromRuntimeMessage,
validateGameRunBridgeIdentity,
} from '../src/features/project-workspace/gameRunBridge';
const identity: GameRunBridgeIdentity = {
projectId: 'project-1',
versionId: 'runnable-r7',
projectRevision: 7,
previewId: 'preview-1',
previewOrigin: 'http://127.0.0.1:4173',
};
function runtimeMessage(
overrides: Partial<GameRuntimeMessage> = {},
): GameRuntimeMessage {
return {
schemaVersion: 'game-creator-runtime-message.v1',
protocolVersion: 'game-creator-run-bridge.v1',
requestId: 'runtime-request-1',
responseTo: 'host-request-1',
sessionId: 'session-1',
previewId: identity.previewId,
projectId: identity.projectId,
sequence: 1,
type: 'runtime.ready',
versionId: identity.versionId,
projectRevision: identity.projectRevision,
payload: { capabilities: ['state', 'slice', 'inspection'] },
...overrides,
} as GameRuntimeMessage;
}
describe('P3 Game Run Bridge', () => {
it('creates a fully bound host request and accepts its exact ready response', () => {
expect(validateGameRunBridgeIdentity(identity)).toEqual(identity);
const host = createGameHostMessage({
identity,
sessionId: 'session-1',
requestId: 'host-request-1',
sequence: 1,
type: 'host.start',
});
expect(host).toMatchObject({
protocolVersion: 'game-creator-run-bridge.v1',
projectId: identity.projectId,
previewId: identity.previewId,
type: 'host.start',
});
const sliceStart = createGameHostMessage({
identity,
sessionId: 'session-1',
requestId: 'host-request-slice',
sequence: 2,
type: 'host.slice.start',
payload: { sliceId: 'core-gameplay' },
});
expect(sliceStart).toMatchObject({
type: 'host.slice.start',
payload: { sliceId: 'core-gameplay' },
});
const guard = new GameRunBridgeRuntimeGuard(identity, 'session-1');
expect(guard.registerHostRequest(host.requestId, host.type)).toBe(true);
expect(guard.accept(runtimeMessage())).toEqual(runtimeMessage());
expect(
guard.accept(
runtimeMessage({
requestId: 'runtime-request-2',
responseTo: undefined,
sequence: 2,
type: 'runtime.state',
payload: { status: 'playing' },
}),
),
).toMatchObject({ type: 'runtime.state' });
});
it('rejects stale identities, bad ordering and uncorrelated ready messages', () => {
const guard = new GameRunBridgeRuntimeGuard(identity, 'session-1');
guard.registerHostRequest('host-request-1', 'host.start');
expect(
guard.accept(runtimeMessage({ responseTo: 'unknown-request' })),
).toBeNull();
expect(
guard.accept(runtimeMessage({ projectId: 'other-project' })),
).toBeNull();
expect(guard.accept(runtimeMessage({ sequence: 2 }))).toBeNull();
expect(guard.accept(runtimeMessage())).not.toBeNull();
expect(
guard.accept(
runtimeMessage({
requestId: 'runtime-request-2',
responseTo: undefined,
sequence: 2,
previewId: 'old-preview',
type: 'runtime.state',
payload: { status: 'paused' },
}),
),
).toBeNull();
});
it('enforces message and payload whitelists without consuming a rejected sequence', () => {
const guard = new GameRunBridgeRuntimeGuard(identity, 'session-1');
guard.registerHostRequest('host-request-1', 'host.start');
expect(guard.accept(runtimeMessage())).not.toBeNull();
const rejectedPayloads = [
{
type: 'runtime.state',
payload: { status: 'playing', summary: '/tmp/secret' },
},
{
type: 'runtime.state',
payload: { status: 'playing', summary: 'https://untrusted.example' },
},
{
type: 'runtime.state',
payload: { status: 'playing', summary: '<script>run()</script>' },
},
{
type: 'runtime.inspection',
payload: {
inspectionId: 'inspection-1',
label: '运行画面',
metrics: { filePath: '/tmp/secret' },
},
},
{
type: 'runtime.inspection',
payload: {
inspectionId: 'inspection-1',
label: '运行画面',
metrics: {},
targetKind: 'resource',
},
},
{
type: 'runtime.inspection',
payload: {
inspectionId: 'inspection-1',
label: '运行画面',
metrics: { nested: { command: 'host.stop' } },
},
},
] as const;
for (const [index, rejected] of rejectedPayloads.entries()) {
expect(
guard.accept(
runtimeMessage({
requestId: `runtime-request-rejected-${index}`,
responseTo: undefined,
sequence: 2,
...rejected,
} as Partial<GameRuntimeMessage>),
),
).toBeNull();
}
expect(
guard.accept({
...runtimeMessage({
requestId: 'runtime-request-extra-field',
responseTo: undefined,
sequence: 2,
type: 'runtime.state',
payload: { status: 'playing' },
}),
command: 'host.stop',
}),
).toBeNull();
expect(
guard.accept(
runtimeMessage({
requestId: 'runtime-request-2',
responseTo: undefined,
sequence: 2,
type: 'runtime.inspection',
payload: {
inspectionId: 'inspection-1',
label: '运行画面',
metrics: { fps: 60, visible: true },
},
}),
),
).toMatchObject({ type: 'runtime.inspection' });
const oversized = runtimeMessage({
requestId: 'runtime-request-3',
responseTo: undefined,
sequence: 3,
type: 'runtime.state',
payload: { status: 'playing', summary: 'x'.repeat(70 * 1024) },
});
expect(guard.accept(oversized)).toBeNull();
});
it('projects only validated runtime states into the ephemeral session', () => {
const session = createGameRunSession(identity, 'session-1', 100);
expect(session.status).toBe('starting');
const ready = runtimeMessage();
const playing = gameRunSessionFromRuntimeMessage(session, ready, 101);
expect(playing).toMatchObject({ status: 'playing', updatedAt: 101 });
const failed = gameRunSessionFromRuntimeMessage(
playing,
runtimeMessage({
type: 'runtime.error',
payload: { code: 'game-error', message: '运行失败', recoverable: true },
}),
102,
);
expect(failed).toMatchObject({ status: 'failed', updatedAt: 102 });
});
it('accepts only exact 127.0.0.1 origins with explicit ports', () => {
expect(
validateGameRunBridgeIdentity({
...identity,
previewOrigin: 'http://localhost:4173',
}),
).toBeNull();
expect(
validateGameRunBridgeIdentity({
...identity,
previewOrigin: 'http://127.0.0.1:4173/path',
}),
).toBeNull();
});
});