Public: anyone can read this; the crew can change it.
scripts/drills/run.ts <domain> refuses a domain with no drill, prints the fault to cause, waits for Enter and prints watchDrill's result #2579
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/2394-scripts-drills-run-ts-domain-refuses-a-d → main 2 commits fork 2 threads · 2 unresolved
3 files
changed +54
−13
docs/DOMAINS.md +1 −1
| @@ -35,7 +35,7 @@ two devices backed by the same physical disk, are not. | ||
| 35 | 35 | ## Running a drill |
| 36 | 36 | |
| 37 | 37 | `scripts/drills/run.ts` walks you through one. It refuses a domain whose drill |
| 38 | is none, prints the fault to cause, and waits for Enter. It then polls | |
| 38 | is none, prints the fault to cause, and waits for Enter (a line on stdin, so piping one works; closing stdin aborts). It then polls | |
| 39 | 39 | `/api/v1/health` and `/` once a second, keeping each distinct line the forge |
| 40 | 40 | says while unhealthy; when the forge is healthy again it posts what it saw and |
| 41 | 41 | the seconds it took to `POST /api/v1/drills` and prints the verdict. If the |
scripts/drills/run.ts +3 −4
| @@ -141,7 +141,6 @@ const LOOPBACK = new Set(["localhost", "127.0.0.1", "[::1]", "::1"]); | ||
| 141 | 141 | export function checkArgs( |
| 142 | 142 | argv: string[], |
| 143 | 143 | env: Record<string, string | undefined>, |
| 144 | isTTY: boolean, | |
| 145 | 144 | ): { ok: true; args: DrillArgs } | { ok: false; error: string } { |
| 146 | 145 | const at = argv.indexOf("--forge"); |
| 147 | 146 | const forge = at >= 0 ? argv[at + 1] : undefined; |
| @@ -156,7 +155,6 @@ export function checkArgs( | ||
| 156 | 155 | } |
| 157 | 156 | const token = env.FORGE_TOKEN ?? ""; |
| 158 | 157 | if (token === "") return { ok: false, error: `FORGE_TOKEN is empty; ${USAGE}` }; |
| 159 | if (!isTTY) return { ok: false, error: "stdin is not a terminal; run the drill from an interactive shell" }; | |
| 160 | 158 | if (url.protocol === "http:" && !LOOPBACK.has(url.hostname) && !insecure) { |
| 161 | 159 | return { ok: false, error: `refusing to send the admin token over plain http to ${url.hostname}; use https, or pass --insecure` }; |
| 162 | 160 | } |
| @@ -164,7 +162,7 @@ export function checkArgs( | ||
| 164 | 162 | } |
| 165 | 163 | |
| 166 | 164 | if (import.meta.main) { |
| 167 | const checked = checkArgs(process.argv.slice(2), process.env, process.stdin.isTTY === true); | |
| 165 | const checked = checkArgs(process.argv.slice(2), process.env); | |
| 168 | 166 | if (!checked.ok) { |
| 169 | 167 | console.error(checked.error); |
| 170 | 168 | process.exit(2); |
| @@ -173,7 +171,8 @@ if (import.meta.main) { | ||
| 173 | 171 | ...checked.args, |
| 174 | 172 | say: (l) => console.log(clean(l)), |
| 175 | 173 | enter: async () => { |
| 176 | for await (const _ of console) break; | |
| 174 | for await (const _ of console) return; | |
| 175 | throw new Error("stdin closed before Enter; nothing was caused or watched"); | |
| 177 | 176 | }, |
This throw surfaces as an unhandled rejection with a stack trace. Catch it where enter is awaited, print only the message and exit non-zero. |
||
| 178 | 177 | }); |
| 179 | 178 | process.exit(code); |
test/drill-run.test.ts +50 −8
| @@ -119,17 +119,16 @@ test("a sign-in redirect from / is up, and a changing counter is one line", asyn | ||
| 119 | 119 | |
| 120 | 120 | test("checkArgs: each missing piece and the cleartext refusal is an error; the good case is options", () => { |
| 121 | 121 | const env = { FORGE_TOKEN: "t" }; |
| 122 | const bad = (argv: string[], e: Record<string, string | undefined>, tty: boolean) => { | |
| 123 | const r = checkArgs(argv, e, tty); | |
| 122 | const bad = (argv: string[], e: Record<string, string | undefined>) => { | |
| 123 | const r = checkArgs(argv, e); | |
| 124 | 124 | expect(r.ok).toBe(false); |
| 125 | 125 | return r.ok ? "" : r.error; |
| 126 | 126 | }; |
| 127 | expect(bad(["--forge", "https://f.example"], env, true)).toContain("domain"); | |
| 128 | expect(bad(["runner"], env, true)).toContain("--forge"); | |
| 129 | expect(bad(["runner", "--forge", "https://f.example"], { FORGE_TOKEN: "" }, true)).toContain("FORGE_TOKEN"); | |
| 130 | expect(bad(["runner", "--forge", "https://f.example"], env, false)).toContain("not a terminal"); | |
| 131 | expect(bad(["runner", "--forge", "http://forge.example"], env, true)).toContain("plain http"); | |
| 132 | const ok = (argv: string[]) => checkArgs(argv, env, true); | |
| 127 | expect(bad(["--forge", "https://f.example"], env)).toContain("domain"); | |
| 128 | expect(bad(["runner"], env)).toContain("--forge"); | |
| 129 | expect(bad(["runner", "--forge", "https://f.example"], { FORGE_TOKEN: "" })).toContain("FORGE_TOKEN"); | |
| 130 | expect(bad(["runner", "--forge", "http://forge.example"], env)).toContain("plain http"); | |
| 131 | const ok = (argv: string[]) => checkArgs(argv, env); | |
| 133 | 132 | expect(ok(["runner", "--forge", "http://forge.example", "--insecure"]).ok).toBe(true); |
| 134 | 133 | for (const host of ["localhost:3000", "127.0.0.1:1", "[::1]:1"]) expect(ok(["runner", "--forge", `http://${host}/`]).ok).toBe(true); |
| 135 | 134 | expect(ok(["runner", "--forge", "https://f.example/"])).toEqual({ |
| @@ -161,3 +160,46 @@ test("control characters the forge sends are removed before printing or posting; | ||
| 161 | 160 | expect(said).not.toContain("\x1b"); |
| 162 | 161 | expect(JSON.stringify(state.posts[0]!.body)).not.toContain("\\u001b"); |
| 163 | 162 | }); |
| 163 | ||
| 164 | test("spawned with piped stdin: waits for Enter, watches, prints the JSON; no drill exits non-zero", async () => { | |
| 165 | const state: Stub = { down: false, posts: [], refuse: false }; | |
| 166 | const forge = stub(state); | |
| 167 | const spawn = (domain: string) => | |
| 168 | Bun.spawn(["bun", "scripts/drills/run.ts", domain, "--forge", forge], { | |
| 169 | stdin: "pipe", | |
| 170 | stdout: "pipe", | |
| 171 | stderr: "pipe", | |
| 172 | env: { ...process.env, FORGE_TOKEN: "tok" }, | |
| 173 | }); | |
| 174 | ||
| 175 | const none = spawn("state"); | |
| 176 | expect(await none.exited).not.toBe(0); | |
| 177 | expect(await new Response(none.stdout).text()).toContain("has no drill"); | |
| 178 | ||
| 179 | const p = spawn("compute"); | |
| 180 | const out = new Response(p.stdout).text(); | |
| 181 | await Bun.sleep(300); | |
| 182 | expect(state.posts).toEqual([]); | |
| 183 | state.down = true; | |
| 184 | p.stdin.write("\n"); | |
| 185 | p.stdin.end(); | |
| 186 | await Bun.sleep(1500); | |
| 187 | state.down = false; | |
| 188 | expect(await p.exited).toBe(0); | |
| 189 | const lines = (await out).trim().split("\n"); | |
| 190 | expect(lines[0]).toContain("kill-pod"); | |
| 191 | const result = JSON.parse(lines[lines.length - 1]!); | |
| 192 | expect(result).toMatchObject({ domain: "compute", posted: true }); | |
| 193 | expect(state.posts).toHaveLength(1); | |
| 194 | expect(state.posts[0]!.auth).toBe("Bearer tok"); | |
| 195 | }); | |
| 196 | ||
| 197 | test("stdin closed without Enter exits non-zero and posts nothing", async () => { | |
| 198 | const state: Stub = { down: false, posts: [], refuse: false }; | |
| 199 | const forge = stub(state); | |
| 200 | const p = Bun.spawn(["bun", "scripts/drills/run.ts", "compute", "--forge", forge], { | |
| 201 | stdin: "ignore", stdout: "pipe", stderr: "pipe", env: { ...process.env, FORGE_TOKEN: "tok" }, | |
| 202 | }); | |
| 203 | expect(await p.exited).not.toBe(0); | |
| 204 | expect(state.posts).toEqual([]); | |
| 205 | }); |
new above
offline
Wrap this line like the ones around it. Also say plainly that a non-interactive stdin is now accepted.