import test from 'node:test'; import assert from 'node:assert/strict'; import { serializeExecutionGraph, getAutoRunnableNodes, hasBlockingAutoRunInput, } from '../src/executionGraph.js'; test('serializeExecutionGraph excludes isolated nodes from the backend prompt', () => { const nodes = [ { id: '1', data: { className: 'Image', definition: { input: { required: { filename: ['FILE_PICKER', {}] }, optional: {} }, manual_trigger: false, }, widgetValues: { filename: 'scan.gwy' }, }, }, { id: '2', data: { className: 'PreviewImage', definition: { input: { required: {}, optional: { input: ['ANNOTATION_SOURCE', {}] } }, manual_trigger: false, }, widgetValues: {}, }, }, { id: '3', data: { className: 'Image', definition: { input: { required: { filename: ['FILE_PICKER', {}] }, optional: {} }, manual_trigger: false, }, widgetValues: {}, }, }, ]; const edges = [ { source: '1', sourceHandle: 'output::0::DATA_FIELD', target: '2', targetHandle: 'input::input::ANNOTATION_SOURCE', }, ]; const prompt = serializeExecutionGraph(nodes, edges); assert.deepEqual(prompt, { '1': { class_type: 'Image', inputs: { filename: 'scan.gwy' }, }, '2': { class_type: 'PreviewImage', inputs: { input: ['1', 0] }, }, }); assert.equal('3' in prompt, false); }); test('serializeExecutionGraph includes isolated preview-load nodes alongside connected subgraphs', () => { const nodes = [ { id: '1', data: { className: 'Image', definition: { input: { required: { filename: ['FILE_PICKER', {}] }, optional: {} }, manual_trigger: false, }, widgetValues: { filename: 'first.gwy' }, }, }, { id: '2', data: { className: 'PreviewImage', definition: { input: { required: {}, optional: { input: ['ANNOTATION_SOURCE', {}] } }, manual_trigger: false, }, widgetValues: {}, }, }, { id: '3', data: { className: 'ImageDemo', definition: { input: { required: { name: [['demo.npy'], {}] }, optional: {} }, manual_trigger: false, }, widgetValues: { name: 'demo.npy' }, }, }, { id: '4', data: { className: 'Image', definition: { input: { required: { filename: ['FILE_PICKER', {}] }, optional: {} }, manual_trigger: false, }, widgetValues: { filename: '' }, }, }, ]; const edges = [ { source: '1', sourceHandle: 'output::0::DATA_FIELD', target: '2', targetHandle: 'input::input::ANNOTATION_SOURCE', }, ]; const prompt = serializeExecutionGraph(nodes, edges); assert.deepEqual(prompt, { '1': { class_type: 'Image', inputs: { filename: 'first.gwy' }, }, '2': { class_type: 'PreviewImage', inputs: { input: ['1', 0] }, }, '3': { class_type: 'ImageDemo', inputs: { name: 'demo.npy' }, }, }); assert.equal('4' in prompt, false); }); test('serializeExecutionGraph allows a singleton Image graph so previews can run', () => { const nodes = [ { id: '1', data: { className: 'Image', definition: { input: { required: { filename: ['FILE_PICKER', {}] }, optional: {} }, manual_trigger: false, }, widgetValues: { filename: 'scan.gwy' }, }, }, ]; const prompt = serializeExecutionGraph(nodes, []); assert.deepEqual(prompt, { '1': { class_type: 'Image', inputs: { filename: 'scan.gwy' }, }, }); }); test('serializeExecutionGraph allows a singleton ImageDemo graph so previews can run', () => { const nodes = [ { id: '1', data: { className: 'ImageDemo', definition: { input: { required: { name: [['demo.npy'], {}] }, optional: {} }, manual_trigger: false, }, widgetValues: { name: 'demo.npy' }, }, }, ]; const prompt = serializeExecutionGraph(nodes, []); assert.deepEqual(prompt, { '1': { class_type: 'ImageDemo', inputs: { name: 'demo.npy' }, }, }); }); test('serializeExecutionGraph ignores group shells and resolves collapsed proxy edges back to child endpoints', () => { const nodes = [ { id: '1', data: { className: 'Image', definition: { input: { required: { filename: ['FILE_PICKER', {}] }, optional: {} }, manual_trigger: false, }, widgetValues: { filename: 'scan.gwy' }, }, }, { id: '10', data: { className: 'Group', definition: null, widgetValues: {}, }, }, { id: '2', parentId: '10', hidden: true, data: { className: 'PreviewImage', definition: { input: { required: {}, optional: { input: ['ANNOTATION_SOURCE', {}] } }, manual_trigger: false, }, widgetValues: {}, }, }, ]; const edges = [ { source: '1', sourceHandle: 'output::0::DATA_FIELD', target: '10', targetHandle: 'group-proxy::in::2::ANNOTATION_SOURCE::input%3A%3Ainput%3A%3AANNOTATION_SOURCE', data: { groupProxyOwner: '10', groupProxyOriginal: { target: '2', targetHandle: 'input::input::ANNOTATION_SOURCE', }, }, }, ]; const prompt = serializeExecutionGraph(nodes, edges); assert.deepEqual(prompt, { '1': { class_type: 'Image', inputs: { filename: 'scan.gwy' }, }, '2': { class_type: 'PreviewImage', inputs: { input: ['1', 0] }, }, }); assert.equal('10' in prompt, false); }); test('serializeExecutionGraph keeps only the View3D viewport snapshot, not camera pose', () => { const nodes = [ { id: '1', data: { className: 'FieldSource', definition: { input: { required: {}, optional: {} }, manual_trigger: false, }, widgetValues: {}, }, }, { id: '2', data: { className: 'View3D', definition: { input: { required: { field: ['DATA_FIELD', {}], camera_azimuth: ['FLOAT', {}], camera_polar: ['FLOAT', {}], camera_distance: ['FLOAT', {}], camera_target_x: ['FLOAT', {}], camera_target_y: ['FLOAT', {}], camera_target_z: ['FLOAT', {}], viewport_snapshot: ['STRING', {}], }, optional: {}, }, manual_trigger: false, }, widgetValues: { camera_azimuth: 0, camera_polar: 1.1, camera_distance: 1.8, camera_target_x: 0, camera_target_y: 0, camera_target_z: 0, viewport_snapshot: '', }, runtimeValues: { camera_azimuth: 0.4, camera_polar: 1.3, camera_distance: 2.6, camera_target_x: 99, camera_target_y: 88, camera_target_z: 77, viewport_snapshot: 'data:image/png;base64,abc', }, }, }, ]; const edges = [ { source: '1', sourceHandle: 'output::0::DATA_FIELD', target: '2', targetHandle: 'input::field::DATA_FIELD', }, ]; const prompt = serializeExecutionGraph(nodes, edges); assert.deepEqual(prompt['2'], { class_type: 'View3D', inputs: { field: ['1', 0], viewport_snapshot: 'data:image/png;base64,abc', }, }); }); test('getAutoRunnableNodes ignores disconnected nodes when deciding what can auto-run', () => { const nodes = [ { id: '1', data: { definition: {}, widgetValues: {} } }, { id: '2', data: { definition: {}, widgetValues: {} } }, { id: '3', data: { definition: {}, widgetValues: {} } }, ]; const edges = [ { source: '1', sourceHandle: 'output::0::DATA_FIELD', target: '2', targetHandle: 'input::field::DATA_FIELD', }, ]; const runnable = getAutoRunnableNodes(nodes, edges); assert.deepEqual(runnable.map((node) => node.id), ['1', '2']); }); test('getAutoRunnableNodes includes isolated preview-load nodes with selections', () => { const nodes = [ { id: '1', data: { className: 'Image', definition: {}, widgetValues: { filename: 'first.gwy' } } }, { id: '2', data: { className: 'PreviewImage', definition: {}, widgetValues: {} } }, { id: '3', data: { className: 'ImageDemo', definition: {}, widgetValues: { name: 'demo.npy' } } }, { id: '4', data: { className: 'Image', definition: {}, widgetValues: { filename: '' } } }, ]; const edges = [ { source: '1', sourceHandle: 'output::0::DATA_FIELD', target: '2', targetHandle: 'input::input::ANNOTATION_SOURCE', }, ]; const runnable = getAutoRunnableNodes(nodes, edges); assert.deepEqual(runnable.map((node) => node.id), ['1', '2', '3']); }); test('getAutoRunnableNodes allows a singleton Image graph', () => { const nodes = [ { id: '1', data: { className: 'Image', definition: {}, widgetValues: { filename: 'scan.gwy' }, }, }, ]; const runnable = getAutoRunnableNodes(nodes, []); assert.deepEqual(runnable.map((node) => node.id), ['1']); }); test('getAutoRunnableNodes allows a singleton ImageDemo graph', () => { const nodes = [ { id: '1', data: { className: 'ImageDemo', definition: {}, widgetValues: { name: 'demo.npy' }, }, }, ]; const runnable = getAutoRunnableNodes(nodes, []); assert.deepEqual(runnable.map((node) => node.id), ['1']); }); test('hasBlockingAutoRunInput only blocks connected nodes with incomplete required inputs', () => { const node = { id: '2', data: { definition: { manual_trigger: false, input: { required: { field: ['DATA_FIELD', {}], filename: ['FILE_PICKER', {}], }, }, }, widgetValues: { filename: '' }, }, }; const completeEdges = [ { source: '1', sourceHandle: 'output::0::DATA_FIELD', target: '2', targetHandle: 'input::field::DATA_FIELD', }, ]; assert.equal(hasBlockingAutoRunInput(node, completeEdges), true); assert.equal( hasBlockingAutoRunInput( { ...node, data: { ...node.data, widgetValues: { filename: 'scan.gwy' }, }, }, completeEdges, ), false, ); }); test('hasBlockingAutoRunInput skips required file widgets when a connected socket overrides them', () => { const node = { id: '2', data: { definition: { manual_trigger: false, input: { required: { filename: ['FILE_PICKER', { hide_when_input_connected: 'path' }], }, optional: { path: ['FILE_PATH', {}], }, }, }, widgetValues: { filename: '' }, }, }; const edges = [ { source: '1', sourceHandle: 'output::0::FILE_PATH', target: '2', targetHandle: 'input::path::FILE_PATH', }, ]; assert.equal(hasBlockingAutoRunInput(node, edges), false); });