Ctrl K
ComunidadGuías…

test/client-office-pods.test.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// @vitest-environment happy-dom
import { describe, it, expect, vi } from 'vitest';
import { existsSync } from 'node:fs';

// office.js only needs switchToSession from terminal.js, which pulls in xterm.
vi.mock('../public/modules/terminal.js', () => ({ switchToSession: vi.fn(), restartAgent: vi.fn(), restartPending: () => false }));

const { computePodLayout, podRugRect, computeDecorPlacement, SPRITE_PATHS, computeBookshelfRuns, computeBoardLayout, computeSpareDesks, computeConference, entryRoute, wanderRoute, walkObstacles, corridorY } =
  await import('../public/modules/office.js');

const Z = 3;
const WS_W = 32, WS_H = 36, WS_GAP_X = 12, WS_GAP_Y = 18;
const FLOOR_TOP = (36 * Z + 2 * Z) + 26 * Z; // WALL_BOTTOM + 26 Z
const TOP_MARGIN = (8 + 7) * Z; // POD_TOP_MARGIN + RUG_PAD_TOP: desk y when the floor has room

const W = 900, H = 700;

function agentsOn(...repoLists) {
  // agentsOn(['a','r1'], ['b','r1'], ['c', null]) → infos in spawn order
  return repoLists.map(([id, repo]) => ({ id, repoPath: repo, slug: repo ? repo.split('/').pop() : null }));
}

// The leg that crosses the room: the horizontal one running to the entrance.
// A blocked descent adds a second horizontal — the sidestep along the desk's
// own row — so "the first horizontal leg" is not the crossing any more.
const crossingLeg = p => p.legs.find(l => l.y0 === l.y1 && (l.x0 === Z || l.x1 === Z));

const overlaps = (a, b) =>
  a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;

// A route leg's swept area: the walker box dragged from one end to the other.
const sweptLeg = l => ({
  x: Math.min(l.x0, l.x1), y: Math.min(l.y0, l.y1),
  w: Math.abs(l.x1 - l.x0) + 16 * 2, h: Math.abs(l.y1 - l.y0) + 32 * 2,
});

describe('pod layout', () => {
  it('groups agents by repo into separate pods, no-repo agents in a final unlabeled pod', () => {
    const { pods, positions } = computePodLayout(agentsOn(
      ['a', '/r/one'], ['b', '/r/two'], ['c', '/r/one'], ['d', null],
    ), W, H);
    expect(pods.map(p => p.repoPath)).toEqual(['/r/one', '/r/two', null]);
    expect(pods.map(p => p.label)).toEqual(['one', 'two', null]);
    expect(positions.size).toBe(4);
    // a and c share the pod, side by side in one front-facing row
    expect(positions.get('c').y).toBe(positions.get('a').y);
    expect(positions.get('c').x - positions.get('a').x).toBe((WS_W + WS_GAP_X) * Z);
  });

  it('seats Billion alone at the top center, above every pod, whenever it arrived', () => {
    const infos = [
      ...agentsOn(['a', '/r/one'], ['c', null]),
      { id: 'billion', repoPath: null, slug: null, isBillion: true },
      ...agentsOn(['b', '/r/one']),
    ];
    const { pods, positions } = computePodLayout(infos, W, H);
    // Its own unlabeled pod, first, and not merged with the no-repo pod.
    expect(pods.map(p => p.repoPath)).toEqual([null, '/r/one', null]);
    expect(pods.map(p => p.label)).toEqual([null, 'one', null]);
    const billion = positions.get('billion');
    expect(billion.x).toBe(Math.floor((W - WS_W * Z) / 2));
    for (const id of ['a', 'b', 'c']) expect(positions.get(id).y).toBeGreaterThan(billion.y);
  });

  it('anchors a sparse office near the top of the floor, but still centers a full one', () => {
    const one = computePodLayout(agentsOn(['a', '/r/one']), W, H);
    expect(one.positions.get('a').y).toBe(FLOOR_TOP + TOP_MARGIN);
    expect(podRugRect(one.pods[0]).y).toBe(FLOOR_TOP + 8 * Z);
    // Floor exactly one desk-block tall plus a hair: centering offset < margin wins.
    const shortH = FLOOR_TOP + WS_H * Z + 2 * Z;
    expect(computePodLayout(agentsOn(['a', '/r/one']), W, shortH).positions.get('a').y).toBe(FLOOR_TOP + Z);
    // Overflowing floor: never above FLOOR_TOP.
    expect(computePodLayout(agentsOn(['a', '/r/one']), W, FLOOR_TOP).positions.get('a').y).toBe(FLOOR_TOP);
  });

  it('a single repo lays out as a top-anchored front-facing grid, wrapping after maxCols', () => {
    const infos = agentsOn(['a', '/r/one'], ['b', '/r/one'], ['c', '/r/one'], ['d', '/r/one'], ['e', '/r/one']);
    const { pods, positions } = computePodLayout(infos, W, H);
    expect(pods).toHaveLength(1);
    // 5 agents → 4 columns (maxCols) × 2 rows, anchored near the top of the floor
    const cols = 4, rows = 2;
    const gridW = (cols * WS_W + (cols - 1) * WS_GAP_X) * Z;
    const gridH = (rows * WS_H + (rows - 1) * WS_GAP_Y) * Z;
    const startX = Math.floor((W - gridW) / 2);
    const startY = Math.max(FLOOR_TOP, FLOOR_TOP + Math.min(TOP_MARGIN, Math.floor((H - FLOOR_TOP - gridH) / 2)));
    infos.forEach(({ id }, i) => {
      expect(positions.get(id)).toEqual({
        x: startX + (i % cols) * (WS_W + WS_GAP_X) * Z,
        y: startY + Math.floor(i / cols) * (WS_H + WS_GAP_Y) * Z,
      });
    });
  });

  it('fills exactly maxCols in one row, and sizes a wrapped pod to its full grid', () => {
    const four = agentsOn(['a', '/r/one'], ['b', '/r/one'], ['c', '/r/one'], ['d', '/r/one']);
    const full = computePodLayout(four, W, H);
    // 4 agents on a 4-col panel: one row, no empty second row reserved
    expect(full.pods[0].w).toBe((4 * WS_W + 3 * WS_GAP_X) * Z);
    expect(full.pods[0].h).toBe(WS_H * Z);
    expect(new Set(four.map(({ id }) => full.positions.get(id).y)).size).toBe(1);

    // A fifth agent wraps: the rug grows by exactly one row plus the aisle
    const { pods } = computePodLayout([...four, ...agentsOn(['e', '/r/one'])], W, H);
    expect(pods[0].w).toBe((4 * WS_W + 3 * WS_GAP_X) * Z);
    expect(pods[0].h).toBe((2 * WS_H + WS_GAP_Y) * Z);
  });

  it('a lone agent gets a single desk', () => {
    const { pods } = computePodLayout(agentsOn(['a', '/r/one']), W, H);
    expect(pods[0].w).toBe(WS_W * Z);
    expect(pods[0].h).toBe(WS_H * Z); // one desk: no aisle, no second row
  });

  it('wraps pods to a new row when they do not fit, and rugs never overlap', () => {
    const infos = agentsOn(
      ['a', '/r/1'], ['b', '/r/1'], ['c', '/r/2'], ['d', '/r/2'],
      ['e', '/r/3'], ['f', '/r/3'], ['g', '/r/4'], ['h', '/r/4'],
    );
    const { pods } = computePodLayout(infos, 400, 900);
    expect(pods.length).toBe(4);
    expect(new Set(pods.map(p => p.y)).size).toBeGreaterThan(1); // wrapped
    const rugs = pods.map(podRugRect);
    for (let i = 0; i < rugs.length; i++)
      for (let j = i + 1; j < rugs.length; j++)
        expect(overlaps(rugs[i], rugs[j]), `rug ${i} vs ${j}`).toBe(false);
    // Desks stay on the open floor
    for (const p of pods) expect(p.y).toBeGreaterThanOrEqual(FLOOR_TOP);
  });

  it('keeps within-pod desk order stable when an unrelated agent spawns or exits', () => {
    const base = agentsOn(['a', '/r/one'], ['b', '/r/one'], ['c', '/r/one']);
    const rel = (positions, pod) => base.map(({ id }) => {
      const p = positions.get(id);
      return { dx: p.x - pod.x, dy: p.y - pod.y };
    });
    const before = computePodLayout(base, W, H);
    const withStranger = computePodLayout(
      [...base, ...agentsOn(['z', '/r/other'])], W, H);
    const podBefore = before.pods[0];
    const podAfter = withStranger.pods.find(p => p.repoPath === '/r/one');
    expect(rel(withStranger.positions, podAfter)).toEqual(rel(before.positions, podBefore));
  });

  it('an empty office yields no pods and no positions, but decor still places', () => {
    const { pods, positions } = computePodLayout([], W, H);
    expect(pods).toEqual([]);
    expect(positions.size).toBe(0);
    expect(computeDecorPlacement(pods.map(podRugRect), W, H).length).toBeGreaterThan(0);
  });

  it('clamps to a single column on a narrow panel and keeps desks inside it', () => {
    const narrow = (WS_W + 4) * Z; // room for one desk, not two
    const infos = agentsOn(['a', '/r/one'], ['b', '/r/one'], ['c', '/r/one']);
    const { pods, positions } = computePodLayout(infos, narrow, 2000);
    expect(pods[0].w).toBe(WS_W * Z); // one column
    const xs = new Set(infos.map(({ id }) => positions.get(id).x));
    expect(xs.size).toBe(1); // all stacked vertically
    // One desk per row, a full aisle between rows
    expect(positions.get('b').y - positions.get('a').y).toBe((WS_H + WS_GAP_Y) * Z);
    expect(positions.get('c').y - positions.get('a').y).toBe(2 * (WS_H + WS_GAP_Y) * Z);
    // The rug ends at the last desk row
    const lastDeskBottom = Math.max(...infos.map(({ id }) => positions.get(id).y)) + WS_H * Z;
    expect(pods[0].y + pods[0].h).toBe(lastDeskBottom);
    for (const { id } of infos) {
      const p = positions.get(id);
      expect(p.x).toBeGreaterThanOrEqual(0);
      expect(p.x + WS_W * Z).toBeLessThanOrEqual(narrow);
    }
  });

  it('falls back to the repo basename as the pod label when slug is missing', () => {
    const { pods } = computePodLayout([{ id: 'a', repoPath: '/r/deep/cobra', slug: null }], W, H);
    expect(pods[0].label).toBe('cobra');
    // Trailing slash must not yield an empty (hidden) label
    const trailing = computePodLayout([{ id: 'a', repoPath: '/r/deep/cobra/', slug: null }], W, H);
    expect(trailing.pods[0].label).toBe('cobra');
  });

  it('decor fills empty floor, yields to pods, and never overlaps a rug', () => {
    // Empty office: decor everywhere it fits
    expect(computeDecorPlacement([], W, H).length).toBeGreaterThan(0);

    // Crowded office: a rug covering the whole floor leaves no decor
    const floorRug = { x: 0, y: FLOOR_TOP, w: W, h: H - FLOOR_TOP };
    expect(computeDecorPlacement([floorRug], W, H)).toEqual([]);

    // Normal office: whatever is placed clears every rug and stays on the floor
    const { pods } = computePodLayout(agentsOn(['a', '/r/one'], ['b', '/r/two']), W, H);
    const rugs = pods.map(podRugRect);
    const spots = computeDecorPlacement(rugs, W, H);
    const MARGIN = 6 * Z; // decor keeps this clearance so it also clears the glow halos
    const inflate = (r) => ({ x: r.x - MARGIN, y: r.y - MARGIN, w: r.w + 2 * MARGIN, h: r.h + 2 * MARGIN });
    for (const s of spots) {
      expect(s.y).toBeGreaterThanOrEqual(FLOOR_TOP);
      expect(s.x + s.w).toBeLessThanOrEqual(W);
      expect(s.y + s.h).toBeLessThanOrEqual(H);
      for (const r of rugs) expect(overlaps(s, inflate(r))).toBe(false);
    }
    // Same inputs → same placement (deterministic)
    expect(computeDecorPlacement(rugs, W, H)).toEqual(spots);
  });

  it('an empty office gets a chat area in each bottom corner with the plants stacked between them', () => {
    const spots = computeDecorPlacement([], W, H);
    expect(spots.map(s => s.kind).sort()).toEqual(['cactus', 'chat', 'chat', 'plant2']);
    const [left, right] = spots.filter(s => s.kind === 'chat');
    for (const chat of [left, right]) {
      expect(chat.w).toBe(170); // two side sofas, the table, gaps and padding
      expect(chat.h).toBe(130); // front sofa above the table
      expect(chat.y + chat.h).toBe(H - 3 * Z);
    }
    expect(left.x).toBe(13 * Z);
    expect(right.x + right.w).toBe(W - 13 * Z);
    // The divider: leafy plant stacked above the cactus, centred, drawn small
    const plant = spots.find(s => s.kind === 'plant2'), cactus = spots.find(s => s.kind === 'cactus');
    for (const p of [plant, cactus]) {
      expect(p.w).toBe(20); expect(p.h).toBe(40); // 16×32 art at the divider scale
      expect(Math.abs((p.x + p.w / 2) - W / 2)).toBeLessThanOrEqual(1);
    }
    expect(cactus.y + cactus.h).toBe(H - 3 * Z);
    expect(plant.y + plant.h).toBeLessThan(cactus.y);
  });

  it('a chat area yields to a pod row that reaches down into it', () => {
    const [left] = computeDecorPlacement([], W, H).filter(s => s.kind === 'chat');
    const rug = { x: left.x, y: left.y - 6 * Z, w: left.w, h: 10 }; // just inside the margin
    const survivors = computeDecorPlacement([rug], W, H).filter(s => s.kind === 'chat');
    expect(survivors.length).toBe(1);
    expect(survivors[0].x).toBe(W - 170 - 13 * Z); // the RIGHT one survives, not the blocked left
    const clear = { ...rug, y: left.y - 6 * Z - 10 };
    expect(computeDecorPlacement([clear], W, H).filter(s => s.kind === 'chat').length).toBe(2);
  });

  it('every sprite path points at a file that exists (a missing one silently drops its decor group)', () => {
    for (const [key, path] of Object.entries(SPRITE_PATHS)) expect(existsSync('public/' + path), key).toBe(true);
  });

  it('a bookshelf run sits centred under each window, feet tight against the wall', () => {
    const WINDOW_W = 28 * Z;
    for (const w of [280, 440, 600, W]) {
      const board = computeBoardLayout(w);
      const runs = computeBookshelfRuns(w);
      expect(runs.length, `panel ${w}`).toBe(2);
      // Windows sit between the board sections: section i's right edge + 1Z gap
      runs.forEach((r, i) => {
        const winX = board.sections[i].right + Z;
        expect(r.w).toBe(3 * 12 * 2); // three 12px-wide units at 2x
        expect(r.x, `panel ${w}`).toBeGreaterThanOrEqual(winX);
        expect(r.x + r.w, `panel ${w}`).toBeLessThanOrEqual(winX + WINDOW_W);
        expect(Math.abs((r.x - winX) - (winX + WINDOW_W - r.x - r.w)), `panel ${w} centred`).toBeLessThanOrEqual(1);
        expect(r.y + r.h).toBe(board.wallBottom + Z);
        expect(r.y + r.h).toBeLessThan(board.footY);
      });
    }
    // Too narrow for a board: no shelves either
    expect(computeBookshelfRuns(120)).toEqual([]);
  });

  it('spare desks fill the row below a single pod row and vanish as the office fills', () => {
    const one = computePodLayout(agentsOn(['a', '/r/one'], ['b', '/r/two']), W, H);
    const decor = computeDecorPlacement(one.pods.map(podRugRect), W, H);
    const spare = computeSpareDesks(one.pods, decor, W, H);
    expect(spare.length).toBe(3);
    expect(spare.map(d => d.variant)).toEqual([0, 1, 0]);
    // One row-pitch below the real row, on the desk grid, centred
    for (const d of spare) expect(d.y).toBe(one.pods[0].y + (WS_H + WS_GAP_Y) * Z);
    expect(spare[1].x - spare[0].x).toBe((WS_W + WS_GAP_X) * Z);
    expect(Math.abs((spare[0].x + spare[2].x + WS_W * Z) / 2 - W / 2)).toBeLessThanOrEqual(1);
    // Clear of the chat area and the panel bottom
    const chat = decor.find(s => s.kind === 'chat');
    expect(spare[0].y + WS_H * Z + 6 * Z).toBeLessThanOrEqual(chat.y);
    // One pod: the spare desks share the real desks' columns
    const solo = computePodLayout(agentsOn(['a', '/r/one'], ['b', '/r/one']), W, H);
    const soloSpare = computeSpareDesks(solo.pods, [], W, H);
    const colOf = (x) => ((x - solo.pods[0].x) % ((WS_W + WS_GAP_X) * Z) + (WS_W + WS_GAP_X) * Z) % ((WS_W + WS_GAP_X) * Z);
    for (const d of soloSpare) expect(colOf(d.x)).toBe(0);

    // Two rows of real pods: none
    const two = computePodLayout(agentsOn(['a', '/r/one'], ['b', '/r/one'], ['c', '/r/one'], ['d', '/r/one'], ['e', '/r/one']), W, H);
    expect(two.pods[0].h).toBeGreaterThan(WS_H * Z);
    expect(computeSpareDesks(two.pods, computeDecorPlacement(two.pods.map(podRugRect), W, H), W, H)).toEqual([]);
    // Two pod rows (wrapped): none
    const wrapped = computePodLayout(agentsOn(['a', '/r/one'], ['b', '/r/two']), 2 * (WS_W + 4) * Z, H);
    expect(new Set(wrapped.pods.map(p => p.y)).size).toBe(2);
    expect(computeSpareDesks(wrapped.pods, [], 2 * (WS_W + 4) * Z, H)).toEqual([]);
    // Short panel: the spare row would intrude on the chat area
    const short = computePodLayout(agentsOn(['a', '/r/one']), W, 600);
    const shortDecor = computeDecorPlacement(short.pods.map(podRugRect), W, 600);
    expect(shortDecor.map(s => s.kind)).toContain('chat');
    expect(computeSpareDesks(short.pods, shortDecor, W, 600)).toEqual([]);
    // Empty office: no real row to sit below
    expect(computeSpareDesks([], computeDecorPlacement([], W, H), W, H)).toEqual([]);
    // Narrow panel: only as many spare desks as columns fit (300px → 2)
    const narrow = computePodLayout(agentsOn(['a', '/r/one']), 300, H);
    expect(computeSpareDesks(narrow.pods, [], 300, H).length).toBe(2);
    // No decor at all, but the row would run into the panel bottom: none
    const shallow = computePodLayout(agentsOn(['a', '/r/one']), W, 500);
    expect(computeSpareDesks(shallow.pods, [], W, 500)).toEqual([]);
  });

  it('the conference table centres in the band between the desks and the chat areas', () => {
    const SET_TOP = 18, SET_BOTTOM = 32, SET_H = 240 + SET_TOP + SET_BOTTOM;
    const decor = computeDecorPlacement([], W, H);
    const conf = computeConference([], [], decor, W, H);
    expect(conf).not.toBeNull();
    expect(Math.abs((conf.table.x + conf.table.w / 2) - W / 2)).toBeLessThanOrEqual(1);
    expect(conf.seats.length).toBe(6);
    // Equal gaps above the head seat and below the foot chair
    const chatTop = decor.find(s => s.kind === 'chat').y;
    const above = (conf.table.y - SET_TOP) - (FLOOR_TOP);
    const below = chatTop - (conf.table.y + 240 + SET_BOTTOM);
    expect(Math.abs(above - below)).toBeLessThanOrEqual(1);
    // Deterministic
    expect(computeConference([], [], decor, W, H)).toEqual(conf);

    // Yields when the spare-desk row leaves too little room
    const { pods } = computePodLayout(agentsOn(['a', '/r/one'], ['b', '/r/two']), W, H);
    const rugs = pods.map(podRugRect);
    const spots = computeDecorPlacement(rugs, W, H);
    const spares = computeSpareDesks(pods, spots, W, H);
    expect(spares.length).toBe(3);
    expect(computeConference(rugs, spares, spots, W, H)).toBeNull();

    // On a taller panel it returns, sitting clear below the spare row
    const TALL = 1100;
    const tallSpots = computeDecorPlacement(rugs, W, TALL);
    const tallSpares = computeSpareDesks(pods, tallSpots, W, TALL);
    const tall = computeConference(rugs, tallSpares, tallSpots, W, TALL);
    expect(tall).not.toBeNull();
    const spareBottom = tallSpares[0].y + WS_H * Z;
    expect(tall.table.y - SET_TOP).toBeGreaterThanOrEqual(spareBottom + 6 * Z);
    expect(tall.table.y + 240 + SET_BOTTOM + 6 * Z)
      .toBeLessThanOrEqual(tallSpots.find(s => s.kind === 'chat').y);
    // Chairs and seats stay inside the panel
    for (const r of [...tall.chairs, ...tall.seats]) {
      expect(r.x).toBeGreaterThanOrEqual(0);
      expect(r.x + 16 * Z).toBeLessThanOrEqual(W);
    }
  });

  it('the conference set seats both ends, each chair tucked against the tabletop', () => {
    const conf = computeConference([], [], [], W, H);
    const CHAIR = 16 * Z;
    // The table sprite's top 11 of 64 rows are transparent, so the painted
    // tabletop starts here — not at conf.table.y. An end chair placed against
    // the sprite box instead floats in the open floor above the table.
    const topEdge = conf.table.y + Math.round(11 / 64 * conf.table.h);
    const head = conf.chairs.find(c => c.kind === 'confchairfront');
    const foot = conf.chairs.find(c => c.kind === 'confchairback');
    expect(head).toBeDefined();
    expect(foot).toBeDefined();
    // Both end chairs share the table's centreline
    expect(head.x).toBe(foot.x);
    // Head chair faces the viewer and stands flush on the tabletop's near edge.
    // It draws behind the table, so any overlap is swallowed and it reads as a
    // stub — this is the mirror of the foot chair's tuck, not a copy of it.
    expect(head.y + CHAIR).toBe(topEdge);
    // ...and it stays below the head sitter, who bounds the set from above
    expect(head.y).toBeGreaterThan(conf.seats[0].y);
    // Foot chair faces away and tucks 16px under the far edge, the mirror of it
    expect(conf.table.y + conf.table.h - foot.y).toBe(16);
    // Neither end chair escapes the set bbox the walk route steers around
    const [set] = walkObstacles([], [], [], conf).slice(-1);
    expect(head.y).toBeGreaterThanOrEqual(set.y);
    expect(foot.y + CHAIR).toBeLessThanOrEqual(set.y + set.h);
  });

  it('sits the side sitters on their chairs, not hanging off the front of them', () => {
    // A side sitter draws the seated frame, which has no legs, so its box has
    // to end on the chair rather than below it — the standing-frame offset put
    // the body 4px past the chair's bottom edge and it read as sliding off.
    // Seat order is head, then alternating sides top-down: 1,3 are the left
    // chairs (0,1), 2,4 the right ones (2,3).
    const conf = computeConference([], [], [], W, H);
    const CHAIR = 16 * Z, CHAR_H = 64;
    const chairFor = [0, 2, 1, 3];
    for (let i = 1; i <= 4; i++) {
      const chair = conf.chairs[chairFor[i - 1]];
      const bottom = conf.seats[i].y + CHAR_H;
      expect(bottom, `seat ${i} hangs below its chair`).toBeLessThanOrEqual(chair.y + CHAIR);
      expect(bottom, `seat ${i} floats above its chair`).toBeGreaterThan(chair.y);
    }
  });

  it('the conference table yields on a panel narrower than its chair overhang', () => {
    expect(computeConference([], [], [], 200, 700)).toBeNull();
  });

  it('with no chat areas the conference band runs to the panel bottom', () => {
    const SET_TOP = 18, SET_BOTTOM = 32;
    const conf = computeConference([], [], [], W, H);
    expect(conf).not.toBeNull();
    // Centred between FLOOR_TOP and the bottom margin (the empty-spread
    // Math.min must fall back to panelHeight - 3 Z, not Infinity/NaN)
    const above = (conf.table.y - SET_TOP) - FLOOR_TOP;
    const below = (H - 3 * Z) - (conf.table.y + 240 + SET_BOTTOM);
    expect(above).toBeGreaterThan(0);
    expect(Math.abs(above - below)).toBeLessThanOrEqual(1);
  });

  it('the walk-in entrance stands on bare floor, clear of the decor and the conference chairs', () => {
    for (const w of [440, 458, W]) {
      const decor = computeDecorPlacement([], w, H);
      const conf = computeConference([], [], decor, w, H);
      const obstacles = walkObstacles([], decor, [], conf);
      // Wherever the route puts the entrance, the walker standing there is clear
      const e = entryRoute({ x: 200, y: FLOOR_TOP }, conf, true, H, obstacles).from;
      const walker = { x: e.x, y: e.y, w: 16 * 2, h: 32 * 2 };
      for (const s of decor) expect(overlaps(walker, s), `panel ${w}: ${s.kind}`).toBe(false);
      for (const c of conf.chairs) expect(overlaps(walker, { ...c, w: 16 * Z, h: 16 * Z }), `panel ${w}: chair`).toBe(false);
    }
  });

  it('the walk in/out route around a conference set corridors along a clear row', () => {
    // 1400px tall: the set places with room to spare, so the bands above and
    // below it are wide enough to walk
    const T = 1400;
    const { pods, positions } = computePodLayout(agentsOn(['a', '/r/one']), W, T);
    const rugs = pods.map(podRugRect);
    const decor = computeDecorPlacement(rugs, W, T);
    const spares = computeSpareDesks(pods, decor, W, T);
    const conf = computeConference(rugs, spares, decor, W, T);
    expect(conf).not.toBeNull();
    const obstacles = walkObstacles(rugs, decor, spares, conf);
    const desk = positions.get('a');
    const entry = { x: Z, y: corridorY(obstacles, T, desk.y) };
    for (const inbound of [true, false]) {
      const p = entryRoute(desk, conf, inbound, T, obstacles);
      const [start, end] = inbound ? [entry, desk] : [desk, entry];
      expect(p.from).toEqual(start);
      const last = p.legs[p.legs.length - 1];
      expect({ x: last.x1, y: last.y1 }).toEqual(end);
      for (const l of p.legs) expect(l.x0 === l.x1 || l.y0 === l.y1, 'axis-aligned leg').toBe(true);
      // The crossing leg is the clear row nearest the desk: it touches nothing
      const across = crossingLeg(p);
      expect(across.y0).toBe(corridorY(obstacles, T, desk.y));
      for (const o of obstacles) expect(overlaps(sweptLeg(across), o), `${inbound ? 'in' : 'out'}: corridor`).toBe(false);
      // The entrance is that row's left end, so nothing walks the left strip
      expect(p.legs.some(l => l.x0 === l.x1 && l.x0 === entry.x),
        `${inbound ? 'in' : 'out'}: leg on the left strip`).toBe(false);
    }
  });

  it('a walk-out from the conference foot seat steps out via the lane, never up through the table', () => {
    const T = 1400;
    const { pods } = computePodLayout(agentsOn(['a', '/r/one']), W, T);
    const rugs = pods.map(podRugRect);
    const decor = computeDecorPlacement(rugs, W, T);
    const spares = computeSpareDesks(pods, decor, W, T);
    const conf = computeConference(rugs, spares, decor, W, T);
    const obstacles = walkObstacles(rugs, decor, spares, conf);
    const foot = conf.seats[5];
    const p = entryRoute(foot, conf, false, T, obstacles);
    // The sitter steps sideways out of the table's columns first
    expect(p.legs[0].y0).toBe(p.legs[0].y1);
    for (const l of p.legs) {
      if (l.x0 !== l.x1) continue;
      const clearOfTable = l.x0 + 16 * 2 <= conf.table.x || l.x0 >= conf.table.x + conf.table.w;
      expect(clearOfTable, `vertical leg at x=${l.x0} runs through the table`).toBe(true);
    }
  });

  it('a walk-out from the conference set steps around what blocks the lane', () => {
    const T = 1400;
    const { pods } = computePodLayout(agentsOn(['a', '/r/one']), W, T);
    const rugs = pods.map(podRugRect);
    const decor = computeDecorPlacement(rugs, W, T);
    const spares = computeSpareDesks(pods, decor, W, T);
    const conf = computeConference(rugs, spares, decor, W, T);
    const obstacles = walkObstacles(rugs, decor, spares, conf);
    const foot = conf.seats[5];
    // The lane the clear floor picks, then something parked in its column
    // between the set and the corridor — a plant, a spare desk, anything the
    // table's geometry cannot see.
    const clear = entryRoute(foot, conf, false, T, obstacles);
    const descent = clear.legs.find(l => l.x0 === l.x1);
    const blocker = { x: descent.x0 - 20, y: descent.y1 + 31, w: 80, h: 60 };
    const p = entryRoute(foot, conf, false, T, [...obstacles, blocker], W);
    for (const l of p.legs)
      expect(overlaps(sweptLeg(l), blocker), `leg ${l.x0},${l.y0}-${l.x1},${l.y1} crosses the blocker`).toBe(false);
    // Still out sideways first, and still never down through the table
    expect(p.legs[0].y0).toBe(p.legs[0].y1);
    for (const l of p.legs)
      if (l.x0 === l.x1)
        expect(l.x0 + 16 * 2 <= conf.table.x || l.x0 >= conf.table.x + conf.table.w,
          `vertical leg at x=${l.x0} runs through the table`).toBe(true);
  });

  it('the wander to a conference seat steps around what blocks the lane', () => {
    const T = 1400;
    const { pods, positions } = computePodLayout(agentsOn(['a', '/r/one']), W, T);
    const rugs = pods.map(podRugRect);
    const decor = computeDecorPlacement(rugs, W, T);
    const spares = computeSpareDesks(pods, decor, W, T);
    const conf = computeConference(rugs, spares, decor, W, T);
    const obstacles = walkObstacles(rugs, decor, spares, conf);
    const desk = positions.get('a');
    const seat = conf.seats[5]; // the foot seat: approached down an outer lane
    // The lane the clear floor picks, then something parked in its column
    // between the set and the crossing row — a plant, a spare desk, anything
    // the table's geometry cannot see.
    const clear = wanderRoute(desk, seat, conf, true, obstacles, W, T);
    const descent = clear.legs.find(l => l.x0 === l.x1 && l.y1 === seat.y);
    const blocker = { x: descent.x0 - 20, y: descent.y0 + 31, w: 80, h: 60 };
    for (const toSeat of [true, false]) {
      const p = wanderRoute(desk, seat, conf, toSeat, [...obstacles, blocker], W, T);
      for (const l of p.legs)
        expect(overlaps(sweptLeg(l), blocker),
          `${toSeat ? 'to' : 'from'} leg ${l.x0},${l.y0}-${l.x1},${l.y1} crosses the blocker`).toBe(false);
      // Still out sideways from between the chairs, and still never down
      // through the tabletop
      const step = toSeat ? p.legs[p.legs.length - 1] : p.legs[0];
      expect(step.y0).toBe(step.y1);
      // (the horizontal at the seat row goes through the chairs by design —
      // that is the step out from between them)
      for (const l of p.legs)
        if (l.x0 === l.x1)
          expect(overlaps(sweptLeg(l), conf.table),
            `vertical leg at x=${l.x0} runs through the table`).toBe(false);
    }
  });

  it('the wander to the head seat still comes straight down its own column', () => {
    // The head chair sits above the table, so its own column is the clear one
    // — vetting the lane must not push that approach out to a side lane.
    const T = 1400;
    const { pods, positions } = computePodLayout(agentsOn(['a', '/r/one']), W, T);
    const rugs = pods.map(podRugRect);
    const decor = computeDecorPlacement(rugs, W, T);
    const spares = computeSpareDesks(pods, decor, W, T);
    const conf = computeConference(rugs, spares, decor, W, T);
    const obstacles = walkObstacles(rugs, decor, spares, conf);
    const head = conf.seats[0];
    const p = wanderRoute(positions.get('a'), head, conf, true, obstacles, W, T);
    const last = p.legs[p.legs.length - 1];
    expect({ x: last.x1, y: last.y1 }).toEqual({ x: head.x, y: head.y });
    expect(last.x0).toBe(head.x); // straight down, no sidestep at the seat row
  });

  it('the wander to the head seat clears the furniture in a room too tight to spare a column', () => {
    // 990px: the set only just places, and a spare desk sits square in the head
    // seat's own column. Taking that column on faith — it is above the table,
    // so it was assumed clear — walked the agent straight over the desk.
    const T = 990;
    const { pods, positions } = computePodLayout(agentsOn(['a', '/r/one']), W, T);
    const rugs = pods.map(podRugRect);
    const decor = computeDecorPlacement(rugs, W, T);
    const spares = computeSpareDesks(pods, decor, W, T);
    const conf = computeConference(rugs, spares, decor, W, T);
    const desks = [...positions.values()];
    const obstacles = walkObstacles(rugs, decor, spares, conf, desks);
    // The set is the walker's destination and its own floor once inside, so it
    // is the one rect the route is allowed to enter.
    const setBox = walkObstacles([], [], [], conf, [])[0];
    const others = obstacles.filter(o => o.x !== setBox.x || o.y !== setBox.y || o.w !== setBox.w || o.h !== setBox.h);
    const desk = positions.get('a');
    const mine = others.filter(o => overlaps({ x: desk.x, y: desk.y, w: 16 * 2, h: 32 * 2 }, o));
    const p = wanderRoute(desk, conf.seats[0], conf, true, obstacles, W, T);
    for (const l of p.legs)
      for (const o of others)
        if (!mine.includes(o))
          expect(overlaps(sweptLeg(l), o),
            `leg ${l.x0},${l.y0}-${l.x1},${l.y1} crosses ${JSON.stringify(o)}`).toBe(false);
  });

  it('on a panel too tight for a clear row the conference route still clears the chairs', () => {
    // 1000px tall: the set places with only its 6 Z margins, so no band fits a
    // whole character and the route crosses on the widest gap it can find
    const T = 1000;
    const { pods, positions } = computePodLayout(agentsOn(['a', '/r/one']), W, T);
    const rugs = pods.map(podRugRect);
    const decor = computeDecorPlacement(rugs, W, T);
    const spares = computeSpareDesks(pods, decor, W, T);
    const conf = computeConference(rugs, spares, decor, W, T);
    const obstacles = walkObstacles(rugs, decor, spares, conf);
    const chairs = conf.chairs.map(c => ({ ...c, w: 16 * Z, h: 16 * Z }));
    for (const inbound of [true, false]) {
      const p = entryRoute(positions.get('a'), conf, inbound, T, obstacles);
      for (const l of p.legs)
        for (const c of chairs) expect(overlaps(sweptLeg(l), c), `${inbound ? 'in' : 'out'}: chair at ${c.x},${c.y}`).toBe(false);
      const across = crossingLeg(p);
      expect(across.y0).toBeLessThan(conf.table.y);
    }
  });

  it('a packed panel still keeps the crossing off the chat furniture', () => {
    // 900x700 with one agent: rug, spare row and chats leave gaps of 24, 18, 54
    // and 9px against a 64px character, so nothing fits. The old fallback put
    // the crossing on the bottom edge — straight through both sofa areas.
    const T = 700;
    const { pods, positions } = computePodLayout(agentsOn(['a', '/r/one']), W, T);
    const rugs = pods.map(podRugRect);
    const decor = computeDecorPlacement(rugs, W, T);
    const spares = computeSpareDesks(pods, decor, W, T);
    const conf = computeConference(rugs, spares, decor, W, T);
    expect(conf).toBeNull();
    const obstacles = walkObstacles(rugs, decor, spares, conf);
    const chats = decor.filter(s => s.kind === 'chat');
    expect(chats.length).toBe(2);
    for (const inbound of [true, false]) {
      const p = entryRoute(positions.get('a'), conf, inbound, T, obstacles);
      const across = crossingLeg(p);
      expect(across.y0).not.toBe(T - 32 * 2);
      for (const c of chats)
        expect(overlaps(sweptLeg(across), c), `${inbound ? 'in' : 'out'}: chat at ${c.x},${c.y}`).toBe(false);
    }
  });

  it('spare desks and the conference set keep out of the entry strip on narrow panels', () => {
    const STRIP = Z + 16 * 2;
    // Spare rows that would centre into the strip drop a desk instead of sitting on the entrance
    for (const [w, ids] of [[420, ['a', 'b', 'c']], [260, ['a']]]) {
      const { pods } = computePodLayout(agentsOn(...ids.map(id => [id, '/r/one'])), w, H);
      const spares = computeSpareDesks(pods, computeDecorPlacement(pods.map(podRugRect), w, H), w, H);
      expect(spares.length, `panel ${w}`).toBeGreaterThan(0);
      for (const d of spares) expect(d.x, `panel ${w}`).toBeGreaterThanOrEqual(STRIP);
    }
    // The set yields while its left chairs would overhang the strip, and clears it once placed
    expect(computeConference([], [], [], 297, 1100)).toBeNull();
    for (const w of [298, 320, 440]) {
      const conf = computeConference([], [], [], w, 1100);
      expect(conf, `panel ${w}`).not.toBeNull();
      for (const c of conf.chairs) expect(c.x, `panel ${w}`).toBeGreaterThanOrEqual(STRIP);
    }
  });

  it('with no conference set the walk in/out corridors along a clear row, not the chat furniture', () => {
    // 8 agents on a 700px panel: two pod rows, no room for the set
    const ids = Array.from({ length: 8 }, (_, i) => [`s${i}`, '/r/one']);
    const { pods, positions } = computePodLayout(agentsOn(...ids), 700, 800);
    const rugs = pods.map(podRugRect);
    const decor = computeDecorPlacement(rugs, 700, 800);
    const spares = computeSpareDesks(pods, decor, 700, 800);
    expect(computeConference(rugs, spares, decor, 700, 800)).toBeNull();
    // The desks go in the list the way renderOffice passes them, so the
    // no-desk-is-touched assertion below covers the sidestep leg too.
    const obstacles = walkObstacles(rugs, decor, spares, null, [...positions.values()]);
    const desk = positions.get('s7');
    for (const inbound of [true, false]) {
      const p = entryRoute(desk, null, inbound, 800, obstacles, 700);
      const across = crossingLeg(p);
      // The old route crossed along the bottom edge — straight through the chat rugs
      expect(across.y0).not.toBe(800 - 32 * 2);
      expect(across.y0).toBe(corridorY(obstacles, 800, desk.y));
      for (const o of obstacles) expect(overlaps(sweptLeg(across), o), `${inbound ? 'in' : 'out'}: corridor`).toBe(false);
      // Only the destination's own desk is touched: every other desk is clear of every leg
      for (const [id, d] of positions) {
        if (id === 's7') continue;
        const other = { x: d.x, y: d.y, w: WS_W * Z, h: WS_H * Z };
        for (const l of p.legs) {
          expect(overlaps(sweptLeg(l), other), `${inbound ? 'in' : 'out'}: desk ${id}`).toBe(false);
        }
      }
    }
  });

  it('decor spots never overlap each other, even on a narrow panel', () => {
    for (const w of [140, 180, 240, W]) {
      const spots = computeDecorPlacement([], w, H);
      for (let i = 0; i < spots.length; i++)
        for (let j = i + 1; j < spots.length; j++)
          expect(overlaps(spots[i], spots[j]), `panel ${w}: spot ${i} vs ${j}`).toBe(false);
    }
  });
});