Repository navigation
test(unit): 負荷の高いときだけ落ちる 3 本のテストを時間に頼らない形に直す (#3662) - #3666
Merged
Merged
Conversation
## 原因と直し方(落ちた出力から特定)
### useInlinedHtmlImages「keeps the original src once the total data URI length exceeds the limit」
- 出力: `Error: Test timed out in 5000ms.`(単独でも 1702ms)
- 計測: フックの仕事(replaceImageSources)は 6ms。テスト側の srcsOf() が 20 MB の HTML を
jsdom で解析し直すのが 1 回 798ms で、waitFor の中と後で 2 回呼んでいた。負荷で 5000ms 超。
- 直し方: waitFor は `result.current === html` の安い比較で待ち、結果は文字列の includes で
`<img src="${big}">` と `<img src="second.png">` を確かめる(base64 はエスケープされない)。
失敗時に 20 MB を出力しないよう boolean で比べる。1702ms → 66ms。期待の意味は同じ。
- 陽性対照: src の上限判定の行を外すと、このテストは落ちる(確認後 git checkout で戻した)。
### opencode-resilience-1900「deciding a port is free > sees a server bound to every interface」
- 出力: `Error: listen EADDRINUSE: address already in use 0.0.0.0:4288`
- 原因: 固定ポート 4288/4289/4290 を、同時に走る同じファイルの別コピーと奪い合っていた
(共有状態は OS のポート。isPortFree に届く前に occupy が落ちる)。
- 直し方: occupy は port 0 で listen してカーネルが選んだ番号を返す。「nobody holds」は
その番号を閉じてから問う(4200-4299 の範囲は並行の allocation が probe しうるので避ける)。
- 陽性対照: OPENCODE_PORT_PROBE_HOSTS を 127.0.0.1 だけ(#1900 以前)にすると
「sees a server bound to every interface」は ephemeral のポートでも落ちる(確認後戻した)。
macOS で 0.0.0.0 の ephemeral ポートに 127.0.0.1 の bind が通ることも node で実測した
(Darwin 27.0.0)。
### session-ownership-ws-relay-3334(#3554 で報告の 2 件)
- 出力: 1 件目「refuses to subscribe…」は `Test timed out in 5000ms`、2 件目「types nothing…」は
subscribedClient の `expected "vi.fn()" to be called 1 times, but got 2 times`。
- 負荷で分けた: 単独 3 回は 18/18 合格(1 件目 2639ms、relay delivery の 1 件 7068ms)。
tests/unit/guards を 8 本並行+CPU 負荷で 8 回中 7 回、2 件そろって落ちた。
- 原因: 各テスト内の `await import('@/lib/ws-server')` / `relay-delivery` 等の初回読み込みが
最初のテストの時間に計上されていた(負荷下で 4961〜5109ms)。timeout で打ち切られた 1 件目は
import が解決した後も次のテストの裏で動き続け、共有の fakeTmux / transport / ws-server の状態を
触るので、2 件目の subscribe が 2 回数えられた。2 件目は 1 件目の連鎖。
- 判定: テストの欠陥。実装(src/lib の relay / ws-server)の欠陥ではない — 読み込みを先に
済ませると負荷下でも 2 件とも合格し、期待はひとつも変えていない。
- 直し方: dynamic import をファイル先頭の静的 import にした(収集時に読み込まれ、test timeout
の外)。vi.mock は hoist されるので mock は効いたまま。1 件目 2639ms → 20ms、relay delivery
7068ms → 14ms。既存の 30_000 の timeout はそのまま残した。
## 対になる場所を探した結果
- tests/unit/hooks/sources/opencode-ports.test.ts「skips a port something else is already
listening on」: 同じ固定ポートの奪い合い(負荷下の 1 回目の測定で
`listen EADDRINUSE: address already in use 127.0.0.1:4250` を確認)。scope 内なので直した:
自分で確保できた候補を preferred にもつ instance を選ぶ(opencode-v2/reattach-once.test.ts と
同じ考え方)。
- tests/unit/hooks/sources と tests/unit/guards の他の createServer は listen(0) か、候補を
順に試す形で、固定ポートは無かった。
- tests/unit/guards で test 内に `await import('@/…')` を持つのは 3290 の db-instance(軽い mock、
負荷下でも最大 34ms)と security-no-hooks-import(文字列の中)だけで、同じ形は無かった。
- MAX_INLINED_HTML_IMAGE_CHARS / 20 MB の fixture を使うテストはこのファイルだけ。
- tests/helpers・tests/setup.ts は変えていない。src/ も変えていない。
## 確かめたこと(受入: CPU 負荷の下で 24 回連続)
負荷のかけ方(自分の worktree だけ。サーバー・他の worktree には触れていない):
- A: 1 ラウンド = `npx vitest run tests/unit/hooks tests/unit/guards/session-ownership-ws-relay-3334.test.ts
--maxWorkers=12` を 8 本同時 + `yes > /dev/null` を 14 本、3 ラウンド = 24 回(28 コアの Mac)
- B: 1 ラウンド = `npx vitest run tests/unit/guards --maxWorkers=12` を 8 本同時 + `yes` 14 本
直す前:
- A×3: useInlinedHtmlImages 16/24 失敗、opencode-resilience-1900 2/24 失敗、3334 0/24
- A×1(yes なし): useInlinedHtmlImages 8/8 失敗、opencode-ports 1/8 失敗
- B×1: session-ownership-ws-relay-3334 7/8 失敗(上の 2 件)
直した後:
- A×3: 24 回すべて `Tests 3436 passed (3436)`(3 本+opencode-ports とも 0/24)
- B×3: 24 回すべて `Tests 482 passed (482)`(3334 は 0/24、1 件目は最大 79ms)
- npx vitest run <4 本> tests/unit/guards tests/unit/docs: 52 files / 796 tests 合格
- npm run lint、npx tsc --noEmit、node scripts/changelog-fragments.mjs check: 合格
## 本文に無い指摘
- tests/unit/hooks/sources/opencode-ports.test.ts の allocation の他のテスト(「prefers the number a
previous run recorded」など)も 4200-4299 の実ポートを probe するので、同時に走る別のコピーの
probe と bind が重なれば理屈の上では揺れうる(今回の 48 回では 0 回)。範囲を外から変える
手段が無く、src の変更が要るので直していない。
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
原因と直し方(落ちた出力から特定)
useInlinedHtmlImages「keeps the original src once the total data URI length exceeds the limit」
Error: Test timed out in 5000ms.(単独でも 1702ms)jsdom で解析し直すのが 1 回 798ms で、waitFor の中と後で 2 回呼んでいた。負荷で 5000ms 超。
result.current === htmlの安い比較で待ち、結果は文字列の includes で<img src="${big}">と<img src="second.png">を確かめる(base64 はエスケープされない)。失敗時に 20 MB を出力しないよう boolean で比べる。1702ms → 66ms。期待の意味は同じ。
opencode-resilience-1900「deciding a port is free > sees a server bound to every interface」
Error: listen EADDRINUSE: address already in use 0.0.0.0:4288(共有状態は OS のポート。isPortFree に届く前に occupy が落ちる)。
その番号を閉じてから問う(4200-4299 の範囲は並行の allocation が probe しうるので避ける)。
「sees a server bound to every interface」は ephemeral のポートでも落ちる(確認後戻した)。
macOS で 0.0.0.0 の ephemeral ポートに 127.0.0.1 の bind が通ることも node で実測した
(Darwin 27.0.0)。
session-ownership-ws-relay-3334(#3554 で報告の 2 件)
Test timed out in 5000ms、2 件目「types nothing…」はsubscribedClient の
expected "vi.fn()" to be called 1 times, but got 2 times。tests/unit/guards を 8 本並行+CPU 負荷で 8 回中 7 回、2 件そろって落ちた。
await import('@/lib/ws-server')/relay-delivery等の初回読み込みが最初のテストの時間に計上されていた(負荷下で 4961〜5109ms)。timeout で打ち切られた 1 件目は
import が解決した後も次のテストの裏で動き続け、共有の fakeTmux / transport / ws-server の状態を
触るので、2 件目の subscribe が 2 回数えられた。2 件目は 1 件目の連鎖。
済ませると負荷下でも 2 件とも合格し、期待はひとつも変えていない。
の外)。vi.mock は hoist されるので mock は効いたまま。1 件目 2639ms → 20ms、relay delivery
7068ms → 14ms。既存の 30_000 の timeout はそのまま残した。
対になる場所を探した結果
listening on」: 同じ固定ポートの奪い合い(負荷下の 1 回目の測定で
listen EADDRINUSE: address already in use 127.0.0.1:4250を確認)。scope 内なので直した:自分で確保できた候補を preferred にもつ instance を選ぶ(opencode-v2/reattach-once.test.ts と
同じ考え方)。
順に試す形で、固定ポートは無かった。
await import('@/…')を持つのは 3290 の db-instance(軽い mock、負荷下でも最大 34ms)と security-no-hooks-import(文字列の中)だけで、同じ形は無かった。
確かめたこと(受入: CPU 負荷の下で 24 回連続)
負荷のかけ方(自分の worktree だけ。サーバー・他の worktree には触れていない):
npx vitest run tests/unit/hooks tests/unit/guards/session-ownership-ws-relay-3334.test.ts --maxWorkers=12を 8 本同時 +yes > /dev/nullを 14 本、3 ラウンド = 24 回(28 コアの Mac)npx vitest run tests/unit/guards --maxWorkers=12を 8 本同時 +yes14 本直す前:
直した後:
Tests 3436 passed (3436)(3 本+opencode-ports とも 0/24)Tests 482 passed (482)(3334 は 0/24、1 件目は最大 79ms)本文に無い指摘
previous run recorded」など)も 4200-4299 の実ポートを probe するので、同時に走る別のコピーの
probe と bind が重なれば理屈の上では揺れうる(今回の 48 回では 0 回)。範囲を外から変える
手段が無く、src の変更が要るので直していない。
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
Refs #3662