Skip to content
Open
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
28 changes: 28 additions & 0 deletions amplifier_app_cli/commands/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ def register_run_command(
@cli.command()
@click.argument("prompt", required=False)
@click.option("--bundle", "-B", help="Bundle to use for this session")
@click.option(
"--matrix",
default=None,
help="Routing matrix to use for this session (e.g. anthropic, balanced)",
)
@click.option("--provider", "-p", default=None, help="LLM provider to use")
@click.option("--model", "-m", help="Model to use (provider-specific)")
@click.option("--max-tokens", type=int, help="Maximum output tokens")
Expand All @@ -65,6 +70,7 @@ def register_run_command(
def run(
prompt: str | None,
bundle: str | None,
matrix: str | None,
provider: str,
model: str | None,
max_tokens: int | None,
Expand Down Expand Up @@ -196,6 +202,28 @@ def run(

search_paths = get_module_search_paths()

# Per-session routing matrix override (--matrix).
# Bridge the flag into the mounted hooks-routing config's default_matrix,
# mirroring how --bundle selects a per-session bundle. This intentionally
# does not persist anything to settings; it only affects this session.
if matrix and prepared_bundle and hasattr(prepared_bundle, "mount_plan"):
routing_entry = None
for hook in prepared_bundle.mount_plan.get("hooks") or []:
if isinstance(hook, dict) and hook.get("module") == "hooks-routing":
routing_entry = hook
break
if routing_entry is None:
console.print(
"[yellow]Warning:[/yellow] --matrix ignored: this bundle does "
"not mount the 'hooks-routing' module"
)
else:
routing_config = routing_entry.get("config")
if not isinstance(routing_config, dict):
routing_config = {}
routing_entry["config"] = routing_config
routing_config["default_matrix"] = matrix

# Handle provider/model CLI overrides
if model and not provider:
# Require --provider when using --model for clarity
Expand Down
152 changes: 152 additions & 0 deletions tests/test_run_matrix_flag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
"""Tests for the per-session ``--matrix`` flag on the ``run`` command.

The flag mirrors ``--bundle`` for per-session selection, but instead of
choosing a bundle it injects ``default_matrix`` into the mounted
``hooks-routing`` config within the prepared bundle's mount plan. It must
never persist anything to settings (scope is strictly this session).

These tests build a minimal Click group by registering the run command with
mocked collaborators (so no chat/session is actually launched), then invoke
it and inspect the mutated mount plan.
"""

from __future__ import annotations

from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch

import click
from click.testing import CliRunner

from amplifier_app_cli.commands.run import register_run_command

_MODULE = "amplifier_app_cli.commands.run"


def _make_prepared_bundle(with_routing: bool = True) -> SimpleNamespace:
"""A stand-in prepared bundle exposing a mutable ``mount_plan``."""
hooks: list[dict] = []
if with_routing:
hooks.append({"module": "hooks-routing", "config": {}})
# Include an unrelated hook to prove we target hooks-routing specifically.
hooks.append({"module": "hooks-mode", "config": {}})
return SimpleNamespace(mount_plan={"hooks": hooks})


def _build_cli(prepared_bundle: SimpleNamespace) -> tuple[click.Group, AsyncMock]:
"""Register the run command with mocked collaborators.

Returns the CLI group and the execute_single AsyncMock so tests can assert
the session-launch path was reached (i.e. the command didn't bail early).
"""
execute_single = AsyncMock()
cli = click.Group()
register_run_command(
cli,
interactive_chat=AsyncMock(),
execute_single=execute_single,
get_module_search_paths=lambda: [],
check_first_run=lambda: False,
prompt_first_run_init=lambda console: True,
)
return cli, execute_single


def _invoke(cli: click.Group, prepared_bundle: SimpleNamespace, *args: str):
"""Invoke ``run`` with resolve_config and startup checks stubbed out."""
with (
patch(f"{_MODULE}.resolve_config", return_value=({}, prepared_bundle)),
patch(f"{_MODULE}.create_config_manager", return_value=MagicMock()),
patch(f"{_MODULE}.AppSettings", return_value=MagicMock()),
patch(
"amplifier_app_cli.utils.startup_checker.check_and_notify",
new=AsyncMock(),
),
):
runner = CliRunner()
return runner.invoke(cli, ["run", *args])


def test_matrix_flag_sets_default_matrix_in_mount_plan():
"""``run --matrix foo`` must set default_matrix on the hooks-routing entry."""
prepared_bundle = _make_prepared_bundle(with_routing=True)
cli, execute_single = _build_cli(prepared_bundle)

result = _invoke(
cli,
prepared_bundle,
"hello",
"--bundle",
"anchors",
"--matrix",
"foo",
"--mode",
"single",
"--output-format",
"json",
)

assert result.exit_code == 0, result.output
execute_single.assert_awaited_once()

routing = next(
h for h in prepared_bundle.mount_plan["hooks"] if h["module"] == "hooks-routing"
)
assert routing["config"]["default_matrix"] == "foo"
# Unrelated hooks are left untouched.
mode_hook = next(
h for h in prepared_bundle.mount_plan["hooks"] if h["module"] == "hooks-mode"
)
assert "default_matrix" not in mode_hook["config"]


def test_no_matrix_flag_leaves_routing_untouched():
"""Omitting ``--matrix`` must not inject default_matrix anywhere."""
prepared_bundle = _make_prepared_bundle(with_routing=True)
cli, execute_single = _build_cli(prepared_bundle)

result = _invoke(
cli,
prepared_bundle,
"hello",
"--bundle",
"anchors",
"--mode",
"single",
"--output-format",
"json",
)

assert result.exit_code == 0, result.output
execute_single.assert_awaited_once()

routing = next(
h for h in prepared_bundle.mount_plan["hooks"] if h["module"] == "hooks-routing"
)
assert "default_matrix" not in routing["config"]


def test_matrix_flag_warns_when_routing_hook_missing():
"""When hooks-routing isn't mounted, warn and continue (no crash)."""
prepared_bundle = _make_prepared_bundle(with_routing=False)
cli, execute_single = _build_cli(prepared_bundle)

result = _invoke(
cli,
prepared_bundle,
"hello",
"--bundle",
"anchors",
"--matrix",
"foo",
"--mode",
"single",
"--output-format",
"json",
)

assert result.exit_code == 0, result.output
execute_single.assert_awaited_once()
# No hooks-routing entry means nothing was mutated to carry default_matrix.
for hook in prepared_bundle.mount_plan["hooks"]:
assert "default_matrix" not in hook.get("config", {})