Skip to content

test(unit): 負荷の高いときだけ落ちる 3 本のテストを時間に頼らない形に直す (#3662) - #3666

Merged
Kewton merged 2 commits into
developfrom
feature/3662-worktree
Oct 13, 2026
Merged

Kewton merged 2 commits into
developfrom
feature/3662-worktree

Conversation

@Kewton

@Kewton Kewton commented Oct 13, 2026

Copy link
Copy Markdown
Owner

原因と直し方(落ちた出力から特定)

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 だけ(fix(hooks): opencode SSE 購読の堅牢性 — 再接続途中ターンの stop 消失・別 session の idle を stop 扱い・isPortFree が 0.0.0.0 を見ない・attach が 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

Refs #3662

## 原因と直し方(落ちた出力から特定)

### 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>
@Kewton Kewton added the bug Something isn't working label Oct 13, 2026
@Kewton
Kewton merged commit a690cf1 into develop Oct 13, 2026
16 of 17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant