567 lines
13 KiB
JavaScript
567 lines
13 KiB
JavaScript
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);
|
|
});
|
|
|
|
test('serializeExecutionGraph treats accepted_types inputs as sockets, not widgets', () => {
|
|
const nodes = [
|
|
{
|
|
id: '1',
|
|
data: {
|
|
className: 'TableSource',
|
|
definition: {
|
|
input: { required: {}, optional: {} },
|
|
output: ['DATA_TABLE'],
|
|
output_name: ['rows'],
|
|
manual_trigger: false,
|
|
},
|
|
widgetValues: {},
|
|
},
|
|
},
|
|
{
|
|
id: '2',
|
|
data: {
|
|
className: 'PrintTable',
|
|
definition: {
|
|
input: {
|
|
required: {
|
|
table: ['RECORD_TABLE', { accepted_types: ['DATA_TABLE'] }],
|
|
},
|
|
optional: {},
|
|
},
|
|
manual_trigger: false,
|
|
},
|
|
widgetValues: { table: 'should-not-serialize' },
|
|
},
|
|
},
|
|
];
|
|
const edges = [
|
|
{
|
|
source: '1',
|
|
sourceHandle: 'output::0::DATA_TABLE',
|
|
target: '2',
|
|
targetHandle: 'input::table::RECORD_TABLE',
|
|
},
|
|
];
|
|
|
|
const prompt = serializeExecutionGraph(nodes, edges);
|
|
|
|
assert.deepEqual(prompt, {
|
|
'1': {
|
|
class_type: 'TableSource',
|
|
inputs: {},
|
|
},
|
|
'2': {
|
|
class_type: 'PrintTable',
|
|
inputs: { table: ['1', 0] },
|
|
},
|
|
});
|
|
});
|
|
|
|
test('hasBlockingAutoRunInput still blocks unconnected accepted_types sockets', () => {
|
|
const node = {
|
|
id: '2',
|
|
data: {
|
|
definition: {
|
|
manual_trigger: false,
|
|
input: {
|
|
required: {
|
|
input: ['DATA_FIELD', { accepted_types: ['IMAGE', 'LINE', 'DATA_TABLE'] }],
|
|
},
|
|
optional: {},
|
|
},
|
|
},
|
|
widgetValues: {},
|
|
},
|
|
};
|
|
|
|
assert.equal(hasBlockingAutoRunInput(node, []), true);
|
|
assert.equal(
|
|
hasBlockingAutoRunInput(node, [
|
|
{
|
|
source: '1',
|
|
sourceHandle: 'output::0::DATA_TABLE',
|
|
target: '2',
|
|
targetHandle: 'input::input::DATA_FIELD',
|
|
},
|
|
]),
|
|
false,
|
|
);
|
|
});
|