Publish: Koolook_PublishOutput output_mode/input_switch/switch; setupSurface.app.outputSwitch; routers auto-bound as the output selector; unwired output types hidden; 'Same as input' offered only when every input type has a wired writer; result path follows the output switch. Pipeline: EasyAIPipeline IS_CHANGED keeps version=auto/next perpetually dirty so the vNNN disk scan re-runs every queue; literal versions stay cacheable.
231 lines
8.5 KiB
Python
231 lines
8.5 KiB
Python
from __future__ import annotations
|
|
|
|
import subprocess
|
|
import textwrap
|
|
from pathlib import Path
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
|
|
|
|
def run_node_scenario(source: str) -> subprocess.CompletedProcess[str]:
|
|
return subprocess.run(
|
|
["node", "--input-type=module"],
|
|
input=source,
|
|
cwd=REPO_ROOT,
|
|
text=True,
|
|
capture_output=True,
|
|
check=False,
|
|
)
|
|
|
|
|
|
def test_infer_setup_surface_from_koolook_groups() -> None:
|
|
script = textwrap.dedent(
|
|
"""
|
|
import assert from "node:assert/strict";
|
|
import { inferSetupSurface } from "./web/sidebar/published_surface.js";
|
|
|
|
const surface = inferSetupSurface({
|
|
nodes: [
|
|
{ id: 12, type: "Load Image", title: "Source image", pos: [40, 40], size: [180, 80] },
|
|
{ id: 20, type: "Preview Image", title: "Preview", pos: [420, 40], size: [180, 80] },
|
|
{ id: 30, type: "Note", title: "Outside", pos: [800, 40], size: [180, 80] },
|
|
],
|
|
groups: [
|
|
{ title: "Koolook Input", bounding: [20, 20, 240, 140] },
|
|
{ title: "Koolook Output", pos: [400, 20], size: [240, 140] },
|
|
],
|
|
});
|
|
|
|
assert.deepEqual(surface, {
|
|
sourceInputs: [{
|
|
group: "Koolook Input",
|
|
nodes: [{ id: "12", type: "Load Image", title: "Source image" }],
|
|
}],
|
|
outputs: [{
|
|
group: "Koolook Output",
|
|
nodes: [{ id: "20", type: "Preview Image", title: "Preview" }],
|
|
}],
|
|
controls: [],
|
|
app: { inputs: [], outputs: [], results: [] },
|
|
});
|
|
"""
|
|
)
|
|
|
|
result = run_node_scenario(script)
|
|
assert result.returncode == 0, result.stderr
|
|
|
|
|
|
def test_infer_setup_surface_app_contract_from_publish_nodes() -> None:
|
|
script = textwrap.dedent(
|
|
"""
|
|
import assert from "node:assert/strict";
|
|
import { inferSetupSurface } from "./web/sidebar/published_surface.js";
|
|
|
|
const surface = inferSetupSurface({
|
|
nodes: [
|
|
{
|
|
id: 100,
|
|
type: "Koolook_PublishInput",
|
|
title: "Koolook Publish Input",
|
|
pos: [40, 60],
|
|
size: [360, 320],
|
|
widgets_values: [
|
|
"Img",
|
|
"/shots/example/frames",
|
|
"/shots/example/movie.mov",
|
|
"/shots/example/image.png",
|
|
"unused prompt",
|
|
],
|
|
},
|
|
{
|
|
id: 200,
|
|
type: "Koolook_PublishOutput",
|
|
title: "Koolook Publish Output",
|
|
pos: [520, 60],
|
|
size: [360, 240],
|
|
widgets_values: [
|
|
"/shots/example/output",
|
|
"publish-OUT",
|
|
"1",
|
|
],
|
|
},
|
|
{
|
|
id: 300,
|
|
type: "Koolook_PublishResult",
|
|
title: "Koolook Publish Result",
|
|
pos: [520, 340],
|
|
size: [360, 160],
|
|
widgets_values: ["/shots/example/output/publish-OUT_v001.mov"],
|
|
},
|
|
],
|
|
groups: [
|
|
{ title: "Koolook Input", bounding: [20, 20, 440, 420] },
|
|
{ title: "Koolook Output", bounding: [500, 20, 360, 560] },
|
|
],
|
|
});
|
|
|
|
assert.deepEqual(surface.app, {
|
|
inputs: [
|
|
{ key: "sequence_folder", label: "Sequence folder", visible: true, target: { node: "100", input: "sequence_folder" }, default: "/shots/example/frames" },
|
|
{ key: "qt_file", label: "QT file", visible: true, target: { node: "100", input: "qt_file" }, default: "/shots/example/movie.mov" },
|
|
{ key: "single_file", label: "Single file", visible: true, target: { node: "100", input: "single_file" }, default: "/shots/example/image.png" },
|
|
{ key: "prompt", label: "Prompt", visible: true, standalone: true, multiline: true, target: { node: "100", input: "prompt" }, default: "", placeholder: "unused prompt", help: "Describe the shot in one simple line: subject + action + setting." },
|
|
],
|
|
outputs: [
|
|
{ key: "folder", label: "Output folder", visible: true, target: { node: "200", input: "folder" }, default: "/shots/example/output" },
|
|
{ key: "name", label: "Output name", visible: true, target: { node: "200", input: "name" }, default: "publish-OUT" },
|
|
{ key: "version", label: "Version", visible: true, target: { node: "200", input: "version" }, default: "1" },
|
|
],
|
|
results: [
|
|
{ key: "result", label: "Result", visible: true, target: { node: "300", input: "result" }, default: "/shots/example/output/publish-OUT_v001.mov" },
|
|
],
|
|
switch: {
|
|
key: "switch",
|
|
label: "Input type",
|
|
visible: true,
|
|
target: { node: "100", input: "mode" },
|
|
default: 2,
|
|
options: [
|
|
{ value: 0, label: "EXR", visible: true, input: "sequence_folder" },
|
|
{ value: 1, label: "QT", visible: true, input: "qt_file" },
|
|
{ value: 2, label: "Img", visible: true, input: "single_file" },
|
|
{ value: 3, label: "Prompt", visible: false, input: "prompt" },
|
|
],
|
|
},
|
|
outputSwitch: {
|
|
key: "output_switch",
|
|
label: "Output type",
|
|
visible: true,
|
|
sameAsInput: true,
|
|
target: { node: "200", input: "output_mode" },
|
|
default: -1,
|
|
options: [
|
|
{ value: 0, label: "EXR", visible: true },
|
|
{ value: 1, label: "QT", visible: true },
|
|
{ value: 2, label: "Img", visible: true },
|
|
],
|
|
},
|
|
});
|
|
"""
|
|
)
|
|
|
|
result = run_node_scenario(script)
|
|
assert result.returncode == 0, result.stderr
|
|
|
|
|
|
def test_prompt_is_an_always_on_field_not_a_source_mode() -> None:
|
|
# The prompt must render independently of the EXR/QT/Img source switch:
|
|
# visible + standalone, with the author's prompt-widget text surfaced as a
|
|
# placeholder hint (not a submitted default), and excluded from the switch's
|
|
# source options.
|
|
script = textwrap.dedent(
|
|
"""
|
|
import assert from "node:assert/strict";
|
|
import { inferSetupSurface } from "./web/sidebar/published_surface.js";
|
|
|
|
const surface = inferSetupSurface({
|
|
nodes: [
|
|
{
|
|
id: 100,
|
|
type: "Koolook_PublishInput",
|
|
title: "Koolook Publish Input",
|
|
pos: [40, 60],
|
|
size: [360, 320],
|
|
widgets_values: [
|
|
"EXR",
|
|
"/shots/example/frames",
|
|
"/shots/example/movie.mov",
|
|
"/shots/example/image.png",
|
|
"a bear talking to the camera in a sunny forest",
|
|
],
|
|
},
|
|
],
|
|
groups: [
|
|
{ title: "Koolook Input", bounding: [20, 20, 440, 420] },
|
|
],
|
|
});
|
|
|
|
const prompt = surface.app.inputs.find(field => field.key === "prompt");
|
|
assert.ok(prompt, "prompt field is present");
|
|
assert.equal(prompt.visible, true);
|
|
assert.equal(prompt.standalone, true);
|
|
assert.equal(prompt.multiline, true);
|
|
assert.equal(prompt.default, "");
|
|
assert.equal(prompt.placeholder, "a bear talking to the camera in a sunny forest");
|
|
assert.ok(prompt.help && prompt.help.length > 0, "prompt carries a help hint");
|
|
|
|
// The prompt is never offered as a source mode in the switch.
|
|
const promptOption = surface.app.switch.options.find(o => o.input === "prompt");
|
|
assert.equal(promptOption.visible, false);
|
|
const visibleSources = surface.app.switch.options.filter(o => o.visible !== false).map(o => o.input);
|
|
assert.deepEqual(visibleSources, ["sequence_folder", "qt_file", "single_file"]);
|
|
"""
|
|
)
|
|
|
|
result = run_node_scenario(script)
|
|
assert result.returncode == 0, result.stderr
|
|
|
|
|
|
def test_infer_setup_surface_ignores_malformed_group_geometry() -> None:
|
|
script = textwrap.dedent(
|
|
"""
|
|
import assert from "node:assert/strict";
|
|
import { inferSetupSurface } from "./web/sidebar/published_surface.js";
|
|
|
|
const surface = inferSetupSurface({
|
|
nodes: [
|
|
{ id: 12, type: "Load Image", title: "Source image", pos: [40, 40], size: [180, 80] },
|
|
],
|
|
groups: [
|
|
{ title: "Koolook Input", bounding: ["bad", 20, 240, 140] },
|
|
],
|
|
});
|
|
|
|
assert.deepEqual(surface.sourceInputs, []);
|
|
"""
|
|
)
|
|
|
|
result = run_node_scenario(script)
|
|
assert result.returncode == 0, result.stderr
|