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
54 changes: 54 additions & 0 deletions src/areas/workflows/slotInputs.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
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'

// slotInputs.ts has no runtime imports, so esbuild bundles it standalone.
function loadModule() {
const outfile = join(mkdtempSync(join(tmpdir(), 'modly-slotinputs-test-')), 'slotInputs.cjs')
const require = createRequire(import.meta.url)
const result = buildSync({
entryPoints: [resolve('src/areas/workflows/slotInputs.ts')],
bundle: true,
platform: 'node',
format: 'cjs',
write: false,
})
writeFileSync(outfile, result.outputFiles[0].text, 'utf8')
return require(outfile)
}

const { assignSlotFilePaths } = loadModule()

test('a mesh slot becomes the mesh path', () => {
const r = assignSlotFilePaths(['mesh', 'image'], ['a.glb', 'b.png'])
assert.equal(r.nodeInputMeshPath, 'a.glb')
assert.equal(r.nodeInputPath, 'b.png')
assert.deepEqual(r.extraImagePaths, [])
})

test('the first image slot is the primary path and later images are extras', () => {
const r = assignSlotFilePaths(['image', 'image', 'image'], ['1.png', '2.png', '3.png'])
assert.equal(r.nodeInputPath, '1.png')
assert.deepEqual(r.extraImagePaths, ['2.png', '3.png'])
assert.equal(r.nodeInputMeshPath, undefined)
})

test('an audio slot becomes the primary path', () => {
// Regression: audio slots were resolved and then dropped, so a multi-input
// node such as [audio, text] failed with "needs an incoming audio connection".
const r = assignSlotFilePaths(['audio', 'text'], ['song.wav', undefined])
assert.equal(r.nodeInputPath, 'song.wav')
assert.equal(r.nodeInputMeshPath, undefined)
assert.deepEqual(r.extraImagePaths, [])
})

test('text slots never claim a file path, and empty slots are skipped', () => {
const r = assignSlotFilePaths(['text', 'image'], ['leaked.png', undefined])
assert.equal(r.nodeInputPath, undefined)
assert.equal(r.nodeInputMeshPath, undefined)
assert.deepEqual(r.extraImagePaths, [])
})
36 changes: 36 additions & 0 deletions src/areas/workflows/slotInputs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Multi-input slot → file-path assignment for extension nodes.
//
// `inputTypes` is the node's declared `inputs` (one per handle); `inputPaths` is
// the file path resolved for each slot, indexed the same way (undefined where the
// slot carries text or nothing). Pure so it can be tested without the store.

export type SlotInputType = 'image' | 'text' | 'mesh' | 'audio'

export interface SlotFilePaths {
/** Primary file: what the extension receives as `filePath` when no mesh is present. */
nodeInputPath?: string
/** Mesh slot, if any. Takes precedence as `filePath`; the primary file then rides in params. */
nodeInputMeshPath?: string
/** Every image beyond the first resolved image slot. */
extraImagePaths: string[]
}

export function assignSlotFilePaths(
inputTypes: readonly SlotInputType[],
inputPaths: readonly (string | undefined)[],
): SlotFilePaths {
const out: SlotFilePaths = { extraImagePaths: [] }
for (let i = 0; i < inputTypes.length; i++) {
const fp = inputPaths[i]
if (!fp) continue
if (inputTypes[i] === 'mesh') {
out.nodeInputMeshPath = fp
} else if (inputTypes[i] === 'image') {
if (!out.nodeInputPath) out.nodeInputPath = fp
else out.extraImagePaths.push(fp)
} else if (inputTypes[i] === 'audio') {
if (!out.nodeInputPath) out.nodeInputPath = fp
}
}
return out
}
15 changes: 5 additions & 10 deletions src/areas/workflows/workflowRunStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { showCompletionNotification } from '@shared/utils/notification'
import type { WorkflowExtension } from './mockExtensions'
import type { Workflow, WFNode, WFEdge } from '@shared/types/electron.d'
import { isBranchStarter, isSceneOutput, resolveDataSource, reachesSceneOutput, nearestUpstreamWaits } from './nodeBehaviors'
import { assignSlotFilePaths } from './slotInputs'

// ─── Types ────────────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -344,16 +345,10 @@ async function executeExtensionNode(
}
}

for (let i = 0; i < inputTypes.length; i++) {
const fp = inputPaths[i]
if (!fp) continue
if (inputTypes[i] === 'mesh') {
nodeInputMeshPath = fp
} else if (inputTypes[i] === 'image') {
if (!nodeInputPath) nodeInputPath = fp
else extraImagePaths.push(fp)
}
}
const slots = assignSlotFilePaths(inputTypes, inputPaths)
nodeInputPath = slots.nodeInputPath
nodeInputMeshPath = slots.nodeInputMeshPath
extraImagePaths.push(...slots.extraImagePaths)
} else {
for (const edge of incomingEdges) {
const src = resolveSource(edge.source)
Expand Down