test(tmux): rewrite stale-sweep tests via DI to eliminate cross-file mock leak

Oracle flagged that the previous test file monkey-patched process.kill
and relied on mock.module for 5 modules. Running it after manager.test.ts
in the same Bun process reproduced 2 failures - the test resolution of
`./session-kill` specifier interacted badly with manager.test.ts's
`../../shared/tmux` barrel mock.

Solution: refactor stale-session-sweep.ts to expose
`sweepStaleOmoAgentSessionsWith(deps)` that accepts a SweepDeps record
(isInsideTmux, getTmuxPath, listCandidateSessions, killSession,
processAlive, currentPid, log). The public `sweepStaleOmoAgentSessions()`
still uses runtime-built deps so call sites are unchanged.

The test file now imports the pure function directly and constructs a
fixture with fake deps. Zero mock.module calls, zero process.kill
patching, zero cache-bust dynamic imports. 8 tests (up from 6) run
deterministically in any order with any neighbor.

Before: combined run with manager.test.ts = 2 fail, 50 pass.
After:  combined run with manager.test.ts = 0 fail, 54 pass.
This commit is contained in:
YeonGyu-Kim
2026-04-18 21:00:34 +09:00
parent d1fc46da42
commit e35ac38bbf
2 changed files with 128 additions and 135 deletions
@@ -10,7 +10,7 @@ function isProcessAlive(pid: number): boolean {
}
}
async function listOmoAgentSessions(tmux: string): Promise<string[]> {
async function listOmoAgentSessionsViaTmux(tmux: string): Promise<string[]> {
const { spawn } = await import("./spawn-process")
const proc = spawn([tmux, "list-sessions", "-F", "#{session_name}"], {
stdout: "pipe",
@@ -32,7 +32,17 @@ async function listOmoAgentSessions(tmux: string): Promise<string[]> {
.filter((name) => STALE_SESSION_PATTERN.test(name))
}
export async function sweepStaleOmoAgentSessions(): Promise<number> {
export type SweepDeps = {
isInsideTmux: () => boolean
getTmuxPath: () => Promise<string | null | undefined>
listCandidateSessions: (tmux: string) => Promise<string[]>
killSession: (sessionName: string) => Promise<boolean>
processAlive: (pid: number) => boolean
currentPid: number
log: (message: string, payload?: unknown) => void
}
async function buildRuntimeDeps(): Promise<SweepDeps> {
const [{ log }, { isInsideTmux }, { getTmuxPath }, { killTmuxSessionIfExists }] = await Promise.all([
import("../../logger"),
import("./environment"),
@@ -40,16 +50,28 @@ export async function sweepStaleOmoAgentSessions(): Promise<number> {
import("./session-kill"),
])
if (!isInsideTmux()) {
return {
isInsideTmux,
getTmuxPath,
listCandidateSessions: listOmoAgentSessionsViaTmux,
killSession: killTmuxSessionIfExists,
processAlive: isProcessAlive,
currentPid: process.pid,
log,
}
}
export async function sweepStaleOmoAgentSessionsWith(deps: SweepDeps): Promise<number> {
if (!deps.isInsideTmux()) {
return 0
}
const tmux = await getTmuxPath()
const tmux = await deps.getTmuxPath()
if (!tmux) {
return 0
}
const candidateSessions = await listOmoAgentSessions(tmux)
const candidateSessions = await deps.listCandidateSessions(tmux)
let killedCount = 0
for (const sessionName of candidateSessions) {
@@ -58,11 +80,11 @@ export async function sweepStaleOmoAgentSessions(): Promise<number> {
const pid = Number.parseInt(pidMatch[1], 10)
if (!Number.isFinite(pid)) continue
if (pid === process.pid) continue
if (isProcessAlive(pid)) continue
if (pid === deps.currentPid) continue
if (deps.processAlive(pid)) continue
log("[sweepStaleOmoAgentSessions] killing stale session", { sessionName, deadPid: pid })
const killed = await killTmuxSessionIfExists(sessionName)
deps.log("[sweepStaleOmoAgentSessions] killing stale session", { sessionName, deadPid: pid })
const killed = await deps.killSession(sessionName)
if (killed) {
killedCount += 1
}
@@ -70,3 +92,8 @@ export async function sweepStaleOmoAgentSessions(): Promise<number> {
return killedCount
}
export async function sweepStaleOmoAgentSessions(): Promise<number> {
const deps = await buildRuntimeDeps()
return sweepStaleOmoAgentSessionsWith(deps)
}