コンテンツにスキップ

試験

ターミナルウィンドウ
pnpm run test # 単体。スタック不要、数秒
pnpm run test:e2e # e2e 1 件。スタックが要る
pnpm run check # audit / format / env / typecheck / 単体 / CI との一致 / docs サイト

pretest:e2e が先に doctor を --e2e 付きで走らせ、足りないものを名指しする —— クイックスタートの「確かめる」の 14 本に、capture-fixtures と、 試験が名乗る e2e のクロールの許可(fga:grant submitter e2e acme)を足した 16 本。立っているかだけで なく、クロールが最後まで走る設定かも見る —— どれかが外れていると、クロールは段の報告のところで 初めて落ち、running のまま残る。vitest の外に置いてあるのは意図的 —— global setup が throw すると vitest は必ず「No test files found, exiting with code 1」を先に出し、中に書いたどんなメッセージも その後ろに隠れるため。

この repo が実際に出した不具合は、重ならない 2 つの層に分かれる。

不具合 単体 e2e
respect_robots が null で robots が読まれなかった ○ ○
段の境目で間隔が 3000ms → 521ms に潰れた ○ △
本文の無い POST に content-type を付けて 400 ○ ○
hostParallelism が camelCase で null になった ✗ ○
schema の既定値が webhook 実行では効かない ✗ ○

下 2 つは定義上単体には見えない —— 引数を渡すのは flow.yaml であって TypeScript ではないから。片方だけを選ぶと、穴の半分がそのまま残る。

windmill-client は既に devDependency で、plan_level.ts が import するのは robots-parser だけ。だから script はそのまま vitest に読み込める。IO は差し替える: vi.stubGlobal("fetch", …) と vi.mock("windmill-client", …)。

captureHost を crawl_host.ts から export してあるのは、間隔のループを偽の client で 回すため —— call() は client[method](req, cb) を呼ぶだけなので、偽物はただの オブジェクトで足りる。retry(全 BrowserHive が busy のときの待ち幅)を引数にして あるのは、実タイマーの ms スケールで書くため。fake timer は、この workspace の 別の場所で capture 系の sleep に対して試して一往復溶かしている。

どの試験も、Windmill の実行の口ではなく capture-ledger の API でクロールを始める —— 目的が 「capture-ledger が実際に送る引数」を運ぶことだから: crawl_id / depth / frontier / per_host_delay_ms / host_parallelism / capture_formats / signing / scripts と、 受け口を出している配備では artifact_sink で、respect_robots は入っていない。

最初の試験が見ているのは 4 つ:

  1. capture-fixtures が /links/hidden を一度も受け取っていないこと。robots.txt が禁じている ページで、判定は相手のリクエストログから採る —— 台帳は「記録したこと」しか 言わない。
  2. クロールが成功し、1 ページ以上取り込んでいること
  3. GET /api/search?q=hub がヒットを返すこと —— 索引の step まで flow の中で 終わっている証拠
  4. capture-ledger が受け口を出していれば(トークン無しの PUT /api/sink/… が 404 でなく 401 を返す)、そのクロールのアーカイブの objectKey が全部 org/acme/<YYYY-MM>/ の下に あること。平らな鍵は、口が配られず BrowserHive が自前の bucket へ書いた印 —— capture-ledger #226 から直るまで実際にそうなっていて、その形のクロールは上の 3 つを通る。

plan_level.ts の ?? true を消して配備すると、単体と e2e が両方赤くなり、 e2e のほうは capture-fixtures が禁じられたページを受け取ったことを示す。この対は一度わざと 確かめてある: 片方しか赤くならないなら、もう片方は見ているつもりで見ていない。

残りは、クロールがページの中で走らせるスクリプトを見る。判定はやはり capture-fixtures の リクエストログで、query を含む URL ごとに全部の要求を数えている。

  • 台帳のスクリプトは、頼んだときだけ走る。 名指した autofetch が /assets/hero-2x.svg を 取りに行く(1280px・DPR 1 のブラウザは自分では要求しない)。対照は同じページへの 2 本で、 scripts: [] を送っても、scripts を省いて台帳の既定に任せても、誰も要求しない —— autofetch は 既定に入らない(capture-scripts v0.5.0 から)。
  • 型の通らないスクリプトは、compile の段でクロールごと落ちる。 診断の件数と最初の 5 件が、 クロールの error に残る。 台帳のスクリプト(台帳の CLI で壊れたスクリプトを足して消す)と、その場の JS の両方で見る。
  • その場の JS は、頼んだ順に走る。 /plain-html?inline=<token>&state=<document.readyState> を 取りに行き、ログに印を残す。遷移の前に走れば loading、読み込みの後なら complete。
  • 同じその場の JS を preload で頼み直すと、遷移の前に走る。 2 本とも同じ source なので、 2 本目は ts-compile-service の cache に当たる。答えごと覚えていた cache は 1 本目の behavior を 返し、印は complete になった。ts-compile-service v0.2.2 までがその形で、この試験はそれに対して赤い。
  • 型の誤りがいくつあっても、クロールは failed で締まり、次を塞がない。 型の誤りを数百件出す その場の JS で始める。error が件数を言い、台帳の上限の 2,000 字に収まることと、続けて始めた 1 本が通ることを見る。v0.24.2 までは診断を全部載せたので台帳が断り、クロールは running のまま残った。

trigger_crawl.ts は日次の取り込みの成否を決めている唯一の場所で、 e2e はそこを触らない —— あちらは自分で起点を capture-ledger に投げるので、この script を通らない。 以前は覆いが 1 つも無かった —— POLL_INTERVAL_MS が 15 秒の定数で、しかも sleep が 最初の問い合わせより前に入るため、試験 1 本が 15 秒かかったから。

いまは poll_interval_ms を引数で受ける。crawl_host の busy 待ちと同じ手で、 実タイマーの ms スケールで回せる。

知っておくべき砦はこれ:

if (crawl.state !== "succeeded") throw new Error(`知らない状態「…」`);

これが無いと、script が知らない状態は 2 つの比較を素通りして succeeded を 件数 0 で返す。名前がずれたときの見え方がまさにこれ —— そして実際に一度起きた: capture-ledger の runs.status が runs.state になったとき、改名前の script を改名後の API に当てて、ここが発火することを確かめた。走らなかった実行を緑と報告する代わりに、 原因の見当まで添えて落ちた。その後 runs は crawls に畳まれたが、砦はそのまま 移り、いまは crawl.state を見ている。

線の形を守っているものは他に無い —— capture-ledger 側に response schema は無く、こちらは as CrawlState の素のキャスト。

e2e はクロールの起点を URL で渡すので、 fromTargets(capture_targets を起点にする道 —— 日次が通るのはこちら)はどこでも回っていない。 capture-ledger 側のその経路は、両側の単体試験でしか触れていない。