fix(daemon): preserve hyphenated browser flags in arg serialization and lazy daemon startup (#2405)

When executing CLI subcommands with custom Chromium startup options
(such as `--use-fake-device-for-media-stream` or
`--use-file-for-fake-audio-capture=...`), two independent bugs
previously prevented flags from reaching `puppeteer.launch`:

1. Yargs Value Collision: `serializeArgs` emitted options and values as
detached token pairs (`['--chrome-arg', '--use-fake-device...']`). When
initializing the spawned daemon process, `yargs` interpreted the
hyphenated trailing token as an unrelated command option, stripping the
value from `--chrome-arg`.
2. Lazy Startup Parameter Dropping: When invoking action subcommands
against an inactive session, auto-start initialization called `start([],
sessionId)`. This hardcoded empty array silently discarded all browser
configuration flags passed on the invocation command line.

Fix:
- Enforce strict `--key=value` syntax in `serializeArgs`, compelling
`yargs` in the child daemon process to parse hyphenated payloads
strictly as string values.
- Forward `serializeArgs(cliOptions, argv)` during lazy subcommand
auto-start so implicit daemon instantiations inherit all requested
browser boot capabilities.

Includes comprehensive unit test coverage in
`tests/daemon/utils.test.ts` validating `--key=value` formatting across
scalar and array option serialization.

Fixes https://github.com/ChromeDevTools/chrome-devtools-mcp/issues/2406

---------

Co-authored-by: Alex Rudenko <alexrudenko@chromium.org>
This commit is contained in:
gvojta
2026-07-27 15:33:17 +02:00
committed by GitHub
parent e2c19fed80
commit d79f3ba24f
3 changed files with 18 additions and 10 deletions
+1 -1
View File
@@ -263,7 +263,7 @@ for (const [commandName, commandDef] of Object.entries(commands)) {
const sessionId = argv.sessionId as string;
try {
if (!isDaemonRunning(sessionId)) {
await start([], sessionId);
await start(serializeArgs(cliOptions, argv), sessionId);
}
const commandArgs: Record<string, unknown> = {};
+2 -2
View File
@@ -128,10 +128,10 @@ export function serializeArgs(
}
} else if (Array.isArray(value)) {
for (const item of value) {
args.push(`--${kebabKey}`, String(item));
args.push(`--${kebabKey}=${String(item)}`);
}
} else {
args.push(`--${kebabKey}`, String(value));
args.push(`--${kebabKey}=${String(value)}`);
}
}
return args;
+15 -7
View File
@@ -26,7 +26,7 @@ describe('serializeArgs', () => {
$0: 'test',
} as unknown as ParsedArguments;
const result = serializeArgs(options, argv);
assert.deepStrictEqual(result, ['--baz', 'value']);
assert.deepStrictEqual(result, ['--baz=value']);
});
it('should handle boolean values', () => {
@@ -41,15 +41,24 @@ describe('serializeArgs', () => {
assert.deepStrictEqual(result, ['--foo', '--no-bar']);
});
it('should handle array values', () => {
const options: Record<string, YargsOptions> = {foo: {}};
it('should handle array values including hyphenated flags', () => {
const options: Record<string, YargsOptions> = {foo: {}, chromeArg: {}};
const argv = {
foo: ['val1', 'val2'],
chromeArg: [
'--use-fake-device-for-media-stream',
'--use-file-for-fake-audio-capture=/tmp/test.wav',
],
_: [],
$0: 'test',
} as unknown as ParsedArguments;
const result = serializeArgs(options, argv);
assert.deepStrictEqual(result, ['--foo', 'val1', '--foo', 'val2']);
assert.deepStrictEqual(result, [
'--foo=val1',
'--foo=val2',
'--chrome-arg=--use-fake-device-for-media-stream',
'--chrome-arg=--use-file-for-fake-audio-capture=/tmp/test.wav',
]);
});
it('should handle primitive values', () => {
@@ -61,7 +70,7 @@ describe('serializeArgs', () => {
$0: 'test',
} as unknown as ParsedArguments;
const result = serializeArgs(options, argv);
assert.deepStrictEqual(result, ['--foo', 'string', '--bar', '42']);
assert.deepStrictEqual(result, ['--foo=string', '--bar=42']);
});
it('should convert camelCase keys to kebab-case', () => {
@@ -77,8 +86,7 @@ describe('serializeArgs', () => {
} as unknown as ParsedArguments;
const result = serializeArgs(options, argv);
assert.deepStrictEqual(result, [
'--camel-case-key',
'value1',
'--camel-case-key=value1',
'--another-key',
]);
});