diff --git a/crates/mergify-cli/src/main.rs b/crates/mergify-cli/src/main.rs index 53f47f99..12dca9ec 100644 --- a/crates/mergify-cli/src/main.rs +++ b/crates/mergify-cli/src/main.rs @@ -49,6 +49,18 @@ fn main() -> ExitCode { println!("✅"); } + // Hidden flag — used by `mergify_cli/tests/queue/test_skill.py` + // (and any future cross-language test) to learn the set of + // commands the Rust binary handles natively without resorting + // to a hardcoded list that drifts. Format is one + // ` ` pair per line. + if argv.first().is_some_and(|a| a == "--list-native-commands") { + for (group, sub) in NATIVE_COMMANDS { + println!("{group} {sub}"); + } + return ExitCode::SUCCESS; + } + if let Some(cmd) = detect_native(&argv) { return run_native(cmd); } @@ -62,6 +74,23 @@ fn main() -> ExitCode { } } +/// Single source of truth for the `(group, subcommand)` pairs the +/// Rust binary handles natively. Used by [`looks_native`] for argv +/// recognition and by the `--list-native-commands` hidden flag so +/// out-of-process tests can discover the list without hard-coding +/// it. Add new entries here when porting a command; the matching +/// `clap` `Subcommands` variant is what actually wires it up. +const NATIVE_COMMANDS: &[(&str, &str)] = &[ + ("config", "validate"), + ("config", "simulate"), + ("ci", "scopes-send"), + ("ci", "git-refs"), + ("ci", "queue-info"), + ("queue", "pause"), + ("queue", "unpause"), + ("queue", "status"), +]; + /// Native commands the Rust binary handles without delegating to /// the Python shim. enum NativeCommand { @@ -126,12 +155,9 @@ struct QueueStatusOpts { /// classify the invocation as native. fn looks_native(argv: &[String]) -> bool { argv.windows(2).any(|pair| { - matches!( - (pair[0].as_str(), pair[1].as_str()), - ("config", "validate" | "simulate") - | ("ci", "scopes-send" | "git-refs" | "queue-info") - | ("queue", "pause" | "unpause" | "status"), - ) + NATIVE_COMMANDS + .iter() + .any(|(g, s)| pair[0] == *g && pair[1] == *s) }) } diff --git a/mergify_cli/tests/queue/test_skill.py b/mergify_cli/tests/queue/test_skill.py index e701b83b..bbc98c07 100644 --- a/mergify_cli/tests/queue/test_skill.py +++ b/mergify_cli/tests/queue/test_skill.py @@ -16,6 +16,8 @@ import pathlib import re +import shutil +import subprocess import yaml @@ -29,6 +31,42 @@ def _get_skill_content() -> str: return SKILL_PATH.read_text(encoding="utf-8") +def _native_commands_for_group(group: str) -> set[str]: + """Ask the installed `mergify` binary which ` ` pairs + it handles natively, then return the subcommands for `group`. + + Spawning the binary keeps this test honest: the source of truth + is the binary itself, so a port that adds a native subcommand + (and its `NATIVE_COMMANDS` entry) automatically becomes visible + here. No parallel hard-coded list to drift. + """ + # Fail (don't skip) when `mergify` isn't on PATH: the wheel must + # be installed for the test to be meaningful, and silently + # skipping would let a regression in the wheel's bin scripts + # slip through unnoticed. `uv run pytest` installs the wheel + # before invoking pytest, so on every supported entry point the + # binary is present. + binary = shutil.which("mergify") + assert binary is not None, ( + "`mergify` binary not on PATH. Install the wheel first " + "(`uv run pytest` does this automatically) — running this " + "test against an uninstalled checkout would give a false pass." + ) + out = subprocess.run( + [binary, "--list-native-commands"], + check=True, + capture_output=True, + text=True, + ).stdout + pairs = (line.split(maxsplit=1) for line in out.splitlines() if line.strip()) + return { + sub + for pair in pairs + if len(pair) == 2 and pair[0] == group + for sub in [pair[1]] + } + + def test_skill_content_is_readable() -> None: content = _get_skill_content() assert len(content) > 0 @@ -62,25 +100,23 @@ def test_skill_has_required_sections() -> None: assert section in content, f"Skill is missing required section: {section}" -# Rust-native queue commands. Each port PR appends to this list when -# it deletes the Python copy, so the validation below stays accurate -# without needing to spawn the Rust binary at test time. -NATIVE_QUEUE_COMMANDS: frozenset[str] = frozenset({"pause", "unpause", "status"}) - - def test_skill_references_valid_commands() -> None: """Every `mergify queue ` reference in the skill must resolve - to either a registered click command (still-shimmed) or a known - Rust-native command. Catches typos and skill drift after a port.""" + to either a registered click command (still-shimmed) or a + Rust-native command reported by the binary. Catches typos and + skill drift after a port — and stays accurate without a parallel + hard-coded list because the native set is queried from + `mergify --list-native-commands` itself. + """ from mergify_cli.queue.cli import queue content = _get_skill_content() referenced = set(re.findall(r"mergify queue ([\w-]+)", content)) - available = set(queue.commands.keys()) | NATIVE_QUEUE_COMMANDS + available = set(queue.commands.keys()) | _native_commands_for_group("queue") for cmd in referenced: assert cmd in available, ( f"Skill references 'mergify queue {cmd}' but it's neither a " - f"registered click command nor a known Rust-native command. " + f"registered click command nor a Rust-native command. " f"Available: {sorted(available)}" )