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
8 changes: 7 additions & 1 deletion src/opensquilla/cli/cron_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from rich.table import Table

from opensquilla.cli.gateway_rpc import confirm_or_exit, run_gateway_sync
from opensquilla.cli.output import print_json
from opensquilla.cli.output import exit_invalid_request, print_json
from opensquilla.cli.ui import ACCENT_HEADER, console

cron_app = typer.Typer(help="Inspect and manage scheduled OpenSquilla runs.")
Expand Down Expand Up @@ -858,6 +858,12 @@ def cron_runs(
json_output: bool = typer.Option(False, "--json", help="Emit machine-readable JSON"),
) -> None:
"""List recent runs for a cron job."""
if limit < 1:
exit_invalid_request(
"--limit must be >= 1",
json_output=json_output,
details={"parameter": "limit", "minimum": 1},
)

async def _run(client):
return await client.call("cron.runs", {"id": job_id, "limit": limit})
Expand Down
19 changes: 18 additions & 1 deletion src/opensquilla/cli/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from __future__ import annotations

import json
from typing import Any
from typing import Any, NoReturn

import typer

Expand Down Expand Up @@ -50,3 +50,20 @@ def emit_error(
)
else:
typer.secho(f"Error: {message}", fg=typer.colors.RED, err=True)


def exit_invalid_request(
message: str,
*,
json_output: bool = False,
details: Any | None = None,
) -> NoReturn:
"""Report invalid CLI input through the standard error envelope."""

emit_error(
message,
json_output=json_output,
code="INVALID_REQUEST",
details=details,
)
raise typer.Exit(2)
8 changes: 7 additions & 1 deletion src/opensquilla/cli/sessions_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from opensquilla.cli.chat.session_state import messages_to_markdown
from opensquilla.cli.gateway_client import session_history_all
from opensquilla.cli.gateway_rpc import default_gateway_url, run_gateway_sync
from opensquilla.cli.output import print_json
from opensquilla.cli.output import exit_invalid_request, print_json
from opensquilla.cli.ui import ACCENT, ACCENT_HEADER, console, error_panel

app = typer.Typer(help="Manage chat sessions.")
Expand Down Expand Up @@ -135,6 +135,12 @@ def sessions_list(
json_output: bool = typer.Option(False, "--json", help="Emit machine-readable JSON"),
) -> None:
"""List recent sessions."""
if limit < 1:
exit_invalid_request(
"--limit must be >= 1",
json_output=json_output,
details={"parameter": "limit", "minimum": 1},
)
since_dt = _parse_since(since)

async def _run(client):
Expand Down
8 changes: 8 additions & 0 deletions src/opensquilla/gateway/rpc_sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2781,6 +2781,14 @@ def empty_payload() -> dict[str, Any]:

is_guest = GuestRpcPolicy.is_guest(ctx)
owner_id = getattr(ctx.principal, "guest_owner_id", None) if is_guest else None
if not is_guest:
try:
numeric_limit = int(limit)
except (TypeError, ValueError):
pass
else:
if numeric_limit < 1:
raise ValueError("params.limit must be >= 1")
if count_only:
count_sessions = getattr(storage, "count_sessions", None)
if callable(count_sessions):
Expand Down
29 changes: 29 additions & 0 deletions tests/test_cli/test_cli_product_completeness.py
Original file line number Diff line number Diff line change
Expand Up @@ -946,6 +946,19 @@ def test_sessions_list_json_filters_client_side(monkeypatch):
assert payload["sessions"][0]["key"] == "a"


def test_sessions_list_rejects_negative_limit_before_gateway_call(monkeypatch):
fake = _install_fake_gateway(monkeypatch)

result = runner.invoke(app, ["sessions", "list", "--limit", "-1", "--json"])

assert result.exit_code == 2
assert result.stdout == ""
payload = json.loads(result.stderr)
assert payload["error"]["code"] == "INVALID_REQUEST"
assert "--limit must be >= 1" in payload["error"]["message"]
assert fake.calls == []


def test_sessions_list_uses_active_profile_managed_gateway_runtime_port(
tmp_path: Path,
monkeypatch,
Expand Down Expand Up @@ -1407,6 +1420,22 @@ def test_cron_run_yes_calls_existing_rpc(monkeypatch):
assert ("cron.run", {"id": "job-1"}) in fake.calls


def test_cron_runs_rejects_negative_limit_before_gateway_call(monkeypatch):
fake = _install_fake_gateway(monkeypatch)

result = runner.invoke(
app,
["cron", "runs", "job-1", "--limit", "-1", "--json"],
)

assert result.exit_code == 2
assert result.stdout == ""
payload = json.loads(result.stderr)
assert payload["error"]["code"] == "INVALID_REQUEST"
assert "--limit must be >= 1" in payload["error"]["message"]
assert fake.calls == []


def test_cron_commands_use_existing_rpc_payloads(monkeypatch):
fake = _install_fake_gateway(monkeypatch)
fake.rpc_payloads = {
Expand Down
20 changes: 20 additions & 0 deletions tests/test_gateway/test_sessions_list_contract_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,26 @@ async def legacy_caller(method: str, params: dict[str, Any] | None) -> Any:
assert isinstance(result["ts"], int)


@pytest.mark.asyncio
async def test_legacy_sessions_list_rejects_negative_limit_before_storage() -> None:
storage = _LegacyListStorage()
ctx = RpcContext(
conn_id="invalid-limit",
principal=Principal(
role="operator",
scopes=frozenset({"operator.admin"}),
is_owner=True,
authenticated=True,
),
session_manager=SimpleNamespace(storage=storage),
)

with pytest.raises(ValueError, match=r"params\.limit must be >= 1"):
await _handle_sessions_list({"limit": -1}, ctx)

assert storage.list_calls == []


@pytest.mark.parametrize(
("legacy_surface", "params", "expected_page_aliases"),
[
Expand Down
Loading