Public: anyone can read this; the crew can change it.
A test pins the CLI's argument parser and durations: value flags, `--`, and `90s`/`5m`/`2h`/`250ms` #6631
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/6622-a-test-pins-the-cli-s-argument-parser-an → main 4 commits fork
1 file
changed +114
−0
test/cli-args.test.ts added +114
| @@ -0,0 +1,114 @@ | ||
| 1 | import { describe, expect, test } from "bun:test"; | |
| 2 | import { parseArgs, parseDuration, parseRepoRef } from "../src/cli/args.ts"; | |
| 3 | ||
| 4 | describe("parseArgs", () => { | |
| 5 | test("the first word is the command, the rest are positionals", () => { | |
| 6 | const a = parseArgs(["run", "one", "two"]); | |
| 7 | expect(a.command).toBe("run"); | |
| 8 | expect(a.positional).toEqual(["one", "two"]); | |
| 9 | }); | |
| 10 | ||
| 11 | test("no words means no command", () => { | |
| 12 | const a = parseArgs([]); | |
| 13 | expect(a.command).toBeUndefined(); | |
| 14 | expect(a.positional).toEqual([]); | |
| 15 | }); | |
| 16 | ||
| 17 | test("a declared value flag takes the next word", () => { | |
| 18 | const a = parseArgs(["scan", "--root", "src", "x"], { values: ["root"] }); | |
| 19 | expect(a.value("root")).toBe("src"); | |
| 20 | expect(a.flag("root")).toBe(true); | |
| 21 | expect(a.positional).toEqual(["x"]); | |
| 22 | }); | |
| 23 | ||
| 24 | test("an undeclared flag is a boolean and leaves the next word positional", () => { | |
| 25 | const a = parseArgs(["scan", "--root", "src"]); | |
| 26 | expect(a.flag("root")).toBe(true); | |
| 27 | expect(a.value("root")).toBeUndefined(); | |
| 28 | expect(a.positional).toEqual(["src"]); | |
| 29 | }); | |
| 30 | ||
| 31 | test("a declared value flag followed by a flag, or nothing, stays boolean", () => { | |
| 32 | const a = parseArgs(["c", "--root", "--other"], { values: ["root"] }); | |
| 33 | expect(a.value("root")).toBeUndefined(); | |
| 34 | expect(a.flag("root")).toBe(true); | |
| 35 | expect(a.flag("other")).toBe(true); | |
| 36 | const b = parseArgs(["c", "--root"], { values: ["root"] }); | |
| 37 | expect(b.value("root")).toBeUndefined(); | |
| 38 | expect(b.flag("root")).toBe(true); | |
| 39 | }); | |
| 40 | ||
| 41 | test("an absent flag is false", () => { | |
| 42 | expect(parseArgs(["c"]).flag("nope")).toBe(false); | |
| 43 | }); | |
| 44 | ||
| 45 | test("--key=value is supported, with or without a declaration", () => { | |
| 46 | const a = parseArgs(["c", "--root=src", "--eq=a=b"]); | |
| 47 | expect(a.value("root")).toBe("src"); | |
| 48 | expect(a.value("eq")).toBe("a=b"); | |
| 49 | expect(a.positional).toEqual([]); | |
| 50 | }); | |
| 51 | ||
| 52 | test("short flags are booleans, one per letter", () => { | |
| 53 | const a = parseArgs(["c", "-hv"]); | |
| 54 | expect(a.flag("h")).toBe(true); | |
| 55 | expect(a.flag("v")).toBe(true); | |
| 56 | }); | |
| 57 | ||
| 58 | test("a lone dash is a positional", () => { | |
| 59 | expect(parseArgs(["c", "-"]).positional).toEqual(["-"]); | |
| 60 | }); | |
| 61 | ||
| 62 | test("after -- every word is positional, even --x", () => { | |
| 63 | const a = parseArgs(["c", "--a", "--", "--x", "-v", "--"]); | |
| 64 | expect(a.flag("a")).toBe(true); | |
| 65 | expect(a.flag("x")).toBe(false); | |
| 66 | expect(a.flag("v")).toBe(false); | |
| 67 | expect(a.positional).toEqual(["--x", "-v", "--"]); | |
| 68 | }); | |
| 69 | ||
| 70 | test("-- before the command makes the command positional too", () => { | |
| 71 | const a = parseArgs(["--", "--weird"]); | |
| 72 | expect(a.command).toBe("--weird"); | |
| 73 | }); | |
| 74 | }); | |
| 75 | ||
| 76 | describe("parseDuration", () => { | |
| 77 | test("units and bare seconds, in milliseconds", () => { | |
| 78 | expect(parseDuration("250ms")).toBe(250); | |
| 79 | expect(parseDuration("90s")).toBe(90_000); | |
| 80 | expect(parseDuration("90")).toBe(90_000); | |
| 81 | expect(parseDuration("5m")).toBe(300_000); | |
| 82 | expect(parseDuration("2h")).toBe(7_200_000); | |
| 83 | expect(parseDuration("1.5s")).toBe(1500); | |
| 84 | }); | |
| 85 | ||
| 86 | test("anything else is undefined", () => { | |
| 87 | for (const bad of ["", "5d", "-1s", "s", undefined]) { | |
| 88 | expect(parseDuration(bad)).toBeUndefined(); | |
| 89 | } | |
| 90 | }); | |
| 91 | }); | |
| 92 | ||
| 93 | describe("parseDuration trimming", () => { | |
| 94 | test("surrounding whitespace is ignored", () => { | |
| 95 | expect(parseDuration(" 5m ")).toBe(300_000); | |
| 96 | }); | |
| 97 | ||
| 98 | test("a malformed decimal is rejected", () => { | |
| 99 | expect(parseDuration("1.5.2s")).toBeUndefined(); | |
| 100 | }); | |
| 101 | }); | |
| 102 | ||
| 103 | describe("parseRepoRef", () => { | |
| 104 | test("owner/name is a pair", () => { | |
| 105 | expect(parseRepoRef("trevato/forge")).toEqual({ owner: "trevato", name: "forge" }); | |
| 106 | }); | |
| 107 | ||
| 108 | test("one part, three parts, empty and undefined are rejected", () => { | |
| 109 | expect(parseRepoRef("forge")).toBeUndefined(); | |
| 110 | expect(parseRepoRef("a/b/c")).toBeUndefined(); | |
| 111 | expect(parseRepoRef("")).toBeUndefined(); | |
| 112 | expect(parseRepoRef(undefined)).toBeUndefined(); | |
| 113 | }); | |
| 114 | }); |
new above
offline