Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 32 additions & 6 deletions crates/mergify-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
// `<group> <subcommand>` pair per line.
if argv.first().is_some_and(|a| a == "--list-native-commands") {
for (group, sub) in NATIVE_COMMANDS {
Comment thread
jd marked this conversation as resolved.
println!("{group} {sub}");
}
return ExitCode::SUCCESS;
}

if let Some(cmd) = detect_native(&argv) {
return run_native(cmd);
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
})
}

Expand Down
56 changes: 46 additions & 10 deletions mergify_cli/tests/queue/test_skill.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

import pathlib
import re
import shutil
import subprocess

import yaml

Expand All @@ -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 `<group> <sub>` 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
Expand Down Expand Up @@ -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 <cmd>` 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)}"
)
Loading