e35ac38bbf
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.
100 lines
2.5 KiB
TypeScript
100 lines
2.5 KiB
TypeScript
const STALE_SESSION_PATTERN = /^omo-agents-(\d+)$/
|
|
|
|
function isProcessAlive(pid: number): boolean {
|
|
try {
|
|
process.kill(pid, 0)
|
|
return true
|
|
} catch (error) {
|
|
const err = error as NodeJS.ErrnoException
|
|
return err?.code === "EPERM"
|
|
}
|
|
}
|
|
|
|
async function listOmoAgentSessionsViaTmux(tmux: string): Promise<string[]> {
|
|
const { spawn } = await import("./spawn-process")
|
|
const proc = spawn([tmux, "list-sessions", "-F", "#{session_name}"], {
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
})
|
|
const [stdout, , exitCode] = await Promise.all([
|
|
new Response(proc.stdout).text(),
|
|
new Response(proc.stderr).text(),
|
|
proc.exited,
|
|
])
|
|
|
|
if (exitCode !== 0) {
|
|
return []
|
|
}
|
|
|
|
return stdout
|
|
.split("\n")
|
|
.map((line) => line.trim())
|
|
.filter((name) => STALE_SESSION_PATTERN.test(name))
|
|
}
|
|
|
|
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"),
|
|
import("../../../tools/interactive-bash/tmux-path-resolver"),
|
|
import("./session-kill"),
|
|
])
|
|
|
|
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 deps.getTmuxPath()
|
|
if (!tmux) {
|
|
return 0
|
|
}
|
|
|
|
const candidateSessions = await deps.listCandidateSessions(tmux)
|
|
let killedCount = 0
|
|
|
|
for (const sessionName of candidateSessions) {
|
|
const pidMatch = sessionName.match(STALE_SESSION_PATTERN)
|
|
if (!pidMatch) continue
|
|
|
|
const pid = Number.parseInt(pidMatch[1], 10)
|
|
if (!Number.isFinite(pid)) continue
|
|
if (pid === deps.currentPid) continue
|
|
if (deps.processAlive(pid)) continue
|
|
|
|
deps.log("[sweepStaleOmoAgentSessions] killing stale session", { sessionName, deadPid: pid })
|
|
const killed = await deps.killSession(sessionName)
|
|
if (killed) {
|
|
killedCount += 1
|
|
}
|
|
}
|
|
|
|
return killedCount
|
|
}
|
|
|
|
export async function sweepStaleOmoAgentSessions(): Promise<number> {
|
|
const deps = await buildRuntimeDeps()
|
|
return sweepStaleOmoAgentSessionsWith(deps)
|
|
}
|