mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-11 11:10:13 +01:00
* fix(browse): spawn bun.exe directly for the terminal-agent on Windows Under the Node polyfill a bare 'bun' resolved (via cross-spawn) to npm's bun.cmd shim and ran as `cmd.exe /c bun run terminal-agent.ts`. windowsHide only hid cmd.exe, and the detached cmd.exe had no console, so bun.exe allocated a visible one that stole focus on every spawn. proc.pid was cmd.exe's, so the agent's startup check (record.pid === process.pid) never matched; it exited after 2s and the 60s watchdog respawned it (#2295). resolveBunExecutable() finds the real bun.exe (own execPath under bun, BUN_INSTALL, PATH, the npm-global node_modules layout, ~/.bun) and falls back to 'bun'. POSIX is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(browse): one Windows bun.exe lookup for the terminal agent and the native cookie-import worker (#2295) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(changelog): Windows terminal agent spawns bun.exe directly (#2295, port of #3357) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(browse): the terminal-agent spawn pin expects resolveBunExecutable() (#3357) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: Adam Martin <adammartin0087@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
86 lines
4.3 KiB
TypeScript
86 lines
4.3 KiB
TypeScript
/**
|
|
* Windows terminal-agent console flash: under the Node polyfill a bare 'bun'
|
|
* resolved to npm's bun.cmd shim and ran as `cmd.exe /c bun ...`. The detached
|
|
* cmd.exe had no console, so bun.exe opened a visible one on every spawn, and
|
|
* proc.pid was cmd.exe's, so the agent failed its startup record check and
|
|
* exited, and the 60s watchdog respawned it. spawnTerminalAgent now spawns
|
|
* bun.exe directly.
|
|
*/
|
|
import { describe, expect, test } from 'bun:test';
|
|
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import { findWindowsBunExe } from '../src/bun-child-config';
|
|
import { resolveBunExecutable } from '../src/terminal-agent-control';
|
|
|
|
const files = (...paths: string[]) => (p: string) => paths.some(f => f.toLowerCase() === p.toLowerCase());
|
|
|
|
describe('resolveBunExecutable', () => {
|
|
test('POSIX keeps bare bun', () => {
|
|
expect(resolveBunExecutable({ platform: 'linux', isFile: () => true })).toBe('bun');
|
|
expect(resolveBunExecutable({ platform: 'darwin', isFile: () => true })).toBe('bun');
|
|
});
|
|
|
|
test('Windows under bun.exe reuses its own executable', () => {
|
|
expect(resolveBunExecutable({
|
|
platform: 'win32', isBun: true, execPath: 'C:\\Tools\\bun.exe', env: {}, isFile: () => false,
|
|
})).toBe('C:\\Tools\\bun.exe');
|
|
});
|
|
|
|
test('a compiled browse.exe is not mistaken for bun', () => {
|
|
expect(resolveBunExecutable({
|
|
platform: 'win32', isBun: true, execPath: 'C:\\gstack\\browse\\dist\\browse.exe',
|
|
env: { PATH: 'C:\\bun\\bin' }, isFile: files('C:\\bun\\bin\\bun.exe'),
|
|
})).toBe('C:\\bun\\bin\\bun.exe');
|
|
});
|
|
|
|
test('npm global install resolves through the bun.cmd shim to the real bun.exe', () => {
|
|
const npm = 'C:\\Users\\u\\AppData\\Roaming\\npm';
|
|
expect(resolveBunExecutable({
|
|
platform: 'win32', isBun: false, execPath: 'C:\\Program Files\\nodejs\\node.exe',
|
|
env: { PATH: `C:\\Windows\\System32;${npm}` },
|
|
isFile: files(`${npm}\\bun.cmd`, `${npm}\\node_modules\\bun\\bin\\bun.exe`),
|
|
})).toBe(`${npm}\\node_modules\\bun\\bin\\bun.exe`);
|
|
});
|
|
|
|
test('BUN_INSTALL wins over PATH, and USERPROFILE\\.bun is the last resort', () => {
|
|
const isFile = files('C:\\bi\\bin\\bun.exe', 'C:\\p\\bun.exe', 'C:\\Users\\u\\.bun\\bin\\bun.exe');
|
|
const base = { platform: 'win32', isBun: false, execPath: 'node.exe', isFile };
|
|
expect(resolveBunExecutable({ ...base, env: { BUN_INSTALL: 'C:\\bi', PATH: 'C:\\p' } })).toBe('C:\\bi\\bin\\bun.exe');
|
|
expect(resolveBunExecutable({ ...base, env: { PATH: 'C:\\p' } })).toBe('C:\\p\\bun.exe');
|
|
expect(resolveBunExecutable({ ...base, env: { PATH: 'C:\\none', USERPROFILE: 'C:\\Users\\u' } })).toBe('C:\\Users\\u\\.bun\\bin\\bun.exe');
|
|
});
|
|
|
|
test('falls back to bare bun when no bun.exe exists', () => {
|
|
expect(resolveBunExecutable({
|
|
platform: 'win32', isBun: false, execPath: 'node.exe', env: { PATH: 'C:\\x;relative' }, isFile: () => false,
|
|
})).toBe('bun');
|
|
});
|
|
|
|
test('spawnTerminalAgent spawns through the resolver, not a bare bun', () => {
|
|
const source = fs.readFileSync(path.resolve(import.meta.dir, '../src/terminal-agent-control.ts'), 'utf8');
|
|
const spawnSite = source.slice(source.indexOf('export function spawnTerminalAgent'));
|
|
expect(spawnSite).toContain('spawn([resolveBunExecutable(), \'run\'');
|
|
expect(spawnSite).not.toMatch(/spawn\(\[\s*'bun'/);
|
|
});
|
|
});
|
|
|
|
describe('findWindowsBunExe is the one bun.exe lookup', () => {
|
|
test('returns null, not a bare bun, when no bun.exe is installed', () => {
|
|
expect(findWindowsBunExe({ isBun: false, execPath: 'node.exe', env: { PATH: 'C:\\x;relative' }, isFile: () => false })).toBeNull();
|
|
});
|
|
|
|
test('the native cookie-import worker finds an npm-global bun.exe through the same lookup', () => {
|
|
const npm = 'C:\\Users\\u\\AppData\\Roaming\\npm';
|
|
expect(findWindowsBunExe({
|
|
isBun: false, execPath: 'C:\\Program Files\\nodejs\\node.exe', env: { PATH: npm },
|
|
isFile: files(`${npm}\\bun.cmd`, `${npm}\\node_modules\\bun\\bin\\bun.exe`),
|
|
})).toBe(`${npm}\\node_modules\\bun\\bin\\bun.exe`);
|
|
});
|
|
|
|
test('cookie-import-native has no bun.exe candidate list of its own', () => {
|
|
const source = fs.readFileSync(path.resolve(import.meta.dir, '../src/cookie-import-native.ts'), 'utf8');
|
|
expect(source).toContain('const bunExecutable = findWindowsBunExe();');
|
|
expect(source).not.toContain("'bun.exe'");
|
|
});
|
|
});
|