diff --git a/packages/e2b/e2b/tests/composition.e2e.ts b/packages/e2b/e2b/tests/composition.e2e.ts index 98fcdeae7f..e87627af8e 100644 --- a/packages/e2b/e2b/tests/composition.e2e.ts +++ b/packages/e2b/e2b/tests/composition.e2e.ts @@ -1,5 +1,5 @@ import { access } from 'node:fs/promises' -import { join } from 'node:path' +import { join, posix } from 'node:path' import { fileURLToPath } from 'node:url' import { Context } from 'cordis' import { describe, expect, it } from 'vitest' @@ -40,6 +40,9 @@ describe.skipIf(!process.env.E2B_API_KEY)('E2B live Loader composition', () => { } as never) const ptyFiber = await ctx.plugin(PtyService) const subprocessFiber = await ctx.plugin(E2BSubprocessService) + const node = await ctx.subprocess.resolveExecutable('node') + const relativeNodePath = posix.relative(ctx.subprocess.cwd, posix.dirname(node)) || '.' + await expect(ctx.subprocess.resolveExecutable('node', { PATH: relativeNodePath })).resolves.toBe(node) const ownerId = SessionId('e2b-pty-env-owner') const owner: Agent = { id: ownerId, diff --git a/packages/e2b/subprocess-e2b/src/index.ts b/packages/e2b/subprocess-e2b/src/index.ts index 1547e1b46e..86ca5ceada 100644 --- a/packages/e2b/subprocess-e2b/src/index.ts +++ b/packages/e2b/subprocess-e2b/src/index.ts @@ -79,14 +79,14 @@ export class E2BSubprocessService extends SubprocessService { const prefix = path === undefined ? '' : `PATH=${quoteE2BShellArg(path)} ` const result = await sandbox.commands.run( `${prefix}command -v -- ${quoteE2BShellArg(command)}`, - signalOpts(signal), + { cwd: this.cwd, ...signalOpts(signal) }, ) signal?.throwIfAborted() const executable = result.stdout.trim() - if (!posix.isAbsolute(executable) || executable.includes('\n')) { + if (executable.includes('\n') || (!posix.isAbsolute(executable) && !executable.includes('/'))) { throw new Error(`subprocess-e2b: executable ${JSON.stringify(command)} did not resolve to one absolute path`) } - return executable + return posix.resolve(this.cwd, executable) } /** @inheritdoc */ diff --git a/packages/e2b/subprocess-e2b/tests/terminal.spec.ts b/packages/e2b/subprocess-e2b/tests/terminal.spec.ts index 43cb88ea87..81716c3d52 100644 --- a/packages/e2b/subprocess-e2b/tests/terminal.spec.ts +++ b/packages/e2b/subprocess-e2b/tests/terminal.spec.ts @@ -19,6 +19,11 @@ function commandError(exitCode: number): CommandExitError { return new CommandExitError({ exitCode, stdout: '', stderr: '', error: `exit ${exitCode}` }) } +interface CommandOptions { + signal?: AbortSignal + cwd?: string +} + class FakeTerminalCommandHandle { pid = 123 disconnects = 0 @@ -74,6 +79,7 @@ class FakeTerminalCommandHandle { class FakeTerminalSandbox { readonly handle = new FakeTerminalCommandHandle() readonly commands: string[] = [] + readonly commandOptions: CommandOptions[] = [] readonly inputs: Array<{ pid: number; data: Buffer }> = [] readonly removed: string[] = [] readonly directories: string[] = [] @@ -121,8 +127,9 @@ class FakeTerminalSandbox { }, }, commands: { - run: async (command: string, options?: { signal?: AbortSignal }): Promise => { + run: async (command: string, options?: CommandOptions): Promise => { this.commands.push(command) + if (options !== undefined) this.commandOptions.push(options) options?.signal?.throwIfAborted() if (this.commandFailure !== undefined) { const error = this.commandFailure @@ -525,12 +532,16 @@ describe('E2B subprocess terminal service', () => { } it('publishes execution-world coordinates and resolves remote executables', async () => { - const { ctx } = await service() + const { ctx, fake } = await service() expect(ctx.subprocess.cwd).toBe('/workspace') expect(ctx.subprocess.runtimeRoot).toBe('/workspace/.dsh-e2b') await expect(ctx.subprocess.resolveExecutable('/bin/bash')).resolves.toBe('/bin/bash') await expect(ctx.subprocess.resolveExecutable('node', { PATH: '/custom/bin' }, new AbortController().signal)) .resolves.toBe('/usr/bin/node') + fake.resolvedExecutable = 'tools/bin/node\n' + await expect(ctx.subprocess.resolveExecutable('node', { PATH: 'tools/bin' })) + .resolves.toBe('/workspace/tools/bin/node') + expect(fake.commandOptions.at(-1)).toMatchObject({ cwd: '/workspace' }) expect((ctx.e2b)).toBeDefined() }) @@ -539,7 +550,7 @@ describe('E2B subprocess terminal service', () => { await expect(ctx.subprocess.resolveExecutable('')).rejects.toThrow('non-empty') await expect(ctx.subprocess.resolveExecutable('node', undefined, AbortSignal.abort(new Error('stop')))) .rejects.toThrow('stop') - fake.resolvedExecutable = 'relative/node\n' + fake.resolvedExecutable = 'node\n' await expect(ctx.subprocess.resolveExecutable('node')).rejects.toThrow('did not resolve') fake.resolvedExecutable = '/one\n/two\n' await expect(ctx.subprocess.resolveExecutable('node')).rejects.toThrow('did not resolve')