test(canvas): WORKSPACE_PROVISIONING parentId+coord coverage + growParentsToFitChildren tests #1236

Open
fullstack-engineer wants to merge 2 commits from feat/canvas-growParentsToFitChildren-coverage into staging
2 changed files with 467 additions and 0 deletions
@@ -332,6 +332,105 @@ describe("handleCanvasEvent WORKSPACE_PROVISIONING", () => {
const bPos = lastNodes.find((n) => n.id === "ws-b")!.position;
expect(bPos).toEqual({ x: 420, y: 100 }); // idx 1 = (100 + 320, 100)
});
it("uses finalX/finalY from payload when parentId is set and parent exists in store", () => {
// Org-import child lands with explicit coords — these are server-computed
// parent-relative positions. The handler must trust them verbatim.
const parent = makeNode("parent-root", { name: "Root" });
const { get, set } = makeStore([parent]);
handleCanvasEvent(
makeMsg({
event: "WORKSPACE_PROVISIONING",
workspace_id: "child-org",
payload: {
name: "Org Child",
tier: 2,
parent_id: "parent-root",
x: 500,
y: 300,
},
}),
get,
set
);
const newNodes = (set.mock.calls[0][0] as { nodes: Node<WorkspaceNodeData>[] }).nodes;
expect(newNodes).toHaveLength(2);
const child = newNodes.find((n) => n.id === "child-org")!;
// Must use the server-provided coords, not grid
expect(child.position).toEqual({ x: 500, y: 300 });
// Must bind parentId so RF renders it nested inside the parent card
expect(child.parentId).toBe("parent-root");
expect(child.data.parentId).toBe("parent-root");
expect(child.data.name).toBe("Org Child");
expect(child.data.status).toBe("provisioning");
});
it("uses grid position when parentId is set but parent is NOT in store yet", () => {
// Rare WS-reorder: child event arrives before parent's PROVISIONING event.
// Must not crash — uses grid slot as fallback. Parent will reparent
// the child when it lands.
const { get, set } = makeStore([]);
handleCanvasEvent(
makeMsg({
event: "WORKSPACE_PROVISIONING",
workspace_id: "orphan-child",
payload: {
name: "Orphan",
parent_id: "unknown-parent",
x: 999,
y: 888,
},
}),
get,
set
);
const newNodes = (set.mock.calls[0][0] as { nodes: Node<WorkspaceNodeData>[] }).nodes;
const child = newNodes.find((n) => n.id === "orphan-child")!;
// Must NOT use finalX/finalY — parent isn't in store so grid slot is used
expect(child.position).not.toEqual({ x: 999, y: 888 });
// Grid slot for idx 0: (100, 100)
expect(child.position).toEqual({ x: 100, y: 100 });
// parentId is NOT set on the node when parent is unknown:
// the node will be reparented when the parent eventually lands
expect(child.data.parentId).not.toBe("unknown-parent");
});
it("no-op cascade: parent in store but no finalX/Y → grid position, no parentId", () => {
// Parent exists but payload has no x/y → must not crash, uses grid slot.
// parentId is NOT set because we don't have parent-relative coords.
const parent = makeNode("parent-exists");
const { get, set } = makeStore([parent]);
handleCanvasEvent(
makeMsg({
event: "WORKSPACE_PROVISIONING",
workspace_id: "child-no-coords",
payload: {
name: "No Coords",
parent_id: "parent-exists",
// no x or y
},
}),
get,
set
);
const newNodes = (set.mock.calls[0][0] as { nodes: Node<WorkspaceNodeData>[] }).nodes;
const child = newNodes.find((n) => n.id === "child-no-coords")!;
// Grid slot for idx 0: (100, 100)
expect(child.position).toEqual({ x: 100, y: 100 });
// parentId stays null (not undefined) when no finalX/Y — server has no
// position for this node, and the handler initialises parentId=null
expect(child.parentId).toBeUndefined();
expect(child.data.parentId).toBeNull();
});
});
// ---------------------------------------------------------------------------
+368
View File
@@ -848,6 +848,374 @@ describe("hydrationError", () => {
});
});
// ---------- growParentsToFitChildren ----------
//
// growParentsToFitChildren walks every parent node and expands its width/height
// so all children fit inside with padding. Collapsed parents are skipped (grow-
// only, never shrink). Returns the same array reference when no changes are
// needed, a new array when at least one parent grew.
//
// Constants (from canvas-topology.ts):
// CHILD_DEFAULT_WIDTH = 240
// CHILD_DEFAULT_HEIGHT = 130
// PARENT_SIDE_PADDING = 16
// PARENT_BOTTOM_PADDING = 16
//
// For a child at (childX, childY) with size (childW, childH):
// requiredParentW = childX + childW + PARENT_SIDE_PADDING
// requiredParentH = childY + childH + PARENT_BOTTOM_PADDING
//
// Coverage targets:
// - Node with no parentId → skipped entirely (returns same node)
// - Parent with no children → skipped (kids.length === 0 → returns n)
// - Collapsed parent → skipped even when children overflow
// - Child fits within existing parent → no-op (requiredW <= currentW && requiredH <= currentH)
// - Child overflows parent width → grows width only
// - Child overflows parent height → grows height only
// - Child overflows both → grows both
// - Missing measured.width (falls back to width, then CHILD_DEFAULT_WIDTH)
// - Missing measured.height (falls back to height, then CHILD_DEFAULT_HEIGHT)
// - Missing parent measured.width (falls back to width, then 0)
// - Missing parent measured.height (falls back to height, then 0)
// - No change at all → returns same array reference (changed=false path)
describe("growParentsToFitChildren", () => {
it("skips nodes with no parentId (standalone roots)", () => {
useCanvasStore.setState({
nodes: [
{
id: "root",
type: "workspaceNode",
position: { x: 0, y: 0 },
data: { name: "Root", status: "online", tier: 1, agentCard: null, activeTasks: 0, collapsed: false, role: "agent", lastErrorRate: 0, lastSampleError: "", url: "", parentId: null, currentTask: "", needsRestart: false, runtime: "", budgetLimit: null },
measured: { width: 200, height: 150 },
},
],
});
const before = useCanvasStore.getState().nodes;
useCanvasStore.getState().growParentsToFitChildren();
const after = useCanvasStore.getState().nodes;
// Same array reference (no change needed)
expect(after).toBe(before);
});
it("skips parent with no children (orphan parentId)", () => {
useCanvasStore.setState({
nodes: [
{
id: "orphan",
type: "workspaceNode",
position: { x: 0, y: 0 },
parentId: "nonexistent",
data: { name: "Orphan", status: "online", tier: 1, agentCard: null, activeTasks: 0, collapsed: false, role: "agent", lastErrorRate: 0, lastSampleError: "", url: "", parentId: null, currentTask: "", needsRestart: false, runtime: "", budgetLimit: null },
measured: { width: 100, height: 100 },
},
],
});
const before = useCanvasStore.getState().nodes;
useCanvasStore.getState().growParentsToFitChildren();
const after = useCanvasStore.getState().nodes;
// Same array reference (parentId exists but no children reference it)
expect(after).toBe(before);
expect(after[0].measured).toEqual({ width: 100, height: 100 });
});
it("skips collapsed parents even when children overflow", () => {
// Child at (500, 400) → requires parent 500+240+16=756w, 400+130+16=546h
// Parent is collapsed AND tiny — must NOT grow
useCanvasStore.setState({
nodes: [
{
id: "parent",
type: "workspaceNode",
position: { x: 0, y: 0 },
data: { name: "Parent", status: "online", tier: 1, agentCard: null, activeTasks: 0, collapsed: true, role: "agent", lastErrorRate: 0, lastSampleError: "", url: "", parentId: null, currentTask: "", needsRestart: false, runtime: "", budgetLimit: null },
measured: { width: 200, height: 150 },
},
{
id: "child",
type: "workspaceNode",
position: { x: 500, y: 400 },
parentId: "parent",
data: { name: "Child", status: "online", tier: 1, agentCard: null, activeTasks: 0, collapsed: false, role: "agent", lastErrorRate: 0, lastSampleError: "", url: "", parentId: "parent", currentTask: "", needsRestart: false, runtime: "", budgetLimit: null },
measured: { width: 240, height: 130 },
},
],
});
const before = useCanvasStore.getState().nodes;
useCanvasStore.getState().growParentsToFitChildren();
const after = useCanvasStore.getState().nodes;
// Same reference (collapsed → skipped entirely)
expect(after).toBe(before);
const parent = after.find((n) => n.id === "parent")!;
expect(parent.measured).toEqual({ width: 200, height: 150 });
});
it("no-op when child fits within existing parent size", () => {
// Child at (0,0) 240x130 → requires 0+240+16=256w, 0+130+16=146h
// Parent is exactly 256×146 → fits perfectly
useCanvasStore.setState({
nodes: [
{
id: "parent",
type: "workspaceNode",
position: { x: 0, y: 0 },
data: { name: "Parent", status: "online", tier: 1, agentCard: null, activeTasks: 0, collapsed: false, role: "agent", lastErrorRate: 0, lastSampleError: "", url: "", parentId: null, currentTask: "", needsRestart: false, runtime: "", budgetLimit: null },
measured: { width: 256, height: 146 },
},
{
id: "child",
type: "workspaceNode",
position: { x: 0, y: 0 },
parentId: "parent",
data: { name: "Child", status: "online", tier: 1, agentCard: null, activeTasks: 0, collapsed: false, role: "agent", lastErrorRate: 0, lastSampleError: "", url: "", parentId: "parent", currentTask: "", needsRestart: false, runtime: "", budgetLimit: null },
measured: { width: 240, height: 130 },
},
],
});
const before = useCanvasStore.getState().nodes;
useCanvasStore.getState().growParentsToFitChildren();
const after = useCanvasStore.getState().nodes;
// Same array reference (no change needed)
expect(after).toBe(before);
const parent = after.find((n) => n.id === "parent")!;
expect(parent.measured).toEqual({ width: 256, height: 146 });
});
it("grows parent width only when child overflows width but not height", () => {
// Child at (100, 0) 240x130 → requires 100+240+16=356w, 0+130+16=146h
// Parent is 256×146 → fits height, overflows width → grows to 356×146
useCanvasStore.setState({
nodes: [
{
id: "parent",
type: "workspaceNode",
position: { x: 0, y: 0 },
data: { name: "Parent", status: "online", tier: 1, agentCard: null, activeTasks: 0, collapsed: false, role: "agent", lastErrorRate: 0, lastSampleError: "", url: "", parentId: null, currentTask: "", needsRestart: false, runtime: "", budgetLimit: null },
measured: { width: 256, height: 146 },
},
{
id: "child",
type: "workspaceNode",
position: { x: 100, y: 0 },
parentId: "parent",
data: { name: "Child", status: "online", tier: 1, agentCard: null, activeTasks: 0, collapsed: false, role: "agent", lastErrorRate: 0, lastSampleError: "", url: "", parentId: "parent", currentTask: "", needsRestart: false, runtime: "", budgetLimit: null },
measured: { width: 240, height: 130 },
},
],
});
useCanvasStore.getState().growParentsToFitChildren();
const parent = useCanvasStore.getState().nodes.find((n) => n.id === "parent")!;
expect(parent.width).toBe(356); // 100+240+16
expect(parent.height).toBe(146); // unchanged
});
it("grows parent height only when child overflows height but not width", () => {
// Child at (0, 50) 240x130 → requires 0+240+16=256w, 50+130+16=196h
// Parent is 256×146 → fits width, overflows height → grows to 256×196
useCanvasStore.setState({
nodes: [
{
id: "parent",
type: "workspaceNode",
position: { x: 0, y: 0 },
data: { name: "Parent", status: "online", tier: 1, agentCard: null, activeTasks: 0, collapsed: false, role: "agent", lastErrorRate: 0, lastSampleError: "", url: "", parentId: null, currentTask: "", needsRestart: false, runtime: "", budgetLimit: null },
measured: { width: 256, height: 146 },
},
{
id: "child",
type: "workspaceNode",
position: { x: 0, y: 50 },
parentId: "parent",
data: { name: "Child", status: "online", tier: 1, agentCard: null, activeTasks: 0, collapsed: false, role: "agent", lastErrorRate: 0, lastSampleError: "", url: "", parentId: "parent", currentTask: "", needsRestart: false, runtime: "", budgetLimit: null },
measured: { width: 240, height: 130 },
},
],
});
useCanvasStore.getState().growParentsToFitChildren();
const parent = useCanvasStore.getState().nodes.find((n) => n.id === "parent")!;
expect(parent.width).toBe(256); // unchanged
expect(parent.height).toBe(196); // 50+130+16
});
it("grows parent in both dimensions when child overflows both", () => {
// Child at (200, 100) 240x130 → requires 200+240+16=456w, 100+130+16=246h
// Parent is 256×146 → grows to 456×246
useCanvasStore.setState({
nodes: [
{
id: "parent",
type: "workspaceNode",
position: { x: 0, y: 0 },
data: { name: "Parent", status: "online", tier: 1, agentCard: null, activeTasks: 0, collapsed: false, role: "agent", lastErrorRate: 0, lastSampleError: "", url: "", parentId: null, currentTask: "", needsRestart: false, runtime: "", budgetLimit: null },
measured: { width: 256, height: 146 },
},
{
id: "child",
type: "workspaceNode",
position: { x: 200, y: 100 },
parentId: "parent",
data: { name: "Child", status: "online", tier: 1, agentCard: null, activeTasks: 0, collapsed: false, role: "agent", lastErrorRate: 0, lastSampleError: "", url: "", parentId: "parent", currentTask: "", needsRestart: false, runtime: "", budgetLimit: null },
measured: { width: 240, height: 130 },
},
],
});
useCanvasStore.getState().growParentsToFitChildren();
const parent = useCanvasStore.getState().nodes.find((n) => n.id === "parent")!;
expect(parent.width).toBe(456); // 200+240+16
expect(parent.height).toBe(246); // 100+130+16
});
it("uses CHILD_DEFAULT_WIDTH/HEIGHT when child has no measured or explicit dimensions", () => {
// Child with NO measured, NO width/height → falls back to 240×130 defaults
// Child at (500, 200) → requires 500+240+16=756w, 200+130+16=346h
useCanvasStore.setState({
nodes: [
{
id: "parent",
type: "workspaceNode",
position: { x: 0, y: 0 },
data: { name: "Parent", status: "online", tier: 1, agentCard: null, activeTasks: 0, collapsed: false, role: "agent", lastErrorRate: 0, lastSampleError: "", url: "", parentId: null, currentTask: "", needsRestart: false, runtime: "", budgetLimit: null },
measured: { width: 100, height: 100 },
},
{
id: "child",
type: "workspaceNode",
position: { x: 500, y: 200 },
parentId: "parent",
// No measured, no width/height → uses CHILD_DEFAULT_WIDTH=240, CHILD_DEFAULT_HEIGHT=130
data: { name: "Child", status: "online", tier: 1, agentCard: null, activeTasks: 0, collapsed: false, role: "agent", lastErrorRate: 0, lastSampleError: "", url: "", parentId: "parent", currentTask: "", needsRestart: false, runtime: "", budgetLimit: null },
},
],
});
useCanvasStore.getState().growParentsToFitChildren();
const parent = useCanvasStore.getState().nodes.find((n) => n.id === "parent")!;
expect(parent.width).toBe(756); // 500+240+16
expect(parent.height).toBe(346); // 200+130+16
});
it("uses explicit width/height when measured is absent on child", () => {
// Child has width/height but NOT measured
// Child at (300, 50) with explicit 200×100 → requires 300+200+16=516w, 50+100+16=166h
useCanvasStore.setState({
nodes: [
{
id: "parent",
type: "workspaceNode",
position: { x: 0, y: 0 },
data: { name: "Parent", status: "online", tier: 1, agentCard: null, activeTasks: 0, collapsed: false, role: "agent", lastErrorRate: 0, lastSampleError: "", url: "", parentId: null, currentTask: "", needsRestart: false, runtime: "", budgetLimit: null },
measured: { width: 200, height: 100 },
},
{
id: "child",
type: "workspaceNode",
position: { x: 300, y: 50 },
parentId: "parent",
width: 200,
height: 100,
// No measured → falls back to width=200, height=100
data: { name: "Child", status: "online", tier: 1, agentCard: null, activeTasks: 0, collapsed: false, role: "agent", lastErrorRate: 0, lastSampleError: "", url: "", parentId: "parent", currentTask: "", needsRestart: false, runtime: "", budgetLimit: null },
},
],
});
useCanvasStore.getState().growParentsToFitChildren();
const parent = useCanvasStore.getState().nodes.find((n) => n.id === "parent")!;
expect(parent.width).toBe(516); // 300+200+16
expect(parent.height).toBe(166); // 50+100+16
});
it("uses measured when present (takes precedence over explicit width/height)", () => {
// Child has both measured AND explicit width/height — measured should win
// Child at (0,0) measured=240×130 explicit=100×50 → uses measured
// Required: 0+240+16=256w, 0+130+16=146h
useCanvasStore.setState({
nodes: [
{
id: "parent",
type: "workspaceNode",
position: { x: 0, y: 0 },
data: { name: "Parent", status: "online", tier: 1, agentCard: null, activeTasks: 0, collapsed: false, role: "agent", lastErrorRate: 0, lastSampleError: "", url: "", parentId: null, currentTask: "", needsRestart: false, runtime: "", budgetLimit: null },
measured: { width: 256, height: 146 }, // fits exactly
},
{
id: "child",
type: "workspaceNode",
position: { x: 0, y: 0 },
parentId: "parent",
width: 100, // ignored (measured present)
height: 50, // ignored
measured: { width: 240, height: 130 },
data: { name: "Child", status: "online", tier: 1, agentCard: null, activeTasks: 0, collapsed: false, role: "agent", lastErrorRate: 0, lastSampleError: "", url: "", parentId: "parent", currentTask: "", needsRestart: false, runtime: "", budgetLimit: null },
},
],
});
const before = useCanvasStore.getState().nodes;
useCanvasStore.getState().growParentsToFitChildren();
const after = useCanvasStore.getState().nodes;
// Same reference (measured fits exactly)
expect(after).toBe(before);
});
it("multiple children: grows to fit the furthest child in each dimension", () => {
// Child 1 at (0, 0) 240×130 → maxRight=240, maxBottom=130
// Child 2 at (300, 200) 240×130 → maxRight=540, maxBottom=330
// Required: 540+16=556w, 330+16=346h
useCanvasStore.setState({
nodes: [
{
id: "parent",
type: "workspaceNode",
position: { x: 0, y: 0 },
data: { name: "Parent", status: "online", tier: 1, agentCard: null, activeTasks: 0, collapsed: false, role: "agent", lastErrorRate: 0, lastSampleError: "", url: "", parentId: null, currentTask: "", needsRestart: false, runtime: "", budgetLimit: null },
measured: { width: 100, height: 100 },
},
{
id: "child1",
type: "workspaceNode",
position: { x: 0, y: 0 },
parentId: "parent",
data: { name: "Child1", status: "online", tier: 1, agentCard: null, activeTasks: 0, collapsed: false, role: "agent", lastErrorRate: 0, lastSampleError: "", url: "", parentId: "parent", currentTask: "", needsRestart: false, runtime: "", budgetLimit: null },
measured: { width: 240, height: 130 },
},
{
id: "child2",
type: "workspaceNode",
position: { x: 300, y: 200 },
parentId: "parent",
data: { name: "Child2", status: "online", tier: 1, agentCard: null, activeTasks: 0, collapsed: false, role: "agent", lastErrorRate: 0, lastSampleError: "", url: "", parentId: "parent", currentTask: "", needsRestart: false, runtime: "", budgetLimit: null },
measured: { width: 240, height: 130 },
},
],
});
useCanvasStore.getState().growParentsToFitChildren();
const parent = useCanvasStore.getState().nodes.find((n) => n.id === "parent")!;
expect(parent.width).toBe(556); // max(0+240, 300+240)+16
expect(parent.height).toBe(346); // max(0+130, 200+130)+16
});
});
// ---------- ACTIVITY_LOGGED event ----------
describe("ACTIVITY_LOGGED event", () => {