Files
Genarrative/apps/ai-game-creator-shell/tests/resourceDependencyGraphModel.test.ts
T
menghao 8531a9af3e
Project CI / Frontend tests (pull_request) Successful in 2m40s
Project CI / Native shell tests (pull_request) Successful in 10m40s
Project CI / Repository checks (pull_request) Failing after 7s
Project CI / Backend tests (pull_request) Failing after 7s
实现资源依赖关系图层
新增资源依赖图模型、去重、环检测与上下游查询。

使用原生 SVG 渲染资源引用和聚合任务流,并接入搜索、高亮、拖动及生命周期清理。

修复资源自引用连线被卡片遮挡的问题。

补充前端回归测试并同步工作台 PRD、技术方案和决策记录。
2026-07-31 15:13:18 +08:00

158 lines
5.0 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
createProjectResourceGraph,
projectResourceGraphNeighbors,
type ProjectResourceGraphNodeInput,
} from '../src/view/project-development/resourceDependencyGraphModel';
function resource(
id: string,
producerTaskId: string | null,
options: Partial<ProjectResourceGraphNodeInput> = {},
): ProjectResourceGraphNodeInput {
return {
id,
producerTaskId,
externalResourceId: null,
referenceResourceIds: [],
...options,
};
}
describe('resource dependency graph model', () => {
it('maps exact external asset references, deduplicates them, and filters missing resources', () => {
const graph = createProjectResourceGraph(
[
resource('asset:spec', 'art-director', {
externalResourceId: 'canvas-resource-spec',
}),
resource('asset:ui', 'design-foundation', {
externalResourceId: 'canvas-resource-ui',
referenceResourceIds: [
'canvas-resource-spec',
'canvas-resource-spec',
'missing-resource',
],
}),
],
[
{ id: 'art-director', dependencies: [] },
{ id: 'design-foundation', dependencies: ['art-director'] },
],
);
expect(graph.referenceEdges).toEqual([
expect.objectContaining({
kind: 'asset-reference',
sourceResourceId: 'asset:spec',
targetResourceId: 'asset:ui',
cyclic: false,
}),
]);
expect(graph.unresolvedReferenceResourceIds).toEqual(['missing-resource']);
});
it('aggregates each task dependency into one flow instead of a resource cartesian product', () => {
const graph = createProjectResourceGraph(
[
resource('upstream:a', 'task-a'),
resource('upstream:b', 'task-a'),
resource('downstream:a', 'task-b'),
resource('downstream:b', 'task-b'),
resource('downstream:c', 'task-b'),
],
[
{ id: 'task-a', dependencies: [] },
{ id: 'task-b', dependencies: ['task-a', 'task-a'] },
],
);
expect(graph.taskFlows).toHaveLength(1);
expect(graph.taskFlows[0]).toMatchObject({
sourceTaskId: 'task-a',
targetTaskId: 'task-b',
sourceResourceIds: ['upstream:a', 'upstream:b'],
targetResourceIds: ['downstream:a', 'downstream:b', 'downstream:c'],
});
});
it('detects resource and task dependency cycles without recursive traversal', () => {
const graph = createProjectResourceGraph(
[
resource('asset:a', 'task-a', {
externalResourceId: 'external-a',
referenceResourceIds: ['external-b'],
}),
resource('asset:b', 'task-b', {
externalResourceId: 'external-b',
referenceResourceIds: ['external-a'],
}),
],
[
{ id: 'task-a', dependencies: ['task-b'] },
{ id: 'task-b', dependencies: ['task-a'] },
],
);
expect(graph.cyclicResourceIds).toEqual(new Set(['asset:a', 'asset:b']));
expect(graph.referenceEdges.every((edge) => edge.cyclic)).toBe(true);
expect(graph.cyclicTaskIds).toEqual(new Set(['task-a', 'task-b']));
expect(graph.taskFlows.every((flow) => flow.cyclic)).toBe(true);
});
it('detects task cycles even when a cyclic task has no resource card', () => {
const graph = createProjectResourceGraph(
[resource('asset:a', 'task-a')],
[
{ id: 'task-a', dependencies: ['task-b'] },
{ id: 'task-b', dependencies: ['task-a'] },
],
);
expect(graph.taskFlows).toHaveLength(0);
expect(graph.cyclicTaskIds).toEqual(new Set(['task-a', 'task-b']));
});
it('returns direct upstream and downstream resources for exact and aggregated edges', () => {
const graph = createProjectResourceGraph(
[
resource('task-a:one', 'task-a', {
externalResourceId: 'external-a',
}),
resource('task-a:two', 'task-a'),
resource('task-b:one', 'task-b', {
referenceResourceIds: ['external-a'],
}),
],
[
{ id: 'task-a', dependencies: [] },
{ id: 'task-b', dependencies: ['task-a'] },
],
);
const neighbors = projectResourceGraphNeighbors(graph, 'task-b:one');
expect(neighbors.upstreamResourceIds).toEqual(
new Set(['task-a:one', 'task-a:two']),
);
expect(neighbors.downstreamResourceIds).toEqual(new Set());
expect(neighbors.connectedEdgeIds.size).toBe(2);
});
it('keeps graph construction linear for the supported resource bound', () => {
const resources = Array.from({ length: 4096 }, (_, index) =>
resource(`resource:${index}`, `task:${index}`),
);
const tasks = Array.from({ length: 4096 }, (_, index) => ({
id: `task:${index}`,
dependencies: index === 0 ? [] : [`task:${index - 1}`],
}));
const startedAt = performance.now();
const graph = createProjectResourceGraph(resources, tasks);
const elapsedMs = performance.now() - startedAt;
expect(graph.taskFlows).toHaveLength(4095);
expect(elapsedMs).toBeLessThan(2000);
});
});