// 起動に失敗したとき、その理由が画面に出ることを守るテスト (#1235)。 // // 起動を断る判定は 3 行以上の本文で「なぜ止めたか」と「どう直すか」を書く。 // ところが致命的な例外は `src/main.ts` の最上位ハンドラが `err.stack` ごと // 1 レコードで書き出すので、記録の並びは【本文】→【呼び出し位置の羅列】になる。 // `scripts/server.sh` が末尾 5 行しか出さないと、出るのは呼び出し位置だけで、 // 肝心の本文は画面から溢れる。直し方が書いてあるのに管理者には見えない。 // // だからここで守るのは「行数の数字」ではなく、**本文が収まる幅が残っていること**。 // 末尾の行数を減らす変更は、ここで落ちる。 import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..'); /** 起動失敗の案内は最長で 4 行。加えてスタックの見出しと呼び出し位置が続く。 */ const MIN_TAIL_LINES = 20; describe('起動失敗時の出力 (#1235)', () => { it('server.sh の起動失敗は、案内の本文が収まる行数を出す', () => { const sh = readFileSync(join(repoRoot, 'scripts', 'server.sh'), 'utf8'); const failureBlock = sh.slice(sh.indexOf('Server failed to start')); const tail = /tail\s+(?:-n\s*)?-?(\d+)\s+"\$LOG_FILE"/.exec(failureBlock); expect(tail).not.toBeNull(); expect(Number(tail![1])).toBeGreaterThanOrEqual(MIN_TAIL_LINES); }); it('起動を断る本文とスタックの合計が、その行数に収まる', () => { // 本文の行数は実物から数える。文面を伸ばしたときに、この歯止めが // 一緒に見直されるようにするため。 const src = readFileSync(join(repoRoot, 'src', 'worker-bootstrap.ts'), 'utf8'); // throw new Error(`...`) のテンプレートリテラルに含まれる改行の数。 const refusals = src.match(/throw new Error\(\s*`[\s\S]*?\)\s*;/g) ?? []; expect(refusals.length).toBeGreaterThan(0); const longest = Math.max(...refusals.map((r) => (r.match(/\\n/g) ?? []).length + 1)); // スタックの見出し 1 行 + 呼び出し位置の余裕。 expect(longest + 10).toBeLessThanOrEqual(MIN_TAIL_LINES); }); });