mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-05-13 07:10:46 +00:00
fix: extract flags module to fix KNOWN_FLAGS drift in tests (#1757)
KNOWN_FLAGS in unknown-flags.test.ts was copy-pasted from index.ts and was missing the --name flag, causing silent test gaps. Extract KNOWN_FLAGS, findUnknownFlag, and expandEqualsFlags into a new flags.ts module so tests import the real source of truth. - Create cli/src/flags.ts with KNOWN_FLAGS, findUnknownFlag, expandEqualsFlags - Update index.ts to import from flags.ts (checkUnknownFlags now uses findUnknownFlag) - Update unknown-flags.test.ts to import from flags.ts instead of copy-pasting - Add tests for --name flag, KNOWN_FLAGS completeness, and expandEqualsFlags - Bump CLI version to 0.6.15 Fixes #1744 Agent: test-engineer Co-authored-by: B <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
7e7d4aa3d7
commit
545ddafe4a
4 changed files with 138 additions and 107 deletions
|
|
@ -1,48 +1,13 @@
|
|||
import { describe, it, expect } from "bun:test";
|
||||
import { KNOWN_FLAGS, findUnknownFlag, expandEqualsFlags } from "../flags";
|
||||
|
||||
/**
|
||||
* Tests for unknown flag detection in CLI argument parsing.
|
||||
* Tests for unknown flag detection and flag expansion in CLI argument parsing.
|
||||
*
|
||||
* Since index.ts runs main() on import, we replicate the checkUnknownFlags
|
||||
* logic as a pure function to test it without side effects.
|
||||
* Imports KNOWN_FLAGS and findUnknownFlag directly from flags.ts to avoid
|
||||
* copy-paste drift (see issue #1744).
|
||||
*/
|
||||
|
||||
const KNOWN_FLAGS = new Set([
|
||||
"--help",
|
||||
"-h",
|
||||
"--version",
|
||||
"-v",
|
||||
"-V",
|
||||
"--prompt",
|
||||
"-p",
|
||||
"--prompt-file",
|
||||
"-f",
|
||||
"--dry-run",
|
||||
"-n",
|
||||
"--debug",
|
||||
"--headless",
|
||||
"--output",
|
||||
"--default",
|
||||
"-a",
|
||||
"-c",
|
||||
"--agent",
|
||||
"--cloud",
|
||||
"--clear",
|
||||
]);
|
||||
|
||||
/** Replicated from index.ts for testability - returns the first unknown flag or null */
|
||||
function findUnknownFlag(args: string[]): string | null {
|
||||
for (const arg of args) {
|
||||
if (
|
||||
(arg.startsWith("--") || (arg.startsWith("-") && arg.length > 1 && !/^-\d/.test(arg))) &&
|
||||
!KNOWN_FLAGS.has(arg)
|
||||
) {
|
||||
return arg;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
describe("Unknown Flag Detection", () => {
|
||||
describe("detects unknown flags", () => {
|
||||
it("should detect --json as unknown", () => {
|
||||
|
|
@ -246,6 +211,10 @@ describe("Unknown Flag Detection", () => {
|
|||
]),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("should allow --name", () => {
|
||||
expect(findUnknownFlag(["claude", "sprite", "--name", "my-box"])).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ignores positional arguments", () => {
|
||||
|
|
@ -333,3 +302,64 @@ describe("Unknown Flag Detection", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("KNOWN_FLAGS completeness", () => {
|
||||
it("should contain --name flag", () => {
|
||||
expect(KNOWN_FLAGS.has("--name")).toBe(true);
|
||||
});
|
||||
|
||||
it("should contain all expected flags", () => {
|
||||
const expected = [
|
||||
"--help", "-h",
|
||||
"--version", "-v", "-V",
|
||||
"--prompt", "-p", "--prompt-file", "-f",
|
||||
"--dry-run", "-n",
|
||||
"--debug",
|
||||
"--headless",
|
||||
"--output",
|
||||
"--name",
|
||||
"--default",
|
||||
"-a", "-c", "--agent", "--cloud",
|
||||
"--clear",
|
||||
];
|
||||
for (const flag of expected) {
|
||||
expect(KNOWN_FLAGS.has(flag)).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("expandEqualsFlags", () => {
|
||||
it("should expand --flag=value into two args", () => {
|
||||
expect(expandEqualsFlags(["--prompt=hello"])).toEqual(["--prompt", "hello"]);
|
||||
});
|
||||
|
||||
it("should expand multiple --flag=value pairs", () => {
|
||||
expect(expandEqualsFlags(["--prompt=hello", "--name=box"])).toEqual([
|
||||
"--prompt", "hello", "--name", "box",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should pass through args without equals", () => {
|
||||
expect(expandEqualsFlags(["--help", "claude", "sprite"])).toEqual([
|
||||
"--help", "claude", "sprite",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should not expand short flags", () => {
|
||||
expect(expandEqualsFlags(["-p=value"])).toEqual(["-p=value"]);
|
||||
});
|
||||
|
||||
it("should handle empty args", () => {
|
||||
expect(expandEqualsFlags([])).toEqual([]);
|
||||
});
|
||||
|
||||
it("should handle value containing equals sign", () => {
|
||||
expect(expandEqualsFlags(["--prompt=a=b"])).toEqual(["--prompt", "a=b"]);
|
||||
});
|
||||
|
||||
it("should handle mixed args", () => {
|
||||
expect(expandEqualsFlags(["claude", "--prompt=hello", "sprite", "--dry-run"])).toEqual([
|
||||
"claude", "--prompt", "hello", "sprite", "--dry-run",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
42
cli/src/flags.ts
Normal file
42
cli/src/flags.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
/** CLI flag definitions and utilities — single source of truth for flag validation */
|
||||
|
||||
export const KNOWN_FLAGS = new Set([
|
||||
"--help", "-h",
|
||||
"--version", "-v", "-V",
|
||||
"--prompt", "-p", "--prompt-file", "-f",
|
||||
"--dry-run", "-n",
|
||||
"--debug",
|
||||
"--headless",
|
||||
"--output",
|
||||
"--name",
|
||||
"--default",
|
||||
"-a", "-c", "--agent", "--cloud",
|
||||
"--clear",
|
||||
]);
|
||||
|
||||
/** Return the first unknown flag in args, or null if all are known/positional */
|
||||
export function findUnknownFlag(args: string[]): string | null {
|
||||
for (const arg of args) {
|
||||
if (
|
||||
(arg.startsWith("--") || (arg.startsWith("-") && arg.length > 1 && !/^-\d/.test(arg))) &&
|
||||
!KNOWN_FLAGS.has(arg)
|
||||
) {
|
||||
return arg;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Expand --flag=value into --flag value so all flag parsing works uniformly */
|
||||
export function expandEqualsFlags(args: string[]): string[] {
|
||||
const result: string[] = [];
|
||||
for (const arg of args) {
|
||||
if (arg.startsWith("--") && arg.includes("=")) {
|
||||
const eqIdx = arg.indexOf("=");
|
||||
result.push(arg.slice(0, eqIdx), arg.slice(eqIdx + 1));
|
||||
} else {
|
||||
result.push(arg);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ import pc from "picocolors";
|
|||
import pkg from "../package.json" with { type: "json" };
|
||||
import { checkForUpdates } from "./update-check.js";
|
||||
import { loadManifest, agentKeys, cloudKeys, getCacheAge } from "./manifest.js";
|
||||
import { KNOWN_FLAGS, findUnknownFlag, expandEqualsFlags } from "./flags.js";
|
||||
|
||||
const VERSION = pkg.version;
|
||||
|
||||
|
|
@ -78,75 +79,33 @@ const HELP_FLAGS = [
|
|||
"help",
|
||||
];
|
||||
|
||||
const KNOWN_FLAGS = new Set([
|
||||
"--help",
|
||||
"-h",
|
||||
"--version",
|
||||
"-v",
|
||||
"-V",
|
||||
"--prompt",
|
||||
"-p",
|
||||
"--prompt-file",
|
||||
"-f",
|
||||
"--dry-run",
|
||||
"-n",
|
||||
"--debug",
|
||||
"--headless",
|
||||
"--output",
|
||||
"--name",
|
||||
"--default",
|
||||
"-a",
|
||||
"-c",
|
||||
"--agent",
|
||||
"--cloud",
|
||||
"--clear",
|
||||
]);
|
||||
|
||||
/** Expand --flag=value into --flag value so all flag parsing works uniformly */
|
||||
export function expandEqualsFlags(args: string[]): string[] {
|
||||
const result: string[] = [];
|
||||
for (const arg of args) {
|
||||
if (arg.startsWith("--") && arg.includes("=")) {
|
||||
const eqIdx = arg.indexOf("=");
|
||||
result.push(arg.slice(0, eqIdx), arg.slice(eqIdx + 1));
|
||||
} else {
|
||||
result.push(arg);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Check for unknown flags and show an actionable error */
|
||||
function checkUnknownFlags(args: string[]): void {
|
||||
for (const arg of args) {
|
||||
if (
|
||||
(arg.startsWith("--") || (arg.startsWith("-") && arg.length > 1 && !/^-\d/.test(arg))) &&
|
||||
!KNOWN_FLAGS.has(arg)
|
||||
) {
|
||||
console.error(pc.red(`Unknown flag: ${pc.bold(arg)}`));
|
||||
console.error();
|
||||
console.error(" Supported flags:");
|
||||
console.error(` ${pc.cyan("--prompt, -p")} Provide a prompt for non-interactive execution`);
|
||||
console.error(` ${pc.cyan("--prompt-file, -f")} Read prompt from a file`);
|
||||
console.error(` ${pc.cyan("--dry-run, -n")} Preview what would be provisioned`);
|
||||
console.error(` ${pc.cyan("--debug")} Show all commands being executed`);
|
||||
console.error(` ${pc.cyan("--headless")} Non-interactive mode (no prompts, no SSH session)`);
|
||||
console.error(` ${pc.cyan("--output json")} Output structured JSON to stdout`);
|
||||
console.error(` ${pc.cyan("--name")} Set the spawn/resource name`);
|
||||
console.error(` ${pc.cyan("--help, -h")} Show help information`);
|
||||
console.error(` ${pc.cyan("--version, -v")} Show version`);
|
||||
console.error();
|
||||
console.error(` For ${pc.cyan("spawn pick")}:`);
|
||||
console.error(` ${pc.cyan("--default")} Pre-selected value in the picker`);
|
||||
console.error();
|
||||
console.error(` For ${pc.cyan("spawn list")}:`);
|
||||
console.error(` ${pc.cyan("-a, --agent")} Filter history by agent`);
|
||||
console.error(` ${pc.cyan("-c, --cloud")} Filter history by cloud`);
|
||||
console.error(` ${pc.cyan("--clear")} Clear all spawn history`);
|
||||
console.error();
|
||||
console.error(` Run ${pc.cyan("spawn help")} for full usage information.`);
|
||||
process.exit(1);
|
||||
}
|
||||
const unknown = findUnknownFlag(args);
|
||||
if (unknown) {
|
||||
console.error(pc.red(`Unknown flag: ${pc.bold(unknown)}`));
|
||||
console.error();
|
||||
console.error(` Supported flags:`);
|
||||
console.error(` ${pc.cyan("--prompt, -p")} Provide a prompt for non-interactive execution`);
|
||||
console.error(` ${pc.cyan("--prompt-file, -f")} Read prompt from a file`);
|
||||
console.error(` ${pc.cyan("--dry-run, -n")} Preview what would be provisioned`);
|
||||
console.error(` ${pc.cyan("--debug")} Show all commands being executed`);
|
||||
console.error(` ${pc.cyan("--headless")} Non-interactive mode (no prompts, no SSH session)`);
|
||||
console.error(` ${pc.cyan("--output json")} Output structured JSON to stdout`);
|
||||
console.error(` ${pc.cyan("--name")} Set the spawn/resource name`);
|
||||
console.error(` ${pc.cyan("--help, -h")} Show help information`);
|
||||
console.error(` ${pc.cyan("--version, -v")} Show version`);
|
||||
console.error();
|
||||
console.error(` For ${pc.cyan("spawn pick")}:`);
|
||||
console.error(` ${pc.cyan("--default")} Pre-selected value in the picker`);
|
||||
console.error();
|
||||
console.error(` For ${pc.cyan("spawn list")}:`);
|
||||
console.error(` ${pc.cyan("-a, --agent")} Filter history by agent`);
|
||||
console.error(` ${pc.cyan("-c, --cloud")} Filter history by cloud`);
|
||||
console.error(` ${pc.cyan("--clear")} Clear all spawn history`);
|
||||
console.error();
|
||||
console.error(` Run ${pc.cyan("spawn help")} for full usage information.`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue