fix(tests): feed guided install PTY answers only after each prompt

The real-PTY test piped answers on fixed sleeps, typing them ahead of
readline. Under CI load the first answer could land before the interface
listened, shifting every later answer onto the wrong question: the
ubuntu-latest Node 18.x npm job installed Claude only, exited 0, and never
printed the Kimi profile prompt while the sibling yarn, pnpm, and bun jobs
on the same Node version passed. Answer each prompt once it appears on
screen instead; spawned stdio goes through cat because the macOS script(1)
refuses a socket stdin.
This commit is contained in:
Affaan Mustafa
2026-09-18 18:47:05 -04:00
parent 34de45f210
commit f0378ccdb6
+54 -20
View File
@@ -2,7 +2,7 @@
const assert = require('assert');
const path = require('path');
const { spawnSync } = require('child_process');
const { spawn } = require('child_process');
const {
collectInteractiveOptions,
@@ -60,25 +60,56 @@ function quoteShellArgument(value) {
return `'${String(value).replace(/'/g, `'\\''`)}'`;
}
function runGuidedPtyFixture(answers) {
if (process.platform === 'win32') return null;
function stripPtyControlBytes(value) {
return value
// eslint-disable-next-line no-control-regex
.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, '')
.replace(/\r/g, '');
}
function runGuidedPtyFixture(exchanges) {
if (process.platform === 'win32') return Promise.resolve(null);
const command = [process.execPath, guidedPtyFixture];
const scriptArgs = process.platform === 'darwin'
? ['-q', '-e', '/dev/null', ...command]
: ['-q', '-e', '-c', command.map(quoteShellArgument).join(' '), '/dev/null'];
const pseudoTerminalCommand = ['script', ...scriptArgs]
.map(quoteShellArgument)
.join(' ');
const answerCommands = answers
.map(answer => `sleep 0.35; printf '%s\\n' ${quoteShellArgument(answer)}`)
.join('; ');
return spawnSync('sh', ['-c', `(${answerCommands}; sleep 0.1) | ${pseudoTerminalCommand}`], {
cwd: repoRoot,
encoding: 'utf8',
timeout: 15000,
return new Promise((resolve, reject) => {
// Answers go through cat so script reads a plain pipe: spawned stdio is
// a socketpair, and the macOS script(1) refuses a socket stdin.
const feeder = `cat | ${['script', ...scriptArgs].map(quoteShellArgument).join(' ')}`;
const child = spawn('sh', ['-c', feeder], { cwd: repoRoot });
let stdout = '';
let stderr = '';
let sent = 0;
let settled = false;
const finish = callback => {
if (settled) return;
settled = true;
clearTimeout(timer);
callback();
};
const timer = setTimeout(() => {
child.kill('SIGKILL');
finish(() => reject(new Error('guided PTY fixture timed out')));
}, 15000);
const feed = () => {
// Answer only once the matching prompt is on screen. Fixed sleeps
// typed answers ahead of readline; under CI load the first answer
// could land before the interface listened, shifting every later
// answer onto the wrong question (ubuntu Node 18 npm job).
const visible = stripPtyControlBytes(stdout + stderr);
while (sent < exchanges.length && visible.includes(exchanges[sent].expect)) {
child.stdin.write(`${exchanges[sent].send}\n`);
sent += 1;
}
if (sent === exchanges.length) child.stdin.end();
};
child.stdout.on('data', data => { stdout += data; feed(); });
child.stderr.on('data', data => { stderr += data; feed(); });
child.on('error', error => finish(() => reject(error)));
child.on('close', (status, signal) => finish(() => resolve({ status, signal, stdout, stderr })));
});
}
(async () => {
console.log('\n=== Guided multi-harness CLI tests ===\n');
@@ -175,14 +206,17 @@ function runGuidedPtyFixture(answers) {
);
});
await test('real PTY shows every all-harness question and applies after visible yes', () => {
const result = runGuidedPtyFixture(['all', '1', '3', '2', 'y']);
await test('real PTY shows every all-harness question and applies after visible yes', async () => {
const result = await runGuidedPtyFixture([
{ expect: 'Choose one or more (for example 1,3 or all):', send: 'all' },
{ expect: 'Choose [Recommended: user] (one option only):', send: '1' },
{ expect: 'Choose [Recommended: standard] (one option only):', send: '3' },
{ expect: 'Choose [Recommended: core] (one option only):', send: '2' },
{ expect: 'Apply ECC to these harnesses? [y/N]:', send: 'y' },
]);
if (result === null) return;
assert.strictEqual(result.status, 0, result.stderr);
const visible = `${result.stdout}${result.stderr}`
// eslint-disable-next-line no-control-regex
.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, '')
.replace(/\r/g, '');
const visible = stripPtyControlBytes(`${result.stdout}${result.stderr}`);
const orderedPrompts = [
'Choose one or more (for example 1,3 or all):',
'Choose [Recommended: user] (one option only):',