gstack/test/design-flag-utils.test.ts

114 lines
4.7 KiB
TypeScript

/**
* design/src/flag-utils.ts — integer-flag contract (#2032, eng-review 7A/8A).
*
* Lives under test/ (NOT design/test/) deliberately: design/test/ is invisible
* to the bun test glob, scripts/test-free-shards TEST_ROOTS, and every CI
* workflow (eng-review 11A), so a tripwire there guards nothing. flag-utils is
* a pure module, so importing it from here is clean.
*
* The bug class: the design CLI parser yields string | true | undefined;
* parseInt produced NaN that flowed silently into loop bounds and setTimeout —
* `variants --count abc` generated ZERO variants and exited 0, `generate
* --retry abc` was a silent no-op, `serve --timeout abc` died at boot.
*/
import { describe, test, expect } from "bun:test";
import { spawnSync } from "child_process";
import * as path from "path";
import { parseIntFlag } from "../design/src/flag-utils";
const ROOT = path.resolve(import.meta.dir, "..");
const COUNT_SPEC = { name: "count", def: 3, min: 1, max: 7 } as const;
const RETRY_SPEC = { name: "retry", def: 0, min: 0 } as const;
const TIMEOUT_SPEC = { name: "timeout", def: 600, min: 1 } as const;
describe("parseIntFlag contract (#2032, codex 17a-c)", () => {
test("undefined → default (flag absent)", () => {
expect(parseIntFlag(undefined, COUNT_SPEC)).toEqual({ ok: true, value: 3 });
expect(parseIntFlag(undefined, RETRY_SPEC)).toEqual({ ok: true, value: 0 });
});
test("non-numeric string → error, never a silent default ('--count abc')", () => {
const r = parseIntFlag("abc", COUNT_SPEC);
expect(r.ok).toBe(false);
if (!r.ok) expect(r.error).toContain('"abc" is not an integer');
});
test("bare flag (parser yields true) → error 'requires a value'", () => {
const r = parseIntFlag(true, COUNT_SPEC);
expect(r.ok).toBe(false);
if (!r.ok) expect(r.error).toContain("requires a value");
});
test("empty string → error 'requires a value'", () => {
const r = parseIntFlag("", COUNT_SPEC);
expect(r.ok).toBe(false);
if (!r.ok) expect(r.error).toContain("requires a value");
});
test("non-integer '3.7' → error (rejected, not silently truncated to 3)", () => {
const r = parseIntFlag("3.7", COUNT_SPEC);
expect(r.ok).toBe(false);
if (!r.ok) expect(r.error).toContain("not an integer");
});
test("below min → error ('--count 0' billed the user for 1 they asked 0 of; now loud)", () => {
expect(parseIntFlag("0", COUNT_SPEC).ok).toBe(false);
expect(parseIntFlag("-2", COUNT_SPEC).ok).toBe(false);
// retry allows 0 (min: 0)
expect(parseIntFlag("0", RETRY_SPEC)).toEqual({ ok: true, value: 0 });
});
test("above max → clamp WITH warning (capability limit, not a user mistake)", () => {
const r = parseIntFlag("99", COUNT_SPEC);
expect(r.ok).toBe(true);
if (r.ok) {
expect(r.value).toBe(7);
expect(r.warning).toContain("exceeds the maximum");
}
});
test("in-range integers pass through untouched", () => {
expect(parseIntFlag("3", COUNT_SPEC)).toEqual({ ok: true, value: 3 });
expect(parseIntFlag(5, COUNT_SPEC)).toEqual({ ok: true, value: 5 });
expect(parseIntFlag("120", TIMEOUT_SPEC)).toEqual({ ok: true, value: 120 });
});
test("retry-NaN and timeout-NaN are errors, not silent no-ops (#2032 siblings)", () => {
// Pre-fix: --retry abc → generate() loop never ran (attempt <= NaN),
// printed null, exited 0. --timeout abc → setTimeout(NaN) ≈ immediate
// SERVE_TIMEOUT. Both members of the same NaN class, same file.
expect(parseIntFlag("abc", RETRY_SPEC).ok).toBe(false);
expect(parseIntFlag("abc", TIMEOUT_SPEC).ok).toBe(false);
});
test("NaN number input (legacy pre-parsed callers) → error", () => {
expect(parseIntFlag(Number.NaN, COUNT_SPEC).ok).toBe(false);
});
});
describe("normalizeIntFlag CLI wrapper (exit-1 semantics)", () => {
function runWrapper(rawExpr: string, specExpr: string): { status: number; stderr: string } {
const script = `
import { normalizeIntFlag } from "${ROOT}/design/src/flag-utils";
const v = normalizeIntFlag(${rawExpr}, ${specExpr});
console.log("VALUE:" + v);
`;
const res = spawnSync("bun", ["-e", script], { encoding: "utf-8", cwd: ROOT });
return { status: res.status ?? -1, stderr: res.stderr ?? "" };
}
test("invalid input exits 1 with the error on stderr", () => {
const r = runWrapper('"abc"', '{ name: "count", def: 3, min: 1, max: 7 }');
expect(r.status).toBe(1);
expect(r.stderr).toContain("not an integer");
});
test("clamp warns on stderr but exits 0", () => {
const r = runWrapper('"99"', '{ name: "count", def: 3, min: 1, max: 7 }');
expect(r.status).toBe(0);
expect(r.stderr).toContain("exceeds the maximum");
});
});