Files
kdletters 10786c9672 打通多模态 UI 工作流与 Agent Runtime 路由
- 接入 Codex app-server 原生图片输入与隔离暂存

- 统一 UI 识别、合并、绑定到 Agent Runtime Provider

- 增加页面自动发现、UI 资源桥接与阶段持久化

- 同步画布编辑器投影、测试与技术文档
2026-08-25 13:06:46 +08:00

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();
});
});