fix(team-mode): refactor layout to use testable spawn-process helper
The existing layout.test.ts relied on mock.module("bun", ...) registered
at the top level, but test-setup.ts calls mock.restore() + restoreModuleMocks()
in afterEach, so every test except the first one lost its mocks. CI has
been red on this file since e303feef.
Two changes:
1. layout.ts now imports spawn from the existing spawn-process helper
instead of "bun" directly, matching the pattern established for
closeTmuxPane and killTmuxSessionIfExists. This does not change
runtime behavior - spawn-process just re-exports Bun's spawn.
2. layout.test.ts registers module mocks inside beforeEach and uses the
?test=UUID cache-busting dynamic-import pattern so the mocks apply
on every test run, not just the first.
All 4 layout.test.ts cases now pass.
This commit is contained in:
@@ -1,21 +1,32 @@
|
|||||||
import { beforeEach, describe, expect, mock, test } from "bun:test"
|
import { beforeEach, describe, expect, mock, test } from "bun:test"
|
||||||
|
|
||||||
|
type LayoutModule = typeof import("./layout")
|
||||||
|
|
||||||
const spawnMock = mock(() => ({
|
const spawnMock = mock(() => ({
|
||||||
exited: Promise.resolve(0),
|
exited: Promise.resolve(0),
|
||||||
stdout: new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode("%1\n")); controller.close() } }),
|
stdout: new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode("%1\n")); controller.close() } }),
|
||||||
stderr: new ReadableStream({ start(controller) { controller.close() } }),
|
stderr: new ReadableStream({ start(controller) { controller.close() } }),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
mock.module("bun", () => ({ spawn: spawnMock }))
|
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")
|
||||||
|
|
||||||
mock.module("../../../tools/interactive-bash/tmux-path-resolver", () => ({ getTmuxPath: mock(() => Promise.resolve("tmux")) }))
|
function registerModuleMocks(): void {
|
||||||
|
mock.module(spawnProcessSpecifier, () => ({ spawn: spawnMock }))
|
||||||
|
mock.module(tmuxPathResolverSpecifier, () => ({ getTmuxPath: mock(() => Promise.resolve("tmux")) }))
|
||||||
|
mock.module(sharedSpecifier, () => ({ log: mock(() => undefined) }))
|
||||||
|
}
|
||||||
|
|
||||||
mock.module("../../../shared", () => ({ log: mock(() => undefined) }))
|
async function loadLayoutModule(): Promise<LayoutModule> {
|
||||||
|
const module = await import(`${layoutSpecifier}?test=${crypto.randomUUID()}`)
|
||||||
import { createTeamLayout, removeTeamLayout, canVisualize } from "./layout"
|
return module as LayoutModule
|
||||||
|
}
|
||||||
|
|
||||||
describe("team-layout-tmux", () => {
|
describe("team-layout-tmux", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
registerModuleMocks()
|
||||||
spawnMock.mockClear()
|
spawnMock.mockClear()
|
||||||
process.env.TMUX = "/tmp/tmux-1"
|
process.env.TMUX = "/tmp/tmux-1"
|
||||||
})
|
})
|
||||||
@@ -23,6 +34,7 @@ describe("team-layout-tmux", () => {
|
|||||||
test("returns null and makes no tmux calls when visualization unavailable", async () => {
|
test("returns null and makes no tmux calls when visualization unavailable", async () => {
|
||||||
// given
|
// given
|
||||||
delete process.env.TMUX
|
delete process.env.TMUX
|
||||||
|
const { createTeamLayout, canVisualize } = await loadLayoutModule()
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const result = await createTeamLayout("run-1", [], {} as never)
|
const result = await createTeamLayout("run-1", [], {} as never)
|
||||||
@@ -35,6 +47,7 @@ describe("team-layout-tmux", () => {
|
|||||||
|
|
||||||
test("creates focus and grid windows", async () => {
|
test("creates focus and grid windows", async () => {
|
||||||
// given
|
// given
|
||||||
|
const { createTeamLayout } = await loadLayoutModule()
|
||||||
const members = [
|
const members = [
|
||||||
{ name: "lead", sessionId: "s1", color: "red" },
|
{ name: "lead", sessionId: "s1", color: "red" },
|
||||||
{ name: "m2", sessionId: "s2" },
|
{ name: "m2", sessionId: "s2" },
|
||||||
@@ -54,6 +67,7 @@ describe("team-layout-tmux", () => {
|
|||||||
|
|
||||||
test("returns null when tmux command fails", async () => {
|
test("returns null when tmux command fails", async () => {
|
||||||
// given
|
// given
|
||||||
|
const { createTeamLayout } = await loadLayoutModule()
|
||||||
spawnMock.mockImplementationOnce(() => ({
|
spawnMock.mockImplementationOnce(() => ({
|
||||||
exited: Promise.resolve(1),
|
exited: Promise.resolve(1),
|
||||||
stdout: new ReadableStream({ start(controller) { controller.close() } }),
|
stdout: new ReadableStream({ start(controller) { controller.close() } }),
|
||||||
@@ -69,6 +83,8 @@ describe("team-layout-tmux", () => {
|
|||||||
|
|
||||||
test("cleans up the tmux session", async () => {
|
test("cleans up the tmux session", async () => {
|
||||||
// given
|
// given
|
||||||
|
const { removeTeamLayout } = await loadLayoutModule()
|
||||||
|
|
||||||
// when
|
// when
|
||||||
await removeTeamLayout("run-4", {} as never)
|
await removeTeamLayout("run-4", {} as never)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { spawn } from "bun"
|
import { spawn } from "../../../shared/tmux/tmux-utils/spawn-process"
|
||||||
import { log } from "../../../shared"
|
import { log } from "../../../shared"
|
||||||
import { getTmuxPath } from "../../../tools/interactive-bash/tmux-path-resolver"
|
import { getTmuxPath } from "../../../tools/interactive-bash/tmux-path-resolver"
|
||||||
import type { TmuxSessionManager } from "../../tmux-subagent/manager"
|
import type { TmuxSessionManager } from "../../tmux-subagent/manager"
|
||||||
|
|||||||
Reference in New Issue
Block a user