Files
wehub-resource-sync adf0d17497
publish / version_or_publish (push) Has been cancelled
storybook-build / changes (push) Has been cancelled
storybook-build / :storybook-build (push) Has been cancelled
Sync Gradio Skills to Hugging Face / sync-skills (push) Has been cancelled
functional / changes (push) Has been cancelled
functional / build-frontend (push) Has been cancelled
functional / functional-test-SSR=false (push) Has been cancelled
functional / functional-reload (push) Has been cancelled
js / changes (push) Has been cancelled
js / js-test (push) Has been cancelled
docs-build / changes (push) Has been cancelled
docs-build / docs-build (push) Has been cancelled
docs-build / website-build (push) Has been cancelled
functional / functional-test-SSR=true (push) Has been cancelled
hygiene / hygiene-test (push) Has been cancelled
python / changes (push) Has been cancelled
python / build (push) Has been cancelled
python / test-ubuntu-latest-flaky (push) Has been cancelled
python / test-ubuntu-latest-not-flaky (push) Has been cancelled
python / test-windows-latest-flaky (push) Has been cancelled
python / test-windows-latest-not-flaky (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:17:32 +08:00

504 lines
12 KiB
TypeScript

import { describe, test, expect, beforeEach } from "vitest";
import { get } from "svelte/store";
import {
workflow,
addOperator,
addEdge,
replaceNodeSource,
removeNode,
removeEdge,
sanitize_for_save,
switch_endpoint
} from "./workflow-store";
import type { OperatorNode, ReferenceNode, Workflow } from "./workflow-types";
function resetWorkflow(): void {
workflow.set({
schema_version: "2",
name: "Test",
runtime: { default: "client" },
references: [],
operators: [],
subjects: [],
edges: [],
view: { default: "canvas" }
});
}
describe("addOperator — dataset shape", () => {
beforeEach(resetWorkflow);
test("persists dataset config + split + row_index input port", () => {
const id = addOperator(
{
kind: "dataset",
label: "ds",
dataset_id: "user/ds",
dataset_config: "default",
dataset_split: "train",
inputs: [
{ id: "row_index", label: "Row", type: "number", default_value: 0 }
],
outputs: [{ id: "out_0", label: "v", type: "text" }],
width: 240,
height: 90,
runtime: "client"
} as unknown as Omit<OperatorNode, "id" | "role" | "x" | "y" | "data">,
0,
0
);
const op = get(workflow).operators.find((o) => o.id === id);
expect(op?.kind).toBe("dataset");
expect(op?.dataset_id).toBe("user/ds");
expect(op?.inputs.find((p) => p.id === "row_index")?.type).toBe("number");
});
});
describe("replaceNodeSource — dataset transitions", () => {
beforeEach(resetWorkflow);
test("switching away from a dataset clears dataset fields", () => {
const id = addOperator(
{
kind: "dataset",
label: "ds",
dataset_id: "user/ds",
inputs: [],
outputs: [],
width: 240,
height: 90,
runtime: "client"
} as unknown as Omit<OperatorNode, "id" | "role" | "x" | "y" | "data">,
0,
0
);
replaceNodeSource(id, {
label: "model",
kind: "model",
model_id: "user/m",
inputs: [{ id: "in", label: "x", type: "text" }],
outputs: [{ id: "out", label: "y", type: "text" }],
width: 280
});
const op = get(workflow).operators.find((o) => o.id === id);
expect(op?.dataset_id).toBeUndefined();
expect(op?.kind).toBe("model");
});
});
describe("removeNode", () => {
beforeEach(resetWorkflow);
function _makeOp(label: string): string {
return addOperator(
{
kind: "dataset",
label,
dataset_id: `user/${label}`,
inputs: [{ id: "in", label: "x", type: "text" }],
outputs: [{ id: "out", label: "y", type: "text" }],
width: 240,
height: 90,
runtime: "client"
} as unknown as Omit<OperatorNode, "id" | "role" | "x" | "y" | "data">,
0,
0
);
}
test("removes an operator from the store", () => {
const id = _makeOp("a");
removeNode(id);
expect(get(workflow).operators.find((o) => o.id === id)).toBeUndefined();
});
test("strips edges where removed node is the source", () => {
const a = _makeOp("a");
const b = _makeOp("b");
addEdge({
from_node_id: a,
from_port_id: "out",
to_node_id: b,
to_port_id: "in",
type: "text"
});
removeNode(a);
expect(get(workflow).edges).toEqual([]);
});
test("strips edges where removed node is the target", () => {
const a = _makeOp("a");
const b = _makeOp("b");
addEdge({
from_node_id: a,
from_port_id: "out",
to_node_id: b,
to_port_id: "in",
type: "text"
});
removeNode(b);
expect(get(workflow).edges).toEqual([]);
});
test("preserves edges between other nodes", () => {
const a = _makeOp("a");
const b = _makeOp("b");
const c = _makeOp("c");
addEdge({
from_node_id: a,
from_port_id: "out",
to_node_id: b,
to_port_id: "in",
type: "text"
});
addEdge({
from_node_id: b,
from_port_id: "out",
to_node_id: c,
to_port_id: "in",
type: "text"
});
removeNode(a);
const edges = get(workflow).edges;
expect(edges).toHaveLength(1);
expect(edges[0].from_node_id).toBe(b);
expect(edges[0].to_node_id).toBe(c);
});
test("no-op when id doesn't exist", () => {
const a = _makeOp("a");
removeNode("nonexistent");
expect(get(workflow).operators.find((o) => o.id === a)).toBeDefined();
});
});
describe("removeEdge", () => {
beforeEach(resetWorkflow);
function _makeOp(label: string): string {
return addOperator(
{
kind: "fn",
label,
fn: label,
inputs: [{ id: "in", label: "x", type: "text" }],
outputs: [{ id: "out", label: "y", type: "text" }],
width: 200,
height: 80
} as unknown as Omit<OperatorNode, "id" | "role" | "x" | "y" | "data">,
0,
0
);
}
test("removes only the targeted edge", () => {
const a = _makeOp("a");
const b = _makeOp("b");
const c = _makeOp("c");
addEdge({
from_node_id: a,
from_port_id: "out",
to_node_id: b,
to_port_id: "in",
type: "text"
});
addEdge({
from_node_id: b,
from_port_id: "out",
to_node_id: c,
to_port_id: "in",
type: "text"
});
const firstEdgeId = get(workflow).edges[0].id;
removeEdge(firstEdgeId);
const edges = get(workflow).edges;
expect(edges).toHaveLength(1);
expect(edges[0].from_node_id).toBe(b);
});
test("no-op when id doesn't exist", () => {
const a = _makeOp("a");
const b = _makeOp("b");
addEdge({
from_node_id: a,
from_port_id: "out",
to_node_id: b,
to_port_id: "in",
type: "text"
});
removeEdge("nonexistent");
expect(get(workflow).edges).toHaveLength(1);
});
test("does not touch nodes", () => {
const a = _makeOp("a");
const b = _makeOp("b");
addEdge({
from_node_id: a,
from_port_id: "out",
to_node_id: b,
to_port_id: "in",
type: "text"
});
const edgeId = get(workflow).edges[0].id;
removeEdge(edgeId);
expect(get(workflow).operators).toHaveLength(2);
});
});
describe("switch_endpoint", () => {
beforeEach(resetWorkflow);
function _spaceOp(): string {
return addOperator(
{
kind: "space",
label: "moondream2",
space_id: "user/moondream2",
endpoint: "/process_answer",
endpoints: [
{
name: "/answer_question",
inputs: [
{ id: "in_0", label: "img", type: "image" },
{ id: "in_1", label: "prompt", type: "text" }
],
outputs: [
{
id: "out_0",
label: "Response",
type: "text",
output_index: 0
}
]
},
{
name: "/process_answer",
inputs: [
{ id: "in_0", label: "img", type: "image" },
{ id: "in_1", label: "answer", type: "text" }
],
outputs: [
{
id: "out_0",
label: "Annotated",
type: "image",
output_index: 0
}
]
}
],
inputs: [
{ id: "in_0", label: "img", type: "image" },
{ id: "in_1", label: "answer", type: "text" }
],
outputs: [
{ id: "out_0", label: "Annotated", type: "image", output_index: 0 }
],
width: 280,
height: 90
} as unknown as Omit<OperatorNode, "id" | "role" | "x" | "y" | "data">,
0,
0
);
}
test("switches inputs and outputs to the new endpoint signature", () => {
const id = _spaceOp();
switch_endpoint(id, "/answer_question");
const op = get(workflow).operators.find((o) => o.id === id);
expect(op?.endpoint).toBe("/answer_question");
expect(op?.outputs[0].type).toBe("text");
expect(op?.outputs[0].label).toBe("Response");
});
test("no-op when switching to the current endpoint", () => {
const id = _spaceOp();
const before = get(workflow);
switch_endpoint(id, "/process_answer");
expect(get(workflow)).toBe(before);
});
test("no-op for unknown endpoint name", () => {
const id = _spaceOp();
const before = get(workflow);
switch_endpoint(id, "/nonexistent");
expect(get(workflow)).toBe(before);
});
test("drops edges to removed input ports", () => {
const id = _spaceOp();
const upstream = addOperator(
{
kind: "fn",
label: "src",
fn: "src",
inputs: [],
outputs: [{ id: "out", label: "y", type: "image" }],
width: 200,
height: 80
} as unknown as Omit<OperatorNode, "id" | "role" | "x" | "y" | "data">,
0,
0
);
addEdge({
from_node_id: upstream,
from_port_id: "out",
to_node_id: id,
to_port_id: "in_0",
type: "image"
});
switch_endpoint(id, "/answer_question");
expect(get(workflow).edges).toHaveLength(1);
});
test("drops edges that become type-incompatible after the switch", () => {
const id = _spaceOp();
const downstream = addOperator(
{
kind: "fn",
label: "sink",
fn: "sink",
inputs: [{ id: "in", label: "x", type: "image" }],
outputs: [],
width: 200,
height: 80
} as unknown as Omit<OperatorNode, "id" | "role" | "x" | "y" | "data">,
0,
0
);
addEdge({
from_node_id: id,
from_port_id: "out_0",
to_node_id: downstream,
to_port_id: "in",
type: "image"
});
switch_endpoint(id, "/answer_question");
expect(get(workflow).edges).toHaveLength(0);
});
test("preserves edges when the port type matches the new endpoint's port", () => {
const id = _spaceOp();
const upstream = addOperator(
{
kind: "fn",
label: "src",
fn: "src",
inputs: [],
outputs: [{ id: "out", label: "y", type: "text" }],
width: 200,
height: 80
} as unknown as Omit<OperatorNode, "id" | "role" | "x" | "y" | "data">,
0,
0
);
addEdge({
from_node_id: upstream,
from_port_id: "out",
to_node_id: id,
to_port_id: "in_1",
type: "text"
});
switch_endpoint(id, "/answer_question");
expect(get(workflow).edges).toHaveLength(1);
});
test("clears node.data so stale values don't bleed into new ports", () => {
const id = _spaceOp();
workflow.update((wf) => ({
...wf,
operators: wf.operators.map((o) =>
o.id === id ? { ...o, data: { in_1: "stale value" } } : o
)
}));
switch_endpoint(id, "/answer_question");
const op = get(workflow).operators.find((o) => o.id === id);
expect(op?.data).toEqual({});
});
});
describe("sanitize_for_save", () => {
function wf(refs: ReferenceNode[] = []): Workflow {
return {
schema_version: "2",
name: "Test",
runtime: { default: "client" },
references: refs,
operators: [],
subjects: [],
edges: [],
view: { default: "canvas" }
};
}
function refNode(id: string, data: Record<string, unknown>): ReferenceNode {
return {
id,
role: "reference",
label: id,
asset_type: "image",
inputs: [{ id: "in", label: "in", type: "image" }],
outputs: [{ id: "out", label: "out", type: "image" }],
data: data as ReferenceNode["data"],
x: 0,
y: 0,
width: 220,
height: 160
};
}
test("strips blob: URLs from node data", () => {
const w = wf([
refNode("a", {
in: { name: "img.png", url: "blob:http://x/abc", mime: "image/png" }
})
]);
expect(sanitize_for_save(w).references[0].data).toEqual({});
});
test("strips data: URLs from node data", () => {
const w = wf([
refNode("a", {
in: {
name: "img.png",
url: "data:image/png;base64,iVBOR",
mime: "image/png"
}
})
]);
expect(sanitize_for_save(w).references[0].data).toEqual({});
});
test("preserves server-served file URLs (http / file paths)", () => {
const fileVal = {
name: "out.png",
url: "/file=/tmp/out.png",
mime: "image/png"
};
const w = wf([refNode("a", { in: fileVal })]);
expect(sanitize_for_save(w).references[0].data).toEqual({ in: fileVal });
});
test("preserves non-file primitive data (text, numbers, booleans)", () => {
const w = wf([
refNode("a", { in: "hello world" }),
refNode("b", { in: 42 }),
refNode("c", { in: true })
]);
const result = sanitize_for_save(w);
expect(result.references[0].data).toEqual({ in: "hello world" });
expect(result.references[1].data).toEqual({ in: 42 });
expect(result.references[2].data).toEqual({ in: true });
});
test("does not mutate the input workflow", () => {
const original = wf([
refNode("a", { in: { name: "x", url: "blob:nope" } })
]);
const snapshot = JSON.stringify(original);
sanitize_for_save(original);
expect(JSON.stringify(original)).toBe(snapshot);
});
});