Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ae8887f2a | |||
| d3d5a71d09 | |||
| 9529fc9eb7 |
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -19,15 +19,23 @@ import (
|
||||
|
||||
// allowedRoots are the container paths that the Files API can browse.
|
||||
var allowedRoots = map[string]bool{
|
||||
"/configs": true,
|
||||
"/workspace": true,
|
||||
"/home": true,
|
||||
"/plugins": true,
|
||||
"/configs": true,
|
||||
"/workspace": true,
|
||||
"/home": true,
|
||||
"/plugins": true,
|
||||
"/agent-home": true, // Phase 1 stub (RFC internal#425); full implementation to follow
|
||||
}
|
||||
|
||||
// maxUploadFiles limits the number of files in a single import/replace.
|
||||
const maxUploadFiles = 200
|
||||
|
||||
// isAgentHomeStubRequest returns true when the rootPath is /agent-home,
|
||||
// which is a Phase 1 stub (RFC internal#425). Canvas designs against the
|
||||
// shape; the full implementation will follow in a later phase.
|
||||
func isAgentHomeStubRequest(rootPath string) bool {
|
||||
return rootPath == "/agent-home"
|
||||
}
|
||||
|
||||
type TemplatesHandler struct {
|
||||
configsDir string
|
||||
docker *client.Client
|
||||
@@ -218,6 +226,11 @@ func (h *TemplatesHandler) ListFiles(c *gin.Context) {
|
||||
// ?path= — subdirectory to list (relative to root, default: "")
|
||||
// ?depth= — max depth to recurse (default: 1, max: 5)
|
||||
rootPath := c.DefaultQuery("root", "/configs")
|
||||
// Phase 1 stub — RFC internal#425
|
||||
if isAgentHomeStubRequest(rootPath) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"error": "/agent-home is not yet implemented"})
|
||||
return
|
||||
}
|
||||
if !allowedRoots[rootPath] {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "root must be one of: /configs, /workspace, /home, /plugins"})
|
||||
return
|
||||
@@ -382,6 +395,11 @@ func (h *TemplatesHandler) ReadFile(c *gin.Context) {
|
||||
|
||||
ctx := c.Request.Context()
|
||||
rootPath := c.DefaultQuery("root", "/configs")
|
||||
// Phase 1 stub — RFC internal#425
|
||||
if isAgentHomeStubRequest(rootPath) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"error": "/agent-home is not yet implemented"})
|
||||
return
|
||||
}
|
||||
if !allowedRoots[rootPath] {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "root must be one of: /configs, /workspace, /home, /plugins"})
|
||||
return
|
||||
@@ -495,6 +513,11 @@ func (h *TemplatesHandler) WriteFile(c *gin.Context) {
|
||||
|
||||
ctx := c.Request.Context()
|
||||
rootPath := c.DefaultQuery("root", "/configs")
|
||||
// Phase 1 stub — RFC internal#425
|
||||
if isAgentHomeStubRequest(rootPath) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"error": "/agent-home is not yet implemented"})
|
||||
return
|
||||
}
|
||||
if !allowedRoots[rootPath] {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "root must be one of: /configs, /workspace, /home, /plugins"})
|
||||
return
|
||||
@@ -572,6 +595,11 @@ func (h *TemplatesHandler) DeleteFile(c *gin.Context) {
|
||||
|
||||
ctx := c.Request.Context()
|
||||
rootPath := c.DefaultQuery("root", "/configs")
|
||||
// Phase 1 stub — RFC internal#425
|
||||
if isAgentHomeStubRequest(rootPath) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"error": "/agent-home is not yet implemented"})
|
||||
return
|
||||
}
|
||||
if !allowedRoots[rootPath] {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "root must be one of: /configs, /workspace, /home, /plugins"})
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user