import { describe, expect, test } from 'vitest' import type { WorkflowNodeDefinition } from '../../api/workflows' import { createWorkflowModuleBundleInsertion, getWorkflowModuleBundles } from '../../components/workflows/workflowModuleBundles' import { createWorkflowReferenceBundleInsertion, getWorkflowReferenceBundles, } from '../../components/workflows/workflowReferenceBundles' const definitions: WorkflowNodeDefinition[] = [ { step: 'order_line_setup', label: 'Order Line Setup', family: 'order_line', module_key: 'order_line.prepare_render_context', category: 'input', description: 'Prepare render context.', node_type: 'inputNode', icon: 'refresh-cw', defaults: {}, fields: [], execution_kind: 'bridge', legacy_compatible: true, input_contract: { context: 'order_line' }, output_contract: { context: 'order_line', provides: ['order_line'] }, artifact_roles_produced: [], artifact_roles_consumed: [], legacy_source: 'legacy.order_line_setup', }, { step: 'resolve_template', label: 'Resolve Template', family: 'order_line', module_key: 'rendering.resolve_template', category: 'processing', description: 'Resolve template.', node_type: 'processNode', icon: 'layers', defaults: {}, fields: [], execution_kind: 'bridge', legacy_compatible: true, input_contract: { context: 'order_line', requires: ['order_line'] }, output_contract: { context: 'order_line', provides: ['render_template'] }, artifact_roles_produced: [], artifact_roles_consumed: [], legacy_source: 'legacy.resolve_template', }, { step: 'auto_populate_materials', label: 'Auto Populate Materials', family: 'order_line', module_key: 'materials.auto_populate', category: 'processing', description: 'Populate materials.', node_type: 'processNode', icon: 'layers', defaults: {}, fields: [], execution_kind: 'bridge', legacy_compatible: true, input_contract: { context: 'order_line', requires: ['order_line'] }, output_contract: { context: 'order_line', provides: ['cad_materials'] }, artifact_roles_produced: [], artifact_roles_consumed: [], legacy_source: 'legacy.auto_populate_materials', }, { step: 'glb_bbox', label: 'Compute Bounding Box', family: 'order_line', module_key: 'geometry.compute_bbox', category: 'processing', description: 'Compute bbox.', node_type: 'processNode', icon: 'layers', defaults: {}, fields: [], execution_kind: 'native', legacy_compatible: false, input_contract: { context: 'order_line', requires: ['order_line'] }, output_contract: { context: 'order_line', provides: ['bbox'] }, artifact_roles_produced: [], artifact_roles_consumed: [], legacy_source: null, }, { step: 'material_map_resolve', label: 'Resolve Material Map', family: 'order_line', module_key: 'materials.resolve_map', category: 'processing', description: 'Resolve material map.', node_type: 'processNode', icon: 'layers', defaults: {}, fields: [], execution_kind: 'native', legacy_compatible: false, input_contract: { context: 'order_line', requires: ['order_line'] }, output_contract: { context: 'order_line', provides: ['material_map'] }, artifact_roles_produced: [], artifact_roles_consumed: [], legacy_source: null, }, { step: 'blender_still', label: 'Blender Still', family: 'order_line', module_key: 'render.production.still', category: 'rendering', description: 'Render still image.', node_type: 'renderNode', icon: 'camera', defaults: {}, fields: [], execution_kind: 'native', legacy_compatible: false, input_contract: { context: 'order_line', requires: ['order_line'] }, output_contract: { context: 'order_line', provides: ['rendered_image'] }, artifact_roles_produced: [], artifact_roles_consumed: [], legacy_source: null, }, { step: 'output_save', label: 'Save Output', family: 'order_line', module_key: 'media.save_output', category: 'output', description: 'Save output.', node_type: 'outputNode', icon: 'download', defaults: {}, fields: [], execution_kind: 'bridge', legacy_compatible: true, input_contract: { context: 'order_line', requires: ['rendered_image'] }, output_contract: { context: 'order_line', provides: ['saved_output'] }, artifact_roles_produced: [], artifact_roles_consumed: [], legacy_source: 'legacy.output_save', }, { step: 'notify', label: 'Notify Result', family: 'order_line', module_key: 'notifications.emit', category: 'output', description: 'Notify result.', node_type: 'outputNode', icon: 'bell', defaults: {}, fields: [], execution_kind: 'bridge', legacy_compatible: true, input_contract: { context: 'order_line', requires: ['saved_output'] }, output_contract: { context: 'order_line', provides: ['notification'] }, artifact_roles_produced: [], artifact_roles_consumed: [], legacy_source: 'legacy.notify', }, { step: 'export_blend', label: 'Export Blend', family: 'order_line', module_key: 'media.export_blend', category: 'output', description: 'Export blend.', node_type: 'outputNode', icon: 'download', defaults: {}, fields: [], execution_kind: 'bridge', legacy_compatible: true, input_contract: { context: 'order_line', requires: ['render_template'] }, output_contract: { context: 'order_line', provides: ['blend_asset'] }, artifact_roles_produced: [], artifact_roles_consumed: [], legacy_source: 'legacy.export_blend', }, ] const cadDefinitions: WorkflowNodeDefinition[] = [ { step: 'resolve_step_path', label: 'Resolve STEP Path', family: 'cad_file', module_key: 'cad.resolve_step_path', category: 'input', description: 'Resolve the STEP path.', node_type: 'inputNode', icon: 'file-code-2', defaults: {}, fields: [], execution_kind: 'bridge', legacy_compatible: true, input_contract: { context: 'cad_file' }, output_contract: { context: 'cad_file', provides: ['cad_file_record'] }, artifact_roles_produced: [], artifact_roles_consumed: [], legacy_source: 'legacy.resolve_step_path', }, { step: 'occ_object_extract', label: 'Extract STEP Objects', family: 'cad_file', module_key: 'cad.extract_objects', category: 'processing', description: 'Extract objects.', node_type: 'processNode', icon: 'boxes', defaults: {}, fields: [], execution_kind: 'native', legacy_compatible: false, input_contract: { context: 'cad_file', requires: ['cad_file_record'] }, output_contract: { context: 'cad_file', provides: ['occ_scene'] }, artifact_roles_produced: [], artifact_roles_consumed: [], legacy_source: null, }, { step: 'occ_glb_export', label: 'Export GLB', family: 'cad_file', module_key: 'cad.export_glb', category: 'processing', description: 'Export GLB.', node_type: 'processNode', icon: 'package', defaults: {}, fields: [], execution_kind: 'native', legacy_compatible: false, input_contract: { context: 'cad_file', requires: ['occ_scene'] }, output_contract: { context: 'cad_file', provides: ['glb_preview'] }, artifact_roles_produced: [], artifact_roles_consumed: [], legacy_source: null, }, { step: 'stl_cache_generate', label: 'Generate STL Cache', family: 'cad_file', module_key: 'cad.generate_stl_cache', category: 'processing', description: 'Generate STL cache.', node_type: 'processNode', icon: 'database', defaults: {}, fields: [], execution_kind: 'bridge', legacy_compatible: true, input_contract: { context: 'cad_file', requires: ['glb_preview'] }, output_contract: { context: 'cad_file', provides: ['stl_cache'] }, artifact_roles_produced: [], artifact_roles_consumed: [], legacy_source: 'legacy.stl_cache_generate', }, { step: 'blender_render', label: 'Render Thumbnail (Blender)', family: 'cad_file', module_key: 'cad.thumbnail.blender', category: 'rendering', description: 'Render Blender thumbnail.', node_type: 'renderNode', icon: 'camera', defaults: {}, fields: [], execution_kind: 'bridge', legacy_compatible: true, input_contract: { context: 'cad_file', requires: ['glb_preview'] }, output_contract: { context: 'cad_file', provides: ['rendered_image'] }, artifact_roles_produced: [], artifact_roles_consumed: [], legacy_source: 'legacy.blender_render', }, { step: 'threejs_render', label: 'Render Thumbnail (Three.js)', family: 'cad_file', module_key: 'cad.thumbnail.threejs', category: 'rendering', description: 'Render Three.js thumbnail.', node_type: 'renderNode', icon: 'box', defaults: {}, fields: [], execution_kind: 'bridge', legacy_compatible: true, input_contract: { context: 'cad_file', requires: ['glb_preview'] }, output_contract: { context: 'cad_file', provides: ['rendered_image'] }, artifact_roles_produced: [], artifact_roles_consumed: [], legacy_source: 'legacy.threejs_render', }, { step: 'thumbnail_save', label: 'Save Thumbnail', family: 'cad_file', module_key: 'cad.thumbnail.save', category: 'output', description: 'Persist thumbnail.', node_type: 'outputNode', icon: 'download', defaults: {}, fields: [], execution_kind: 'bridge', legacy_compatible: true, input_contract: { context: 'cad_file', requires: ['rendered_image'] }, output_contract: { context: 'cad_file', provides: ['saved_thumbnail'] }, artifact_roles_produced: [], artifact_roles_consumed: [], legacy_source: 'legacy.thumbnail_save', }, ] describe('workflowModuleBundles', () => { test('exposes family-scoped bundles when required steps exist', () => { const bundles = getWorkflowModuleBundles(definitions, 'order_line') expect(bundles.map(bundle => bundle.id)).toEqual([ 'scene_prep_core', 'materials_core', 'still_render_core', 'output_publish_notify', 'blend_export_publish', ]) }) test('creates a connected bundle insertion graph for scene-prep authoring', () => { const insertion = createWorkflowModuleBundleInsertion({ bundleId: 'scene_prep_core', graphFamily: 'order_line', nodeDefinitionsByStep: Object.fromEntries(definitions.map(definition => [definition.step, definition])), existingNodes: [], preferredPosition: { x: 200, y: 320 }, }) expect(insertion.ok).toBe(true) if (!insertion.ok) return expect(insertion.nodes).toHaveLength(3) expect(insertion.edges).toHaveLength(2) expect(insertion.nodes[0].position).toEqual({ x: 200, y: 320 }) expect(insertion.nodes[1].position).toEqual({ x: 420, y: 320 }) expect(insertion.nodes[0].data).toMatchObject({ step: 'order_line_setup', label: 'Order Line Setup' }) expect(insertion.nodes[2].data).toMatchObject({ step: 'glb_bbox', label: 'Compute Bounding Box' }) expect(insertion.edges[0]).toMatchObject({ source: insertion.nodes[0].id, target: insertion.nodes[1].id, }) }) test('creates a single-node still-render module for stage-level insertion', () => { const insertion = createWorkflowModuleBundleInsertion({ bundleId: 'still_render_core', graphFamily: 'order_line', nodeDefinitionsByStep: Object.fromEntries(definitions.map(definition => [definition.step, definition])), existingNodes: [], preferredPosition: { x: 640, y: 180 }, }) expect(insertion.ok).toBe(true) if (!insertion.ok) return expect(insertion.nodes).toHaveLength(1) expect(insertion.edges).toHaveLength(0) expect(insertion.nodes[0].position).toEqual({ x: 640, y: 180 }) expect(insertion.nodes[0].data).toMatchObject({ step: 'blender_still', label: 'Blender Still' }) }) test('exposes full reference paths for complete non-legacy authoring flows', () => { const bundles = getWorkflowReferenceBundles(definitions, 'order_line') expect(bundles.map(bundle => bundle.id)).toEqual([ 'still_render_reference', 'still_render_alpha_reference', 'still_render_blend_reference', ]) }) test('creates the canonical still-render reference graph with branched edges', () => { const insertion = createWorkflowReferenceBundleInsertion({ bundleId: 'still_render_reference', graphFamily: 'order_line', nodeDefinitionsByStep: Object.fromEntries(definitions.map(definition => [definition.step, definition])), existingNodes: [], preferredPosition: { x: 200, y: 320 }, }) expect(insertion.ok).toBe(true) if (!insertion.ok) return expect(insertion.nodes).toHaveLength(8) expect(insertion.edges).toHaveLength(10) expect(insertion.nodes[0].position).toEqual({ x: 200, y: 440 }) expect(insertion.nodes[1].position).toEqual({ x: 420, y: 440 }) expect(insertion.nodes[5].data).toMatchObject({ step: 'blender_still', label: 'Still Render', params: { use_custom_render_settings: false, render_engine: 'cycles', samples: 256, width: 1920, height: 1080, }, }) expect(insertion.edges).toEqual( expect.arrayContaining([ expect.objectContaining({ source: insertion.nodes[0].id, target: insertion.nodes[1].id, }), expect.objectContaining({ source: insertion.nodes[5].id, target: insertion.nodes[6].id, }), expect.objectContaining({ source: insertion.nodes[5].id, target: insertion.nodes[7].id, }), ]), ) }) test('creates the canonical CAD intake reference graph from the shared blueprint', () => { const insertion = createWorkflowReferenceBundleInsertion({ bundleId: 'cad_intake_reference', graphFamily: 'cad_file', nodeDefinitionsByStep: Object.fromEntries(cadDefinitions.map(definition => [definition.step, definition])), existingNodes: [], preferredPosition: { x: 120, y: 240 }, }) expect(insertion.ok).toBe(true) if (!insertion.ok) return expect(insertion.nodes).toHaveLength(9) expect(insertion.edges).toHaveLength(9) expect(insertion.nodes.map(node => node.data.step)).toEqual([ 'resolve_step_path', 'occ_object_extract', 'occ_glb_export', 'glb_bbox', 'stl_cache_generate', 'blender_render', 'threejs_render', 'thumbnail_save', 'thumbnail_save', ]) expect(insertion.edges).toEqual( expect.arrayContaining([ expect.objectContaining({ source: insertion.nodes[2].id, target: insertion.nodes[5].id, }), expect.objectContaining({ source: insertion.nodes[2].id, target: insertion.nodes[6].id, }), expect.objectContaining({ source: insertion.nodes[3].id, target: insertion.nodes[6].id, }), ]), ) }) })