// The layout of a group of agent terminals: splitting, removing, resizing, naming // and reading a saved group back. Run by `npm test` with Node's type stripping. import assert from "node:assert/strict"; import { test } from "node:test"; import { agentsIn, groupTitle, MIN_SHARE, pane, paneKey, parseGroups, splitKey, pruneMissing, removeAgent, resizeAt, sideAt, splitPane, groupHref, groupIdOf, terminalAgentOf, } from "../src/agent/terminal/split-layout.ts"; const row = (children, sizes) => ({ kind: "split", dir: "row", children, sizes: sizes ?? children.map(() => 1 / children.length), }); const column = (children, sizes) => ({ ...row(children, sizes), dir: "column", }); const near = (got, want) => assert.ok(Math.abs(got - want) < 1e-9, `${got} is not ${want}`); test("dropping on the right edge puts the agent beside the pane, in half its box", () => { const got = splitPane(pane("foo"), "foo", "bar", "right"); assert.deepEqual(got, row([pane("foo"), pane("bar")], [0.5, 0.5])); }); test("the left and top edges put the agent before the pane, the bottom edge below it", () => { assert.deepEqual(agentsIn(splitPane(pane("foo"), "foo", "bar", "left")), [ "bar", "foo", ]); assert.equal(splitPane(pane("foo"), "foo", "bar", "top").dir, "column"); assert.deepEqual( splitPane(pane("foo"), "foo", "bar", "bottom"), column([pane("foo"), pane("bar")], [0.5, 0.5]), ); }); test("splitting again the same way adds a child and halves only the pane's share", () => { const two = row([pane("a"), pane("b")], [0.7, 0.3]); const got = splitPane(two, "a", "c", "right"); assert.equal(got.kind, "split"); assert.deepEqual(agentsIn(got), ["a", "c", "b"]); near(got.sizes[0], 0.35); near(got.sizes[1], 0.35); near(got.sizes[2], 0.3); }); test("splitting the other way nests a split inside the pane's box", () => { const two = row([pane("a"), pane("b")], [0.6, 0.4]); const got = splitPane(two, "b", "c", "bottom"); assert.deepEqual(got.sizes, [0.6, 0.4]); assert.deepEqual(got.children[1], column([pane("b"), pane("c")], [0.5, 0.5])); }); test("an agent already in the group is moved, not copied, and the gap closes", () => { const three = row([pane("a"), pane("b"), pane("c")]); const got = splitPane(three, "a", "c", "top"); assert.deepEqual(agentsIn(got).sort(), ["a", "b", "c"]); assert.equal(agentsIn(got).filter((x) => x === "c").length, 1); assert.equal(got.kind, "split"); assert.equal(got.dir, "row"); assert.deepEqual(got.children[0], column([pane("c"), pane("a")], [0.5, 0.5])); }); test("dropping an agent on its own pane, or on a pane that is not there, changes nothing", () => { const two = row([pane("a"), pane("b")]); assert.equal(splitPane(two, "a", "a", "left"), two); assert.equal(splitPane(two, "zzz", "c", "left"), two); }); test("removing an agent hands its share to the others and a lone child replaces its split", () => { const three = row([pane("a"), pane("b"), pane("c")], [0.5, 0.25, 0.25]); const two = removeAgent(three, "a"); assert.deepEqual(two, row([pane("b"), pane("c")], [0.5, 0.5])); assert.deepEqual(removeAgent(two, "b"), pane("c")); assert.equal(removeAgent(pane("c"), "c"), null); assert.equal(removeAgent(two, "nobody") !== null, true); }); test("removing the last pane of a nested split lets its sibling take the place and merge upwards", () => { const nested = row([pane("a"), column([pane("b"), pane("c")])], [0.4, 0.6]); const got = removeAgent(nested, "c"); assert.deepEqual(got, row([pane("a"), pane("b")], [0.4, 0.6])); // a column that ends up inside a column joins it const deep = column([ row([pane("a"), column([pane("b"), pane("c")])]), pane("d"), ]); assert.deepEqual(agentsIn(removeAgent(deep, "a")), ["b", "c", "d"]); assert.equal(removeAgent(deep, "a").dir, "column"); }); test("a divider moves share from one neighbour to the other and stops short of zero", () => { const two = row([pane("a"), pane("b")], [0.5, 0.5]); const moved = resizeAt(two, [], 0, 0.2); near(moved.sizes[0], 0.7); near(moved.sizes[1], 0.3); const far = resizeAt(two, [], 0, 5); near(far.sizes[0], 1 - MIN_SHARE); near(far.sizes[1], MIN_SHARE); const back = resizeAt(two, [], 0, -5); near(back.sizes[0], MIN_SHARE); assert.equal(resizeAt(two, [], 7, 0.1), two); }); test("a divider of a nested split is found by its path", () => { const nested = row([pane("a"), column([pane("b"), pane("c")])]); const got = resizeAt(nested, [1], 0, 0.25); assert.deepEqual(got.sizes, [0.5, 0.5]); near(got.children[1].sizes[0], 0.75); near(got.children[1].sizes[1], 0.25); }); test("a saved layout loses the agents that no longer exist", () => { const three = row([pane("a"), pane("b"), pane("c")]); assert.deepEqual(agentsIn(pruneMissing(three, new Set(["a", "c"]))), [ "a", "c", ]); assert.equal(pruneMissing(three, new Set()), null); assert.equal(pruneMissing(three, new Set(["a", "b", "c"])), three); }); test("a group goes by its own name, else by its agents", () => { const root = row([pane("foo"), pane("bar")]); assert.equal(groupTitle({ id: "1", name: "", root }), "foo + bar"); assert.equal(groupTitle({ id: "1", name: " ", root }), "foo + bar"); assert.equal(groupTitle({ id: "1", name: "infra", root }), "infra"); }); test("a saved group is read back, and what is malformed of it is dropped", () => { const good = { id: "g1", name: "infra", root: { kind: "split", dir: "row", sizes: [3, 1], children: [ { kind: "pane", agent: "a" }, { kind: "pane", agent: "b" }, ], }, }; const [got] = parseGroups([good]); assert.equal(got.name, "infra"); near(got.root.sizes[0], 0.75); near(got.root.sizes[1], 0.25); assert.deepEqual(parseGroups("nope"), []); assert.deepEqual( parseGroups([null, 5, { id: "", root: good.root }, { id: "x" }]), [], ); // the same agent twice, a bad direction, a bad child, missing sizes const odd = parseGroups([ { id: "g2", root: { kind: "split", dir: "row", children: [ { kind: "pane", agent: "a" }, { kind: "pane", agent: "a" }, { kind: "pane", agent: "c" }, 7, ], }, }, { id: "g3", root: { kind: "split", dir: "diagonal", children: [{ kind: "pane", agent: "a" }], }, }, { id: "g1", root: { kind: "pane", agent: "z" } }, ]); assert.deepEqual( odd.map((g) => g.id), ["g2", "g1"], ); assert.deepEqual(agentsIn(odd[0].root), ["a", "c"]); near(odd[0].root.sizes[0], 0.5); assert.equal(odd[0].name, ""); }); test("a drop falls on the nearest edge of the pane, the diagonals dividing the four", () => { assert.equal(sideAt(0.1, 0.5), "left"); assert.equal(sideAt(0.9, 0.5), "right"); assert.equal(sideAt(0.5, 0.1), "top"); assert.equal(sideAt(0.5, 0.9), "bottom"); assert.equal(sideAt(0.3, 0.1), "top"); assert.equal(sideAt(0.1, 0.3), "left"); assert.equal(sideAt(0.9, 0.7), "right"); }); test("a group's header tab and an agent's terminal tab are told apart by their paths", () => { assert.equal(groupHref("ab12"), "/agents/group/ab12"); assert.equal(groupIdOf("/agents/group/ab12"), "ab12"); assert.equal(groupIdOf("/agents/group/"), null); assert.equal(groupIdOf("/agents/iris/terminal"), null); assert.equal(terminalAgentOf("/agents/iris/terminal"), "iris"); assert.equal(terminalAgentOf("/agents/iris/subagents/x/terminal"), null); assert.equal(terminalAgentOf("/agents/group/ab12"), null); }); test("a subagent's pane is keyed by agent/subagent, split beside its agent, and goes with its agent", () => { assert.equal(paneKey("a"), "a"); assert.equal(paneKey("a", "s"), "a/s"); assert.deepEqual(splitKey("a"), { agent: "a" }); assert.deepEqual(splitKey("a/s"), { agent: "a", subagent: "s" }); const two = splitPane(pane("a"), "a", paneKey("a", "s"), "right"); assert.deepEqual(agentsIn(two), ["a", "a/s"]); // The same subagent twice changes nothing but its place. assert.deepEqual(agentsIn(splitPane(two, "a", paneKey("a", "s"), "left")), [ "a/s", "a", ]); // The agent a subagent belongs to being gone takes both panes. const three = splitPane(two, "a/s", "b", "bottom"); assert.deepEqual(agentsIn(pruneMissing(three, new Set(["b"]))), ["b"]); assert.deepEqual(agentsIn(pruneMissing(three, new Set(["a", "b"]))), [ "a", "a/s", "b", ]); assert.equal(groupTitle({ id: "x", name: "", root: two }), "a + a/s"); }); test("a pane already below the floor can grow but never shrinks further, and no share goes negative", () => { const tiny = row([pane("a"), pane("b"), pane("c")], [0.005, 0.005, 0.99]); const grown = resizeAt(tiny, [], 1, 0.5); assert.ok(grown.sizes[1] > 0.005); assert.ok(grown.sizes.every((s) => s >= 0)); near( grown.sizes.reduce((x, y) => x + y, 0), 1, ); const shrunk = resizeAt(tiny, [], 0, -0.5); assert.deepEqual(shrunk.sizes, tiny.sizes); const both = resizeAt( row([pane("a"), pane("b")], [0.005, 0.005]), [], 0, 0.3, ); assert.ok(both.sizes.every((s) => s >= 0)); });