From 5b65751bd516ad844c26e495d9325b1d3c17178d Mon Sep 17 00:00:00 2001 From: Seekers2001 Date: Sun, 27 Sep 2026 11:02:31 +0800 Subject: [PATCH 1/2] fix: restore plan canvas process launcher --- scripts/control-pane.js | 2 -- scripts/plan-canvas.js | 1 + tests/scripts/control-pane.test.js | 6 +++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/scripts/control-pane.js b/scripts/control-pane.js index e5234d9c2..dceed7539 100755 --- a/scripts/control-pane.js +++ b/scripts/control-pane.js @@ -1,8 +1,6 @@ #!/usr/bin/env node 'use strict'; -const { spawn } = require('child_process'); - const { createControlPaneServer, parseArgs, diff --git a/scripts/plan-canvas.js b/scripts/plan-canvas.js index 6ecefe49a..4ed1b6331 100755 --- a/scripts/plan-canvas.js +++ b/scripts/plan-canvas.js @@ -20,6 +20,7 @@ const fs = require('fs'); const http = require('http'); const path = require('path'); +const { spawn } = require('child_process'); const { canonicalizeArtifactPath, createSessionStore, diff --git a/tests/scripts/control-pane.test.js b/tests/scripts/control-pane.test.js index 5503bcf95..7c9f4c102 100644 --- a/tests/scripts/control-pane.test.js +++ b/tests/scripts/control-pane.test.js @@ -602,11 +602,11 @@ async function runTests() { else failed++; if ( - await test('CLI browser opener handles spawn errors', async () => { + await test('CLI browser opener delegates spawn errors to the shared launcher', async () => { const source = fs.readFileSync(SCRIPT, 'utf8'); - assert.match(source, /child\.on\('error'/); - assert.match(source, /child\.unref\(\)/); + assert.match(source, /openBrowser: launchOpenBrowser/); + assert.match(source, /launchOpenBrowser\(url\)/); }) ) passed++; From 2b35e299eaa94ec0013b0cd1fbbabd219ad00275 Mon Sep 17 00:00:00 2001 From: Seekers2001 Date: Sun, 27 Sep 2026 17:32:44 +0800 Subject: [PATCH 2/2] test: cover launcher error handling --- scripts/lib/platform-launch.js | 5 +++-- tests/lib/platform-launch.test.js | 30 +++++++++++++++++++++++------- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/scripts/lib/platform-launch.js b/scripts/lib/platform-launch.js index ff2e8c0b5..40bed9ae6 100644 --- a/scripts/lib/platform-launch.js +++ b/scripts/lib/platform-launch.js @@ -45,9 +45,10 @@ function openerCommandFor(platform, url) { * * @param {string} url * @param {NodeJS.Platform} [platform] - injectable for tests; defaults to process.platform + * @param {typeof spawn} [spawnProcess] - injectable process launcher for tests * @returns {{ opened: boolean, reason: string }} */ -function openBrowser(url, platform = process.platform) { +function openBrowser(url, platform = process.platform, spawnProcess = spawn) { if (typeof url !== 'string' || url.length === 0) { return { opened: false, reason: 'invalid-url' }; } @@ -55,7 +56,7 @@ function openBrowser(url, platform = process.platform) { const [cmd, args] = openerCommandFor(platform, url); let child; try { - child = spawn(cmd, args, { + child = spawnProcess(cmd, args, { detached: true, stdio: 'ignore', }); diff --git a/tests/lib/platform-launch.test.js b/tests/lib/platform-launch.test.js index 482aaa006..d4f44bbe7 100644 --- a/tests/lib/platform-launch.test.js +++ b/tests/lib/platform-launch.test.js @@ -38,11 +38,27 @@ test('openBrowser: returns structured { opened, reason }', () => { assert.ok(r.reason.length > 0); }); -test('openBrowser: uses xdg-open on linux', () => { - // Spy by stubbing spawn via require cache (not possible without mocking module). - // Smoke-test: just ensure the function is callable. - const r = openBrowser('http://localhost:0', 'linux'); - // Either opened=true (xdg-open exists on runner) or opened=false with reason - assert.ok(['spawned', 'child-error:ENOENT', 'child-error:EACCES', 'spawn-threw:ENOENT'].includes(r.reason) - || r.opened === true || r.opened === false); +test('openBrowser: installs an error listener and detaches the launcher', () => { + const handlers = new Map(); + let unrefCalls = 0; + const child = { + on(event, listener) { + handlers.set(event, listener); + }, + unref() { + unrefCalls += 1; + }, + }; + + const result = openBrowser('http://localhost:0', 'linux', (command, args, options) => { + assert.equal(command, 'xdg-open'); + assert.deepEqual(args, ['http://localhost:0']); + assert.deepEqual(options, { detached: true, stdio: 'ignore' }); + return child; + }); + + assert.deepEqual(result, { opened: true, reason: 'spawned' }); + assert.equal(unrefCalls, 1); + assert.equal(typeof handlers.get('error'), 'function'); + assert.doesNotThrow(() => handlers.get('error')({ code: 'ENOENT' })); });