Files
civitai__civitai/scripts/__tests__/dev-server-no-console-window.test.ts
Justin Maier 06e2651d47 fix(dev-server): stop the daemon popping a terminal window that steals focus (#4373)
`startDaemon` spawned the daemon with `shell: true`. Node applies `windowsHide`
only to the process it creates, so the flag landed on cmd.exe and never on the
node cmd.exe then started. That node got a fresh console, Windows 11 handed it to
the default terminal app, and a Windows Terminal window opened and took focus off
whatever the user was doing. Measured: the window appeared on every run of the
unpatched code and stole focus on four of six.

Dropping the shell fixes that, but it also leaves the daemon with no console of
its own — so every child it spawns is then in the same position and pops its own
window, once per dev server started, per taskkill, per queued vitest run, per
branch-watch `git`. Verified that too, so `windowsHide` goes on all of them.

Guarded by scripts/__tests__/dev-server-no-console-window.test.ts, which fails if
either half regresses (checked by mutating both back).

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 08:22:06 -06:00

72 lines
2.5 KiB
TypeScript

import { readFileSync } from 'fs';
import { resolve } from 'path';
import { describe, expect, it } from 'vitest';
// On Windows a console child with no console of its own gets a fresh one, and Windows 11 hands that
// to the default terminal app — a Windows Terminal window opens and takes focus. `windowsHide`
// suppresses it, but node applies it only to the process IT creates: with `shell: true` it lands on
// cmd.exe, and the process cmd.exe then starts is created with default flags. The daemon is spawned
// detached with stdio ignored, so it owns no console to lend, and every one of its children is in
// that position. Piping stdio does not prevent the allocation.
const SKILL = resolve(__dirname, '../../.claude/skills/dev-server');
const FILES = [
'cli.mjs',
'console.mjs',
'scripts/daemon.mjs',
'scripts/test-queue.mjs',
'scripts/worktree.mjs',
];
const read = (file: string) => readFileSync(resolve(SKILL, file), 'utf8');
/** The object literal enclosing `at`, by brace balance. */
function enclosingLiteral(source: string, at: number) {
let depth = 0;
let open = -1;
for (let i = at; i >= 0; i--) {
if (source[i] === '}') depth++;
else if (source[i] === '{') {
if (depth === 0) {
open = i;
break;
}
depth--;
}
}
if (open === -1) return null;
depth = 0;
for (let i = open; i < source.length; i++) {
if (source[i] === '{') depth++;
else if (source[i] === '}') {
depth--;
if (depth === 0)
return { text: source.slice(open, i + 1), line: source.slice(0, open).split('\n').length };
}
}
return null;
}
describe('the dev-server skill never pops a console window', () => {
it.each(FILES)('every spawn option object in %s hides the console', (file) => {
const source = read(file);
const offenders: string[] = [];
for (let i = source.indexOf('stdio:'); i !== -1; i = source.indexOf('stdio:', i + 1)) {
const literal = enclosingLiteral(source, i);
if (literal && !literal.text.includes('windowsHide')) {
offenders.push(`${file}:${literal.line}`);
}
}
expect(offenders).toEqual([]);
});
it.each(['cli.mjs', 'console.mjs'])('%s starts the daemon without a shell', (file) => {
const source = read(file);
const start = source.indexOf('async function startDaemon');
expect(start).toBeGreaterThan(-1);
const body = source.slice(start, source.indexOf('\n}', start));
expect(body).toContain('spawn(process.execPath, [serverScript]');
expect(body.replace(/^\s*\/\/.*$/gm, '')).not.toMatch(/\bshell\s*:/);
});
});