Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/ruby/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,14 @@ set `$stdout.sync = false` itself. Attach mode connects to a process the server
start, so no prelude is injected there — set `$stdout.sync = true` in your program if you
need mid-run output while attached.

### Environment variables

Launch mode applies `dapLaunchArgs.env` to the debuggee's process environment at spawn
time (because `rdbg -c` starts the script immediately, the later DAP launch request
cannot carry it). An explicit value there wins over the server's inherited environment.
Attach mode cannot set env — the target process is already running; set variables before
starting it.

### Bundler projects

Pass `useBundler` through the launch configuration to run the target via `bundle exec`:
Expand Down
7 changes: 6 additions & 1 deletion packages/adapter-ruby/src/ruby-debug-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,9 +260,14 @@ export class RubyDebugAdapter extends EventEmitter implements IDebugAdapter {
return {
command: invocation.command,
args: invocation.args,
// rdbg -c starts the debuggee at spawn time, so the spawn env is the
// only channel for launchConfig.env — the later DAP launch request is
// an ack (issue #318). User env last: an explicit value wins over both
// inherited process.env and adapter defaults.
env: {
...process.env,
RUBY_DEBUG_DAP_SHOW_PROTOCOL: process.env.DEBUG ? '1' : '0'
RUBY_DEBUG_DAP_SHOW_PROTOCOL: process.env.DEBUG ? '1' : '0',
...(launchConfig.env ?? {})
}
};
}
Expand Down
56 changes: 56 additions & 0 deletions packages/adapter-ruby/tests/unit/ruby-debug-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const createDependencies = () => ({
describe('RubyDebugAdapter', () => {
afterEach(() => {
vi.clearAllMocks();
vi.unstubAllEnvs();
});

it('caches resolveExecutablePath results', async () => {
Expand Down Expand Up @@ -133,6 +134,61 @@ describe('RubyDebugAdapter', () => {
]);
});

it('merges launchConfig.env into the spawn env (issue #318)', () => {
vi.mocked(ensureRubySyncHelper).mockReturnValue('/tmp/logs/mcp_stdout_sync.rb');
vi.stubEnv('MCP_TEST_INHERITED', 'old');
const adapter = new RubyDebugAdapter(createDependencies());
(adapter as unknown as { rdbgPathCache: Map<string, { path: string; timestamp: number }> })
.rdbgPathCache.set('default', { path: '/usr/bin/rdbg', timestamp: Date.now() });

const command = adapter.buildAdapterCommand({
sessionId: 'ruby-session',
executablePath: '/usr/bin/ruby',
adapterHost: '127.0.0.1',
adapterPort: 8123,
logDir: '/tmp/logs',
scriptPath: '/workspace/app.rb',
scriptArgs: [],
launchConfig: {
env: {
RAILS_ENV: 'test',
MCP_TEST_INHERITED: 'new',
RUBY_DEBUG_DAP_SHOW_PROTOCOL: '1'
}
}
});

// User-supplied values reach the debuggee (rdbg -c starts it at spawn
// time, so the spawn env is the only channel).
expect(command.env?.RAILS_ENV).toBe('test');
// An explicit user value wins over the inherited process.env value...
expect(command.env?.MCP_TEST_INHERITED).toBe('new');
// ...and over adapter defaults.
expect(command.env?.RUBY_DEBUG_DAP_SHOW_PROTOCOL).toBe('1');
});

it('keeps the default spawn env when no launchConfig.env is given', () => {
vi.mocked(ensureRubySyncHelper).mockReturnValue('/tmp/logs/mcp_stdout_sync.rb');
vi.stubEnv('MCP_TEST_INHERITED', 'inherited');
const adapter = new RubyDebugAdapter(createDependencies());
(adapter as unknown as { rdbgPathCache: Map<string, { path: string; timestamp: number }> })
.rdbgPathCache.set('default', { path: '/usr/bin/rdbg', timestamp: Date.now() });

const command = adapter.buildAdapterCommand({
sessionId: 'ruby-session',
executablePath: '/usr/bin/ruby',
adapterHost: '127.0.0.1',
adapterPort: 8123,
logDir: '/tmp/logs',
scriptPath: '/workspace/app.rb',
scriptArgs: [],
launchConfig: {}
});

expect(command.env?.MCP_TEST_INHERITED).toBe('inherited');
expect(command.env?.RUBY_DEBUG_DAP_SHOW_PROTOCOL).toBe('0');
});

it('launches without the prelude when the sync helper cannot be materialized', () => {
vi.mocked(ensureRubySyncHelper).mockReturnValue(null);
const adapter = new RubyDebugAdapter(createDependencies());
Expand Down
42 changes: 42 additions & 0 deletions tests/e2e/mcp-server-smoke-ruby.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,4 +268,46 @@ describe('MCP Server Ruby Debugging Smoke Test @requires-ruby', () => {
})) as { sessions?: Array<{ id: string; state: string }> };
expect(listResponse.sessions?.find(s => s.id === sessionId)?.state).toBe('paused');
}, 90000);

it('applies dapLaunchArgs.env to the debuggee (issue #318)', async () => {
if (!(await rubyToolchainAvailable())) {
console.log('[Ruby Smoke Test] Ruby/rdbg not available, skipping launch env test');
return;
}

const testRubyFile = path.resolve(ROOT, 'examples', 'ruby', 'fizzbuzz.rb');

const createResponse = parseSdkToolResult(await mcpClient!.callTool({
name: 'create_debug_session',
arguments: { language: 'ruby', name: 'ruby-launch-env-test' }
}));
expect(createResponse.sessionId).toBeDefined();
sessionId = createResponse.sessionId as string;

const bpResponse = await callToolSafely(mcpClient!, 'set_breakpoint', {
sessionId,
file: testRubyFile,
line: 15
});
expect(bpResponse.success).toBe(true);

// rdbg -c starts the debuggee at spawn time, so env can only reach it
// through the spawn environment — the DAP launch request is an ack.
const startResponse = parseSdkToolResult(await mcpClient!.callTool({
name: 'start_debugging',
arguments: {
sessionId,
scriptPath: testRubyFile,
dapLaunchArgs: { env: { MCP_TEST_ENV_318: 'hello-318' } }
}
})) as { state?: string };
expect(startResponse.state).toBe('paused');

const evalResult = await callToolSafely(mcpClient!, 'evaluate_expression', {
sessionId,
expression: "ENV['MCP_TEST_ENV_318']"
});
// rdbg inspects results, so a String comes back quoted.
expect(String(evalResult.result)).toContain('hello-318');
}, 90000);
});
Loading