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

366 lines
9.9 KiB
TypeScript

import { describe, test, expect } from "vitest";
import {
findFreeSpot,
countSubgraphs,
topoSort,
resolveCurrentInputs,
computeStaleNodes,
buildUpstreamSubgraph
} from "./workflow-graph";
import type {
NodeStatus,
OperatorNode,
ReferenceNode,
SubjectNode,
WFEdge,
WFNode,
Workflow
} from "./workflow-types";
function ref(
id: string,
overrides: Partial<ReferenceNode> = {}
): ReferenceNode {
return {
id,
role: "reference",
label: id,
asset_type: "text",
inputs: [{ id: "in", label: "in", type: "text" }],
outputs: [{ id: "out", label: "out", type: "text" }],
data: {},
x: 0,
y: 0,
width: 200,
height: 160,
...overrides
};
}
function op(id: string, overrides: Partial<OperatorNode> = {}): OperatorNode {
return {
id,
role: "operator",
kind: "model",
label: id,
model_id: `user/${id}`,
inputs: [{ id: "in", label: "in", type: "text" }],
outputs: [{ id: "out", label: "out", type: "text" }],
data: {},
x: 0,
y: 0,
width: 280,
height: 90,
runtime: "client",
...overrides
};
}
function sub(id: string, overrides: Partial<SubjectNode> = {}): SubjectNode {
return {
id,
role: "subject",
label: id,
asset_type: "text",
inputs: [{ id: "in", label: "in", type: "text" }],
outputs: [{ id: "out", label: "out", type: "text" }],
data: {},
x: 0,
y: 0,
width: 200,
height: 160,
...overrides
};
}
function wf(overrides: Partial<Workflow> = {}): Workflow {
return {
schema_version: "2",
name: "Test",
runtime: { default: "client" },
references: [],
operators: [],
subjects: [],
edges: [],
view: { default: "canvas" },
...overrides
};
}
function edge(from: string, to: string, port = "in"): WFEdge {
return {
id: `${from}-${to}`,
from_node_id: from,
from_port_id: "out",
to_node_id: to,
to_port_id: port,
type: "text"
};
}
// Cast helpers to the WFNode shape `resolveCurrentInputs` / `topoSort`
// expect, mirroring `toLegacyShape`'s output.
function asLegacy(n: ReferenceNode | OperatorNode | SubjectNode): WFNode {
return {
id: n.id,
kind: n.role === "operator" ? "transform" : "component",
label: n.label,
source: n.role === "operator" ? "model" : "local",
inputs: n.inputs,
outputs: n.outputs,
x: n.x,
y: n.y,
width: n.width,
height: n.height,
data: n.data
};
}
describe("findFreeSpot", () => {
test("returns the input position unchanged when no node is nearby", () => {
expect(findFreeSpot([], 100, 100)).toEqual({ x: 100, y: 100 });
expect(findFreeSpot([{ x: 500, y: 500 }], 100, 100)).toEqual({
x: 100,
y: 100
});
});
test("cascades diagonally by 28px when the spot is taken", () => {
expect(findFreeSpot([{ x: 100, y: 100 }], 100, 100)).toEqual({
x: 128,
y: 128
});
});
test("keeps cascading until a free spot is found", () => {
const occupied = [
{ x: 100, y: 100 },
{ x: 128, y: 128 },
{ x: 156, y: 156 }
];
expect(findFreeSpot(occupied, 100, 100)).toEqual({ x: 184, y: 184 });
});
test("treats positions within 8px as overlapping", () => {
expect(findFreeSpot([{ x: 104, y: 104 }], 100, 100)).toEqual({
x: 128,
y: 128
});
});
});
describe("topoSort", () => {
test("orders independent nodes in input order", () => {
const a = asLegacy(ref("a"));
const b = asLegacy(ref("b"));
expect(topoSort([a, b], []).map((n) => n.id)).toEqual(["a", "b"]);
});
test("places upstream nodes before downstream ones", () => {
const a = asLegacy(ref("a"));
const b = asLegacy(op("b"));
const c = asLegacy(sub("c"));
const sorted = topoSort([c, b, a], [edge("a", "b"), edge("b", "c")]);
expect(sorted.map((n) => n.id)).toEqual(["a", "b", "c"]);
});
test("handles diamond dependencies", () => {
const a = asLegacy(ref("a"));
const b = asLegacy(op("b"));
const c = asLegacy(op("c"));
const d = asLegacy(sub("d"));
const sorted = topoSort(
[d, c, b, a],
[edge("a", "b"), edge("a", "c"), edge("b", "d"), edge("c", "d")]
);
const order = sorted.map((n) => n.id);
expect(order.indexOf("a")).toBeLessThan(order.indexOf("b"));
expect(order.indexOf("a")).toBeLessThan(order.indexOf("c"));
expect(order.indexOf("b")).toBeLessThan(order.indexOf("d"));
expect(order.indexOf("c")).toBeLessThan(order.indexOf("d"));
});
});
describe("countSubgraphs", () => {
test("returns 0 for an empty graph", () => {
expect(countSubgraphs([], [])).toBe(0);
});
test("counts each unconnected node as its own subgraph", () => {
const a = asLegacy(ref("a"));
const b = asLegacy(ref("b"));
const c = asLegacy(ref("c"));
expect(countSubgraphs([a, b, c], [])).toBe(3);
});
test("collapses a connected chain into one subgraph", () => {
const a = asLegacy(ref("a"));
const b = asLegacy(op("b"));
const c = asLegacy(sub("c"));
expect(countSubgraphs([a, b, c], [edge("a", "b"), edge("b", "c")])).toBe(1);
});
test("counts two independent pipelines separately", () => {
const a = asLegacy(ref("a"));
const b = asLegacy(sub("b"));
const c = asLegacy(ref("c"));
const d = asLegacy(sub("d"));
expect(countSubgraphs([a, b, c, d], [edge("a", "b"), edge("c", "d")])).toBe(
2
);
});
test("treats edges as undirected when merging components", () => {
const a = asLegacy(ref("a"));
const b = asLegacy(op("b"));
const c = asLegacy(ref("c"));
// a -> b and c -> b: both feed b, so all three are one subgraph.
expect(countSubgraphs([a, b, c], [edge("a", "b"), edge("c", "b")])).toBe(1);
});
});
describe("resolveCurrentInputs", () => {
test("falls back to inline data when no edge is wired", () => {
const node = asLegacy(ref("a", { data: { in: "hello" } }));
expect(resolveCurrentInputs(node, [node], [])).toEqual({ in: "hello" });
});
test("uses port default_value if no edge and no inline data", () => {
const node = asLegacy(
ref("a", {
inputs: [
{ id: "in", label: "in", type: "text", default_value: "fallback" }
]
})
);
expect(resolveCurrentInputs(node, [node], [])).toEqual({ in: "fallback" });
});
test("returns null when neither edge, data, nor default is present", () => {
const node = asLegacy(ref("a"));
expect(resolveCurrentInputs(node, [node], [])).toEqual({ in: null });
});
test("reads from upstream node's data when wired", () => {
const up = asLegacy(ref("up", { data: { out: "from-upstream" } }));
const down = asLegacy(op("down"));
expect(
resolveCurrentInputs(down, [up, down], [edge("up", "down")])
).toEqual({ in: "from-upstream" });
});
test("preferring edge over inline data on the downstream node", () => {
const up = asLegacy(ref("up", { data: { out: "wired" } }));
const down = asLegacy(op("down", { data: { in: "inline" } }));
expect(
resolveCurrentInputs(down, [up, down], [edge("up", "down")])
).toEqual({ in: "wired" });
});
});
describe("computeStaleNodes", () => {
test("never marks not-yet-run nodes as stale", () => {
const a = asLegacy(ref("a"));
const status: Record<string, NodeStatus> = {};
expect(computeStaleNodes([a], [], status, {})).toEqual(new Set());
});
test("marks a done node stale when its inputs differ from its snapshot", () => {
const a = asLegacy(ref("a", { data: { in: "now" } }));
const status = { a: "done" as NodeStatus };
const snapshots = { a: JSON.stringify({ in: "then" }) };
expect(computeStaleNodes([a], [], status, snapshots).has("a")).toBe(true);
});
test("does not mark a done node stale when inputs match", () => {
const a = asLegacy(ref("a", { data: { in: "same" } }));
const status = { a: "done" as NodeStatus };
const snapshots = { a: JSON.stringify({ in: "same" }) };
expect(computeStaleNodes([a], [], status, snapshots).has("a")).toBe(false);
});
test("propagates staleness transitively downstream", () => {
const up = asLegacy(ref("up", { data: { in: "now" } }));
const down = asLegacy(op("down", { data: { in: "matches" } }));
const edges = [edge("up", "down")];
const status = {
up: "done" as NodeStatus,
down: "done" as NodeStatus
};
const snapshots = {
up: JSON.stringify({ in: "then" }),
down: JSON.stringify({ in: "matches" })
};
const stale = computeStaleNodes([up, down], edges, status, snapshots);
expect(stale.has("up")).toBe(true);
expect(stale.has("down")).toBe(true);
});
test("skips snapshot-less nodes (never run successfully)", () => {
const a = asLegacy(ref("a", { data: { in: "now" } }));
const status = { a: "done" as NodeStatus };
expect(computeStaleNodes([a], [], status, {}).has("a")).toBe(false);
});
});
describe("buildUpstreamSubgraph", () => {
test("includes only the target when it has no upstream", () => {
const a = ref("a");
const result = buildUpstreamSubgraph(wf({ references: [a] }), "a");
expect(result.references).toHaveLength(1);
expect(result.operators).toHaveLength(0);
expect(result.subjects).toHaveLength(0);
});
test("walks transitively up the edge chain", () => {
const a = ref("a");
const b = op("b");
const c = op("c");
const d = sub("d");
const e = sub("e");
const result = buildUpstreamSubgraph(
wf({
references: [a],
operators: [b, c],
subjects: [d, e],
edges: [edge("a", "b"), edge("b", "c"), edge("c", "d"), edge("a", "e")]
}),
"d"
);
// d → c → b → a should all be included; e is unrelated.
const ids = [
...result.references.map((n) => n.id),
...result.operators.map((n) => n.id),
...result.subjects.map((n) => n.id)
].sort();
expect(ids).toEqual(["a", "b", "c", "d"]);
});
test("drops edges that touch nodes outside the subgraph", () => {
const a = ref("a");
const b = op("b");
const c = sub("c"); // unrelated
const result = buildUpstreamSubgraph(
wf({
references: [a],
operators: [b],
subjects: [c],
edges: [edge("a", "b"), edge("a", "c")]
}),
"b"
);
expect(result.edges.map((e) => e.id)).toEqual(["a-b"]);
});
test("preserves workflow-level metadata", () => {
const a = ref("a");
const source = wf({
name: "Original",
references: [a]
});
const result = buildUpstreamSubgraph(source, "a");
expect(result.schema_version).toBe("2");
expect(result.name).toBe("Original");
});
});