Public: anyone can read this; the crew can change it.
Bug hunters on a schedule, as a self-improving loop: every escape becomes a detector; KPIs (escapes → 0, valid ≥ 80%, fix < 24 h) #5873
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/5846-bug-hunters-on-a-schedule-as-a-self-impr → main 3 commits fork 4 threads · 4 unresolved
10 files
changed +418
−0
README.md +4
| @@ -704,6 +704,10 @@ event, without a fork chain; one picked from a connected origin then has none. | ||
| 704 | 704 | |
| 705 | 705 | `/search` answers repositories and issues in full, and greps code repository by repository. A first search starts no new `git grep` after 30 ms (`codeBudgetMs` in the forge config; a soft stop, since the grep in flight still finishes, one batch of four) and shows what it has with one link, **More code results** (`?code=all`), that greps the rest. The API and the terminal palette always grep everything. |
| 706 | 706 | |
| 707 | ## Bug hunters | |
| 708 | ||
| 709 | `bun scripts/hunt.ts <origin>` runs the hunters' detectors against one running app (GET only, same-origin, one pass) and prints findings as JSON; it exits 1 when there are any. Lanes and what is not built yet: `docs/HUNT.md`. | |
| 710 | ||
| 707 | 711 | ## Health |
| 708 | 712 | |
| 709 | 713 | `GET /api/v1/health` (and the MCP `health` tool) answer a site admin with |
docs/HUNT.md added +13
| @@ -0,0 +1,13 @@ | ||
| 1 | # Hunters (#5846) | |
| 2 | ||
| 3 | `bun scripts/hunt.ts <origin>` runs the detector sets in `src/hunt/` against one app and prints findings as JSON (exit 1 when there are any). It is GET-only by construction (`probe.ts` has no other method), same-origin only, and ends after one pass. | |
| 4 | ||
| 5 | | Lane | Module | Detectors | | |
| 6 | |---|---|---| | |
| 7 | | Front door | `door.ts` | personal pages send anonymous visitors to sign-in with `next=`; public pages answer 200; unknown path is 404 | | |
| 8 | | UI (links) | `links.ts` | every link on the top pages: no 404/5xx, and a link landing on `/` must be labelled Home (#5844) | | |
| 9 | | Contract | `contract.ts` | no OpenAPI GET route answers 5xx; the status is documented or a refusal | | |
| 10 | ||
| 11 | A finding's `key` is stable (`detector:subject`): one issue per key, a comment only when the evidence changes. | |
| 12 | ||
| 13 | Not built yet: headless-browser lane (buttons, console errors, fold, overflow), flows, speed, the schedule (post-release and 08:00Z), filing issues, the KPI card on home, and the weekly retro. |
scripts/hunt.ts added +16
| @@ -0,0 +1,16 @@ | ||
| 1 | /** | |
| 2 | * `bun scripts/hunt.ts <https-origin>`: run the hunters' detectors (GET only) | |
| 3 | * against one app and print the findings as JSON. Exit 1 when there are any. | |
| 4 | * Never run on a person's machine in a loop: it is one pass, then it ends. | |
| 5 | */ | |
| 6 | ||
| 7 | import { huntTarget } from "../src/hunt/hunt.ts"; | |
| 8 | ||
| 9 | const target = process.argv[2]; | |
| 10 | if (!target || !/^https?:\/\//.test(target)) { | |
| 11 | console.error("usage: bun scripts/hunt.ts <origin>"); | |
| 12 | process.exit(2); | |
| 13 | } | |
| 14 | const report = await huntTarget(target.replace(/\/$/, "")); | |
| 15 | console.log(JSON.stringify(report, null, 2)); | |
| 16 | process.exit(report.findings.length === 0 ? 0 : 1); |
src/hunt/contract.ts added +36
| @@ -0,0 +1,36 @@ | ||
| 1 | /** | |
| 2 | * Contract: no documented GET route answers 500 to an anonymous caller, and | |
| 3 | * what it answers is a status its OpenAPI entry documents (or a refusal). | |
| 4 | */ | |
| 5 | ||
| 6 | import { found, seenLine, type Finding } from "./finding.ts"; | |
| 7 | import type { Probe } from "./probe.ts"; | |
| 8 | ||
| 9 | const REFUSALS = new Set(["401", "403", "404", "429"]); | |
| 10 | ||
| 11 | interface Doc { | |
| 12 | paths?: Record<string, Record<string, { responses?: Record<string, unknown> }>>; | |
| 13 | } | |
| 14 | ||
| 15 | export async function contract(p: Probe): Promise<Finding[]> { | |
| 16 | const out: Finding[] = []; | |
| 17 | const doc = await p.get("/contract/openapi.json"); | |
| 18 | let parsed: Doc; | |
| 19 | try { | |
| 20 | parsed = JSON.parse(doc.body) as Doc; | |
| 21 | } catch { | |
| 22 | return [found("contract.openapi", "/contract/openapi.json", "the OpenAPI document is not JSON", seenLine("/contract/openapi.json", doc.status))]; | |
| 23 | } | |
| 24 | for (const [path, ops] of Object.entries(parsed.paths ?? {})) { | |
| 25 | const get = ops.get; | |
| 26 | if (get === undefined || /[{}]/.test(path)) continue; | |
| 27 | const r = await p.get(path); | |
| 28 | if (r.status === 0 || r.status >= 500) out.push(found("contract.no-500", path, `${path} answers ${r.status}`, seenLine(path, r.status))); | |
| 29 | else if (get.responses && Object.keys(get.responses).length > 0) { | |
| 30 | const code = String(r.status); | |
| 31 | if (!(code in get.responses) && !REFUSALS.has(code) && !("default" in get.responses)) | |
| 32 | out.push(found("contract.documented", path, `${path} answers ${r.status}, which its entry does not document`, `${seenLine(path, r.status)}; documented: ${Object.keys(get.responses).join(", ")}`)); | |
| 33 | } | |
| 34 | } | |
| 35 | return out; | |
| 36 | } |
src/hunt/door.ts added +31
| @@ -0,0 +1,31 @@ | ||
| 1 | /** | |
| 2 | * Front door: an anonymous visitor to a page that needs a person is sent to | |
| 3 | * sign-in with a way back, an unknown path is a plain 404, and the public top | |
| 4 | * pages answer. | |
| 5 | */ | |
| 6 | ||
| 7 | import { found, seenLine, type Finding } from "./finding.ts"; | |
| 8 | import type { Probe } from "./probe.ts"; | |
| 9 | ||
| 10 | export const PERSONAL_PAGES = ["/settings", "/new", "/migrate", "/review"]; | |
| 11 | export const PUBLIC_PAGES = ["/", "/signin", "/explore", "/apps", "/healthz"]; | |
| 12 | ||
| 13 | export async function frontDoor(p: Probe): Promise<Finding[]> { | |
| 14 | const out: Finding[] = []; | |
| 15 | for (const path of PERSONAL_PAGES) { | |
| 16 | const r = await p.get(path); | |
| 17 | const sent = r.status >= 300 && r.status < 400 && /\/signin(\?|$)/.test(r.location ?? ""); | |
| 18 | if (!sent) | |
| 19 | out.push(found("door.signin-redirect", path, `${path} does not send an anonymous visitor to sign-in`, seenLine(path, r.status, r.location))); | |
| 20 | else if (!(r.location ?? "").includes("next=")) | |
| 21 | out.push(found("door.signin-next", path, `${path} sends to sign-in without a way back`, seenLine(path, r.status, r.location))); | |
| 22 | } | |
| 23 | for (const path of PUBLIC_PAGES) { | |
| 24 | const r = await p.get(path); | |
| 25 | if (r.status !== 200) out.push(found("door.public-page", path, `${path} does not answer 200 to an anonymous visitor`, seenLine(path, r.status, r.location))); | |
| 26 | } | |
| 27 | const missing = "/no-such-page-hunter"; | |
| 28 | const r = await p.get(missing); | |
| 29 | if (r.status !== 404) out.push(found("door.unknown-404", missing, "an unknown path does not answer 404", seenLine(missing, r.status, r.location))); | |
| 30 | return out; | |
| 31 | } |
src/hunt/finding.ts added +20
| @@ -0,0 +1,20 @@ | ||
| 1 | /** What a detector says: plain data, one issue per `key` (the sentinel's rule). */ | |
| 2 | ||
| 3 | export interface Finding { | |
| 4 | detector: string; | |
| 5 | /** Stable across runs: the same defect has the same key, so it files once. */ | |
| 6 | key: string; | |
| 7 | title: string; | |
| 8 | /** The minimal repro: the request and what came back. */ | |
| 9 | evidence: string; | |
| 10 | } | |
| 11 | ||
| 12 | export const found = (detector: string, subject: string, title: string, evidence: string): Finding => ({ | |
| 13 | detector, | |
| 14 | key: `${detector}:${subject}`, | |
| 15 | title, | |
| 16 | evidence, | |
| 17 | }); | |
| 18 | ||
| 19 | export const seenLine = (path: string, status: number, location?: string | null): string => | |
| 20 | `GET ${path} → ${status}${location ? ` → ${location}` : ""}`; |
src/hunt/hunt.ts added +33
| @@ -0,0 +1,33 @@ | ||
| 1 | /** One hunt: every detector set against one target, timed (Hunt time KPI). */ | |
| 2 | ||
| 3 | import { contract } from "./contract.ts"; | |
| 4 | import { frontDoor } from "./door.ts"; | |
| 5 | import type { Finding } from "./finding.ts"; | |
| 6 | import { linkLanding } from "./links.ts"; | |
| 7 | import { probe, type Fetcher, type Probe } from "./probe.ts"; | |
| 8 | ||
| 9 | export interface HuntReport { | |
| 10 | target: string; | |
| 11 | ms: number; | |
| 12 | findings: Finding[]; | |
| 13 | } | |
| 14 | ||
| 15 | /** The lanes this module covers; UI (headless), flows and speed need a browser and are not here. */ | |
| 16 | export const LANES: Record<string, (p: Probe, base: string) => Promise<Finding[]>> = { | |
outdated
LANES is unused. huntTarget should loop over it, or this line should go. |
||
| 17 | door: (p) => frontDoor(p), | |
| 18 | links: linkLanding, | |
| 19 | contract: (p) => contract(p), | |
| 20 | }; | |
| 21 | ||
| 22 | export async function huntTarget(base: string, fetcher?: Fetcher): Promise<HuntReport> { | |
| 23 | const started = Date.now(); | |
| 24 | const p = probe(base, fetcher); | |
| 25 | const findings: Finding[] = []; | |
| 26 | for (const lane of Object.values(LANES)) findings.push(...(await lane(p, base))); | |
| 27 | const seen = new Set<string>(); | |
| 28 | return { | |
| 29 | target: base, | |
| 30 | ms: Date.now() - started, | |
| 31 | findings: findings.filter((f) => !seen.has(f.key) && seen.add(f.key)), | |
| 32 | }; | |
| 33 | } |
src/hunt/links.ts added +59
| @@ -0,0 +1,59 @@ | ||
| 1 | /** | |
| 2 | * Every link on the top pages lands where its label says (#5844): no 404 or | |
| 3 | * 5xx, and a link that lands on `/` is only fine when it is Home. | |
| 4 | */ | |
| 5 | ||
| 6 | import { found, seenLine, type Finding } from "./finding.ts"; | |
| 7 | import { inside, type Probe } from "./probe.ts"; | |
| 8 | ||
| 9 | export const TOP_PAGES = ["/", "/explore", "/apps", "/forges", "/board", "/migrate", "/system"]; | |
| 10 | const HOME = /^(home|forge|the forge|feed|back to (the )?(forge|home|feed))$/i; | |
| 11 | const TAG = /<a\s([^>]*)>([\s\S]*?)<\/a>/gi; | |
| 12 | ||
| 13 | export interface Link { | |
| 14 | href: string; | |
| 15 | label: string; | |
| 16 | } | |
| 17 | ||
| 18 | const text = (html: string): string => | |
| 19 | html.replace(/<[^>]*>/g, " ").replace(/&/g, "&").replace(/\s+/g, " ").trim(); | |
| 20 | ||
| 21 | export function linksIn(html: string): Link[] { | |
| 22 | const out: Link[] = []; | |
| 23 | for (const m of html.matchAll(TAG)) { | |
| 24 | const href = /\bhref="([^"]*)"/i.exec(m[1] ?? "")?.[1]; | |
| 25 | if (href === undefined || href.startsWith("#") || /^(mailto|tel|javascript):/i.test(href)) continue; | |
| 26 | const label = text(m[2] ?? "") || /aria-label="([^"]*)"/i.exec(m[1] ?? "")?.[1] || ""; | |
| 27 | out.push({ href: href.replace(/&/g, "&"), label }); | |
| 28 | } | |
| 29 | return out; | |
| 30 | } | |
| 31 | ||
| 32 | const isHome = (label: string): boolean => HOME.test(label); | |
| 33 | ||
| 34 | export async function linkLanding(p: Probe, base: string, pages = TOP_PAGES): Promise<Finding[]> { | |
| 35 | const out: Finding[] = []; | |
| 36 | const checked = new Set<string>(); | |
| 37 | for (const page of pages) { | |
| 38 | const from = await p.get(page); | |
| 39 | if (from.status !== 200) continue; | |
| 40 | for (const link of linksIn(from.body)) { | |
| 41 | const target = inside(base, link.href); | |
| 42 | if (target === null || checked.has(`${target}\n${link.label}`)) continue; | |
| 43 | checked.add(`${target}\n${link.label}`); | |
| 44 | let r = await p.get(target); | |
| 45 | let hops = 0; | |
| 46 | while (r.status >= 300 && r.status < 400 && r.location && hops++ < 3) { | |
| 47 | const next = inside(base, r.location); | |
| 48 | if (next === null) break; | |
| 49 | r = await p.get(next); | |
| 50 | } | |
A 429 or a non-HTML landing passes silently here; report it as inconclusive so a throttled hunt is not read as a clean one. |
||
| 51 | const where = `${seenLine(target, r.status)} (link "${link.label}" on ${page})`; | |
| 52 | if (r.status === 0 || r.status === 404 || r.status >= 500) | |
| 53 | out.push(found("links.status", target, `the link ${target} on ${page} answers ${r.status}`, where)); | |
| 54 | else if (r.status < 300 && r.path === "/" && target !== "/" && !isHome(link.label)) | |
| 55 | out.push(found("links.lands-home", target, `the link "${link.label}" lands on / and is not Home`, where)); | |
| 56 | } | |
| 57 | } | |
| 58 | return out; | |
| 59 | } |
src/hunt/probe.ts added +73
| @@ -0,0 +1,73 @@ | ||
| 1 | /** | |
| 2 | * The hunter's only way to touch an app: GET, never following a redirect on | |
| 3 | * its own. A validator against production must be unable to mutate, so no | |
| 4 | * other method exists here (#5846). | |
| 5 | */ | |
| 6 | ||
| 7 | export type Fetcher = (url: string, init: RequestInit) => Promise<Response>; | |
| 8 | ||
| 9 | export interface Seen { | |
| 10 | path: string; | |
| 11 | status: number; | |
| 12 | location: string | null; | |
| 13 | type: string; | |
| 14 | body: string; | |
| 15 | } | |
| 16 | ||
| 17 | export interface Probe { | |
| 18 | get(path: string): Promise<Seen>; | |
| 19 | } | |
| 20 | ||
| 21 | /** Same-origin only: a path or an absolute URL on the target's origin. */ | |
| 22 | export function inside(base: string, href: string): string | null { | |
| 23 | let url: URL; | |
| 24 | try { | |
| 25 | url = new URL(href, base); | |
| 26 | } catch { | |
| 27 | return null; | |
| 28 | } | |
| 29 | if (url.origin !== new URL(base).origin) return null; | |
| 30 | return url.pathname + url.search; | |
| 31 | } | |
| 32 | ||
| 33 | const BODY_MAX = 400_000; | |
| 34 | ||
| 35 | /** At most BODY_MAX bytes of the body; the rest is never read. */ | |
| 36 | async function readBounded(res: Response): Promise<string> { | |
| 37 | const reader = res.body?.getReader(); | |
| 38 | if (!reader) return ""; | |
outdated
A rejected fetch or the timeout throws out of the whole hunt. Catch and turn it into a finding; test it. Bound the body read too (res.text() reads it all before slice). |
||
| 39 | const chunks: Uint8Array[] = []; | |
| 40 | let size = 0; | |
| 41 | while (size < BODY_MAX) { | |
| 42 | const { done, value } = await reader.read(); | |
| 43 | if (done) break; | |
| 44 | chunks.push(value); | |
| 45 | size += value.length; | |
| 46 | } | |
| 47 | await reader.cancel().catch(() => {}); | |
| 48 | return new TextDecoder().decode(Buffer.concat(chunks).subarray(0, BODY_MAX)); | |
| 49 | } | |
| 50 | ||
| 51 | export function probe(base: string, fetcher: Fetcher = fetch as Fetcher): Probe { | |
| 52 | return { | |
| 53 | async get(path) { | |
| 54 | const target = inside(base, path); | |
| 55 | if (target === null) throw new Error(`refusing a path off the target: ${path}`); | |
| 56 | try { | |
| 57 | const res = await fetcher(new URL(target, base).href, { | |
| 58 | method: "GET", | |
| 59 | redirect: "manual", | |
| 60 | headers: { "user-agent": "forge-hunter" }, | |
| 61 | signal: AbortSignal.timeout(10_000), | |
| 62 | }); | |
| 63 | const type = res.headers.get("content-type") ?? ""; | |
| 64 | const body = /text|json|xml/.test(type) ? await readBounded(res) : ""; | |
| 65 | await res.body?.cancel().catch(() => {}); | |
| 66 | return { path: target, status: res.status, location: res.headers.get("location"), type, body }; | |
| 67 | } catch { | |
| 68 | // A dead or hung route is a result (status 0), not a crash that loses the pass. | |
| 69 | return { path: target, status: 0, location: null, type: "", body: "" }; | |
| 70 | } | |
| 71 | }, | |
| 72 | }; | |
| 73 | } |
test/hunt.test.ts added +133
| @@ -0,0 +1,133 @@ | ||
| 1 | import { describe, expect, test } from "bun:test"; | |
| 2 | import { huntTarget } from "../src/hunt/hunt.ts"; | |
| 3 | import { linksIn } from "../src/hunt/links.ts"; | |
| 4 | import { inside, probe, type Fetcher } from "../src/hunt/probe.ts"; | |
| 5 | ||
| 6 | const BASE = "https://app.test"; | |
| 7 | type Page = { status?: number; body?: string; type?: string; location?: string }; | |
| 8 | ||
| 9 | const site = (pages: Record<string, Page>): { fetcher: Fetcher; methods: string[] } => { | |
| 10 | const methods: string[] = []; | |
| 11 | const fetcher: Fetcher = async (url, init) => { | |
| 12 | methods.push(String(init.method)); | |
| 13 | const u = new URL(url); | |
| 14 | const page = pages[u.pathname + u.search] ?? pages[u.pathname] ?? { status: 404, body: "" }; | |
| 15 | const headers: Record<string, string> = { "content-type": page.type ?? "text/html" }; | |
| 16 | if (page.location) headers.location = page.location; | |
| 17 | return new Response(page.body ?? "", { status: page.status ?? 200, headers }); | |
| 18 | }; | |
| 19 | return { fetcher, methods }; | |
| 20 | }; | |
| 21 | ||
| 22 | const healthy: Record<string, Page> = { | |
| 23 | "/": { body: '<a href="/explore">Explore</a><a href="/">Home</a>' }, | |
| 24 | "/signin": {}, | |
| 25 | "/explore": {}, | |
| 26 | "/apps": {}, | |
| 27 | "/healthz": {}, | |
| 28 | "/settings": { status: 303, location: "/signin?next=%2Fsettings" }, | |
| 29 | "/new": { status: 303, location: "/signin?next=%2Fnew" }, | |
| 30 | "/migrate": { status: 303, location: "/signin?next=%2Fmigrate" }, | |
| 31 | "/review": { status: 303, location: "/signin?next=%2Freview" }, | |
| 32 | "/contract/openapi.json": { | |
| 33 | type: "application/json", | |
| 34 | body: JSON.stringify({ paths: { "/api/v1/apps": { get: { responses: { "200": {} } } }, "/api/v1/x/{id}": { get: {} } } }), | |
| 35 | }, | |
| 36 | "/api/v1/apps": { type: "application/json", body: "{}" }, | |
| 37 | }; | |
| 38 | ||
| 39 | describe("the hunter", () => { | |
| 40 | test("a healthy site has no findings, and only GET was sent", async () => { | |
| 41 | const { fetcher, methods } = site(healthy); | |
| 42 | const r = await huntTarget(BASE, fetcher); | |
| 43 | expect(r.findings).toEqual([]); | |
| 44 | expect(new Set(methods)).toEqual(new Set(["GET"])); | |
| 45 | }); | |
| 46 | ||
| 47 | test("a menu link that lands on / without being Home is found (#5844)", async () => { | |
| 48 | const { fetcher } = site({ | |
| 49 | ...healthy, | |
| 50 | "/": { body: '<a href="/mine">Your repositories</a>' }, | |
| 51 | "/mine": { status: 302, location: "/" }, | |
| 52 | }); | |
| 53 | const r = await huntTarget(BASE, fetcher); | |
| 54 | expect(r.findings.map((f) => f.key)).toContain("links.lands-home:/mine"); | |
| 55 | }); | |
| 56 | ||
| 57 | test("a 404 link, a 500 route and an open personal page are each found once", async () => { | |
| 58 | const { fetcher } = site({ | |
| 59 | ...healthy, | |
| 60 | "/": { body: '<a href="/gone">Gone</a><a href="/gone">Gone again</a>' }, | |
| 61 | "/settings": { status: 200 }, | |
| 62 | "/api/v1/apps": { status: 500, type: "application/json", body: "{}" }, | |
| 63 | }); | |
| 64 | const keys = (await huntTarget(BASE, fetcher)).findings.map((f) => f.key); | |
| 65 | expect(keys).toContain("links.status:/gone"); | |
| 66 | expect(keys).toContain("contract.no-500:/api/v1/apps"); | |
| 67 | expect(keys).toContain("door.signin-redirect:/settings"); | |
| 68 | expect(keys.filter((k) => k === "links.status:/gone")).toHaveLength(1); | |
| 69 | }); | |
| 70 | ||
| 71 | test("it refuses a path off the target", async () => { | |
| 72 | const { fetcher } = site({}); | |
| 73 | await expect(probe(BASE, fetcher).get("https://evil.test/x")).rejects.toThrow(); | |
| 74 | expect(inside(BASE, "//evil.test/x")).toBeNull(); | |
| 75 | expect(inside(BASE, "/a?b=1")).toBe("/a?b=1"); | |
| 76 | }); | |
| 77 | ||
| 78 | test("links are read with their labels", () => { | |
| 79 | expect(linksIn('<a class="x" href="/a?b=1&c=2"><b>Hi</b> there</a><a href="#top">t</a>')).toEqual([ | |
| 80 | { href: "/a?b=1&c=2", label: "Hi there" }, | |
| 81 | ]); | |
| 82 | }); | |
| 83 | }); | |
| 84 | ||
| 85 | describe("more detectors", () => { | |
| 86 | const keys = async (over: Record<string, Page>) => (await huntTarget(BASE, site({ ...healthy, ...over }).fetcher)).findings.map((f) => f.key); | |
| 87 | ||
| 88 | test("a sign-in redirect with no way back is found", async () => { | |
| 89 | expect(await keys({ "/new": { status: 303, location: "/signin" } })).toContain("door.signin-next:/new"); | |
| 90 | }); | |
| 91 | ||
| 92 | test("a status the route does not document is found", async () => { | |
| 93 | expect(await keys({ "/api/v1/apps": { status: 201, type: "application/json", body: "{}" } })).toContain("contract.documented:/api/v1/apps"); | |
| 94 | }); | |
| 95 | ||
| 96 | test("an OpenAPI document that is not JSON is found", async () => { | |
| 97 | expect(await keys({ "/contract/openapi.json": { body: "<html>" } })).toContain("contract.openapi:/contract/openapi.json"); | |
| 98 | }); | |
| 99 | ||
| 100 | test("a public page that fails is found", async () => { | |
| 101 | expect(await keys({ "/explore": { status: 500 } })).toContain("door.public-page:/explore"); | |
| 102 | }); | |
| 103 | ||
| 104 | test("an unknown path that answers 200 is found", async () => { | |
| 105 | expect(await keys({ "/no-such-page-hunter": { status: 200 } })).toContain("door.unknown-404:/no-such-page-hunter"); | |
| 106 | }); | |
| 107 | ||
| 108 | test("a dead route is a finding and does not end the pass", async () => { | |
| 109 | const { fetcher } = site(healthy); | |
| 110 | const dying: Fetcher = (url, init) => | |
| 111 | new URL(url).pathname === "/explore" ? Promise.reject(new Error("reset")) : fetcher(url, init); | |
| 112 | const r = await huntTarget(BASE, dying); | |
| 113 | expect(r.findings.map((f) => f.key)).toContain("door.public-page:/explore"); | |
| 114 | expect((await probe(BASE, dying).get("/explore")).status).toBe(0); | |
| 115 | }); | |
| 116 | ||
| 117 | test("a body is read only up to its bound", async () => { | |
| 118 | let pulled = 0; | |
| 119 | const endless: Fetcher = async () => | |
| 120 | new Response( | |
| 121 | new ReadableStream({ | |
| 122 | pull(c) { | |
| 123 | pulled += 100_000; | |
| 124 | c.enqueue(new Uint8Array(100_000).fill(97)); | |
| 125 | }, | |
| 126 | }), | |
| 127 | { headers: { "content-type": "text/plain" } }, | |
| 128 | ); | |
| 129 | const seen = await probe(BASE, endless).get("/"); | |
| 130 | expect(seen.body.length).toBe(400_000); | |
| 131 | expect(pulled).toBeLessThan(1_000_000); | |
| 132 | }); | |
| 133 | }); |
new above
offline
No test fails if this check or the 404 check below is removed. Add one each.