fix(tmux): prefer split-or-defer with FIFO deferred attach
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
canSplitPane,
|
||||
canSplitPaneAnyDirection,
|
||||
getBestSplitDirection,
|
||||
findSpawnTarget,
|
||||
type SessionMapping
|
||||
} from "./decision-engine"
|
||||
import type { WindowState, CapacityConfig, TmuxPaneInfo } from "./types"
|
||||
@@ -258,10 +259,31 @@ describe("decideSpawnActions", () => {
|
||||
expect(result.actions[0].type).toBe("spawn")
|
||||
})
|
||||
|
||||
it("respects configured agent min width for split decisions", () => {
|
||||
// given
|
||||
const state = createWindowState(240, 44, [
|
||||
{ paneId: "%1", width: 100, height: 44, left: 140, top: 0 },
|
||||
])
|
||||
const mappings: SessionMapping[] = [
|
||||
{ sessionId: "old-ses", paneId: "%1", createdAt: new Date("2024-01-01") },
|
||||
]
|
||||
const strictConfig: CapacityConfig = {
|
||||
mainPaneSize: 60,
|
||||
mainPaneMinWidth: 120,
|
||||
agentPaneWidth: 60,
|
||||
}
|
||||
|
||||
// when
|
||||
const result = decideSpawnActions(state, "ses1", "test", strictConfig, mappings)
|
||||
|
||||
// then
|
||||
expect(result.canSpawn).toBe(false)
|
||||
expect(result.actions).toHaveLength(0)
|
||||
expect(result.reason).toContain("defer")
|
||||
})
|
||||
|
||||
it("returns canSpawn=true when 0 agent panes exist and mainPane occupies full window width", () => {
|
||||
// given - tmux reports mainPane.width === windowWidth when no splits exist
|
||||
// agentAreaWidth = max(0, 252 - 252 - 1) = 0, which is < minPaneWidth
|
||||
// but with 0 agent panes, the early return should be skipped
|
||||
const windowWidth = 252
|
||||
const windowHeight = 56
|
||||
const state: WindowState = {
|
||||
@@ -281,8 +303,7 @@ describe("decideSpawnActions", () => {
|
||||
})
|
||||
|
||||
it("returns canSpawn=false when 0 agent panes and window genuinely too narrow to split", () => {
|
||||
// given - window so narrow that even splitting mainPane wouldn't work
|
||||
// canSplitPane requires width >= 2*minPaneWidth + DIVIDER_SIZE = 2*40+1 = 81
|
||||
// given - window so narrow that even splitting mainPane would fail
|
||||
const windowWidth = 70
|
||||
const windowHeight = 56
|
||||
const state: WindowState = {
|
||||
@@ -295,14 +316,13 @@ describe("decideSpawnActions", () => {
|
||||
// when
|
||||
const result = decideSpawnActions(state, "ses1", "test", defaultConfig, [])
|
||||
|
||||
// then - should fail because mainPane itself is too small to split
|
||||
// then
|
||||
expect(result.canSpawn).toBe(false)
|
||||
expect(result.reason).toContain("too small")
|
||||
})
|
||||
|
||||
it("returns canSpawn=false when agent panes exist but agent area too small", () => {
|
||||
// given - 1 agent pane exists, but agent area is below minPaneWidth
|
||||
// this verifies the early return still works for currentCount > 0
|
||||
// given - 1 agent pane exists, and agent area is below minPaneWidth
|
||||
const state: WindowState = {
|
||||
windowWidth: 180,
|
||||
windowHeight: 44,
|
||||
@@ -313,13 +333,13 @@ describe("decideSpawnActions", () => {
|
||||
// when
|
||||
const result = decideSpawnActions(state, "ses1", "test", defaultConfig, [])
|
||||
|
||||
// then - agent area = max(0, 180-160-1) = 19, which is < agentPaneWidth(40)
|
||||
// then
|
||||
expect(result.canSpawn).toBe(false)
|
||||
expect(result.reason).toContain("too small")
|
||||
})
|
||||
|
||||
it("spawns at exact minimum splittable width with 0 agent panes", () => {
|
||||
// given - canSplitPane requires width >= 2*agentPaneWidth + DIVIDER_SIZE = 2*40+1 = 81
|
||||
// given
|
||||
const exactThreshold = 2 * defaultConfig.agentPaneWidth + 1
|
||||
const state: WindowState = {
|
||||
windowWidth: exactThreshold,
|
||||
@@ -331,12 +351,12 @@ describe("decideSpawnActions", () => {
|
||||
// when
|
||||
const result = decideSpawnActions(state, "ses1", "test", defaultConfig, [])
|
||||
|
||||
// then - exactly at threshold should succeed
|
||||
// then
|
||||
expect(result.canSpawn).toBe(true)
|
||||
})
|
||||
|
||||
it("rejects spawn 1 pixel below minimum splittable width with 0 agent panes", () => {
|
||||
// given - 1 below exact threshold
|
||||
// given
|
||||
const belowThreshold = 2 * defaultConfig.agentPaneWidth
|
||||
const state: WindowState = {
|
||||
windowWidth: belowThreshold,
|
||||
@@ -348,11 +368,11 @@ describe("decideSpawnActions", () => {
|
||||
// when
|
||||
const result = decideSpawnActions(state, "ses1", "test", defaultConfig, [])
|
||||
|
||||
// then - 1 below threshold should fail
|
||||
// then
|
||||
expect(result.canSpawn).toBe(false)
|
||||
})
|
||||
|
||||
it("replaces oldest pane when existing panes are too small to split", () => {
|
||||
it("closes oldest pane when existing panes are too small to split", () => {
|
||||
// given - existing pane is below minimum splittable size
|
||||
const state = createWindowState(220, 30, [
|
||||
{ paneId: "%1", width: 50, height: 15, left: 110, top: 0 },
|
||||
@@ -366,8 +386,9 @@ describe("decideSpawnActions", () => {
|
||||
|
||||
// then
|
||||
expect(result.canSpawn).toBe(true)
|
||||
expect(result.actions.length).toBe(1)
|
||||
expect(result.actions[0].type).toBe("replace")
|
||||
expect(result.actions.length).toBe(2)
|
||||
expect(result.actions[0].type).toBe("close")
|
||||
expect(result.actions[1].type).toBe("spawn")
|
||||
})
|
||||
|
||||
it("can spawn when existing pane is large enough to split", () => {
|
||||
@@ -429,6 +450,64 @@ describe("decideSpawnActions", () => {
|
||||
expect(result.canSpawn).toBe(false)
|
||||
expect(result.reason).toBe("no main pane found")
|
||||
})
|
||||
|
||||
it("uses configured main pane size for split/defer decision", () => {
|
||||
// given
|
||||
const state = createWindowState(240, 44, [
|
||||
{ paneId: "%1", width: 90, height: 44, left: 150, top: 0 },
|
||||
])
|
||||
const mappings: SessionMapping[] = [
|
||||
{ sessionId: "old-ses", paneId: "%1", createdAt: new Date("2024-01-01") },
|
||||
]
|
||||
const wideMainConfig: CapacityConfig = {
|
||||
mainPaneSize: 80,
|
||||
mainPaneMinWidth: 120,
|
||||
agentPaneWidth: 40,
|
||||
}
|
||||
|
||||
// when
|
||||
const result = decideSpawnActions(state, "ses1", "test", wideMainConfig, mappings)
|
||||
|
||||
// then
|
||||
expect(result.canSpawn).toBe(false)
|
||||
expect(result.actions).toHaveLength(0)
|
||||
expect(result.reason).toContain("defer")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("findSpawnTarget", () => {
|
||||
it("uses deterministic vertical fallback order", () => {
|
||||
// given
|
||||
const state: WindowState = {
|
||||
windowWidth: 320,
|
||||
windowHeight: 44,
|
||||
mainPane: {
|
||||
paneId: "%0",
|
||||
width: 160,
|
||||
height: 44,
|
||||
left: 0,
|
||||
top: 0,
|
||||
title: "main",
|
||||
isActive: true,
|
||||
},
|
||||
agentPanes: [
|
||||
{ paneId: "%1", width: 70, height: 20, left: 170, top: 0, title: "a", isActive: false },
|
||||
{ paneId: "%2", width: 120, height: 44, left: 240, top: 0, title: "b", isActive: false },
|
||||
{ paneId: "%3", width: 120, height: 22, left: 240, top: 22, title: "c", isActive: false },
|
||||
],
|
||||
}
|
||||
const config: CapacityConfig = {
|
||||
mainPaneSize: 50,
|
||||
mainPaneMinWidth: 120,
|
||||
agentPaneWidth: 40,
|
||||
}
|
||||
|
||||
// when
|
||||
const target = findSpawnTarget(state, config)
|
||||
|
||||
// then
|
||||
expect(target).toEqual({ targetPaneId: "%2", splitDirection: "-v" })
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user