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

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/2394-scripts-drills-run-ts-domain-refuses-a-d → main 2 commits fork 2 threads · 2 unresolved

  • Merged. scout/2394-scripts-drills-run-ts-domain-refuses-a-d was merged into main as 5b52e9d.

Review

Sign in to review this change.
3 files changed +54 −13
docs/DOMAINS.md +1 −1
@@ -35,7 +35,7 @@ two devices backed by the same physical disk, are not.
3535 ## Running a drill
3636
3737 `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

Wrap this line like the ones around it. Also say plainly that a non-interactive stdin is now accepted.

3939 `/api/v1/health` and `/` once a second, keeping each distinct line the forge
4040 says while unhealthy; when the forge is healthy again it posts what it saw and
4141 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"]);
141141 export function checkArgs(
142142 argv: string[],
143143 env: Record<string, string | undefined>,
144 isTTY: boolean,
145144 ): { ok: true; args: DrillArgs } | { ok: false; error: string } {
146145 const at = argv.indexOf("--forge");
147146 const forge = at >= 0 ? argv[at + 1] : undefined;
@@ -156,7 +155,6 @@ export function checkArgs(
156155 }
157156 const token = env.FORGE_TOKEN ?? "";
158157 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" };
160158 if (url.protocol === "http:" && !LOOPBACK.has(url.hostname) && !insecure) {
161159 return { ok: false, error: `refusing to send the admin token over plain http to ${url.hostname}; use https, or pass --insecure` };
162160 }
@@ -164,7 +162,7 @@ export function checkArgs(
164162 }
165163
166164 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);
168166 if (!checked.ok) {
169167 console.error(checked.error);
170168 process.exit(2);
@@ -173,7 +171,8 @@ if (import.meta.main) {
173171 ...checked.args,
174172 say: (l) => console.log(clean(l)),
175173 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");
177176 },

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.

178177 });
179178 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
119119
120120 test("checkArgs: each missing piece and the cleartext refusal is an error; the good case is options", () => {
121121 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);
124124 expect(r.ok).toBe(false);
125125 return r.ok ? "" : r.error;
126126 };
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);
133132 expect(ok(["runner", "--forge", "http://forge.example", "--insecure"]).ok).toBe(true);
134133 for (const host of ["localhost:3000", "127.0.0.1:1", "[::1]:1"]) expect(ok(["runner", "--forge", `http://${host}/`]).ok).toBe(true);
135134 expect(ok(["runner", "--forge", "https://f.example/"])).toEqual({
@@ -161,3 +160,46 @@ test("control characters the forge sends are removed before printing or posting;
161160 expect(said).not.toContain("\x1b");
162161 expect(JSON.stringify(state.posts[0]!.body)).not.toContain("\\u001b");
163162 });
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 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs