試験
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」を先に出し、中に書いたどんなメッセージも
その後ろに隠れるため。
なぜ二層とも要るのか
Section titled “なぜ二層とも要るのか”この repo が実際に出した不具合は、重ならない 2 つの層に分かれる。
| 不具合 | 単体 | e2e |
|---|---|---|
respect_robots が null で robots が読まれなかった |
○ | ○ |
| 段の境目で間隔が 3000ms → 521ms に潰れた | ○ | △ |
本文の無い POST に content-type を付けて 400 |
○ | ○ |
hostParallelism が camelCase で null になった |
✗ | ○ |
| schema の既定値が webhook 実行では効かない | ✗ | ○ |
下 2 つは定義上単体には見えない —— 引数を渡すのは flow.yaml であって TypeScript
ではないから。片方だけを選ぶと、穴の半分がそのまま残る。
単体は何も要らない
Section titled “単体は何も要らない”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 に対して試して一往復溶かしている。
e2e は本物のクロールを始める
Section titled “e2e は本物のクロールを始める”どの試験も、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 つ:
- capture-fixtures が
/links/hiddenを一度も受け取っていないこと。robots.txt が禁じている ページで、判定は相手のリクエストログから採る —— 台帳は「記録したこと」しか 言わない。 - クロールが成功し、1 ページ以上取り込んでいること
GET /api/search?q=hubがヒットを返すこと —— 索引の step まで flow の中で 終わっている証拠- 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 が禁じられたページを受け取ったことを示す。この対は一度わざと
確かめてある: 片方しか赤くならないなら、もう片方は見ているつもりで見ていない。
ページの中で走るもの
Section titled “ページの中で走るもの”残りは、クロールがページの中で走らせるスクリプトを見る。判定はやはり 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 のまま残った。
日次のループは推測しない
Section titled “日次のループは推測しない”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 の素のキャスト。
覆えていないもの
Section titled “覆えていないもの”e2e はクロールの起点を URL で渡すので、
fromTargets(capture_targets を起点にする道 —— 日次が通るのはこちら)はどこでも回っていない。
capture-ledger 側のその経路は、両側の単体試験でしか触れていない。