import { describe, expect, it } from 'vitest'; import { normalizeProjectResourceGraph, projectResourceGraphNeighbors, type ProjectResourceGraphReadModel, } from '../src/view/project-development/resourceDependencyGraphModel'; function readModel( overrides: Partial = {}, ): 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('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(), }); }); });