Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions src/areas/workflows/mockExtensions.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { buildSync } from 'esbuild'
import { createRequire } from 'node:module'
import { mkdtempSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'

// Bundle mockExtensions.ts into CJS. Its only imports are type-only
// (@shared/*), so esbuild erases them and no path-alias resolution is
// required.
function loadModule() {
const outfile = join(mkdtempSync(join(tmpdir(), 'modly-mockExtensions-test-')), 'mockExtensions.cjs')
const require = createRequire(import.meta.url)
const result = buildSync({
entryPoints: [resolve('src/areas/workflows/mockExtensions.ts')],
bundle: true,
platform: 'node',
format: 'cjs',
write: false,
})
writeFileSync(outfile, result.outputFiles[0].text, 'utf8')
return require(outfile)
}

function processExt(node) {
return {
type: 'process',
id: 'pack',
name: 'Pack',
author: 'tester',
trusted: true,
builtin: false,
entry: 'entry.py',
nodes: [node],
}
}

test('object-shaped manifest inputs are normalized to plain type strings', () => {
const { buildAllWorkflowExtensions } = loadModule()
// Mirrors a real-world extension manifest that declared four named image
// slots as objects instead of the documented plain-string shape.
const ext = processExt({
id: 'image-to-image',
name: 'Image to Image',
input: 'image',
inputs: [
{ name: 'front', label: 'Primary image', type: 'image', required: true },
{ name: 'left', label: 'Image 2', type: 'image', required: false },
{ name: 'back', label: 'Image 3', type: 'image', required: false },
{ name: 'right', label: 'Image 4', type: 'image', required: false },
],
output: 'image',
paramsSchema: [],
})

const [built] = buildAllWorkflowExtensions([], [ext])
assert.deepEqual(built.inputs, ['image', 'image', 'image', 'image'])
})

test('already plain-string manifest inputs pass through unchanged', () => {
const { buildAllWorkflowExtensions } = loadModule()
const ext = processExt({
id: 'sketch-to-photo',
name: 'Sketch to Photo',
input: 'image',
inputs: ['image', 'text'],
inputLabels: ['Your sketch', 'Look description'],
output: 'image',
paramsSchema: [],
})

const [built] = buildAllWorkflowExtensions([], [ext])
assert.deepEqual(built.inputs, ['image', 'text'])
assert.deepEqual(built.inputLabels, ['Your sketch', 'Look description'])
})

test('nodes without a multi-input array are left with inputs undefined', () => {
const { buildAllWorkflowExtensions } = loadModule()
const ext = processExt({
id: 'process-node',
name: 'Process Node',
input: 'mesh',
output: 'mesh',
paramsSchema: [],
})

const [built] = buildAllWorkflowExtensions([], [ext])
assert.equal(built.inputs, undefined)
})
19 changes: 17 additions & 2 deletions src/areas/workflows/mockExtensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,21 @@ function applyParamDefaults(
)
}

// Extension manifests are untyped JSON at runtime. `inputs` is documented as
// an array of plain type strings, but some manifests declare it as an array
// of objects instead (e.g. `{ name, label, type, required }` slots). A string
// never equals such an object, so the workflow preflight check silently
// treats every declared input as missing. Normalize once here, at the
// manifest boundary, so everything downstream can trust the documented type.
function normalizeInputs(
raw: WorkflowExtension['inputs'],
): WorkflowExtension['inputs'] {
if (!raw) return raw
return raw.map((entry) =>
typeof entry === 'string' ? entry : (entry as unknown as { type: WorkflowExtension['input'] }).type,
)
}

export function buildAllWorkflowExtensions(
modelExtensions: ModelExtension[],
processExtensions: ProcessExtension[],
Expand All @@ -48,7 +63,7 @@ export function buildAllWorkflowExtensions(
name: node.name,
description: ext.description ?? '',
input: node.input,
inputs: node.inputs,
inputs: normalizeInputs(node.inputs),
inputLabels: node.inputLabels,
output: node.output,
params: applyParamDefaults(node.paramsSchema as ParamSchema[], node.paramDefaults),
Expand All @@ -69,7 +84,7 @@ export function buildAllWorkflowExtensions(
name: node.name,
description: ext.description ?? '',
input: node.input,
inputs: node.inputs,
inputs: normalizeInputs(node.inputs),
inputLabels: node.inputLabels,
output: node.output,
params: applyParamDefaults(node.paramsSchema as ParamSchema[], node.paramDefaults),
Expand Down
3 changes: 2 additions & 1 deletion src/areas/workflows/preflight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ function formatType(type: DataType): string {
if (type === 'mesh') return 'mesh'
if (type === 'image') return 'image'
if (type === 'audio') return 'audio'
return 'text'
if (type === 'text') return 'text'
return String(type)
}

function formatRequiredTypes(types: DataType[]): string {
Expand Down