mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-10 06:58:59 +02:00
- test/codex-model-probe.test.ts: the 1h TTL and the auth.json half of the mtime signature had no coverage — a regression in either would silently serve a stale MODEL_OK after re-login or forever. Added TTL-expiry (backdated cache line re-probes) and auth.json-mtime invalidation cases, mirroring the existing config.toml case. - make-pdf/test/cli-args.test.ts: structural assertion derived from the commands.ts registry — every --no-* flag must be in BOOLEAN_FLAGS, so a new negation flag can't silently re-open #2514 (swallowing the next positional). - TODOS.md: filed five review-batch deferrals under the v1.67 queue with rationale and effort: setup host-function dedup, cmd.exe %VAR% quoting in gbrainInvocation (cross-spawn direction), make-pdf flag registry metadata (derive BOOLEAN_FLAGS), legacy codex/factory/kiro uninstall provenance gating (parity with the cursor gate), and cursor auto-detect breadth (product call). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
95 lines
4.1 KiB
TypeScript
95 lines
4.1 KiB
TypeScript
/**
|
|
* #2514: boolean flags (--toc, --cover, ...) must never swallow the next
|
|
* positional argument. The parser treated ANY following non-flag token as a
|
|
* flag value, so `$P generate --toc essay.md` ate essay.md as --toc's value
|
|
* and the skill's own documented invocations failed with "missing input".
|
|
*/
|
|
|
|
import { describe, test, expect } from "bun:test";
|
|
import * as fs from "fs";
|
|
import * as path from "path";
|
|
import { parseArgs, BOOLEAN_FLAGS } from "../src/cli";
|
|
import { COMMANDS } from "../src/commands";
|
|
|
|
// parseArgs slices argv from index 2 (node/bun + script path).
|
|
const parse = (...args: string[]) => parseArgs(["bun", "cli.ts", ...args]);
|
|
|
|
describe("#2514 boolean flags do not swallow positionals", () => {
|
|
test("--toc before the input keeps the input positional", () => {
|
|
const r = parse("generate", "--toc", "essay.md");
|
|
expect(r.command).toBe("generate");
|
|
expect(r.flags.toc).toBe(true);
|
|
expect(r.positional).toEqual(["essay.md"]);
|
|
});
|
|
|
|
test("the skill's documented usage parses: --cover --toc essay.md essay.pdf", () => {
|
|
const r = parse("generate", "--cover", "--toc", "essay.md", "essay.pdf");
|
|
expect(r.flags.cover).toBe(true);
|
|
expect(r.flags.toc).toBe(true);
|
|
expect(r.positional).toEqual(["essay.md", "essay.pdf"]);
|
|
});
|
|
|
|
test("value flags still consume their value", () => {
|
|
const r = parse("generate", "--watermark", "DRAFT", "memo.md");
|
|
expect(r.flags.watermark).toBe("DRAFT");
|
|
expect(r.positional).toEqual(["memo.md"]);
|
|
});
|
|
|
|
test("--to consumes its format value", () => {
|
|
const r = parse("generate", "--to", "html", "doc.md");
|
|
expect(r.flags.to).toBe("html");
|
|
expect(r.positional).toEqual(["doc.md"]);
|
|
});
|
|
|
|
test("every boolean read in cli.ts is covered by the set (derived, not hardcoded)", () => {
|
|
// The original guard hardcoded six names, so --strict and --confidential
|
|
// shipped outside the set and still swallowed the next positional — the
|
|
// exact #2514 failure. Derive the list from the source's own boolean
|
|
// reads instead: `f.name === true` / `f["some-name"] === true` direct
|
|
// reads, plus booleanFlag("name", ...) pairs which read BOTH name and
|
|
// no-name. A new boolean read now fails this test until it joins the set.
|
|
const src = fs.readFileSync(path.join(import.meta.dir, "..", "src", "cli.ts"), "utf-8");
|
|
const derived = new Set<string>();
|
|
for (const m of src.matchAll(/\bf\.([a-zA-Z][\w-]*) === true/g)) derived.add(m[1]);
|
|
for (const m of src.matchAll(/\bf\["([\w-]+)"\] === true/g)) derived.add(m[1]);
|
|
for (const m of src.matchAll(/booleanFlag\("([\w-]+)"/g)) {
|
|
derived.add(m[1]);
|
|
derived.add(`no-${m[1]}`);
|
|
}
|
|
expect(derived.size).toBeGreaterThanOrEqual(6); // regex went blind if this drops
|
|
for (const f of derived) {
|
|
expect(BOOLEAN_FLAGS.has(f), `boolean read '--${f}' is missing from BOOLEAN_FLAGS`).toBe(true);
|
|
}
|
|
});
|
|
|
|
test("--strict does not swallow the input (the documented CI-mode flag, #2514)", () => {
|
|
const r = parse("generate", "--strict", "essay.md");
|
|
expect(r.flags.strict).toBe(true);
|
|
expect(r.positional).toEqual(["essay.md"]);
|
|
});
|
|
|
|
test("--no-confidential and --confidential both stay valueless", () => {
|
|
const a = parse("generate", "--no-confidential", "memo.md");
|
|
expect(a.flags["no-confidential"]).toBe(true);
|
|
expect(a.positional).toEqual(["memo.md"]);
|
|
const b = parse("generate", "--confidential", "memo.md");
|
|
expect(b.flags.confidential).toBe(true);
|
|
expect(b.positional).toEqual(["memo.md"]);
|
|
});
|
|
|
|
test("structural (T4): every --no-* flag in the commands.ts registry is boolean", () => {
|
|
// A --no-* flag is a negation — it never takes a value. A new one added
|
|
// to the registry but missed in BOOLEAN_FLAGS silently re-opens #2514
|
|
// (it would swallow the next positional). Derived from the registry, so
|
|
// this cannot rot as commands grow.
|
|
const missing: string[] = [];
|
|
for (const [cmd, spec] of COMMANDS) {
|
|
for (const flag of spec.flags ?? []) {
|
|
if (!/^--no-/.test(flag)) continue;
|
|
if (!BOOLEAN_FLAGS.has(flag.replace(/^--/, ""))) missing.push(`${cmd}: ${flag}`);
|
|
}
|
|
}
|
|
expect(missing).toEqual([]);
|
|
});
|
|
});
|