2026-04-18 18:33:58 +09:00
|
|
|
import { beforeEach, describe, expect, mock, test } from "bun:test"
|
|
|
|
|
|
2026-04-18 19:42:19 +09:00
|
|
|
type LayoutModule = typeof import("./layout")
|
|
|
|
|
|
2026-04-18 18:33:58 +09:00
|
|
|
const spawnMock = mock(() => ({
|
|
|
|
|
exited: Promise.resolve(0),
|
|
|
|
|
stdout: new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode("%1\n")); controller.close() } }),
|
|
|
|
|
stderr: new ReadableStream({ start(controller) { controller.close() } }),
|
|
|
|
|
}))
|
|
|
|
|
|
2026-04-18 19:42:19 +09:00
|
|
|
const layoutSpecifier = import.meta.resolve("./layout")
|
|
|
|
|
const spawnProcessSpecifier = import.meta.resolve("../../../shared/tmux/tmux-utils/spawn-process")
|
|
|
|
|
const tmuxPathResolverSpecifier = import.meta.resolve("../../../tools/interactive-bash/tmux-path-resolver")
|
|
|
|
|
const sharedSpecifier = import.meta.resolve("../../../shared")
|
2026-04-18 18:33:58 +09:00
|
|
|
|
2026-04-18 19:42:19 +09:00
|
|
|
function registerModuleMocks(): void {
|
|
|
|
|
mock.module(spawnProcessSpecifier, () => ({ spawn: spawnMock }))
|
|
|
|
|
mock.module(tmuxPathResolverSpecifier, () => ({ getTmuxPath: mock(() => Promise.resolve("tmux")) }))
|
|
|
|
|
mock.module(sharedSpecifier, () => ({ log: mock(() => undefined) }))
|
|
|
|
|
}
|
2026-04-18 18:33:58 +09:00
|
|
|
|
2026-04-18 19:42:19 +09:00
|
|
|
async function loadLayoutModule(): Promise<LayoutModule> {
|
|
|
|
|
const module = await import(`${layoutSpecifier}?test=${crypto.randomUUID()}`)
|
|
|
|
|
return module as LayoutModule
|
|
|
|
|
}
|
2026-04-18 18:33:58 +09:00
|
|
|
|
|
|
|
|
describe("team-layout-tmux", () => {
|
|
|
|
|
beforeEach(() => {
|
2026-04-18 19:42:19 +09:00
|
|
|
registerModuleMocks()
|
2026-04-18 18:33:58 +09:00
|
|
|
spawnMock.mockClear()
|
|
|
|
|
process.env.TMUX = "/tmp/tmux-1"
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("returns null and makes no tmux calls when visualization unavailable", async () => {
|
|
|
|
|
// given
|
|
|
|
|
delete process.env.TMUX
|
2026-04-18 19:42:19 +09:00
|
|
|
const { createTeamLayout, canVisualize } = await loadLayoutModule()
|
2026-04-18 18:33:58 +09:00
|
|
|
|
|
|
|
|
// when
|
|
|
|
|
const result = await createTeamLayout("run-1", [], {} as never)
|
|
|
|
|
|
|
|
|
|
// then
|
|
|
|
|
expect(canVisualize()).toBe(false)
|
|
|
|
|
expect(result).toBeNull()
|
|
|
|
|
expect(spawnMock).toHaveBeenCalledTimes(0)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("creates focus and grid windows", async () => {
|
|
|
|
|
// given
|
2026-04-18 19:42:19 +09:00
|
|
|
const { createTeamLayout } = await loadLayoutModule()
|
2026-04-18 18:33:58 +09:00
|
|
|
const members = [
|
|
|
|
|
{ name: "lead", sessionId: "s1", color: "red" },
|
|
|
|
|
{ name: "m2", sessionId: "s2" },
|
|
|
|
|
{ name: "m3", sessionId: "s3" },
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
// when
|
|
|
|
|
await createTeamLayout("run-2", members, {} as never)
|
|
|
|
|
|
|
|
|
|
// then
|
|
|
|
|
expect(spawnMock.mock.calls.flatMap((call) => call[0] as Array<string>)).toContain("new-session")
|
|
|
|
|
expect(spawnMock.mock.calls.flatMap((call) => call[0] as Array<string>)).toContain("new-window")
|
|
|
|
|
expect(spawnMock.mock.calls.flatMap((call) => call[0] as Array<string>)).toContain("split-window")
|
|
|
|
|
expect(spawnMock.mock.calls.flatMap((call) => call[0] as Array<string>)).toContain("select-layout")
|
|
|
|
|
expect(spawnMock.mock.calls.flatMap((call) => call[0] as Array<string>)).toContain("select-pane")
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("returns null when tmux command fails", async () => {
|
|
|
|
|
// given
|
2026-04-18 19:42:19 +09:00
|
|
|
const { createTeamLayout } = await loadLayoutModule()
|
2026-04-18 18:33:58 +09:00
|
|
|
spawnMock.mockImplementationOnce(() => ({
|
|
|
|
|
exited: Promise.resolve(1),
|
|
|
|
|
stdout: new ReadableStream({ start(controller) { controller.close() } }),
|
|
|
|
|
stderr: new ReadableStream({ start(controller) { controller.close() } }),
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
// when
|
|
|
|
|
const result = await createTeamLayout("run-3", [{ name: "lead", sessionId: "s1" }], {} as never)
|
|
|
|
|
|
|
|
|
|
// then
|
|
|
|
|
expect(result).toBeNull()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("cleans up the tmux session", async () => {
|
|
|
|
|
// given
|
2026-04-18 19:42:19 +09:00
|
|
|
const { removeTeamLayout } = await loadLayoutModule()
|
|
|
|
|
|
2026-04-18 18:33:58 +09:00
|
|
|
// when
|
|
|
|
|
await removeTeamLayout("run-4", {} as never)
|
|
|
|
|
|
|
|
|
|
// then
|
|
|
|
|
expect(spawnMock.mock.calls.some((call) => (call[0] as Array<string>).includes("kill-session"))).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
})
|