Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
b6dddf934This file is pinned to the selected release. Download the original to open it in your design tool.
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { delimiter, join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { TmuxBackend } from './tmuxBackend.js'
import { ensureTmuxOnPath, managedTmuxPath, requireTmuxAvailable, resolveViaLoginShell } from './tmuxOnPath.js'
const dirs: string[] = []
const originalPath = process.env.PATH
afterEach(() => {
process.env.PATH = originalPath
for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true })
})
function scratch(prefix: string): string {
const dir = mkdtempSync(join(tmpdir(), prefix))
dirs.push(dir)
return dir
}
/** A runtime dir with no managed tmux — so a developer machine's real ~/.harness never leaks in. */
const noManagedTmux = () => scratch('tmux-onpath-no-runtime-')
/** A stand-in for the user's shell: it knows about `binDir`, the daemon's own PATH does not. */
function fakeShell(binDir: string): string {
const dir = scratch('tmux-onpath-shell-')
const shell = join(dir, 'sh')
writeFileSync(shell, `#!/bin/sh
# Mimics "zsh -lic <script> $0 $1": drop the flag, run the script with the remaining positionals.
shift
# Hermetic: ONLY this dir, or the real tmux on the test machine leaks in and the
# "cannot find it either" case silently passes for the wrong reason.
PATH="${binDir}" exec /bin/sh -c "$@"
`)
chmodSync(shell, 0o700)
return shell
}
/** A shell that answers ONLY when asked as a login shell — a bash user with PATH in ~/.bash_profile. */
function loginOnlyShell(binDir: string): string {
const dir = scratch('tmux-onpath-login-')
const shell = join(dir, 'bash')
writeFileSync(shell, `#!/bin/sh
# The daemon asks bash with '-ic'; only '-lic' reads the file this user's PATH lives in.
case "$1" in
-lic) ;;
*) exit 0 ;;
esac
shift
PATH="${binDir}" exec /bin/sh -c "$@"
`)
chmodSync(shell, 0o700)
return shell
}
function fakeTmux(): string {
const dir = scratch('tmux-onpath-bin-')
const tmux = join(dir, 'tmux')
writeFileSync(tmux, '#!/bin/sh\nexit 0\n')
chmodSync(tmux, 0o700)
return dir
}
describe('ensureTmuxOnPath', () => {
it('adopts the directory the user\'s shell finds tmux in', async () => {
const binDir = fakeTmux()
const env: NodeJS.ProcessEnv = { PATH: '/nonexistent-for-this-test' }
const outcome = await ensureTmuxOnPath(env, fakeShell(binDir), noManagedTmux())
expect(outcome.state).toBe('adopted')
if (outcome.state !== 'adopted') return
expect(outcome.from).toBe(binDir)
// The point of the whole exercise: every later execFile('tmux', …) now resolves.
expect(env.PATH?.split(delimiter)[0]).toBe(binDir)
expect(env.PATH).toContain('/nonexistent-for-this-test')
})
it('asks again as a login shell when the interactive one comes back empty', async () => {
// A bash machine, measured here: `bash -ic` resolved nothing and `bash -lic` returned
// /usr/local/bin/tmux, so the daemon logged "tmux: unavailable (spawn tmux ENOENT)" and served
// ZERO agents while nine tmux sessions were running. zsh keeps PATH in .zshrc, which `-i` reads;
// bash keeps it in .bash_profile, which only `-l` does.
const binDir = fakeTmux()
const env: NodeJS.ProcessEnv = { PATH: '/nonexistent' }
const outcome = await ensureTmuxOnPath(env, loginOnlyShell(binDir), noManagedTmux())
expect(outcome.state).toBe('adopted')
expect(env.PATH?.split(delimiter)[0]).toBe(binDir)
})
it('finds tmux after startup chatter from the login shell', async () => {
const binDir = fakeTmux()
const shell = join(scratch('tmux-onpath-chatty-shell-'), 'zsh')
writeFileSync(shell, `#!/bin/sh
shift
printf '%s\\n' 'Now using node v25.7.0'
PATH="${binDir}" exec /bin/sh -c "$@"
`)
chmodSync(shell, 0o700)
const env: NodeJS.ProcessEnv = { PATH: '/nonexistent-for-this-test' }
const outcome = await ensureTmuxOnPath(env, shell, noManagedTmux())
expect(outcome).toMatchObject({ state: 'adopted', path: join(binDir, 'tmux') })
expect(env.PATH?.split(delimiter)[0]).toBe(binDir)
})
it('falls back to the managed tmux when no shell can resolve one', async () => {
const runtimeDir = scratch('tmux-onpath-runtime-')
const binDir = join(runtimeDir, 'tmux-9.9-darwin-arm64', 'bin')
mkdirSync(binDir, { recursive: true })
const managed = join(binDir, 'tmux')
writeFileSync(managed, '#!/bin/sh\nexit 0\n', { mode: 0o755 })
writeFileSync(join(runtimeDir, 'current-tmux'), `${managed}\n`)
const env: NodeJS.ProcessEnv = { PATH: '/nonexistent' }
const outcome = await ensureTmuxOnPath(env, '/nonexistent/shell', runtimeDir)
expect(outcome).toEqual({ state: 'adopted', path: managed, from: 'managed runtime' })
expect(env.PATH!.split(delimiter)[0]).toBe(binDir)
})
it("prefers the tmux the user's shell resolves over the managed one", async () => {
// Both exist: the daemon must run what the terminal runs, or they talk to two servers.
const runtimeDir = scratch('tmux-onpath-runtime-both-')
const managedBin = join(runtimeDir, 'tmux-9.9-darwin-arm64', 'bin')
mkdirSync(managedBin, { recursive: true })
writeFileSync(join(managedBin, 'tmux'), '#!/bin/sh\nexit 0\n', { mode: 0o755 })
writeFileSync(join(runtimeDir, 'current-tmux'), `${join(managedBin, 'tmux')}\n`)
const usersBin = scratch('tmux-onpath-users-')
writeFileSync(join(usersBin, 'tmux'), '#!/bin/sh\nexit 0\n', { mode: 0o755 })
const env: NodeJS.ProcessEnv = { PATH: '/nonexistent' }
const outcome = await ensureTmuxOnPath(env, fakeShell(usersBin), runtimeDir)
expect(outcome).toEqual({ state: 'adopted', path: join(usersBin, 'tmux'), from: usersBin })
expect(env.PATH!.split(delimiter)[0]).toBe(usersBin)
})
it('ignores a current-tmux that points outside the runtime dir or cannot run', () => {
const runtimeDir = scratch('tmux-onpath-runtime-bad-')
const elsewhere = scratch('tmux-onpath-elsewhere-')
const outside = join(elsewhere, 'tmux')
writeFileSync(outside, '#!/bin/sh\nexit 0\n', { mode: 0o755 })
writeFileSync(join(runtimeDir, 'current-tmux'), `${outside}\n`)
expect(managedTmuxPath(runtimeDir)).toBeNull()
writeFileSync(join(runtimeDir, 'current-tmux'), `${join(runtimeDir, 'gone', 'bin', 'tmux')}\n`)
expect(managedTmuxPath(runtimeDir)).toBeNull()
expect(managedTmuxPath(scratch('tmux-onpath-runtime-empty-'))).toBeNull()
})
it('leaves an already-working PATH untouched', async () => {
const binDir = fakeTmux()
const env: NodeJS.ProcessEnv = { PATH: binDir }
const outcome = await ensureTmuxOnPath(env, fakeShell(binDir), noManagedTmux())
expect(outcome.state).toBe('present')
// No duplicate entry: this runs on every start, so it has to be idempotent.
expect(env.PATH).toBe(binDir)
})
it('reports absent when neither the daemon nor the shell can find tmux', async () => {
const emptyBin = scratch('tmux-onpath-empty-')
const env: NodeJS.ProcessEnv = { PATH: '/nonexistent-for-this-test' }
const outcome = await ensureTmuxOnPath(env, fakeShell(emptyBin), noManagedTmux())
expect(outcome.state).toBe('absent')
expect(env.PATH).toBe('/nonexistent-for-this-test')
})
it('fails before daemon startup when tmux is unavailable', () => {
expect(() => requireTmuxAvailable({
state: 'absent',
reason: 'the user\'s login shell does not resolve tmux either',
})).toThrow('tmux is required but unavailable')
})
it('does not consult a shell it cannot trust', async () => {
// A relative SHELL is not something to hand a command to.
expect(await resolveViaLoginShell('tmux', 'sh')).toBeNull()
})
it('turns the reported failure into a working create', async () => {
// A tmux that answers like the real one, in a directory the daemon's PATH does not list.
const binDir = scratch('tmux-onpath-real-')
const tmux = join(binDir, 'tmux')
writeFileSync(tmux, `#!/bin/sh
case "$1" in
new-session) printf '%%7\\n' ;;
esac
exit 0
`)
chmodSync(tmux, 0o700)
const shell = fakeShell(binDir)
// Before: exactly what the machine reported after its reboot.
process.env.PATH = '/nonexistent-for-this-test'
const before = await new TmuxBackend().create({ cwd: '/tmp', label: 'harness-test' })
expect(before.state).not.toBe('succeeded')
if (before.state !== 'succeeded') expect(before.reason).toBe('tmux is unavailable')
// After: the same call, once the daemon has been told where the user's shell finds tmux.
expect((await ensureTmuxOnPath(process.env, shell, noManagedTmux())).state).toBe('adopted')
const after = await new TmuxBackend().create({ cwd: '/tmp', label: 'harness-test' })
expect(after).toEqual({
state: 'succeeded', dispatch: 'executed', runtime: { backend: 'tmux', paneId: '%7' },
})
})
})