storebound 0.8.3: prompts read stdin with rt.readLine (node:readline) instead of Bun-only for await (const line of console), which crashed npx storebound desktop on Node/Windows; node-compat test bans Bun-only stdin iteration; Node prompt smoke, also on the Windows release runner

This commit is contained in:
CRTkafa 2026-10-14 08:07:40 +08:00
parent c3fed8efa2
commit 3a27c78f49
8 changed files with 111 additions and 9 deletions

View file

@ -1,11 +1,12 @@
#!/usr/bin/env bun
// Smoke test of the npm bundle (dist/storebound.mjs) with the real `node` binary: the CLI must run on Node 20+.
// bun tools/node-smoke.ts [--bundle dist/storebound.mjs] [--node <path>]
// bun tools/node-smoke.ts [--bundle dist/storebound.mjs] [--node <path>] [--only prompt]
// --only prompt: just the stdin prompt steps (the Windows release job runs these on the runner).
// Offline only: --version, help, check on examples/demo-game (same exit code and problems as under Bun), scan on the
// teaser fixture, release plan --help, init + check in an empty folder (the TypeScript config the template writes),
// and an MCP initialize handshake over stdio with tools/list and one tools/call (a child run through the bundle).
// Every step has a timeout; on a failure the step's output is printed.
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { ROOT } from './build-sidecars';
@ -45,6 +46,37 @@ const norm = (s: string) => s.replace(/^\d\d:\d\d:\d\d /gm, '').replace(/\/\S*\/
const nodeVersion = run([node, '--version']).out.trim();
console.log(`node-smoke: ${bundle.replace(ROOT + '/', '')} with node ${nodeVersion} (package engines: node ${pkg.engines?.node ?? '?'})`);
// ---- stdin prompts: `for await (const line of console)` is Bun-only and crashed `npx storebound desktop` at its
// [Y/n] prompt on Node ("console is not async iterable", then a libuv assertion on Windows). The bundle must not
// contain it, and rt.readLine (what every prompt uses) must read one piped line, see EOF, and exit cleanly.
step('prompt: the bundle has no Bun-only stdin iteration', () => {
const src = readFileSync(bundle, 'utf8');
if (/\bof\s+console\b/.test(src)) throw new Error('dist bundle iterates `console` (Bun-only): use rt.readLine');
if (!/readline/.test(src)) throw new Error('dist bundle has no node:readline (rt.readLine missing?)');
});
step('prompt: rt.readLine under node reads "n", EOF and CRLF, exits 0 without a crash', () => {
const tmp = mkdtempSync(join(tmpdir(), 'storebound-prompt-smoke-'));
try {
const entry = join(tmp, 'entry.ts');
writeFileSync(entry, `import { readLine } from ${JSON.stringify(join(ROOT, 'cli/lib/rt.ts'))};\nprocess.stdout.write('Install? [Y/n] ');\nconst a = await readLine();\nconst b = await readLine();\nconsole.log(JSON.stringify([a, b]));\n`);
const built = Bun.spawnSync(['bun', 'build', entry, '--target', 'node', '--format', 'esm', '--outfile', join(tmp, 'p.mjs')], { stdout: 'pipe', stderr: 'pipe' });
if (built.exitCode !== 0) throw new Error(`bun build: ${built.stderr.toString()}`);
const notes: string[] = [];
for (const [input, want] of [['n\n', '["n",null]'], ['', '[null,null]'], ['y\r\n', '["y",null]']] as const) {
const r = run([node, join(tmp, 'p.mjs')], tmp, input, 20_000);
const got = r.out.replace('Install? [Y/n] ', '').trim();
if (r.code !== 0 || got !== want || /assert|not async iterable/i.test(r.err)) throw new Error(`stdin ${JSON.stringify(input)}: exit ${r.code}, printed ${JSON.stringify(got)} (want ${want})\n${r.err.slice(-1500)}`);
notes.push(`${JSON.stringify(input)} -> ${got}`);
}
return notes.join(', ');
} finally { rmSync(tmp, { recursive: true, force: true }); }
});
if (arg('--only') === 'prompt') {
if (failed) { console.error(`node-smoke: ${failed} step(s) failed`); process.exit(1); }
console.log('node-smoke: prompt steps passed');
process.exit(0);
}
step('--version', () => {
const r = sb(['--version']);
expectOk(r, '--version');