Files
Genarrative/apps/ai-game-creator-shell/tests/resourceDependencyGraphModel.test.ts
menghao 03a4410272
Project CI / Native shell tests (push) Failing after 11m56s
Project CI / Repository checks (push) Successful in 1m5s
Project CI / Frontend tests (push) Successful in 3m20s
Project CI / Backend tests (push) Successful in 4m1s
game agent无限画布开发 (#136)
主要工作是共享同一套“画布内核与通用 UI 源码”,网站和 Tauri 只分别实现宿主适配层。

---------

Co-authored-by: 段舒康 <kdletters@qq.com>
Co-authored-by: kdletters <kdletters@qq.com>
Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/136
Co-authored-by: menghao <mh18530625731@163.com>
Co-committed-by: menghao <mh18530625731@163.com>
2026-08-12 10:54:16 +08:00

303 lines
9.1 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
normalizeProjectResourceGraph,
projectResourceGraphNeighbors,
type ProjectResourceGraphReadModel,
} from '../src/view/project-development/resourceDependencyGraphModel';
function readModel(
overrides: Partial<ProjectResourceGraphReadModel> = {},
): ProjectResourceGraphReadModel {
return {
resourceIds: [],
referenceEdges: [],
taskFlows: [],
connectionIndex: [],
producerAssignments: [],
dependencyDepths: [],
unresolvedReferenceResourceIds: [],
cyclicResourceIds: [],
cyclicTaskIds: [],
producerMappingTruncated: false,
...overrides,
};
}
describe('resource dependency graph model', () => {
it('normalizes the Rust read model and filters stale resource endpoints', () => {
const graph = normalizeProjectResourceGraph(
readModel({
resourceIds: ['asset:source', 'asset:target'],
referenceEdges: [
{
id: 'valid-reference',
kind: 'asset-reference',
sourceResourceId: 'asset:source',
targetResourceId: 'asset:target',
cyclic: false,
},
{
id: 'ghost-reference',
kind: 'asset-reference',
sourceResourceId: 'asset:deleted',
targetResourceId: 'asset:target',
cyclic: false,
},
],
taskFlows: [
{
id: 'valid-flow',
kind: 'task-flow',
sourceTaskId: 'source-task',
targetTaskId: 'target-task',
sourceResourceIds: ['asset:source', 'asset:deleted'],
targetResourceIds: ['asset:target'],
cyclic: false,
},
{
id: 'ghost-flow',
kind: 'task-flow',
sourceTaskId: 'deleted-task',
targetTaskId: 'target-task',
sourceResourceIds: ['asset:deleted'],
targetResourceIds: ['asset:target'],
cyclic: false,
},
],
cyclicResourceIds: ['asset:source', 'asset:deleted'],
}),
);
expect(graph.referenceEdges.map((edge) => edge.id)).toEqual([
'valid-reference',
]);
expect(graph.taskFlows).toEqual([
expect.objectContaining({
id: 'valid-flow',
sourceResourceIds: ['asset:source'],
targetResourceIds: ['asset:target'],
}),
]);
expect(graph.cyclicResourceIds).toEqual(new Set(['asset:source']));
});
it('queries direct reference and aggregated task-flow neighbors from the bounded index', () => {
const graph = normalizeProjectResourceGraph(
readModel({
resourceIds: ['source:a', 'source:b', 'target:a', 'target:b'],
referenceEdges: [
{
id: 'reference:a',
kind: 'asset-reference',
sourceResourceId: 'source:a',
targetResourceId: 'target:a',
cyclic: false,
},
],
taskFlows: [
{
id: 'flow:a-b',
kind: 'task-flow',
sourceTaskId: 'task:a',
targetTaskId: 'task:b',
sourceResourceIds: ['source:a', 'source:b'],
targetResourceIds: ['target:a', 'target:b'],
cyclic: false,
},
],
connectionIndex: [
{
resourceId: 'target:a',
upstreamReferenceResourceIds: ['source:a'],
downstreamReferenceResourceIds: [],
referenceEdgeIds: ['reference:a'],
taskFlowIds: ['flow:a-b'],
},
],
}),
);
const neighbors = projectResourceGraphNeighbors(graph, 'target:a');
expect(neighbors.upstreamResourceIds).toEqual(
new Set(['source:a', 'source:b']),
);
expect(neighbors.downstreamResourceIds).toEqual(new Set());
expect(neighbors.connectedEdgeIds).toEqual(
new Set(['reference:a', 'flow:a-b']),
);
});
it('keeps a refined asset connected to its source through a real reference edge', () => {
const graph = normalizeProjectResourceGraph(
readModel({
resourceIds: ['asset:source-art', 'asset:refined-art'],
referenceEdges: [
{
id: 'asset-reference:source-refined',
kind: 'asset-reference',
sourceResourceId: 'asset:source-art',
targetResourceId: 'asset:refined-art',
cyclic: false,
},
],
connectionIndex: [
{
resourceId: 'asset:refined-art',
upstreamReferenceResourceIds: ['asset:source-art'],
downstreamReferenceResourceIds: [],
referenceEdgeIds: ['asset-reference:source-refined'],
taskFlowIds: [],
},
],
dependencyDepths: [
{ resourceId: 'asset:source-art', dependencyDepth: 0 },
{ resourceId: 'asset:refined-art', dependencyDepth: 1 },
],
}),
);
expect(graph.referenceEdges).toEqual([
expect.objectContaining({
sourceResourceId: 'asset:source-art',
targetResourceId: 'asset:refined-art',
}),
]);
expect(graph.dependencyDepthByResourceId.get('asset:refined-art')).toBe(1);
expect(
projectResourceGraphNeighbors(graph, 'asset:refined-art')
.upstreamResourceIds,
).toEqual(new Set(['asset:source-art']));
});
it('fails closed only for producer-derived data when the audit tail is truncated', () => {
const graph = normalizeProjectResourceGraph(
readModel({
resourceIds: [
'asset:spec',
'asset:ui',
'asset:derived',
'asset:negative',
'asset:fractional',
'asset:unsafe',
],
producerAssignments: [
{
resourceId: 'asset:spec',
taskId: 'art-director',
},
{
resourceId: 'asset:ui',
taskId: 'design-foundation',
},
{
resourceId: 'asset:deleted',
taskId: 'task-1',
},
],
dependencyDepths: [
{
resourceId: 'asset:spec',
dependencyDepth: 0,
},
{
resourceId: 'asset:ui',
dependencyDepth: 1,
},
{
resourceId: 'asset:deleted',
dependencyDepth: 99,
},
{
resourceId: 'asset:ui',
dependencyDepth: 0,
},
{
resourceId: 'asset:derived',
dependencyDepth: 2,
},
{
resourceId: 'asset:negative',
dependencyDepth: -1,
},
{
resourceId: 'asset:fractional',
dependencyDepth: 1.5,
},
{
resourceId: 'asset:unsafe',
dependencyDepth: Number.MAX_SAFE_INTEGER + 1,
},
],
taskFlows: [
{
id: 'flow:spec-ui',
kind: 'task-flow',
sourceTaskId: 'art-director',
targetTaskId: 'design-foundation',
sourceResourceIds: ['asset:spec'],
targetResourceIds: ['asset:ui'],
cyclic: false,
},
],
referenceEdges: [
{
id: 'reference:spec-ui',
kind: 'asset-reference',
sourceResourceId: 'asset:spec',
targetResourceId: 'asset:ui',
cyclic: false,
},
],
connectionIndex: [
{
resourceId: 'asset:spec',
upstreamReferenceResourceIds: [],
downstreamReferenceResourceIds: ['asset:ui'],
referenceEdgeIds: ['reference:spec-ui'],
taskFlowIds: ['flow:spec-ui'],
},
{
resourceId: 'asset:ui',
upstreamReferenceResourceIds: ['asset:spec'],
downstreamReferenceResourceIds: [],
referenceEdgeIds: ['reference:spec-ui'],
taskFlowIds: ['flow:spec-ui'],
},
],
unresolvedReferenceResourceIds: ['external:missing'],
cyclicResourceIds: ['asset:ui'],
cyclicTaskIds: ['art-director'],
producerMappingTruncated: true,
}),
);
expect(graph.producerTaskIdByResourceId).toEqual(new Map());
expect(graph.dependencyDepthByResourceId).toEqual(
new Map([
['asset:spec', 0],
['asset:ui', 1],
['asset:derived', 2],
]),
);
expect(graph.taskFlows).toEqual([]);
expect(graph.cyclicTaskIds).toEqual(new Set());
expect(graph.referenceEdges.map((edge) => edge.id)).toEqual([
'reference:spec-ui',
]);
expect(graph.unresolvedReferenceResourceIds).toEqual(['external:missing']);
expect(graph.cyclicResourceIds).toEqual(new Set(['asset:ui']));
expect(graph.producerMappingTruncated).toBe(true);
expect(projectResourceGraphNeighbors(graph, 'asset:ui')).toEqual({
upstreamResourceIds: new Set(['asset:spec']),
downstreamResourceIds: new Set(),
connectedEdgeIds: new Set(['reference:spec-ui']),
});
expect(projectResourceGraphNeighbors(graph, 'asset:deleted')).toEqual({
upstreamResourceIds: new Set(),
downstreamResourceIds: new Set(),
connectedEdgeIds: new Set(),
});
});
});