# fix: npx 起動だとリロードで 404 になる (#954)

## 報告と検証

外部からの報告（#954, @ystknsh）。診断が具体的だったので、鵜呑みにせず**最小再現を自分で組んで
確認した**:

| ルート | 結果 |
|---|---|
| `res.sendFile("<base>/.dotted/dist/index.html")` | **404** |
| `res.sendFile("<base>/plain/dist/index.html")` | 200 |
| `res.sendFile("index.html", { root: "<base>/.dotted/dist" })` | 200 |

報告どおり。`send` は `root` が無いと**絶対パス全体**をドットセグメント判定にかけ、既定の
`dotfiles: "ignore"` が 404 を返す。npx の展開先 `~/.npm/_npx/...` は `.npm` を含むので必ず踏む。

## 全 `sendFile` を洗ったら、もう 1 件あった

CLAUDE.md の「ルールを全呼び出し箇所に横断適用する」に従って `sendFile` を全部見た。

| 箇所 | 状態 |
|---|---|
| `server/routes/app-routes.ts:182` | **バグ**（報告された箇所）。`root` なし |
| `server/routes/dir-routes.ts:112` | 無事。`{ dotfiles: "allow" }` を渡している |
| `server/config/config-routes.ts:190` | **同じバグ**。`/api/sound` が `root` も `dotfiles` も無しで絶対パスを渡す |

`/api/sound` は**ユーザーが設定した任意のパス**の音声ファイルを返す。`~/.mulmoterminal/chime.mp3`
のようなごく自然な置き場所がドットディレクトリなので、そこを指すと鳴らない。

**隣の `dir-routes.ts` が `dotfiles: "allow"` を渡していて、こちらが渡していない**という非対称が、
意図ではなく見落としであることの裏付け。同じ種類の値（config 由来の絶対パス）を同じ用途
（音声を返す）で扱っている。

## 直し方は 2 箇所で違う

| 箇所 | 直し | 理由 |
|---|---|---|
| SPA fallback | **`root` を渡す** | `dist` という固定の根がある。root より下だけが判定対象になるので、インストール先のパスに影響されなくなる |
| `/api/sound` | **`{ dotfiles: "allow" }`** | 根が無い（ユーザー設定の任意の絶対パス）。`dir-routes.ts:112` が同じ値に対して既にこうしている |

`dotfiles: "allow"` に traversal の懸念は無い。パスは config 由来でリクエストから来ない
（`config-routes.ts:176-178` のコメントが明記している）。

## テスト

既存の `test/server/infra/spa-fallback.spec.ts` は**正規表現しか見ていない**ため、404 のままでも
緑になっていた。報告者の指摘どおり。

そこで **`mountSpaFallback(app, distDir)` を `server/infra/spa-fallback.ts` に切り出す**。
`mountAppRoutes` は巨大な deps を要求するのでテストから叩けないが、切り出せば素の express に
載せて実際に index.html が返るか見られる。`.dotted/dist` を一時ディレクトリに作って検証する。

`/api/sound` 側は `sound-preset-routes.spec.ts` の隣に、ドットディレクトリ配下の音声ファイルが
返ることを見るケースを足す。

`/api/sound` 側は個別の spec を書かず、**全 `sendFile` を横断で縛るテスト**にした
（`test/server/infra/sendfile-dotfiles.spec.ts`）。この不具合は「ドット判定の除外を忘れる」という
族で、直し方が箇所ごとに違う（`root` / `dotfiles`）ため共通ヘルパに寄せられない。
**寄せられるのは要求のほう** —「どちらを意図しているかを必ず書く」。将来の呼び出し箇所も自動で
カバーされる。ソースを読んで不変条件を見るテストは `headlessScreen.spec.ts` に先例がある。

どちらも**直しを戻すと落ちる**ことを確認済み。横断テストは違反箇所をファイル名付きで名指しする。

## 実物での確認（と、2 回やり直したこと）

実際のサーバをドットディレクトリ配下から起動して比較した:

| 状態 | `/terminals` |
|---|---|
| 修正あり | **200** |
| 修正なし（main） | **404** |

ここに至るまでに**検証手順を 2 回間違えた**ので記録しておく。どちらも「テストが通った」ではなく
「テストが何も試していなかった」型の失敗:

1. **symlink で済ませようとした。** `ln -s` でリポジトリをドットディレクトリに置いたが、
   **Node は symlink を実体パスに解決する**ので `__dirname` にドットが入らない。修正前でも 200 に
   なり、危うく「直っている」と読み違えるところだった。→ 実コピーに変更。
2. **コピーの順序を間違えた。** 「修正あり」の測定時に、前段でコピーした*修正なし*のツリーが
   残っていた。両方 404 になり、今度は「直っていない」と読み違えかけた。
   → 測定のたびにコピーし直し、**コピー先のツリーに修正が入っているかを毎回 grep で表示**する
   形にした。

教訓は同じで、**セットアップが意図を反映しているかを、結果と独立に確かめる印が要る**。
