From 7e559de15ba031d6a03fb7d9d169fbf8e7a4bcab Mon Sep 17 00:00:00 2001 From: "Oliver Cvetkovski (ocv)" Date: Wed, 11 Mar 2026 14:54:24 +0100 Subject: [PATCH] fix: respect executablePath in standalone mode --- src/index.ts | 4 +-- test/compose.test.ts | 69 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 64ba5ae2..12fb4224 100644 --- a/src/index.ts +++ b/src/index.ts @@ -307,8 +307,8 @@ export const execCompose = ( let executablePath: string let executableArgs: string[] = [] - if (executable?.standalone && !executable.executablePath) { - executablePath = 'docker-compose' + if (executable?.standalone) { + executablePath = executable.executablePath || 'docker-compose' } else { executablePath = executable?.executablePath || 'docker' const executableOptions = executable?.options || [] diff --git a/test/compose.test.ts b/test/compose.test.ts index e0933823..fd82fa9c 100644 --- a/test/compose.test.ts +++ b/test/compose.test.ts @@ -5,10 +5,12 @@ import { beforeEach, afterEach, vi, - beforeAll + beforeAll, + MockInstance } from 'vitest' import Docker, { ContainerInfo } from 'dockerode' import * as compose from '../src' +import childProcess from 'child_process' import * as path from 'path' import { readFile } from 'fs' import { mapPsOutput, mapImListOutput } from '../src' @@ -1202,3 +1204,68 @@ describe('when upAll is called', () => { }) }) }) + +describe('executable path resolution', (): void => { + let spawnSpy: MockInstance + + beforeEach((): void => { + const mockProc = { + on: vi.fn(), + stdout: { on: vi.fn(), pipe: vi.fn() }, + stderr: { on: vi.fn(), pipe: vi.fn() }, + stdin: { write: vi.fn(), end: vi.fn() } + } + + mockProc.on.mockImplementation((event, cb) => { + if (event === 'exit') { + setTimeout(() => cb(0), 0) + } + }) + + spawnSpy = vi + .spyOn(childProcess, 'spawn') + .mockReturnValue((mockProc as unknown) as childProcess.ChildProcess) + }) + + afterEach((): void => { + spawnSpy.mockRestore() + }) + + // spawn is always called with a third argument ({ cwd, env }) so we use + // expect.objectContaining({}) to match it without being brittle about its contents. + it('uses custom executablePath in standalone mode without appending compose', async (): Promise => { + await compose.execCompose('up', [], { + executable: { + standalone: true, + executablePath: '/custom/path/docker-compose' + } + }) + expect(spawnSpy).toHaveBeenCalledWith( + '/custom/path/docker-compose', + ['up'], + expect.objectContaining({}) + ) + }) + + it('defaults to docker-compose in standalone mode when no executablePath is provided', async (): Promise => { + await compose.execCompose('up', [], { + executable: { standalone: true } + }) + expect(spawnSpy).toHaveBeenCalledWith( + 'docker-compose', + ['up'], + expect.objectContaining({}) + ) + }) + + it('appends compose subcommand when not in standalone mode', async (): Promise => { + await compose.execCompose('up', [], { + executable: { executablePath: '/custom/docker' } + }) + expect(spawnSpy).toHaveBeenCalledWith( + '/custom/docker', + ['compose', 'up'], + expect.objectContaining({}) + ) + }) +})