10786c9672
- 接入 Codex app-server 原生图片输入与隔离暂存 - 统一 UI 识别、合并、绑定到 Agent Runtime Provider - 增加页面自动发现、UI 资源桥接与阶段持久化 - 同步画布编辑器投影、测试与技术文档
198 lines
5.9 KiB
TypeScript
198 lines
5.9 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
|
|
import type {
|
|
GameCreationAppAssetManifestEntry,
|
|
GameCreationAppManifest,
|
|
} from '../../../packages/shared/src/contracts/gameCreationApp';
|
|
import {
|
|
ensureUiDesignResourceForPrototype,
|
|
findLinkedUiDesignResource,
|
|
} from '../src/features/ui-editor/uiDesignResourceBridge';
|
|
|
|
function manifestWithPrototype(): GameCreationAppManifest {
|
|
const prototype: GameCreationAppAssetManifestEntry = {
|
|
id: 'prototype-asset',
|
|
kind: 'ui-prototype',
|
|
mediaType: 'image/png',
|
|
localPath: 'assets/ui-prototype.png',
|
|
source: { kind: 'canvas', referenceResourceIds: [] },
|
|
imageSequenceFrames: null,
|
|
imageSequenceDurationMs: null,
|
|
};
|
|
return {
|
|
schemaVersion: 'game-creator-app.v1',
|
|
projectId: 'project-1',
|
|
projectName: 'UI bridge',
|
|
tasks: [],
|
|
assets: [prototype],
|
|
versions: [],
|
|
godotProjectRoot: null,
|
|
preview: null,
|
|
resourceCanvasLayouts: [],
|
|
} as unknown as GameCreationAppManifest;
|
|
}
|
|
|
|
describe('ui design resource bridge', () => {
|
|
it('finds only a JSON UI resource linked to the exact prototype asset', () => {
|
|
const manifest = manifestWithPrototype();
|
|
manifest.assets.push({
|
|
id: 'unrelated-ui',
|
|
kind: 'UI',
|
|
mediaType: 'application/json',
|
|
localPath: 'ui/unrelated.json',
|
|
source: { kind: 'generated', referenceResourceIds: ['other'] },
|
|
imageSequenceFrames: null,
|
|
imageSequenceDurationMs: null,
|
|
});
|
|
expect(findLinkedUiDesignResource(manifest, 'prototype-asset')).toBeNull();
|
|
manifest.assets.push({
|
|
id: 'linked-ui',
|
|
kind: 'UI',
|
|
mediaType: 'application/json',
|
|
localPath: 'ui/linked.json',
|
|
source: { kind: 'generated', referenceResourceIds: ['prototype-asset'] },
|
|
imageSequenceFrames: null,
|
|
imageSequenceDurationMs: null,
|
|
});
|
|
expect(findLinkedUiDesignResource(manifest, 'prototype-asset')?.id).toBe(
|
|
'linked-ui',
|
|
);
|
|
});
|
|
|
|
it('reuses workflow resources linked by the prototype canonical resource id', () => {
|
|
const manifest = manifestWithPrototype();
|
|
manifest.assets[0]!.source.resourceId = 'canvas-resource-1';
|
|
manifest.assets.push({
|
|
id: 'workflow-ui',
|
|
kind: 'UI',
|
|
mediaType: 'application/json',
|
|
localPath: 'ui/workflow.json',
|
|
source: {
|
|
kind: 'generated',
|
|
resourceId: 'ui-workflow-page-1',
|
|
referenceResourceIds: ['canvas-resource-1', 'design-resource-1'],
|
|
},
|
|
imageSequenceFrames: null,
|
|
imageSequenceDurationMs: null,
|
|
});
|
|
expect(findLinkedUiDesignResource(manifest, 'prototype-asset')?.id).toBe(
|
|
'workflow-ui',
|
|
);
|
|
});
|
|
|
|
it('prefers a completed workflow resource when an older bridge resource also matches', () => {
|
|
const manifest = manifestWithPrototype();
|
|
manifest.assets.push(
|
|
{
|
|
id: 'bridge-ui',
|
|
kind: 'UI',
|
|
mediaType: 'application/json',
|
|
localPath: 'ui/UI 设计 1.json',
|
|
source: {
|
|
kind: 'generated',
|
|
referenceResourceIds: ['prototype-asset'],
|
|
},
|
|
imageSequenceFrames: null,
|
|
imageSequenceDurationMs: null,
|
|
},
|
|
{
|
|
id: 'completed-ui',
|
|
kind: 'UI',
|
|
mediaType: 'application/json',
|
|
localPath: 'ui/ui-workflow-page.json',
|
|
source: {
|
|
kind: 'generated',
|
|
generationKind: 'ui-workflow.completed',
|
|
referenceResourceIds: ['prototype-asset'],
|
|
},
|
|
imageSequenceFrames: null,
|
|
imageSequenceDurationMs: null,
|
|
},
|
|
);
|
|
expect(findLinkedUiDesignResource(manifest, 'prototype-asset')?.id).toBe(
|
|
'completed-ui',
|
|
);
|
|
});
|
|
|
|
it('reuses an existing link without invoking a mutating command', async () => {
|
|
const manifest = manifestWithPrototype();
|
|
const linked = {
|
|
id: 'linked-ui',
|
|
kind: 'UI',
|
|
mediaType: 'application/json',
|
|
localPath: 'ui/linked.json',
|
|
source: {
|
|
kind: 'generated' as const,
|
|
referenceResourceIds: ['prototype-asset'],
|
|
},
|
|
imageSequenceFrames: null,
|
|
imageSequenceDurationMs: null,
|
|
};
|
|
manifest.assets.push(linked);
|
|
const invoke = vi.fn();
|
|
const result = await ensureUiDesignResourceForPrototype({
|
|
projectPath: '/project',
|
|
manifest,
|
|
prototypeAssetId: 'prototype-asset',
|
|
invoke,
|
|
});
|
|
expect(result.asset).toEqual(linked);
|
|
expect(result.created).toBe(false);
|
|
expect(invoke).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('invokes the atomic Tauri bridge for a missing link', async () => {
|
|
const manifest = manifestWithPrototype();
|
|
const result = {
|
|
asset: {
|
|
id: 'new-ui',
|
|
kind: 'UI',
|
|
mediaType: 'application/json',
|
|
localPath: 'ui/UI 设计 1.json',
|
|
source: {
|
|
kind: 'generated' as const,
|
|
referenceResourceIds: ['prototype-asset'],
|
|
},
|
|
imageSequenceFrames: null,
|
|
imageSequenceDurationMs: null,
|
|
},
|
|
manifest: { ...manifest, assets: [...manifest.assets] },
|
|
committedProjectRevision: 7,
|
|
created: true,
|
|
};
|
|
const invoke = vi.fn().mockResolvedValue(result);
|
|
await expect(
|
|
ensureUiDesignResourceForPrototype({
|
|
projectPath: '/project',
|
|
manifest,
|
|
prototypeAssetId: ' prototype-asset ',
|
|
invoke,
|
|
}),
|
|
).resolves.toEqual(result);
|
|
expect(invoke).toHaveBeenCalledWith(
|
|
'ensure_ui_design_resource_for_prototype',
|
|
{
|
|
input: {
|
|
projectPath: '/project',
|
|
expectedProjectId: 'project-1',
|
|
prototypeAssetId: 'prototype-asset',
|
|
},
|
|
},
|
|
);
|
|
});
|
|
|
|
it('rejects non-prototype assets before invoking Tauri', async () => {
|
|
const manifest = manifestWithPrototype();
|
|
const invoke = vi.fn();
|
|
await expect(
|
|
ensureUiDesignResourceForPrototype({
|
|
projectPath: '/project',
|
|
manifest,
|
|
prototypeAssetId: 'missing',
|
|
invoke,
|
|
}),
|
|
).rejects.toThrow('目标资源不是 UI 原型图片');
|
|
expect(invoke).not.toHaveBeenCalled();
|
|
});
|
|
});
|