8531a9af3e
新增资源依赖图模型、去重、环检测与上下游查询。 使用原生 SVG 渲染资源引用和聚合任务流,并接入搜索、高亮、拖动及生命周期清理。 修复资源自引用连线被卡片遮挡的问题。 补充前端回归测试并同步工作台 PRD、技术方案和决策记录。
158 lines
5.0 KiB
TypeScript
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);
|
|
});
|
|
});
|